electerm 3.3.0 → 3.3.2
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 +73 -4
- package/npm/install.js +448 -130
- package/npm/utils.js +141 -58
- package/package.json +1 -1
- package/LICENSE.electron.txt +0 -21
package/npm/electerm
CHANGED
|
@@ -1,4 +1,73 @@
|
|
|
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
|
+
function getElectermExePath () {
|
|
22
|
+
if (plat === 'darwin') {
|
|
23
|
+
const appBinary = '/Applications/electerm.app/Contents/MacOS/electerm'
|
|
24
|
+
if (fs.existsSync(appBinary)) {
|
|
25
|
+
return appBinary
|
|
26
|
+
}
|
|
27
|
+
return path.join(packageRoot, 'electerm', 'electerm')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (plat === 'win32') {
|
|
31
|
+
return path.join(packageRoot, 'electerm', 'electerm.exe')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return path.join(packageRoot, 'electerm', 'electerm')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function launchElecterm () {
|
|
38
|
+
const exePath = getElectermExePath()
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(exePath)) {
|
|
41
|
+
console.error('electerm binary not found at:', exePath)
|
|
42
|
+
console.error('')
|
|
43
|
+
console.error('The binary may not have been installed properly.')
|
|
44
|
+
console.error('Try running manually:')
|
|
45
|
+
console.error(' node', path.join(packageRoot, 'npm', 'install.js'))
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const child = spawn(exePath, process.argv.slice(2), {
|
|
50
|
+
stdio: 'inherit',
|
|
51
|
+
detached: plat !== 'win32',
|
|
52
|
+
windowsHide: false
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (plat !== 'win32') {
|
|
56
|
+
child.unref()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
child.on('error', (err) => {
|
|
60
|
+
console.error('Failed to start electerm:', err.message)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
child.on('exit', (code) => {
|
|
65
|
+
process.exit(code || 0)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (require.main === module) {
|
|
70
|
+
launchElecterm()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { 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 {
|
|
12
|
+
const { resolve, join } = require('path')
|
|
13
|
+
const { execSync, rm, mv } = require('shelljs')
|
|
8
14
|
const { execFile } = require('child_process')
|
|
9
|
-
const
|
|
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('
|
|
80
|
+
console.log('')
|
|
81
|
+
console.log('========================================')
|
|
83
82
|
console.log('electerm installation complete!')
|
|
84
83
|
console.log('========================================')
|
|
85
|
-
console.log('
|
|
86
|
-
console.log('
|
|
87
|
-
console.log('
|
|
88
|
-
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('')
|
|
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
|
-
|
|
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 =
|
|
119
|
-
|
|
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
|
-
|
|
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,450 @@ function isLinuxLegacy (platform, glibcVersion) {
|
|
|
130
120
|
}
|
|
131
121
|
}
|
|
132
122
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Launch the extracted binary
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
136
126
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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)
|
|
152
|
+
const ver = sanitizeVersion(rawVer)
|
|
172
153
|
|
|
173
|
-
|
|
174
|
-
|
|
154
|
+
console.log(` Version: ${ver}`)
|
|
155
|
+
console.log(` Target: ${folderName}`)
|
|
175
156
|
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
exec('electerm')
|
|
189
|
-
}
|
|
174
|
+
const proxyUrl = applyProxy(releaseInfo.browser_download_url)
|
|
175
|
+
console.log(` URL: ${proxyUrl}`)
|
|
190
176
|
|
|
191
|
-
|
|
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
|
-
|
|
199
|
-
await
|
|
179
|
+
// Extract to tmpDir (keeps top-level folder name)
|
|
180
|
+
await extractTarGz(filepath, tmpDir)
|
|
200
181
|
|
|
201
|
-
|
|
202
|
-
|
|
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())
|
|
203
185
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
186
|
+
if (!extractedFolder) {
|
|
187
|
+
throw new Error('No folder found in extracted archive')
|
|
188
|
+
}
|
|
207
189
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
190
|
+
// Move to extractDir
|
|
191
|
+
console.log(` Installing to: ${extractDir}`)
|
|
192
|
+
mv(join(tmpDir, extractedFolder), extractDir)
|
|
193
|
+
|
|
194
|
+
// Fix chrome-sandbox permissions on Linux (Electron requires specific permissions)
|
|
195
|
+
if (plat === 'linux') {
|
|
196
|
+
const chromeSandboxPath = join(extractDir, 'chrome-sandbox')
|
|
197
|
+
if (fs.existsSync(chromeSandboxPath)) {
|
|
198
|
+
console.log(' Fixing chrome-sandbox permissions...')
|
|
199
|
+
fs.chmodSync(chromeSandboxPath, 0o4755)
|
|
200
|
+
}
|
|
212
201
|
}
|
|
213
202
|
|
|
214
|
-
|
|
215
|
-
|
|
203
|
+
// Clean up temp files
|
|
204
|
+
rm('-rf', tmpDir)
|
|
216
205
|
|
|
217
|
-
const target = resolve(__dirname, '../', safeName)
|
|
218
206
|
showFinalMessage()
|
|
219
|
-
|
|
220
|
-
// execFile does not spawn a shell — no injection possible
|
|
221
|
-
execFile('open', [target])
|
|
222
207
|
}
|
|
223
208
|
|
|
224
209
|
async function runWin (archName) {
|
|
210
|
+
console.log(' [DEBUG] runWin started')
|
|
211
|
+
console.log(` [DEBUG] packageRoot: ${packageRoot}`)
|
|
212
|
+
console.log(` [DEBUG] extractDir: ${extractDir}`)
|
|
213
|
+
|
|
225
214
|
const rawVer = await getVer()
|
|
226
|
-
const ver = sanitizeVersion(rawVer)
|
|
215
|
+
const ver = sanitizeVersion(rawVer)
|
|
216
|
+
|
|
217
|
+
console.log(` [DEBUG] Raw version from server: ${rawVer}`)
|
|
218
|
+
console.log(` Sanitized version: ${ver}`)
|
|
219
|
+
console.log(` Target: win-${archName}`)
|
|
227
220
|
|
|
228
|
-
const target =
|
|
229
|
-
|
|
221
|
+
const target = join(packageRoot, `electerm-${ver}-win-${archName}`)
|
|
222
|
+
console.log(` [DEBUG] Target folder: ${target}`)
|
|
230
223
|
|
|
231
|
-
|
|
224
|
+
console.log(' Cleaning old installations...')
|
|
225
|
+
rm('-rf', [target, extractDir])
|
|
226
|
+
console.log(' [DEBUG] Old installations cleaned')
|
|
232
227
|
|
|
233
228
|
const pattern = new RegExp(`electerm-\\d+\\.\\d+\\.\\d+-win-${archName}\\.tar\\.gz$`)
|
|
229
|
+
console.log(' Fetching release info...')
|
|
234
230
|
const releaseInfo = await getReleaseInfo(r => pattern.test(r.name))
|
|
235
231
|
if (!releaseInfo) {
|
|
236
232
|
throw new Error(`No release found for Windows ${archName}`)
|
|
237
233
|
}
|
|
234
|
+
console.log(` [DEBUG] Release info found: ${JSON.stringify(releaseInfo, null, 2)}`)
|
|
235
|
+
|
|
236
|
+
const tmpDir = join(packageRoot, '.electerm-tmp')
|
|
237
|
+
console.log(` [DEBUG] Creating temp directory: ${tmpDir}`)
|
|
238
|
+
rm('-rf', tmpDir)
|
|
239
|
+
fs.mkdirSync(tmpDir, { recursive: true })
|
|
240
|
+
|
|
241
|
+
const proxyUrl = applyProxy(releaseInfo.browser_download_url)
|
|
242
|
+
console.log(` [DEBUG] Proxy URL: ${proxyUrl}`)
|
|
243
|
+
console.log(` [DEBUG] Download URL: ${releaseInfo.browser_download_url}`)
|
|
244
|
+
|
|
245
|
+
console.log(' Downloading...')
|
|
246
|
+
const { filepath } = await download(releaseInfo.browser_download_url, tmpDir, { extract: false, displayName: releaseInfo.name })
|
|
247
|
+
console.log(` [DEBUG] Downloaded to: ${filepath}`)
|
|
248
|
+
console.log(` [DEBUG] File exists: ${fs.existsSync(filepath)}`)
|
|
249
|
+
console.log(` [DEBUG] File size: ${fs.statSync(filepath).size}`)
|
|
250
|
+
|
|
251
|
+
console.log(' Extracting...')
|
|
252
|
+
await extractTarGz(filepath, tmpDir)
|
|
253
|
+
console.log(' [DEBUG] Extraction complete')
|
|
254
|
+
|
|
255
|
+
console.log(' [DEBUG] Listing temp directory contents:')
|
|
256
|
+
const entries = fs.readdirSync(tmpDir)
|
|
257
|
+
entries.forEach(e => {
|
|
258
|
+
const fullPath = join(tmpDir, e)
|
|
259
|
+
const stat = fs.statSync(fullPath)
|
|
260
|
+
console.log(` [DEBUG] ${e} - ${stat.isDirectory() ? 'DIR' : 'FILE'} (${stat.size} bytes)`)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
const extractedFolder = entries.find(e => fs.statSync(join(tmpDir, e)).isDirectory())
|
|
264
|
+
|
|
265
|
+
if (!extractedFolder) {
|
|
266
|
+
console.error(' [DEBUG] No directory found in extracted archive')
|
|
267
|
+
console.error(' [DEBUG] All entries:', entries)
|
|
268
|
+
throw new Error('No folder found in extracted archive')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log(` [DEBUG] Extracted folder: ${extractedFolder}`)
|
|
272
|
+
console.log(' [DEBUG] Contents of extracted folder:')
|
|
273
|
+
const extractedContents = fs.readdirSync(join(tmpDir, extractedFolder))
|
|
274
|
+
extractedContents.forEach(e => {
|
|
275
|
+
const fullPath = join(tmpDir, extractedFolder, e)
|
|
276
|
+
const stat = fs.statSync(fullPath)
|
|
277
|
+
console.log(` [DEBUG] ${e} - ${stat.isDirectory() ? 'DIR' : 'FILE'} (${stat.size} bytes)`)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
console.log(` Installing to: ${extractDir}`)
|
|
281
|
+
fs.renameSync(join(tmpDir, extractedFolder), extractDir)
|
|
282
|
+
console.log(' [DEBUG] Renamed folder to extractDir')
|
|
283
|
+
|
|
284
|
+
console.log(' [DEBUG] Verifying extractDir contents:')
|
|
285
|
+
const installContents = fs.readdirSync(extractDir)
|
|
286
|
+
installContents.forEach(e => {
|
|
287
|
+
const fullPath = join(extractDir, e)
|
|
288
|
+
const stat = fs.statSync(fullPath)
|
|
289
|
+
console.log(` [DEBUG] ${e} - ${stat.isDirectory() ? 'DIR' : 'FILE'} (${stat.size} bytes)`)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
rm('-rf', tmpDir)
|
|
293
|
+
console.log(' [DEBUG] Temp directory cleaned')
|
|
294
|
+
|
|
295
|
+
const exePath = getElectermExePath()
|
|
296
|
+
console.log(` [DEBUG] Expected exe path: ${exePath}`)
|
|
297
|
+
console.log(` [DEBUG] Exe exists: ${fs.existsSync(exePath)}`)
|
|
298
|
+
if (!fs.existsSync(exePath)) {
|
|
299
|
+
throw new Error(`electerm.exe not found at ${exePath} after extraction. Archive may have an unexpected structure.`)
|
|
300
|
+
}
|
|
238
301
|
|
|
239
|
-
await down(releaseInfo.browser_download_url)
|
|
240
|
-
await mv(target, targetNew)
|
|
241
302
|
showFinalMessage()
|
|
242
|
-
require('child_process').execFile(`${targetNew}\\electerm.exe`)
|
|
243
303
|
}
|
|
244
304
|
|
|
245
|
-
// Windows 7 specific version
|
|
246
305
|
async function runWin7 () {
|
|
306
|
+
console.log(' [DEBUG] runWin7 started')
|
|
307
|
+
console.log(` [DEBUG] packageRoot: ${packageRoot}`)
|
|
308
|
+
console.log(` [DEBUG] extractDir: ${extractDir}`)
|
|
309
|
+
|
|
247
310
|
const rawVer = await getVer()
|
|
248
|
-
const ver = sanitizeVersion(rawVer)
|
|
311
|
+
const ver = sanitizeVersion(rawVer)
|
|
312
|
+
|
|
313
|
+
console.log(` [DEBUG] Raw version from server: ${rawVer}`)
|
|
314
|
+
console.log(` Sanitized version: ${ver}`)
|
|
315
|
+
console.log(' Target: win7')
|
|
249
316
|
|
|
250
|
-
const target =
|
|
251
|
-
|
|
317
|
+
const target = join(packageRoot, `electerm-${ver}-win7`)
|
|
318
|
+
console.log(` [DEBUG] Target folder: ${target}`)
|
|
252
319
|
|
|
253
|
-
|
|
320
|
+
console.log(' Cleaning old installations...')
|
|
321
|
+
rm('-rf', [target, extractDir])
|
|
322
|
+
console.log(' [DEBUG] Old installations cleaned')
|
|
254
323
|
|
|
324
|
+
console.log(' Fetching release info...')
|
|
255
325
|
const releaseInfo = await getReleaseInfo(r => /electerm-\d+\.\d+\.\d+-win7\.tar\.gz$/.test(r.name))
|
|
256
326
|
if (!releaseInfo) {
|
|
257
327
|
throw new Error('No release found for Windows 7')
|
|
258
328
|
}
|
|
329
|
+
console.log(` [DEBUG] Release info found: ${JSON.stringify(releaseInfo, null, 2)}`)
|
|
330
|
+
|
|
331
|
+
const tmpDir = join(packageRoot, '.electerm-tmp')
|
|
332
|
+
console.log(` [DEBUG] Creating temp directory: ${tmpDir}`)
|
|
333
|
+
rm('-rf', tmpDir)
|
|
334
|
+
fs.mkdirSync(tmpDir, { recursive: true })
|
|
335
|
+
|
|
336
|
+
const proxyUrl = applyProxy(releaseInfo.browser_download_url)
|
|
337
|
+
console.log(` [DEBUG] Proxy URL: ${proxyUrl}`)
|
|
338
|
+
console.log(` [DEBUG] Download URL: ${releaseInfo.browser_download_url}`)
|
|
339
|
+
|
|
340
|
+
console.log(' Downloading...')
|
|
341
|
+
const { filepath } = await download(releaseInfo.browser_download_url, tmpDir, { extract: false, displayName: releaseInfo.name })
|
|
342
|
+
console.log(` [DEBUG] Downloaded to: ${filepath}`)
|
|
343
|
+
console.log(` [DEBUG] File exists: ${fs.existsSync(filepath)}`)
|
|
344
|
+
console.log(` [DEBUG] File size: ${fs.statSync(filepath).size}`)
|
|
345
|
+
|
|
346
|
+
console.log(' Extracting...')
|
|
347
|
+
await extractTarGz(filepath, tmpDir)
|
|
348
|
+
console.log(' [DEBUG] Extraction complete')
|
|
349
|
+
|
|
350
|
+
console.log(' [DEBUG] Listing temp directory contents:')
|
|
351
|
+
const entries = fs.readdirSync(tmpDir)
|
|
352
|
+
entries.forEach(e => {
|
|
353
|
+
const fullPath = join(tmpDir, e)
|
|
354
|
+
const stat = fs.statSync(fullPath)
|
|
355
|
+
console.log(` [DEBUG] ${e} - ${stat.isDirectory() ? 'DIR' : 'FILE'} (${stat.size} bytes)`)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
const extractedFolder = entries.find(e => fs.statSync(join(tmpDir, e)).isDirectory())
|
|
359
|
+
|
|
360
|
+
if (!extractedFolder) {
|
|
361
|
+
console.error(' [DEBUG] No directory found in extracted archive')
|
|
362
|
+
console.error(' [DEBUG] All entries:', entries)
|
|
363
|
+
throw new Error('No folder found in extracted archive')
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.log(` [DEBUG] Extracted folder: ${extractedFolder}`)
|
|
367
|
+
console.log(' [DEBUG] Contents of extracted folder:')
|
|
368
|
+
const extractedContents = fs.readdirSync(join(tmpDir, extractedFolder))
|
|
369
|
+
extractedContents.forEach(e => {
|
|
370
|
+
const fullPath = join(tmpDir, extractedFolder, e)
|
|
371
|
+
const stat = fs.statSync(fullPath)
|
|
372
|
+
console.log(` [DEBUG] ${e} - ${stat.isDirectory() ? 'DIR' : 'FILE'} (${stat.size} bytes)`)
|
|
373
|
+
})
|
|
259
374
|
|
|
260
|
-
|
|
261
|
-
|
|
375
|
+
console.log(` Installing to: ${extractDir}`)
|
|
376
|
+
fs.renameSync(join(tmpDir, extractedFolder), extractDir)
|
|
377
|
+
console.log(' [DEBUG] Renamed folder to extractDir')
|
|
378
|
+
|
|
379
|
+
console.log(' [DEBUG] Verifying extractDir contents:')
|
|
380
|
+
const installContents = fs.readdirSync(extractDir)
|
|
381
|
+
installContents.forEach(e => {
|
|
382
|
+
const fullPath = join(extractDir, e)
|
|
383
|
+
const stat = fs.statSync(fullPath)
|
|
384
|
+
console.log(` [DEBUG] ${e} - ${stat.isDirectory() ? 'DIR' : 'FILE'} (${stat.size} bytes)`)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
rm('-rf', tmpDir)
|
|
388
|
+
console.log(' [DEBUG] Temp directory cleaned')
|
|
389
|
+
|
|
390
|
+
const exePath = getElectermExePath()
|
|
391
|
+
console.log(` [DEBUG] Expected exe path: ${exePath}`)
|
|
392
|
+
console.log(` [DEBUG] Exe exists: ${fs.existsSync(exePath)}`)
|
|
393
|
+
if (!fs.existsSync(exePath)) {
|
|
394
|
+
throw new Error(`electerm.exe not found at ${exePath} after extraction. Archive may have an unexpected structure.`)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
showFinalMessage()
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Mount a DMG, copy the .app to /Applications, then detach
|
|
402
|
+
* @param {string} dmgPath - Path to the DMG file
|
|
403
|
+
* @returns {Promise<string>} - Path to the installed app
|
|
404
|
+
*/
|
|
405
|
+
function installFromDmg (dmgPath) {
|
|
406
|
+
return new Promise((resolve, reject) => {
|
|
407
|
+
// Step 1: Mount the DMG
|
|
408
|
+
console.log(' Mounting DMG...')
|
|
409
|
+
execFile('hdiutil', ['attach', dmgPath, '-nobrowse', '-readonly'], (err, stdout) => {
|
|
410
|
+
if (err) {
|
|
411
|
+
reject(new Error(`Failed to mount DMG: ${err.message}`))
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Parse mount point
|
|
416
|
+
const mountMatch = stdout.match(/(\/Volumes\/[^\n]+)/)
|
|
417
|
+
if (!mountMatch) {
|
|
418
|
+
reject(new Error('Could not find mount point'))
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const mountPoint = mountMatch[1].trim()
|
|
423
|
+
console.log(` Mounted at: ${mountPoint}`)
|
|
424
|
+
|
|
425
|
+
// Step 2: Find the .app bundle
|
|
426
|
+
try {
|
|
427
|
+
const entries = fs.readdirSync(mountPoint)
|
|
428
|
+
const appFile = entries.find(e => e.endsWith('.app'))
|
|
429
|
+
|
|
430
|
+
if (!appFile) {
|
|
431
|
+
// Try to detach before rejecting
|
|
432
|
+
execFileSyncIgnore('hdiutil', ['detach', mountPoint])
|
|
433
|
+
reject(new Error('No .app bundle found in DMG'))
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const appSource = join(mountPoint, appFile)
|
|
438
|
+
const appDest = `/Applications/${appFile}`
|
|
439
|
+
|
|
440
|
+
// Check if app already exists
|
|
441
|
+
if (fs.existsSync(appDest)) {
|
|
442
|
+
console.log(` Existing app found at ${appDest}, replacing...`)
|
|
443
|
+
// Remove existing app
|
|
444
|
+
rm('-rf', appDest)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Step 3: Copy the app to /Applications
|
|
448
|
+
console.log(` Installing ${appFile} to /Applications...`)
|
|
449
|
+
execFile('cp', ['-R', appSource, appDest], (cpErr) => {
|
|
450
|
+
// Step 4: Detach the DMG (always, regardless of copy result)
|
|
451
|
+
console.log(' Detaching DMG...')
|
|
452
|
+
execFile('hdiutil', ['detach', mountPoint], (detachErr) => {
|
|
453
|
+
if (detachErr) {
|
|
454
|
+
console.log(' Warning: Failed to detach DMG:', detachErr.message)
|
|
455
|
+
} else {
|
|
456
|
+
console.log(' DMG detached')
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (cpErr) {
|
|
460
|
+
reject(new Error(`Failed to copy app: ${cpErr.message}`))
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.log(` App installed to: ${appDest}`)
|
|
465
|
+
resolve(appDest)
|
|
466
|
+
})
|
|
467
|
+
})
|
|
468
|
+
} catch (e) {
|
|
469
|
+
// Try to detach before rejecting
|
|
470
|
+
execFileSyncIgnore('hdiutil', ['detach', mountPoint])
|
|
471
|
+
reject(e)
|
|
472
|
+
}
|
|
473
|
+
})
|
|
474
|
+
})
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Execute a file synchronously, ignoring errors
|
|
479
|
+
*/
|
|
480
|
+
function execFileSyncIgnore (cmd, args) {
|
|
481
|
+
try {
|
|
482
|
+
execSync(cmd, args, { stdio: 'ignore' })
|
|
483
|
+
} catch (e) {
|
|
484
|
+
// Ignore
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function runMac (archName) {
|
|
489
|
+
const pattern = new RegExp(`mac-${archName}\\.dmg$`)
|
|
490
|
+
console.log(' Fetching release info...')
|
|
491
|
+
const releaseInfo = await getReleaseInfo(r => pattern.test(r.name))
|
|
492
|
+
if (!releaseInfo) {
|
|
493
|
+
throw new Error(`No release found for Mac ${archName}`)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const safeName = sanitizeFilename(releaseInfo.name)
|
|
497
|
+
const proxyUrl = applyProxy(releaseInfo.browser_download_url)
|
|
498
|
+
console.log(` URL: ${proxyUrl}`)
|
|
499
|
+
|
|
500
|
+
await download(releaseInfo.browser_download_url, packageRoot, { extract: false, displayName: releaseInfo.name })
|
|
501
|
+
|
|
502
|
+
const dmgPath = join(packageRoot, safeName)
|
|
262
503
|
showFinalMessage()
|
|
263
|
-
|
|
504
|
+
|
|
505
|
+
// Install from DMG automatically
|
|
506
|
+
try {
|
|
507
|
+
await installFromDmg(dmgPath)
|
|
508
|
+
|
|
509
|
+
// Clean up DMG
|
|
510
|
+
try {
|
|
511
|
+
fs.unlinkSync(dmgPath)
|
|
512
|
+
console.log(' Cleaned up DMG file')
|
|
513
|
+
} catch (e) {
|
|
514
|
+
// Ignore cleanup errors
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
console.log('')
|
|
518
|
+
console.log(' Installation complete! You can now launch electerm from /Applications')
|
|
519
|
+
} catch (err) {
|
|
520
|
+
console.error('')
|
|
521
|
+
console.error(' Warning: Automatic installation failed:', err.message)
|
|
522
|
+
console.error(' Please manually copy the app from the DMG to /Applications')
|
|
523
|
+
console.error('')
|
|
524
|
+
console.log(' Opening DMG for manual installation...')
|
|
525
|
+
execFile('open', [dmgPath])
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function runMac10 () {
|
|
530
|
+
console.log(' Fetching release info...')
|
|
531
|
+
const releaseInfo = await getReleaseInfo(r => /mac10-x64\.dmg$/.test(r.name))
|
|
532
|
+
if (!releaseInfo) {
|
|
533
|
+
throw new Error('No release found for macOS 10.x')
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const safeName = sanitizeFilename(releaseInfo.name)
|
|
537
|
+
const proxyUrl = applyProxy(releaseInfo.browser_download_url)
|
|
538
|
+
console.log(` URL: ${proxyUrl}`)
|
|
539
|
+
|
|
540
|
+
await download(releaseInfo.browser_download_url, packageRoot, { extract: false, displayName: releaseInfo.name })
|
|
541
|
+
|
|
542
|
+
const dmgPath = join(packageRoot, safeName)
|
|
543
|
+
showFinalMessage()
|
|
544
|
+
|
|
545
|
+
// Install from DMG automatically
|
|
546
|
+
try {
|
|
547
|
+
await installFromDmg(dmgPath)
|
|
548
|
+
|
|
549
|
+
// Clean up DMG
|
|
550
|
+
try {
|
|
551
|
+
fs.unlinkSync(dmgPath)
|
|
552
|
+
console.log(' Cleaned up DMG file')
|
|
553
|
+
} catch (e) {
|
|
554
|
+
// Ignore cleanup errors
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
console.log('')
|
|
558
|
+
console.log(' Installation complete! You can now launch electerm from /Applications')
|
|
559
|
+
} catch (err) {
|
|
560
|
+
console.error('')
|
|
561
|
+
console.error(' Warning: Automatic installation failed:', err.message)
|
|
562
|
+
console.error(' Please manually copy the app from the DMG to /Applications')
|
|
563
|
+
console.error('')
|
|
564
|
+
console.log(' Opening DMG for manual installation...')
|
|
565
|
+
execFile('open', [dmgPath])
|
|
566
|
+
}
|
|
264
567
|
}
|
|
265
568
|
|
|
266
569
|
// ---------------------------------------------------------------------------
|
|
@@ -268,18 +571,28 @@ async function runWin7 () {
|
|
|
268
571
|
// ---------------------------------------------------------------------------
|
|
269
572
|
|
|
270
573
|
async function main () {
|
|
271
|
-
console.log(
|
|
574
|
+
console.log('')
|
|
575
|
+
console.log('========================================')
|
|
576
|
+
console.log('electerm binary installer')
|
|
577
|
+
console.log('========================================')
|
|
578
|
+
console.log(`Platform: ${plat}, Architecture: ${arch}`)
|
|
579
|
+
|
|
580
|
+
if (GITHUB_PROXY) {
|
|
581
|
+
console.log(`GitHub Proxy: ${GITHUB_PROXY}`)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
console.log('')
|
|
272
585
|
|
|
273
586
|
// Check for legacy systems
|
|
274
587
|
const win7 = isWindows7OrEarlier(plat, os.release())
|
|
275
588
|
const mac10 = isMacOS10(plat, os.release())
|
|
276
589
|
const linuxLegacy = isLinuxLegacy(plat)
|
|
277
590
|
|
|
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)')
|
|
591
|
+
if (win7) console.log(' Detected: Windows 7 or earlier')
|
|
592
|
+
if (mac10) console.log(' Detected: macOS 10.x')
|
|
593
|
+
if (linuxLegacy) console.log(' Detected: Linux with glibc < 2.34 (legacy)')
|
|
281
594
|
|
|
282
|
-
console.log('Fetching release information
|
|
595
|
+
console.log(' Fetching release information...')
|
|
283
596
|
|
|
284
597
|
try {
|
|
285
598
|
if (plat === 'win32') {
|
|
@@ -300,7 +613,7 @@ async function main () {
|
|
|
300
613
|
}
|
|
301
614
|
} else if (plat === 'linux') {
|
|
302
615
|
const suffix = linuxLegacy ? '-legacy' : ''
|
|
303
|
-
if (arch === 'arm64') {
|
|
616
|
+
if (arch === 'arm64' || arch === 'aarch64') {
|
|
304
617
|
await runLinux(`linux-arm64${suffix}`, `linux-arm64${suffix}.tar.gz`)
|
|
305
618
|
} else if (arch === 'arm') {
|
|
306
619
|
await runLinux(`linux-armv7l${suffix}`, `linux-armv7l${suffix}.tar.gz`)
|
|
@@ -311,32 +624,37 @@ async function main () {
|
|
|
311
624
|
throw new Error(`Platform "${plat}" is not supported.`)
|
|
312
625
|
}
|
|
313
626
|
} catch (err) {
|
|
314
|
-
console.error('
|
|
627
|
+
console.error('')
|
|
628
|
+
console.error('========================================')
|
|
315
629
|
console.error('Installation failed!')
|
|
316
630
|
console.error('========================================')
|
|
317
631
|
console.error(`Error: ${err.message}`)
|
|
318
|
-
console.error(
|
|
319
|
-
console.error('
|
|
320
|
-
console.error('
|
|
632
|
+
console.error(`Platform: ${plat}, Architecture: ${arch}`)
|
|
633
|
+
console.error('')
|
|
634
|
+
console.error('Please visit https://electerm.html5beta.com for manual download options.')
|
|
635
|
+
console.error('========================================')
|
|
636
|
+
console.error('')
|
|
321
637
|
process.exit(1)
|
|
322
638
|
}
|
|
323
639
|
}
|
|
324
640
|
|
|
325
641
|
// ---------------------------------------------------------------------------
|
|
326
|
-
// Exports
|
|
642
|
+
// Exports for testing
|
|
327
643
|
// ---------------------------------------------------------------------------
|
|
328
644
|
|
|
329
|
-
// Export functions for testing
|
|
330
645
|
module.exports = {
|
|
331
646
|
isWindows7OrEarlier,
|
|
332
647
|
isMacOS10,
|
|
333
648
|
isLinuxLegacy,
|
|
334
|
-
getDownloadPattern,
|
|
335
649
|
sanitizeVersion,
|
|
336
|
-
sanitizeFilename
|
|
650
|
+
sanitizeFilename,
|
|
651
|
+
getElectermExePath,
|
|
652
|
+
isElectermExtracted,
|
|
653
|
+
// Expose for test injection
|
|
654
|
+
_packageRoot: packageRoot,
|
|
655
|
+
_extractDir: extractDir
|
|
337
656
|
}
|
|
338
657
|
|
|
339
|
-
// Run main only if this file is executed directly
|
|
340
658
|
if (require.main === module) {
|
|
341
659
|
main()
|
|
342
660
|
}
|
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 {
|
|
17
|
-
* @
|
|
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
|
|
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
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
*
|
|
56
|
-
* @param {string} url - URL to
|
|
57
|
-
* @param {
|
|
58
|
-
* @returns {Promise<string>}
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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(
|
|
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
|
|
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 {
|
|
133
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
+
// Ignore cleanup errors
|
|
147
226
|
}
|
|
227
|
+
extracted = true
|
|
228
|
+
console.log(' Extraction complete!')
|
|
148
229
|
}
|
|
149
230
|
|
|
150
|
-
|
|
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:
|
|
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
|
-
|
|
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
package/LICENSE.electron.txt
DELETED
|
@@ -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.
|