howcode 0.1.2 → 0.1.4

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 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "howcode",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Desktop coding app for Pi with projects, terminal, git, and diff workflows.",
5
5
  "license": "MIT",
6
6
  "author": "Igor Warzocha",