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.
Files changed (3) hide show
  1. package/README.md +28 -9
  2. package/lib/howcode.js +31 -132
  3. package/package.json +6 -3
package/README.md CHANGED
@@ -1,8 +1,16 @@
1
1
  # howcode
2
2
 
3
- Launch the Howcode desktop app from npm.
3
+ Howcode is a desktop app for coding with Pi.
4
4
 
5
- ## Use
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
- On first run, the launcher downloads the matching desktop build from GitHub Releases and caches it locally.
22
+ This npm package is a small launcher.
15
23
 
16
- ## Linux note
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
- If the Linux build hits a WebKit/GBM white-screen issue, the launcher retries with `WEBKIT_DISABLE_DMABUF_RENDERER=1` automatically.
27
+ ## What you actually get
19
28
 
20
- If you are launching a downloaded Linux release asset manually, use:
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
- ```bash
23
- WEBKIT_DISABLE_DMABUF_RENDERER=1 ./howcode/bin/launcher
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, spawnSync } = require("node:child_process");
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/launcher`,
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/launcher`,
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}/bin/launcher`,
31
+ executable: `${APP_NAME}/${APP_NAME}`,
29
32
  },
30
33
  "linux:x64": {
31
34
  os: "linux",
32
35
  arch: "x64",
33
- executable: `${APP_NAME}/bin/launcher`,
36
+ executable: `${APP_NAME}/${APP_NAME}`,
34
37
  },
35
38
  "win32:arm64": {
36
39
  os: "win",
37
- arch: "x64",
38
- executable: `${APP_NAME}/bin/launcher.exe`,
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}/bin/launcher.exe`,
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(), 60_000);
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 extract = spawnSync("tar", ["-xzf", archivePath, "-C", tempInstallDir], {
159
- stdio: "inherit",
160
- });
161
-
162
- if (extract.status !== 0) {
163
- throw new Error("Failed to extract downloaded archive with `tar -xzf`.");
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
- if (process.platform !== "linux" || process.env.WEBKIT_DISABLE_DMABUF_RENDERER === "1") {
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.1",
4
- "description": "Launch the Howcode desktop app from npm or npx.",
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", "launcher", "pi", "coding"],
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"