howcode 0.1.1 → 0.1.2
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 +28 -9
- package/lib/howcode.js +31 -132
- 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,28 @@ 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
|
+
## What you actually get
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
- macOS, Linux, and Windows desktop builds
|
|
30
|
+
- Linux AppImage artifacts for direct installs
|
|
31
|
+
- local cached installs after first download
|
|
32
|
+
- desktop builds that bundle Electron/Chromium for a more consistent renderer
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
## Project
|
|
35
|
+
|
|
36
|
+
- App repo: https://github.com/IgorWarzocha/howcode
|
|
37
|
+
- Issues: https://github.com/IgorWarzocha/howcode/issues
|
|
38
|
+
|
|
39
|
+
## Renderer note
|
|
40
|
+
|
|
41
|
+
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.
|
|
42
|
+
|
|
43
|
+
Expect downloads to be larger than the native-webview builds in exchange for more consistent rendering behavior.
|
|
25
44
|
|
|
26
45
|
## Cache location
|
|
27
46
|
|
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"));
|
|
@@ -110,9 +111,9 @@ async function fetchJson(url) {
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
async function downloadFile(url, filePath) {
|
|
114
|
+
async function downloadFile(url, filePath, timeoutMs = DOWNLOAD_TIMEOUT_MS) {
|
|
114
115
|
const controller = new AbortController();
|
|
115
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
116
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
116
117
|
|
|
117
118
|
try {
|
|
118
119
|
const response = await fetch(url, { signal: controller.signal });
|
|
@@ -127,6 +128,12 @@ async function downloadFile(url, filePath) {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
async function sha256File(filePath) {
|
|
132
|
+
const hash = crypto.createHash("sha256");
|
|
133
|
+
await pipeline(fs.createReadStream(filePath), hash);
|
|
134
|
+
return hash.digest("hex");
|
|
135
|
+
}
|
|
136
|
+
|
|
130
137
|
async function resolveLatestRelease(target) {
|
|
131
138
|
const updateUrl = `${RELEASE_BASE_URL}/stable-${target.os}-${target.arch}-update.json`;
|
|
132
139
|
const metadata = await fetchJson(updateUrl);
|
|
@@ -153,16 +160,19 @@ async function installRelease(target, releaseInfo, paths) {
|
|
|
153
160
|
await fsp.mkdir(tempRoot, { recursive: true });
|
|
154
161
|
await fsp.mkdir(path.dirname(paths.installDir), { recursive: true });
|
|
155
162
|
await downloadFile(releaseInfo.assetUrl, archivePath);
|
|
156
|
-
await fsp.mkdir(tempInstallDir, { recursive: true });
|
|
157
163
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
const archiveHash = await sha256File(archivePath);
|
|
165
|
+
if (archiveHash !== releaseInfo.hash) {
|
|
166
|
+
await fsp.rm(tempRoot, { recursive: true, force: true });
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Downloaded archive hash mismatch. Expected ${releaseInfo.hash}, got ${archiveHash}.`,
|
|
169
|
+
);
|
|
164
170
|
}
|
|
165
171
|
|
|
172
|
+
await fsp.mkdir(tempInstallDir, { recursive: true });
|
|
173
|
+
|
|
174
|
+
await tar.x({ file: archivePath, cwd: tempInstallDir });
|
|
175
|
+
|
|
166
176
|
if (!fs.existsSync(path.join(tempInstallDir, target.executable))) {
|
|
167
177
|
throw new Error(`Downloaded archive did not contain ${target.executable}.`);
|
|
168
178
|
}
|
|
@@ -213,127 +223,16 @@ function spawnLauncherProcess(executablePath, options = {}) {
|
|
|
213
223
|
cwd: path.dirname(executablePath),
|
|
214
224
|
env: {
|
|
215
225
|
...process.env,
|
|
226
|
+
HOWCODE_REPO_ROOT: process.env.HOWCODE_REPO_ROOT || process.cwd(),
|
|
216
227
|
...(options.env || {}),
|
|
217
228
|
},
|
|
218
229
|
});
|
|
219
230
|
}
|
|
220
231
|
|
|
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
232
|
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
|
-
};
|
|
233
|
+
const child = spawnLauncherProcess(executablePath);
|
|
264
234
|
|
|
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
|
-
};
|
|
286
|
-
|
|
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
235
|
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
236
|
}
|
|
338
237
|
|
|
339
238
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "howcode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.2",
|
|
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"
|