@usearete/a4 0.12.0 → 0.14.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 +59 -24
- package/bin/a4.js +322 -68
- package/package.json +3 -5
- package/bin/a4.test.js +0 -86
- package/scripts/postinstall.js +0 -214
package/README.md
CHANGED
|
@@ -1,46 +1,81 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @usearete/a4
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Arete CLI (`a4`) as an npm package. Programmable, real-time Solana data
|
|
4
|
+
feeds: deploy stacks, explore live data, wire up your agent.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## Install
|
|
6
7
|
|
|
7
8
|
```bash
|
|
8
|
-
npx @usearete/a4
|
|
9
|
-
cd my-app
|
|
10
|
-
npm run dev
|
|
9
|
+
npx @usearete/a4 install
|
|
11
10
|
```
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
This downloads the `a4` release binary for your platform (the version is
|
|
13
|
+
locked to the package version), verifies its SHA-256 and minisign signature,
|
|
14
|
+
copies it to `~/.local/bin/a4` (`%USERPROFILE%\.local\bin\a4.exe` on
|
|
15
|
+
Windows), adds that directory to your shell PATH and writes
|
|
16
|
+
`~/.arete/receipt.json`. The last two stdout lines are always:
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
```text
|
|
19
|
+
A4_BIN=/home/you/.local/bin/a4
|
|
20
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Agents: your shell snapshotted PATH when the session started, so either
|
|
24
|
+
`eval` the `export` line or call the `A4_BIN` path directly.
|
|
25
|
+
|
|
26
|
+
Equivalent:
|
|
16
27
|
|
|
17
28
|
```bash
|
|
18
|
-
npm install -g @usearete/a4
|
|
29
|
+
npm install -g @usearete/a4 && a4 install # global shim, same download path
|
|
30
|
+
pnpm dlx @usearete/a4 install
|
|
31
|
+
bunx @usearete/a4 install
|
|
19
32
|
```
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
Or without Node at all:
|
|
22
35
|
|
|
23
36
|
```bash
|
|
24
|
-
|
|
37
|
+
curl -fsSL https://arete.run/install.sh | sh # macOS, Linux
|
|
38
|
+
irm https://arete.run/install.ps1 | iex # Windows PowerShell
|
|
25
39
|
```
|
|
26
40
|
|
|
27
|
-
|
|
41
|
+
Flags and environment:
|
|
42
|
+
|
|
43
|
+
| Flag / env | Effect |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `--install-dir DIR` / `A4_INSTALL_DIR` | Install somewhere other than `~/.local/bin` |
|
|
46
|
+
| `--no-modify-path` / `A4_NO_MODIFY_PATH=1` | Do not edit shell rc files / the Windows user PATH |
|
|
47
|
+
| `--json` | Print the install receipt as JSON (the `A4_BIN=` line still follows) |
|
|
48
|
+
| `A4_MANIFEST_BASE_URL`, `A4_LATEST_URL` | Test overrides for the release download base and latest pointer |
|
|
49
|
+
|
|
50
|
+
The package has no `postinstall` script and no dependencies; nothing touches
|
|
51
|
+
the network at `npm install` time.
|
|
52
|
+
|
|
53
|
+
## Run
|
|
28
54
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
| `a4 up <path.stack-manifest.json>` | Deploy one exact StackManifest |
|
|
34
|
-
| `a4 status` | Show project overview |
|
|
35
|
-
| `a4 auth login` | Authenticate with Arete |
|
|
55
|
+
Once installed, `a4` is on PATH in new shells. `npx @usearete/a4 <args>` also
|
|
56
|
+
works in the same session: it runs the binary recorded in
|
|
57
|
+
`~/.arete/receipt.json`, installing it silently first if needed, so
|
|
58
|
+
`npx @usearete/a4 explore --json` prints only the command's JSON.
|
|
36
59
|
|
|
37
|
-
|
|
60
|
+
```bash
|
|
61
|
+
a4 init -y # arete.toml, AGENTS.md, CLAUDE.md, MCP config, skills
|
|
62
|
+
a4 doctor --json # check the setup
|
|
63
|
+
a4 explore --json # what live data is available
|
|
64
|
+
a4 self update # upgrade in place (alias: a4 upgrade)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Full reference: <https://docs.arete.run/cli/commands/>. Agent setup guide:
|
|
68
|
+
<https://docs.arete.run/agent.md>.
|
|
69
|
+
|
|
70
|
+
## Building from source
|
|
38
71
|
|
|
39
|
-
|
|
72
|
+
Only needed for unsupported platforms (anything other than
|
|
73
|
+
darwin-arm64/x64, linux-x64/arm64, win32-x64):
|
|
40
74
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
75
|
+
```bash
|
|
76
|
+
cargo install a4-cli
|
|
77
|
+
a4 self install --source manual
|
|
78
|
+
```
|
|
44
79
|
|
|
45
80
|
## License
|
|
46
81
|
|
package/bin/a4.js
CHANGED
|
@@ -1,106 +1,360 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// @usearete/a4 launcher. Scriptless bootstrapper for the Arete CLI (`a4`):
|
|
5
|
+
//
|
|
6
|
+
// npx @usearete/a4 install [--install-dir DIR] [--no-modify-path] [--json]
|
|
7
|
+
// Download the release asset for this package's version, verify sha256 +
|
|
8
|
+
// minisign signature, then run `<tmp>/a4 self install --source npm ...`.
|
|
9
|
+
// npx @usearete/a4 <anything else>
|
|
10
|
+
// Run the binary recorded in ~/.arete/receipt.json (installing silently
|
|
11
|
+
// first when there is no receipt) with the given argv.
|
|
12
|
+
//
|
|
13
|
+
// No lifecycle scripts, no dependencies, no network access at `npm install`.
|
|
14
|
+
// Design: docs/internal/agent-first-onboarding.md (WP3).
|
|
6
15
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
16
|
+
const crypto = require("node:crypto");
|
|
17
|
+
const fs = require("node:fs");
|
|
18
|
+
const http = require("node:http");
|
|
19
|
+
const https = require("node:https");
|
|
20
|
+
const os = require("node:os");
|
|
21
|
+
const path = require("node:path");
|
|
22
|
+
const { spawnSync } = require("node:child_process");
|
|
23
|
+
|
|
24
|
+
const pkg = require("../package.json");
|
|
9
25
|
|
|
10
26
|
const RECURSION_SENTINEL = "ARETE_A4_LAUNCHER_ACTIVE";
|
|
27
|
+
const MINISIGN_PUBLIC_KEY = "RWRsiwmDW0371BZbcE1IWD6Y8/KIoAArUAp7mpyG6VweJ5rE3Lf3g5qA";
|
|
28
|
+
const RELEASE_BASE = "https://github.com/AreteA4/arete/releases/download";
|
|
29
|
+
const MAX_REDIRECTS = 5;
|
|
30
|
+
const ASSETS = {
|
|
31
|
+
"darwin-arm64": "a4-darwin-arm64",
|
|
32
|
+
"darwin-x64": "a4-darwin-x64",
|
|
33
|
+
"linux-x64": "a4-linux-x64",
|
|
34
|
+
"linux-arm64": "a4-linux-arm64",
|
|
35
|
+
"win32-x64": "a4-win32-x64.exe",
|
|
36
|
+
};
|
|
37
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
11
38
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
39
|
+
class InstallError extends Error {}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Platform, URLs, receipt
|
|
43
|
+
|
|
44
|
+
function pickAsset(platform = process.platform, arch = process.arch) {
|
|
45
|
+
return ASSETS[`${platform}-${arch}`] || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Same semantics as cli/src/selfhost/platform.rs::release_base_url.
|
|
49
|
+
function releaseBaseUrl(version, env = process.env) {
|
|
50
|
+
const override = (env.A4_MANIFEST_BASE_URL || "").trim();
|
|
51
|
+
if (override) {
|
|
52
|
+
const base = override.replace(/\/+$/, "");
|
|
53
|
+
return base.includes("{version}")
|
|
54
|
+
? base.split("{version}").join(version)
|
|
55
|
+
: `${base}/a4-cli-v${version}`;
|
|
17
56
|
}
|
|
57
|
+
return `${RELEASE_BASE}/a4-cli-v${version}`;
|
|
18
58
|
}
|
|
19
59
|
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
return (env.PATH || "")
|
|
23
|
-
.split(path.delimiter)
|
|
24
|
-
.filter(Boolean)
|
|
25
|
-
.flatMap((directory) => names.map((name) => path.join(directory, name)));
|
|
60
|
+
function areteHome(env = process.env, homedir = os.homedir()) {
|
|
61
|
+
return env.ARETE_HOME ? env.ARETE_HOME : path.join(homedir, ".arete");
|
|
26
62
|
}
|
|
27
63
|
|
|
28
|
-
function
|
|
64
|
+
function receiptPath(env = process.env, homedir = os.homedir()) {
|
|
65
|
+
return path.join(areteHome(env, homedir), "receipt.json");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Parsed receipt when it exists, is well-formed and its binary is present; else null.
|
|
69
|
+
function readReceipt(env = process.env, homedir = os.homedir()) {
|
|
70
|
+
let receipt;
|
|
29
71
|
try {
|
|
30
|
-
|
|
31
|
-
if (platform !== "win32") {
|
|
32
|
-
fs.accessSync(candidate, fs.constants.X_OK);
|
|
33
|
-
}
|
|
34
|
-
return true;
|
|
72
|
+
receipt = JSON.parse(fs.readFileSync(receiptPath(env, homedir), "utf8"));
|
|
35
73
|
} catch {
|
|
36
|
-
return
|
|
74
|
+
return null;
|
|
37
75
|
}
|
|
76
|
+
if (!receipt || typeof receipt !== "object" || typeof receipt.binary !== "string") return null;
|
|
77
|
+
try {
|
|
78
|
+
if (!fs.statSync(receipt.binary).isFile()) return null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return receipt;
|
|
38
83
|
}
|
|
39
84
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
function getBinaryPath(env = process.env, platform = process.platform) {
|
|
43
|
-
const launcherPaths = new Set(
|
|
44
|
-
[__filename, process.argv[1]].filter(Boolean).map(canonicalPath)
|
|
45
|
-
);
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Checksums
|
|
46
87
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
88
|
+
// `<sha256> <asset>` per line (optional `*` binary marker) -> Map(asset -> sha256 lowercase).
|
|
89
|
+
function parseChecksums(text) {
|
|
90
|
+
const map = new Map();
|
|
91
|
+
for (const line of String(text).split(/\r?\n/)) {
|
|
92
|
+
const match = line.match(/^([a-fA-F0-9]{64})\s+\*?(\S.*?)\s*$/);
|
|
93
|
+
if (match) map.set(match[2], match[1].toLowerCase());
|
|
53
94
|
}
|
|
95
|
+
return map;
|
|
96
|
+
}
|
|
54
97
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
98
|
+
function sha256File(filePath) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const hash = crypto.createHash("sha256");
|
|
101
|
+
fs.createReadStream(filePath)
|
|
102
|
+
.on("error", reject)
|
|
103
|
+
.on("data", (chunk) => hash.update(chunk))
|
|
104
|
+
.on("end", () => resolve(hash.digest("hex")));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
61
107
|
|
|
62
|
-
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// minisign (https://jedisct1.github.io/minisign/) verification in pure Node.
|
|
110
|
+
//
|
|
111
|
+
// Public key: base64 of alg("Ed") || key_id(8) || ed25519_pk(32)
|
|
112
|
+
// Signature file:
|
|
113
|
+
// untrusted comment: ...
|
|
114
|
+
// base64( alg("ED") || key_id(8) || signature(64) )
|
|
115
|
+
// trusted comment: <text>
|
|
116
|
+
// base64( global_signature(64) ) over signature || trusted comment
|
|
117
|
+
// alg "ED" signs blake2b-512(message); legacy "Ed" (signs raw message) is rejected.
|
|
118
|
+
|
|
119
|
+
function parseMinisignPublicKey(text) {
|
|
120
|
+
const line = String(text)
|
|
121
|
+
.split(/\r?\n/)
|
|
122
|
+
.map((l) => l.trim())
|
|
123
|
+
.find((l) => l && !l.startsWith("untrusted comment:"));
|
|
124
|
+
const raw = line ? Buffer.from(line, "base64") : Buffer.alloc(0);
|
|
125
|
+
if (raw.length !== 42) throw new InstallError("minisign public key: expected 42 bytes");
|
|
126
|
+
const algorithm = raw.subarray(0, 2).toString("latin1");
|
|
127
|
+
if (algorithm !== "Ed") throw new InstallError(`minisign public key: unsupported algorithm ${JSON.stringify(algorithm)}`);
|
|
128
|
+
return { algorithm, keyId: raw.subarray(2, 10), key: raw.subarray(10, 42) };
|
|
63
129
|
}
|
|
64
130
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
131
|
+
function parseMinisignSignature(text) {
|
|
132
|
+
const lines = String(text).split(/\r?\n/).map((l) => l.trim());
|
|
133
|
+
const [untrusted, sigLine, trustedLine, globalLine] = lines;
|
|
134
|
+
if (!untrusted || !untrusted.startsWith("untrusted comment:") || !sigLine || !trustedLine || !globalLine) {
|
|
135
|
+
throw new InstallError("minisign signature: malformed file (expected 4 lines)");
|
|
136
|
+
}
|
|
137
|
+
if (!trustedLine.startsWith("trusted comment:")) {
|
|
138
|
+
throw new InstallError("minisign signature: missing trusted comment line");
|
|
69
139
|
}
|
|
140
|
+
const raw = Buffer.from(sigLine, "base64");
|
|
141
|
+
if (raw.length !== 74) throw new InstallError("minisign signature: expected 74 bytes");
|
|
142
|
+
const algorithm = raw.subarray(0, 2).toString("latin1");
|
|
143
|
+
if (algorithm === "Ed") throw new InstallError("minisign signature: legacy (non-prehashed) signature rejected");
|
|
144
|
+
if (algorithm !== "ED") throw new InstallError(`minisign signature: unsupported algorithm ${JSON.stringify(algorithm)}`);
|
|
145
|
+
const globalSignature = Buffer.from(globalLine, "base64");
|
|
146
|
+
if (globalSignature.length !== 64) throw new InstallError("minisign signature: global signature must be 64 bytes");
|
|
147
|
+
return {
|
|
148
|
+
algorithm,
|
|
149
|
+
keyId: raw.subarray(2, 10),
|
|
150
|
+
signature: raw.subarray(10, 74),
|
|
151
|
+
trustedComment: trustedLine.slice("trusted comment:".length).replace(/^ /, ""),
|
|
152
|
+
globalSignature,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
70
155
|
|
|
71
|
-
|
|
156
|
+
function ed25519KeyObject(rawKey) {
|
|
157
|
+
return crypto.createPublicKey({
|
|
158
|
+
key: Buffer.concat([ED25519_SPKI_PREFIX, rawKey]),
|
|
159
|
+
format: "der",
|
|
160
|
+
type: "spki",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
72
163
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"
|
|
164
|
+
// Throws InstallError on any failure; returns { trustedComment } on success.
|
|
165
|
+
function verifyMinisign({ publicKey, signature, message }) {
|
|
166
|
+
const pk = typeof publicKey === "string" ? parseMinisignPublicKey(publicKey) : publicKey;
|
|
167
|
+
const sig = typeof signature === "string" ? parseMinisignSignature(signature) : signature;
|
|
168
|
+
if (!pk.keyId.equals(sig.keyId)) {
|
|
169
|
+
throw new InstallError(
|
|
170
|
+
`minisign signature: key id ${sig.keyId.toString("hex")} does not match the embedded key ${pk.keyId.toString("hex")}`
|
|
80
171
|
);
|
|
81
|
-
process.exit(1);
|
|
82
172
|
}
|
|
173
|
+
const key = ed25519KeyObject(pk.key);
|
|
174
|
+
const prehash = crypto.createHash("blake2b512").update(message).digest();
|
|
175
|
+
if (!crypto.verify(null, prehash, key, sig.signature)) {
|
|
176
|
+
throw new InstallError("minisign signature: signature does not match checksums.txt");
|
|
177
|
+
}
|
|
178
|
+
const globalMessage = Buffer.concat([sig.signature, Buffer.from(sig.trustedComment, "utf8")]);
|
|
179
|
+
if (!crypto.verify(null, globalMessage, key, sig.globalSignature)) {
|
|
180
|
+
throw new InstallError("minisign signature: global signature (trusted comment) invalid");
|
|
181
|
+
}
|
|
182
|
+
return { trustedComment: sig.trustedComment };
|
|
183
|
+
}
|
|
83
184
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// HTTP (follows up to MAX_REDIRECTS redirects; http: allowed for test servers)
|
|
187
|
+
|
|
188
|
+
function getResponse(url, depth = 0) {
|
|
189
|
+
if (depth > MAX_REDIRECTS) return Promise.reject(new InstallError(`too many redirects fetching ${url}`));
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
const client = url.startsWith("http:") ? http : https;
|
|
192
|
+
client
|
|
193
|
+
.get(url, { headers: { "user-agent": `@usearete/a4 ${pkg.version}` } }, (response) => {
|
|
194
|
+
const { statusCode, headers } = response;
|
|
195
|
+
if ([301, 302, 303, 307, 308].includes(statusCode)) {
|
|
196
|
+
response.resume();
|
|
197
|
+
if (!headers.location) return reject(new InstallError(`redirect from ${url} has no location header`));
|
|
198
|
+
return resolve(getResponse(new URL(headers.location, url).toString(), depth + 1));
|
|
199
|
+
}
|
|
200
|
+
resolve(response);
|
|
201
|
+
})
|
|
202
|
+
.on("error", reject);
|
|
87
203
|
});
|
|
204
|
+
}
|
|
88
205
|
|
|
206
|
+
async function fetchToFile(url, dest, mode) {
|
|
207
|
+
const response = await getResponse(url);
|
|
208
|
+
if (response.statusCode !== 200) {
|
|
209
|
+
response.resume();
|
|
210
|
+
return response.statusCode;
|
|
211
|
+
}
|
|
212
|
+
await new Promise((resolve, reject) => {
|
|
213
|
+
const file = fs.createWriteStream(dest, { mode });
|
|
214
|
+
response.on("error", (err) => { file.destroy(); reject(err); });
|
|
215
|
+
file.on("error", reject);
|
|
216
|
+
file.on("finish", () => file.close(resolve));
|
|
217
|
+
response.pipe(file);
|
|
218
|
+
});
|
|
219
|
+
return 200;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Install
|
|
224
|
+
|
|
225
|
+
function stillPublishing(version, url) {
|
|
226
|
+
return new InstallError(`Release ${version} is still publishing; retry in a few minutes (missing ${url})`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Download + verify into a fresh temp dir. Returns { dir, binary, checksums, signature }.
|
|
230
|
+
async function downloadRelease({ version = pkg.version, env = process.env, platform = process.platform, arch = process.arch, publicKey = MINISIGN_PUBLIC_KEY, log = () => {} } = {}) {
|
|
231
|
+
const asset = pickAsset(platform, arch);
|
|
232
|
+
if (!asset) {
|
|
233
|
+
throw new InstallError(
|
|
234
|
+
`no prebuilt a4 binary for ${platform}-${arch}. Build from source: https://github.com/AreteA4/arete#building-from-source`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const base = releaseBaseUrl(version, env);
|
|
238
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "a4-install-"));
|
|
239
|
+
const binary = path.join(dir, platform === "win32" ? "a4.exe" : "a4");
|
|
240
|
+
const checksums = path.join(dir, "checksums.txt");
|
|
241
|
+
const signature = path.join(dir, "checksums.txt.minisig");
|
|
242
|
+
log(`Installing a4 ${version} (${platform}-${arch}) from ${base}`);
|
|
243
|
+
|
|
244
|
+
for (const [name, dest, mode] of [[asset, binary, 0o755], ["checksums.txt", checksums, 0o644], ["checksums.txt.minisig", signature, 0o644]]) {
|
|
245
|
+
const url = `${base}/${name}`;
|
|
246
|
+
let status;
|
|
247
|
+
try {
|
|
248
|
+
status = await fetchToFile(url, dest, mode);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
throw new InstallError(`download of ${url} failed: ${err.message}. Check network/proxy settings and retry`);
|
|
251
|
+
}
|
|
252
|
+
if (status === 404) throw stillPublishing(version, url);
|
|
253
|
+
if (status !== 200) throw new InstallError(`download of ${url} failed (HTTP ${status}). Retry in a few minutes`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const checksumText = fs.readFileSync(checksums, "utf8");
|
|
257
|
+
verifyMinisign({ publicKey, signature: fs.readFileSync(signature, "utf8"), message: Buffer.from(checksumText, "utf8") });
|
|
258
|
+
const expected = parseChecksums(checksumText).get(asset);
|
|
259
|
+
if (!expected) throw new InstallError(`checksums.txt has no entry for ${asset}; the release may be incomplete, retry in a few minutes`);
|
|
260
|
+
const actual = await sha256File(binary);
|
|
261
|
+
if (actual !== expected) {
|
|
262
|
+
throw new InstallError(
|
|
263
|
+
`sha256 mismatch for ${asset} (expected ${expected}, got ${actual}). Retry; if it persists, report at https://github.com/AreteA4/arete/issues`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (platform !== "win32") fs.chmodSync(binary, 0o755);
|
|
267
|
+
return { dir, binary, checksums, signature };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Run the full install path. `quiet` captures stdout and returns the A4_BIN path.
|
|
271
|
+
// Returns { status, a4Bin }.
|
|
272
|
+
async function runInstall(passthrough = [], options = {}) {
|
|
273
|
+
const { quiet = false, env = process.env, stdio = "inherit", log = (m) => process.stderr.write(`${m}\n`) } = options;
|
|
274
|
+
const release = await downloadRelease({ ...options, log });
|
|
275
|
+
try {
|
|
276
|
+
const args = ["self", "install", "--source", "npm", "--checksums", release.checksums, "--signature", release.signature, ...passthrough];
|
|
277
|
+
const result = spawnSync(release.binary, args, {
|
|
278
|
+
stdio: quiet ? ["ignore", "pipe", "inherit"] : stdio,
|
|
279
|
+
encoding: "utf8",
|
|
280
|
+
env: { ...env, [RECURSION_SENTINEL]: "1" },
|
|
281
|
+
});
|
|
282
|
+
if (result.error) throw new InstallError(`failed to run ${release.binary}: ${result.error.message}`);
|
|
283
|
+
let a4Bin = null;
|
|
284
|
+
if (quiet && result.stdout) {
|
|
285
|
+
const line = result.stdout.split(/\r?\n/).reverse().find((l) => l.startsWith("A4_BIN="));
|
|
286
|
+
if (line) a4Bin = line.slice("A4_BIN=".length).trim();
|
|
287
|
+
}
|
|
288
|
+
return { status: result.status ?? 1, a4Bin };
|
|
289
|
+
} finally {
|
|
290
|
+
fs.rmSync(release.dir, { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Launcher
|
|
296
|
+
|
|
297
|
+
function runBinary(binary, argv, { env = process.env, stdio = "inherit" } = {}) {
|
|
298
|
+
const result = spawnSync(binary, argv, { stdio, env: { ...env, [RECURSION_SENTINEL]: "1" }, encoding: "utf8" });
|
|
89
299
|
if (result.error) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
300
|
+
const hint = result.error.code === "EACCES" ? ` Try: chmod +x "${binary}"` : "";
|
|
301
|
+
throw new InstallError(`failed to run ${binary}: ${result.error.message}.${hint} Reinstall with: npx @usearete/a4 install`);
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Returns the process exit code. Options are for tests (env, homedir, publicKey, stdio).
|
|
307
|
+
async function launch(argv, options = {}) {
|
|
308
|
+
const { env = process.env, homedir = os.homedir(), stdio = "inherit", log = (m) => process.stderr.write(`${m}\n`) } = options;
|
|
309
|
+
if (env[RECURSION_SENTINEL] === "1") {
|
|
310
|
+
log("Refusing to recursively launch the Arete CLI shim.");
|
|
311
|
+
return 1;
|
|
312
|
+
}
|
|
313
|
+
if (argv[0] === "install") {
|
|
314
|
+
return (await runInstall(argv.slice(1), { ...options, env, log })).status;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let receipt = readReceipt(env, homedir);
|
|
318
|
+
if (!receipt) {
|
|
319
|
+
log(`a4 is not installed yet (no ${receiptPath(env, homedir)}); installing a4 ${pkg.version} first`);
|
|
320
|
+
const { status, a4Bin } = await runInstall([], { ...options, env, log, quiet: true });
|
|
321
|
+
if (status !== 0 || !a4Bin) {
|
|
322
|
+
throw new InstallError(`a4 self install failed (exit ${status}). Run: npx @usearete/a4 install`);
|
|
97
323
|
}
|
|
98
|
-
|
|
324
|
+
receipt = { binary: a4Bin };
|
|
99
325
|
}
|
|
326
|
+
const result = runBinary(receipt.binary, argv, { env, stdio });
|
|
327
|
+
if (options.onResult) options.onResult(result);
|
|
328
|
+
return result.status ?? 1;
|
|
329
|
+
}
|
|
100
330
|
|
|
101
|
-
|
|
331
|
+
async function main() {
|
|
332
|
+
try {
|
|
333
|
+
process.exitCode = await launch(process.argv.slice(2));
|
|
334
|
+
} catch (err) {
|
|
335
|
+
process.stderr.write(`error: ${err instanceof InstallError ? err.message : err.stack || err}\n`);
|
|
336
|
+
process.exitCode = 1;
|
|
337
|
+
}
|
|
102
338
|
}
|
|
103
339
|
|
|
104
340
|
if (require.main === module) main();
|
|
105
341
|
|
|
106
|
-
module.exports = {
|
|
342
|
+
module.exports = {
|
|
343
|
+
ASSETS,
|
|
344
|
+
InstallError,
|
|
345
|
+
MINISIGN_PUBLIC_KEY,
|
|
346
|
+
RECURSION_SENTINEL,
|
|
347
|
+
areteHome,
|
|
348
|
+
downloadRelease,
|
|
349
|
+
launch,
|
|
350
|
+
parseChecksums,
|
|
351
|
+
parseMinisignPublicKey,
|
|
352
|
+
parseMinisignSignature,
|
|
353
|
+
pickAsset,
|
|
354
|
+
readReceipt,
|
|
355
|
+
receiptPath,
|
|
356
|
+
releaseBaseUrl,
|
|
357
|
+
runInstall,
|
|
358
|
+
sha256File,
|
|
359
|
+
verifyMinisign,
|
|
360
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usearete/a4",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Arete CLI (a4) - Real-time Solana streaming pipelines",
|
|
5
5
|
"bin": {
|
|
6
6
|
"a4": "bin/a4.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"
|
|
10
|
-
"test": "node --test bin/*.test.js"
|
|
9
|
+
"test": "node --test test/a4.test.js"
|
|
11
10
|
},
|
|
12
11
|
"repository": {
|
|
13
12
|
"type": "git",
|
|
@@ -33,11 +32,10 @@
|
|
|
33
32
|
"access": "public"
|
|
34
33
|
},
|
|
35
34
|
"engines": {
|
|
36
|
-
"node": ">=
|
|
35
|
+
"node": ">=18"
|
|
37
36
|
},
|
|
38
37
|
"files": [
|
|
39
38
|
"bin",
|
|
40
|
-
"scripts",
|
|
41
39
|
"README.md"
|
|
42
40
|
]
|
|
43
41
|
}
|
package/bin/a4.test.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const { spawnSync } = require("node:child_process");
|
|
3
|
-
const fs = require("node:fs");
|
|
4
|
-
const os = require("node:os");
|
|
5
|
-
const path = require("node:path");
|
|
6
|
-
const test = require("node:test");
|
|
7
|
-
|
|
8
|
-
const { canonicalPath, getBinaryPath } = require("./a4.js");
|
|
9
|
-
|
|
10
|
-
test("skips a symlink to the launcher and selects the next native binary", (t) => {
|
|
11
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "arete-a4-launcher-"));
|
|
12
|
-
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
13
|
-
const shimDir = path.join(root, "shim");
|
|
14
|
-
const nativeDir = path.join(root, "native");
|
|
15
|
-
fs.mkdirSync(shimDir);
|
|
16
|
-
fs.mkdirSync(nativeDir);
|
|
17
|
-
fs.symlinkSync(__dirname + "/a4.js", path.join(shimDir, "a4"));
|
|
18
|
-
const native = path.join(nativeDir, "a4");
|
|
19
|
-
fs.writeFileSync(native, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
20
|
-
|
|
21
|
-
const selected = getBinaryPath({ PATH: [shimDir, nativeDir].join(path.delimiter) }, "darwin");
|
|
22
|
-
assert.equal(canonicalPath(selected), canonicalPath(native));
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("returns null when PATH only resolves to the launcher", (t) => {
|
|
26
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "arete-a4-launcher-"));
|
|
27
|
-
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
28
|
-
fs.symlinkSync(__dirname + "/a4.js", path.join(root, "a4"));
|
|
29
|
-
assert.equal(getBinaryPath({ PATH: root }, "darwin"), null);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test(
|
|
33
|
-
"skips non-executable files and directories before a native binary",
|
|
34
|
-
{ skip: process.platform === "win32" },
|
|
35
|
-
(t) => {
|
|
36
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "arete-a4-launcher-"));
|
|
37
|
-
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
38
|
-
const fileDir = path.join(root, "file");
|
|
39
|
-
const directoryDir = path.join(root, "directory");
|
|
40
|
-
const nativeDir = path.join(root, "native");
|
|
41
|
-
fs.mkdirSync(fileDir);
|
|
42
|
-
fs.mkdirSync(directoryDir);
|
|
43
|
-
fs.mkdirSync(nativeDir);
|
|
44
|
-
fs.writeFileSync(path.join(fileDir, "a4"), "not executable\n", { mode: 0o644 });
|
|
45
|
-
fs.mkdirSync(path.join(directoryDir, "a4"));
|
|
46
|
-
const native = path.join(nativeDir, "a4");
|
|
47
|
-
fs.writeFileSync(native, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
48
|
-
|
|
49
|
-
const selected = getBinaryPath({
|
|
50
|
-
PATH: [fileDir, directoryDir, nativeDir].join(path.delimiter),
|
|
51
|
-
});
|
|
52
|
-
assert.equal(canonicalPath(selected), canonicalPath(native));
|
|
53
|
-
}
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
test("skips Windows command shims and directories before a native binary", (t) => {
|
|
57
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "arete-a4-launcher-"));
|
|
58
|
-
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
59
|
-
const shimDir = path.join(root, "shim");
|
|
60
|
-
const directoryDir = path.join(root, "directory");
|
|
61
|
-
const nativeDir = path.join(root, "native");
|
|
62
|
-
fs.mkdirSync(shimDir);
|
|
63
|
-
fs.mkdirSync(directoryDir);
|
|
64
|
-
fs.mkdirSync(nativeDir);
|
|
65
|
-
fs.writeFileSync(path.join(shimDir, "a4"), "#!/bin/sh\n");
|
|
66
|
-
fs.writeFileSync(path.join(shimDir, "a4.cmd"), "@echo off\n");
|
|
67
|
-
fs.writeFileSync(path.join(shimDir, "a4.bat"), "@echo off\n");
|
|
68
|
-
fs.mkdirSync(path.join(directoryDir, "a4.exe"));
|
|
69
|
-
const native = path.join(nativeDir, "a4.exe");
|
|
70
|
-
fs.writeFileSync(native, "native binary placeholder\n");
|
|
71
|
-
|
|
72
|
-
const selected = getBinaryPath({
|
|
73
|
-
PATH: [shimDir, directoryDir, nativeDir].join(path.delimiter),
|
|
74
|
-
}, "win32");
|
|
75
|
-
assert.equal(canonicalPath(selected), canonicalPath(native));
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("recursion sentinel stops the launcher before resolving another a4", () => {
|
|
79
|
-
const result = spawnSync(process.execPath, [path.join(__dirname, "a4.js")], {
|
|
80
|
-
encoding: "utf8",
|
|
81
|
-
env: { ...process.env, ARETE_A4_LAUNCHER_ACTIVE: "1" },
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
assert.equal(result.status, 1);
|
|
85
|
-
assert.match(result.stderr, /Refusing to recursively launch/);
|
|
86
|
-
});
|
package/scripts/postinstall.js
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const https = require("https");
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
const path = require("path");
|
|
6
|
-
const crypto = require("crypto");
|
|
7
|
-
|
|
8
|
-
const pkg = require("../package.json");
|
|
9
|
-
const version = pkg.version;
|
|
10
|
-
|
|
11
|
-
const PLATFORMS = {
|
|
12
|
-
"darwin-arm64": "a4-darwin-arm64",
|
|
13
|
-
"darwin-x64": "a4-darwin-x64",
|
|
14
|
-
"linux-x64": "a4-linux-x64",
|
|
15
|
-
"linux-arm64": "a4-linux-arm64",
|
|
16
|
-
"win32-x64": "a4-win32-x64.exe",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const platform = process.platform;
|
|
20
|
-
const arch = process.arch;
|
|
21
|
-
const key = `${platform}-${arch}`;
|
|
22
|
-
|
|
23
|
-
const binaryName = PLATFORMS[key];
|
|
24
|
-
if (!binaryName) {
|
|
25
|
-
console.warn(
|
|
26
|
-
`Arete CLI does not have a prebuilt binary for ${key}.\n` +
|
|
27
|
-
"You can build from source: cargo install a4-cli"
|
|
28
|
-
);
|
|
29
|
-
process.exit(0);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const binDir = path.join(__dirname, "..", "bin");
|
|
33
|
-
const binPath = path.join(binDir, platform === "win32" ? "a4.exe" : "a4");
|
|
34
|
-
|
|
35
|
-
if (fs.existsSync(binPath)) {
|
|
36
|
-
process.exit(0);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const releaseUrl = `https://github.com/AreteA4/arete/releases/download/a4-cli-v${version}`;
|
|
40
|
-
const url = `${releaseUrl}/${binaryName}`;
|
|
41
|
-
const checksumUrl = `${releaseUrl}/checksums.txt`;
|
|
42
|
-
const MAX_REDIRECTS = 5;
|
|
43
|
-
|
|
44
|
-
console.log(`Downloading Arete CLI v${version} for ${key}...`);
|
|
45
|
-
|
|
46
|
-
function removeIfExists(filePath) {
|
|
47
|
-
try {
|
|
48
|
-
fs.unlinkSync(filePath);
|
|
49
|
-
} catch (err) {
|
|
50
|
-
if (err.code !== "ENOENT") {
|
|
51
|
-
throw err;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function getResponse(url, depth = 0) {
|
|
57
|
-
if (depth > MAX_REDIRECTS) {
|
|
58
|
-
return Promise.reject(new Error("Too many redirects"));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return new Promise((resolve, reject) => {
|
|
62
|
-
https
|
|
63
|
-
.get(url, (response) => {
|
|
64
|
-
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
65
|
-
if (!response.headers.location) {
|
|
66
|
-
response.resume();
|
|
67
|
-
reject(new Error("Redirect missing location header"));
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
response.resume();
|
|
72
|
-
resolve(
|
|
73
|
-
getResponse(
|
|
74
|
-
new URL(response.headers.location, url).toString(),
|
|
75
|
-
depth + 1
|
|
76
|
-
)
|
|
77
|
-
);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
resolve(response);
|
|
82
|
-
})
|
|
83
|
-
.on("error", reject);
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function download(url, dest) {
|
|
88
|
-
const response = await getResponse(url);
|
|
89
|
-
|
|
90
|
-
if (response.statusCode !== 200) {
|
|
91
|
-
response.resume();
|
|
92
|
-
removeIfExists(dest);
|
|
93
|
-
throw new Error(`Download failed: HTTP ${response.statusCode}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const total = parseInt(response.headers["content-length"], 10);
|
|
97
|
-
let downloaded = 0;
|
|
98
|
-
|
|
99
|
-
await new Promise((resolve, reject) => {
|
|
100
|
-
const file = fs.createWriteStream(dest, { mode: 0o755 });
|
|
101
|
-
|
|
102
|
-
response.on("data", (chunk) => {
|
|
103
|
-
downloaded += chunk.length;
|
|
104
|
-
if (total && process.stdout.isTTY) {
|
|
105
|
-
const pct = Math.round((downloaded / total) * 100);
|
|
106
|
-
process.stdout.write(`\rDownloading... ${pct}%`);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
response.on("error", (err) => {
|
|
111
|
-
file.destroy();
|
|
112
|
-
removeIfExists(dest);
|
|
113
|
-
reject(err);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
file.on("error", (err) => {
|
|
117
|
-
response.destroy();
|
|
118
|
-
removeIfExists(dest);
|
|
119
|
-
reject(err);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
file.on("finish", () => {
|
|
123
|
-
file.close(() => {
|
|
124
|
-
if (process.stdout.isTTY) {
|
|
125
|
-
process.stdout.write("\n");
|
|
126
|
-
}
|
|
127
|
-
resolve();
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
response.pipe(file);
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async function fetchText(url) {
|
|
136
|
-
const response = await getResponse(url);
|
|
137
|
-
|
|
138
|
-
if (response.statusCode !== 200) {
|
|
139
|
-
response.resume();
|
|
140
|
-
throw new Error(`Checksum download failed: HTTP ${response.statusCode}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return new Promise((resolve, reject) => {
|
|
144
|
-
let body = "";
|
|
145
|
-
response.setEncoding("utf8");
|
|
146
|
-
response.on("data", (chunk) => {
|
|
147
|
-
body += chunk;
|
|
148
|
-
});
|
|
149
|
-
response.on("end", () => resolve(body));
|
|
150
|
-
response.on("error", reject);
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function sha256File(filePath) {
|
|
155
|
-
return new Promise((resolve, reject) => {
|
|
156
|
-
const hash = crypto.createHash("sha256");
|
|
157
|
-
const stream = fs.createReadStream(filePath);
|
|
158
|
-
|
|
159
|
-
stream.on("error", reject);
|
|
160
|
-
stream.on("data", (chunk) => hash.update(chunk));
|
|
161
|
-
stream.on("end", () => resolve(hash.digest("hex")));
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function verifyChecksum(checksumUrl, fileName, filePath) {
|
|
166
|
-
const checksums = await fetchText(checksumUrl);
|
|
167
|
-
const expected = checksums
|
|
168
|
-
.split(/\r?\n/)
|
|
169
|
-
.map((line) => line.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/))
|
|
170
|
-
.find((match) => match && match[2] === fileName);
|
|
171
|
-
|
|
172
|
-
if (!expected) {
|
|
173
|
-
throw new Error(`Missing checksum for ${fileName}`);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const actual = await sha256File(filePath);
|
|
177
|
-
if (actual !== expected[1].toLowerCase()) {
|
|
178
|
-
throw new Error(`Checksum mismatch for ${fileName}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function main() {
|
|
183
|
-
try {
|
|
184
|
-
if (!fs.existsSync(binDir)) {
|
|
185
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
await download(url, binPath);
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
await verifyChecksum(checksumUrl, binaryName, binPath);
|
|
192
|
-
} catch (err) {
|
|
193
|
-
removeIfExists(binPath);
|
|
194
|
-
throw err;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (platform !== "win32") {
|
|
198
|
-
fs.chmodSync(binPath, 0o755);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
console.log("Arete CLI installed successfully.");
|
|
202
|
-
} catch (err) {
|
|
203
|
-
console.error(`\nFailed to download Arete CLI: ${err.message}`);
|
|
204
|
-
console.error(
|
|
205
|
-
"\nYou can install manually via Cargo:\n" +
|
|
206
|
-
" cargo install a4-cli\n" +
|
|
207
|
-
"\nOr download directly from:\n" +
|
|
208
|
-
` ${url}`
|
|
209
|
-
);
|
|
210
|
-
process.exit(0);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
main();
|