howcode 0.1.2 → 0.1.3
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/README.md +4 -0
- package/lib/howcode.js +141 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,9 +24,13 @@ This npm package is a small launcher.
|
|
|
24
24
|
On first run, it downloads the matching desktop app for your platform from GitHub Releases and caches it locally.
|
|
25
25
|
After the first successful download, it can fall back to the cached app if release metadata is temporarily unavailable.
|
|
26
26
|
|
|
27
|
+
On Windows, the first successful run also creates a Start Menu shortcut for `howcode`, so you do
|
|
28
|
+
not need to find the cached executable or add the cache directory to `PATH`.
|
|
29
|
+
|
|
27
30
|
## What you actually get
|
|
28
31
|
|
|
29
32
|
- macOS, Linux, and Windows desktop builds
|
|
33
|
+
- Windows installer artifacts in GitHub Releases
|
|
30
34
|
- Linux AppImage artifacts for direct installs
|
|
31
35
|
- local cached installs after first download
|
|
32
36
|
- desktop builds that bundle Electron/Chromium for a more consistent renderer
|
package/lib/howcode.js
CHANGED
|
@@ -88,14 +88,134 @@ function getPaths(target, releaseInfo) {
|
|
|
88
88
|
const versionsRoot = path.join(cacheRoot, "versions");
|
|
89
89
|
const releaseKey = `${releaseInfo.version}-${releaseInfo.hash}`;
|
|
90
90
|
const installDir = path.join(versionsRoot, releaseKey);
|
|
91
|
+
const launcherWorkingDirectory = path.dirname(path.join(installDir, target.executable));
|
|
91
92
|
return {
|
|
92
93
|
cacheRoot,
|
|
93
94
|
currentFile: path.join(cacheRoot, "current.json"),
|
|
95
|
+
windowsCommandFile: path.join(cacheRoot, `${APP_NAME}.cmd`),
|
|
96
|
+
launcherWorkingDirectory,
|
|
94
97
|
installDir,
|
|
95
98
|
executablePath: path.join(installDir, target.executable),
|
|
96
99
|
};
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
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`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function escapeWindowsCommandValue(value) {
|
|
108
|
+
return value.replace(/%/g, "%%");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getWindowsScriptHostPath(executableName) {
|
|
112
|
+
const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT;
|
|
113
|
+
if (systemRoot) {
|
|
114
|
+
return path.join(systemRoot, "System32", executableName);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return path.join("C:", "Windows", "System32", executableName);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function writeWindowsCommandLauncher(paths) {
|
|
121
|
+
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)}\"`,
|
|
128
|
+
'if not exist "%HOWCODE_EXE%" (',
|
|
129
|
+
` echo ${APP_NAME}: installed app executable was not found.`,
|
|
130
|
+
` echo Run npx ${APP_NAME} to repair the local install.`,
|
|
131
|
+
" exit /b 1",
|
|
132
|
+
")",
|
|
133
|
+
'start "" /D "%HOWCODE_REPO_ROOT%" "%HOWCODE_EXE%"',
|
|
134
|
+
"endlocal",
|
|
135
|
+
"",
|
|
136
|
+
].join("\r\n");
|
|
137
|
+
|
|
138
|
+
await fsp.writeFile(paths.windowsCommandFile, commandContents, "utf8");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function createWindowsStartMenuShortcut(paths) {
|
|
142
|
+
const shortcutPath = getWindowsStartMenuShortcutPath();
|
|
143
|
+
const shortcutScriptPath = path.join(
|
|
144
|
+
paths.cacheRoot,
|
|
145
|
+
`.create-${APP_NAME}-shortcut-${process.pid}.js`,
|
|
146
|
+
);
|
|
147
|
+
await fsp.mkdir(path.dirname(shortcutPath), { recursive: true });
|
|
148
|
+
await fsp.writeFile(
|
|
149
|
+
shortcutScriptPath,
|
|
150
|
+
[
|
|
151
|
+
"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
|
+
);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await new Promise((resolve, reject) => {
|
|
165
|
+
const child = spawn(
|
|
166
|
+
getWindowsScriptHostPath("cscript.exe"),
|
|
167
|
+
[
|
|
168
|
+
"//NoLogo",
|
|
169
|
+
shortcutScriptPath,
|
|
170
|
+
shortcutPath,
|
|
171
|
+
paths.windowsCommandFile,
|
|
172
|
+
paths.launcherWorkingDirectory,
|
|
173
|
+
`${paths.executablePath},0`,
|
|
174
|
+
"howcode",
|
|
175
|
+
],
|
|
176
|
+
{ stdio: "ignore", windowsHide: true },
|
|
177
|
+
);
|
|
178
|
+
child.on("error", reject);
|
|
179
|
+
child.on("exit", (code) => {
|
|
180
|
+
if (code === 0) {
|
|
181
|
+
resolve();
|
|
182
|
+
} else {
|
|
183
|
+
reject(new Error(`cscript exited with code ${code} while creating Start Menu shortcut.`));
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
} finally {
|
|
188
|
+
await fsp.rm(shortcutScriptPath, { force: true });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return shortcutPath;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function ensureWindowsLaunchIntegration(target, paths) {
|
|
195
|
+
if (target.os !== "win") {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await writeWindowsCommandLauncher(paths);
|
|
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;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await createWindowsStartMenuShortcut(paths);
|
|
210
|
+
return true;
|
|
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;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
99
219
|
async function fetchJson(url) {
|
|
100
220
|
const controller = new AbortController();
|
|
101
221
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
@@ -216,16 +336,19 @@ async function pruneOldVersions(cacheRoot, keepDir) {
|
|
|
216
336
|
}
|
|
217
337
|
|
|
218
338
|
function spawnLauncherProcess(executablePath, options = {}) {
|
|
339
|
+
const env = {
|
|
340
|
+
...process.env,
|
|
341
|
+
HOWCODE_REPO_ROOT: process.env.HOWCODE_REPO_ROOT || process.cwd(),
|
|
342
|
+
...(options.env || {}),
|
|
343
|
+
};
|
|
344
|
+
Reflect.deleteProperty(env, "NODE_TLS_REJECT_UNAUTHORIZED");
|
|
345
|
+
|
|
219
346
|
return spawn(executablePath, [], {
|
|
220
347
|
detached: true,
|
|
221
348
|
stdio: options.stdio || "ignore",
|
|
222
349
|
windowsHide: true,
|
|
223
350
|
cwd: path.dirname(executablePath),
|
|
224
|
-
env
|
|
225
|
-
...process.env,
|
|
226
|
-
HOWCODE_REPO_ROOT: process.env.HOWCODE_REPO_ROOT || process.cwd(),
|
|
227
|
-
...(options.env || {}),
|
|
228
|
-
},
|
|
351
|
+
env,
|
|
229
352
|
});
|
|
230
353
|
}
|
|
231
354
|
|
|
@@ -247,6 +370,14 @@ async function main() {
|
|
|
247
370
|
releaseInfo = await resolveLatestRelease(target);
|
|
248
371
|
} catch (error) {
|
|
249
372
|
if (current?.executablePath && fs.existsSync(current.executablePath)) {
|
|
373
|
+
await ensureWindowsLaunchIntegration(target, {
|
|
374
|
+
cacheRoot,
|
|
375
|
+
currentFile: path.join(cacheRoot, "current.json"),
|
|
376
|
+
windowsCommandFile: path.join(cacheRoot, `${APP_NAME}.cmd`),
|
|
377
|
+
installDir: current.installDir || path.dirname(path.dirname(current.executablePath)),
|
|
378
|
+
launcherWorkingDirectory: path.dirname(current.executablePath),
|
|
379
|
+
executablePath: current.executablePath,
|
|
380
|
+
});
|
|
250
381
|
await launch(current.executablePath);
|
|
251
382
|
return;
|
|
252
383
|
}
|
|
@@ -255,10 +386,15 @@ async function main() {
|
|
|
255
386
|
}
|
|
256
387
|
|
|
257
388
|
const paths = getPaths(target, releaseInfo);
|
|
389
|
+
const didInstall = !fs.existsSync(paths.executablePath);
|
|
258
390
|
if (!fs.existsSync(paths.executablePath)) {
|
|
259
391
|
await installRelease(target, releaseInfo, paths);
|
|
260
392
|
}
|
|
261
393
|
|
|
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
|
+
}
|
|
262
398
|
await pruneOldVersions(cacheRoot, paths.installDir);
|
|
263
399
|
await launch(paths.executablePath);
|
|
264
400
|
}
|