howcode 0.1.0 → 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 +32 -3
- package/lib/howcode.js +43 -22
- 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,7 +19,28 @@ npm i -g howcode
|
|
|
11
19
|
howcode
|
|
12
20
|
```
|
|
13
21
|
|
|
14
|
-
|
|
22
|
+
This npm package is a small launcher.
|
|
23
|
+
|
|
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.
|
|
26
|
+
|
|
27
|
+
## What you actually get
|
|
28
|
+
|
|
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
|
|
33
|
+
|
|
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.
|
|
15
44
|
|
|
16
45
|
## Cache location
|
|
17
46
|
|
package/lib/howcode.js
CHANGED
|
@@ -1,46 +1,49 @@
|
|
|
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
|
|
|
@@ -108,9 +111,9 @@ async function fetchJson(url) {
|
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
async function downloadFile(url, filePath) {
|
|
114
|
+
async function downloadFile(url, filePath, timeoutMs = DOWNLOAD_TIMEOUT_MS) {
|
|
112
115
|
const controller = new AbortController();
|
|
113
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
116
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
114
117
|
|
|
115
118
|
try {
|
|
116
119
|
const response = await fetch(url, { signal: controller.signal });
|
|
@@ -125,6 +128,12 @@ async function downloadFile(url, filePath) {
|
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
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
|
+
|
|
128
137
|
async function resolveLatestRelease(target) {
|
|
129
138
|
const updateUrl = `${RELEASE_BASE_URL}/stable-${target.os}-${target.arch}-update.json`;
|
|
130
139
|
const metadata = await fetchJson(updateUrl);
|
|
@@ -151,16 +160,19 @@ async function installRelease(target, releaseInfo, paths) {
|
|
|
151
160
|
await fsp.mkdir(tempRoot, { recursive: true });
|
|
152
161
|
await fsp.mkdir(path.dirname(paths.installDir), { recursive: true });
|
|
153
162
|
await downloadFile(releaseInfo.assetUrl, archivePath);
|
|
154
|
-
await fsp.mkdir(tempInstallDir, { recursive: true });
|
|
155
|
-
|
|
156
|
-
const extract = spawnSync("tar", ["-xzf", archivePath, "-C", tempInstallDir], {
|
|
157
|
-
stdio: "inherit",
|
|
158
|
-
});
|
|
159
163
|
|
|
160
|
-
|
|
161
|
-
|
|
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
|
+
);
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
await fsp.mkdir(tempInstallDir, { recursive: true });
|
|
173
|
+
|
|
174
|
+
await tar.x({ file: archivePath, cwd: tempInstallDir });
|
|
175
|
+
|
|
164
176
|
if (!fs.existsSync(path.join(tempInstallDir, target.executable))) {
|
|
165
177
|
throw new Error(`Downloaded archive did not contain ${target.executable}.`);
|
|
166
178
|
}
|
|
@@ -203,13 +215,22 @@ async function pruneOldVersions(cacheRoot, keepDir) {
|
|
|
203
215
|
);
|
|
204
216
|
}
|
|
205
217
|
|
|
206
|
-
function
|
|
207
|
-
|
|
218
|
+
function spawnLauncherProcess(executablePath, options = {}) {
|
|
219
|
+
return spawn(executablePath, [], {
|
|
208
220
|
detached: true,
|
|
209
|
-
stdio: "ignore",
|
|
221
|
+
stdio: options.stdio || "ignore",
|
|
210
222
|
windowsHide: true,
|
|
211
223
|
cwd: path.dirname(executablePath),
|
|
224
|
+
env: {
|
|
225
|
+
...process.env,
|
|
226
|
+
HOWCODE_REPO_ROOT: process.env.HOWCODE_REPO_ROOT || process.cwd(),
|
|
227
|
+
...(options.env || {}),
|
|
228
|
+
},
|
|
212
229
|
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function launch(executablePath) {
|
|
233
|
+
const child = spawnLauncherProcess(executablePath);
|
|
213
234
|
|
|
214
235
|
child.unref();
|
|
215
236
|
}
|
|
@@ -226,7 +247,7 @@ async function main() {
|
|
|
226
247
|
releaseInfo = await resolveLatestRelease(target);
|
|
227
248
|
} catch (error) {
|
|
228
249
|
if (current?.executablePath && fs.existsSync(current.executablePath)) {
|
|
229
|
-
launch(current.executablePath);
|
|
250
|
+
await launch(current.executablePath);
|
|
230
251
|
return;
|
|
231
252
|
}
|
|
232
253
|
|
|
@@ -239,7 +260,7 @@ async function main() {
|
|
|
239
260
|
}
|
|
240
261
|
|
|
241
262
|
await pruneOldVersions(cacheRoot, paths.installDir);
|
|
242
|
-
launch(paths.executablePath);
|
|
263
|
+
await launch(paths.executablePath);
|
|
243
264
|
}
|
|
244
265
|
|
|
245
266
|
module.exports = {
|
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"
|