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 CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- require("../lib/howcode.js").main();
3
+ require('../lib/howcode.js').main()
package/lib/howcode.js CHANGED
@@ -1,305 +1,329 @@
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;
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
- "darwin:arm64": {
19
- os: "macos",
20
- arch: "arm64",
18
+ 'darwin:arm64': {
19
+ os: 'macos',
20
+ arch: 'arm64',
21
21
  executable: `${APP_NAME}.app/Contents/MacOS/${APP_NAME}`,
22
22
  },
23
- "darwin:x64": {
24
- os: "macos",
25
- arch: "x64",
23
+ 'darwin:x64': {
24
+ os: 'macos',
25
+ arch: 'x64',
26
26
  executable: `${APP_NAME}.app/Contents/MacOS/${APP_NAME}`,
27
27
  },
28
- "linux:arm64": {
29
- os: "linux",
30
- arch: "arm64",
28
+ 'linux:arm64': {
29
+ os: 'linux',
30
+ arch: 'arm64',
31
31
  executable: `${APP_NAME}/${APP_NAME}`,
32
32
  },
33
- "linux:x64": {
34
- os: "linux",
35
- arch: "x64",
33
+ 'linux:x64': {
34
+ os: 'linux',
35
+ arch: 'x64',
36
36
  executable: `${APP_NAME}/${APP_NAME}`,
37
37
  },
38
- "win32:arm64": {
39
- os: "win",
40
- arch: "arm64",
38
+ 'win32:arm64': {
39
+ os: 'win',
40
+ arch: 'arm64',
41
41
  executable: `${APP_NAME}/${APP_NAME}.exe`,
42
42
  },
43
- "win32:x64": {
44
- os: "win",
45
- arch: "x64",
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, "utf8"));
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 === "win32") {
72
+ if (process.platform === 'win32') {
73
73
  return path.join(
74
- process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"),
74
+ process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
75
75
  APP_NAME,
76
- );
76
+ )
77
77
  }
78
78
 
79
- if (process.platform === "darwin") {
80
- return path.join(os.homedir(), "Library", "Caches", APP_NAME);
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(), ".cache"), APP_NAME);
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, "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));
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, "current.json"),
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(), "AppData", "Roaming");
104
- return path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", `${APP_NAME}.lnk`);
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, "System32", executableName);
134
+ return path.join(systemRoot, 'System32', executableName)
115
135
  }
116
136
 
117
- return path.join("C:", "Windows", "System32", executableName);
137
+ return path.join('C:', 'Windows', 'System32', executableName)
118
138
  }
119
139
 
120
140
  async function writeWindowsCommandLauncher(paths) {
121
141
  const commandContents = [
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)}\"`,
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
- " exit /b 1",
132
- ")",
151
+ ' exit /b 1',
152
+ ')',
133
153
  'start "" /D "%HOWCODE_REPO_ROOT%" "%HOWCODE_EXE%"',
134
- "endlocal",
135
- "",
136
- ].join("\r\n");
154
+ 'endlocal',
155
+ '',
156
+ ].join('\r\n')
137
157
 
138
- await fsp.writeFile(paths.windowsCommandFile, commandContents, "utf8");
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
- "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
- );
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("cscript.exe"),
186
+ getWindowsScriptHostPath('cscript.exe'),
167
187
  [
168
- "//NoLogo",
188
+ '//NoLogo',
169
189
  shortcutScriptPath,
170
190
  shortcutPath,
171
191
  paths.windowsCommandFile,
172
192
  paths.launcherWorkingDirectory,
173
193
  `${paths.executablePath},0`,
174
- "howcode",
194
+ 'howcode',
175
195
  ],
176
- { stdio: "ignore", windowsHide: true },
177
- );
178
- child.on("error", reject);
179
- child.on("exit", (code) => {
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 !== "win") {
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 || !response.body) {
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("sha256");
253
- await pipeline(fs.createReadStream(filePath), hash);
254
- return hash.digest("hex");
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 !== "string" || typeof metadata.hash !== "string") {
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, "versions");
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, "NODE_TLS_REJECT_UNAUTHORIZED");
367
+ }
368
+ Reflect.deleteProperty(env, 'NODE_TLS_REJECT_UNAUTHORIZED')
345
369
 
346
370
  return spawn(executablePath, [], {
347
371
  detached: true,
348
- stdio: options.stdio || "ignore",
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, "current.json"));
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 && fs.existsSync(current.executablePath)) {
373
- await ensureWindowsLaunchIntegration(target, {
396
+ if (current?.executablePath) {
397
+ const currentPaths = {
374
398
  cacheRoot,
375
- currentFile: path.join(cacheRoot, "current.json"),
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
- await launch(current.executablePath);
382
- return;
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 = !fs.existsSync(paths.executablePath);
390
- if (!fs.existsSync(paths.executablePath)) {
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 === "win" && didInstall && launchIntegrationReady) {
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.5",
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": ["bin", "lib", "README.md", "LICENSE"],
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": ["desktop", "coding", "ai", "terminal", "git", "diff", "assistant"],
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"