@usearete/a4 0.12.0 → 0.13.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 CHANGED
@@ -1,46 +1,81 @@
1
- # Arete CLI
1
+ # @usearete/a4
2
2
 
3
- Programmable data feeds for Solana. Arete CLI lets you deploy feeds for any Solana data to our cloud.
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
- ## Quick Start
6
+ ## Install
6
7
 
7
8
  ```bash
8
- npx @usearete/a4 create my-app
9
- cd my-app
10
- npm run dev
9
+ npx @usearete/a4 install
11
10
  ```
12
11
 
13
- ## Installation
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
- ### npm (recommended for JS/TS developers)
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
- ### Cargo (for Rust developers)
34
+ Or without Node at all:
22
35
 
23
36
  ```bash
24
- cargo install a4-cli
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
- ## Commands
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
- | Command | Description |
30
- |---------|-------------|
31
- | `a4 create [name]` | Scaffold a new app from a template |
32
- | `a4 init` | Initialize a stack project |
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
- The npm package `@usearete/a4` and the Cargo crate `a4-cli` both install the same `a4` command.
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
- ## Documentation
72
+ Only needed for unsupported platforms (anything other than
73
+ darwin-arm64/x64, linux-x64/arm64, win32-x64):
40
74
 
41
- - [Getting Started](https://docs.arete.run)
42
- - [CLI Reference](https://docs.arete.run/cli/commands/)
43
- - [Stack Development](https://docs.arete.run/building-stacks/workflow/)
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
- const { spawnSync } = require("child_process");
4
- const path = require("path");
5
- const fs = require("fs");
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 binName = process.platform === "win32" ? "a4.exe" : "a4";
8
- const localBinPath = path.join(__dirname, binName);
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
- function canonicalPath(candidate) {
13
- try {
14
- return fs.realpathSync(candidate);
15
- } catch {
16
- return path.resolve(candidate);
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 pathCandidates(env = process.env, platform = process.platform) {
21
- const names = platform === "win32" ? ["a4.exe"] : ["a4"];
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 isExecutableFile(candidate, platform = process.platform) {
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
- if (!fs.statSync(candidate).isFile()) return false;
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 false;
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
- // Try the concrete postinstall destination first, then every PATH candidate
41
- // except this JavaScript launcher (including symlinks in node_modules/.bin).
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
- // 1. Check for bundled binary (npm postinstall)
48
- if (
49
- isExecutableFile(localBinPath, platform)
50
- && !launcherPaths.has(canonicalPath(localBinPath))
51
- ) {
52
- return localBinPath;
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
- // 2. Check system PATH (cargo install, manual install)
56
- for (const candidate of pathCandidates(env, platform)) {
57
- if (!isExecutableFile(candidate, platform)) continue;
58
- if (launcherPaths.has(canonicalPath(candidate))) continue;
59
- return candidate;
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
- return null;
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 main() {
66
- if (process.env[RECURSION_SENTINEL] === "1") {
67
- console.error("Refusing to recursively launch the Arete CLI shim.");
68
- process.exit(1);
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
- const binPath = getBinaryPath();
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
- if (!binPath) {
74
- console.error(
75
- "Arete CLI binary not found. This usually means the postinstall script failed.\n" +
76
- "Try reinstalling: npm install @usearete/a4\n" +
77
- "\n" +
78
- "If the problem persists, you can install the CLI via Cargo:\n" +
79
- " cargo install a4-cli"
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
- const result = spawnSync(binPath, process.argv.slice(2), {
85
- stdio: "inherit",
86
- env: { ...process.env, [RECURSION_SENTINEL]: "1" },
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
- if (result.error.code === "EACCES") {
91
- console.error(
92
- "Permission denied. Try running:\n" +
93
- ` chmod +x "${binPath}"`
94
- );
95
- } else {
96
- console.error("Failed to run Arete CLI:", result.error.message);
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
- process.exit(1);
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
- process.exit(result.status ?? 1);
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 = { canonicalPath, getBinaryPath, pathCandidates };
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.12.0",
3
+ "version": "0.13.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
- "postinstall": "node scripts/postinstall.js",
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": ">=16.0.0"
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
- });
@@ -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();