@abacus-ai/cli 2.5.5-canary.8 → 2.6.0-canary.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abacus-ai/cli",
3
- "version": "2.5.5-canary.8",
3
+ "version": "2.6.0-canary.10",
4
4
  "bin": {
5
5
  "abacusai": "scripts/npm-wrapper/shim.cjs"
6
6
  },
@@ -20,21 +20,103 @@ function detectInstallPm() {
20
20
  return "npm";
21
21
  }
22
22
 
23
+ // glibc builds report a runtime glibc version; musl builds do not.
24
+ function isMuslLinux() {
25
+ if (process.platform !== "linux") return false;
26
+ try {
27
+ return !process.report.getReport().header.glibcVersionRuntime;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ // Native CPU arch — corrected for emulation so we match what install.sh/install.ps1
34
+ // resolve. process.arch reports x64 for a Rosetta-translated process on Apple Silicon
35
+ // and for an x64-emulated process on Windows ARM64; both should get the arm64 build.
36
+ function detectArch() {
37
+ let arch = process.arch;
38
+ if (arch === "x64" && process.platform === "darwin") {
39
+ try {
40
+ if (
41
+ execFileSync("sysctl", ["-n", "sysctl.proc_translated"], { encoding: "utf8" }).trim() ===
42
+ "1"
43
+ )
44
+ arch = "arm64";
45
+ } catch {}
46
+ }
47
+ if (arch === "x64" && process.platform === "win32") {
48
+ try {
49
+ const out = execFileSync(
50
+ "reg",
51
+ [
52
+ "query",
53
+ "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment",
54
+ "/v",
55
+ "PROCESSOR_ARCHITECTURE",
56
+ ],
57
+ { encoding: "utf8" },
58
+ );
59
+ if (/\bARM64\b/i.test(out)) arch = "arm64";
60
+ } catch {}
61
+ }
62
+ return arch;
63
+ }
64
+
65
+ // x64 CPUs without AVX2 need the -baseline build.
66
+ function isBaselineX64(arch) {
67
+ if (arch !== "x64") return false;
68
+ try {
69
+ if (process.platform === "linux") {
70
+ return !fs.readFileSync("/proc/cpuinfo", "utf8").includes("avx2");
71
+ }
72
+ if (process.platform === "darwin") {
73
+ return !/avx2/i.test(
74
+ execFileSync("sysctl", ["-n", "machdep.cpu.leaf7_features"], { encoding: "utf8" }),
75
+ );
76
+ }
77
+ if (process.platform === "win32") {
78
+ // PF_AVX2_INSTRUCTIONS_AVAILABLE = 40
79
+ const out = execFileSync(
80
+ "powershell.exe",
81
+ [
82
+ "-NoProfile",
83
+ "-NonInteractive",
84
+ "-Command",
85
+ "[int]((Add-Type -PassThru -Name AbacusCpu -Namespace Win32 -MemberDefinition '[DllImport(\"kernel32.dll\")] public static extern bool IsProcessorFeaturePresent(int f);')::IsProcessorFeaturePresent(40))",
86
+ ],
87
+ { encoding: "utf8" },
88
+ );
89
+ return out.trim() === "0";
90
+ }
91
+ } catch {
92
+ return false; // detection failed → assume modern CPU
93
+ }
94
+ return false;
95
+ }
96
+
97
+ // -musl wins over -baseline: a glibc binary won't load on musl, and there's no
98
+ // combined artifact (a non-AVX2 musl host is an unbuilt combo).
99
+ function variantSuffix(arch) {
100
+ if (isMuslLinux()) return "-musl";
101
+ if (isBaselineX64(arch)) return "-baseline";
102
+ return "";
103
+ }
104
+
23
105
  function detectPlatform() {
24
- const arch = process.arch;
106
+ const arch = detectArch();
25
107
  if (arch !== "x64" && arch !== "arm64") {
26
108
  throw new Error(`Unsupported architecture: ${arch}`);
27
109
  }
28
- switch (process.platform) {
29
- case "darwin":
30
- return { target: `darwin-${arch}`, ext: "tar.gz", binaryName: "abacusai" };
31
- case "linux":
32
- return { target: `linux-${arch}`, ext: "tar.gz", binaryName: "abacusai" };
33
- case "win32":
34
- return { target: `win32-${arch}`, ext: "zip", binaryName: "abacusai.exe" };
35
- default:
36
- throw new Error(`Unsupported operating system: ${process.platform}`);
110
+ const platform = process.platform;
111
+ if (platform !== "darwin" && platform !== "linux" && platform !== "win32") {
112
+ throw new Error(`Unsupported operating system: ${platform}`);
37
113
  }
114
+ const win = platform === "win32";
115
+ return {
116
+ target: `${platform}-${arch}${variantSuffix(arch)}`,
117
+ ext: win ? "zip" : "tar.gz",
118
+ binaryName: win ? "abacusai.exe" : "abacusai",
119
+ };
38
120
  }
39
121
 
40
122
  function download(url, dest) {
@@ -86,48 +168,52 @@ function extract(archivePath, destDir, ext) {
86
168
  }
87
169
  }
88
170
 
89
- function readStamp(stampPath) {
90
- try {
91
- return fs.readFileSync(stampPath, "utf8").trim();
92
- } catch {
93
- return null;
94
- }
95
- }
171
+ // Per-version cache under the user's home, shared across npm installs and every
172
+ // npx/bunx run — so the same version is downloaded once, and older versions stay
173
+ // available to roll back to if a release fails to launch.
174
+ const VERSIONS_DIR = path.join(os.homedir(), ".abacusai", "versions");
175
+ const MAX_CACHED_VERSIONS = 3;
96
176
 
97
- function replaceBinary(stagedPath, targetPath) {
177
+ // Keep the newest few version dirs (by mtime); the cache is ~80 MB each.
178
+ function pruneOldVersions(keep) {
179
+ let dirs;
98
180
  try {
99
- fs.renameSync(stagedPath, targetPath);
181
+ dirs = fs.readdirSync(VERSIONS_DIR);
182
+ } catch {
100
183
  return;
101
- } catch (err) {
102
- if (process.platform !== "win32") throw err;
103
184
  }
104
- const oldPath = `${targetPath}.old`;
105
- try {
106
- fs.unlinkSync(oldPath);
107
- } catch {}
108
- try {
109
- fs.renameSync(targetPath, oldPath);
110
- } catch {}
111
- fs.renameSync(stagedPath, targetPath);
185
+ const scored = [];
186
+ for (const v of dirs) {
187
+ try {
188
+ scored.push({ v, mtime: fs.statSync(path.join(VERSIONS_DIR, v)).mtimeMs });
189
+ } catch {}
190
+ }
191
+ scored.sort((a, b) => b.mtime - a.mtime);
192
+ for (const { v } of scored.slice(MAX_CACHED_VERSIONS)) {
193
+ if (v === keep) continue;
194
+ try {
195
+ fs.rmSync(path.join(VERSIONS_DIR, v), { recursive: true, force: true });
196
+ } catch {}
197
+ }
112
198
  }
113
199
 
114
200
  async function main() {
115
201
  const { target, ext, binaryName } = detectPlatform();
116
- const binDir = path.join(PKG_ROOT, "bin");
117
- const targetPath = path.join(binDir, binaryName);
118
- const stampPath = path.join(binDir, ".version");
202
+ const version = process.env.ABACUSAI_TARGET_VERSION || VERSION;
203
+ const versionDir = path.join(VERSIONS_DIR, version);
204
+ const targetPath = path.join(versionDir, binaryName);
119
205
 
120
- if (readStamp(stampPath) === VERSION && fs.existsSync(targetPath)) return;
206
+ if (fs.existsSync(targetPath)) return;
121
207
 
122
- fs.mkdirSync(binDir, { recursive: true });
208
+ fs.mkdirSync(versionDir, { recursive: true });
123
209
 
124
210
  const archiveName = `abacusai-${target}.${ext}`;
125
- const url = `${BASE_URL}/${VERSION}/${archiveName}`;
211
+ const url = `${BASE_URL}/${version}/${archiveName}`;
126
212
 
127
213
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "abacusai-install-"));
128
214
  const archivePath = path.join(tmpDir, archiveName);
129
215
 
130
- process.stdout.write(`abacusai: downloading v${VERSION} for ${target}...\n`);
216
+ process.stdout.write(`abacusai: downloading v${version} for ${target}...\n`);
131
217
 
132
218
  try {
133
219
  await download(url, archivePath);
@@ -137,13 +223,12 @@ async function main() {
137
223
  const stagedPath = `${targetPath}.new`;
138
224
  fs.copyFileSync(sourceBinary, stagedPath);
139
225
  if (process.platform !== "win32") fs.chmodSync(stagedPath, 0o755);
226
+ fs.renameSync(stagedPath, targetPath); // atomic within the version dir
140
227
 
141
- replaceBinary(stagedPath, targetPath);
142
-
143
- fs.writeFileSync(stampPath, VERSION);
144
- fs.writeFileSync(path.join(binDir, ".install-source"), detectInstallPm());
228
+ fs.writeFileSync(path.join(versionDir, ".install-source"), detectInstallPm());
229
+ pruneOldVersions(version);
145
230
 
146
- process.stdout.write(`abacusai: installed v${VERSION} to ${targetPath}\n`);
231
+ process.stdout.write(`abacusai: installed v${version} to ${targetPath}\n`);
147
232
  } finally {
148
233
  try {
149
234
  fs.rmSync(tmpDir, { recursive: true, force: true });
@@ -2,44 +2,107 @@
2
2
  "use strict";
3
3
 
4
4
  const fs = require("node:fs");
5
+ const os = require("node:os");
5
6
  const path = require("node:path");
6
7
  const { spawnSync } = require("node:child_process");
7
8
 
8
9
  const PKG_ROOT = path.resolve(__dirname, "..", "..");
9
- const BIN_DIR = path.join(PKG_ROOT, "bin");
10
+ const VERSION = require(path.join(PKG_ROOT, "package.json")).version;
10
11
  const BINARY_NAME = process.platform === "win32" ? "abacusai.exe" : "abacusai";
11
- const BINARY_PATH = path.join(BIN_DIR, BINARY_NAME);
12
-
13
- if (!fs.existsSync(BINARY_PATH)) {
14
- const installer = path.join(__dirname, "install.cjs");
15
- const res = spawnSync(process.execPath, [installer], { stdio: "inherit" });
16
- if (res.status !== 0 || !fs.existsSync(BINARY_PATH)) {
17
- process.stderr.write(`abacusai: failed to install native binary at ${BINARY_PATH}\n`);
18
- process.exit(res.status ?? 1);
19
- }
20
- }
12
+
13
+ // Binaries are cached per-version under the user's home, NOT inside the package
14
+ // dir — so ephemeral runners (npx/bunx) that recreate the package each run still
15
+ // reuse the download, and so older versions remain available to roll back to.
16
+ const VERSIONS_DIR = path.join(os.homedir(), ".abacusai", "versions");
17
+ const binaryFor = (version) => path.join(VERSIONS_DIR, version, BINARY_NAME);
18
+
19
+ // A binary that dies with one of these never actually ran — wrong arch, missing
20
+ // libc, truncated download. Distinct from a normal non-zero exit.
21
+ const FATAL_SIGNALS = new Set(["SIGILL", "SIGSEGV", "SIGABRT", "SIGTRAP", "SIGBUS"]);
22
+ const LAUNCH_FAILURE_MS = 3000;
21
23
 
22
24
  function readInstallSource() {
23
25
  try {
24
- return fs.readFileSync(path.join(BIN_DIR, ".install-source"), "utf8").trim() || "npm";
26
+ return (
27
+ fs.readFileSync(path.join(VERSIONS_DIR, VERSION, ".install-source"), "utf8").trim() || "npm"
28
+ );
25
29
  } catch {
26
30
  return "npm";
27
31
  }
28
32
  }
29
33
 
30
- const result = spawnSync(BINARY_PATH, process.argv.slice(2), {
31
- stdio: "inherit",
32
- env: { ...process.env, ABACUSAI_INSTALL_SOURCE: readInstallSource() },
33
- });
34
+ function ensureBinary(version) {
35
+ const target = binaryFor(version);
36
+ if (fs.existsSync(target)) return target;
37
+ spawnSync(process.execPath, [path.join(__dirname, "install.cjs")], {
38
+ stdio: "inherit",
39
+ env: { ...process.env, ABACUSAI_TARGET_VERSION: version },
40
+ });
41
+ return fs.existsSync(target) ? target : null;
42
+ }
43
+
44
+ // Newest cached binary other than `exclude` (by mtime), to roll back to.
45
+ function newestCachedFallback(exclude) {
46
+ let versions;
47
+ try {
48
+ versions = fs.readdirSync(VERSIONS_DIR);
49
+ } catch {
50
+ return null;
51
+ }
52
+ let best = null;
53
+ for (const v of versions) {
54
+ if (v === exclude) continue;
55
+ const bin = binaryFor(v);
56
+ let mtime;
57
+ try {
58
+ mtime = fs.statSync(bin).mtimeMs;
59
+ } catch {
60
+ continue;
61
+ }
62
+ if (!best || mtime > best.mtime) best = { version: v, bin, mtime };
63
+ }
64
+ return best;
65
+ }
66
+
67
+ function run(binPath) {
68
+ const start = Date.now();
69
+ const res = spawnSync(binPath, process.argv.slice(2), {
70
+ stdio: "inherit",
71
+ env: { ...process.env, ABACUSAI_INSTALL_SOURCE: readInstallSource() },
72
+ });
73
+ const launchFailed =
74
+ !!res.error ||
75
+ (!!res.signal && FATAL_SIGNALS.has(res.signal) && Date.now() - start < LAUNCH_FAILURE_MS);
76
+ return { res, launchFailed };
77
+ }
34
78
 
35
- if (result.error) {
36
- process.stderr.write(`abacusai: failed to launch binary: ${result.error.message}\n`);
79
+ const binPath = ensureBinary(VERSION);
80
+ if (!binPath) {
81
+ process.stderr.write(`abacusai: failed to install native binary for v${VERSION}\n`);
37
82
  process.exit(1);
38
83
  }
39
84
 
40
- if (result.signal) {
41
- process.kill(process.pid, result.signal);
42
- return;
85
+ let { res, launchFailed } = run(binPath);
86
+
87
+ // A corrupt/incompatible release must not brick the CLI: if the binary couldn't
88
+ // launch, fall back to the newest other cached version so the user keeps working
89
+ // (and can update past the bad release).
90
+ if (launchFailed) {
91
+ const fallback = newestCachedFallback(VERSION);
92
+ if (fallback) {
93
+ process.stderr.write(
94
+ `abacusai: v${VERSION} failed to launch — falling back to cached v${fallback.version}.\n`,
95
+ );
96
+ ({ res } = run(fallback.bin));
97
+ }
43
98
  }
44
99
 
45
- process.exit(result.status ?? 0);
100
+ if (res.error) {
101
+ process.stderr.write(`abacusai: failed to launch binary: ${res.error.message}\n`);
102
+ process.exit(1);
103
+ }
104
+ if (res.signal) {
105
+ process.kill(process.pid, res.signal);
106
+ } else {
107
+ process.exit(res.status ?? 0);
108
+ }