electerm 3.2.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/npm/electerm CHANGED
@@ -1,4 +1,100 @@
1
- #!/bin/bash
2
- cd `dirname $0`
3
- cd ..
4
- ./lib/node_modules/electerm/electerm/electerm
1
+ #!/usr/bin/env node
2
+ /**
3
+ * electerm CLI launcher (cross-platform)
4
+ * After npm i -g electerm, the postinstall script downloads and installs the binary.
5
+ * This script simply finds and launches the installed binary.
6
+ *
7
+ * Binary locations:
8
+ * macOS: /Applications/electerm.app/Contents/MacOS/electerm
9
+ * Windows: <package>/electerm/electerm.exe
10
+ * Linux: <package>/electerm/electerm
11
+ */
12
+
13
+ const path = require('path')
14
+ const fs = require('fs')
15
+ const { spawn } = require('child_process')
16
+ const os = require('os')
17
+
18
+ const plat = os.platform()
19
+ const packageRoot = path.resolve(__dirname, '..')
20
+
21
+ /**
22
+ * Get the path to the installed electerm binary
23
+ */
24
+ function getElectermExePath () {
25
+ // macOS: prefer the installed app in /Applications
26
+ if (plat === 'darwin') {
27
+ const appBinary = '/Applications/electerm.app/Contents/MacOS/electerm'
28
+ if (fs.existsSync(appBinary)) {
29
+ return appBinary
30
+ }
31
+ // Fallback: extracted folder
32
+ return path.join(packageRoot, 'electerm', 'electerm')
33
+ }
34
+
35
+ // Windows
36
+ if (plat === 'win32') {
37
+ return path.join(packageRoot, 'electerm', 'electerm.exe')
38
+ }
39
+
40
+ // Linux
41
+ return path.join(packageRoot, 'electerm', 'electerm')
42
+ }
43
+
44
+ /**
45
+ * Check if the electerm binary exists
46
+ */
47
+ function isElectermInstalled () {
48
+ return fs.existsSync(getElectermExePath())
49
+ }
50
+
51
+ /**
52
+ * Launch the installed electerm binary
53
+ */
54
+ function launchElecterm () {
55
+ const exePath = getElectermExePath()
56
+
57
+ if (!fs.existsSync(exePath)) {
58
+ console.error('Error: electerm binary not found at:', exePath)
59
+ console.error('')
60
+ console.error('The binary may not have been installed. Try running:')
61
+ console.error(' node', path.join(packageRoot, 'npm', 'install.js'))
62
+ process.exit(1)
63
+ }
64
+
65
+ // Spawn the binary, passing through all args
66
+ const child = spawn(exePath, process.argv.slice(2), {
67
+ stdio: 'inherit',
68
+ // On macOS/Linux, detach so the app survives if the terminal closes
69
+ detached: plat !== 'win32',
70
+ // On Windows, don't create a console window for the spawn
71
+ windowsHide: false
72
+ })
73
+
74
+ if (plat !== 'win32') {
75
+ child.unref()
76
+ }
77
+
78
+ child.on('error', (err) => {
79
+ console.error('Failed to start electerm:', err.message)
80
+ process.exit(1)
81
+ })
82
+
83
+ child.on('exit', (code) => {
84
+ process.exit(code || 0)
85
+ })
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Main
90
+ // ---------------------------------------------------------------------------
91
+
92
+ if (!isElectermInstalled()) {
93
+ console.error('electerm binary not found. It should have been installed during npm install.')
94
+ console.error('')
95
+ console.error('Try running manually:')
96
+ console.error(' node', path.join(packageRoot, 'npm', 'install.js'))
97
+ process.exit(1)
98
+ }
99
+
100
+ launchElecterm()
package/npm/install.js CHANGED
@@ -1,28 +1,62 @@
1
1
  /**
2
2
  * install electerm from binary
3
+ * After npm i -g electerm, running `electerm` command will:
4
+ * 1. Download the appropriate binary for the platform
5
+ * 2. Extract it to the package directory (electerm/)
6
+ * 3. The bash script (npm/electerm) then launches the extracted binary
7
+ *
8
+ * This script only downloads and extracts. Launching is handled by the bash script.
3
9
  */
4
10
 
5
11
  const os = require('os')
6
- const { resolve } = require('path')
7
- const { exec, rm, mv } = require('shelljs')
8
- const rp = require('phin').promisified
9
- const download = require('download')
12
+ const { resolve, join } = require('path')
13
+ const { execSync, rm, mv } = require('shelljs')
14
+ const { execFile } = require('child_process')
15
+ const fs = require('fs')
16
+ const { phin, download, extractTarGz, GITHUB_PROXY, applyProxy } = require('./utils')
17
+
10
18
  const plat = os.platform()
11
19
  const arch = os.arch()
12
20
  const { homepage } = require('../package.json')
21
+
13
22
  const releaseInfoUrl = `${homepage}/data/electerm-github-release.json?_=${+new Date()}`
14
23
  const versionUrl = `${homepage}/version.html?_=${+new Date()}`
15
24
 
16
- function down (url, extract = true) {
17
- const local = resolve(__dirname, '../')
18
- console.log('downloading ' + url)
19
- return download(url, local, { extract }).then(() => {
20
- console.log('done!')
21
- })
25
+ // Directory where electerm package is installed
26
+ const packageRoot = resolve(__dirname, '..')
27
+ // Directory where the extracted binary will live
28
+ const extractDir = join(packageRoot, 'electerm')
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Security helpers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function sanitizeVersion (ver) {
35
+ const clean = String(ver).trim().replace(/^v/, '')
36
+ if (!/^\d+\.\d+\.\d+$/.test(clean)) {
37
+ throw new Error(
38
+ `Refusing to continue: remote version string failed validation: "${ver}"`
39
+ )
40
+ }
41
+ return clean
22
42
  }
23
43
 
44
+ function sanitizeFilename (name) {
45
+ const clean = String(name).trim()
46
+ if (!/^[\w.-]+$/.test(clean)) {
47
+ throw new Error(
48
+ `Refusing to continue: remote filename failed validation: "${name}"`
49
+ )
50
+ }
51
+ return clean
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Core helpers
56
+ // ---------------------------------------------------------------------------
57
+
24
58
  function getVer () {
25
- return rp({
59
+ return phin({
26
60
  url: versionUrl,
27
61
  timeout: 15000
28
62
  })
@@ -30,7 +64,7 @@ function getVer () {
30
64
  }
31
65
 
32
66
  function getReleaseInfo (filter) {
33
- return rp({
67
+ return phin({
34
68
  url: releaseInfoUrl,
35
69
  timeout: 15000
36
70
  })
@@ -43,46 +77,42 @@ function getReleaseInfo (filter) {
43
77
  }
44
78
 
45
79
  function showFinalMessage () {
46
- console.log('\n========================================')
80
+ console.log('')
81
+ console.log('========================================')
47
82
  console.log('electerm installation complete!')
48
83
  console.log('========================================')
49
- console.log('\nFor more information, documentation, and updates, please visit:')
50
- console.log('\x1b[36m%s\x1b[0m', 'https://electerm.html5beta.com')
51
- console.log('\nThank you for using electerm!')
52
- console.log('========================================\n')
84
+ console.log('')
85
+ console.log('For more information, documentation, and updates, please visit:')
86
+ console.log('https://electerm.html5beta.com')
87
+ console.log('')
88
+ console.log('Thank you for using electerm!')
89
+ console.log('========================================')
90
+ console.log('')
53
91
  }
54
92
 
55
- // Check if running on Windows 7 or earlier
93
+ // ---------------------------------------------------------------------------
94
+ // Platform detection helpers
95
+ // ---------------------------------------------------------------------------
96
+
56
97
  function isWindows7OrEarlier (platform, release) {
57
98
  if (platform !== 'win32') return false
58
- // Windows 7 is NT 6.1, Windows 8 is NT 6.2, Windows 10 is NT 10.0
59
99
  const [major, minor] = release.split('.').map(Number)
60
100
  return major < 10 && (major < 6 || (major === 6 && minor <= 1))
61
101
  }
62
102
 
63
- // Check if running on macOS 10.x (older than Big Sur 11.0)
64
103
  function isMacOS10 (platform, release) {
65
104
  if (platform !== 'darwin') return false
66
- // Darwin kernel version: macOS 11 (Big Sur) = Darwin 20.x, macOS 10.15 = Darwin 19.x
67
105
  const majorVersion = parseInt(release.split('.')[0], 10)
68
106
  return majorVersion < 20
69
107
  }
70
108
 
71
- // Check if running on Linux with old glibc (< 2.34)
72
- function isLinuxLegacy (platform, glibcVersion) {
109
+ function isLinuxLegacy (platform) {
73
110
  if (platform !== 'linux') return false
74
- if (typeof glibcVersion === 'number') {
75
- return glibcVersion < 2.34
76
- }
77
111
  try {
78
- const result = exec('ldd --version 2>&1 | head -n1', { silent: true })
79
- if (result.code !== 0) return false
80
- const output = result.stdout || ''
81
- // Extract version number like "ldd (GNU libc) 2.31" or "ldd (Ubuntu GLIBC 2.35-0ubuntu3) 2.35"
82
- const match = output.match(/(\d+\.\d+)\s*$/)
112
+ const result = execSync('ldd --version 2>&1 | head -n1', { encoding: 'utf8' })
113
+ const match = result.match(/(\d+\.\d+)\s*$/)
83
114
  if (match) {
84
- const version = parseFloat(match[1])
85
- return version < 2.34
115
+ return parseFloat(match[1]) < 2.34
86
116
  }
87
117
  return false
88
118
  } catch (e) {
@@ -90,186 +120,428 @@ function isLinuxLegacy (platform, glibcVersion) {
90
120
  }
91
121
  }
92
122
 
93
- // Get the file pattern for download based on platform/arch/legacy status
94
- function getDownloadPattern (platform, architecture, options = {}) {
95
- const { win7, mac10, linuxLegacy } = options
123
+ // ---------------------------------------------------------------------------
124
+ // Launch the extracted binary
125
+ // ---------------------------------------------------------------------------
96
126
 
97
- if (platform === 'win32') {
98
- if (win7) {
99
- return { pattern: /electerm-\d+\.\d+\.\d+-win7\.tar\.gz$/, type: 'win7' }
100
- } else if (architecture === 'arm64') {
101
- return { pattern: /electerm-\d+\.\d+\.\d+-win-arm64\.tar\.gz$/, type: 'win-arm64' }
102
- } else {
103
- return { pattern: /electerm-\d+\.\d+\.\d+-win-x64\.tar\.gz$/, type: 'win-x64' }
104
- }
105
- } else if (platform === 'darwin') {
106
- if (mac10) {
107
- return { pattern: /mac10-x64\.dmg$/, type: 'mac10-x64' }
108
- } else if (architecture === 'arm64') {
109
- return { pattern: /mac-arm64\.dmg$/, type: 'mac-arm64' }
110
- } else {
111
- return { pattern: /mac-x64\.dmg$/, type: 'mac-x64' }
112
- }
113
- } else if (platform === 'linux') {
114
- const suffix = linuxLegacy ? '-legacy' : ''
115
- if (architecture === 'arm64') {
116
- return { pattern: new RegExp(`linux-arm64${suffix}\\.tar\\.gz$`), type: `linux-arm64${suffix}` }
117
- } else if (architecture === 'arm') {
118
- return { pattern: new RegExp(`linux-armv7l${suffix}\\.tar\\.gz$`), type: `linux-armv7l${suffix}` }
119
- } else {
120
- return { pattern: new RegExp(`linux-x64${suffix}\\.tar\\.gz$`), type: `linux-x64${suffix}` }
121
- }
127
+ /**
128
+ * Get the path to the extracted electerm executable
129
+ */
130
+ function getElectermExePath () {
131
+ if (plat === 'win32') {
132
+ return join(extractDir, 'electerm.exe')
122
133
  }
123
- return { pattern: null, type: 'unsupported' }
134
+ // Linux and macOS (if extracted)
135
+ return join(extractDir, 'electerm')
124
136
  }
125
137
 
138
+ /**
139
+ * Check if the electerm binary has been extracted already
140
+ */
141
+ function isElectermExtracted () {
142
+ const exePath = getElectermExePath()
143
+ return fs.existsSync(exePath)
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Platform installers
148
+ // ---------------------------------------------------------------------------
149
+
126
150
  async function runLinux (folderName, filePattern) {
127
- const ver = await getVer()
128
- const target = resolve(__dirname, `../electerm-${ver.replace('v', '')}-${folderName}`)
129
- const targetNew = resolve(__dirname, '../electerm')
130
- exec(`rm -rf ${target} ${targetNew}`)
151
+ const rawVer = await getVer()
152
+ const ver = sanitizeVersion(rawVer)
153
+
154
+ console.log(` Version: ${ver}`)
155
+ console.log(` Target: ${folderName}`)
156
+
157
+ const target = join(packageRoot, `electerm-${ver}-${folderName}`)
158
+
159
+ // Clean up old installations
160
+ rm('-rf', [target, extractDir])
161
+
162
+ console.log(' Fetching release info...')
131
163
  const releaseInfo = await getReleaseInfo(r => r.name.includes(filePattern))
132
164
  if (!releaseInfo) {
133
165
  throw new Error(`No release found for pattern: ${filePattern}`)
134
166
  }
135
- await down(releaseInfo.browser_download_url)
136
- exec(`mv ${target} ${targetNew}`)
167
+
168
+ // Download without extracting to packageRoot directly
169
+ // We'll extract to a temp location first
170
+ const tmpDir = join(packageRoot, '.electerm-tmp')
171
+ rm('-rf', tmpDir)
172
+ fs.mkdirSync(tmpDir, { recursive: true })
173
+
174
+ const proxyUrl = applyProxy(releaseInfo.browser_download_url)
175
+ console.log(` URL: ${proxyUrl}`)
176
+
177
+ const { filepath } = await download(releaseInfo.browser_download_url, tmpDir, { extract: false, displayName: releaseInfo.name })
178
+
179
+ // Extract to tmpDir (keeps top-level folder name)
180
+ await extractTarGz(filepath, tmpDir)
181
+
182
+ // Find the extracted folder (should be the only directory)
183
+ const entries = fs.readdirSync(tmpDir)
184
+ const extractedFolder = entries.find(e => fs.statSync(join(tmpDir, e)).isDirectory())
185
+
186
+ if (!extractedFolder) {
187
+ throw new Error('No folder found in extracted archive')
188
+ }
189
+
190
+ // Move to extractDir
191
+ console.log(` Installing to: ${extractDir}`)
192
+ mv(join(tmpDir, extractedFolder), extractDir)
193
+
194
+ // Clean up temp files
195
+ rm('-rf', tmpDir)
196
+
137
197
  showFinalMessage()
138
- exec('electerm')
139
198
  }
140
199
 
141
- async function runMac (archName) {
142
- const pattern = new RegExp(`mac-${archName}\\.dmg$`)
200
+ async function runWin (archName) {
201
+ const rawVer = await getVer()
202
+ const ver = sanitizeVersion(rawVer)
203
+
204
+ console.log(` Version: ${ver}`)
205
+ console.log(` Target: win-${archName}`)
206
+
207
+ const target = join(packageRoot, `electerm-${ver}-win-${archName}`)
208
+
209
+ rm('-rf', [target, extractDir])
210
+
211
+ const pattern = new RegExp(`electerm-\\d+\\.\\d+\\.\\d+-win-${archName}\\.tar\\.gz$`)
212
+ console.log(' Fetching release info...')
143
213
  const releaseInfo = await getReleaseInfo(r => pattern.test(r.name))
144
214
  if (!releaseInfo) {
145
- throw new Error(`No release found for Mac ${archName}`)
215
+ throw new Error(`No release found for Windows ${archName}`)
216
+ }
217
+
218
+ // Extract to temp, then move
219
+ const tmpDir = join(packageRoot, '.electerm-tmp')
220
+ rm('-rf', tmpDir)
221
+ fs.mkdirSync(tmpDir, { recursive: true })
222
+
223
+ const proxyUrl = applyProxy(releaseInfo.browser_download_url)
224
+ console.log(` URL: ${proxyUrl}`)
225
+
226
+ const { filepath } = await download(releaseInfo.browser_download_url, tmpDir, { extract: false, displayName: releaseInfo.name })
227
+
228
+ await extractTarGz(filepath, tmpDir)
229
+
230
+ const entries = fs.readdirSync(tmpDir)
231
+ const extractedFolder = entries.find(e => fs.statSync(join(tmpDir, e)).isDirectory())
232
+
233
+ if (!extractedFolder) {
234
+ throw new Error('No folder found in extracted archive')
146
235
  }
147
- await down(releaseInfo.browser_download_url, false)
148
- const target = resolve(__dirname, '../', releaseInfo.name)
236
+
237
+ console.log(` Installing to: ${extractDir}`)
238
+ mv(join(tmpDir, extractedFolder), extractDir)
239
+ rm('-rf', tmpDir)
240
+
149
241
  showFinalMessage()
150
- exec(`open ${target}`)
151
242
  }
152
243
 
153
- // macOS 10.x specific version
154
- async function runMac10 () {
155
- const releaseInfo = await getReleaseInfo(r => /mac10-x64\.dmg$/.test(r.name))
244
+ async function runWin7 () {
245
+ const rawVer = await getVer()
246
+ const ver = sanitizeVersion(rawVer)
247
+
248
+ console.log(` Version: ${ver}`)
249
+ console.log(' Target: win7')
250
+
251
+ const target = join(packageRoot, `electerm-${ver}-win7`)
252
+
253
+ rm('-rf', [target, extractDir])
254
+
255
+ console.log(' Fetching release info...')
256
+ const releaseInfo = await getReleaseInfo(r => /electerm-\d+\.\d+\.\d+-win7\.tar\.gz$/.test(r.name))
156
257
  if (!releaseInfo) {
157
- throw new Error('No release found for macOS 10.x')
258
+ throw new Error('No release found for Windows 7')
158
259
  }
159
- await down(releaseInfo.browser_download_url, false)
160
- const target = resolve(__dirname, '../', releaseInfo.name)
260
+
261
+ // Extract to temp, then move
262
+ const tmpDir = join(packageRoot, '.electerm-tmp')
263
+ rm('-rf', tmpDir)
264
+ fs.mkdirSync(tmpDir, { recursive: true })
265
+
266
+ const proxyUrl = applyProxy(releaseInfo.browser_download_url)
267
+ console.log(` URL: ${proxyUrl}`)
268
+
269
+ const { filepath } = await download(releaseInfo.browser_download_url, tmpDir, { extract: false, displayName: releaseInfo.name })
270
+
271
+ await extractTarGz(filepath, tmpDir)
272
+
273
+ const entries = fs.readdirSync(tmpDir)
274
+ const extractedFolder = entries.find(e => fs.statSync(join(tmpDir, e)).isDirectory())
275
+
276
+ if (!extractedFolder) {
277
+ throw new Error('No folder found in extracted archive')
278
+ }
279
+
280
+ console.log(` Installing to: ${extractDir}`)
281
+ mv(join(tmpDir, extractedFolder), extractDir)
282
+ rm('-rf', tmpDir)
283
+
161
284
  showFinalMessage()
162
- exec(`open ${target}`)
163
285
  }
164
286
 
165
- async function runWin (archName) {
166
- const ver = await getVer()
167
- const target = resolve(__dirname, `../electerm-${ver.replace('v', '')}-win-${archName}`)
168
- const targetNew = resolve(__dirname, '../electerm')
169
- rm('-rf', [
170
- target,
171
- targetNew
172
- ])
173
- const pattern = new RegExp(`electerm-\\d+\\.\\d+\\.\\d+-win-${archName}\\.tar\\.gz$`)
287
+ /**
288
+ * Mount a DMG, copy the .app to /Applications, then detach
289
+ * @param {string} dmgPath - Path to the DMG file
290
+ * @returns {Promise<string>} - Path to the installed app
291
+ */
292
+ function installFromDmg (dmgPath) {
293
+ return new Promise((resolve, reject) => {
294
+ // Step 1: Mount the DMG
295
+ console.log(' Mounting DMG...')
296
+ execFile('hdiutil', ['attach', dmgPath, '-nobrowse', '-readonly'], (err, stdout) => {
297
+ if (err) {
298
+ reject(new Error(`Failed to mount DMG: ${err.message}`))
299
+ return
300
+ }
301
+
302
+ // Parse mount point
303
+ const mountMatch = stdout.match(/(\/Volumes\/[^\n]+)/)
304
+ if (!mountMatch) {
305
+ reject(new Error('Could not find mount point'))
306
+ return
307
+ }
308
+
309
+ const mountPoint = mountMatch[1].trim()
310
+ console.log(` Mounted at: ${mountPoint}`)
311
+
312
+ // Step 2: Find the .app bundle
313
+ try {
314
+ const entries = fs.readdirSync(mountPoint)
315
+ const appFile = entries.find(e => e.endsWith('.app'))
316
+
317
+ if (!appFile) {
318
+ // Try to detach before rejecting
319
+ execFileSyncIgnore('hdiutil', ['detach', mountPoint])
320
+ reject(new Error('No .app bundle found in DMG'))
321
+ return
322
+ }
323
+
324
+ const appSource = join(mountPoint, appFile)
325
+ const appDest = `/Applications/${appFile}`
326
+
327
+ // Check if app already exists
328
+ if (fs.existsSync(appDest)) {
329
+ console.log(` Existing app found at ${appDest}, replacing...`)
330
+ // Remove existing app
331
+ rm('-rf', appDest)
332
+ }
333
+
334
+ // Step 3: Copy the app to /Applications
335
+ console.log(` Installing ${appFile} to /Applications...`)
336
+ execFile('cp', ['-R', appSource, appDest], (cpErr) => {
337
+ // Step 4: Detach the DMG (always, regardless of copy result)
338
+ console.log(' Detaching DMG...')
339
+ execFile('hdiutil', ['detach', mountPoint], (detachErr) => {
340
+ if (detachErr) {
341
+ console.log(' Warning: Failed to detach DMG:', detachErr.message)
342
+ } else {
343
+ console.log(' DMG detached')
344
+ }
345
+
346
+ if (cpErr) {
347
+ reject(new Error(`Failed to copy app: ${cpErr.message}`))
348
+ return
349
+ }
350
+
351
+ console.log(` App installed to: ${appDest}`)
352
+ resolve(appDest)
353
+ })
354
+ })
355
+ } catch (e) {
356
+ // Try to detach before rejecting
357
+ execFileSyncIgnore('hdiutil', ['detach', mountPoint])
358
+ reject(e)
359
+ }
360
+ })
361
+ })
362
+ }
363
+
364
+ /**
365
+ * Execute a file synchronously, ignoring errors
366
+ */
367
+ function execFileSyncIgnore (cmd, args) {
368
+ try {
369
+ execSync(cmd, args, { stdio: 'ignore' })
370
+ } catch (e) {
371
+ // Ignore
372
+ }
373
+ }
374
+
375
+ async function runMac (archName) {
376
+ const pattern = new RegExp(`mac-${archName}\\.dmg$`)
377
+ console.log(' Fetching release info...')
174
378
  const releaseInfo = await getReleaseInfo(r => pattern.test(r.name))
175
379
  if (!releaseInfo) {
176
- throw new Error(`No release found for Windows ${archName}`)
380
+ throw new Error(`No release found for Mac ${archName}`)
177
381
  }
178
- await down(releaseInfo.browser_download_url)
179
- await mv(target, targetNew)
382
+
383
+ const safeName = sanitizeFilename(releaseInfo.name)
384
+ const proxyUrl = applyProxy(releaseInfo.browser_download_url)
385
+ console.log(` URL: ${proxyUrl}`)
386
+
387
+ await download(releaseInfo.browser_download_url, packageRoot, { extract: false, displayName: releaseInfo.name })
388
+
389
+ const dmgPath = join(packageRoot, safeName)
180
390
  showFinalMessage()
181
- require('child_process').execFile(`${targetNew}\\electerm.exe`)
391
+
392
+ // Install from DMG automatically
393
+ try {
394
+ await installFromDmg(dmgPath)
395
+
396
+ // Clean up DMG
397
+ try {
398
+ fs.unlinkSync(dmgPath)
399
+ console.log(' Cleaned up DMG file')
400
+ } catch (e) {
401
+ // Ignore cleanup errors
402
+ }
403
+
404
+ console.log('')
405
+ console.log(' Installation complete! You can now launch electerm from /Applications')
406
+ } catch (err) {
407
+ console.error('')
408
+ console.error(' Warning: Automatic installation failed:', err.message)
409
+ console.error(' Please manually copy the app from the DMG to /Applications')
410
+ console.error('')
411
+ console.log(' Opening DMG for manual installation...')
412
+ execFile('open', [dmgPath])
413
+ }
182
414
  }
183
415
 
184
- // Windows 7 specific version
185
- async function runWin7 () {
186
- const ver = await getVer()
187
- const target = resolve(__dirname, `../electerm-${ver.replace('v', '')}-win7`)
188
- const targetNew = resolve(__dirname, '../electerm')
189
- rm('-rf', [
190
- target,
191
- targetNew
192
- ])
193
- const releaseInfo = await getReleaseInfo(r => /electerm-\d+\.\d+\.\d+-win7\.tar\.gz$/.test(r.name))
416
+ async function runMac10 () {
417
+ console.log(' Fetching release info...')
418
+ const releaseInfo = await getReleaseInfo(r => /mac10-x64\.dmg$/.test(r.name))
194
419
  if (!releaseInfo) {
195
- throw new Error('No release found for Windows 7')
420
+ throw new Error('No release found for macOS 10.x')
196
421
  }
197
- await down(releaseInfo.browser_download_url)
198
- await mv(target, targetNew)
422
+
423
+ const safeName = sanitizeFilename(releaseInfo.name)
424
+ const proxyUrl = applyProxy(releaseInfo.browser_download_url)
425
+ console.log(` URL: ${proxyUrl}`)
426
+
427
+ await download(releaseInfo.browser_download_url, packageRoot, { extract: false, displayName: releaseInfo.name })
428
+
429
+ const dmgPath = join(packageRoot, safeName)
199
430
  showFinalMessage()
200
- require('child_process').execFile(`${targetNew}\\electerm.exe`)
431
+
432
+ // Install from DMG automatically
433
+ try {
434
+ await installFromDmg(dmgPath)
435
+
436
+ // Clean up DMG
437
+ try {
438
+ fs.unlinkSync(dmgPath)
439
+ console.log(' Cleaned up DMG file')
440
+ } catch (e) {
441
+ // Ignore cleanup errors
442
+ }
443
+
444
+ console.log('')
445
+ console.log(' Installation complete! You can now launch electerm from /Applications')
446
+ } catch (err) {
447
+ console.error('')
448
+ console.error(' Warning: Automatic installation failed:', err.message)
449
+ console.error(' Please manually copy the app from the DMG to /Applications')
450
+ console.error('')
451
+ console.log(' Opening DMG for manual installation...')
452
+ execFile('open', [dmgPath])
453
+ }
201
454
  }
202
455
 
456
+ // ---------------------------------------------------------------------------
457
+ // Main
458
+ // ---------------------------------------------------------------------------
459
+
203
460
  async function main () {
204
- console.log(`Detected platform: ${plat}, architecture: ${arch}`)
461
+ console.log('')
462
+ console.log('========================================')
463
+ console.log('electerm binary installer')
464
+ console.log('========================================')
465
+ console.log(`Platform: ${plat}, Architecture: ${arch}`)
466
+
467
+ if (GITHUB_PROXY) {
468
+ console.log(`GitHub Proxy: ${GITHUB_PROXY}`)
469
+ }
470
+
471
+ console.log('')
205
472
 
206
473
  // Check for legacy systems
207
474
  const win7 = isWindows7OrEarlier(plat, os.release())
208
475
  const mac10 = isMacOS10(plat, os.release())
209
476
  const linuxLegacy = isLinuxLegacy(plat)
210
477
 
211
- if (win7) console.log('Detected: Windows 7 or earlier')
212
- if (mac10) console.log('Detected: macOS 10.x')
213
- if (linuxLegacy) console.log('Detected: Linux with glibc < 2.34 (legacy)')
478
+ if (win7) console.log(' Detected: Windows 7 or earlier')
479
+ if (mac10) console.log(' Detected: macOS 10.x')
480
+ if (linuxLegacy) console.log(' Detected: Linux with glibc < 2.34 (legacy)')
214
481
 
215
- console.log('Fetching release information...\n')
482
+ console.log(' Fetching release information...')
216
483
 
217
484
  try {
218
485
  if (plat === 'win32') {
219
- // Windows: x64, arm64, win7
220
486
  if (win7) {
221
487
  await runWin7()
222
488
  } else if (arch === 'arm64') {
223
489
  await runWin('arm64')
224
490
  } else {
225
- // Default to x64 for all other Windows architectures
226
491
  await runWin('x64')
227
492
  }
228
493
  } else if (plat === 'darwin') {
229
- // macOS: x64, arm64, mac10
230
494
  if (mac10) {
231
495
  await runMac10()
232
496
  } else if (arch === 'arm64') {
233
497
  await runMac('arm64')
234
498
  } else {
235
- // Default to x64 for Intel Macs
236
499
  await runMac('x64')
237
500
  }
238
501
  } else if (plat === 'linux') {
239
- // Linux: x64, arm64, armv7l (with legacy variants)
240
502
  const suffix = linuxLegacy ? '-legacy' : ''
241
- if (arch === 'arm64') {
503
+ if (arch === 'arm64' || arch === 'aarch64') {
242
504
  await runLinux(`linux-arm64${suffix}`, `linux-arm64${suffix}.tar.gz`)
243
505
  } else if (arch === 'arm') {
244
506
  await runLinux(`linux-armv7l${suffix}`, `linux-armv7l${suffix}.tar.gz`)
245
507
  } else {
246
- // Default to x64 for all other Linux architectures
247
508
  await runLinux(`linux-x64${suffix}`, `linux-x64${suffix}.tar.gz`)
248
509
  }
249
510
  } else {
250
511
  throw new Error(`Platform "${plat}" is not supported.`)
251
512
  }
252
513
  } catch (err) {
253
- console.error('\n========================================')
514
+ console.error('')
515
+ console.error('========================================')
254
516
  console.error('Installation failed!')
255
517
  console.error('========================================')
256
518
  console.error(`Error: ${err.message}`)
257
- console.error(`\nPlatform: ${plat}, Architecture: ${arch}`)
258
- console.error('\nPlease visit https://electerm.html5beta.com for manual download options.')
259
- console.error('========================================\n')
519
+ console.error(`Platform: ${plat}, Architecture: ${arch}`)
520
+ console.error('')
521
+ console.error('Please visit https://electerm.html5beta.com for manual download options.')
522
+ console.error('========================================')
523
+ console.error('')
260
524
  process.exit(1)
261
525
  }
262
526
  }
263
527
 
264
- // Export functions for testing
528
+ // ---------------------------------------------------------------------------
529
+ // Exports for testing
530
+ // ---------------------------------------------------------------------------
531
+
265
532
  module.exports = {
266
533
  isWindows7OrEarlier,
267
534
  isMacOS10,
268
535
  isLinuxLegacy,
269
- getDownloadPattern
536
+ sanitizeVersion,
537
+ sanitizeFilename,
538
+ getElectermExePath,
539
+ isElectermExtracted,
540
+ // Expose for test injection
541
+ _packageRoot: packageRoot,
542
+ _extractDir: extractDir
270
543
  }
271
544
 
272
- // Run main only if this file is executed directly
273
545
  if (require.main === module) {
274
546
  main()
275
547
  }
package/npm/utils.js ADDED
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Utility functions for npm installer
3
+ * Replaces download and phin packages with native Node.js http/https and tar
4
+ * Supports Node.js 16+
5
+ * Supports GITHUB_PROXY environment variable for proxying GitHub URLs
6
+ */
7
+
8
+ const https = require('https')
9
+ const http = require('http')
10
+ const fs = require('fs')
11
+ const path = require('path')
12
+ const tar = require('tar')
13
+
14
+ // GitHub proxy support
15
+ const GITHUB_PROXY = process.env.GITHUB_PROXY || ''
16
+
17
+ /**
18
+ * Apply GitHub proxy to URLs if configured
19
+ * @param {string} url - Original URL
20
+ * @returns {string} - Proxy URL or original URL
21
+ */
22
+ function applyProxy (url) {
23
+ if (!GITHUB_PROXY) return url
24
+ if (!url.includes('github.com')) return url
25
+
26
+ // Remove trailing slash from proxy if present
27
+ const proxy = GITHUB_PROXY.replace(/\/+$/, '')
28
+ // Ensure url has protocol
29
+ const urlWithProto = url.startsWith('http') ? url : `https://${url}`
30
+
31
+ return `${proxy}/${urlWithProto}`
32
+ }
33
+
34
+ /**
35
+ * Format bytes to human readable
36
+ */
37
+ function formatBytes (bytes) {
38
+ if (bytes === 0) return '0 B'
39
+ const k = 1024
40
+ const sizes = ['B', 'KB', 'MB', 'GB']
41
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
42
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
43
+ }
44
+
45
+ /**
46
+ * Make an HTTP GET request and download to a file with progress
47
+ * @param {string} url - URL to fetch
48
+ * @param {string} filepath - Destination file path
49
+ * @param {number} timeout - Request timeout in milliseconds (default: 300000 = 5min)
50
+ * @param {function} onProgress - Progress callback (received, total, percent)
51
+ * @returns {Promise<string>} Path to downloaded file
52
+ */
53
+ function httpDownload (url, filepath, timeout = 300000, onProgress) {
54
+ return new Promise((resolve, reject) => {
55
+ const client = url.startsWith('https') ? https : http
56
+
57
+ const req = client.get(url, { timeout }, (res) => {
58
+ // Handle redirects
59
+ if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) {
60
+ if (res.headers.location) {
61
+ // Handle relative URLs
62
+ let redirectUrl = res.headers.location
63
+ if (!redirectUrl.startsWith('http://') && !redirectUrl.startsWith('https://')) {
64
+ const parsedUrl = new URL(url)
65
+ redirectUrl = `${parsedUrl.protocol}//${parsedUrl.host}${redirectUrl}`
66
+ }
67
+ resolve(httpDownload(redirectUrl, filepath, timeout, onProgress))
68
+ return
69
+ }
70
+ }
71
+
72
+ if (res.statusCode !== 200) {
73
+ reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage || 'Unknown error'}`))
74
+ return
75
+ }
76
+
77
+ const total = parseInt(res.headers['content-length'] || '0', 10)
78
+ let received = 0
79
+ let lastPercent = -1
80
+
81
+ const fileStream = fs.createWriteStream(filepath)
82
+
83
+ res.on('data', (chunk) => {
84
+ received += chunk.length
85
+ if (onProgress && total > 0) {
86
+ const percent = Math.round((received / total) * 100)
87
+ if (percent !== lastPercent) {
88
+ lastPercent = percent
89
+ onProgress(received, total, percent)
90
+ }
91
+ }
92
+ })
93
+
94
+ res.pipe(fileStream)
95
+ fileStream.on('finish', () => {
96
+ fileStream.close()
97
+ resolve(filepath)
98
+ })
99
+ fileStream.on('error', (err) => {
100
+ fs.unlink(filepath, () => {}) // Clean up partial download
101
+ reject(err)
102
+ })
103
+ })
104
+
105
+ req.on('error', reject)
106
+ req.on('timeout', () => {
107
+ req.destroy()
108
+ reject(new Error(`Request timeout after ${timeout}ms`))
109
+ })
110
+ })
111
+ }
112
+
113
+ /**
114
+ * Make an HTTP GET request and return response body as string
115
+ * @param {string} url - URL to fetch
116
+ * @param {number} timeout - Request timeout in milliseconds (default: 15000)
117
+ * @returns {Promise<string>} Response body as string
118
+ */
119
+ function httpGet (url, timeout = 15000) {
120
+ return new Promise((resolve, reject) => {
121
+ const client = url.startsWith('https') ? https : http
122
+
123
+ const req = client.get(url, { timeout }, (res) => {
124
+ // Handle redirects
125
+ if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) {
126
+ if (res.headers.location) {
127
+ // Handle relative URLs
128
+ let redirectUrl = res.headers.location
129
+ if (!redirectUrl.startsWith('http://') && !redirectUrl.startsWith('https://')) {
130
+ const parsedUrl = new URL(url)
131
+ redirectUrl = `${parsedUrl.protocol}//${parsedUrl.host}${redirectUrl}`
132
+ }
133
+ resolve(httpGet(redirectUrl, timeout))
134
+ return
135
+ }
136
+ }
137
+
138
+ if (res.statusCode !== 200) {
139
+ reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage || 'Unknown error'}`))
140
+ return
141
+ }
142
+
143
+ const chunks = []
144
+ res.on('data', (chunk) => chunks.push(chunk))
145
+ res.on('end', () => {
146
+ const buffer = Buffer.concat(chunks)
147
+ resolve(buffer.toString())
148
+ })
149
+ res.on('error', reject)
150
+ })
151
+
152
+ req.on('error', reject)
153
+ req.on('timeout', () => {
154
+ req.destroy()
155
+ reject(new Error(`Request timeout after ${timeout}ms`))
156
+ })
157
+ })
158
+ }
159
+
160
+ /**
161
+ * Extract a tar.gz file to destination directory
162
+ * @param {string} filepath - Path to tar.gz file
163
+ * @param {string} dest - Destination directory
164
+ * @param {number} strip - Number of leading path components to strip (default: 0)
165
+ * @returns {Promise<void>}
166
+ */
167
+ function extractTarGz (filepath, dest, strip = 0) {
168
+ return tar.extract({
169
+ file: filepath,
170
+ cwd: dest,
171
+ strip
172
+ })
173
+ }
174
+
175
+ /**
176
+ * Download and optionally extract a file with progress
177
+ * @param {string} url - URL to download from
178
+ * @param {string} dest - Destination directory
179
+ * @param {object} options - Options
180
+ * @param {boolean} options.extract - Whether to extract the file (default: true)
181
+ * @param {string} options.displayName - Display name for progress output
182
+ * @returns {Promise<{filepath: string, extracted: boolean}>}
183
+ */
184
+ async function download (url, dest, { extract: doExtract = true, displayName } = {}) {
185
+ // Ensure dest directory exists
186
+ if (!fs.existsSync(dest)) {
187
+ fs.mkdirSync(dest, { recursive: true })
188
+ }
189
+
190
+ // Extract filename from URL
191
+ const urlParts = url.split('/')
192
+ const filename = urlParts[urlParts.length - 1].split('?')[0] || 'download'
193
+ const filepath = path.join(dest, filename)
194
+
195
+ // Apply proxy if configured
196
+ const downloadUrl = applyProxy(url)
197
+
198
+ const label = displayName || filename
199
+ const proxyInfo = GITHUB_PROXY ? ' [via proxy]' : ''
200
+
201
+ console.log('')
202
+ console.log(` Downloading: ${label}${proxyInfo}`)
203
+
204
+ let lastPercent = -1
205
+ await httpDownload(downloadUrl, filepath, 300000, (received, total, percent) => {
206
+ if (percent !== lastPercent && percent % 10 === 0) {
207
+ lastPercent = percent
208
+ const receivedStr = formatBytes(received)
209
+ const totalStr = formatBytes(total)
210
+ process.stdout.write(` Progress: ${percent}% (${receivedStr} / ${totalStr})\n`)
211
+ }
212
+ })
213
+
214
+ console.log(` Progress: 100% (${formatBytes(fs.statSync(filepath).size)})`)
215
+ console.log(' Download complete!')
216
+
217
+ let extracted = false
218
+ if (doExtract && (filepath.endsWith('.tar.gz') || filepath.endsWith('.tgz'))) {
219
+ console.log(' Extracting archive...')
220
+ await extractTarGz(filepath, dest)
221
+ // Clean up the downloaded archive
222
+ try {
223
+ fs.unlinkSync(filepath)
224
+ } catch (err) {
225
+ // Ignore cleanup errors
226
+ }
227
+ extracted = true
228
+ console.log(' Extraction complete!')
229
+ }
230
+
231
+ return { filepath, extracted }
232
+ }
233
+
234
+ /**
235
+ * Phin replacement - simple promisified HTTP client
236
+ * @param {object} options - Request options
237
+ * @param {string} options.url - URL to fetch
238
+ * @param {number} options.timeout - Request timeout (default: 15000)
239
+ * @returns {Promise<{body: Buffer, statusCode: number, headers: object}>}
240
+ */
241
+ async function phin (options) {
242
+ const { url, timeout = 15000 } = options
243
+ const body = await httpGet(url, timeout)
244
+
245
+ return {
246
+ body: Buffer.from(body),
247
+ statusCode: 200,
248
+ headers: {}
249
+ }
250
+ }
251
+
252
+ phin.promisified = phin
253
+
254
+ module.exports = {
255
+ httpGet,
256
+ httpDownload,
257
+ extractTarGz,
258
+ download,
259
+ phin,
260
+ applyProxy,
261
+ formatBytes,
262
+ GITHUB_PROXY
263
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electerm",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
4
4
  "description": "Terminal/ssh/telnet/serialport/sftp client(linux, mac, win)",
5
5
  "main": "app.js",
6
6
  "bin": "npm/electerm",
@@ -28,8 +28,7 @@
28
28
  "preferGlobal": true,
29
29
  "dependencies": {
30
30
  "shelljs": "*",
31
- "phin": "*",
32
- "download": "*"
31
+ "tar": "*"
33
32
  },
34
33
  "files": [
35
34
  "npm",