@joule-sh/code 0.19.0
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 +56 -0
- package/bin/joule +3 -0
- package/bin/relay +3 -0
- package/lib/download.js +84 -0
- package/lib/platform.js +87 -0
- package/lib/postinstall.js +154 -0
- package/lib/resolve.js +70 -0
- package/lib/shim.js +64 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @joule-sh/code
|
|
2
|
+
|
|
3
|
+
An agentic coding terminal you can also drive from a web page.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm i -g @joule-sh/code
|
|
7
|
+
joule
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The command is `joule`, not `code`: `code` is already VS Code's CLI on most
|
|
11
|
+
machines, and this would silently shadow or lose to it depending on `PATH`
|
|
12
|
+
order. `relay` is installed alongside it, the same pair
|
|
13
|
+
[install.sh](https://github.com/joule-sh/code/blob/main/install.sh) puts on
|
|
14
|
+
your `PATH`.
|
|
15
|
+
|
|
16
|
+
## What this package is
|
|
17
|
+
|
|
18
|
+
A wrapper. The binary itself lives in a per-platform package that npm installs
|
|
19
|
+
as an optional dependency and skips everywhere it does not apply:
|
|
20
|
+
|
|
21
|
+
| platform | package |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `linux-x64` | `@joule-sh/code-linux-x64` |
|
|
24
|
+
| `darwin-x64` | `@joule-sh/code-darwin-x64` |
|
|
25
|
+
| `darwin-arm64` | `@joule-sh/code-darwin-arm64` |
|
|
26
|
+
|
|
27
|
+
Every one of them carries the garbage collector it needs, so there is nothing
|
|
28
|
+
to install alongside. The Linux binary is linked statically against musl and
|
|
29
|
+
needs nothing at all from the machine it lands on.
|
|
30
|
+
|
|
31
|
+
**There is no Windows build yet.** Installing here prints a message saying so
|
|
32
|
+
and pointing at
|
|
33
|
+
[#173](https://github.com/joule-sh/code/issues/173). Until that lands, joule
|
|
34
|
+
runs on Windows inside WSL.
|
|
35
|
+
|
|
36
|
+
## If npm skipped the binary
|
|
37
|
+
|
|
38
|
+
npm has a [long-standing bug](https://github.com/npm/cli/issues/4828) where it
|
|
39
|
+
fails to install the optional dependency it should have. The install step here
|
|
40
|
+
falls back to fetching the platform package from the registry itself, so you
|
|
41
|
+
should never see it. If you do, any of these fixes it:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npm install @joule-sh/code-linux-x64
|
|
45
|
+
rm -rf node_modules package-lock.json && npm install
|
|
46
|
+
JOULE_BINARY_PATH=/path/to/the/binaries joule
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`JOULE_BINARY_PATH` takes a directory holding `joule`, `relay` and
|
|
50
|
+
`joule-daemon`, or the path of one of them, and overrides resolution entirely.
|
|
51
|
+
|
|
52
|
+
## Elsewhere
|
|
53
|
+
|
|
54
|
+
- Source, issues and the shell installer:
|
|
55
|
+
<https://github.com/joule-sh/code>
|
|
56
|
+
- The VS Code extension: `joule-sh.joule-editor`
|
package/bin/joule
ADDED
package/bin/relay
ADDED
package/lib/download.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("node:child_process");
|
|
4
|
+
const crypto = require("node:crypto");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
|
|
9
|
+
const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
|
|
10
|
+
|
|
11
|
+
function registry() {
|
|
12
|
+
const configured = (process.env.npm_config_registry || "").trim();
|
|
13
|
+
const base = configured === "" ? DEFAULT_REGISTRY : configured;
|
|
14
|
+
return base.endsWith("/") ? base : base + "/";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function metadataUrl(pkg, version) {
|
|
18
|
+
return registry() + pkg.replace("/", "%2f") + "/" + version;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function checkIntegrity(bytes, integrity) {
|
|
22
|
+
if (typeof integrity !== "string" || integrity === "") { return ""; }
|
|
23
|
+
const at = integrity.indexOf("-");
|
|
24
|
+
const algorithm = integrity.slice(0, at);
|
|
25
|
+
const expected = integrity.slice(at + 1);
|
|
26
|
+
if (["sha512", "sha384", "sha256", "sha1"].indexOf(algorithm) < 0) {
|
|
27
|
+
return "the registry gave an integrity hash this cannot check: " + integrity;
|
|
28
|
+
}
|
|
29
|
+
const actual = crypto.createHash(algorithm).update(bytes).digest("base64");
|
|
30
|
+
if (actual !== expected) {
|
|
31
|
+
return "the downloaded tarball does not match the integrity hash the registry published for it";
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function untar(tarball, into) {
|
|
37
|
+
const result = spawnSync("tar", ["-xzf", tarball, "-C", into], { encoding: "utf8" });
|
|
38
|
+
if (result.error) { return "could not run tar: " + result.error.message; }
|
|
39
|
+
if (result.status !== 0) { return "tar exited " + result.status + ": " + (result.stderr || "").trim(); }
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchPackage(pkg, version, into) {
|
|
44
|
+
const url = metadataUrl(pkg, version);
|
|
45
|
+
let meta = null;
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(url);
|
|
48
|
+
if (!response.ok) { return "the registry answered " + response.status + " for " + url; }
|
|
49
|
+
meta = await response.json();
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return "could not reach " + url + ": " + e.message;
|
|
52
|
+
}
|
|
53
|
+
const tarball = meta && meta.dist && meta.dist.tarball;
|
|
54
|
+
if (typeof tarball !== "string" || tarball === "") {
|
|
55
|
+
return "the registry entry for " + pkg + "@" + version + " names no tarball";
|
|
56
|
+
}
|
|
57
|
+
let bytes = null;
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(tarball);
|
|
60
|
+
if (!response.ok) { return "the registry answered " + response.status + " for " + tarball; }
|
|
61
|
+
bytes = Buffer.from(await response.arrayBuffer());
|
|
62
|
+
} catch (e) {
|
|
63
|
+
return "could not download " + tarball + ": " + e.message;
|
|
64
|
+
}
|
|
65
|
+
const mismatch = checkIntegrity(bytes, meta.dist.integrity);
|
|
66
|
+
if (mismatch !== "") { return mismatch; }
|
|
67
|
+
const work = fs.mkdtempSync(path.join(os.tmpdir(), "joule-npm-"));
|
|
68
|
+
try {
|
|
69
|
+
const file = path.join(work, "package.tgz");
|
|
70
|
+
fs.writeFileSync(file, bytes);
|
|
71
|
+
const failure = untar(file, work);
|
|
72
|
+
if (failure !== "") { return failure; }
|
|
73
|
+
const unpacked = path.join(work, "package", "bin");
|
|
74
|
+
if (!fs.existsSync(unpacked)) { return "the tarball for " + pkg + "@" + version + " carries no bin directory"; }
|
|
75
|
+
fs.rmSync(into, { recursive: true, force: true });
|
|
76
|
+
fs.mkdirSync(path.dirname(into), { recursive: true });
|
|
77
|
+
fs.cpSync(unpacked, into, { recursive: true });
|
|
78
|
+
} finally {
|
|
79
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { registry, metadataUrl, checkIntegrity, fetchPackage, DEFAULT_REGISTRY };
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const REPO = "https://github.com/joule-sh/code";
|
|
4
|
+
const SCOPE = "@joule-sh";
|
|
5
|
+
const WRAPPER = SCOPE + "/code";
|
|
6
|
+
const WINDOWS_ISSUE = REPO + "/issues/173";
|
|
7
|
+
const NPM_BUG = "https://github.com/npm/cli/issues/4828";
|
|
8
|
+
const OVERRIDE = "JOULE_BINARY_PATH";
|
|
9
|
+
|
|
10
|
+
const BUILT = {
|
|
11
|
+
"linux-x64": SCOPE + "/code-linux-x64",
|
|
12
|
+
"darwin-x64": SCOPE + "/code-darwin-x64",
|
|
13
|
+
"darwin-arm64": SCOPE + "/code-darwin-arm64",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const BINARIES = ["joule", "relay", "joule-daemon"];
|
|
17
|
+
const COMMANDS = ["joule", "relay"];
|
|
18
|
+
|
|
19
|
+
function current() {
|
|
20
|
+
return process.platform + "-" + process.arch;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function packageFor(id) {
|
|
24
|
+
return BUILT[id] || "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function supported() {
|
|
28
|
+
return Object.keys(BUILT);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isWindows(id) {
|
|
32
|
+
return id.startsWith("win32-");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function windowsNotice() {
|
|
36
|
+
return [
|
|
37
|
+
"joule: there is no Windows build of joule yet, so there is no binary for this package to install here.",
|
|
38
|
+
" " + WINDOWS_ISSUE + " is the ticket for the Windows binary; watch that rather than this package.",
|
|
39
|
+
" Until it lands, joule runs on Windows through WSL: open a WSL shell and install it from inside there.",
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function unsupportedNotice(id) {
|
|
44
|
+
return [
|
|
45
|
+
"joule: no binary is built for " + id + ".",
|
|
46
|
+
" built platforms: " + supported().join(", ") + ".",
|
|
47
|
+
" build from source instead: " + REPO + "#build-from-source",
|
|
48
|
+
].join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function missingNotice(id, version) {
|
|
52
|
+
const pkg = packageFor(id);
|
|
53
|
+
const pinned = version === "" ? pkg : pkg + "@" + version;
|
|
54
|
+
return [
|
|
55
|
+
"joule: " + WRAPPER + " is installed but the binary for " + id + " is not.",
|
|
56
|
+
" " + pkg + " should have been installed alongside it as an optional dependency.",
|
|
57
|
+
" npm has a long-standing bug where it skips one (" + NPM_BUG + ").",
|
|
58
|
+
" Any one of these fixes it:",
|
|
59
|
+
" npm install " + pinned,
|
|
60
|
+
" remove node_modules and the lockfile, then install again",
|
|
61
|
+
" " + OVERRIDE + "=/path/to/the/binaries " + COMMANDS[0],
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function notice(id, version) {
|
|
66
|
+
if (isWindows(id)) { return windowsNotice(); }
|
|
67
|
+
if (packageFor(id) === "") { return unsupportedNotice(id); }
|
|
68
|
+
return missingNotice(id, version);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
REPO,
|
|
73
|
+
SCOPE,
|
|
74
|
+
WRAPPER,
|
|
75
|
+
WINDOWS_ISSUE,
|
|
76
|
+
OVERRIDE,
|
|
77
|
+
BINARIES,
|
|
78
|
+
COMMANDS,
|
|
79
|
+
current,
|
|
80
|
+
packageFor,
|
|
81
|
+
supported,
|
|
82
|
+
isWindows,
|
|
83
|
+
windowsNotice,
|
|
84
|
+
unsupportedNotice,
|
|
85
|
+
missingNotice,
|
|
86
|
+
notice,
|
|
87
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("node:child_process");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const platform = require("./platform.js");
|
|
7
|
+
const resolve = require("./resolve.js");
|
|
8
|
+
const shim = require("./shim.js");
|
|
9
|
+
const download = require("./download.js");
|
|
10
|
+
|
|
11
|
+
const ROOT = path.join(__dirname, "..");
|
|
12
|
+
const BIN = path.join(ROOT, "bin");
|
|
13
|
+
|
|
14
|
+
function say(message) {
|
|
15
|
+
console.log(message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function warn(message) {
|
|
19
|
+
console.error(message);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeExecutable(dir) {
|
|
23
|
+
const changed = [];
|
|
24
|
+
for (const name of platform.BINARIES) {
|
|
25
|
+
const file = path.join(dir, name);
|
|
26
|
+
if (!fs.existsSync(file)) { continue; }
|
|
27
|
+
const mode = fs.statSync(file).mode & 0o777;
|
|
28
|
+
const wanted = mode | 0o111;
|
|
29
|
+
if (mode === wanted) { continue; }
|
|
30
|
+
fs.chmodSync(file, wanted);
|
|
31
|
+
changed.push(name);
|
|
32
|
+
}
|
|
33
|
+
return changed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function signed(file) {
|
|
37
|
+
const result = spawnSync("codesign", ["--verify", "--strict", file], { encoding: "utf8" });
|
|
38
|
+
return !result.error && result.status === 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function adHocSign(dir) {
|
|
42
|
+
const failures = [];
|
|
43
|
+
for (const name of platform.BINARIES) {
|
|
44
|
+
const file = path.join(dir, name);
|
|
45
|
+
if (!fs.existsSync(file) || signed(file)) { continue; }
|
|
46
|
+
const result = spawnSync("codesign", ["--sign", "-", "--force", file], { encoding: "utf8" });
|
|
47
|
+
if (result.error) { failures.push(name + ": could not run codesign: " + result.error.message); continue; }
|
|
48
|
+
if (result.status !== 0) { failures.push(name + ": " + ((result.stderr || "").trim() || "codesign exited " + result.status)); }
|
|
49
|
+
}
|
|
50
|
+
return failures;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeCommand(command, target) {
|
|
54
|
+
const link = path.join(BIN, command);
|
|
55
|
+
let current = "";
|
|
56
|
+
try { current = fs.readlinkSync(link); } catch (e) { current = ""; }
|
|
57
|
+
if (target === "") {
|
|
58
|
+
if (current === "" && fs.existsSync(link) && fs.readFileSync(link, "utf8") === shim.source(command)) { return "shim"; }
|
|
59
|
+
fs.rmSync(link, { force: true });
|
|
60
|
+
fs.writeFileSync(link, shim.source(command));
|
|
61
|
+
fs.chmodSync(link, 0o755);
|
|
62
|
+
return "shim";
|
|
63
|
+
}
|
|
64
|
+
const relative = path.relative(BIN, target);
|
|
65
|
+
if (current === relative) { return "link"; }
|
|
66
|
+
fs.rmSync(link, { force: true });
|
|
67
|
+
fs.symlinkSync(relative, link);
|
|
68
|
+
return "link";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function restore() {
|
|
72
|
+
for (const command of platform.COMMANDS) { writeCommand(command, ""); }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function smoke(dir) {
|
|
76
|
+
const failures = [];
|
|
77
|
+
for (const name of platform.BINARIES) {
|
|
78
|
+
const file = path.join(dir, name);
|
|
79
|
+
if (!fs.existsSync(file)) { failures.push(name + " is missing from " + dir); continue; }
|
|
80
|
+
const result = spawnSync(file, ["--version"], { encoding: "utf8" });
|
|
81
|
+
if (result.error) { failures.push(name + " will not run: " + result.error.message); continue; }
|
|
82
|
+
if (result.status === 0) { continue; }
|
|
83
|
+
const output = ((result.stdout || "") + (result.stderr || "")).trim();
|
|
84
|
+
const detail = output === "" ? "it exited " + result.status + " without printing anything" : output;
|
|
85
|
+
failures.push(name + " will not run: " + detail);
|
|
86
|
+
if (result.status === 137) {
|
|
87
|
+
failures.push(name + ": 137 is SIGKILL on exec, which on Apple Silicon means the kernel refused the code signature");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return failures;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function obtain(id) {
|
|
94
|
+
const found = resolve.binaryDir(id);
|
|
95
|
+
if (found.dir !== "") { return found; }
|
|
96
|
+
const pkg = platform.packageFor(id);
|
|
97
|
+
const version = resolve.selfVersion();
|
|
98
|
+
if (version === "") { return found; }
|
|
99
|
+
say("joule: " + pkg + " was not installed, so it is being fetched from the registry directly.");
|
|
100
|
+
const failure = await download.fetchPackage(pkg, version, path.join(resolve.vendorDir(), "bin"));
|
|
101
|
+
if (failure !== "") {
|
|
102
|
+
warn("joule: fetching " + pkg + "@" + version + " failed: " + failure);
|
|
103
|
+
return { dir: "", source: "" };
|
|
104
|
+
}
|
|
105
|
+
return resolve.binaryDir(id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function main(id) {
|
|
109
|
+
if (platform.isWindows(id)) {
|
|
110
|
+
warn(platform.windowsNotice());
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
if (platform.packageFor(id) === "") {
|
|
114
|
+
warn(platform.unsupportedNotice(id));
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
const found = await obtain(id);
|
|
118
|
+
if (found.dir === "") {
|
|
119
|
+
restore();
|
|
120
|
+
warn(platform.notice(id, resolve.selfVersion()));
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
const changed = makeExecutable(found.dir);
|
|
124
|
+
if (changed.length > 0) {
|
|
125
|
+
say("joule: restored the execute bit on " + changed.join(", ") + ", which an npm tarball does not carry reliably.");
|
|
126
|
+
}
|
|
127
|
+
if (id.startsWith("darwin-")) {
|
|
128
|
+
const unsigned = adHocSign(found.dir);
|
|
129
|
+
for (const failure of unsigned) { warn("joule: " + failure); }
|
|
130
|
+
}
|
|
131
|
+
const broken = smoke(found.dir);
|
|
132
|
+
if (broken.length > 0) {
|
|
133
|
+
restore();
|
|
134
|
+
for (const failure of broken) { warn("joule: " + failure); }
|
|
135
|
+
warn("joule: the binaries are installed at " + found.dir + " but will not run, so no command was linked.");
|
|
136
|
+
warn("joule: please report this at " + platform.REPO + "/issues with the lines above and the output of 'uname -a'.");
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
for (const command of platform.COMMANDS) {
|
|
140
|
+
writeCommand(command, path.join(found.dir, command));
|
|
141
|
+
}
|
|
142
|
+
say("joule: " + found.source + " installed to " + found.dir + ", and " + platform.COMMANDS.join(" and ") + " point at it.");
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (require.main === module) {
|
|
147
|
+
main(platform.current()).then((code) => { process.exitCode = code; }).catch((e) => {
|
|
148
|
+
warn("joule: the install step failed: " + e.message);
|
|
149
|
+
warn("joule: please report this at " + platform.REPO + "/issues with the line above.");
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { main, makeExecutable, writeCommand, restore, smoke, signed, adHocSign };
|
package/lib/resolve.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const platform = require("./platform.js");
|
|
6
|
+
|
|
7
|
+
const ROOT = path.join(__dirname, "..");
|
|
8
|
+
const VENDOR = path.join(ROOT, "vendor");
|
|
9
|
+
|
|
10
|
+
function selfVersion() {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version || "";
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function overrideDir() {
|
|
19
|
+
const raw = (process.env[platform.OVERRIDE] || "").trim();
|
|
20
|
+
if (raw === "") { return ""; }
|
|
21
|
+
if (!fs.existsSync(raw)) {
|
|
22
|
+
throw new Error("joule: " + platform.OVERRIDE + " is set to " + raw + ", which does not exist.");
|
|
23
|
+
}
|
|
24
|
+
return fs.statSync(raw).isDirectory() ? raw : path.dirname(raw);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function packageDir(pkg) {
|
|
28
|
+
try {
|
|
29
|
+
return path.dirname(require.resolve(pkg + "/package.json", { paths: [ROOT] }));
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function holdsBinaries(dir) {
|
|
36
|
+
if (dir === "") { return false; }
|
|
37
|
+
return platform.BINARIES.every((name) => fs.existsSync(path.join(dir, name)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function vendorDir() {
|
|
41
|
+
return VENDOR;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function binaryDir(id) {
|
|
45
|
+
if (platform.isWindows(id)) { return { dir: "", source: "" }; }
|
|
46
|
+
const override = overrideDir();
|
|
47
|
+
if (override !== "") { return { dir: override, source: platform.OVERRIDE }; }
|
|
48
|
+
const pkg = platform.packageFor(id);
|
|
49
|
+
if (pkg !== "") {
|
|
50
|
+
const installed = path.join(packageDir(pkg), "bin");
|
|
51
|
+
if (holdsBinaries(installed)) { return { dir: installed, source: pkg }; }
|
|
52
|
+
}
|
|
53
|
+
const vendored = path.join(VENDOR, "bin");
|
|
54
|
+
if (holdsBinaries(vendored)) { return { dir: vendored, source: "vendor" }; }
|
|
55
|
+
return { dir: "", source: "" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function binaryPath(name, id) {
|
|
59
|
+
const found = binaryDir(id);
|
|
60
|
+
if (found.dir === "") {
|
|
61
|
+
throw new Error(platform.notice(id, selfVersion()));
|
|
62
|
+
}
|
|
63
|
+
const file = path.join(found.dir, name);
|
|
64
|
+
if (!fs.existsSync(file)) {
|
|
65
|
+
throw new Error("joule: " + found.source + " has no " + name + " in " + found.dir + ".");
|
|
66
|
+
}
|
|
67
|
+
return file;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { selfVersion, overrideDir, packageDir, holdsBinaries, vendorDir, binaryDir, binaryPath };
|
package/lib/shim.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("node:child_process");
|
|
4
|
+
const platform = require("./platform.js");
|
|
5
|
+
const resolve = require("./resolve.js");
|
|
6
|
+
|
|
7
|
+
const IGNORED = ["SIGINT", "SIGQUIT"];
|
|
8
|
+
const FORWARDED = ["SIGTERM", "SIGHUP"];
|
|
9
|
+
|
|
10
|
+
function listen(signal, handler) {
|
|
11
|
+
try {
|
|
12
|
+
process.on(signal, handler);
|
|
13
|
+
return true;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function run(name) {
|
|
20
|
+
let target = "";
|
|
21
|
+
try {
|
|
22
|
+
target = resolve.binaryPath(name, platform.current());
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.error(e.message);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const child = spawn(target, process.argv.slice(2), { stdio: "inherit" });
|
|
28
|
+
const attached = [];
|
|
29
|
+
for (const signal of IGNORED) {
|
|
30
|
+
const handler = () => {};
|
|
31
|
+
if (listen(signal, handler)) { attached.push([signal, handler]); }
|
|
32
|
+
}
|
|
33
|
+
for (const signal of FORWARDED) {
|
|
34
|
+
const handler = () => { try { child.kill(signal); } catch (e) { return; } };
|
|
35
|
+
if (listen(signal, handler)) { attached.push([signal, handler]); }
|
|
36
|
+
}
|
|
37
|
+
function release() {
|
|
38
|
+
for (const pair of attached) { process.removeListener(pair[0], pair[1]); }
|
|
39
|
+
}
|
|
40
|
+
child.on("error", (e) => {
|
|
41
|
+
release();
|
|
42
|
+
console.error("joule: could not run " + target + ": " + e.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
45
|
+
child.on("exit", (code, signal) => {
|
|
46
|
+
release();
|
|
47
|
+
if (signal !== null) {
|
|
48
|
+
process.kill(process.pid, signal);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
process.exit(code === null ? 1 : code);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function source(command) {
|
|
56
|
+
return [
|
|
57
|
+
"#!/usr/bin/env node",
|
|
58
|
+
"\"use strict\";",
|
|
59
|
+
"require(\"../lib/shim.js\").run(\"" + command + "\");",
|
|
60
|
+
"",
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { run, source, IGNORED, FORWARDED };
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@joule-sh/code",
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/joule-sh/code.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/joule-sh/code#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/joule-sh/code/issues"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"description": "An agentic coding terminal you can also drive from a web page.",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"agent",
|
|
19
|
+
"cli",
|
|
20
|
+
"coding",
|
|
21
|
+
"terminal",
|
|
22
|
+
"joule"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"joule": "bin/joule",
|
|
26
|
+
"relay": "bin/relay"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin",
|
|
30
|
+
"lib",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"postinstall": "node lib/postinstall.js"
|
|
38
|
+
},
|
|
39
|
+
"optionalDependencies": {
|
|
40
|
+
"@joule-sh/code-linux-x64": "0.19.0",
|
|
41
|
+
"@joule-sh/code-darwin-x64": "0.19.0",
|
|
42
|
+
"@joule-sh/code-darwin-arm64": "0.19.0"
|
|
43
|
+
}
|
|
44
|
+
}
|