@usearete/a4 0.6.0 → 0.7.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.
Files changed (3) hide show
  1. package/bin/a4.js +80 -34
  2. package/bin/a4.test.js +86 -0
  3. package/package.json +3 -2
package/bin/a4.js CHANGED
@@ -7,54 +7,100 @@ const fs = require("fs");
7
7
  const binName = process.platform === "win32" ? "a4.exe" : "a4";
8
8
  const localBinPath = path.join(__dirname, binName);
9
9
 
10
- // Try local binary first, then fall back to PATH
11
- function getBinaryPath() {
10
+ const RECURSION_SENTINEL = "ARETE_A4_LAUNCHER_ACTIVE";
11
+
12
+ function canonicalPath(candidate) {
13
+ try {
14
+ return fs.realpathSync(candidate);
15
+ } catch {
16
+ return path.resolve(candidate);
17
+ }
18
+ }
19
+
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)));
26
+ }
27
+
28
+ function isExecutableFile(candidate, platform = process.platform) {
29
+ 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;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
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
+ );
46
+
12
47
  // 1. Check for bundled binary (npm postinstall)
13
- if (fs.existsSync(localBinPath)) {
48
+ if (
49
+ isExecutableFile(localBinPath, platform)
50
+ && !launcherPaths.has(canonicalPath(localBinPath))
51
+ ) {
14
52
  return localBinPath;
15
53
  }
16
54
 
17
55
  // 2. Check system PATH (cargo install, manual install)
18
- const whichCmd = process.platform === "win32" ? "where" : "which";
19
- const result = spawnSync(whichCmd, ["a4"], { encoding: "utf8" });
20
- if (result.status === 0 && result.stdout) {
21
- const systemBin = result.stdout.trim().split("\n")[0];
22
- if (fs.existsSync(systemBin)) {
23
- return systemBin;
24
- }
56
+ for (const candidate of pathCandidates(env, platform)) {
57
+ if (!isExecutableFile(candidate, platform)) continue;
58
+ if (launcherPaths.has(canonicalPath(candidate))) continue;
59
+ return candidate;
25
60
  }
26
61
 
27
62
  return null;
28
63
  }
29
64
 
30
- const binPath = getBinaryPath();
31
-
32
- if (!binPath) {
33
- console.error(
34
- "Arete CLI binary not found. This usually means the postinstall script failed.\n" +
35
- "Try reinstalling: npm install @usearete/a4\n" +
36
- "\n" +
37
- "If the problem persists, you can install the CLI via Cargo:\n" +
38
- " cargo install a4-cli"
39
- );
40
- process.exit(1);
41
- }
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);
69
+ }
42
70
 
43
- const result = spawnSync(binPath, process.argv.slice(2), {
44
- stdio: "inherit",
45
- env: process.env,
46
- });
71
+ const binPath = getBinaryPath();
47
72
 
48
- if (result.error) {
49
- if (result.error.code === "EACCES") {
73
+ if (!binPath) {
50
74
  console.error(
51
- "Permission denied. Try running:\n" +
52
- ` chmod +x "${binPath}"`
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"
53
80
  );
54
- } else {
55
- console.error("Failed to run Arete CLI:", result.error.message);
81
+ process.exit(1);
56
82
  }
57
- process.exit(1);
83
+
84
+ const result = spawnSync(binPath, process.argv.slice(2), {
85
+ stdio: "inherit",
86
+ env: { ...process.env, [RECURSION_SENTINEL]: "1" },
87
+ });
88
+
89
+ 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);
97
+ }
98
+ process.exit(1);
99
+ }
100
+
101
+ process.exit(result.status ?? 1);
58
102
  }
59
103
 
60
- process.exit(result.status ?? 1);
104
+ if (require.main === module) main();
105
+
106
+ module.exports = { canonicalPath, getBinaryPath, pathCandidates };
package/bin/a4.test.js ADDED
@@ -0,0 +1,86 @@
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/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@usearete/a4",
3
- "version": "0.6.0",
3
+ "version": "0.7.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"
9
+ "postinstall": "node scripts/postinstall.js",
10
+ "test": "node --test bin/*.test.js"
10
11
  },
11
12
  "repository": {
12
13
  "type": "git",