howcode 0.1.1 → 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 +32 -9
- package/lib/howcode.js +171 -136
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# howcode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Howcode is a desktop app for coding with Pi.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It gives you:
|
|
6
|
+
|
|
7
|
+
- threaded Pi chats tied to your projects
|
|
8
|
+
- a built-in terminal
|
|
9
|
+
- project and inbox sidebars
|
|
10
|
+
- git and diff workflows in the app, with some early-release actions still partial
|
|
11
|
+
- local desktop performance instead of a browser tab
|
|
12
|
+
|
|
13
|
+
## Install / run
|
|
6
14
|
|
|
7
15
|
```bash
|
|
8
16
|
npx howcode
|
|
@@ -11,17 +19,32 @@ npm i -g howcode
|
|
|
11
19
|
howcode
|
|
12
20
|
```
|
|
13
21
|
|
|
14
|
-
|
|
22
|
+
This npm package is a small launcher.
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+
On first run, it downloads the matching desktop app for your platform from GitHub Releases and caches it locally.
|
|
25
|
+
After the first successful download, it can fall back to the cached app if release metadata is temporarily unavailable.
|
|
17
26
|
|
|
18
|
-
|
|
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`.
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
## What you actually get
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
- macOS, Linux, and Windows desktop builds
|
|
33
|
+
- Windows installer artifacts in GitHub Releases
|
|
34
|
+
- Linux AppImage artifacts for direct installs
|
|
35
|
+
- local cached installs after first download
|
|
36
|
+
- desktop builds that bundle Electron/Chromium for a more consistent renderer
|
|
37
|
+
|
|
38
|
+
## Project
|
|
39
|
+
|
|
40
|
+
- App repo: https://github.com/IgorWarzocha/howcode
|
|
41
|
+
- Issues: https://github.com/IgorWarzocha/howcode/issues
|
|
42
|
+
|
|
43
|
+
## Renderer note
|
|
44
|
+
|
|
45
|
+
Release builds now bundle Electron/Chromium on macOS, Linux, and Windows. The launcher no longer needs to inject the old Linux `WEBKIT_DISABLE_DMABUF_RENDERER` workaround.
|
|
46
|
+
|
|
47
|
+
Expect downloads to be larger than the native-webview builds in exchange for more consistent rendering behavior.
|
|
25
48
|
|
|
26
49
|
## Cache location
|
|
27
50
|
|
package/lib/howcode.js
CHANGED
|
@@ -1,51 +1,52 @@
|
|
|
1
1
|
const fs = require("node:fs");
|
|
2
2
|
const fsp = require("node:fs/promises");
|
|
3
|
+
const crypto = require("node:crypto");
|
|
3
4
|
const os = require("node:os");
|
|
4
5
|
const path = require("node:path");
|
|
5
|
-
const { spawn
|
|
6
|
+
const { spawn } = require("node:child_process");
|
|
6
7
|
const { pipeline } = require("node:stream/promises");
|
|
7
8
|
const { Readable } = require("node:stream");
|
|
9
|
+
const tar = require("tar");
|
|
8
10
|
|
|
9
11
|
const packageJson = require("../package.json");
|
|
10
12
|
|
|
11
13
|
const APP_NAME = packageJson.howcode.appName;
|
|
12
14
|
const RELEASE_BASE_URL = process.env.HOWCODE_BASE_URL || packageJson.howcode.releaseBaseUrl;
|
|
15
|
+
const DOWNLOAD_TIMEOUT_MS = 5 * 60_000;
|
|
13
16
|
|
|
14
17
|
const TARGETS = {
|
|
15
18
|
"darwin:arm64": {
|
|
16
19
|
os: "macos",
|
|
17
20
|
arch: "arm64",
|
|
18
|
-
executable: `${APP_NAME}.app/Contents/MacOS
|
|
21
|
+
executable: `${APP_NAME}.app/Contents/MacOS/${APP_NAME}`,
|
|
19
22
|
},
|
|
20
23
|
"darwin:x64": {
|
|
21
24
|
os: "macos",
|
|
22
25
|
arch: "x64",
|
|
23
|
-
executable: `${APP_NAME}.app/Contents/MacOS
|
|
26
|
+
executable: `${APP_NAME}.app/Contents/MacOS/${APP_NAME}`,
|
|
24
27
|
},
|
|
25
28
|
"linux:arm64": {
|
|
26
29
|
os: "linux",
|
|
27
30
|
arch: "arm64",
|
|
28
|
-
executable: `${APP_NAME}
|
|
31
|
+
executable: `${APP_NAME}/${APP_NAME}`,
|
|
29
32
|
},
|
|
30
33
|
"linux:x64": {
|
|
31
34
|
os: "linux",
|
|
32
35
|
arch: "x64",
|
|
33
|
-
executable: `${APP_NAME}
|
|
36
|
+
executable: `${APP_NAME}/${APP_NAME}`,
|
|
34
37
|
},
|
|
35
38
|
"win32:arm64": {
|
|
36
39
|
os: "win",
|
|
37
|
-
arch: "
|
|
38
|
-
executable: `${APP_NAME}
|
|
40
|
+
arch: "arm64",
|
|
41
|
+
executable: `${APP_NAME}/${APP_NAME}.exe`,
|
|
39
42
|
},
|
|
40
43
|
"win32:x64": {
|
|
41
44
|
os: "win",
|
|
42
45
|
arch: "x64",
|
|
43
|
-
executable: `${APP_NAME}
|
|
46
|
+
executable: `${APP_NAME}/${APP_NAME}.exe`,
|
|
44
47
|
},
|
|
45
48
|
};
|
|
46
49
|
|
|
47
|
-
const LINUX_DMABUF_FAILURE_PATTERNS = [/Failed to create GBM buffer/i, /GLXBadWindow/i, /dmabuf/i];
|
|
48
|
-
|
|
49
50
|
function readJsonIfPresent(filePath) {
|
|
50
51
|
try {
|
|
51
52
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
@@ -87,14 +88,134 @@ function getPaths(target, releaseInfo) {
|
|
|
87
88
|
const versionsRoot = path.join(cacheRoot, "versions");
|
|
88
89
|
const releaseKey = `${releaseInfo.version}-${releaseInfo.hash}`;
|
|
89
90
|
const installDir = path.join(versionsRoot, releaseKey);
|
|
91
|
+
const launcherWorkingDirectory = path.dirname(path.join(installDir, target.executable));
|
|
90
92
|
return {
|
|
91
93
|
cacheRoot,
|
|
92
94
|
currentFile: path.join(cacheRoot, "current.json"),
|
|
95
|
+
windowsCommandFile: path.join(cacheRoot, `${APP_NAME}.cmd`),
|
|
96
|
+
launcherWorkingDirectory,
|
|
93
97
|
installDir,
|
|
94
98
|
executablePath: path.join(installDir, target.executable),
|
|
95
99
|
};
|
|
96
100
|
}
|
|
97
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
|
+
|
|
98
219
|
async function fetchJson(url) {
|
|
99
220
|
const controller = new AbortController();
|
|
100
221
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
@@ -110,9 +231,9 @@ async function fetchJson(url) {
|
|
|
110
231
|
}
|
|
111
232
|
}
|
|
112
233
|
|
|
113
|
-
async function downloadFile(url, filePath) {
|
|
234
|
+
async function downloadFile(url, filePath, timeoutMs = DOWNLOAD_TIMEOUT_MS) {
|
|
114
235
|
const controller = new AbortController();
|
|
115
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
236
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
116
237
|
|
|
117
238
|
try {
|
|
118
239
|
const response = await fetch(url, { signal: controller.signal });
|
|
@@ -127,6 +248,12 @@ async function downloadFile(url, filePath) {
|
|
|
127
248
|
}
|
|
128
249
|
}
|
|
129
250
|
|
|
251
|
+
async function sha256File(filePath) {
|
|
252
|
+
const hash = crypto.createHash("sha256");
|
|
253
|
+
await pipeline(fs.createReadStream(filePath), hash);
|
|
254
|
+
return hash.digest("hex");
|
|
255
|
+
}
|
|
256
|
+
|
|
130
257
|
async function resolveLatestRelease(target) {
|
|
131
258
|
const updateUrl = `${RELEASE_BASE_URL}/stable-${target.os}-${target.arch}-update.json`;
|
|
132
259
|
const metadata = await fetchJson(updateUrl);
|
|
@@ -153,16 +280,19 @@ async function installRelease(target, releaseInfo, paths) {
|
|
|
153
280
|
await fsp.mkdir(tempRoot, { recursive: true });
|
|
154
281
|
await fsp.mkdir(path.dirname(paths.installDir), { recursive: true });
|
|
155
282
|
await downloadFile(releaseInfo.assetUrl, archivePath);
|
|
156
|
-
await fsp.mkdir(tempInstallDir, { recursive: true });
|
|
157
|
-
|
|
158
|
-
const extract = spawnSync("tar", ["-xzf", archivePath, "-C", tempInstallDir], {
|
|
159
|
-
stdio: "inherit",
|
|
160
|
-
});
|
|
161
283
|
|
|
162
|
-
|
|
163
|
-
|
|
284
|
+
const archiveHash = await sha256File(archivePath);
|
|
285
|
+
if (archiveHash !== releaseInfo.hash) {
|
|
286
|
+
await fsp.rm(tempRoot, { recursive: true, force: true });
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Downloaded archive hash mismatch. Expected ${releaseInfo.hash}, got ${archiveHash}.`,
|
|
289
|
+
);
|
|
164
290
|
}
|
|
165
291
|
|
|
292
|
+
await fsp.mkdir(tempInstallDir, { recursive: true });
|
|
293
|
+
|
|
294
|
+
await tar.x({ file: archivePath, cwd: tempInstallDir });
|
|
295
|
+
|
|
166
296
|
if (!fs.existsSync(path.join(tempInstallDir, target.executable))) {
|
|
167
297
|
throw new Error(`Downloaded archive did not contain ${target.executable}.`);
|
|
168
298
|
}
|
|
@@ -206,134 +336,26 @@ async function pruneOldVersions(cacheRoot, keepDir) {
|
|
|
206
336
|
}
|
|
207
337
|
|
|
208
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
|
+
|
|
209
346
|
return spawn(executablePath, [], {
|
|
210
347
|
detached: true,
|
|
211
348
|
stdio: options.stdio || "ignore",
|
|
212
349
|
windowsHide: true,
|
|
213
350
|
cwd: path.dirname(executablePath),
|
|
214
|
-
env
|
|
215
|
-
...process.env,
|
|
216
|
-
...(options.env || {}),
|
|
217
|
-
},
|
|
351
|
+
env,
|
|
218
352
|
});
|
|
219
353
|
}
|
|
220
354
|
|
|
221
|
-
function killDetachedProcess(child) {
|
|
222
|
-
if (!child.pid) {
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
if (process.platform !== "win32") {
|
|
228
|
-
process.kill(-child.pid, "SIGTERM");
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
} catch {}
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
child.kill("SIGTERM");
|
|
235
|
-
} catch {}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function hasLinuxDmabufFailure(output) {
|
|
239
|
-
return LINUX_DMABUF_FAILURE_PATTERNS.some((pattern) => pattern.test(output));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function trimLauncherLog(output) {
|
|
243
|
-
return output.trim().split(/\r?\n/).slice(-10).join("\n");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
355
|
async function launch(executablePath) {
|
|
247
|
-
|
|
248
|
-
const child = spawnLauncherProcess(executablePath);
|
|
249
|
-
child.unref();
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const child = spawnLauncherProcess(executablePath, {
|
|
254
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
let output = "";
|
|
258
|
-
const appendOutput = (chunk) => {
|
|
259
|
-
output += chunk.toString();
|
|
260
|
-
if (output.length > 32_000) {
|
|
261
|
-
output = output.slice(-32_000);
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
child.stdout?.on("data", appendOutput);
|
|
266
|
-
child.stderr?.on("data", appendOutput);
|
|
267
|
-
|
|
268
|
-
const outcome = await new Promise((resolve) => {
|
|
269
|
-
let settled = false;
|
|
270
|
-
|
|
271
|
-
const finish = (value) => {
|
|
272
|
-
if (settled) {
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
settled = true;
|
|
277
|
-
clearTimeout(timer);
|
|
278
|
-
resolve(value);
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
const checkForFallback = () => {
|
|
282
|
-
if (hasLinuxDmabufFailure(output)) {
|
|
283
|
-
finish({ type: "fallback" });
|
|
284
|
-
}
|
|
285
|
-
};
|
|
356
|
+
const child = spawnLauncherProcess(executablePath);
|
|
286
357
|
|
|
287
|
-
const timer = setTimeout(() => finish({ type: "ok" }), 4_000);
|
|
288
|
-
|
|
289
|
-
child.stdout?.on("data", checkForFallback);
|
|
290
|
-
child.stderr?.on("data", checkForFallback);
|
|
291
|
-
child.once("error", (error) => finish({ type: "error", error }));
|
|
292
|
-
child.once("exit", (code, signal) => {
|
|
293
|
-
if (hasLinuxDmabufFailure(output)) {
|
|
294
|
-
finish({ type: "fallback" });
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
finish({ type: "exit", code, signal });
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
child.stdout?.destroy();
|
|
303
|
-
child.stderr?.destroy();
|
|
304
358
|
child.unref();
|
|
305
|
-
|
|
306
|
-
if (outcome.type === "fallback") {
|
|
307
|
-
killDetachedProcess(child);
|
|
308
|
-
|
|
309
|
-
const fallbackChild = spawnLauncherProcess(executablePath, {
|
|
310
|
-
env: {
|
|
311
|
-
WEBKIT_DISABLE_DMABUF_RENDERER: "1",
|
|
312
|
-
},
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
fallbackChild.unref();
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (outcome.type === "ok") {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (outcome.type === "error") {
|
|
324
|
-
throw outcome.error;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (outcome.code === 0) {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const logTail = trimLauncherLog(output);
|
|
332
|
-
throw new Error(
|
|
333
|
-
logTail
|
|
334
|
-
? `Desktop launcher exited early.\n${logTail}`
|
|
335
|
-
: `Desktop launcher exited early with code ${outcome.code ?? "unknown"}.`,
|
|
336
|
-
);
|
|
337
359
|
}
|
|
338
360
|
|
|
339
361
|
async function main() {
|
|
@@ -348,6 +370,14 @@ async function main() {
|
|
|
348
370
|
releaseInfo = await resolveLatestRelease(target);
|
|
349
371
|
} catch (error) {
|
|
350
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
|
+
});
|
|
351
381
|
await launch(current.executablePath);
|
|
352
382
|
return;
|
|
353
383
|
}
|
|
@@ -356,10 +386,15 @@ async function main() {
|
|
|
356
386
|
}
|
|
357
387
|
|
|
358
388
|
const paths = getPaths(target, releaseInfo);
|
|
389
|
+
const didInstall = !fs.existsSync(paths.executablePath);
|
|
359
390
|
if (!fs.existsSync(paths.executablePath)) {
|
|
360
391
|
await installRelease(target, releaseInfo, paths);
|
|
361
392
|
}
|
|
362
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
|
+
}
|
|
363
398
|
await pruneOldVersions(cacheRoot, paths.installDir);
|
|
364
399
|
await launch(paths.executablePath);
|
|
365
400
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "howcode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Desktop coding app for Pi with projects, terminal, git, and diff workflows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Igor Warzocha",
|
|
7
7
|
"homepage": "https://github.com/IgorWarzocha/howcode",
|
|
@@ -16,10 +16,13 @@
|
|
|
16
16
|
"howcode": "bin/howcode.js"
|
|
17
17
|
},
|
|
18
18
|
"files": ["bin", "lib", "README.md", "LICENSE"],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"tar": "^7.5.1"
|
|
21
|
+
},
|
|
19
22
|
"engines": {
|
|
20
23
|
"node": ">=18"
|
|
21
24
|
},
|
|
22
|
-
"keywords": ["desktop", "
|
|
25
|
+
"keywords": ["desktop", "coding", "ai", "terminal", "git", "diff", "assistant"],
|
|
23
26
|
"howcode": {
|
|
24
27
|
"appName": "howcode",
|
|
25
28
|
"releaseBaseUrl": "https://github.com/IgorWarzocha/howcode/releases/latest/download"
|