howcode 0.1.5 → 0.1.61
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/bin/howcode.js +1 -1
- package/lib/howcode.js +221 -191
- package/package.json +16 -3
package/bin/howcode.js
CHANGED
package/lib/howcode.js
CHANGED
|
@@ -1,305 +1,329 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const fsp = require(
|
|
3
|
-
const crypto = require(
|
|
4
|
-
const os = require(
|
|
5
|
-
const path = require(
|
|
6
|
-
const { spawn } = require(
|
|
7
|
-
const { pipeline } = require(
|
|
8
|
-
const { Readable } = require(
|
|
9
|
-
const tar = require(
|
|
10
|
-
|
|
11
|
-
const packageJson = require(
|
|
12
|
-
|
|
13
|
-
const APP_NAME = packageJson.howcode.appName
|
|
14
|
-
const RELEASE_BASE_URL = process.env.HOWCODE_BASE_URL || packageJson.howcode.releaseBaseUrl
|
|
15
|
-
const DOWNLOAD_TIMEOUT_MS = 5 * 60_000
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const fsp = require('node:fs/promises')
|
|
3
|
+
const crypto = require('node:crypto')
|
|
4
|
+
const os = require('node:os')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
const { spawn } = require('node:child_process')
|
|
7
|
+
const { pipeline } = require('node:stream/promises')
|
|
8
|
+
const { Readable } = require('node:stream')
|
|
9
|
+
const tar = require('tar')
|
|
10
|
+
|
|
11
|
+
const packageJson = require('../package.json')
|
|
12
|
+
|
|
13
|
+
const APP_NAME = packageJson.howcode.appName
|
|
14
|
+
const RELEASE_BASE_URL = process.env.HOWCODE_BASE_URL || packageJson.howcode.releaseBaseUrl
|
|
15
|
+
const DOWNLOAD_TIMEOUT_MS = 5 * 60_000
|
|
16
16
|
|
|
17
17
|
const TARGETS = {
|
|
18
|
-
|
|
19
|
-
os:
|
|
20
|
-
arch:
|
|
18
|
+
'darwin:arm64': {
|
|
19
|
+
os: 'macos',
|
|
20
|
+
arch: 'arm64',
|
|
21
21
|
executable: `${APP_NAME}.app/Contents/MacOS/${APP_NAME}`,
|
|
22
22
|
},
|
|
23
|
-
|
|
24
|
-
os:
|
|
25
|
-
arch:
|
|
23
|
+
'darwin:x64': {
|
|
24
|
+
os: 'macos',
|
|
25
|
+
arch: 'x64',
|
|
26
26
|
executable: `${APP_NAME}.app/Contents/MacOS/${APP_NAME}`,
|
|
27
27
|
},
|
|
28
|
-
|
|
29
|
-
os:
|
|
30
|
-
arch:
|
|
28
|
+
'linux:arm64': {
|
|
29
|
+
os: 'linux',
|
|
30
|
+
arch: 'arm64',
|
|
31
31
|
executable: `${APP_NAME}/${APP_NAME}`,
|
|
32
32
|
},
|
|
33
|
-
|
|
34
|
-
os:
|
|
35
|
-
arch:
|
|
33
|
+
'linux:x64': {
|
|
34
|
+
os: 'linux',
|
|
35
|
+
arch: 'x64',
|
|
36
36
|
executable: `${APP_NAME}/${APP_NAME}`,
|
|
37
37
|
},
|
|
38
|
-
|
|
39
|
-
os:
|
|
40
|
-
arch:
|
|
38
|
+
'win32:arm64': {
|
|
39
|
+
os: 'win',
|
|
40
|
+
arch: 'arm64',
|
|
41
41
|
executable: `${APP_NAME}/${APP_NAME}.exe`,
|
|
42
42
|
},
|
|
43
|
-
|
|
44
|
-
os:
|
|
45
|
-
arch:
|
|
43
|
+
'win32:x64': {
|
|
44
|
+
os: 'win',
|
|
45
|
+
arch: 'x64',
|
|
46
46
|
executable: `${APP_NAME}/${APP_NAME}.exe`,
|
|
47
47
|
},
|
|
48
|
-
}
|
|
48
|
+
}
|
|
49
49
|
|
|
50
50
|
function readJsonIfPresent(filePath) {
|
|
51
51
|
try {
|
|
52
|
-
return JSON.parse(fs.readFileSync(filePath,
|
|
52
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
53
53
|
} catch {
|
|
54
|
-
return null
|
|
54
|
+
return null
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function getTarget() {
|
|
59
|
-
const key = `${process.platform}:${process.arch}
|
|
60
|
-
const target = TARGETS[key]
|
|
59
|
+
const key = `${process.platform}:${process.arch}`
|
|
60
|
+
const target = TARGETS[key]
|
|
61
61
|
if (!target) {
|
|
62
|
-
throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`)
|
|
62
|
+
throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`)
|
|
63
63
|
}
|
|
64
|
-
return target
|
|
64
|
+
return target
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function getCacheRoot() {
|
|
68
68
|
if (process.env.HOWCODE_CACHE_DIR) {
|
|
69
|
-
return process.env.HOWCODE_CACHE_DIR
|
|
69
|
+
return process.env.HOWCODE_CACHE_DIR
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
if (process.platform ===
|
|
72
|
+
if (process.platform === 'win32') {
|
|
73
73
|
return path.join(
|
|
74
|
-
process.env.LOCALAPPDATA || path.join(os.homedir(),
|
|
74
|
+
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
|
75
75
|
APP_NAME,
|
|
76
|
-
)
|
|
76
|
+
)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
if (process.platform ===
|
|
80
|
-
return path.join(os.homedir(),
|
|
79
|
+
if (process.platform === 'darwin') {
|
|
80
|
+
return path.join(os.homedir(), 'Library', 'Caches', APP_NAME)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
return path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(),
|
|
83
|
+
return path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), APP_NAME)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function getPaths(target, releaseInfo) {
|
|
87
|
-
const cacheRoot = getCacheRoot()
|
|
88
|
-
const versionsRoot = path.join(cacheRoot,
|
|
89
|
-
const releaseKey = `${releaseInfo.version}-${releaseInfo.hash}
|
|
90
|
-
const installDir = path.join(versionsRoot, releaseKey)
|
|
91
|
-
const launcherWorkingDirectory = path.dirname(path.join(installDir, target.executable))
|
|
87
|
+
const cacheRoot = getCacheRoot()
|
|
88
|
+
const versionsRoot = path.join(cacheRoot, 'versions')
|
|
89
|
+
const releaseKey = `${releaseInfo.version}-${releaseInfo.hash}`
|
|
90
|
+
const installDir = path.join(versionsRoot, releaseKey)
|
|
91
|
+
const launcherWorkingDirectory = path.dirname(path.join(installDir, target.executable))
|
|
92
92
|
return {
|
|
93
93
|
cacheRoot,
|
|
94
|
-
currentFile: path.join(cacheRoot,
|
|
94
|
+
currentFile: path.join(cacheRoot, 'current.json'),
|
|
95
95
|
windowsCommandFile: path.join(cacheRoot, `${APP_NAME}.cmd`),
|
|
96
96
|
launcherWorkingDirectory,
|
|
97
97
|
installDir,
|
|
98
98
|
executablePath: path.join(installDir, target.executable),
|
|
99
|
-
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getAppResourcesPath(installDir, target) {
|
|
103
|
+
if (target.os === 'macos') {
|
|
104
|
+
return path.join(installDir, `${APP_NAME}.app`, 'Contents', 'Resources')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return path.join(installDir, APP_NAME, 'resources')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function hasPackagedAppBundle(installDir, target) {
|
|
111
|
+
const resourcesPath = getAppResourcesPath(installDir, target)
|
|
112
|
+
return (
|
|
113
|
+
fs.existsSync(path.join(resourcesPath, 'app.asar')) ||
|
|
114
|
+
fs.existsSync(path.join(resourcesPath, 'app', 'package.json'))
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isValidInstall(paths, target) {
|
|
119
|
+
return fs.existsSync(paths.executablePath) && hasPackagedAppBundle(paths.installDir, target)
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
function getWindowsStartMenuShortcutPath() {
|
|
103
|
-
const appData = process.env.APPDATA || path.join(os.homedir(),
|
|
104
|
-
return path.join(appData,
|
|
123
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
|
|
124
|
+
return path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', `${APP_NAME}.lnk`)
|
|
105
125
|
}
|
|
106
126
|
|
|
107
127
|
function escapeWindowsCommandValue(value) {
|
|
108
|
-
return value.replace(/%/g,
|
|
128
|
+
return value.replace(/%/g, '%%')
|
|
109
129
|
}
|
|
110
130
|
|
|
111
131
|
function getWindowsScriptHostPath(executableName) {
|
|
112
|
-
const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT
|
|
132
|
+
const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT
|
|
113
133
|
if (systemRoot) {
|
|
114
|
-
return path.join(systemRoot,
|
|
134
|
+
return path.join(systemRoot, 'System32', executableName)
|
|
115
135
|
}
|
|
116
136
|
|
|
117
|
-
return path.join(
|
|
137
|
+
return path.join('C:', 'Windows', 'System32', executableName)
|
|
118
138
|
}
|
|
119
139
|
|
|
120
140
|
async function writeWindowsCommandLauncher(paths) {
|
|
121
141
|
const commandContents = [
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
`set
|
|
127
|
-
`set
|
|
142
|
+
'@echo off',
|
|
143
|
+
'chcp 65001 >nul',
|
|
144
|
+
'setlocal',
|
|
145
|
+
'set NODE_TLS_REJECT_UNAUTHORIZED=',
|
|
146
|
+
`set "HOWCODE_EXE=${escapeWindowsCommandValue(paths.executablePath)}"`,
|
|
147
|
+
`set "HOWCODE_REPO_ROOT=${escapeWindowsCommandValue(paths.launcherWorkingDirectory)}"`,
|
|
128
148
|
'if not exist "%HOWCODE_EXE%" (',
|
|
129
149
|
` echo ${APP_NAME}: installed app executable was not found.`,
|
|
130
150
|
` echo Run npx ${APP_NAME} to repair the local install.`,
|
|
131
|
-
|
|
132
|
-
|
|
151
|
+
' exit /b 1',
|
|
152
|
+
')',
|
|
133
153
|
'start "" /D "%HOWCODE_REPO_ROOT%" "%HOWCODE_EXE%"',
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
].join(
|
|
154
|
+
'endlocal',
|
|
155
|
+
'',
|
|
156
|
+
].join('\r\n')
|
|
137
157
|
|
|
138
|
-
await fsp.writeFile(paths.windowsCommandFile, commandContents,
|
|
158
|
+
await fsp.writeFile(paths.windowsCommandFile, commandContents, 'utf8')
|
|
139
159
|
}
|
|
140
160
|
|
|
141
161
|
async function createWindowsStartMenuShortcut(paths) {
|
|
142
|
-
const shortcutPath = getWindowsStartMenuShortcutPath()
|
|
162
|
+
const shortcutPath = getWindowsStartMenuShortcutPath()
|
|
143
163
|
const shortcutScriptPath = path.join(
|
|
144
164
|
paths.cacheRoot,
|
|
145
165
|
`.create-${APP_NAME}-shortcut-${process.pid}.js`,
|
|
146
|
-
)
|
|
147
|
-
await fsp.mkdir(path.dirname(shortcutPath), { recursive: true })
|
|
166
|
+
)
|
|
167
|
+
await fsp.mkdir(path.dirname(shortcutPath), { recursive: true })
|
|
148
168
|
await fsp.writeFile(
|
|
149
169
|
shortcutScriptPath,
|
|
150
170
|
[
|
|
151
171
|
"var shell = WScript.CreateObject('WScript.Shell');",
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
].join(
|
|
160
|
-
|
|
161
|
-
)
|
|
172
|
+
'var shortcut = shell.CreateShortcut(WScript.Arguments.Item(0));',
|
|
173
|
+
'shortcut.TargetPath = WScript.Arguments.Item(1);',
|
|
174
|
+
'shortcut.WorkingDirectory = WScript.Arguments.Item(2);',
|
|
175
|
+
'shortcut.IconLocation = WScript.Arguments.Item(3);',
|
|
176
|
+
'shortcut.Description = WScript.Arguments.Item(4);',
|
|
177
|
+
'shortcut.Save();',
|
|
178
|
+
'',
|
|
179
|
+
].join('\r\n'),
|
|
180
|
+
'utf8',
|
|
181
|
+
)
|
|
162
182
|
|
|
163
183
|
try {
|
|
164
184
|
await new Promise((resolve, reject) => {
|
|
165
185
|
const child = spawn(
|
|
166
|
-
getWindowsScriptHostPath(
|
|
186
|
+
getWindowsScriptHostPath('cscript.exe'),
|
|
167
187
|
[
|
|
168
|
-
|
|
188
|
+
'//NoLogo',
|
|
169
189
|
shortcutScriptPath,
|
|
170
190
|
shortcutPath,
|
|
171
191
|
paths.windowsCommandFile,
|
|
172
192
|
paths.launcherWorkingDirectory,
|
|
173
193
|
`${paths.executablePath},0`,
|
|
174
|
-
|
|
194
|
+
'howcode',
|
|
175
195
|
],
|
|
176
|
-
{ stdio:
|
|
177
|
-
)
|
|
178
|
-
child.on(
|
|
179
|
-
child.on(
|
|
196
|
+
{ stdio: 'ignore', windowsHide: true },
|
|
197
|
+
)
|
|
198
|
+
child.on('error', reject)
|
|
199
|
+
child.on('exit', (code) => {
|
|
180
200
|
if (code === 0) {
|
|
181
|
-
resolve()
|
|
201
|
+
resolve()
|
|
182
202
|
} else {
|
|
183
|
-
reject(new Error(`cscript exited with code ${code} while creating Start Menu shortcut.`))
|
|
203
|
+
reject(new Error(`cscript exited with code ${code} while creating Start Menu shortcut.`))
|
|
184
204
|
}
|
|
185
|
-
})
|
|
186
|
-
})
|
|
205
|
+
})
|
|
206
|
+
})
|
|
187
207
|
} finally {
|
|
188
|
-
await fsp.rm(shortcutScriptPath, { force: true })
|
|
208
|
+
await fsp.rm(shortcutScriptPath, { force: true })
|
|
189
209
|
}
|
|
190
210
|
|
|
191
|
-
return shortcutPath
|
|
211
|
+
return shortcutPath
|
|
192
212
|
}
|
|
193
213
|
|
|
194
214
|
async function ensureWindowsLaunchIntegration(target, paths) {
|
|
195
|
-
if (target.os !==
|
|
196
|
-
return true
|
|
215
|
+
if (target.os !== 'win') {
|
|
216
|
+
return true
|
|
197
217
|
}
|
|
198
218
|
|
|
199
219
|
try {
|
|
200
|
-
await writeWindowsCommandLauncher(paths)
|
|
220
|
+
await writeWindowsCommandLauncher(paths)
|
|
201
221
|
} catch (error) {
|
|
202
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
203
|
-
console.warn(`${APP_NAME}: could not create command launcher: ${message}`)
|
|
204
|
-
console.warn(`${APP_NAME}: Start Menu shortcut was not updated.`)
|
|
205
|
-
return false
|
|
222
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
223
|
+
console.warn(`${APP_NAME}: could not create command launcher: ${message}`)
|
|
224
|
+
console.warn(`${APP_NAME}: Start Menu shortcut was not updated.`)
|
|
225
|
+
return false
|
|
206
226
|
}
|
|
207
227
|
|
|
208
228
|
try {
|
|
209
|
-
await createWindowsStartMenuShortcut(paths)
|
|
210
|
-
return true
|
|
229
|
+
await createWindowsStartMenuShortcut(paths)
|
|
230
|
+
return true
|
|
211
231
|
} catch (error) {
|
|
212
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
213
|
-
console.warn(`${APP_NAME}: could not create Start Menu shortcut: ${message}`)
|
|
214
|
-
console.warn(`${APP_NAME}: you can still relaunch with ${paths.windowsCommandFile}`)
|
|
215
|
-
return false
|
|
232
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
233
|
+
console.warn(`${APP_NAME}: could not create Start Menu shortcut: ${message}`)
|
|
234
|
+
console.warn(`${APP_NAME}: you can still relaunch with ${paths.windowsCommandFile}`)
|
|
235
|
+
return false
|
|
216
236
|
}
|
|
217
237
|
}
|
|
218
238
|
|
|
219
239
|
async function fetchJson(url) {
|
|
220
|
-
const controller = new AbortController()
|
|
221
|
-
const timeout = setTimeout(() => controller.abort(), 5000)
|
|
240
|
+
const controller = new AbortController()
|
|
241
|
+
const timeout = setTimeout(() => controller.abort(), 5000)
|
|
222
242
|
|
|
223
243
|
try {
|
|
224
|
-
const response = await fetch(url, { signal: controller.signal })
|
|
244
|
+
const response = await fetch(url, { signal: controller.signal })
|
|
225
245
|
if (!response.ok) {
|
|
226
|
-
throw new Error(`HTTP ${response.status} while fetching ${url}`)
|
|
246
|
+
throw new Error(`HTTP ${response.status} while fetching ${url}`)
|
|
227
247
|
}
|
|
228
|
-
return await response.json()
|
|
248
|
+
return await response.json()
|
|
229
249
|
} finally {
|
|
230
|
-
clearTimeout(timeout)
|
|
250
|
+
clearTimeout(timeout)
|
|
231
251
|
}
|
|
232
252
|
}
|
|
233
253
|
|
|
234
254
|
async function downloadFile(url, filePath, timeoutMs = DOWNLOAD_TIMEOUT_MS) {
|
|
235
|
-
const controller = new AbortController()
|
|
236
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
255
|
+
const controller = new AbortController()
|
|
256
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
237
257
|
|
|
238
258
|
try {
|
|
239
|
-
const response = await fetch(url, { signal: controller.signal })
|
|
240
|
-
if (!response.ok
|
|
241
|
-
throw new Error(`HTTP ${response.status} while downloading ${url}`)
|
|
259
|
+
const response = await fetch(url, { signal: controller.signal })
|
|
260
|
+
if (!(response.ok && response.body)) {
|
|
261
|
+
throw new Error(`HTTP ${response.status} while downloading ${url}`)
|
|
242
262
|
}
|
|
243
263
|
|
|
244
|
-
await fsp.mkdir(path.dirname(filePath), { recursive: true })
|
|
245
|
-
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(filePath))
|
|
264
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true })
|
|
265
|
+
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(filePath))
|
|
246
266
|
} finally {
|
|
247
|
-
clearTimeout(timeout)
|
|
267
|
+
clearTimeout(timeout)
|
|
248
268
|
}
|
|
249
269
|
}
|
|
250
270
|
|
|
251
271
|
async function sha256File(filePath) {
|
|
252
|
-
const hash = crypto.createHash(
|
|
253
|
-
await pipeline(fs.createReadStream(filePath), hash)
|
|
254
|
-
return hash.digest(
|
|
272
|
+
const hash = crypto.createHash('sha256')
|
|
273
|
+
await pipeline(fs.createReadStream(filePath), hash)
|
|
274
|
+
return hash.digest('hex')
|
|
255
275
|
}
|
|
256
276
|
|
|
257
277
|
async function resolveLatestRelease(target) {
|
|
258
|
-
const updateUrl = `${RELEASE_BASE_URL}/stable-${target.os}-${target.arch}-update.json
|
|
259
|
-
const metadata = await fetchJson(updateUrl)
|
|
260
|
-
if (!metadata || typeof metadata.version !==
|
|
261
|
-
throw new Error(`Invalid release metadata from ${updateUrl}`)
|
|
278
|
+
const updateUrl = `${RELEASE_BASE_URL}/stable-${target.os}-${target.arch}-update.json`
|
|
279
|
+
const metadata = await fetchJson(updateUrl)
|
|
280
|
+
if (!metadata || typeof metadata.version !== 'string' || typeof metadata.hash !== 'string') {
|
|
281
|
+
throw new Error(`Invalid release metadata from ${updateUrl}`)
|
|
262
282
|
}
|
|
263
283
|
|
|
264
284
|
return {
|
|
265
285
|
version: metadata.version,
|
|
266
286
|
hash: metadata.hash,
|
|
267
287
|
assetUrl: `${RELEASE_BASE_URL}/${APP_NAME}-${target.os}-${target.arch}.tar.gz`,
|
|
268
|
-
}
|
|
288
|
+
}
|
|
269
289
|
}
|
|
270
290
|
|
|
271
291
|
async function installRelease(target, releaseInfo, paths) {
|
|
272
|
-
const tempRoot = path.join(paths.cacheRoot, `.tmp-${Date.now()}-${process.pid}`)
|
|
273
|
-
const tempInstallDir = `${paths.installDir}.partial
|
|
274
|
-
const archivePath = path.join(tempRoot, `${APP_NAME}-${target.os}-${target.arch}.tar.gz`)
|
|
292
|
+
const tempRoot = path.join(paths.cacheRoot, `.tmp-${Date.now()}-${process.pid}`)
|
|
293
|
+
const tempInstallDir = `${paths.installDir}.partial`
|
|
294
|
+
const archivePath = path.join(tempRoot, `${APP_NAME}-${target.os}-${target.arch}.tar.gz`)
|
|
275
295
|
|
|
276
|
-
console.log(`Downloading ${APP_NAME} ${releaseInfo.version} for ${target.os}-${target.arch}...`)
|
|
296
|
+
console.log(`Downloading ${APP_NAME} ${releaseInfo.version} for ${target.os}-${target.arch}...`)
|
|
277
297
|
|
|
278
|
-
await fsp.rm(tempRoot, { recursive: true, force: true })
|
|
279
|
-
await fsp.rm(tempInstallDir, { recursive: true, force: true })
|
|
280
|
-
await fsp.mkdir(tempRoot, { recursive: true })
|
|
281
|
-
await fsp.mkdir(path.dirname(paths.installDir), { recursive: true })
|
|
282
|
-
await downloadFile(releaseInfo.assetUrl, archivePath)
|
|
298
|
+
await fsp.rm(tempRoot, { recursive: true, force: true })
|
|
299
|
+
await fsp.rm(tempInstallDir, { recursive: true, force: true })
|
|
300
|
+
await fsp.mkdir(tempRoot, { recursive: true })
|
|
301
|
+
await fsp.mkdir(path.dirname(paths.installDir), { recursive: true })
|
|
302
|
+
await downloadFile(releaseInfo.assetUrl, archivePath)
|
|
283
303
|
|
|
284
|
-
const archiveHash = await sha256File(archivePath)
|
|
304
|
+
const archiveHash = await sha256File(archivePath)
|
|
285
305
|
if (archiveHash !== releaseInfo.hash) {
|
|
286
|
-
await fsp.rm(tempRoot, { recursive: true, force: true })
|
|
306
|
+
await fsp.rm(tempRoot, { recursive: true, force: true })
|
|
287
307
|
throw new Error(
|
|
288
308
|
`Downloaded archive hash mismatch. Expected ${releaseInfo.hash}, got ${archiveHash}.`,
|
|
289
|
-
)
|
|
309
|
+
)
|
|
290
310
|
}
|
|
291
311
|
|
|
292
|
-
await fsp.mkdir(tempInstallDir, { recursive: true })
|
|
312
|
+
await fsp.mkdir(tempInstallDir, { recursive: true })
|
|
293
313
|
|
|
294
|
-
await tar.x({ file: archivePath, cwd: tempInstallDir })
|
|
314
|
+
await tar.x({ file: archivePath, cwd: tempInstallDir })
|
|
295
315
|
|
|
296
316
|
if (!fs.existsSync(path.join(tempInstallDir, target.executable))) {
|
|
297
|
-
throw new Error(`Downloaded archive did not contain ${target.executable}.`)
|
|
317
|
+
throw new Error(`Downloaded archive did not contain ${target.executable}.`)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!hasPackagedAppBundle(tempInstallDir, target)) {
|
|
321
|
+
throw new Error('Downloaded archive did not contain the packaged app bundle.')
|
|
298
322
|
}
|
|
299
323
|
|
|
300
|
-
await fsp.rm(paths.installDir, { recursive: true, force: true })
|
|
301
|
-
await fsp.rename(tempInstallDir, paths.installDir)
|
|
302
|
-
await fsp.rm(tempRoot, { recursive: true, force: true })
|
|
324
|
+
await fsp.rm(paths.installDir, { recursive: true, force: true })
|
|
325
|
+
await fsp.rename(tempInstallDir, paths.installDir)
|
|
326
|
+
await fsp.rm(tempRoot, { recursive: true, force: true })
|
|
303
327
|
|
|
304
328
|
await fsp.writeFile(
|
|
305
329
|
paths.currentFile,
|
|
@@ -313,17 +337,17 @@ async function installRelease(target, releaseInfo, paths) {
|
|
|
313
337
|
null,
|
|
314
338
|
2,
|
|
315
339
|
),
|
|
316
|
-
)
|
|
340
|
+
)
|
|
317
341
|
}
|
|
318
342
|
|
|
319
343
|
async function pruneOldVersions(cacheRoot, keepDir) {
|
|
320
|
-
const versionsRoot = path.join(cacheRoot,
|
|
321
|
-
let entries = []
|
|
344
|
+
const versionsRoot = path.join(cacheRoot, 'versions')
|
|
345
|
+
let entries = []
|
|
322
346
|
|
|
323
347
|
try {
|
|
324
|
-
entries = await fsp.readdir(versionsRoot, { withFileTypes: true })
|
|
348
|
+
entries = await fsp.readdir(versionsRoot, { withFileTypes: true })
|
|
325
349
|
} catch {
|
|
326
|
-
return
|
|
350
|
+
return
|
|
327
351
|
}
|
|
328
352
|
|
|
329
353
|
await Promise.all(
|
|
@@ -332,7 +356,7 @@ async function pruneOldVersions(cacheRoot, keepDir) {
|
|
|
332
356
|
.map((entry) => path.join(versionsRoot, entry.name))
|
|
333
357
|
.filter((dirPath) => dirPath !== keepDir)
|
|
334
358
|
.map((dirPath) => fsp.rm(dirPath, { recursive: true, force: true })),
|
|
335
|
-
)
|
|
359
|
+
)
|
|
336
360
|
}
|
|
337
361
|
|
|
338
362
|
function spawnLauncherProcess(executablePath, options = {}) {
|
|
@@ -340,77 +364,83 @@ function spawnLauncherProcess(executablePath, options = {}) {
|
|
|
340
364
|
...process.env,
|
|
341
365
|
HOWCODE_REPO_ROOT: process.env.HOWCODE_REPO_ROOT || process.cwd(),
|
|
342
366
|
...(options.env || {}),
|
|
343
|
-
}
|
|
344
|
-
Reflect.deleteProperty(env,
|
|
367
|
+
}
|
|
368
|
+
Reflect.deleteProperty(env, 'NODE_TLS_REJECT_UNAUTHORIZED')
|
|
345
369
|
|
|
346
370
|
return spawn(executablePath, [], {
|
|
347
371
|
detached: true,
|
|
348
|
-
stdio: options.stdio ||
|
|
372
|
+
stdio: options.stdio || 'ignore',
|
|
349
373
|
windowsHide: true,
|
|
350
374
|
cwd: path.dirname(executablePath),
|
|
351
375
|
env,
|
|
352
|
-
})
|
|
376
|
+
})
|
|
353
377
|
}
|
|
354
378
|
|
|
355
379
|
async function launch(executablePath) {
|
|
356
|
-
const child = spawnLauncherProcess(executablePath)
|
|
380
|
+
const child = spawnLauncherProcess(executablePath)
|
|
357
381
|
|
|
358
|
-
child.unref()
|
|
382
|
+
child.unref()
|
|
359
383
|
}
|
|
360
384
|
|
|
361
385
|
async function main() {
|
|
362
|
-
const target = getTarget()
|
|
363
|
-
const cacheRoot = getCacheRoot()
|
|
364
|
-
await fsp.mkdir(cacheRoot, { recursive: true })
|
|
386
|
+
const target = getTarget()
|
|
387
|
+
const cacheRoot = getCacheRoot()
|
|
388
|
+
await fsp.mkdir(cacheRoot, { recursive: true })
|
|
365
389
|
|
|
366
|
-
const current = readJsonIfPresent(path.join(cacheRoot,
|
|
390
|
+
const current = readJsonIfPresent(path.join(cacheRoot, 'current.json'))
|
|
367
391
|
|
|
368
|
-
let releaseInfo = null
|
|
392
|
+
let releaseInfo = null
|
|
369
393
|
try {
|
|
370
|
-
releaseInfo = await resolveLatestRelease(target)
|
|
394
|
+
releaseInfo = await resolveLatestRelease(target)
|
|
371
395
|
} catch (error) {
|
|
372
|
-
if (current?.executablePath
|
|
373
|
-
|
|
396
|
+
if (current?.executablePath) {
|
|
397
|
+
const currentPaths = {
|
|
374
398
|
cacheRoot,
|
|
375
|
-
currentFile: path.join(cacheRoot,
|
|
399
|
+
currentFile: path.join(cacheRoot, 'current.json'),
|
|
376
400
|
windowsCommandFile: path.join(cacheRoot, `${APP_NAME}.cmd`),
|
|
377
401
|
installDir: current.installDir || path.dirname(path.dirname(current.executablePath)),
|
|
378
402
|
launcherWorkingDirectory: path.dirname(current.executablePath),
|
|
379
403
|
executablePath: current.executablePath,
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
|
|
404
|
+
}
|
|
405
|
+
if (!isValidInstall(currentPaths, target)) {
|
|
406
|
+
throw error
|
|
407
|
+
}
|
|
408
|
+
await ensureWindowsLaunchIntegration(target, {
|
|
409
|
+
...currentPaths,
|
|
410
|
+
})
|
|
411
|
+
await launch(current.executablePath)
|
|
412
|
+
return
|
|
383
413
|
}
|
|
384
414
|
|
|
385
|
-
throw error
|
|
415
|
+
throw error
|
|
386
416
|
}
|
|
387
417
|
|
|
388
|
-
const paths = getPaths(target, releaseInfo)
|
|
389
|
-
const didInstall = !
|
|
390
|
-
if (
|
|
391
|
-
await installRelease(target, releaseInfo, paths)
|
|
418
|
+
const paths = getPaths(target, releaseInfo)
|
|
419
|
+
const didInstall = !isValidInstall(paths, target)
|
|
420
|
+
if (didInstall) {
|
|
421
|
+
await installRelease(target, releaseInfo, paths)
|
|
392
422
|
}
|
|
393
423
|
|
|
394
|
-
const launchIntegrationReady = await ensureWindowsLaunchIntegration(target, paths)
|
|
395
|
-
if (target.os ===
|
|
396
|
-
console.log(`${APP_NAME}: installed. You can relaunch it from the Windows Start Menu.`)
|
|
424
|
+
const launchIntegrationReady = await ensureWindowsLaunchIntegration(target, paths)
|
|
425
|
+
if (target.os === 'win' && didInstall && launchIntegrationReady) {
|
|
426
|
+
console.log(`${APP_NAME}: installed. You can relaunch it from the Windows Start Menu.`)
|
|
397
427
|
}
|
|
398
|
-
await pruneOldVersions(cacheRoot, paths.installDir)
|
|
399
|
-
await launch(paths.executablePath)
|
|
428
|
+
await pruneOldVersions(cacheRoot, paths.installDir)
|
|
429
|
+
await launch(paths.executablePath)
|
|
400
430
|
}
|
|
401
431
|
|
|
402
432
|
module.exports = {
|
|
403
433
|
main: async () => {
|
|
404
434
|
try {
|
|
405
|
-
await main()
|
|
435
|
+
await main()
|
|
406
436
|
} catch (error) {
|
|
407
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
408
|
-
console.error(`howcode: ${message}`)
|
|
409
|
-
process.exit(1)
|
|
437
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
438
|
+
console.error(`howcode: ${message}`)
|
|
439
|
+
process.exit(1)
|
|
410
440
|
}
|
|
411
441
|
},
|
|
412
|
-
}
|
|
442
|
+
}
|
|
413
443
|
|
|
414
444
|
if (require.main === module) {
|
|
415
|
-
module.exports.main()
|
|
445
|
+
module.exports.main()
|
|
416
446
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "howcode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.61",
|
|
4
4
|
"description": "Desktop coding app for Pi with projects, terminal, git, and diff workflows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Igor Warzocha",
|
|
@@ -15,14 +15,27 @@
|
|
|
15
15
|
"bin": {
|
|
16
16
|
"howcode": "bin/howcode.js"
|
|
17
17
|
},
|
|
18
|
-
"files": [
|
|
18
|
+
"files": [
|
|
19
|
+
"bin",
|
|
20
|
+
"lib",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
19
24
|
"dependencies": {
|
|
20
25
|
"tar": "^7.5.1"
|
|
21
26
|
},
|
|
22
27
|
"engines": {
|
|
23
28
|
"node": ">=18"
|
|
24
29
|
},
|
|
25
|
-
"keywords": [
|
|
30
|
+
"keywords": [
|
|
31
|
+
"desktop",
|
|
32
|
+
"coding",
|
|
33
|
+
"ai",
|
|
34
|
+
"terminal",
|
|
35
|
+
"git",
|
|
36
|
+
"diff",
|
|
37
|
+
"assistant"
|
|
38
|
+
],
|
|
26
39
|
"howcode": {
|
|
27
40
|
"appName": "howcode",
|
|
28
41
|
"releaseBaseUrl": "https://github.com/IgorWarzocha/howcode/releases/latest/download"
|