559 lines
17 KiB
JavaScript
559 lines
17 KiB
JavaScript
/**
|
|
* install electerm from binary
|
|
* After npm i -g electerm, running `electerm` command will:
|
|
* 1. Download the appropriate binary for the platform
|
|
* 2. Extract it to the package directory (electerm/)
|
|
* 3. The bash script (npm/electerm) then launches the extracted binary
|
|
*
|
|
* This script only downloads and extracts. Launching is handled by the bash script.
|
|
*/
|
|
|
|
const os = require('os')
|
|
const { resolve, join } = require('path')
|
|
const { execSync, rm, mv } = require('shelljs')
|
|
const { execFile } = require('child_process')
|
|
const fs = require('fs')
|
|
const { phin, download, extractTarGz, GITHUB_PROXY, applyProxy } = require('./utils')
|
|
|
|
const plat = os.platform()
|
|
const arch = os.arch()
|
|
const { homepage } = require('../package.json')
|
|
|
|
const releaseInfoUrl = `${homepage}/data/electerm-github-release.json?_=${+new Date()}`
|
|
const versionUrl = `${homepage}/version.html?_=${+new Date()}`
|
|
|
|
// Directory where electerm package is installed
|
|
const packageRoot = resolve(__dirname, '..')
|
|
// Directory where the extracted binary will live
|
|
const extractDir = join(packageRoot, 'electerm')
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Security helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function sanitizeVersion (ver) {
|
|
const clean = String(ver).trim().replace(/^v/, '')
|
|
if (!/^\d+\.\d+\.\d+$/.test(clean)) {
|
|
throw new Error(
|
|
`Refusing to continue: remote version string failed validation: "${ver}"`
|
|
)
|
|
}
|
|
return clean
|
|
}
|
|
|
|
function sanitizeFilename (name) {
|
|
const clean = String(name).trim()
|
|
if (!/^[\w.-]+$/.test(clean)) {
|
|
throw new Error(
|
|
`Refusing to continue: remote filename failed validation: "${name}"`
|
|
)
|
|
}
|
|
return clean
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Core helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function getVer () {
|
|
return phin({
|
|
url: versionUrl,
|
|
timeout: 15000
|
|
})
|
|
.then(res => res.body.toString())
|
|
}
|
|
|
|
function getReleaseInfo (filter) {
|
|
return phin({
|
|
url: releaseInfoUrl,
|
|
timeout: 15000
|
|
})
|
|
.then((res) => {
|
|
return JSON.parse(res.body.toString())
|
|
.release
|
|
.assets
|
|
.filter(filter)[0]
|
|
})
|
|
}
|
|
|
|
function showFinalMessage () {
|
|
console.log('')
|
|
console.log('========================================')
|
|
console.log('electerm installation complete!')
|
|
console.log('========================================')
|
|
console.log('')
|
|
console.log('For more information, documentation, and updates, please visit:')
|
|
console.log('https://electerm.org')
|
|
console.log('')
|
|
console.log('Thank you for using electerm!')
|
|
console.log('========================================')
|
|
console.log('')
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Platform detection helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function isWindows7OrEarlier (platform, release) {
|
|
if (platform !== 'win32') return false
|
|
const [major, minor] = release.split('.').map(Number)
|
|
return major < 10 && (major < 6 || (major === 6 && minor <= 1))
|
|
}
|
|
|
|
function isMacOS10 (platform, release) {
|
|
if (platform !== 'darwin') return false
|
|
const majorVersion = parseInt(release.split('.')[0], 10)
|
|
return majorVersion < 20
|
|
}
|
|
|
|
function isLinuxLegacy (platform) {
|
|
if (platform !== 'linux') return false
|
|
try {
|
|
const result = execSync('ldd --version 2>&1 | head -n1', { encoding: 'utf8' })
|
|
const match = result.match(/(\d+\.\d+)\s*$/)
|
|
if (match) {
|
|
return parseFloat(match[1]) < 2.34
|
|
}
|
|
return false
|
|
} catch (e) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Launch the extracted binary
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Get the path to the extracted electerm executable
|
|
*/
|
|
function getElectermExePath () {
|
|
if (plat === 'win32') {
|
|
return join(extractDir, 'electerm.exe')
|
|
}
|
|
// Linux and macOS (if extracted)
|
|
return join(extractDir, 'electerm')
|
|
}
|
|
|
|
/**
|
|
* Check if the electerm binary has been extracted already
|
|
*/
|
|
function isElectermExtracted () {
|
|
const exePath = getElectermExePath()
|
|
return fs.existsSync(exePath)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Platform installers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function runLinux (folderName, filePattern) {
|
|
const rawVer = await getVer()
|
|
const ver = sanitizeVersion(rawVer)
|
|
|
|
console.log(` Version: ${ver}`)
|
|
console.log(` Target: ${folderName}`)
|
|
|
|
const target = join(packageRoot, `electerm-${ver}-${folderName}`)
|
|
|
|
// Clean up old installations
|
|
rm('-rf', [target, extractDir])
|
|
|
|
console.log(' Fetching release info...')
|
|
const releaseInfo = await getReleaseInfo(r => r.name.includes(filePattern))
|
|
if (!releaseInfo) {
|
|
throw new Error(`No release found for pattern: ${filePattern}`)
|
|
}
|
|
|
|
// Download without extracting to packageRoot directly
|
|
// We'll extract to a temp location first
|
|
const tmpDir = join(packageRoot, '.electerm-tmp')
|
|
rm('-rf', tmpDir)
|
|
fs.mkdirSync(tmpDir, { recursive: true })
|
|
|
|
const proxyUrl = applyProxy(releaseInfo.browser_download_url)
|
|
console.log(` URL: ${proxyUrl}`)
|
|
|
|
const { filepath } = await download(releaseInfo.browser_download_url, tmpDir, { extract: false, displayName: releaseInfo.name })
|
|
|
|
// Extract to tmpDir (keeps top-level folder name)
|
|
await extractTarGz(filepath, tmpDir)
|
|
|
|
// Find the extracted folder (should be the only directory)
|
|
const entries = fs.readdirSync(tmpDir)
|
|
const extractedFolder = entries.find(e => fs.statSync(join(tmpDir, e)).isDirectory())
|
|
|
|
if (!extractedFolder) {
|
|
throw new Error('No folder found in extracted archive')
|
|
}
|
|
|
|
// Move to extractDir
|
|
console.log(` Installing to: ${extractDir}`)
|
|
mv(join(tmpDir, extractedFolder), extractDir)
|
|
|
|
// Fix chrome-sandbox permissions on Linux (Electron requires specific permissions)
|
|
// Note: setting the setuid bit requires root ownership, which npm install cannot provide.
|
|
// The launcher handles this by passing --no-sandbox when the sandbox is not root-owned.
|
|
if (plat === 'linux') {
|
|
const chromeSandboxPath = join(extractDir, 'chrome-sandbox')
|
|
if (fs.existsSync(chromeSandboxPath)) {
|
|
console.log(' Note: To enable the Electron sandbox, run:')
|
|
console.log(` sudo chown root:root "${chromeSandboxPath}"`)
|
|
console.log(` sudo chmod 4755 "${chromeSandboxPath}"`)
|
|
console.log(' Otherwise, electerm will launch with --no-sandbox automatically.')
|
|
}
|
|
}
|
|
|
|
// Clean up temp files
|
|
rm('-rf', tmpDir)
|
|
|
|
showFinalMessage()
|
|
}
|
|
|
|
async function runWin (archName) {
|
|
const rawVer = await getVer()
|
|
const ver = sanitizeVersion(rawVer)
|
|
|
|
console.log(` Version: ${ver}`)
|
|
console.log(` Target: win-${archName}`)
|
|
|
|
const target = join(packageRoot, `electerm-${ver}-win-${archName}`)
|
|
|
|
rm('-rf', [target, extractDir])
|
|
fs.mkdirSync(extractDir, { recursive: true })
|
|
|
|
const pattern = new RegExp(`electerm-\\d+\\.\\d+\\.\\d+-win-${archName}\\.tar\\.gz$`)
|
|
console.log(' Fetching release info...')
|
|
const releaseInfo = await getReleaseInfo(r => pattern.test(r.name))
|
|
if (!releaseInfo) {
|
|
throw new Error(`No release found for Windows ${archName}`)
|
|
}
|
|
|
|
// Download to a temp file, then extract directly to extractDir with strip:1
|
|
// (avoids a rename which can fail on Windows when AV has file locks)
|
|
const tmpDir = join(packageRoot, '.electerm-tmp')
|
|
rm('-rf', tmpDir)
|
|
fs.mkdirSync(tmpDir, { recursive: true })
|
|
|
|
const proxyUrl = applyProxy(releaseInfo.browser_download_url)
|
|
console.log(` URL: ${proxyUrl}`)
|
|
|
|
const { filepath } = await download(releaseInfo.browser_download_url, tmpDir, { extract: false, displayName: releaseInfo.name })
|
|
|
|
console.log(` Installing to: ${extractDir}`)
|
|
await extractTarGz(filepath, extractDir, 1)
|
|
|
|
rm('-rf', tmpDir)
|
|
|
|
const exePath = getElectermExePath()
|
|
if (!fs.existsSync(exePath)) {
|
|
throw new Error(`electerm.exe not found at ${exePath} after extraction. Archive may have an unexpected structure.`)
|
|
}
|
|
|
|
showFinalMessage()
|
|
}
|
|
|
|
async function runWin7 () {
|
|
const rawVer = await getVer()
|
|
const ver = sanitizeVersion(rawVer)
|
|
|
|
console.log(` Version: ${ver}`)
|
|
console.log(' Target: win7')
|
|
|
|
const target = join(packageRoot, `electerm-${ver}-win7`)
|
|
|
|
rm('-rf', [target, extractDir])
|
|
fs.mkdirSync(extractDir, { recursive: true })
|
|
|
|
console.log(' Fetching release info...')
|
|
const releaseInfo = await getReleaseInfo(r => /electerm-\d+\.\d+\.\d+-win7\.tar\.gz$/.test(r.name))
|
|
if (!releaseInfo) {
|
|
throw new Error('No release found for Windows 7')
|
|
}
|
|
|
|
const tmpDir = join(packageRoot, '.electerm-tmp')
|
|
rm('-rf', tmpDir)
|
|
fs.mkdirSync(tmpDir, { recursive: true })
|
|
|
|
const proxyUrl = applyProxy(releaseInfo.browser_download_url)
|
|
console.log(` URL: ${proxyUrl}`)
|
|
|
|
const { filepath } = await download(releaseInfo.browser_download_url, tmpDir, { extract: false, displayName: releaseInfo.name })
|
|
|
|
console.log(` Installing to: ${extractDir}`)
|
|
await extractTarGz(filepath, extractDir, 1)
|
|
|
|
rm('-rf', tmpDir)
|
|
|
|
const exePath = getElectermExePath()
|
|
if (!fs.existsSync(exePath)) {
|
|
throw new Error(`electerm.exe not found at ${exePath} after extraction. Archive may have an unexpected structure.`)
|
|
}
|
|
|
|
showFinalMessage()
|
|
}
|
|
|
|
/**
|
|
* Mount a DMG, copy the .app to /Applications, then detach
|
|
* @param {string} dmgPath - Path to the DMG file
|
|
* @returns {Promise<string>} - Path to the installed app
|
|
*/
|
|
function installFromDmg (dmgPath) {
|
|
return new Promise((resolve, reject) => {
|
|
// Step 1: Mount the DMG
|
|
console.log(' Mounting DMG...')
|
|
execFile('hdiutil', ['attach', dmgPath, '-nobrowse', '-readonly'], (err, stdout) => {
|
|
if (err) {
|
|
reject(new Error(`Failed to mount DMG: ${err.message}`))
|
|
return
|
|
}
|
|
|
|
// Parse mount point
|
|
const mountMatch = stdout.match(/(\/Volumes\/[^\n]+)/)
|
|
if (!mountMatch) {
|
|
reject(new Error('Could not find mount point'))
|
|
return
|
|
}
|
|
|
|
const mountPoint = mountMatch[1].trim()
|
|
console.log(` Mounted at: ${mountPoint}`)
|
|
|
|
// Step 2: Find the .app bundle
|
|
try {
|
|
const entries = fs.readdirSync(mountPoint)
|
|
const appFile = entries.find(e => e.endsWith('.app'))
|
|
|
|
if (!appFile) {
|
|
// Try to detach before rejecting
|
|
execFileSyncIgnore('hdiutil', ['detach', mountPoint])
|
|
reject(new Error('No .app bundle found in DMG'))
|
|
return
|
|
}
|
|
|
|
const appSource = join(mountPoint, appFile)
|
|
const appDest = `/Applications/${appFile}`
|
|
|
|
// Check if app already exists
|
|
if (fs.existsSync(appDest)) {
|
|
console.log(` Existing app found at ${appDest}, replacing...`)
|
|
// Remove existing app
|
|
rm('-rf', appDest)
|
|
}
|
|
|
|
// Step 3: Copy the app to /Applications
|
|
console.log(` Installing ${appFile} to /Applications...`)
|
|
execFile('cp', ['-R', appSource, appDest], (cpErr) => {
|
|
// Step 4: Detach the DMG (always, regardless of copy result)
|
|
console.log(' Detaching DMG...')
|
|
execFile('hdiutil', ['detach', mountPoint], (detachErr) => {
|
|
if (detachErr) {
|
|
console.log(' Warning: Failed to detach DMG:', detachErr.message)
|
|
} else {
|
|
console.log(' DMG detached')
|
|
}
|
|
|
|
if (cpErr) {
|
|
reject(new Error(`Failed to copy app: ${cpErr.message}`))
|
|
return
|
|
}
|
|
|
|
console.log(` App installed to: ${appDest}`)
|
|
resolve(appDest)
|
|
})
|
|
})
|
|
} catch (e) {
|
|
// Try to detach before rejecting
|
|
execFileSyncIgnore('hdiutil', ['detach', mountPoint])
|
|
reject(e)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Execute a file synchronously, ignoring errors
|
|
*/
|
|
function execFileSyncIgnore (cmd, args) {
|
|
try {
|
|
execSync(cmd, args, { stdio: 'ignore' })
|
|
} catch (e) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
async function runMac (archName) {
|
|
const pattern = new RegExp(`mac-${archName}\\.dmg$`)
|
|
console.log(' Fetching release info...')
|
|
const releaseInfo = await getReleaseInfo(r => pattern.test(r.name))
|
|
if (!releaseInfo) {
|
|
throw new Error(`No release found for Mac ${archName}`)
|
|
}
|
|
|
|
const safeName = sanitizeFilename(releaseInfo.name)
|
|
const proxyUrl = applyProxy(releaseInfo.browser_download_url)
|
|
console.log(` URL: ${proxyUrl}`)
|
|
|
|
await download(releaseInfo.browser_download_url, packageRoot, { extract: false, displayName: releaseInfo.name })
|
|
|
|
const dmgPath = join(packageRoot, safeName)
|
|
showFinalMessage()
|
|
|
|
// Install from DMG automatically
|
|
try {
|
|
await installFromDmg(dmgPath)
|
|
|
|
// Clean up DMG
|
|
try {
|
|
fs.unlinkSync(dmgPath)
|
|
console.log(' Cleaned up DMG file')
|
|
} catch (e) {
|
|
// Ignore cleanup errors
|
|
}
|
|
|
|
console.log('')
|
|
console.log(' Installation complete! You can now launch electerm from /Applications')
|
|
} catch (err) {
|
|
console.error('')
|
|
console.error(' Warning: Automatic installation failed:', err.message)
|
|
console.error(' Please manually copy the app from the DMG to /Applications')
|
|
console.error('')
|
|
console.log(' Opening DMG for manual installation...')
|
|
execFile('open', [dmgPath])
|
|
}
|
|
}
|
|
|
|
async function runMac10 () {
|
|
console.log(' Fetching release info...')
|
|
const releaseInfo = await getReleaseInfo(r => /mac10-x64\.dmg$/.test(r.name))
|
|
if (!releaseInfo) {
|
|
throw new Error('No release found for macOS 10.x')
|
|
}
|
|
|
|
const safeName = sanitizeFilename(releaseInfo.name)
|
|
const proxyUrl = applyProxy(releaseInfo.browser_download_url)
|
|
console.log(` URL: ${proxyUrl}`)
|
|
|
|
await download(releaseInfo.browser_download_url, packageRoot, { extract: false, displayName: releaseInfo.name })
|
|
|
|
const dmgPath = join(packageRoot, safeName)
|
|
showFinalMessage()
|
|
|
|
// Install from DMG automatically
|
|
try {
|
|
await installFromDmg(dmgPath)
|
|
|
|
// Clean up DMG
|
|
try {
|
|
fs.unlinkSync(dmgPath)
|
|
console.log(' Cleaned up DMG file')
|
|
} catch (e) {
|
|
// Ignore cleanup errors
|
|
}
|
|
|
|
console.log('')
|
|
console.log(' Installation complete! You can now launch electerm from /Applications')
|
|
} catch (err) {
|
|
console.error('')
|
|
console.error(' Warning: Automatic installation failed:', err.message)
|
|
console.error(' Please manually copy the app from the DMG to /Applications')
|
|
console.error('')
|
|
console.log(' Opening DMG for manual installation...')
|
|
execFile('open', [dmgPath])
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function main () {
|
|
console.log('')
|
|
console.log('========================================')
|
|
console.log('electerm binary installer')
|
|
console.log('========================================')
|
|
console.log(`Platform: ${plat}, Architecture: ${arch}`)
|
|
|
|
if (GITHUB_PROXY) {
|
|
console.log(`GitHub Proxy: ${GITHUB_PROXY}`)
|
|
}
|
|
|
|
console.log('')
|
|
|
|
// Check for legacy systems
|
|
const win7 = isWindows7OrEarlier(plat, os.release())
|
|
const mac10 = isMacOS10(plat, os.release())
|
|
const linuxLegacy = isLinuxLegacy(plat)
|
|
|
|
if (win7) console.log(' Detected: Windows 7 or earlier')
|
|
if (mac10) console.log(' Detected: macOS 10.x')
|
|
if (linuxLegacy) console.log(' Detected: Linux with glibc < 2.34 (legacy)')
|
|
|
|
console.log(' Fetching release information...')
|
|
|
|
try {
|
|
if (plat === 'win32') {
|
|
if (win7) {
|
|
await runWin7()
|
|
} else if (arch === 'arm64') {
|
|
await runWin('arm64')
|
|
} else {
|
|
await runWin('x64')
|
|
}
|
|
} else if (plat === 'darwin') {
|
|
if (mac10) {
|
|
await runMac10()
|
|
} else if (arch === 'arm64') {
|
|
await runMac('arm64')
|
|
} else {
|
|
await runMac('x64')
|
|
}
|
|
} else if (plat === 'linux') {
|
|
const suffix = linuxLegacy ? '-legacy' : ''
|
|
if (arch === 'arm64' || arch === 'aarch64') {
|
|
await runLinux(`linux-arm64${suffix}`, `linux-arm64${suffix}.tar.gz`)
|
|
} else if (arch === 'arm') {
|
|
await runLinux(`linux-armv7l${suffix}`, `linux-armv7l${suffix}.tar.gz`)
|
|
} else if (arch.includes('loong')) {
|
|
await runLinux(`linux-loong64${suffix}`, `linux-loong64${suffix}.tar.gz`)
|
|
} else {
|
|
await runLinux(`linux-x64${suffix}`, `linux-x64${suffix}.tar.gz`)
|
|
}
|
|
} else {
|
|
throw new Error(`Platform "${plat}" is not supported.`)
|
|
}
|
|
} catch (err) {
|
|
console.error('')
|
|
console.error('========================================')
|
|
console.error('Installation failed!')
|
|
console.error('========================================')
|
|
console.error(`Error: ${err.message}`)
|
|
console.error(`Platform: ${plat}, Architecture: ${arch}`)
|
|
console.error('')
|
|
console.error('Please visit https://electerm.org for manual download options.')
|
|
console.error('========================================')
|
|
console.error('')
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Exports for testing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
module.exports = {
|
|
isWindows7OrEarlier,
|
|
isMacOS10,
|
|
isLinuxLegacy,
|
|
sanitizeVersion,
|
|
sanitizeFilename,
|
|
getElectermExePath,
|
|
isElectermExtracted,
|
|
// Expose for test injection
|
|
_packageRoot: packageRoot,
|
|
_extractDir: extractDir
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main()
|
|
}
|