@aicraftalchemy/aca 0.0.33 → 0.0.34

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/cli-wrapper.cjs CHANGED
@@ -12,6 +12,11 @@ const fs = require('node:fs')
12
12
  const binDir = path.join(__dirname, 'bin')
13
13
  const isWin = process.platform === 'win32'
14
14
  const bin = path.join(binDir, isWin ? 'aca.exe' : 'aca')
15
+ // Version of the binary currently in bin/ — written by install.cjs on install
16
+ // and kept current by applyStagedUpdate below. Lets the launcher detect a
17
+ // stale binary without paying a --version spawn on every start.
18
+ const stampFile = path.join(binDir, '.aca-bin-version')
19
+ const pkgVersion = require('./package.json').version
15
20
 
16
21
  /**
17
22
  * Apply a staged auto-update. The running binary downloads a new version to
@@ -25,6 +30,10 @@ const bin = path.join(binDir, isWin ? 'aca.exe' : 'aca')
25
30
  function applyStagedUpdate() {
26
31
  const marker = path.join(binDir, '.aca-pending-version')
27
32
  if (!fs.existsSync(marker)) return
33
+ // The marker holds the staged version string (written by autoUpdater);
34
+ // capture it before the swap so the stamp can track what bin/ now contains.
35
+ let stagedVersion = null
36
+ try { stagedVersion = fs.readFileSync(marker, 'utf8').trim() } catch {}
28
37
  const acaPending = path.join(binDir, isWin ? 'aca.exe.pending' : 'aca.pending')
29
38
  if (!fs.existsSync(acaPending)) {
30
39
  // Marker without a binary — stale/partial. Clear it and move on.
@@ -46,6 +55,9 @@ function applyStagedUpdate() {
46
55
  swap(isWin ? 'aca.exe' : 'aca')
47
56
  swap(isWin ? 'rg.exe' : 'rg')
48
57
  try { fs.rmSync(marker, { force: true }) } catch {}
58
+ if (stagedVersion) {
59
+ try { fs.writeFileSync(stampFile, stagedVersion) } catch {}
60
+ }
49
61
  } catch (err) {
50
62
  // Swap failed (e.g. transient FS error) — leave the pending files in place
51
63
  // so the next launch retries; fall through and run the current binary.
@@ -84,10 +96,62 @@ function selfHealBinary() {
84
96
  }
85
97
  }
86
98
 
99
+ /**
100
+ * Refresh a STALE binary. On upgrade npm replaces the package directory, but
101
+ * when install scripts are blocked (allowScripts policy, `--ignore-scripts`)
102
+ * the postinstall never runs — and a surviving old bin/ means `aca` silently
103
+ * keeps launching the previous version forever. The stamp written by
104
+ * install.cjs (and kept current by applyStagedUpdate) says what bin/ holds;
105
+ * if it's OLDER than package.json, re-run the installer. Never downgrade: the
106
+ * auto-updater may legitimately have advanced the binary past the npm
107
+ * version. Best-effort — if the refresh fails (e.g. offline), warn and run
108
+ * the existing binary rather than dead-ending the user.
109
+ */
110
+ function refreshStaleBinary() {
111
+ let stamped = null
112
+ try { stamped = fs.readFileSync(stampFile, 'utf8').trim() } catch {}
113
+ // A malformed stamp would compare as NaN (treated as equal) and block
114
+ // refresh forever — treat it as missing so the probe below recovers it.
115
+ if (stamped && !/^\d+\.\d+\.\d+$/.test(stamped)) stamped = null
116
+ if (!stamped) {
117
+ // Pre-stamp install: ask the binary itself once and record the answer.
118
+ const probe = spawnSync(bin, ['--version'], { encoding: 'utf8', timeout: 15000 })
119
+ const m = /(\d+\.\d+\.\d+)/.exec(probe.stdout || '')
120
+ if (m) {
121
+ stamped = m[1]
122
+ try { fs.writeFileSync(stampFile, stamped) } catch {}
123
+ }
124
+ }
125
+ const cmpVersions = (a, b) => {
126
+ const pa = a.split('.').map(Number)
127
+ const pb = b.split('.').map(Number)
128
+ for (let i = 0; i < 3; i++) {
129
+ const d = (pa[i] || 0) - (pb[i] || 0)
130
+ if (d) return d
131
+ }
132
+ return 0
133
+ }
134
+ if (stamped && cmpVersions(stamped, pkgVersion) >= 0) return
135
+ const installer = path.join(__dirname, 'install.cjs')
136
+ if (!fs.existsSync(installer)) return
137
+ console.error(
138
+ `ACA: refreshing binary ${stamped ? `v${stamped}` : '(unknown version)'} → v${pkgVersion} (install scripts were skipped)…`,
139
+ )
140
+ const r = spawnSync(process.execPath, [installer], { stdio: ['ignore', 2, 2] })
141
+ if (r.error || r.status !== 0) {
142
+ console.error(
143
+ 'ACA: binary refresh failed; continuing with the existing binary.' +
144
+ '\nRetry with: npm install -g @aicraftalchemy/aca',
145
+ )
146
+ }
147
+ }
148
+
87
149
  applyStagedUpdate()
88
150
 
89
151
  if (!fs.existsSync(bin)) {
90
152
  selfHealBinary()
153
+ } else {
154
+ refreshStaleBinary()
91
155
  }
92
156
 
93
157
  const result = spawnSync(bin, process.argv.slice(2), { stdio: 'inherit' })
package/install.cjs CHANGED
@@ -100,6 +100,12 @@ async function main() {
100
100
  writeReplacing(dest, bin)
101
101
  if (process.platform !== 'win32') fs.chmodSync(dest, 0o755)
102
102
 
103
+ // Stamp the installed version. cli-wrapper compares this against its own
104
+ // package.json to detect a STALE binary — the case where npm upgraded the
105
+ // package but skipped this script (allowScripts policy / --ignore-scripts),
106
+ // leaving an old binary that would otherwise run forever.
107
+ fs.writeFileSync(path.join(binDir, '.aca-bin-version'), VERSION)
108
+
103
109
  // Also fetch the sibling ripgrep binary. `bun build --compile` does not embed
104
110
  // ripgrep, so ACA resolves this `rg` next to itself at runtime — it powers
105
111
  // Grep, Glob, and @-mention file search. Best-effort: a failure here must not
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicraftalchemy/aca",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
4
4
  "description": "ACA — agentic coding CLI. Any model, any provider (Claude, Gemini, OpenAI, NVIDIA, Groq, Cerebras, OpenRouter, Hugging Face). Developed by Aicraftalchemy.",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {