chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:46:20 +08:00
commit eb21b65815
830 changed files with 113645 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
# files
Makefile
Gulpfile.js
Gruntfile.js
.tern-project
.gitattributes
.editorconfig
.eslintrc
.jshintrc
.flowconfig
.documentup.json
.yarn-metadata.json
.travis.yml
appveyor.yml
LICENSE.txt
LICENSE
AUTHORS
CONTRIBUTORS
.yarn-integrity
*.md
*.ts
*.js.map
*.ts.map
*.jst
*.coffee
*.d.cts
*.d.mts
tsconfig.json
.nycrc
.nycrc.json
opslevel.yml
package-support.json
bench.js
tests.js
# folders
__tests__
test
tests
powered-test
docs
doc
website
images
assets
example
examples
coverage
.nyc_output
dist/esm
zmodem2/dist/cjs
zmodem2/dist/browser
zmodem2/dist/esm
trzsz2/dist/cjs
trzsz2/dist/esm
package-lock.json
.github
.circleci
scripts
samplejson
flash
third_party
tools
bench
benchmarks
spec
specs
fixture
fixtures
umd
+8
View File
@@ -0,0 +1,8 @@
const { exec } = require('shelljs')
const os = require('os')
const platform = os.platform()
console.log('platform:', platform)
const cmd = platform.startsWith('win')
? 'node_modules\\.bin\\cross-env NODE_ENV=development node_modules\\.bin\\electron -r dotenv/config src\\app\\app'
: 'node_modules/.bin/cross-env NODE_ENV=development node_modules/.bin/electron -r dotenv/config src/app/app'
exec(cmd)
+100
View File
@@ -0,0 +1,100 @@
/**
* Install .desktop file and icon for AppImage on Linux
* so that taskbars (e.g. UOS/Deepin dde-dock) can find the icon.
*
* This is standard practice for AppImage — the format uses a volatile
* FUSE mount path, so the embedded .desktop file and icon are never
* found by desktop environments that look in XDG standard locations.
*/
const fs = require('fs')
const path = require('path')
const os = require('os')
const log = require('../common/log')
function installDesktopFile () {
if (process.platform !== 'linux' || !process.env.APPIMAGE) {
return
}
const appImagePath = process.env.APPIMAGE
const home = os.homedir()
const desktopId = 'electerm'
const appsDir = path.join(home, '.local', 'share', 'applications')
const iconsDir = path.join(
home, '.local', 'share', 'icons',
'hicolor', '128x128', 'apps'
)
const desktopFilePath = path.join(appsDir, `${desktopId}.desktop`)
const iconFilePath = path.join(iconsDir, `${desktopId}.png`)
// Icon source inside the AppImage (asar-aware path — Electron's fs
// module transparently reads from inside app.asar)
const srcIconPath = path.join(
process.resourcesPath,
'app.asar', 'assets', 'images', 'electerm-round-128x128.png'
)
const desktopContent = [
'[Desktop Entry]',
'Name=electerm',
'Comment=Terminal/SSH/SFTP client',
`Exec=${appImagePath} %U`,
`Icon=${iconFilePath}`,
'Terminal=false',
'Type=Application',
'Categories=Development;System;TerminalEmulator;',
'StartupWMClass=electerm',
'MimeType=x-scheme-handler/ssh;x-scheme-handler/telnet;' +
'x-scheme-handler/rdp;x-scheme-handler/vnc;' +
'x-scheme-handler/serial;x-scheme-handler/spice;' +
'x-scheme-handler/electerm;',
''
].join('\n')
try {
// Skip if a system-level .desktop file exists (deb/rpm installs one
// to /usr/share/applications/) — don't override it.
// Also clean up any leftover user-level file from a previous AppImage run.
const sysDesktop = '/usr/share/applications/electerm.desktop'
if (fs.existsSync(sysDesktop)) {
try {
fs.unlinkSync(desktopFilePath)
fs.unlinkSync(iconFilePath)
} catch (_) {}
return
}
// Skip if .desktop file already has the correct content
let existing = ''
try {
existing = fs.readFileSync(desktopFilePath, 'utf8')
} catch (_) {}
if (existing === desktopContent) {
return
}
fs.mkdirSync(appsDir, { recursive: true })
fs.mkdirSync(iconsDir, { recursive: true })
// Copy icon once (doesn't change between versions)
if (!fs.existsSync(iconFilePath) && fs.existsSync(srcIconPath)) {
fs.copyFileSync(srcIconPath, iconFilePath)
}
fs.writeFileSync(desktopFilePath, desktopContent)
// Refresh desktop database so the DE picks up the new entry
try {
require('child_process').execSync(
'update-desktop-database ~/.local/share/applications 2>/dev/null'
)
} catch (_) {}
log.info(`[appimage] Installed .desktop file to ${desktopFilePath}`)
} catch (e) {
log.error('[appimage] Failed to install desktop file:', e.message)
}
}
module.exports = { installDesktopFile }
+342
View File
@@ -0,0 +1,342 @@
/**
* common functions for build
*/
const { exec } = require('child_process')
const { resolve } = require('path')
const { writeFileSync, readFileSync } = require('fs')
const replace = require('replace-in-file')
const { rm, mv } = require('shelljs')
exports.run = function (cmd) {
return new Promise((resolve, reject) => {
console.log('Executing command:', cmd)
const childProcess = exec(cmd, {
env: {
...process.env,
DEBUG: 'electron-builder:*',
ELECTRON_BUILDER_CACHE: process.env.ELECTRON_BUILDER_CACHE || ''
},
maxBuffer: 1024 * 1024 * 50 // 50MB buffer for large debug output
}, (err, stdout, stderr) => {
// Always log stdout and stderr regardless of success/failure
if (stdout) {
console.log('=== STDOUT ===')
console.log(stdout)
}
if (stderr) {
console.log('=== STDERR ===')
console.log(stderr)
}
if (err) {
console.error('=== COMMAND FAILED ===')
console.error('Command:', cmd)
console.error('Exit code:', err.code)
console.error('Signal:', err.signal)
console.error('Error message:', err.message)
// Create a more detailed error message
const detailedError = new Error(`Command failed with exit code ${err.code}: ${cmd}`)
detailedError.originalError = err
detailedError.stdout = stdout
detailedError.stderr = stderr
detailedError.command = cmd
return reject(detailedError)
}
resolve(stdout)
})
// Also pipe output in real-time for long-running commands
childProcess.stdout.on('data', (data) => {
process.stdout.write(data)
})
childProcess.stderr.on('data', (data) => {
process.stderr.write(data)
})
})
}
exports.writeSrc = function (src) {
const p = resolve(__dirname, '../../work/app/lib/install-src.js')
writeFileSync(p, `module.exports = '${src}'`)
if (src.includes('AppImage')) {
exports.patchAppImage()
}
}
/**
* Patch work/app to include AppImage desktop integration.
* Copies the integration module and wires it into create-app.js
* so the .desktop file + icon are installed on first run.
* Only called for AppImage builds — zero overhead for other targets.
*/
exports.patchAppImage = function patchAppImage () {
const fs = require('fs')
const workLib = resolve(__dirname, '../../work/app/lib')
const workAssets = resolve(__dirname, '../../work/app/assets/images')
// Copy integration module to work/app/lib/
const modSrc = resolve(__dirname, 'appimage-integration.js')
const modDst = resolve(workLib, 'appimage-integration.js')
fs.copyFileSync(modSrc, modDst)
// Copy icon for desktop integration (outside asar, accessible at runtime)
const iconSrc = resolve(
__dirname, '../../node_modules/@electerm/electerm-resource/res/imgs/electerm-round-128x128.png'
)
const iconDst = resolve(workAssets, 'electerm-round-128x128.png')
if (fs.existsSync(iconSrc)) {
fs.copyFileSync(iconSrc, iconDst)
}
// Patch create-app.js to import and call installDesktopFile
const createAppPath = resolve(workLib, 'create-app.js')
let code = fs.readFileSync(createAppPath, 'utf8')
if (!code.includes('installDesktopFile')) {
code = code.replace(
"const log = require('../common/log')",
"const log = require('../common/log')\n" +
"const { installDesktopFile } = require('./appimage-integration')"
)
code = code.replace(
'app.setName(packInfo.name)',
'app.setName(packInfo.name)\n installDesktopFile()'
)
fs.writeFileSync(createAppPath, code)
}
console.log('[appimage] Patched work/app for desktop integration')
}
exports.builder = resolve(
__dirname, '../../node_modules/.bin/electron-builder'
)
exports.reBuild = resolve(
__dirname, '../../node_modules/.bin/electron-rebuild'
)
exports.replaceArr = function (froms, tos) {
const pth = resolve(__dirname, '../../electron-builder.json')
console.log('electron-builder', pth)
let str = readFileSync(pth, 'utf8')
for (let i = 0; i < froms.length; i++) {
str = str.replace(froms[i], tos[i])
}
writeFileSync(pth, str)
}
exports.replaceJSON = function (func) {
const pth = resolve(__dirname, '../../electron-builder.json')
const js = require(pth)
func(js)
writeFileSync(pth, JSON.stringify(js, null, 2))
}
exports.setChannel = function (suffix) {
exports.replaceJSON((data) => {
if (!data.linux) { data.linux = {} }
if (!data.linux.publish) { data.linux.publish = {} }
data.linux.publish.channel = '${env.WORKFLOW_NAME}-' + suffix // eslint-disable-line
})
}
const options = {
files: require('path').resolve(__dirname, '../../electron-builder.json'),
from: ['"asar": true', '${productName}-${version}-${os}-${arch}.${ext}', ', "appx", "nsis"'], // eslint-disable-line
to: ['"asar": false', '${productName}-${version}-${os}-${arch}-loose.${ext}', ''] // eslint-disable-line
}
exports.replaceRun = function () {
return new Promise((resolve, reject) => {
replace(options, (err) => {
if (err) {
return reject(err)
}
console.log('start build loose file(no asar)')
resolve()
})
})
}
const shouldKeepFile = !!process.env.KEEP_FILE
exports.renameDist = function renameDist () {
if (!shouldKeepFile) {
return rm('-rf', 'dist')
}
mv('dist', 'dist' + new Date().getTime())
}
exports.uploadToR2 = async function uploadToR2 (src) {
// Must be running in CI
if (!process.env.CI) {
console.log('[r2] Skipping upload: not in CI environment')
return
}
// Check commit message contains "[r2]"
const { execSync } = require('child_process')
let commitMsg = ''
try {
commitMsg = execSync('git log -1 --format=%s', { encoding: 'utf8' }).trim()
} catch (e) {
console.log('[r2] Skipping upload: could not read commit message')
return
}
if (!commitMsg.includes('[r2]')) {
console.log('[r2] Skipping upload: commit message does not contain "[r2]"')
return
}
// Check required env vars
const accountId = process.env.CF_R2_ACCOUNT_ID
const bucket = process.env.CF_R2_BUCKET
const accessKeyId = process.env.CF_R2_ACCESS_KEY_ID
const secretAccessKey = process.env.CF_R2_SECRET_ACCESS_KEY
if (!accountId || !bucket || !accessKeyId || !secretAccessKey) {
console.error('[r2] Missing required env vars: CF_R2_ACCOUNT_ID, CF_R2_BUCKET, CF_R2_ACCESS_KEY_ID, CF_R2_SECRET_ACCESS_KEY')
return
}
// Find the built file in dist/
const fs = require('fs')
const path = require('path')
const distDir = resolve(__dirname, '../../dist')
let filePath = null
if (fs.existsSync(distDir)) {
const files = fs.readdirSync(distDir)
const match = files.find(f => f.endsWith(src))
if (match) {
filePath = path.join(distDir, match)
}
}
if (!filePath) {
// Try to find recursively one level deeper
if (fs.existsSync(distDir)) {
const entries = fs.readdirSync(distDir, { withFileTypes: true })
for (const entry of entries) {
if (entry.isDirectory()) {
const subDir = path.join(distDir, entry.name)
const subFiles = fs.readdirSync(subDir)
const match = subFiles.find(f => f.endsWith(src))
if (match) {
filePath = path.join(subDir, match)
break
}
}
}
}
}
if (!filePath) {
console.error(`[r2] Could not find built file matching "${src}" in dist/`)
return
}
console.log(`[r2] Uploading ${filePath} to R2 bucket "${bucket}" as "${path.basename(filePath)}"`)
// Polyfill crypto.getRandomValues for Node < 19.
// @smithy/core reads getRandomValues from require('node:crypto') directly
// (line: const _getRandomValues = node_crypto.getRandomValues), so we must
// patch the crypto module itself, not just globalThis.crypto.
const nodeCrypto = require('crypto')
if (!nodeCrypto.getRandomValues) {
nodeCrypto.getRandomValues = (buf) => nodeCrypto.randomFillSync(buf)
}
if (!globalThis.crypto) {
globalThis.crypto = nodeCrypto
}
let S3Client, PutObjectCommand
try {
const s3Module = require('@aws-sdk/client-s3')
S3Client = s3Module.S3Client
PutObjectCommand = s3Module.PutObjectCommand
} catch (e) {
console.error('[r2] @aws-sdk/client-s3 is not installed. Run: npm install --save-dev @aws-sdk/client-s3')
throw e
}
const client = new S3Client({
region: 'auto',
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId,
secretAccessKey
}
})
const fileStream = fs.createReadStream(filePath)
const key = path.basename(filePath)
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: fileStream,
ContentLength: fs.statSync(filePath).size
})
await client.send(command)
console.log(`[r2] Successfully uploaded ${key} to R2`)
}
/**
* Patch the electron-builder NSIS template so that the installer always tries
* to keep shortcuts (desktop / start-menu / taskbar pins) when upgrading.
*
* By default, when `allowToChangeInstallationDirectory` is set, the template
* sets `isTryToKeepShortcuts = "false"` for manual (non-auto-update) installs,
* which causes `WinShell::UninstShortcut` / `UninstAppUserModelId` to be called
* and the taskbar pin to be silently removed. Removing the guard makes the
* installer always pass `--keep-shortcuts` to the old uninstaller when the
* previous install wrote `KeepShortcuts = "true"` to the registry.
*/
exports.patchNsisKeepShortcuts = function patchNsisKeepShortcuts () {
const fs = require('fs')
const path = require('path')
const templatePath = path.join(
require.resolve('app-builder-lib/package.json'),
'../templates/nsis/include/installUtil.nsh'
)
const original = fs.readFileSync(templatePath, 'utf-8')
const patched = original.replace(
/(!macro setIsTryToKeepShortcuts\s+StrCpy \$isTryToKeepShortcuts "true"\s*)!ifdef allowToChangeInstallationDirectory[\s\S]*?!endif(\s*!macroend)/,
'$1$2'
)
if (patched === original) {
console.log('NSIS keep-shortcuts patch: already applied or pattern not found, skipping')
} else {
fs.writeFileSync(templatePath, patched, 'utf-8')
console.log('NSIS keep-shortcuts patch: applied successfully')
}
}
/**
* Patch electron-builder's Snap target so classic confinement builds keep the
* Chromium sandbox and do not force --no-sandbox into the generated launcher.
*
* electerm's packaged entrypoint rejects the --no-sandbox CLI flag, and classic
* snaps do not need the browser-support plug path used by strict confinement.
*/
exports.patchSnapClassicSandbox = function patchSnapClassicSandbox () {
const fs = require('fs')
const path = require('path')
const targetPath = path.join(
require.resolve('app-builder-lib/package.json'),
'../out/targets/snap.js'
)
const original = fs.readFileSync(targetPath, 'utf-8')
const patched = original.replace(
' if (this.isElectronVersionGreaterOrEqualThan("5.0.0") && !isBrowserSandboxAllowed(snap)) {',
' if (this.isElectronVersionGreaterOrEqualThan("5.0.0") && snap.confinement !== "classic" && !isBrowserSandboxAllowed(snap)) {'
)
if (patched === original) {
console.log('Snap classic sandbox patch: already applied or pattern not found, skipping')
} else {
fs.writeFileSync(targetPath, patched, 'utf-8')
console.log('Snap classic sandbox patch: applied successfully')
}
}
+183
View File
@@ -0,0 +1,183 @@
const { echo } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder: pb,
renameDist,
reBuild,
replaceJSON,
setChannel
} = require('./build-common')
// Function to add "-legacy" suffix to artifact names in electron-builder.json
function addLegacySuffix () {
echo('============')
echo('Updating electron-builder.json to add -legacy suffix')
replaceJSON((data) => {
// Update main artifactName
if (data.artifactName) {
data.artifactName = data.artifactName.replace('${productName}-${version}-${os}-${arch}.${ext}', '${productName}-${version}-${os}-${arch}-legacy.${ext}') // eslint-disable-line
}
})
echo('Updated artifact names with -legacy suffix')
}
async function main () {
echo('============================================')
echo('==== Start: running build for linux part 3 arm64/armv7l legacy ====')
echo('============================================')
// Add legacy suffix to names
addLegacySuffix()
echo('============================================')
echo('==== Start: build linux.arm64.tar.gz ====')
echo('============================================')
let src = 'linux-arm64-legacy.tar.gz'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['tar.gz']
}
)
setChannel(src)
// await run(`${reBuild} --arch arm64 -f work/app`)
await run(`${pb} --linux --arm64`).catch(error => {
echo('❌ Fatal error in build linux.arm64.tar.gz:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.arm64.deb ====')
echo('============================================')
src = 'linux-arm64-legacy.deb'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['deb']
}
)
setChannel(src)
await run(`${pb} --linux --arm64`).catch(error => {
echo('❌ Fatal error in build linux.arm64.deb:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.aarch64.rpm ====')
echo('============================================')
src = 'linux-aarch64-legacy.rpm'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['rpm']
}
)
setChannel(src)
await run(`${pb} --linux --arm64`).catch(error => {
echo('❌ Fatal error in build linux.aarch64.rpm:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.arm64.AppImage ====')
echo('============================================')
src = 'linux-arm64-legacy.AppImage'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['AppImage']
}
)
setChannel(src)
await run(`${pb} --linux --arm64`).catch(error => {
echo('❌ Fatal error in build linux.arm64.AppImage:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.armv7l.tar.gz ====')
echo('============================================')
src = 'linux-armv7l-legacy.tar.gz'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['tar.gz']
}
)
setChannel(src)
await run(`${reBuild} --arch armv7l -f work/app`)
await run(`${pb} --linux --armv7l`).catch(error => {
echo('❌ Fatal error in build linux.armv7l:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.armv7l.deb ====')
echo('============================================')
src = 'linux-armv7l-legacy.deb'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['deb']
}
)
setChannel(src)
await run(`${pb} --linux --armv7l`).catch(error => {
echo('❌ Fatal error in build linux.armv7l.deb:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.armv7l.rpm ====')
echo('============================================')
src = 'linux-armv7l-legacy.rpm'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['rpm']
}
)
setChannel(src)
await run(`${pb} --linux --armv7l`).catch(error => {
echo('❌ Fatal error in build linux.armv7l.rpm:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.armv7l.AppImage ====')
echo('============================================')
src = 'linux-armv7l-legacy.AppImage'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['AppImage']
}
)
setChannel(src)
await run(`${pb} --linux --armv7l`).catch(error => {
echo('❌ Fatal error in build linux.armv7l.AppImage:')
console.error(error)
})
await uploadToR2(src)
echo('✅ All Linux ARM builds completed successfully')
}
main()
+158
View File
@@ -0,0 +1,158 @@
const { echo } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder: pb,
renameDist,
reBuild,
replaceJSON
} = require('./build-common')
async function main () {
echo('============================================')
echo('==== Start: running build for linux part 3 arm64/armv7l ====')
echo('============================================')
echo('============================================')
echo('==== Start: build linux.arm64.tar.gz ====')
echo('============================================')
let src = 'linux-arm64.tar.gz'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['tar.gz']
}
)
// await run(`${reBuild} --arch arm64 -f work/app`)
await run(`${pb} --linux --arm64`).catch(error => {
echo('❌ Fatal error in build linux.arm64.tar.gz:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.arm64.deb ====')
echo('============================================')
src = 'linux-arm64.deb'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['deb']
}
)
await run(`${pb} --linux --arm64`).catch(error => {
echo('❌ Fatal error in build linux.arm64.deb:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.aarch64.rpm ====')
echo('============================================')
src = 'linux-aarch64.rpm'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['rpm']
}
)
await run(`${pb} --linux --arm64`).catch(error => {
echo('❌ Fatal error in build linux.aarch64.rpm:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.arm64.AppImage ====')
echo('============================================')
src = 'linux-arm64.AppImage'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['AppImage']
}
)
await run(`${pb} --linux --arm64`).catch(error => {
echo('❌ Fatal error in build linux.arm64.AppImage:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.armv7l.tar.gz ====')
echo('============================================')
src = 'linux-armv7l.tar.gz'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['tar.gz']
}
)
await run(`${reBuild} --arch armv7l -f work/app`)
await run(`${pb} --linux --armv7l`).catch(error => {
echo('❌ Fatal error in build linux.armv7l:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.armv7l.deb ====')
echo('============================================')
src = 'linux-armv7l.deb'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['deb']
}
)
await run(`${pb} --linux --armv7l`).catch(error => {
echo('❌ Fatal error in build linux.armv7l.deb:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.armv7l.rpm ====')
echo('============================================')
src = 'linux-armv7l.rpm'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['rpm']
}
)
await run(`${pb} --linux --armv7l`).catch(error => {
echo('❌ Fatal error in build linux.armv7l.rpm:')
console.error(error)
})
await uploadToR2(src)
echo('============================================')
echo('==== Start: build linux.armv7l.AppImage ====')
echo('============================================')
src = 'linux-armv7l.AppImage'
renameDist()
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['AppImage']
}
)
await run(`${pb} --linux --armv7l`).catch(error => {
echo('❌ Fatal error in build linux.armv7l.AppImage:')
console.error(error)
})
await uploadToR2(src)
echo('✅ All Linux ARM builds completed successfully')
}
main()
+44
View File
@@ -0,0 +1,44 @@
const { echo, rm } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder: pb,
replaceJSON,
renameDist
} = require('./build-common')
async function main () {
echo('running build for linux part 1')
echo('build tar.gz')
rm('-rf', 'dist')
let src = 'linux-x64.tar.gz'
writeSrc(src)
await run(`${pb} --linux tar.gz`)
await uploadToR2(src)
renameDist()
echo('build deb')
rm('-rf', 'dist')
src = 'linux-amd64.deb'
writeSrc(src)
await run(`${pb} --linux deb`)
await uploadToR2(src)
renameDist()
echo('build linux-x86_64.AppImage')
rm('-rf', 'dist')
src = 'linux-x86_64.AppImage'
writeSrc(src)
replaceJSON(
(data) => {
data.linux.target = ['AppImage']
}
)
await run(`${pb} --linux`)
await uploadToR2(src)
renameDist()
}
main()
+88
View File
@@ -0,0 +1,88 @@
const { echo, rm } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder: pb,
replaceJSON,
renameDist,
setChannel
} = require('./build-common')
// Function to add "-legacy" suffix to artifact names in electron-builder.json
function addLegacySuffix () {
echo('============')
echo('Updating electron-builder.json to add -legacy suffix')
replaceJSON((data) => {
// Update main artifactName
if (data.artifactName) {
data.artifactName = data.artifactName.replace('${productName}-${version}-${os}-${arch}.${ext}', '${productName}-${version}-${os}-${arch}-legacy.${ext}') // eslint-disable-line
}
})
echo('Updated artifact names with -legacy suffix')
}
async function main () {
echo('============')
echo('Starting legacy Linux build process')
echo('============')
// Add legacy suffix to names
addLegacySuffix()
echo('============')
echo('Building tar.gz package')
echo('============')
rm('-rf', 'dist')
let src = 'linux-x64-legacy.tar.gz'
writeSrc(src)
setChannel(src)
await run(`${pb} --linux tar.gz`)
await uploadToR2(src)
renameDist()
echo('============')
echo('Building deb package')
echo('============')
rm('-rf', 'dist')
src = 'linux-amd64-legacy.deb'
writeSrc(src)
setChannel(src)
await run(`${pb} --linux deb`)
await uploadToR2(src)
renameDist()
echo('============')
echo('Building rpm package')
echo('============')
rm('-rf', 'dist')
src = 'linux-x86_64-legacy.rpm'
writeSrc(src)
replaceJSON((data) => {
data.linux.target = ['rpm']
})
setChannel(src)
await run(`${pb} --linux rpm`)
await uploadToR2(src)
renameDist()
echo('============')
echo('Building AppImage package')
echo('============')
rm('-rf', 'dist')
src = 'linux-x86_64-legacy.AppImage'
writeSrc(src)
replaceJSON((data) => {
data.linux.target = ['AppImage']
})
setChannel(src)
await run(`${pb} --linux`)
await uploadToR2(src)
renameDist()
echo('============')
echo('Legacy Linux build process completed')
echo('============')
}
main()
+31
View File
@@ -0,0 +1,31 @@
/**
* Build electerm for loong64 (LoongArch64) Linux - Legacy (old world)
*
* This script orchestrates the loong64 legacy build process:
* 1. Build x64 version with legacy deps to get the asar
* 2. Download electron loong64 from Loongnix FTP
* 3. Cross-compile native modules for loong64 (node-pty@0.10.1, serialport@10.5.0)
* 4. Merge x64 asar with loong64 electron and native modules
* 5. Upload tar.gz to GitHub release draft
* 6. Build deb package and upload to GitHub release draft
*/
const { resolve } = require('path')
const { echo } = require('shelljs')
const { run } = require('./build-common')
const SCRIPT_PATH = resolve(__dirname, 'build-linux-loong64-legacy.sh')
async function main () {
echo('Starting electerm loong64-legacy build...')
try {
await run(`bash ${SCRIPT_PATH}`)
echo('Loong64-legacy build completed successfully!')
} catch (err) {
console.error('Loong64-legacy build failed:', err.message)
process.exit(1)
}
}
main()
+559
View File
@@ -0,0 +1,559 @@
#!/bin/bash
# Build electerm for loong64 (LoongArch64) Linux - Legacy (old world)
#
# Strategy:
# 1. Build x64 version with legacy deps to get the asar
# 2. Download electron loong64 from Loongnix FTP
# 3. Cross-compile native modules for loong64 (node-pty@0.10.1, serialport@10.5.0)
# 4. Merge x64 asar with loong64 electron and native modules
# 5. Upload tar.gz to GitHub release draft
# 6. Build deb package and upload to GitHub release draft
#
# Prerequisites:
# - loongarch64-linux-gnu-g++ (cross-compiler, preferably GCC 8 from Loongnix)
# - curl, tar, unzip
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
WORK_DIR="$PROJECT_ROOT/work-loong64-legacy"
ELECTRON_VERSION="22.3.27"
# Loongnix FTP electron download URL - update version as needed
ELECTRON_LOONG64_URL="https://ftp.loongnix.cn/electron/LoongArch/v${ELECTRON_VERSION}/electron-v${ELECTRON_VERSION}-linux-loong64.zip"
OUTPUT_DIR="$PROJECT_ROOT/dist-loong64-legacy"
SKIP_NATIVE="${SKIP_NATIVE:-0}"
WORKFLOW_NAME="${WORKFLOW_NAME:-linux-loong64-legacy}"
# Legacy native module versions
NODE_PTY_VERSION="1.1.0"
SERIALPORT_VERSION="10.5.0"
# GCC 8 from Loongnix (old-world ABI)
GCC8_PREFIX="${GCC8_PREFIX:-/opt/gcc8-loong64}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
# ============================================================================
# Helpers
# ============================================================================
get_version() {
node -e "console.log(require('$PROJECT_ROOT/package.json').version)"
}
write_install_src() {
local src="$1"
node -e "
const { writeFileSync } = require('fs');
writeFileSync('$PROJECT_ROOT/work/app/lib/install-src.js', \"module.exports = '${src}'\");
"
}
# ============================================================================
# Step 0: Install prerequisites
# ============================================================================
install_prerequisites() {
log_info "Checking prerequisites..."
local SUDO="sudo"
if [ "$(id -u)" -eq 0 ]; then
SUDO=""
fi
for cmd in curl tar unzip; do
if ! command -v "$cmd" &>/dev/null; then
$SUDO apt-get install -y "$cmd"
fi
done
# Check for GCC 8 from Loongnix
if [ -d "$GCC8_PREFIX/bin" ]; then
log_info "Using GCC 8 from Loongnix at $GCC8_PREFIX"
export PATH="$GCC8_PREFIX/bin:$PATH"
loongarch64-linux-gnu-gcc --version | head -1
fi
log_info "Prerequisites ready."
}
# ============================================================================
# Step 1: Build x64 version with legacy deps
# ============================================================================
build_x64() {
log_info "Step 1: Building x64 version with electron v${ELECTRON_VERSION}..."
cd "$PROJECT_ROOT"
# Save original electron version
local original_electron_version
original_electron_version=$(node -e "console.log(require('./package.json').devDependencies.electron)")
# Temporarily set electron version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.devDependencies.electron = '${ELECTRON_VERSION}';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
log_info "Running npm run b..."
npm run b
npm run pb
# Build x64 tar.gz
rm -rf dist
local builder="./node_modules/.bin/electron-builder"
write_install_src "linux-loong64-legacy.tar.gz"
export WORKFLOW_NAME
$builder --linux tar.gz --x64 --publish=never
# Find the built tar.gz
local tar_gz
tar_gz=$(find dist -name "*.tar.gz" -type f | head -1)
if [ -z "$tar_gz" ]; then
log_error "Could not find built tar.gz in dist/"
exit 1
fi
log_info "x64 build complete: $tar_gz"
# Extract asar and full app directory from tar.gz
mkdir -p "$WORK_DIR/x64-extract"
tar xzf "$tar_gz" -C "$WORK_DIR/x64-extract"
local asar_file
asar_file=$(find "$WORK_DIR/x64-extract" -name "app.asar" -type f | head -1)
if [ -z "$asar_file" ]; then
log_error "Could not find app.asar in extracted tar.gz"
exit 1
fi
cp "$asar_file" "$WORK_DIR/app.asar"
local app_dir
app_dir=$(find "$WORK_DIR/x64-extract" -maxdepth 1 -name "electerm*" -type d | head -1)
if [ -n "$app_dir" ]; then
cp -r "$app_dir" "$WORK_DIR/x64-app"
fi
# Restore original electron version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.devDependencies.electron = '${original_electron_version}';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
rm -rf "$WORK_DIR/x64-extract"
}
# ============================================================================
# Step 2: Download electron loong64 from Loongnix
# ============================================================================
download_electron_loong64() {
log_info "Step 2: Downloading electron v${ELECTRON_VERSION} for loong64 from Loongnix..."
mkdir -p "$WORK_DIR/electron-loong64"
local zip_file="$WORK_DIR/electron-loong64.zip"
if [ ! -f "$zip_file" ]; then
curl -L -o "$zip_file" "$ELECTRON_LOONG64_URL"
fi
# Try to unzip, the archive may contain electron directly or in a subdirectory
unzip -q -o "$zip_file" -d "$WORK_DIR/electron-loong64-raw"
# Find electron binary - may be at root or in a subdirectory
if [ -f "$WORK_DIR/electron-loong64-raw/electron" ]; then
cp -r "$WORK_DIR/electron-loong64-raw"/* "$WORK_DIR/electron-loong64/"
else
# Look for electron binary in subdirectory
local electron_bin
electron_bin=$(find "$WORK_DIR/electron-loong64-raw" -name "electron" -type f | head -1)
if [ -n "$electron_bin" ]; then
local subdir
subdir=$(dirname "$electron_bin")
cp -r "$subdir"/* "$WORK_DIR/electron-loong64/"
fi
fi
rm -rf "$WORK_DIR/electron-loong64-raw"
if [ ! -f "$WORK_DIR/electron-loong64/electron" ]; then
log_error "Could not find electron binary in extracted zip"
exit 1
fi
log_info "Electron loong64 downloaded and extracted."
}
# ============================================================================
# Step 3: Rebuild native modules for loong64 using cross-compilation
# ============================================================================
rebuild_native_modules() {
if [ "$SKIP_NATIVE" = "1" ]; then
log_warn "Step 3: Skipping native module rebuild (SKIP_NATIVE=1)"
return 0
fi
if ! command -v loongarch64-linux-gnu-g++ &>/dev/null; then
log_warn "Step 3: loongarch64-linux-gnu-g++ not found, skipping native module rebuild"
log_warn "Install with: sudo apt-get install g++-loongarch64-linux-gnu"
return 0
fi
log_info "Step 3: Cross-compiling native modules for loong64 (legacy deps)..."
log_info " node-pty@${NODE_PTY_VERSION}, serialport@${SERIALPORT_VERSION}"
local native_modules_dir="$WORK_DIR/native-modules-loong64"
mkdir -p "$native_modules_dir"
local cross_build_dir="$WORK_DIR/cross-build"
rm -rf "$cross_build_dir"
mkdir -p "$cross_build_dir"
cd "$cross_build_dir"
# Use Electron 22.3.27 headers which have:
# - NODE_MODULE_VERSION 110 (matching Loongnix Electron)
# - v8 10.8 headers (matching Electron's Chromium-based v8)
# Standard Node 16 headers have v8 9.4 and ABI 93 - wrong for Electron.
local headers_dir="$WORK_DIR/electron-headers"
log_info "Downloading Electron v${ELECTRON_VERSION} headers..."
curl -sL "https://artifacts.electronjs.org/headers/dist/v${ELECTRON_VERSION}/node-v${ELECTRON_VERSION}-headers.tar.gz" -o "$WORK_DIR/electron-headers.tar.gz"
mkdir -p "$headers_dir"
tar xzf "$WORK_DIR/electron-headers.tar.gz" -C "$headers_dir" --strip-components=1
log_info "Electron headers NODE_MODULE_VERSION: $(grep NODE_MODULE_VERSION "$headers_dir/include/node/node_version.h" | grep define | head -1)"
# Build node-pty (legacy version)
log_info "Building node-pty@${NODE_PTY_VERSION} for loong64..."
log_info "CC=loongarch64-linux-gnu-gcc CXX=loongarch64-linux-gnu-g++"
log_info "npm_config_nodedir=$headers_dir"
log_info "npm_config_arch=loong64 npm_config_target_arch=loong64"
mkdir -p build-node-pty && cd build-node-pty
npm init -y 2>/dev/null
npm_config_nodedir="$headers_dir" npm_config_arch=loong64 npm_config_target_arch=loong64 CC=loongarch64-linux-gnu-gcc CXX=loongarch64-linux-gnu-g++ npm install node-pty@${NODE_PTY_VERSION} --build-from-source 2>&1 | tee /tmp/node-pty-build.log | tail -30
log_info "Full node-pty build log saved to /tmp/node-pty-build.log"
find . -name "pty.node" -type f | while read f; do
log_info "Found pty.node: $f"
file "$f"
log_info "ELF headers:"
readelf -h "$f" 2>/dev/null | grep -E "Machine|Class" || true
log_info "Dynamic dependencies:"
readelf -d "$f" 2>/dev/null | grep NEEDED || true
log_info "Undefined symbols:"
readelf -s "$f" 2>/dev/null | grep "UND" | grep -v "UND$" | head -20 || true
cp "$f" "$native_modules_dir/node-pty.node"
done
cd "$cross_build_dir"
# Build @serialport/bindings-cpp (legacy serialport@10.5.0)
log_info "Building serialport@${SERIALPORT_VERSION} bindings for loong64..."
mkdir -p build-serialport && cd build-serialport
npm init -y 2>/dev/null
npm_config_nodedir="$headers_dir" npm_config_arch=loong64 npm_config_target_arch=loong64 CC=loongarch64-linux-gnu-gcc CXX=loongarch64-linux-gnu-g++ npm install @serialport/bindings-cpp --build-from-source 2>&1 | tee /tmp/serialport-build.log | tail -30
find . -path "*/build/Release/bindings.node" -type f | while read f; do
log_info "Found bindings.node: $f"
file "$f"
log_info "ELF headers:"
readelf -h "$f" 2>/dev/null | grep -E "Machine|Class" || true
cp "$f" "$native_modules_dir/serialport-bindings.node"
done
cd "$cross_build_dir"
unset CC CXX AR RANLIB LINK GYP_DEFINES npm_config_arch npm_config_target_arch npm_config_nodedir
# Verify native modules are LoongArch64 ELF binaries
if [ -d "$native_modules_dir" ] && [ "$(ls -A "$native_modules_dir" 2>/dev/null)" ]; then
log_info "Native modules built successfully:"
ls -la "$native_modules_dir/"
for mod in "$native_modules_dir"/*.node; do
log_info "Verifying $mod:"
file "$mod"
if ! file "$mod" | grep -q "LoongArch"; then
log_error "WARNING: $mod does not appear to be a LoongArch binary!"
fi
done
else
log_warn "No native modules were built."
fi
if [ -d "$native_modules_dir" ] && [ "$(ls -A "$native_modules_dir" 2>/dev/null)" ]; then
log_info "Native modules built successfully:"
ls -la "$native_modules_dir/"
else
log_warn "No native modules were built."
fi
}
# ============================================================================
# Step 4: Merge x64 asar with loong64 electron and native modules
# ============================================================================
merge_loong64() {
log_info "Step 4: Merging x64 asar with loong64 electron..."
local electerm_version
electerm_version=$(get_version)
mkdir -p "$OUTPUT_DIR"
local output_name="electerm-${electerm_version}-linux-loong64-legacy"
local output_dir="$OUTPUT_DIR/$output_name"
rm -rf "$output_dir"
mkdir -p "$output_dir"
# Start with x64 app directory
if [ -d "$WORK_DIR/x64-app" ]; then
cp -r "$WORK_DIR/x64-app"/* "$output_dir/"
fi
# Replace x64 binary with loong64 electron
rm -f "$output_dir/electerm"
cp "$WORK_DIR/electron-loong64/electron" "$output_dir/electerm"
chmod +x "$output_dir/electerm"
# Copy loong64 electron runtime files (libraries, locales, etc.)
for f in "$WORK_DIR/electron-loong64"/*; do
local base
base=$(basename "$f")
if [ "$base" = "electron" ]; then
continue
fi
if [ -d "$f" ] && [ "$base" != "locales" ]; then
continue
fi
if [ -d "$f" ]; then
cp -r "$f" "$output_dir/"
else
cp "$f" "$output_dir/"
fi
done
# Replace native modules with loong64 versions
local native_modules_dir="$WORK_DIR/native-modules-loong64"
if [ -d "$native_modules_dir" ] && [ "$(ls -A "$native_modules_dir" 2>/dev/null)" ]; then
log_info "Replacing native modules with loong64 versions..."
if [ -f "$native_modules_dir/node-pty.node" ]; then
find "$output_dir" -path "*/node-pty/build/Release/*.node" -exec cp "$native_modules_dir/node-pty.node" {} \; 2>/dev/null || true
fi
if [ -f "$native_modules_dir/serialport-bindings.node" ]; then
find "$output_dir" -path "*@serialport/bindings-cpp*" -name "*.node" -exec cp "$native_modules_dir/serialport-bindings.node" {} \; 2>/dev/null || true
fi
if [ -f "$native_modules_dir/ssh2-crypto.node" ]; then
find "$output_dir" -path "*@electerm/ssh2*" -name "*.node" -exec cp "$native_modules_dir/ssh2-crypto.node" {} \; 2>/dev/null || true
fi
fi
# Ensure asar is in place
if [ -f "$WORK_DIR/app.asar" ] && [ ! -f "$output_dir/resources/app.asar" ]; then
mkdir -p "$output_dir/resources"
cp "$WORK_DIR/app.asar" "$output_dir/resources/"
fi
# Create tar.gz
log_info "Creating loong64-legacy tar.gz..."
cd "$OUTPUT_DIR"
tar czf "${output_name}.tar.gz" "$output_name"
log_info "Tar.gz complete: $OUTPUT_DIR/${output_name}.tar.gz"
}
# ============================================================================
# Step 5: Build deb package
# ============================================================================
patch_asar_install_src() {
local asar_file="$1"
local new_src="$2"
log_info "Patching asar install-src to '${new_src}'..."
local asar_bin="$PROJECT_ROOT/node_modules/@electron/asar/bin/asar.js"
local tmp_dir
tmp_dir=$(mktemp -d)
# Extract, patch, repack using CLI
node "$asar_bin" extract "$asar_file" "$tmp_dir"
echo "module.exports = '${new_src}'" > "$tmp_dir/lib/install-src.js"
node "$asar_bin" pack "$tmp_dir" "$asar_file"
rm -rf "$tmp_dir"
log_info "Asar patched successfully."
}
build_deb() {
local install_src="${1:-linux-loong64-legacy.deb}"
local deb_arch="${2:-loong64}"
local suffix="$deb_arch"
log_info "Step 5: Building ${suffix} deb package..."
local electerm_version
electerm_version=$(get_version)
local output_dir="$OUTPUT_DIR/electerm-${electerm_version}-linux-loong64-legacy"
local deb_build="$OUTPUT_DIR/deb-build-${suffix}"
local deb_name="electerm_${electerm_version}_loongarch64"
local deb_dir="$deb_build/$deb_name"
if [ ! -d "$output_dir" ]; then
log_error "Output directory not found: $output_dir"
return 1
fi
# Patch asar to use deb install-src
local asar_file="$output_dir/resources/app.asar"
if [ -f "$asar_file" ]; then
patch_asar_install_src "$asar_file" "$install_src"
else
log_warn "app.asar not found at $asar_file, skipping install-src patch"
fi
rm -rf "$deb_build"
mkdir -p "$deb_dir/DEBIAN"
mkdir -p "$deb_dir/opt/electerm"
mkdir -p "$deb_dir/usr/share/applications"
mkdir -p "$deb_dir/usr/share/icons/hicolor/128x128/apps"
cp -r "$output_dir"/* "$deb_dir/opt/electerm/"
# Install icon
local icon_src="$PROJECT_ROOT/node_modules/@electerm/electerm-resource/res/imgs/electerm-round-128x128.png"
if [ -f "$icon_src" ]; then
cp "$icon_src" "$deb_dir/usr/share/icons/hicolor/128x128/apps/electerm.png"
fi
# Install desktop file
cat > "$deb_dir/usr/share/applications/electerm.desktop" <<'DESKTOP'
[Desktop Entry]
Name=electerm
Comment=Terminal/SSH/SFTP client
Exec=/opt/electerm/electerm %U
Icon=electerm
Terminal=false
Type=Application
Categories=Development;System;TerminalEmulator;
StartupWMClass=electerm
MimeType=x-scheme-handler/ssh;x-scheme-handler/telnet;x-scheme-handler/rdp;x-scheme-handler/vnc;x-scheme-handler/serial;x-scheme-handler/spice;x-scheme-handler/electerm;
DESKTOP
cat > "$deb_dir/DEBIAN/control" <<CTRL
Package: electerm
Version: ${electerm_version}
Section: utils
Priority: optional
Architecture: ${deb_arch}
Depends: libglib2.0-0, libnss3, libnspr4, libdbus-1-3, libatk1.0-0, libatk-bridge2.0-0, libcups2, libcairo2, libpango-1.0-0, libx11-6, libxcomposite1, libxdamage1, libxext6, libxfixes3, libxrandr2, libxkbcommon0, libdrm2, libgbm1, libatspi2.0-0, libpulse0, libgtk-3-0
Maintainer: ZHAO Xudong <zxdong@gmail.com>
Description: Open-sourced terminal/ssh/sftp/telnet/serialport/RDP/VNC/Spice/ftp client
CTRL
cat > "$deb_dir/DEBIAN/postinst" <<'POSTINST'
#!/bin/bash
chown root:root /opt/electerm/chrome-sandbox
chmod 4755 /opt/electerm/chrome-sandbox
update-desktop-database /usr/share/applications/ 2>/dev/null || true
gtk-update-icon-cache /usr/share/icons/hicolor/ 2>/dev/null || true
POSTINST
chmod 755 "$deb_dir/DEBIAN/postinst"
local deb_file="$OUTPUT_DIR/electerm-${electerm_version}-linux-${suffix}-legacy.deb"
# Use gzip compression for compatibility with dpkg < 1.21.18 (e.g. Loongnix 20 has dpkg 1.19.7)
if command -v fakeroot &>/dev/null; then
fakeroot dpkg-deb -Zgzip --root-owner-group --build "$deb_dir" "$deb_file"
else
dpkg-deb -Zgzip --root-owner-group --build "$deb_dir" "$deb_file"
fi
if [ ! -f "$deb_file" ]; then
log_error "Deb build failed: $deb_file not created"
return 1
fi
log_info "Deb complete: $deb_file"
rm -rf "$deb_build"
}
# ============================================================================
# Step 6: Upload to GitHub release draft
# ============================================================================
upload_to_github() {
local file="$1"
local filename
filename=$(basename "$file")
if [ ! -f "$file" ]; then
log_warn "Upload skipped: file not found: $file"
return 0
fi
if [ -z "${GH_TOKEN:-}" ]; then
log_warn "Upload skipped: GH_TOKEN not set"
return 0
fi
local electerm_version
electerm_version=$(get_version)
local release_name="v${electerm_version}"
log_info "Uploading $filename to GitHub release draft '${release_name}'..."
if ! gh release view "$release_name" >/dev/null 2>&1; then
gh release create "$release_name" \
--draft \
--title "$release_name" \
--generate-notes \
"$file"
else
gh release upload "$release_name" "$file" --clobber
fi
log_info "Uploaded $filename."
}
# ============================================================================
# Main
# ============================================================================
main() {
log_info "Starting electerm loong64-legacy build..."
log_info "Project root: $PROJECT_ROOT"
log_info "Work directory: $WORK_DIR"
log_info "Output directory: $OUTPUT_DIR"
log_info "Electron version: $ELECTRON_VERSION"
log_info "node-pty: $NODE_PTY_VERSION, serialport: $SERIALPORT_VERSION"
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
install_prerequisites
build_x64
download_electron_loong64
rebuild_native_modules
merge_loong64
local electerm_version
electerm_version=$(get_version)
# Upload tar.gz
upload_to_github "$OUTPUT_DIR/electerm-${electerm_version}-linux-loong64-legacy.tar.gz"
# Build and upload debs
build_deb "linux-loong64-legacy.deb" "loong64"
upload_to_github "$OUTPUT_DIR/electerm-${electerm_version}-linux-loong64-legacy.deb"
build_deb "linux-loongarch64-legacy.deb" "loongarch64"
upload_to_github "$OUTPUT_DIR/electerm-${electerm_version}-linux-loongarch64-legacy.deb"
log_info "=========================================="
log_info "Build complete!"
log_info " $OUTPUT_DIR/electerm-${electerm_version}-linux-loong64-legacy.tar.gz"
log_info " $OUTPUT_DIR/electerm-${electerm_version}-linux-loong64-legacy.deb"
log_info " $OUTPUT_DIR/electerm-${electerm_version}-linux-loongarch64-legacy.deb"
log_info "=========================================="
}
main "$@"
+31
View File
@@ -0,0 +1,31 @@
/**
* Build electerm for loong64 (LoongArch64) Linux
*
* This script orchestrates the loong64 build process:
* 1. Build x64 version with electron v39.2.7 to get the asar
* 2. Download electron loong64 v39.2.7
* 3. Cross-compile native modules for loong64
* 4. Merge x64 asar with loong64 electron and native modules
* 5. Upload tar.gz to GitHub release draft
* 6. Build deb package (patched asar) and upload to GitHub release draft
*/
const { resolve } = require('path')
const { echo } = require('shelljs')
const { run } = require('./build-common')
const SCRIPT_PATH = resolve(__dirname, 'build-linux-loong64.sh')
async function main () {
echo('Starting electerm loong64 build...')
try {
await run(`bash ${SCRIPT_PATH}`)
echo('Loong64 build completed successfully!')
} catch (err) {
console.error('Loong64 build failed:', err.message)
process.exit(1)
}
}
main()
+481
View File
@@ -0,0 +1,481 @@
#!/bin/bash
# Build electerm for loong64 (LoongArch64) Linux
#
# Strategy:
# 1. Build x64 version with electron v39.2.7 to get the asar
# 2. Download electron loong64 v39.2.7 from darkyzhou/electron-loong64
# 3. Cross-compile native modules for loong64
# 4. Merge x64 asar with loong64 electron and native modules
# 5. Upload tar.gz to GitHub release draft
# 6. Build deb packages (loong64 and loongarch64, patched asar) and upload to GitHub release draft
#
# Prerequisites:
# - loongarch64-linux-gnu-g++ (for native module cross-compilation)
# - curl, tar, unzip
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
WORK_DIR="$PROJECT_ROOT/work-loong64"
ELECTRON_VERSION="39.2.7"
ELECTRON_LOONG64_URL="https://github.com/darkyzhou/electron-loong64/releases/download/v${ELECTRON_VERSION}/electron-v${ELECTRON_VERSION}-linux-loong64.zip"
OUTPUT_DIR="$PROJECT_ROOT/dist-loong64"
SKIP_NATIVE="${SKIP_NATIVE:-0}" # Set to 1 to skip native module rebuild
WORKFLOW_NAME="${WORKFLOW_NAME:-linux-loong64}" # For electron-builder publish channel
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
# ============================================================================
# Helpers
# ============================================================================
get_version() {
node -e "console.log(require('$PROJECT_ROOT/package.json').version)"
}
write_install_src() {
local src="$1"
node -e "
const { writeFileSync } = require('fs');
writeFileSync('$PROJECT_ROOT/work/app/lib/install-src.js', \"module.exports = '${src}'\");
"
}
# ============================================================================
# Step 0: Install prerequisites
# ============================================================================
install_prerequisites() {
log_info "Checking prerequisites..."
local SUDO="sudo"
if [ "$(id -u)" -eq 0 ]; then
SUDO=""
fi
for cmd in curl tar unzip; do
if ! command -v "$cmd" &>/dev/null; then
$SUDO apt-get install -y "$cmd"
fi
done
log_info "Prerequisites ready."
}
# ============================================================================
# Step 1: Build x64 version with electron v39.2.7
# ============================================================================
build_x64() {
log_info "Step 1: Building x64 version with electron v${ELECTRON_VERSION}..."
cd "$PROJECT_ROOT"
# Save original electron version
local original_electron_version
original_electron_version=$(node -e "console.log(require('./package.json').devDependencies.electron)")
# Temporarily set electron version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.devDependencies.electron = '${ELECTRON_VERSION}';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
log_info "Running npm run b..."
npm run b
npm run pb
# Build x64 tar.gz
rm -rf dist
local builder="./node_modules/.bin/electron-builder"
write_install_src "linux-loong64.tar.gz"
export WORKFLOW_NAME
$builder --linux tar.gz --x64 --publish=never
# Find the built tar.gz
local tar_gz
tar_gz=$(find dist -name "*.tar.gz" -type f | head -1)
if [ -z "$tar_gz" ]; then
log_error "Could not find built tar.gz in dist/"
exit 1
fi
log_info "x64 build complete: $tar_gz"
# Extract asar and full app directory from tar.gz
mkdir -p "$WORK_DIR/x64-extract"
tar xzf "$tar_gz" -C "$WORK_DIR/x64-extract"
local asar_file
asar_file=$(find "$WORK_DIR/x64-extract" -name "app.asar" -type f | head -1)
if [ -z "$asar_file" ]; then
log_error "Could not find app.asar in extracted tar.gz"
exit 1
fi
cp "$asar_file" "$WORK_DIR/app.asar"
local app_dir
app_dir=$(find "$WORK_DIR/x64-extract" -maxdepth 1 -name "electerm*" -type d | head -1)
if [ -n "$app_dir" ]; then
cp -r "$app_dir" "$WORK_DIR/x64-app"
fi
# Restore original electron version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.devDependencies.electron = '${original_electron_version}';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
rm -rf "$WORK_DIR/x64-extract"
}
# ============================================================================
# Step 2: Download electron loong64
# ============================================================================
download_electron_loong64() {
log_info "Step 2: Downloading electron v${ELECTRON_VERSION} for loong64..."
mkdir -p "$WORK_DIR/electron-loong64"
local zip_file="$WORK_DIR/electron-loong64.zip"
if [ ! -f "$zip_file" ]; then
curl -L -o "$zip_file" "$ELECTRON_LOONG64_URL"
fi
unzip -q -o "$zip_file" -d "$WORK_DIR/electron-loong64"
if [ ! -f "$WORK_DIR/electron-loong64/electron" ]; then
log_error "Could not find electron binary in extracted zip"
exit 1
fi
log_info "Electron loong64 downloaded and extracted."
}
# ============================================================================
# Step 3: Rebuild native modules for loong64 using cross-compilation
# ============================================================================
rebuild_native_modules() {
if [ "$SKIP_NATIVE" = "1" ]; then
log_warn "Step 3: Skipping native module rebuild (SKIP_NATIVE=1)"
return 0
fi
if ! command -v loongarch64-linux-gnu-g++ &>/dev/null; then
log_warn "Step 3: loongarch64-linux-gnu-g++ not found, skipping native module rebuild"
log_warn "Install with: sudo apt-get install g++-loongarch64-linux-gnu"
return 0
fi
log_info "Step 3: Cross-compiling native modules for loong64..."
local native_modules_dir="$WORK_DIR/native-modules-loong64"
mkdir -p "$native_modules_dir"
local cross_build_dir="$WORK_DIR/cross-build"
rm -rf "$cross_build_dir"
mkdir -p "$cross_build_dir"
cd "$cross_build_dir"
# Build node-pty
log_info "Building node-pty for loong64..."
mkdir -p build-node-pty && cd build-node-pty
npm init -y 2>/dev/null
npm_config_arch=loong64 npm_config_target_arch=loong64 CC=loongarch64-linux-gnu-gcc CXX=loongarch64-linux-gnu-g++ npm install node-pty@1.1.0-beta34 --build-from-source 2>&1 | tail -5
find . -name "pty.node" -type f | while read f; do
cp "$f" "$native_modules_dir/node-pty.node"
done
cd "$cross_build_dir"
# Build @serialport/bindings-cpp
log_info "Building @serialport/bindings-cpp for loong64..."
mkdir -p build-serialport && cd build-serialport
npm init -y 2>/dev/null
npm_config_arch=loong64 npm_config_target_arch=loong64 CC=loongarch64-linux-gnu-gcc CXX=loongarch64-linux-gnu-g++ npm install @serialport/bindings-cpp --build-from-source 2>&1 | tail -5
find . -path "*/build/Release/bindings.node" -type f | while read f; do
cp "$f" "$native_modules_dir/serialport-bindings.node"
done
cd "$cross_build_dir"
unset CC CXX AR RANLIB LINK GYP_DEFINES npm_config_arch npm_config_target_arch
if [ -d "$native_modules_dir" ] && [ "$(ls -A "$native_modules_dir" 2>/dev/null)" ]; then
log_info "Native modules built successfully:"
ls -la "$native_modules_dir/"
else
log_warn "No native modules were built."
fi
}
# ============================================================================
# Step 4: Merge x64 asar with loong64 electron and native modules
# ============================================================================
merge_loong64() {
log_info "Step 4: Merging x64 asar with loong64 electron..."
local electerm_version
electerm_version=$(get_version)
mkdir -p "$OUTPUT_DIR"
local output_name="electerm-${electerm_version}-linux-loong64"
local output_dir="$OUTPUT_DIR/$output_name"
rm -rf "$output_dir"
mkdir -p "$output_dir"
# Start with x64 app directory
if [ -d "$WORK_DIR/x64-app" ]; then
cp -r "$WORK_DIR/x64-app"/* "$output_dir/"
fi
# Replace x64 binary with loong64 electron
rm -f "$output_dir/electerm"
cp "$WORK_DIR/electron-loong64/electron" "$output_dir/electerm"
chmod +x "$output_dir/electerm"
# Copy loong64 electron runtime files (libraries, locales, etc.)
for f in "$WORK_DIR/electron-loong64"/*; do
local base
base=$(basename "$f")
if [ "$base" = "electron" ]; then
continue
fi
if [ -d "$f" ] && [ "$base" != "locales" ]; then
continue
fi
if [ -d "$f" ]; then
cp -r "$f" "$output_dir/"
else
cp "$f" "$output_dir/"
fi
done
# Replace native modules with loong64 versions
local native_modules_dir="$WORK_DIR/native-modules-loong64"
if [ -d "$native_modules_dir" ] && [ "$(ls -A "$native_modules_dir" 2>/dev/null)" ]; then
log_info "Replacing native modules with loong64 versions..."
if [ -f "$native_modules_dir/node-pty.node" ]; then
find "$output_dir" -path "*/node-pty/build/Release/*.node" -exec cp "$native_modules_dir/node-pty.node" {} \; 2>/dev/null || true
fi
if [ -f "$native_modules_dir/serialport-bindings.node" ]; then
find "$output_dir" -path "*@serialport/bindings-cpp*" -name "*.node" -exec cp "$native_modules_dir/serialport-bindings.node" {} \; 2>/dev/null || true
fi
if [ -f "$native_modules_dir/ssh2-crypto.node" ]; then
find "$output_dir" -path "*@electerm/ssh2*" -name "*.node" -exec cp "$native_modules_dir/ssh2-crypto.node" {} \; 2>/dev/null || true
fi
fi
# Ensure asar is in place
if [ -f "$WORK_DIR/app.asar" ] && [ ! -f "$output_dir/resources/app.asar" ]; then
mkdir -p "$output_dir/resources"
cp "$WORK_DIR/app.asar" "$output_dir/resources/"
fi
# Create tar.gz
log_info "Creating loong64 tar.gz..."
cd "$OUTPUT_DIR"
tar czf "${output_name}.tar.gz" "$output_name"
log_info "Tar.gz complete: $OUTPUT_DIR/${output_name}.tar.gz"
}
# ============================================================================
# Step 5: Build deb package
# ============================================================================
patch_asar_install_src() {
local asar_file="$1"
local new_src="$2"
log_info "Patching asar install-src to '${new_src}'..."
local asar_bin="$PROJECT_ROOT/node_modules/@electron/asar/bin/asar.js"
local tmp_dir
tmp_dir=$(mktemp -d)
# Extract, patch, repack using CLI
node "$asar_bin" extract "$asar_file" "$tmp_dir"
echo "module.exports = '${new_src}'" > "$tmp_dir/lib/install-src.js"
node "$asar_bin" pack "$tmp_dir" "$asar_file"
rm -rf "$tmp_dir"
log_info "Asar patched successfully."
}
build_deb() {
local install_src="${1:-linux-loong64.deb}"
local deb_arch="${2:-loong64}"
local suffix="$deb_arch"
log_info "Step 5: Building ${suffix} deb package..."
local electerm_version
electerm_version=$(get_version)
local output_dir="$OUTPUT_DIR/electerm-${electerm_version}-linux-loong64"
local deb_build="$OUTPUT_DIR/deb-build-${suffix}"
local deb_name="electerm_${electerm_version}_loongarch64"
local deb_dir="$deb_build/$deb_name"
if [ ! -d "$output_dir" ]; then
log_error "Output directory not found: $output_dir"
return 1
fi
# Patch asar to use deb install-src
local asar_file="$output_dir/resources/app.asar"
if [ -f "$asar_file" ]; then
patch_asar_install_src "$asar_file" "$install_src"
else
log_warn "app.asar not found at $asar_file, skipping install-src patch"
fi
rm -rf "$deb_build"
mkdir -p "$deb_dir/DEBIAN"
mkdir -p "$deb_dir/opt/electerm"
mkdir -p "$deb_dir/usr/share/applications"
mkdir -p "$deb_dir/usr/share/icons/hicolor/128x128/apps"
cp -r "$output_dir"/* "$deb_dir/opt/electerm/"
# Install icon
local icon_src="$PROJECT_ROOT/node_modules/@electerm/electerm-resource/res/imgs/electerm-round-128x128.png"
if [ -f "$icon_src" ]; then
cp "$icon_src" "$deb_dir/usr/share/icons/hicolor/128x128/apps/electerm.png"
fi
# Install desktop file
cat > "$deb_dir/usr/share/applications/electerm.desktop" <<'DESKTOP'
[Desktop Entry]
Name=electerm
Comment=Terminal/SSH/SFTP client
Exec=/opt/electerm/electerm %U
Icon=electerm
Terminal=false
Type=Application
Categories=Development;System;TerminalEmulator;
StartupWMClass=electerm
MimeType=x-scheme-handler/ssh;x-scheme-handler/telnet;x-scheme-handler/rdp;x-scheme-handler/vnc;x-scheme-handler/serial;x-scheme-handler/spice;x-scheme-handler/electerm;
DESKTOP
cat > "$deb_dir/DEBIAN/control" <<CTRL
Package: electerm
Version: ${electerm_version}
Section: utils
Priority: optional
Architecture: ${deb_arch}
Depends: libglib2.0-0, libnss3, libnspr4, libdbus-1-3, libatk1.0-0, libatk-bridge2.0-0, libcups2, libcairo2, libpango-1.0-0, libx11-6, libxcomposite1, libxdamage1, libxext6, libxfixes3, libxrandr2, libxkbcommon0, libdrm2, libgbm1, libatspi2.0-0, libpulse0, libgtk-3-0
Maintainer: ZHAO Xudong <zxdong@gmail.com>
Description: Open-sourced terminal/ssh/sftp/telnet/serialport/RDP/VNC/Spice/ftp client
CTRL
cat > "$deb_dir/DEBIAN/postinst" <<'POSTINST'
#!/bin/bash
chown root:root /opt/electerm/chrome-sandbox
chmod 4755 /opt/electerm/chrome-sandbox
update-desktop-database /usr/share/applications/ 2>/dev/null || true
gtk-update-icon-cache /usr/share/icons/hicolor/ 2>/dev/null || true
POSTINST
chmod 755 "$deb_dir/DEBIAN/postinst"
local deb_file="$OUTPUT_DIR/electerm-${electerm_version}-linux-${suffix}.deb"
if command -v fakeroot &>/dev/null; then
fakeroot dpkg-deb --build "$deb_dir" "$deb_file"
else
dpkg-deb --build "$deb_dir" "$deb_file"
fi
if [ ! -f "$deb_file" ]; then
log_error "Deb build failed: $deb_file not created"
return 1
fi
log_info "Deb complete: $deb_file"
rm -rf "$deb_build"
}
# ============================================================================
# Step 6: Upload to GitHub release draft
# ============================================================================
upload_to_github() {
local file="$1"
local filename
filename=$(basename "$file")
if [ ! -f "$file" ]; then
log_warn "Upload skipped: file not found: $file"
return 0
fi
if [ -z "${GH_TOKEN:-}" ]; then
log_warn "Upload skipped: GH_TOKEN not set"
return 0
fi
local electerm_version
electerm_version=$(get_version)
local release_name="v${electerm_version}"
log_info "Uploading $filename to GitHub release draft '${release_name}'..."
if ! gh release view "$release_name" >/dev/null 2>&1; then
gh release create "$release_name" \
--draft \
--title "$release_name" \
--generate-notes \
"$file"
else
gh release upload "$release_name" "$file" --clobber
fi
log_info "Uploaded $filename."
}
# ============================================================================
# Main
# ============================================================================
main() {
log_info "Starting electerm loong64 build..."
log_info "Project root: $PROJECT_ROOT"
log_info "Work directory: $WORK_DIR"
log_info "Output directory: $OUTPUT_DIR"
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
install_prerequisites
build_x64
download_electron_loong64
rebuild_native_modules
merge_loong64
local electerm_version
electerm_version=$(get_version)
# Upload tar.gz
upload_to_github "$OUTPUT_DIR/electerm-${electerm_version}-linux-loong64.tar.gz"
# Build and upload debs
build_deb "linux-loong64.deb" "loong64"
upload_to_github "$OUTPUT_DIR/electerm-${electerm_version}-linux-loong64.deb"
build_deb "linux-loongarch64.deb" "loongarch64"
upload_to_github "$OUTPUT_DIR/electerm-${electerm_version}-linux-loongarch64.deb"
log_info "=========================================="
log_info "Build complete!"
log_info " $OUTPUT_DIR/electerm-${electerm_version}-linux-loong64.tar.gz"
log_info " $OUTPUT_DIR/electerm-${electerm_version}-linux-loong64.deb"
log_info " $OUTPUT_DIR/electerm-${electerm_version}-linux-loongarch64.deb"
log_info "=========================================="
}
main "$@"
+29
View File
@@ -0,0 +1,29 @@
const { echo, rm } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder: pb,
patchSnapClassicSandbox
} = require('./build-common')
async function main () {
echo('running build for linux part 2')
echo('build rpm')
rm('-rf', 'dist')
let src = 'linux-x86_64.rpm'
writeSrc(src)
await run(`${pb} --linux rpm`)
await uploadToR2(src)
echo('build snap')
rm('-rf', 'dist')
src = 'linux-amd64.snap'
writeSrc(src)
patchSnapClassicSandbox()
await run(`${pb} --linux snap -p always`)
await uploadToR2(src)
}
main()
+41
View File
@@ -0,0 +1,41 @@
const { rm, echo } = require('shelljs')
const replace = require('replace-in-file')
const {
run,
writeSrc,
uploadToR2,
builder
} = require('./build-common')
const options = {
files: require('path').resolve(__dirname, '../../electron-builder.json'),
from: ['${productName}-${version}-${os}-${arch}.${ext}'], // eslint-disable-line
to: ['${productName}-${version}-${os}10-${arch}.${ext}'] // eslint-disable-line
}
function replaceRun () {
return new Promise((resolve, reject) => {
replace(options, (err) => {
if (err) {
return reject(err)
}
console.log('start build mac10 file')
resolve()
})
})
}
async function main () {
const pb = builder
echo('running build for mac10 (macOS 10.15 Catalina compatible)')
echo('build dmg')
await replaceRun()
rm('-rf', 'dist')
const src = 'mac10-x64.dmg'
writeSrc(src)
await run(`${pb} --mac --x64`)
await uploadToR2(src)
}
main()
+22
View File
@@ -0,0 +1,22 @@
const { echo, rm } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder: pb,
reBuild
} = require('./build-common')
async function main () {
echo('running build for mac arm')
echo('build dmg')
const src = 'mac-arm64.dmg'
rm('-rf', 'dist')
writeSrc(src)
await run(`${reBuild} --arch arm64 -f work/app`)
await run(`${pb} --mac --arm64`)
await uploadToR2(src)
}
main()
+28
View File
@@ -0,0 +1,28 @@
// const cmd = 'rm -rf dist && ' +
// 'echo "build x64 mac" && ' +
// './node_modules/.bin/electron-builder --mac --x64'
// 'echo "build arm64 mac" && ' +
// './node_modules/.bin/electron-rebuild --arch arm64 -f -p work/app && ' +
// './node_modules/.bin/electron-builder --mac --arm64'
// const cmd = './node_modules/.bin/electron-builder --mac'
const { echo, rm } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder: pb
} = require('./build-common')
async function main () {
echo('running build for mac')
echo('build dmg')
const src = 'mac-x64.dmg'
rm('-rf', 'dist')
writeSrc(src)
await run(`${pb} --mac`)
await uploadToR2(src)
}
main()
+25
View File
@@ -0,0 +1,25 @@
const { rm, echo } = require('shelljs')
const { resolve } = require('path')
const fs = require('fs')
const {
run,
writeSrc,
uploadToR2,
builder
} = require('./build-common')
async function main () {
const pb = builder
echo('build appx')
const p = resolve(__dirname, '../../electron-builder.json')
const txt = require('../../electron-builder.json')
delete txt.electronLanguages
fs.writeFileSync(p, JSON.stringify(txt, null, 2))
rm('-rf', 'dist')
const src = 'win-x64.appx'
writeSrc(src)
await run(`${pb} --win appx`)
await uploadToR2(src)
}
main()
+28
View File
@@ -0,0 +1,28 @@
const { echo, rm } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder: pb,
reBuild,
replaceJSON
} = require('./build-common')
async function main () {
echo('running build for Windows ARM64')
echo('build tar.gz for Windows ARM64')
const src = 'win-arm64.tar.gz'
rm('-rf', 'dist')
writeSrc(src)
replaceJSON(
(data) => {
data.win.target = ['tar.gz']
}
)
await run(`${reBuild} --arch arm64 -f work/app`)
await run(`${pb} --win --arm64`)
await uploadToR2(src)
}
main()
+21
View File
@@ -0,0 +1,21 @@
const { rm, echo } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder,
replaceRun
} = require('./build-common')
async function main () {
const pb = builder
echo('build loose tar.gz')
await replaceRun()
rm('-rf', 'dist')
const src = 'win-x64-loose.tar.gz'
writeSrc(src)
await run(`${pb} --win tar.gz`)
await uploadToR2(src)
}
main()
+32
View File
@@ -0,0 +1,32 @@
const { echo, rm } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder: pb,
reBuild,
replaceJSON,
patchNsisKeepShortcuts
} = require('./build-common')
async function main () {
echo('running build for Windows ARM64')
patchNsisKeepShortcuts()
echo('build tar.gz for Windows ARM64')
const src = 'win-arm64-installer.exe'
rm('-rf', 'dist')
writeSrc(src)
replaceJSON(
(data) => {
data.win.target = ['nsis']
data.afterAllArtifactBuild = 'build/bin/clean-yml.js'
}
)
await run(`${reBuild} --arch arm64 -f work/app`)
await run(`${pb} --win --arm64`)
await uploadToR2(src)
}
main()
+24
View File
@@ -0,0 +1,24 @@
const { rm, echo } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder,
patchNsisKeepShortcuts
} = require('./build-common')
async function main () {
const pb = builder
echo('running build for win part nsis installer')
patchNsisKeepShortcuts()
echo('build nsis')
const src = 'win-x64-installer.exe'
rm('-rf', 'dist')
writeSrc(src)
await run(`${pb} --win nsis`)
await uploadToR2(src)
}
main()
+40
View File
@@ -0,0 +1,40 @@
const replace = require('replace-in-file')
const { rm, echo } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder
} = require('./build-common')
const options = {
files: require('path').resolve(__dirname, '../../electron-builder.json'),
from: ['${productName}-${version}-${os}-${arch}.${ext}', ', "appx", "nsis"'], // eslint-disable-line
to: ['${productName}-${version}-${os}-${arch}-portable.${ext}', ''] // eslint-disable-line
}
function replaceRun () {
return new Promise((resolve, reject) => {
replace(options, (err) => {
if (err) {
return reject(err)
}
console.log('start build portable file')
resolve()
})
})
}
async function main () {
const pb = builder
echo('running build for win part 3')
echo('build portable tar.gz')
await replaceRun()
rm('-rf', 'dist')
const src = 'win-x64-portable.tar.gz'
writeSrc(src)
await run(`${pb} --win tar.gz`)
await uploadToR2(src)
}
main()
+21
View File
@@ -0,0 +1,21 @@
const { rm, echo } = require('shelljs')
const {
run,
writeSrc,
uploadToR2,
builder
} = require('./build-common')
async function main () {
const pb = builder
echo('running build for win part 1')
echo('build tar.gz')
rm('-rf', 'dist')
const src = 'win-x64.tar.gz'
writeSrc(src)
await run(`${pb} --win tar.gz`)
await uploadToR2(src)
}
main()
+39
View File
@@ -0,0 +1,39 @@
const { rm, echo } = require('shelljs')
const replace = require('replace-in-file')
const {
run,
writeSrc,
uploadToR2,
builder
} = require('./build-common')
const options = {
files: require('path').resolve(__dirname, '../../electron-builder.json'),
from: ['${productName}-${version}-${os}-${arch}.${ext}', ', "appx", "nsis"'], // eslint-disable-line
to: ['${productName}-${version}-${os}7.${ext}', ''] // eslint-disable-line
}
function replaceRun () {
return new Promise((resolve, reject) => {
replace(options, (err) => {
if (err) {
return reject(err)
}
console.log('start build win7 file')
resolve()
})
})
}
async function main () {
const pb = builder
echo('running build for win7')
echo('build tar.gz')
await replaceRun()
rm('-rf', 'dist')
const src = 'win7.tar.gz'
writeSrc(src)
await run(`${pb} --win tar.gz`)
await uploadToR2(src)
}
main()
+22
View File
@@ -0,0 +1,22 @@
/**
* build
*/
const { exec, echo } = require('shelljs')
echo('start build')
const timeStart = +new Date()
// echo('clean')
// exec('npm run clean')
echo('version file')
echo('js/css file')
exec('npm run vite-build')
echo('copy file')
exec('node ./build/bin/copy.js')
echo('html file')
exec('node ./build/bin/pug.js')
const endTime = +new Date()
echo(`done build in ${(endTime - timeStart) / 1000} s`)
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
/**
* Clean empty folders recursively in a given directory
* @param {string} dirPath - The directory path to clean
* @returns {number} - Number of empty folders removed
*/
function cleanEmptyFolders (dirPath) {
let removedCount = 0
if (!fs.existsSync(dirPath)) {
console.log('Directory does not exist:', dirPath)
return removedCount
}
try {
const items = fs.readdirSync(dirPath)
// First, recursively clean subdirectories
for (const item of items) {
const itemPath = path.join(dirPath, item)
const stats = fs.statSync(itemPath)
if (stats.isDirectory()) {
removedCount += cleanEmptyFolders(itemPath)
}
}
// After cleaning subdirectories, check if current directory is now empty
const remainingItems = fs.readdirSync(dirPath)
if (remainingItems.length === 0) {
// Don't remove the root node_modules directory itself
const nodeModulesPath = path.resolve(process.cwd(), 'work/app/node_modules')
if (path.resolve(dirPath) !== nodeModulesPath) {
console.log('Removing empty directory:', dirPath)
fs.rmdirSync(dirPath)
removedCount++
}
}
} catch (error) {
console.error('Error processing directory ' + dirPath + ':', error.message)
}
return removedCount
}
/**
* Main function to clean empty folders in work/app/node_modules
*/
function main () {
const targetDir = path.resolve(process.cwd(), 'work/app/node_modules')
console.log('Starting cleanup of empty folders in:', targetDir)
console.log('='.repeat(60))
if (!fs.existsSync(targetDir)) {
console.log('Target directory does not exist:', targetDir)
return
}
const startTime = Date.now()
const removedCount = cleanEmptyFolders(targetDir)
const endTime = Date.now()
console.log('='.repeat(60))
console.log('Cleanup completed!')
console.log('Empty folders removed:', removedCount)
console.log('Time taken:', endTime - startTime + 'ms')
if (removedCount === 0) {
console.log('No empty folders found.')
}
}
// Run the script if called directly
if (require.main === module) {
main()
}
module.exports = { cleanEmptyFolders, main }
+16
View File
@@ -0,0 +1,16 @@
// delete-yml.js
const fs = require('fs')
const path = require('path')
exports.default = async function (context) {
// context.outDir is the path to the build output directory
const ymlFiles = fs.readdirSync(context.outDir).filter(file => {
return path.extname(file) === '.yml'
})
ymlFiles.forEach(file => {
const filePath = path.join(context.outDir, file)
console.log(`Deleting auto-update file: ${filePath}`)
fs.unlinkSync(filePath)
})
}
+5
View File
@@ -0,0 +1,5 @@
const { rm } = require('shelljs')
rm('-rf', [
'work'
])
+39
View File
@@ -0,0 +1,39 @@
const { resolve } = require('path')
const { cp } = require('shelljs')
const from = resolve(
__dirname,
'../../node_modules/@electerm/electerm-resource/tray-icons/*'
)
const from0 = resolve(
__dirname,
'../../node_modules/electerm-icons/icons'
)
const to1 = resolve(
__dirname,
'../../work/app/assets/images/'
)
const to2 = resolve(
__dirname,
'../../work/app/assets/icons'
)
const arr = [
{
from,
to: to1,
file: true
}, {
from: from0,
to: to2
}
]
for (const obj of arr) {
const {
file, from, to
} = obj
if (file) {
cp(from, to)
} else {
cp('-r', from, to)
}
}
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
chown root:root /opt/electerm/chrome-sandbox
chmod 4755 /opt/electerm/chrome-sandbox
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
cd `dirname $0`
cd ../..
git co master
git pull
git pull
git delete-branch test-npm
git create-branch test-npm
git push origin test-npm -u
git co -
+42
View File
@@ -0,0 +1,42 @@
/**
* post install script
*/
const { cp, exec, rm } = require('shelljs')
const { existsSync } = require('fs')
const { resolve } = require('path')
const prePushPath = resolve(__dirname, '../../.git/hooks/pre-push')
const prePushPathFrom = resolve(__dirname, 'pre-push')
const os = require('os')
const platform = os.platform()
const isWin = platform === 'win32'
// const rest = ''
if (isWin && process.env.CI) {
exec('npm cache clear -f')
exec('npm uninstall node-gyp -g')
exec('npm install node-gyp -g')
}
// Remove optional native module that may fail to rebuild
try {
// Check multiple potential locations for cpu-features
const cpuFeaturesPaths = [
resolve(__dirname, '../../node_modules/cpu-features'),
resolve(__dirname, '../../work/app/node_modules/cpu-features')
]
cpuFeaturesPaths.forEach(cpuFeaturesPath => {
if (existsSync(cpuFeaturesPath)) {
rm('-rf', cpuFeaturesPath)
console.log('Removed optional module:', cpuFeaturesPath)
}
})
} catch (e) {
console.warn('Failed to remove cpu-features:', e?.message || e)
}
exec(resolve('./node_modules/.bin/electron-rebuild'))
if (!existsSync(prePushPath)) {
cp(prePushPathFrom, prePushPath)
}
+7
View File
@@ -0,0 +1,7 @@
const { resolve } = require('path')
const { cp, rm } = require('shelljs')
const from = resolve(__dirname, '../../package.json')
const to = resolve(__dirname, '../../package-bak.json')
cp(to, from)
rm(to)
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
cd `dirname $0`
cd ../../
npm run lint
+3
View File
@@ -0,0 +1,3 @@
const { cp } = require('shelljs')
cp('-r', 'build/electron-builder.json', './')
+111
View File
@@ -0,0 +1,111 @@
/**
* prepare the files to be packed
*/
const pack = require('../../package.json')
const os = require('os')
const { resolve } = require('path')
const { version } = pack
const { mkdir, rm, exec, echo, cp } = require('shelljs')
const dir = 'dist/v' + version
const cwd = process.cwd()
const platform = os.platform()
const isWin = platform === 'win32'
pack.main = 'app.js'
delete pack.scripts
delete pack.standard
delete pack.files
delete pack.engines
delete pack.preferGlobal
if (isWin) {
delete pack.dependencies['node-bash']
} else {
delete pack.dependencies['node-powershell']
}
echo('start pack prepare')
// echo('install test deps')
// exec(`PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i -D -E playwright@1.28.1 --no-save && npm i -D -E @playwright/test@1.28.1 --no-save`)
const timeStart = +new Date()
rm('-rf', dir)
rm('-rf', 'dist/latest')
mkdir('-p', dir)
mkdir('-p', 'dist/latest')
cp('-r', 'src/app', 'work/')
rm('-rf', 'work/app/user-config.json')
rm('-rf', 'work/app/localstorage.json')
rm('-rf', 'work/app/nohup.out')
rm('-rf', 'work/app/assets/js/index*')
rm('-rf', 'work/app/assets/js/*.txt')
rm('-rf', 'node_modules/cpu-features')
require('fs').writeFileSync(
resolve(__dirname, '../../work/app/package.json'),
JSON.stringify(
pack, null, 2
)
)
exec(`cd work/app && npm i --omit=dev && cd ${cwd}`)
rm('-rf', 'work/app/node_modules/.bin')
// Remove axios browser/ESM builds and unnecessary files (keep only lib/ and node CJS)
rm('-rf', 'work/app/node_modules/axios/dist/esm')
rm('-rf', 'work/app/node_modules/axios/dist/browser')
rm('-rf', 'work/app/node_modules/axios/dist/*.js')
rm('-rf', 'work/app/node_modules/axios/dist/*.map')
rm('-rf', 'work/app/node_modules/axios/dist/node/*.map')
rm('-rf', 'work/app/node_modules/axios/index.d.cts')
rm('-rf', 'work/app/node_modules/axios/lib')
// Remove cpu-features after npm prune to prevent rebuild issues
rm('-rf', 'node_modules/cpu-features')
rm('-rf', 'work/app/node_modules/cpu-features')
// Clean up node-pty platform-specific files to reduce bundle size
if (isWin) {
// On Windows, remove Unix-specific files
rm('-rf', 'work/app/node_modules/node-pty/lib/unixTerminal.js')
rm('-rf', 'work/app/node_modules/node-pty/lib/unixTerminal.js.map')
rm('-rf', 'work/app/node_modules/node-pty/lib/unixTerminal.test.js')
rm('-rf', 'work/app/node_modules/node-pty/lib/unixTerminal.test.js.map')
rm('-rf', 'work/app/node_modules/node-pty/build/pty.target.mk')
rm('-rf', 'work/app/node_modules/node-pty/build/spawn-helper.target.mk')
rm('-rf', 'work/app/node_modules/node-pty/build/binding.Makefile')
rm('-rf', 'work/app/node_modules/node-pty/build/gyp-mac-tool')
} else {
// On Linux/Mac, remove Windows-specific files
rm('-rf', 'work/app/node_modules/node-pty/lib/conpty_console_list_agent.js')
rm('-rf', 'work/app/node_modules/node-pty/lib/conpty_console_list_agent.js.map')
rm('-rf', 'work/app/node_modules/node-pty/lib/windowsConoutConnection.js')
rm('-rf', 'work/app/node_modules/node-pty/lib/windowsConoutConnection.js.map')
rm('-rf', 'work/app/node_modules/node-pty/lib/windowsPtyAgent.js')
rm('-rf', 'work/app/node_modules/node-pty/lib/windowsPtyAgent.js.map')
rm('-rf', 'work/app/node_modules/node-pty/lib/windowsPtyAgent.test.js')
rm('-rf', 'work/app/node_modules/node-pty/lib/windowsPtyAgent.test.js.map')
rm('-rf', 'work/app/node_modules/node-pty/lib/windowsTerminal.js')
rm('-rf', 'work/app/node_modules/node-pty/lib/windowsTerminal.js.map')
rm('-rf', 'work/app/node_modules/node-pty/lib/windowsTerminal.test.js')
rm('-rf', 'work/app/node_modules/node-pty/lib/windowsTerminal.test.js.map')
rm('-rf', 'work/app/node_modules/node-pty/deps/winpty')
}
// Remove all test files from node-pty to reduce bundle size
rm('-rf', 'work/app/node_modules/node-pty/lib/*.test.js')
rm('-rf', 'work/app/node_modules/node-pty/lib/*.test.js.map')
rm('-rf', 'work/app/node_modules/node-pty/lib/testUtils.test.js')
rm('-rf', 'work/app/node_modules/node-pty/lib/testUtils.test.js.map')
// yarn auto clean
cp('-r', 'build/bin/.yarnclean', 'work/app/')
exec(`cd work/app && yarn generate-lock-entry > yarn.lock && yarn autoclean --force && cd ${cwd}`)
rm('-rf', 'work/app/.yarnclean')
rm('-rf', 'work/app/package-lock.json')
rm('-rf', 'work/app/yarn.lock')
require('./clean-empty-folders').main()
const endTime = +new Date()
echo(`done pack prepare in ${(endTime - timeStart) / 1000} s`)
+28
View File
@@ -0,0 +1,28 @@
const savedPackage = [
'shelljs',
'tar'
]
const pack = require('../../package.json')
const fs = require('fs')
const { resolve } = require('path')
delete pack.devDependencies
pack.dependencies = savedPackage.reduce((prev, p) => {
prev[p] = '*'
return prev
}, {})
pack.scripts = {
postinstall: 'node npm/install.js',
postpublish: 'node bin/postpublish.js'
}
delete pack.langugeRepo
delete pack.privacyNoticeLink
delete pack.knownIssuesLink
delete pack.sponsorLink
delete pack.releases
delete pack.standard
const from = resolve(__dirname, '../../package.json')
const to = resolve(__dirname, '../../package-bak.json')
fs.copyFileSync(from, to)
fs.unlinkSync(from)
fs.writeFileSync(from, JSON.stringify(pack, null, 2))
+16
View File
@@ -0,0 +1,16 @@
const { rm, echo, cd, cp, exec } = require('shelljs')
const { resolve } = require('path')
const fs = require('fs')
const pack = require('../../package.json')
const tpack = require('../web-app/package.json')
tpack.version = pack.version
const to = resolve(__dirname, '../web-app/')
const from = resolve(__dirname, '../../src/client')
echo('start build electerm-react pack and publish')
fs.writeFileSync(resolve(to, 'package.json'), JSON.stringify(tpack, null, 2))
rm('-rf', resolve(to, 'client'))
cp('-r', from, to)
cd(to)
exec(`npm-publish --token ${process.env.token} --access public`)
+40
View File
@@ -0,0 +1,40 @@
// build html
/**
* build common files with react module in it
*/
const fs = require('fs')
const pug = require('pug')
const { resolve } = require('path')
const pack = require('../../package.json')
const deepCopy = require('json-deep-copy')
const entryPug = resolve(
__dirname,
'../../src/client/views/index.pug'
)
const targetFilePath = resolve(
__dirname,
'../../work/app/assets/index.html'
)
const pugContent = fs.readFileSync(entryPug, 'utf-8')
const defaultAIPreset = {
baseURLAI: 'https://ai.electerm.org/api/ai',
apiPathAI: '/chat/completions',
modelAI: 'mistral-small-latest',
authHeaderNameAI: 'Authorization: Bearer',
id: 'ai.electerm.org',
nameAI: 'ai.electerm.org(default free)'
}
const data = {
version: pack.version,
siteName: pack.name,
isDev: false,
defaultAIPreset
}
const htmlContent = pug.render(pugContent, {
filename: entryPug,
...data,
_global: deepCopy(data)
})
fs.writeFileSync(targetFilePath, htmlContent, 'utf8')
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
cd `dirname $0`
cd ../..
git co master
git pull
git pull
git delete-branch build
git create-branch build
git push origin build -u
git co -
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
const { exec, cd } = require('shelljs')
const { resolve } = require('path')
const p = resolve(__dirname, '../vite')
cd(p)
exec('npm start')
+53
View File
@@ -0,0 +1,53 @@
const fs = require('fs')
const path = require('path')
async function main () {
const filePath = process.argv[2]
const key = process.argv[3] || path.basename(filePath || '')
if (!filePath) {
throw new Error('Usage: node build/bin/upload-file-to-r2.js <filePath> [key]')
}
if (!fs.existsSync(filePath)) {
throw new Error(`File does not exist: ${filePath}`)
}
const accountId = process.env.CF_R2_ACCOUNT_ID
const bucket = process.env.CF_R2_BUCKET
const accessKeyId = process.env.CF_R2_ACCESS_KEY_ID
const secretAccessKey = process.env.CF_R2_SECRET_ACCESS_KEY
if (!accountId || !bucket || !accessKeyId || !secretAccessKey) {
throw new Error('Missing required env vars: CF_R2_ACCOUNT_ID, CF_R2_BUCKET, CF_R2_ACCESS_KEY_ID, CF_R2_SECRET_ACCESS_KEY')
}
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3')
const client = new S3Client({
region: 'auto',
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId,
secretAccessKey
}
})
const fileStream = fs.createReadStream(filePath)
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: fileStream,
ContentLength: fs.statSync(filePath).size,
ContentType: 'text/plain; charset=utf-8',
CacheControl: 'no-store'
})
await client.send(command)
console.log(`[r2] Uploaded ${filePath} to ${bucket}/${key}`)
}
main().catch(err => {
console.error(err)
process.exit(1)
})
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
const { exec, cd } = require('shelljs')
const { resolve } = require('path')
const p = resolve(__dirname, '../vite')
cd(p)
exec('npm run build')
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
cd `dirname $0`
cd ../..
git co master
git pull
git pull
git delete-branch electerm-react
git create-branch electerm-react
git push origin electerm-react -u
git co -
+137
View File
@@ -0,0 +1,137 @@
{
"appId": "org.electerm.electerm",
"directories": {
"app": "work/app",
"output": "dist",
"buildResources": "node_modules/@electerm/electerm-resource/build-res"
},
"electronLanguages": [
"en-US"
],
"asar": true,
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
"win": {
"target": [
"tar.gz",
"appx",
"nsis"
],
"icon": "node_modules/@electerm/electerm-resource/build/icons-win.ico",
"publish": {
"provider": "github",
"channel": "${env.WORKFLOW_NAME}"
}
},
"protocols": [
{
"name": "SSH Protocol",
"schemes": [
"ssh"
]
},
{
"name": "Telnet Protocol",
"schemes": [
"telnet"
]
},
{
"name": "RDP Protocol",
"schemes": [
"rdp"
]
},
{
"name": "VNC Protocol",
"schemes": [
"vnc"
]
},
{
"name": "Serial Protocol",
"schemes": [
"serial"
]
},
{
"name": "Spice Protocol",
"schemes": [
"spice"
]
},
{
"name": "Electerm Protocol",
"schemes": [
"electerm"
]
}
],
"appx": {
"applicationId": "electerm",
"identityName": "64159ZHAOXudong.electerm",
"publisher": "CN=9F481654-170A-434C-8259-984D2EB1FD7F",
"publisherDisplayName": "ZHAO Xudong"
},
"nsis": {
"oneClick": false,
"artifactName": "${productName}-${version}-${os}-${arch}-installer.${ext}",
"allowToChangeInstallationDirectory": true
},
"mac": {
"category": "public.app-category.developer-tools",
"target": "dmg",
"icon": "node_modules/@electerm/electerm-resource/build/icons.icns",
"publish": {
"provider": "github",
"channel": "${env.WORKFLOW_NAME}"
},
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "./build/entitlements.plist",
"entitlementsInherit": "./build/entitlements.plist",
"notarize": true
},
"dmg": {
"contents": [
{
"x": 110,
"y": 190
},
{
"x": 440,
"y": 190,
"type": "link",
"path": "/Applications"
}
]
},
"linux": {
"category": "Development;System;TerminalEmulator",
"icon": "node_modules/@electerm/electerm-resource/res/imgs",
"target": [
"rpm",
"deb",
"tar.gz",
"snap"
],
"publish": {
"provider": "github",
"channel": "${env.WORKFLOW_NAME}"
},
"desktop": {
"entry": {
"StartupWMClass": "electerm"
}
}
},
"snap": {
"confinement": "classic",
"publish": {
"provider": "github",
"channel": "${env.WORKFLOW_NAME}"
}
},
"deb": {
"afterInstall": "build/bin/linux-deb-after-build.sh"
}
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>
+5
View File
@@ -0,0 +1,5 @@
# run `cp .sample.env .env` to create your local env
## development server config
DEV_HOST=127.0.0.1
DEV_PORT=5570
+31
View File
@@ -0,0 +1,31 @@
import { config as conf } from 'dotenv'
import { readFileSync } from 'fs'
import { resolve } from 'path'
conf()
export const cwd = process.cwd()
export const env = process.env
export const isProd = env.NODE_ENV === 'production'
const packPath = resolve(cwd, '../../package.json')
export const pack = JSON.parse(readFileSync(packPath).toString())
export const version = pack.version
export const viewPath = resolve(cwd, '../../src/client/views')
export const staticPaths = [
{
dir: resolve(cwd, '../../node_modules/electerm-icons/icons'),
path: '/icons'
},
{
dir: resolve(cwd, '../../node_modules/@electerm/electerm-resource/tray-icons'),
path: '/images'
},
{
dir: resolve(cwd, '../../node_modules/@electerm/electerm-resource/res/imgs'),
path: '/images'
},
{
dir: resolve(cwd, '../../src/client/entry'),
path: '/'
}
]
+87
View File
@@ -0,0 +1,87 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// import htmlPurge from 'vite-plugin-purgecss'
import { cwd, version } from './common.js'
import { resolve } from 'path'
import def from './def.js'
function buildInput () {
return {
electerm: resolve(cwd, '../../src/client/entry/electerm.jsx'),
basic: resolve(cwd, '../../src/client/entry/basic.js'),
worker: resolve(cwd, '../../src/client/entry/worker.js')
}
}
// Custom plugin to replace window.et.isWebApp with false
function replaceWebAppPlugin () {
return {
name: 'replace-webapp',
renderChunk (code, chunk) {
// Replace window.et.isWebApp with false in the bundled code
const newCode = code.replace(/window\.et\.isWebApp/g, 'false')
if (newCode !== code) {
return {
code: newCode,
map: null
}
}
return null
}
}
}
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react({ include: /\.(mdx|js|jsx|ts|tsx|mjs)$/ }),
replaceWebAppPlugin()
],
resolve: {
alias: {
'ironrdp-wasm': resolve(cwd, '../../node_modules/ironrdp-wasm/pkg/rdp_client.js'),
// @xterm/addon-ligatures bundles lru-cache@11, which calls
// channel()/tracingChannel() from node:diagnostics_channel at import time.
// In the renderer (browser) context Vite stubs Node builtins and the call
// throws. lru-cache only uses it for optional metrics, so a no-op stub is
// safe. Covers both bare `diagnostics_channel` and the `node:` prefix.
'node:diagnostics_channel': resolve(cwd, './diagnostics-channel-stub.js'),
diagnostics_channel: resolve(cwd, './diagnostics-channel-stub.js')
}
},
optimizeDeps: {
exclude: ['ironrdp-wasm']
},
define: def,
publicDir: false,
legacy: {
inconsistentCjsInterop: true
},
root: resolve(cwd, '../..'),
build: {
target: 'esnext',
cssCodeSplit: false,
codeSplitting: false,
emptyOutDir: false,
outDir: resolve(cwd, '../../work/app/assets'),
rollupOptions: {
input: buildInput(),
output: {
format: 'esm',
entryFileNames: `js/[name]-${version}.js`,
chunkFileNames: `chunk/[name]-${version}-[hash].js`,
dir: resolve(cwd, '../../work/app/assets'),
assetFileNames: chunkInfo => {
const { name } = chunkInfo
if (/\.(png|jpe?g|gif|svg|webp|ico|bmp)$/i.test(name)) {
return `images/${name}`
} else if (name && name.endsWith('.css')) {
return `css/style-${version}[extname]`
} else {
return 'assets/[name]-[hash][extname]'
}
}
}
}
}
})
+6
View File
@@ -0,0 +1,6 @@
import { version } from './common.js'
export default {
'process.env.VER': JSON.stringify(version),
__DEFINES__: JSON.stringify('some value')
}
+150
View File
@@ -0,0 +1,150 @@
import logger from 'morgan'
import { viewPath, env, staticPaths, pack, isProd, cwd } from './common.js'
import express from 'express'
import { createServer as createViteServer } from 'vite'
import conf from './conf.js'
import copy from 'json-deep-copy'
import fs from 'fs'
import path from 'path'
import { spawn } from 'child_process'
import multer from 'multer'
const devPort = env.DEV_PORT || 5570
const host = env.DEV_HOST || '127.0.0.1'
const h = `http://${host}:${devPort}`
const defaultAIPreset = {
baseURLAI: 'https://ai.electerm.org/api/ai',
apiPathAI: '/chat/completions',
modelAI: 'mistral-small-latest',
authHeaderNameAI: 'Authorization: Bearer',
id: 'ai.electerm.org',
nameAI: 'ai.electerm.org(default free)'
}
const base = {
version: pack.version,
isDev: !isProd,
siteName: pack.name,
defaultAIPreset
}
function handleIndex (req, res) {
const view = 'index'
res.render(view, {
...base,
_global: copy(base)
})
}
function redirect (req, res) {
const {
name
} = req.params
const mapper = {
electerm: '/src/client/entry/electerm.jsx',
worker: '/src/client/entry/worker.js'
}
res.redirect(mapper[name])
}
async function createServer () {
const app = express()
// Create Vite server in middleware mode and configure the app type as
// 'custom', disabling Vite's own HTML serving logic so parent server
// can take control
const vite = await createViteServer({
...conf,
server: {
middlewareMode: true,
hmr: {
port: 30085,
overlay: true
}
},
appType: 'custom'
})
app.use(
logger('dev')
)
app.use(express.json())
app.use(express.urlencoded({
extended: true
}))
staticPaths.forEach(({ path, dir }) => {
app.use(
path,
express.static(dir, { maxAge: '170d' })
)
})
const upload = multer({ dest: 'uploads/' })
app.get('/api/download', (req, res) => {
const filePath = req.query.path
if (!filePath) {
return res.status(400).json({ error: 'path is required' })
}
try {
const stat = fs.statSync(filePath)
if (stat.isFile()) {
const fileName = path.basename(filePath)
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`)
res.setHeader('Content-Type', 'application/octet-stream')
fs.createReadStream(filePath).pipe(res)
} else if (stat.isDirectory()) {
const dirName = path.basename(filePath)
const parentDir = path.dirname(filePath)
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(dirName)}.tar.gz"`)
res.setHeader('Content-Type', 'application/gzip')
const tar = spawn('tar', ['czf', '-', '-C', parentDir, dirName])
tar.stdout.pipe(res)
tar.stderr.on('data', (data) => {
console.error('tar stderr:', data.toString())
})
tar.on('error', (err) => {
console.error('tar error:', err)
if (!res.headersSent) {
res.status(500).json({ error: err.message })
}
})
} else {
res.status(400).json({ error: 'path is not a file or directory' })
}
} catch (err) {
console.error('download error:', err)
res.status(500).json({ error: err.message })
}
})
app.post('/api/upload', upload.single('file'), (req, res) => {
const targetDir = req.body.path
if (!targetDir || !req.file) {
return res.status(400).json({ error: 'path and file are required' })
}
try {
const destPath = path.join(targetDir, req.file.originalname)
fs.renameSync(req.file.path, destPath)
res.json({ success: true, path: destPath })
} catch (err) {
console.error('upload error:', err)
res.status(500).json({ error: err.message })
}
})
app.set('views', viewPath)
app.set('view engine', 'pug')
// Use vite's connect instance as middleware. If you use your own
// express router (express.Router()), you should use router.use
app.use(vite.middlewares)
app.get(['/', '/index.html'], handleIndex)
app.get('/:dir/:name.:ext', redirect)
app.listen(devPort, host, () => {
console.log('cwd:', cwd)
console.log(`server started at ${h}`)
})
}
createServer()
+55
View File
@@ -0,0 +1,55 @@
// Browser-safe stub for Node's `node:diagnostics_channel` module.
//
// Why this exists:
// @xterm/addon-ligatures (beta line) bundles lru-cache@11, which imports
// `channel`/`tracingChannel` from `node:diagnostics_channel` and calls them at
// module-load time (for optional metrics). In the Electron renderer / Vite dev
// server this is a browser context: Vite stubs Node builtins with a
// `browser-external` shim that throws on use, so `channel()` is undefined and
// the addon crashes on import.
//
// lru-cache only publishes metrics when `hasSubscribers` is true, which never
// happens here (we never subscribe). So all of these can be silent no-ops.
function makeChannel () {
return {
publish () {},
subscribe () {
return () => {}
},
unsubscribe () {},
bindStore (store) {
return store
},
unbindStore () {},
hasSubscribers: false
}
}
export function channel () {
return makeChannel()
}
export function tracingChannel () {
const ch = makeChannel()
return {
start: ch,
end: ch,
asyncStart: ch,
asyncEnd: ch,
error: ch,
trace (fn) {
return fn()
}
}
}
export function hasSubscribers () {
return false
}
export default {
channel,
tracingChannel,
hasSubscribers
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "electerm",
"version": "1.29.5",
"main": "app.js",
"type": "module",
"scripts": {
"start": "npm run c",
"c": "node ./dev-server.js",
"build": "cross-env NODE_ENV=production vite build --config ./conf.js"
}
}
+2
View File
@@ -0,0 +1,2 @@
/client/views
/client/entry
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) since 2017~ ZHAO Xudong <zxdong@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+31
View File
@@ -0,0 +1,31 @@
<h1 align="center" style="padding-top: 60px;padding-bottom: 40px;">
<a href="https://electerm.github.io/electerm">
<img src="https://github.com/electerm/electerm-resource/raw/master/static/images/electerm.png", alt="" />
</a>
</h1>
# electerm [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Open%20sourced%20terminal%2Fssh%2Fsftp%20client(linux%2C%20mac%2C%20win)&url=https%3A%2F%2Fgithub.com%2Felecterm%2Felecterm&hashtags=electerm,ssh,terminal,sftp)
[![GitHub version](https://img.shields.io/github/release/electerm/electerm/all.svg)](https://github.com/electerm/electerm/releases)
[![Build Status](https://github.com/electerm/electerm/actions/workflows/mac-test-1.yml/badge.svg)](https://github.com/electerm/electerm/actions)
[![license](https://img.shields.io/github/license/electerm/electerm.svg)](https://github.com/electerm/electerm/blob/master/LICENSE)
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
[![Get it from the Snap Store](https://img.shields.io/badge/Snap-Store-green)](https://snapcraft.io/electerm)
[![Get it from the Microsoft Store](https://img.shields.io/badge/Microsoft-Store-blue)](https://www.microsoft.com/store/apps/9NCN7272GTFF)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/electerm?label=Sponsors)](https://github.com/sponsors/electerm)
Open-sourced terminal/ssh/telnet/serialport/sftp client(linux, mac, win).
This is react components src for electerm
## Use
todo
## Change log
Visit [Releases](https://github.com/electerm/electerm/releases).
## License
MIT
+33
View File
@@ -0,0 +1,33 @@
{
"name": "@electerm/electerm-react",
"version": "1.34.30",
"description": "react components src for electerm",
"main": "./client/components/main/main.jsx",
"license": "MIT",
"langugeRepo": "https://github.com/electerm/electerm-locales",
"privacyNoticeLink": "https://github.com/electerm/electerm/wiki/privacy-notice",
"knownIssuesLink": "https://github.com/electerm/electerm/wiki/Know-issues",
"sponsorLink": "https://electerm.org/sponsor-electerm.html",
"repository": {
"type": "git",
"url": "git+https://github.com/electerm/electerm.git"
},
"author": {
"name": "ZHAO Xudong",
"email": "zxdong@gmail.com",
"url": "https://github.com/zxdong262"
},
"bugs": {
"url": "https://github.com/electerm/electerm/issues"
},
"homepage": "https://electerm.org",
"releases": "https://github.com/electerm/electerm/releases",
"engines": {
"node": ">=18.0.0"
},
"files": [
"client",
"README.md",
"LICENSE"
]
}