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 +100 -4
- package/npm/install.js +400 -128
- package/npm/utils.js +263 -0
- package/package.json +2 -3
package/npm/electerm
CHANGED
|
@@ -1,4 +1,100 @@
|
|
|
1
|
-
#!/bin/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
|
8
|
-
const
|
|
9
|
-
const
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
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('
|
|
80
|
+
console.log('')
|
|
81
|
+
console.log('========================================')
|
|
47
82
|
console.log('electerm installation complete!')
|
|
48
83
|
console.log('========================================')
|
|
49
|
-
console.log('
|
|
50
|
-
console.log('
|
|
51
|
-
console.log('
|
|
52
|
-
console.log('
|
|
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
|
-
//
|
|
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
|
-
|
|
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 =
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Launch the extracted binary
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
96
126
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
|
142
|
-
const
|
|
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
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
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
|
|
258
|
+
throw new Error('No release found for Windows 7')
|
|
158
259
|
}
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
380
|
+
throw new Error(`No release found for Mac ${archName}`)
|
|
177
381
|
}
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
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
|
|
420
|
+
throw new Error('No release found for macOS 10.x')
|
|
196
421
|
}
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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('
|
|
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(
|
|
258
|
-
console.error('
|
|
259
|
-
console.error('
|
|
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
|
-
//
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
// Exports for testing
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
|
|
265
532
|
module.exports = {
|
|
266
533
|
isWindows7OrEarlier,
|
|
267
534
|
isMacOS10,
|
|
268
535
|
isLinuxLegacy,
|
|
269
|
-
|
|
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.
|
|
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
|
-
"
|
|
32
|
-
"download": "*"
|
|
31
|
+
"tar": "*"
|
|
33
32
|
},
|
|
34
33
|
"files": [
|
|
35
34
|
"npm",
|