howcode 0.1.5 → 0.1.6
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 +188 -188
- package/package.json +16 -3
package/bin/howcode.js
CHANGED
package/lib/howcode.js
CHANGED
|
@@ -1,305 +1,305 @@
|
|
|
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
100
|
}
|
|
101
101
|
|
|
102
102
|
function getWindowsStartMenuShortcutPath() {
|
|
103
|
-
const appData = process.env.APPDATA || path.join(os.homedir(),
|
|
104
|
-
return path.join(appData,
|
|
103
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
|
|
104
|
+
return path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', `${APP_NAME}.lnk`)
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function escapeWindowsCommandValue(value) {
|
|
108
|
-
return value.replace(/%/g,
|
|
108
|
+
return value.replace(/%/g, '%%')
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function getWindowsScriptHostPath(executableName) {
|
|
112
|
-
const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT
|
|
112
|
+
const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT
|
|
113
113
|
if (systemRoot) {
|
|
114
|
-
return path.join(systemRoot,
|
|
114
|
+
return path.join(systemRoot, 'System32', executableName)
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
return path.join(
|
|
117
|
+
return path.join('C:', 'Windows', 'System32', executableName)
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
async function writeWindowsCommandLauncher(paths) {
|
|
121
121
|
const commandContents = [
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
`set
|
|
127
|
-
`set
|
|
122
|
+
'@echo off',
|
|
123
|
+
'chcp 65001 >nul',
|
|
124
|
+
'setlocal',
|
|
125
|
+
'set NODE_TLS_REJECT_UNAUTHORIZED=',
|
|
126
|
+
`set "HOWCODE_EXE=${escapeWindowsCommandValue(paths.executablePath)}"`,
|
|
127
|
+
`set "HOWCODE_REPO_ROOT=${escapeWindowsCommandValue(paths.launcherWorkingDirectory)}"`,
|
|
128
128
|
'if not exist "%HOWCODE_EXE%" (',
|
|
129
129
|
` echo ${APP_NAME}: installed app executable was not found.`,
|
|
130
130
|
` echo Run npx ${APP_NAME} to repair the local install.`,
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
' exit /b 1',
|
|
132
|
+
')',
|
|
133
133
|
'start "" /D "%HOWCODE_REPO_ROOT%" "%HOWCODE_EXE%"',
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
].join(
|
|
134
|
+
'endlocal',
|
|
135
|
+
'',
|
|
136
|
+
].join('\r\n')
|
|
137
137
|
|
|
138
|
-
await fsp.writeFile(paths.windowsCommandFile, commandContents,
|
|
138
|
+
await fsp.writeFile(paths.windowsCommandFile, commandContents, 'utf8')
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
async function createWindowsStartMenuShortcut(paths) {
|
|
142
|
-
const shortcutPath = getWindowsStartMenuShortcutPath()
|
|
142
|
+
const shortcutPath = getWindowsStartMenuShortcutPath()
|
|
143
143
|
const shortcutScriptPath = path.join(
|
|
144
144
|
paths.cacheRoot,
|
|
145
145
|
`.create-${APP_NAME}-shortcut-${process.pid}.js`,
|
|
146
|
-
)
|
|
147
|
-
await fsp.mkdir(path.dirname(shortcutPath), { recursive: true })
|
|
146
|
+
)
|
|
147
|
+
await fsp.mkdir(path.dirname(shortcutPath), { recursive: true })
|
|
148
148
|
await fsp.writeFile(
|
|
149
149
|
shortcutScriptPath,
|
|
150
150
|
[
|
|
151
151
|
"var shell = WScript.CreateObject('WScript.Shell');",
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
].join(
|
|
160
|
-
|
|
161
|
-
)
|
|
152
|
+
'var shortcut = shell.CreateShortcut(WScript.Arguments.Item(0));',
|
|
153
|
+
'shortcut.TargetPath = WScript.Arguments.Item(1);',
|
|
154
|
+
'shortcut.WorkingDirectory = WScript.Arguments.Item(2);',
|
|
155
|
+
'shortcut.IconLocation = WScript.Arguments.Item(3);',
|
|
156
|
+
'shortcut.Description = WScript.Arguments.Item(4);',
|
|
157
|
+
'shortcut.Save();',
|
|
158
|
+
'',
|
|
159
|
+
].join('\r\n'),
|
|
160
|
+
'utf8',
|
|
161
|
+
)
|
|
162
162
|
|
|
163
163
|
try {
|
|
164
164
|
await new Promise((resolve, reject) => {
|
|
165
165
|
const child = spawn(
|
|
166
|
-
getWindowsScriptHostPath(
|
|
166
|
+
getWindowsScriptHostPath('cscript.exe'),
|
|
167
167
|
[
|
|
168
|
-
|
|
168
|
+
'//NoLogo',
|
|
169
169
|
shortcutScriptPath,
|
|
170
170
|
shortcutPath,
|
|
171
171
|
paths.windowsCommandFile,
|
|
172
172
|
paths.launcherWorkingDirectory,
|
|
173
173
|
`${paths.executablePath},0`,
|
|
174
|
-
|
|
174
|
+
'howcode',
|
|
175
175
|
],
|
|
176
|
-
{ stdio:
|
|
177
|
-
)
|
|
178
|
-
child.on(
|
|
179
|
-
child.on(
|
|
176
|
+
{ stdio: 'ignore', windowsHide: true },
|
|
177
|
+
)
|
|
178
|
+
child.on('error', reject)
|
|
179
|
+
child.on('exit', (code) => {
|
|
180
180
|
if (code === 0) {
|
|
181
|
-
resolve()
|
|
181
|
+
resolve()
|
|
182
182
|
} else {
|
|
183
|
-
reject(new Error(`cscript exited with code ${code} while creating Start Menu shortcut.`))
|
|
183
|
+
reject(new Error(`cscript exited with code ${code} while creating Start Menu shortcut.`))
|
|
184
184
|
}
|
|
185
|
-
})
|
|
186
|
-
})
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
187
|
} finally {
|
|
188
|
-
await fsp.rm(shortcutScriptPath, { force: true })
|
|
188
|
+
await fsp.rm(shortcutScriptPath, { force: true })
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
return shortcutPath
|
|
191
|
+
return shortcutPath
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
async function ensureWindowsLaunchIntegration(target, paths) {
|
|
195
|
-
if (target.os !==
|
|
196
|
-
return true
|
|
195
|
+
if (target.os !== 'win') {
|
|
196
|
+
return true
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
try {
|
|
200
|
-
await writeWindowsCommandLauncher(paths)
|
|
200
|
+
await writeWindowsCommandLauncher(paths)
|
|
201
201
|
} 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
|
|
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
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
try {
|
|
209
|
-
await createWindowsStartMenuShortcut(paths)
|
|
210
|
-
return true
|
|
209
|
+
await createWindowsStartMenuShortcut(paths)
|
|
210
|
+
return true
|
|
211
211
|
} 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
|
|
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
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
async function fetchJson(url) {
|
|
220
|
-
const controller = new AbortController()
|
|
221
|
-
const timeout = setTimeout(() => controller.abort(), 5000)
|
|
220
|
+
const controller = new AbortController()
|
|
221
|
+
const timeout = setTimeout(() => controller.abort(), 5000)
|
|
222
222
|
|
|
223
223
|
try {
|
|
224
|
-
const response = await fetch(url, { signal: controller.signal })
|
|
224
|
+
const response = await fetch(url, { signal: controller.signal })
|
|
225
225
|
if (!response.ok) {
|
|
226
|
-
throw new Error(`HTTP ${response.status} while fetching ${url}`)
|
|
226
|
+
throw new Error(`HTTP ${response.status} while fetching ${url}`)
|
|
227
227
|
}
|
|
228
|
-
return await response.json()
|
|
228
|
+
return await response.json()
|
|
229
229
|
} finally {
|
|
230
|
-
clearTimeout(timeout)
|
|
230
|
+
clearTimeout(timeout)
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
async function downloadFile(url, filePath, timeoutMs = DOWNLOAD_TIMEOUT_MS) {
|
|
235
|
-
const controller = new AbortController()
|
|
236
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
235
|
+
const controller = new AbortController()
|
|
236
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
237
237
|
|
|
238
238
|
try {
|
|
239
|
-
const response = await fetch(url, { signal: controller.signal })
|
|
240
|
-
if (!response.ok
|
|
241
|
-
throw new Error(`HTTP ${response.status} while downloading ${url}`)
|
|
239
|
+
const response = await fetch(url, { signal: controller.signal })
|
|
240
|
+
if (!(response.ok && response.body)) {
|
|
241
|
+
throw new Error(`HTTP ${response.status} while downloading ${url}`)
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
await fsp.mkdir(path.dirname(filePath), { recursive: true })
|
|
245
|
-
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(filePath))
|
|
244
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true })
|
|
245
|
+
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(filePath))
|
|
246
246
|
} finally {
|
|
247
|
-
clearTimeout(timeout)
|
|
247
|
+
clearTimeout(timeout)
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
async function sha256File(filePath) {
|
|
252
|
-
const hash = crypto.createHash(
|
|
253
|
-
await pipeline(fs.createReadStream(filePath), hash)
|
|
254
|
-
return hash.digest(
|
|
252
|
+
const hash = crypto.createHash('sha256')
|
|
253
|
+
await pipeline(fs.createReadStream(filePath), hash)
|
|
254
|
+
return hash.digest('hex')
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
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}`)
|
|
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 !== 'string' || typeof metadata.hash !== 'string') {
|
|
261
|
+
throw new Error(`Invalid release metadata from ${updateUrl}`)
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
return {
|
|
265
265
|
version: metadata.version,
|
|
266
266
|
hash: metadata.hash,
|
|
267
267
|
assetUrl: `${RELEASE_BASE_URL}/${APP_NAME}-${target.os}-${target.arch}.tar.gz`,
|
|
268
|
-
}
|
|
268
|
+
}
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
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`)
|
|
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`)
|
|
275
275
|
|
|
276
|
-
console.log(`Downloading ${APP_NAME} ${releaseInfo.version} for ${target.os}-${target.arch}...`)
|
|
276
|
+
console.log(`Downloading ${APP_NAME} ${releaseInfo.version} for ${target.os}-${target.arch}...`)
|
|
277
277
|
|
|
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)
|
|
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)
|
|
283
283
|
|
|
284
|
-
const archiveHash = await sha256File(archivePath)
|
|
284
|
+
const archiveHash = await sha256File(archivePath)
|
|
285
285
|
if (archiveHash !== releaseInfo.hash) {
|
|
286
|
-
await fsp.rm(tempRoot, { recursive: true, force: true })
|
|
286
|
+
await fsp.rm(tempRoot, { recursive: true, force: true })
|
|
287
287
|
throw new Error(
|
|
288
288
|
`Downloaded archive hash mismatch. Expected ${releaseInfo.hash}, got ${archiveHash}.`,
|
|
289
|
-
)
|
|
289
|
+
)
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
await fsp.mkdir(tempInstallDir, { recursive: true })
|
|
292
|
+
await fsp.mkdir(tempInstallDir, { recursive: true })
|
|
293
293
|
|
|
294
|
-
await tar.x({ file: archivePath, cwd: tempInstallDir })
|
|
294
|
+
await tar.x({ file: archivePath, cwd: tempInstallDir })
|
|
295
295
|
|
|
296
296
|
if (!fs.existsSync(path.join(tempInstallDir, target.executable))) {
|
|
297
|
-
throw new Error(`Downloaded archive did not contain ${target.executable}.`)
|
|
297
|
+
throw new Error(`Downloaded archive did not contain ${target.executable}.`)
|
|
298
298
|
}
|
|
299
299
|
|
|
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 })
|
|
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 })
|
|
303
303
|
|
|
304
304
|
await fsp.writeFile(
|
|
305
305
|
paths.currentFile,
|
|
@@ -313,17 +313,17 @@ async function installRelease(target, releaseInfo, paths) {
|
|
|
313
313
|
null,
|
|
314
314
|
2,
|
|
315
315
|
),
|
|
316
|
-
)
|
|
316
|
+
)
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
async function pruneOldVersions(cacheRoot, keepDir) {
|
|
320
|
-
const versionsRoot = path.join(cacheRoot,
|
|
321
|
-
let entries = []
|
|
320
|
+
const versionsRoot = path.join(cacheRoot, 'versions')
|
|
321
|
+
let entries = []
|
|
322
322
|
|
|
323
323
|
try {
|
|
324
|
-
entries = await fsp.readdir(versionsRoot, { withFileTypes: true })
|
|
324
|
+
entries = await fsp.readdir(versionsRoot, { withFileTypes: true })
|
|
325
325
|
} catch {
|
|
326
|
-
return
|
|
326
|
+
return
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
await Promise.all(
|
|
@@ -332,7 +332,7 @@ async function pruneOldVersions(cacheRoot, keepDir) {
|
|
|
332
332
|
.map((entry) => path.join(versionsRoot, entry.name))
|
|
333
333
|
.filter((dirPath) => dirPath !== keepDir)
|
|
334
334
|
.map((dirPath) => fsp.rm(dirPath, { recursive: true, force: true })),
|
|
335
|
-
)
|
|
335
|
+
)
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
function spawnLauncherProcess(executablePath, options = {}) {
|
|
@@ -340,77 +340,77 @@ function spawnLauncherProcess(executablePath, options = {}) {
|
|
|
340
340
|
...process.env,
|
|
341
341
|
HOWCODE_REPO_ROOT: process.env.HOWCODE_REPO_ROOT || process.cwd(),
|
|
342
342
|
...(options.env || {}),
|
|
343
|
-
}
|
|
344
|
-
Reflect.deleteProperty(env,
|
|
343
|
+
}
|
|
344
|
+
Reflect.deleteProperty(env, 'NODE_TLS_REJECT_UNAUTHORIZED')
|
|
345
345
|
|
|
346
346
|
return spawn(executablePath, [], {
|
|
347
347
|
detached: true,
|
|
348
|
-
stdio: options.stdio ||
|
|
348
|
+
stdio: options.stdio || 'ignore',
|
|
349
349
|
windowsHide: true,
|
|
350
350
|
cwd: path.dirname(executablePath),
|
|
351
351
|
env,
|
|
352
|
-
})
|
|
352
|
+
})
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
async function launch(executablePath) {
|
|
356
|
-
const child = spawnLauncherProcess(executablePath)
|
|
356
|
+
const child = spawnLauncherProcess(executablePath)
|
|
357
357
|
|
|
358
|
-
child.unref()
|
|
358
|
+
child.unref()
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
async function main() {
|
|
362
|
-
const target = getTarget()
|
|
363
|
-
const cacheRoot = getCacheRoot()
|
|
364
|
-
await fsp.mkdir(cacheRoot, { recursive: true })
|
|
362
|
+
const target = getTarget()
|
|
363
|
+
const cacheRoot = getCacheRoot()
|
|
364
|
+
await fsp.mkdir(cacheRoot, { recursive: true })
|
|
365
365
|
|
|
366
|
-
const current = readJsonIfPresent(path.join(cacheRoot,
|
|
366
|
+
const current = readJsonIfPresent(path.join(cacheRoot, 'current.json'))
|
|
367
367
|
|
|
368
|
-
let releaseInfo = null
|
|
368
|
+
let releaseInfo = null
|
|
369
369
|
try {
|
|
370
|
-
releaseInfo = await resolveLatestRelease(target)
|
|
370
|
+
releaseInfo = await resolveLatestRelease(target)
|
|
371
371
|
} catch (error) {
|
|
372
372
|
if (current?.executablePath && fs.existsSync(current.executablePath)) {
|
|
373
373
|
await ensureWindowsLaunchIntegration(target, {
|
|
374
374
|
cacheRoot,
|
|
375
|
-
currentFile: path.join(cacheRoot,
|
|
375
|
+
currentFile: path.join(cacheRoot, 'current.json'),
|
|
376
376
|
windowsCommandFile: path.join(cacheRoot, `${APP_NAME}.cmd`),
|
|
377
377
|
installDir: current.installDir || path.dirname(path.dirname(current.executablePath)),
|
|
378
378
|
launcherWorkingDirectory: path.dirname(current.executablePath),
|
|
379
379
|
executablePath: current.executablePath,
|
|
380
|
-
})
|
|
381
|
-
await launch(current.executablePath)
|
|
382
|
-
return
|
|
380
|
+
})
|
|
381
|
+
await launch(current.executablePath)
|
|
382
|
+
return
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
throw error
|
|
385
|
+
throw error
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
const paths = getPaths(target, releaseInfo)
|
|
389
|
-
const didInstall = !fs.existsSync(paths.executablePath)
|
|
388
|
+
const paths = getPaths(target, releaseInfo)
|
|
389
|
+
const didInstall = !fs.existsSync(paths.executablePath)
|
|
390
390
|
if (!fs.existsSync(paths.executablePath)) {
|
|
391
|
-
await installRelease(target, releaseInfo, paths)
|
|
391
|
+
await installRelease(target, releaseInfo, paths)
|
|
392
392
|
}
|
|
393
393
|
|
|
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.`)
|
|
394
|
+
const launchIntegrationReady = await ensureWindowsLaunchIntegration(target, paths)
|
|
395
|
+
if (target.os === 'win' && didInstall && launchIntegrationReady) {
|
|
396
|
+
console.log(`${APP_NAME}: installed. You can relaunch it from the Windows Start Menu.`)
|
|
397
397
|
}
|
|
398
|
-
await pruneOldVersions(cacheRoot, paths.installDir)
|
|
399
|
-
await launch(paths.executablePath)
|
|
398
|
+
await pruneOldVersions(cacheRoot, paths.installDir)
|
|
399
|
+
await launch(paths.executablePath)
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
module.exports = {
|
|
403
403
|
main: async () => {
|
|
404
404
|
try {
|
|
405
|
-
await main()
|
|
405
|
+
await main()
|
|
406
406
|
} catch (error) {
|
|
407
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
408
|
-
console.error(`howcode: ${message}`)
|
|
409
|
-
process.exit(1)
|
|
407
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
408
|
+
console.error(`howcode: ${message}`)
|
|
409
|
+
process.exit(1)
|
|
410
410
|
}
|
|
411
411
|
},
|
|
412
|
-
}
|
|
412
|
+
}
|
|
413
413
|
|
|
414
414
|
if (require.main === module) {
|
|
415
|
-
module.exports.main()
|
|
415
|
+
module.exports.main()
|
|
416
416
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "howcode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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"
|