electerm 3.3.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,12 +1,19 @@
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')
12
+ const { resolve, join } = require('path')
13
+ const { execSync, rm, mv } = require('shelljs')
8
14
  const { execFile } = require('child_process')
9
- const { phin, download } = require('./utils')
15
+ const fs = require('fs')
16
+ const { phin, download, extractTarGz, GITHUB_PROXY, applyProxy } = require('./utils')
10
17
 
11
18
  const plat = os.platform()
12
19
  const arch = os.arch()
@@ -15,13 +22,15 @@ const { homepage } = require('../package.json')
15
22
  const releaseInfoUrl = `${homepage}/data/electerm-github-release.json?_=${+new Date()}`
16
23
  const versionUrl = `${homepage}/version.html?_=${+new Date()}`
17
24
 
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
+
18
30
  // ---------------------------------------------------------------------------
19
31
  // Security helpers
20
32
  // ---------------------------------------------------------------------------
21
33
 
22
- /**
23
- * Validate that a version string is a plain semver (e.g. "1.2.3" or "v1.2.3").
24
- */
25
34
  function sanitizeVersion (ver) {
26
35
  const clean = String(ver).trim().replace(/^v/, '')
27
36
  if (!/^\d+\.\d+\.\d+$/.test(clean)) {
@@ -32,9 +41,6 @@ function sanitizeVersion (ver) {
32
41
  return clean
33
42
  }
34
43
 
35
- /**
36
- * Validate that a release asset filename contains only safe characters.
37
- */
38
44
  function sanitizeFilename (name) {
39
45
  const clean = String(name).trim()
40
46
  if (!/^[\w.-]+$/.test(clean)) {
@@ -49,14 +55,6 @@ function sanitizeFilename (name) {
49
55
  // Core helpers
50
56
  // ---------------------------------------------------------------------------
51
57
 
52
- function down (url, extract = true) {
53
- const local = resolve(__dirname, '../')
54
- console.log('downloading ' + url)
55
- return download(url, local, { extract }).then(() => {
56
- console.log('done!')
57
- })
58
- }
59
-
60
58
  function getVer () {
61
59
  return phin({
62
60
  url: versionUrl,
@@ -79,50 +77,42 @@ function getReleaseInfo (filter) {
79
77
  }
80
78
 
81
79
  function showFinalMessage () {
82
- console.log('\n========================================')
80
+ console.log('')
81
+ console.log('========================================')
83
82
  console.log('electerm installation complete!')
84
83
  console.log('========================================')
85
- console.log('\nFor more information, documentation, and updates, please visit:')
86
- console.log('\x1b[36m%s\x1b[0m', 'https://electerm.html5beta.com')
87
- console.log('\nThank you for using electerm!')
88
- 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('')
89
91
  }
90
92
 
91
93
  // ---------------------------------------------------------------------------
92
94
  // Platform detection helpers
93
95
  // ---------------------------------------------------------------------------
94
96
 
95
- // Check if running on Windows 7 or earlier
96
97
  function isWindows7OrEarlier (platform, release) {
97
98
  if (platform !== 'win32') return false
98
- // Windows 7 is NT 6.1, Windows 8 is NT 6.2, Windows 10 is NT 10.0
99
99
  const [major, minor] = release.split('.').map(Number)
100
100
  return major < 10 && (major < 6 || (major === 6 && minor <= 1))
101
101
  }
102
102
 
103
- // Check if running on macOS 10.x (older than Big Sur 11.0)
104
103
  function isMacOS10 (platform, release) {
105
104
  if (platform !== 'darwin') return false
106
- // Darwin kernel version: macOS 11 (Big Sur) = Darwin 20.x, macOS 10.15 = Darwin 19.x
107
105
  const majorVersion = parseInt(release.split('.')[0], 10)
108
106
  return majorVersion < 20
109
107
  }
110
108
 
111
- // Check if running on Linux with old glibc (< 2.34)
112
- function isLinuxLegacy (platform, glibcVersion) {
109
+ function isLinuxLegacy (platform) {
113
110
  if (platform !== 'linux') return false
114
- if (typeof glibcVersion === 'number') {
115
- return glibcVersion < 2.34
116
- }
117
111
  try {
118
- const result = exec('ldd --version 2>&1 | head -n1', { silent: true })
119
- if (result.code !== 0) return false
120
- const output = result.stdout || ''
121
- // Extract version number like "ldd (GNU libc) 2.31" or "ldd (Ubuntu GLIBC 2.35-0ubuntu3) 2.35"
122
- 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*$/)
123
114
  if (match) {
124
- const version = parseFloat(match[1])
125
- return version < 2.34
115
+ return parseFloat(match[1]) < 2.34
126
116
  }
127
117
  return false
128
118
  } catch (e) {
@@ -130,137 +120,337 @@ function isLinuxLegacy (platform, glibcVersion) {
130
120
  }
131
121
  }
132
122
 
133
- // Get the file pattern for download based on platform/arch/legacy status
134
- function getDownloadPattern (platform, architecture, options = {}) {
135
- const { win7, mac10, linuxLegacy } = options
123
+ // ---------------------------------------------------------------------------
124
+ // Launch the extracted binary
125
+ // ---------------------------------------------------------------------------
136
126
 
137
- if (platform === 'win32') {
138
- if (win7) {
139
- return { pattern: /electerm-\d+\.\d+\.\d+-win7\.tar\.gz$/, type: 'win7' }
140
- } else if (architecture === 'arm64') {
141
- return { pattern: /electerm-\d+\.\d+\.\d+-win-arm64\.tar\.gz$/, type: 'win-arm64' }
142
- } else {
143
- return { pattern: /electerm-\d+\.\d+\.\d+-win-x64\.tar\.gz$/, type: 'win-x64' }
144
- }
145
- } else if (platform === 'darwin') {
146
- if (mac10) {
147
- return { pattern: /mac10-x64\.dmg$/, type: 'mac10-x64' }
148
- } else if (architecture === 'arm64') {
149
- return { pattern: /mac-arm64\.dmg$/, type: 'mac-arm64' }
150
- } else {
151
- return { pattern: /mac-x64\.dmg$/, type: 'mac-x64' }
152
- }
153
- } else if (platform === 'linux') {
154
- const suffix = linuxLegacy ? '-legacy' : ''
155
- if (architecture === 'arm64') {
156
- return { pattern: new RegExp(`linux-arm64${suffix}\\.tar\\.gz$`), type: `linux-arm64${suffix}` }
157
- } else if (architecture === 'arm') {
158
- return { pattern: new RegExp(`linux-armv7l${suffix}\\.tar\\.gz$`), type: `linux-armv7l${suffix}` }
159
- } else {
160
- return { pattern: new RegExp(`linux-x64${suffix}\\.tar\\.gz$`), type: `linux-x64${suffix}` }
161
- }
127
+ /**
128
+ * Get the path to the extracted electerm executable
129
+ */
130
+ function getElectermExePath () {
131
+ if (plat === 'win32') {
132
+ return join(extractDir, 'electerm.exe')
162
133
  }
134
+ // Linux and macOS (if extracted)
135
+ return join(extractDir, 'electerm')
136
+ }
163
137
 
164
- return { pattern: null, type: 'unsupported' }
138
+ /**
139
+ * Check if the electerm binary has been extracted already
140
+ */
141
+ function isElectermExtracted () {
142
+ const exePath = getElectermExePath()
143
+ return fs.existsSync(exePath)
165
144
  }
166
145
 
167
146
  // ---------------------------------------------------------------------------
168
147
  // Platform installers
148
+ // ---------------------------------------------------------------------------
149
+
169
150
  async function runLinux (folderName, filePattern) {
170
151
  const rawVer = await getVer()
171
- const ver = sanitizeVersion(rawVer) // throws if tampered
152
+ const ver = sanitizeVersion(rawVer)
172
153
 
173
- const target = resolve(__dirname, `../electerm-${ver}-${folderName}`)
174
- const targetNew = resolve(__dirname, '../electerm')
154
+ console.log(` Version: ${ver}`)
155
+ console.log(` Target: ${folderName}`)
175
156
 
176
- // Use shelljs array API — no shell string interpolation
177
- rm('-rf', [target, targetNew])
157
+ const target = join(packageRoot, `electerm-${ver}-${folderName}`)
178
158
 
159
+ // Clean up old installations
160
+ rm('-rf', [target, extractDir])
161
+
162
+ console.log(' Fetching release info...')
179
163
  const releaseInfo = await getReleaseInfo(r => r.name.includes(filePattern))
180
164
  if (!releaseInfo) {
181
165
  throw new Error(`No release found for pattern: ${filePattern}`)
182
166
  }
183
167
 
184
- await down(releaseInfo.browser_download_url)
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 })
185
173
 
186
- mv(target, targetNew)
187
- showFinalMessage()
188
- exec('electerm')
189
- }
174
+ const proxyUrl = applyProxy(releaseInfo.browser_download_url)
175
+ console.log(` URL: ${proxyUrl}`)
190
176
 
191
- async function runMac (archName) {
192
- const pattern = new RegExp(`mac-${archName}\\.dmg$`)
193
- const releaseInfo = await getReleaseInfo(r => pattern.test(r.name))
194
- if (!releaseInfo) {
195
- throw new Error(`No release found for Mac ${archName}`)
196
- }
177
+ const { filepath } = await download(releaseInfo.browser_download_url, tmpDir, { extract: false, displayName: releaseInfo.name })
197
178
 
198
- const safeName = sanitizeFilename(releaseInfo.name) // throws if tampered
199
- await down(releaseInfo.browser_download_url, false)
179
+ // Extract to tmpDir (keeps top-level folder name)
180
+ await extractTarGz(filepath, tmpDir)
200
181
 
201
- const target = resolve(__dirname, '../', safeName)
202
- showFinalMessage()
203
-
204
- // execFile does not spawn a shell — no injection possible
205
- execFile('open', [target])
206
- }
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())
207
185
 
208
- async function runMac10 () {
209
- const releaseInfo = await getReleaseInfo(r => /mac10-x64\.dmg$/.test(r.name))
210
- if (!releaseInfo) {
211
- throw new Error('No release found for macOS 10.x')
186
+ if (!extractedFolder) {
187
+ throw new Error('No folder found in extracted archive')
212
188
  }
213
189
 
214
- const safeName = sanitizeFilename(releaseInfo.name) // throws if tampered
215
- await down(releaseInfo.browser_download_url, false)
190
+ // Move to extractDir
191
+ console.log(` Installing to: ${extractDir}`)
192
+ mv(join(tmpDir, extractedFolder), extractDir)
216
193
 
217
- const target = resolve(__dirname, '../', safeName)
218
- showFinalMessage()
194
+ // Clean up temp files
195
+ rm('-rf', tmpDir)
219
196
 
220
- // execFile does not spawn a shell — no injection possible
221
- execFile('open', [target])
197
+ showFinalMessage()
222
198
  }
223
199
 
224
200
  async function runWin (archName) {
225
201
  const rawVer = await getVer()
226
- const ver = sanitizeVersion(rawVer) // consistent hardening
202
+ const ver = sanitizeVersion(rawVer)
203
+
204
+ console.log(` Version: ${ver}`)
205
+ console.log(` Target: win-${archName}`)
227
206
 
228
- const target = resolve(__dirname, `../electerm-${ver}-win-${archName}`)
229
- const targetNew = resolve(__dirname, '../electerm')
207
+ const target = join(packageRoot, `electerm-${ver}-win-${archName}`)
230
208
 
231
- rm('-rf', [target, targetNew])
209
+ rm('-rf', [target, extractDir])
232
210
 
233
211
  const pattern = new RegExp(`electerm-\\d+\\.\\d+\\.\\d+-win-${archName}\\.tar\\.gz$`)
212
+ console.log(' Fetching release info...')
234
213
  const releaseInfo = await getReleaseInfo(r => pattern.test(r.name))
235
214
  if (!releaseInfo) {
236
215
  throw new Error(`No release found for Windows ${archName}`)
237
216
  }
238
217
 
239
- await down(releaseInfo.browser_download_url)
240
- await mv(target, targetNew)
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')
235
+ }
236
+
237
+ console.log(` Installing to: ${extractDir}`)
238
+ mv(join(tmpDir, extractedFolder), extractDir)
239
+ rm('-rf', tmpDir)
240
+
241
241
  showFinalMessage()
242
- require('child_process').execFile(`${targetNew}\\electerm.exe`)
243
242
  }
244
243
 
245
- // Windows 7 specific version
246
244
  async function runWin7 () {
247
245
  const rawVer = await getVer()
248
- const ver = sanitizeVersion(rawVer) // consistent hardening
246
+ const ver = sanitizeVersion(rawVer)
247
+
248
+ console.log(` Version: ${ver}`)
249
+ console.log(' Target: win7')
249
250
 
250
- const target = resolve(__dirname, `../electerm-${ver}-win7`)
251
- const targetNew = resolve(__dirname, '../electerm')
251
+ const target = join(packageRoot, `electerm-${ver}-win7`)
252
252
 
253
- rm('-rf', [target, targetNew])
253
+ rm('-rf', [target, extractDir])
254
254
 
255
+ console.log(' Fetching release info...')
255
256
  const releaseInfo = await getReleaseInfo(r => /electerm-\d+\.\d+\.\d+-win7\.tar\.gz$/.test(r.name))
256
257
  if (!releaseInfo) {
257
258
  throw new Error('No release found for Windows 7')
258
259
  }
259
260
 
260
- await down(releaseInfo.browser_download_url)
261
- await mv(target, targetNew)
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
+
262
284
  showFinalMessage()
263
- require('child_process').execFile(`${targetNew}\\electerm.exe`)
285
+ }
286
+
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...')
378
+ const releaseInfo = await getReleaseInfo(r => pattern.test(r.name))
379
+ if (!releaseInfo) {
380
+ throw new Error(`No release found for Mac ${archName}`)
381
+ }
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)
390
+ showFinalMessage()
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
+ }
414
+ }
415
+
416
+ async function runMac10 () {
417
+ console.log(' Fetching release info...')
418
+ const releaseInfo = await getReleaseInfo(r => /mac10-x64\.dmg$/.test(r.name))
419
+ if (!releaseInfo) {
420
+ throw new Error('No release found for macOS 10.x')
421
+ }
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)
430
+ showFinalMessage()
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
+ }
264
454
  }
265
455
 
266
456
  // ---------------------------------------------------------------------------
@@ -268,18 +458,28 @@ async function runWin7 () {
268
458
  // ---------------------------------------------------------------------------
269
459
 
270
460
  async function main () {
271
- 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('')
272
472
 
273
473
  // Check for legacy systems
274
474
  const win7 = isWindows7OrEarlier(plat, os.release())
275
475
  const mac10 = isMacOS10(plat, os.release())
276
476
  const linuxLegacy = isLinuxLegacy(plat)
277
477
 
278
- if (win7) console.log('Detected: Windows 7 or earlier')
279
- if (mac10) console.log('Detected: macOS 10.x')
280
- 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)')
281
481
 
282
- console.log('Fetching release information...\n')
482
+ console.log(' Fetching release information...')
283
483
 
284
484
  try {
285
485
  if (plat === 'win32') {
@@ -300,7 +500,7 @@ async function main () {
300
500
  }
301
501
  } else if (plat === 'linux') {
302
502
  const suffix = linuxLegacy ? '-legacy' : ''
303
- if (arch === 'arm64') {
503
+ if (arch === 'arm64' || arch === 'aarch64') {
304
504
  await runLinux(`linux-arm64${suffix}`, `linux-arm64${suffix}.tar.gz`)
305
505
  } else if (arch === 'arm') {
306
506
  await runLinux(`linux-armv7l${suffix}`, `linux-armv7l${suffix}.tar.gz`)
@@ -311,32 +511,37 @@ async function main () {
311
511
  throw new Error(`Platform "${plat}" is not supported.`)
312
512
  }
313
513
  } catch (err) {
314
- console.error('\n========================================')
514
+ console.error('')
515
+ console.error('========================================')
315
516
  console.error('Installation failed!')
316
517
  console.error('========================================')
317
518
  console.error(`Error: ${err.message}`)
318
- console.error(`\nPlatform: ${plat}, Architecture: ${arch}`)
319
- console.error('\nPlease visit https://electerm.html5beta.com for manual download options.')
320
- 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('')
321
524
  process.exit(1)
322
525
  }
323
526
  }
324
527
 
325
528
  // ---------------------------------------------------------------------------
326
- // Exports
529
+ // Exports for testing
327
530
  // ---------------------------------------------------------------------------
328
531
 
329
- // Export functions for testing
330
532
  module.exports = {
331
533
  isWindows7OrEarlier,
332
534
  isMacOS10,
333
535
  isLinuxLegacy,
334
- getDownloadPattern,
335
536
  sanitizeVersion,
336
- sanitizeFilename
537
+ sanitizeFilename,
538
+ getElectermExePath,
539
+ isElectermExtracted,
540
+ // Expose for test injection
541
+ _packageRoot: packageRoot,
542
+ _extractDir: extractDir
337
543
  }
338
544
 
339
- // Run main only if this file is executed directly
340
545
  if (require.main === module) {
341
546
  main()
342
547
  }
package/npm/utils.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * Utility functions for npm installer
3
3
  * Replaces download and phin packages with native Node.js http/https and tar
4
4
  * Supports Node.js 16+
5
+ * Supports GITHUB_PROXY environment variable for proxying GitHub URLs
5
6
  */
6
7
 
7
8
  const https = require('https')
@@ -10,13 +11,46 @@ const fs = require('fs')
10
11
  const path = require('path')
11
12
  const tar = require('tar')
12
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
+
13
45
  /**
14
- * Make an HTTP GET request
46
+ * Make an HTTP GET request and download to a file with progress
15
47
  * @param {string} url - URL to fetch
16
- * @param {number} timeout - Request timeout in milliseconds (default: 15000)
17
- * @returns {Promise<string>} Response body as string
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
18
52
  */
19
- function httpGet (url, timeout = 15000) {
53
+ function httpDownload (url, filepath, timeout = 300000, onProgress) {
20
54
  return new Promise((resolve, reject) => {
21
55
  const client = url.startsWith('https') ? https : http
22
56
 
@@ -24,7 +58,13 @@ function httpGet (url, timeout = 15000) {
24
58
  // Handle redirects
25
59
  if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) {
26
60
  if (res.headers.location) {
27
- resolve(httpGet(res.headers.location, timeout))
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))
28
68
  return
29
69
  }
30
70
  }
@@ -34,13 +74,32 @@ function httpGet (url, timeout = 15000) {
34
74
  return
35
75
  }
36
76
 
37
- const chunks = []
38
- res.on('data', (chunk) => chunks.push(chunk))
39
- res.on('end', () => {
40
- const buffer = Buffer.concat(chunks)
41
- resolve(buffer.toString())
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)
42
102
  })
43
- res.on('error', reject)
44
103
  })
45
104
 
46
105
  req.on('error', reject)
@@ -52,20 +111,26 @@ function httpGet (url, timeout = 15000) {
52
111
  }
53
112
 
54
113
  /**
55
- * Download a file from URL to local path
56
- * @param {string} url - URL to download from
57
- * @param {string} dest - Destination directory path
58
- * @returns {Promise<string>} Path to downloaded file
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
59
118
  */
60
- function downloadFile (url, dest) {
119
+ function httpGet (url, timeout = 15000) {
61
120
  return new Promise((resolve, reject) => {
62
121
  const client = url.startsWith('https') ? https : http
63
122
 
64
- const req = client.get(url, { timeout: 300000 }, (res) => {
123
+ const req = client.get(url, { timeout }, (res) => {
65
124
  // Handle redirects
66
125
  if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) {
67
126
  if (res.headers.location) {
68
- resolve(downloadFile(res.headers.location, dest))
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))
69
134
  return
70
135
  }
71
136
  }
@@ -75,37 +140,19 @@ function downloadFile (url, dest) {
75
140
  return
76
141
  }
77
142
 
78
- // Extract filename from URL or Content-Disposition header
79
- let filename = 'download'
80
- const contentDisposition = res.headers['content-disposition']
81
- if (contentDisposition) {
82
- const match = contentDisposition.match(/filename[^;=\n]*=(['"]?)([^'";\n]*)\1/)
83
- if (match) {
84
- filename = match[2]
85
- }
86
- } else {
87
- const urlParts = url.split('/')
88
- filename = urlParts[urlParts.length - 1].split('?')[0] || 'download'
89
- }
90
-
91
- const filepath = path.join(dest, filename)
92
- const fileStream = fs.createWriteStream(filepath)
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)
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())
102
148
  })
149
+ res.on('error', reject)
103
150
  })
104
151
 
105
152
  req.on('error', reject)
106
153
  req.on('timeout', () => {
107
154
  req.destroy()
108
- reject(new Error('Download timeout after 300s'))
155
+ reject(new Error(`Request timeout after ${timeout}ms`))
109
156
  })
110
157
  })
111
158
  }
@@ -114,40 +161,74 @@ function downloadFile (url, dest) {
114
161
  * Extract a tar.gz file to destination directory
115
162
  * @param {string} filepath - Path to tar.gz file
116
163
  * @param {string} dest - Destination directory
164
+ * @param {number} strip - Number of leading path components to strip (default: 0)
117
165
  * @returns {Promise<void>}
118
166
  */
119
- function extractTarGz (filepath, dest) {
167
+ function extractTarGz (filepath, dest, strip = 0) {
120
168
  return tar.extract({
121
169
  file: filepath,
122
170
  cwd: dest,
123
- strip: 1 // Strip top-level directory
171
+ strip
124
172
  })
125
173
  }
126
174
 
127
175
  /**
128
- * Download and optionally extract a file
129
- * Replaces the download package functionality
176
+ * Download and optionally extract a file with progress
130
177
  * @param {string} url - URL to download from
131
178
  * @param {string} dest - Destination directory
132
- * @param {boolean} extract - Whether to extract the file (default: true)
133
- * @returns {Promise<void>}
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}>}
134
183
  */
135
- async function download (url, dest, { extract: doExtract = true } = {}) {
136
- console.log('downloading ' + url)
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
+ })
137
213
 
138
- const filepath = await downloadFile(url, dest)
214
+ console.log(` Progress: 100% (${formatBytes(fs.statSync(filepath).size)})`)
215
+ console.log(' Download complete!')
139
216
 
217
+ let extracted = false
140
218
  if (doExtract && (filepath.endsWith('.tar.gz') || filepath.endsWith('.tgz'))) {
219
+ console.log(' Extracting archive...')
141
220
  await extractTarGz(filepath, dest)
142
221
  // Clean up the downloaded archive
143
222
  try {
144
223
  fs.unlinkSync(filepath)
145
224
  } catch (err) {
146
- console.warn('Warning: Failed to clean up downloaded archive:', err.message)
225
+ // Ignore cleanup errors
147
226
  }
227
+ extracted = true
228
+ console.log(' Extraction complete!')
148
229
  }
149
230
 
150
- console.log('done!')
231
+ return { filepath, extracted }
151
232
  }
152
233
 
153
234
  /**
@@ -155,7 +236,7 @@ async function download (url, dest, { extract: doExtract = true } = {}) {
155
236
  * @param {object} options - Request options
156
237
  * @param {string} options.url - URL to fetch
157
238
  * @param {number} options.timeout - Request timeout (default: 15000)
158
- * @returns {Promise<{body: string, statusCode: number, headers: object}>}
239
+ * @returns {Promise<{body: Buffer, statusCode: number, headers: object}>}
159
240
  */
160
241
  async function phin (options) {
161
242
  const { url, timeout = 15000 } = options
@@ -168,13 +249,15 @@ async function phin (options) {
168
249
  }
169
250
  }
170
251
 
171
- // Export promisified version
172
252
  phin.promisified = phin
173
253
 
174
254
  module.exports = {
175
255
  httpGet,
176
- downloadFile,
256
+ httpDownload,
177
257
  extractTarGz,
178
258
  download,
179
- phin
259
+ phin,
260
+ applyProxy,
261
+ formatBytes,
262
+ GITHUB_PROXY
180
263
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electerm",
3
- "version": "3.3.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",
@@ -1,21 +0,0 @@
1
- Copyright (c) Electron contributors
2
- Copyright (c) 2013-2020 GitHub Inc.
3
-
4
- Permission is hereby granted, free of charge, to any person obtaining
5
- a copy of this software and associated documentation files (the
6
- "Software"), to deal in the Software without restriction, including
7
- without limitation the rights to use, copy, modify, merge, publish,
8
- distribute, sublicense, and/or sell copies of the Software, and to
9
- permit persons to whom the Software is furnished to do so, subject to
10
- the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be
13
- included in all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.