create-byan-agent 2.29.1 → 2.29.2

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/CHANGELOG.md CHANGED
@@ -9,6 +9,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [2.29.2] - 2026-06-23
13
+
14
+ ### Fixed - RTK optional install: fail-proof, off-PATH, shell-aware
15
+
16
+ The 2.29.x RTK installer could hang silently and mis-report a successful build.
17
+
18
+ - **Bounded + visible.** The delegated install now runs with a per-strategy
19
+ timeout (brew/script 5min, cargo 20min; override via `BYAN_RTK_TIMEOUT_MS`,
20
+ positive integers only) and inherited stdio, so progress streams live instead
21
+ of a frozen line. Inherited stdio also sidesteps execSync's 1MB maxBuffer cap a
22
+ verbose build would blow.
23
+ - **Off-PATH resolution.** `cargo install` drops the binary in `~/.cargo/bin`,
24
+ which a Debian non-login PATH does not include — the install succeeded but
25
+ `rtk --version` reported "unverified". The installer now resolves rtk across
26
+ known dirs (PATH, `~/.cargo/bin`, `~/.local/bin`, `/usr/local/bin`, brew prefix,
27
+ `CARGO_HOME`), verifies + wires the hook by the resolved path, and prints a
28
+ shell-correct PATH hint (`fish_add_path` on fish, `export` on bash/zsh,
29
+ `$env:PATH` on Windows).
30
+ - **Prebuilt preferred.** Strategy order is now brew > script > cargo: the
31
+ prebuilt-binary script (pinned to the immutable tag ref, which SHA-256-verifies
32
+ the binary per refs/tags/v0.42.4/install.sh) is fast and PATH-stable; cargo
33
+ (from-source, off-PATH, slow) becomes the last-resort fallback.
34
+ - **Stays graceful.** Hardened the no-throw contract: a HOME-less environment
35
+ (`os.homedir()` throwing) no longer propagates out of `setup-rtk.js`.
36
+
37
+ Reviewed by an adversarial workflow (correctness + supply-chain + mantras) plus a
38
+ compliance pass; 2375/2375 tests green (49 dedicated to RTK).
39
+
12
40
  ## [2.29.1] - 2026-06-23
13
41
 
14
42
  ### Fixed - Republish (the 2.29.0 npm tarball was missing)
@@ -1383,11 +1383,14 @@ async function install(options = {}) {
1383
1383
  },
1384
1384
  ]);
1385
1385
  if (proceed) {
1386
+ console.log(chalk.gray(' Installing... progress streams below. The cargo fallback compiles from source (can take minutes); Ctrl+C is safe — BYAN is already installed.'));
1386
1387
  const r = setupRtkIntegration({ log: (m) => console.log(chalk.gray(' ' + m)) });
1387
1388
  if (r.synced) {
1388
1389
  console.log(chalk.green(` ✓ rtk ready (${r.installedVia}, v${r.version || '?'}) — restart Claude Code to activate`));
1390
+ if (r.pathHint) console.log(chalk.yellow(` ⚠ rtk is not on your PATH — add it: ${r.pathHint}`));
1389
1391
  } else {
1390
- console.log(chalk.yellow(` ⚠ rtk not wired (${r.reason}) — BYAN unaffected`));
1392
+ console.log(chalk.yellow(` ⚠ rtk not wired (${r.reason}) — BYAN unaffected; re-run \`npm run setup-rtk\` anytime`));
1393
+ if (r.pathHint) console.log(chalk.yellow(` ⚠ rtk found off-PATH — add it then re-run: ${r.pathHint}`));
1391
1394
  }
1392
1395
  } else {
1393
1396
  console.log(chalk.gray(' rtk skipped — run `npm run setup-rtk` anytime to enable.'));
@@ -11,6 +11,9 @@
11
11
  */
12
12
 
13
13
  const { execSync } = require('child_process');
14
+ const fs = require('fs');
15
+ const os = require('os');
16
+ const path = require('path');
14
17
 
15
18
  /**
16
19
  * commandExists(cmd) -> boolean. Is an executable resolvable on PATH?
@@ -26,6 +29,70 @@ function commandExists(cmd, { run = execSync, platform = process.platform } = {}
26
29
  }
27
30
  }
28
31
 
32
+ /**
33
+ * safeHomedir() -> the home directory, or '' if it cannot be determined. WHY:
34
+ * os.homedir() THROWS on an env with no $HOME/$USERPROFILE AND no passwd entry
35
+ * for the effective uid (distroless / random-uid containers). An optional install
36
+ * helper must never throw on that — it degrades to "no home dir to scan".
37
+ */
38
+ function safeHomedir() {
39
+ try {
40
+ return os.homedir();
41
+ } catch {
42
+ return '';
43
+ }
44
+ }
45
+
46
+ /**
47
+ * knownBinDirs() -> ordered list of directories where installers commonly drop a
48
+ * binary that is NOT on a non-login shell's PATH. The canonical case: `cargo
49
+ * install` lands in ~/.cargo/bin, which Debian's apt-packaged cargo does NOT add
50
+ * to PATH — so a freshly built binary is present yet invisible to a bare probe.
51
+ * fs/env/home are injectable for tests. `home` defaults via safeHomedir so a
52
+ * HOME-less environment yields a shorter scan instead of a throw.
53
+ */
54
+ function knownBinDirs({ env = process.env, home = safeHomedir(), platform = process.platform } = {}) {
55
+ const dirs = [];
56
+ if (env && env.CARGO_HOME) dirs.push(path.join(env.CARGO_HOME, 'bin'));
57
+ dirs.push(path.join(home, '.cargo', 'bin')); // cargo install
58
+ dirs.push(path.join(home, '.local', 'bin')); // pip/pipx + many curl scripts
59
+ if (platform === 'win32') {
60
+ if (env && env.LOCALAPPDATA) dirs.push(path.join(env.LOCALAPPDATA, 'Programs'));
61
+ } else {
62
+ dirs.push('/usr/local/bin'); // common script target + brew (intel mac)
63
+ dirs.push('/opt/homebrew/bin'); // brew (apple silicon)
64
+ dirs.push('/usr/bin');
65
+ }
66
+ return dirs;
67
+ }
68
+
69
+ /**
70
+ * resolveBinary(name) -> the command to invoke the executable, or null if it
71
+ * cannot be located. Returns the BARE name when it is on PATH (let the shell
72
+ * resolve it), else an ABSOLUTE path found in a knownBinDir. WHY: an optional
73
+ * native install can succeed yet leave the binary off PATH; callers must verify
74
+ * and wire it by its real location, not declare failure. All I/O is injectable.
75
+ */
76
+ function resolveBinary(name, {
77
+ has = commandExists,
78
+ existsSync = fs.existsSync,
79
+ env = process.env,
80
+ home = safeHomedir(),
81
+ platform = process.platform,
82
+ } = {}) {
83
+ if (has(name)) return name;
84
+ const exe = platform === 'win32' ? `${name}.exe` : name;
85
+ for (const dir of knownBinDirs({ env, home, platform })) {
86
+ const full = path.join(dir, exe);
87
+ try {
88
+ if (existsSync(full)) return full;
89
+ } catch {
90
+ // a stat error on one candidate must not abort the scan
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+
29
96
  /**
30
97
  * detectPlatform() -> 'mac' | 'windows' | 'linux'. Coarse bucket for choosing an
31
98
  * install strategy. Keeps the platform string in ONE place.
@@ -47,4 +114,4 @@ function firstAvailable(candidates, { has = commandExists } = {}) {
47
114
  return candidates.find((c) => c && c.tool && has(c.tool)) || null;
48
115
  }
49
116
 
50
- module.exports = { commandExists, detectPlatform, firstAvailable };
117
+ module.exports = { commandExists, detectPlatform, firstAvailable, knownBinDirs, resolveBinary, safeHomedir };
@@ -9,18 +9,32 @@
9
9
  *
10
10
  * MAINTAINABILITY by design (the whole point of choosing this shape):
11
11
  * - We do NOT reimplement per-OS download / checksum / version pinning. We
12
- * DELEGATE the install to rtk's own canonical installer (brew / cargo / the
13
- * official install.sh), and DELEGATE the Claude Code hook wiring to rtk's own
14
- * `rtk init -g`. BYAN maintains only "pick the available installer, run it,
15
- * verify, ask rtk to wire its hook" — a tiny surface, bumped via one constant.
12
+ * DELEGATE the install to rtk's own canonical installer (brew / the official
13
+ * install.sh / cargo), and DELEGATE the Claude Code hook wiring to rtk's own
14
+ * `rtk init -g`. BYAN maintains only "pick the available installer, run it
15
+ * bounded + visible, locate the binary, ask rtk to wire its hook" — a tiny
16
+ * surface, bumped via one constant.
16
17
  * - SUPPLY-CHAIN: we pin to a TAG, never a moving branch. cargo builds the
17
- * tagged source (`--tag`), and install.sh is fetched from the IMMUTABLE tag ref
18
- * (and that script itself checksum-verifies the binary it downloads). So the
19
- * fetched artifacts are reproducible, not "whatever master is today".
20
- * - Everything is graceful: a missing installer or a failed step is a no-op that
21
- * NEVER breaks the BYAN install (returns { ok:true, synced:false, reason }).
22
- * - The command runner + the PATH probe are injectable, so the whole flow is
23
- * unit-tested without shelling out (see install/__tests__/rtk-integration.test.js).
18
+ * tagged source (`--tag`), and install.sh is fetched from the IMMUTABLE tag ref.
19
+ * That script verifies a SHA-256 of the downloaded binary against a published
20
+ * checksums.txt and aborts on mismatch (source: refs/tags/v0.42.4/install.sh,
21
+ * fetched + verified 2026-06-23). So the fetched artifacts are reproducible and
22
+ * integrity-checked, not "whatever master is today".
23
+ * - FAIL-PROOF: the delegated install runs with a per-strategy TIMEOUT (a hung
24
+ * network/build is bounded, not infinite) and with INHERITED stdio (the user
25
+ * SEES the installer's progress instead of staring at a frozen line; this also
26
+ * sidesteps execSync's 1MB maxBuffer cap that a verbose build would blow). Any
27
+ * missing installer / failed / timed-out step is a logged no-op that NEVER
28
+ * breaks the BYAN install (returns { ok:true, synced:false, reason }).
29
+ * - OFF-PATH SAFE: a successful install can leave the binary off PATH (the
30
+ * classic: cargo drops it in ~/.cargo/bin, which Debian's apt-cargo does not
31
+ * add to PATH). We RESOLVE the binary across known install dirs before
32
+ * declaring "unverified", verify + wire it by its real path, and hand the user
33
+ * a one-line PATH hint. The user's own report (rtk installed at ~/.cargo/bin,
34
+ * invisible to a bare probe) is exactly this case.
35
+ * - The command runner + the PATH probe + the resolver are injectable, so the
36
+ * whole flow is unit-tested without shelling out (see
37
+ * install/__tests__/rtk-integration.test.js).
24
38
  *
25
39
  * Mirrors the ENTRY-POINT shape of byan-web-integration.js / byan-leantime-
26
40
  * integration.js (one `setup*Integration`); the return contract is { ok, synced,
@@ -29,7 +43,8 @@
29
43
  */
30
44
 
31
45
  const { execSync } = require('child_process');
32
- const { commandExists, firstAvailable } = require('./native-helper');
46
+ const path = require('path');
47
+ const { commandExists, resolveBinary, firstAvailable } = require('./native-helper');
33
48
 
34
49
  // Pinned install target. Bumping rtk = change these two constants only. We pin a
35
50
  // TAG so the install is reproducible and supply-chain-bounded (see header).
@@ -38,20 +53,37 @@ const RTK_TAG = `v${RTK_VERSION}`;
38
53
  const RTK_REPO = 'https://github.com/rtk-ai/rtk';
39
54
  const RTK_INSTALL_SH = `https://raw.githubusercontent.com/rtk-ai/rtk/refs/tags/${RTK_TAG}/install.sh`;
40
55
 
56
+ // Per-strategy timeout (ms). A prebuilt-binary path (brew / script) should be
57
+ // quick; cargo COMPILES from source and is legitimately slow, so it gets a wide
58
+ // budget. The bound exists to cap a genuine HANG, not to race a normal build.
59
+ const MIN = 60 * 1000;
60
+ const RTK_TIMEOUT_MS = { brew: 5 * MIN, script: 5 * MIN, cargo: 20 * MIN };
61
+ const RTK_DEFAULT_TIMEOUT_MS = 10 * MIN;
62
+
41
63
  /**
42
64
  * Ordered install strategies — each DELEGATES to rtk's own canonical installer,
43
- * pinned to RTK_TAG where the mechanism allows. Preference: brew (cleanest
44
- * upgrades, vetted formula) > cargo (any Rust dev, pinned --tag) > the official
45
- * install.sh at the immutable tag ref (universal fallback, self-checksumming).
65
+ * pinned to RTK_TAG where the mechanism allows.
66
+ *
67
+ * Order = brew > script > cargo. WHY this order (not brew > cargo > script):
68
+ * - brew first: vetted formula, cleanest upgrades, prebuilt.
69
+ * - script second: the official install.sh downloads a PREBUILT binary to a
70
+ * PATH-stable location in seconds. It is pinned to the immutable tag ref and
71
+ * verifies the binary's SHA-256 before installing (see the cited check above)
72
+ * — a bounded supply-chain surface.
73
+ * - cargo LAST: it COMPILES from source (minutes) AND drops the binary in
74
+ * ~/.cargo/bin, frequently off a non-login PATH. It stays as a build-from-
75
+ * source fallback for machines without brew/curl, not the default path.
46
76
  */
47
77
  function installStrategies() {
48
78
  return [
49
- { id: 'brew', tool: 'brew', cmd: 'brew install rtk' },
50
- { id: 'cargo', tool: 'cargo', cmd: `cargo install --git ${RTK_REPO} --tag ${RTK_TAG}` },
79
+ { id: 'brew', tool: 'brew', cmd: 'brew install rtk', timeoutMs: RTK_TIMEOUT_MS.brew },
51
80
  // Pass RTK_VERSION into the script so it pins the BINARY too (the script
52
81
  // builds releases/download/${VERSION}/...; without it, it resolves
53
82
  // releases/latest). Pinning the URL alone is not enough — this pins both.
54
- { id: 'script', tool: 'curl', cmd: `curl -fsSL ${RTK_INSTALL_SH} | RTK_VERSION=${RTK_TAG} sh` },
83
+ // The script verifies a SHA-256 of the binary (vs checksums.txt) before
84
+ // installing — verified at refs/tags/v0.42.4/install.sh, 2026-06-23.
85
+ { id: 'script', tool: 'curl', cmd: `curl -fsSL ${RTK_INSTALL_SH} | RTK_VERSION=${RTK_TAG} sh`, timeoutMs: RTK_TIMEOUT_MS.script },
86
+ { id: 'cargo', tool: 'cargo', cmd: `cargo install --git ${RTK_REPO} --tag ${RTK_TAG}`, timeoutMs: RTK_TIMEOUT_MS.cargo },
55
87
  ];
56
88
  }
57
89
 
@@ -59,19 +91,78 @@ function oneLine(err) {
59
91
  return String((err && err.message) || err).split('\n')[0];
60
92
  }
61
93
 
94
+ // A bounded, env-overridable timeout for a strategy. BYAN_RTK_TIMEOUT_MS (ms)
95
+ // lets a user widen the bound (e.g. a slow machine compiling via cargo). Only a
96
+ // POSITIVE integer is honored: Node treats timeout:0 as "no timeout", so a 0
97
+ // override would silently re-introduce the unbounded hang the bound exists to
98
+ // prevent — it falls back to the strategy budget instead.
99
+ function timeoutFor(strat, { env = process.env } = {}) {
100
+ const raw = env && env.BYAN_RTK_TIMEOUT_MS;
101
+ if (raw != null && /^[0-9]+$/.test(String(raw))) {
102
+ const n = Number(raw);
103
+ if (n > 0) return n;
104
+ }
105
+ return (strat && strat.timeoutMs) || RTK_DEFAULT_TIMEOUT_MS;
106
+ }
107
+
108
+ // Did execSync throw because WE killed it on timeout (vs the command failing on
109
+ // its own)? Node sets `killed=true` and code 'ETIMEDOUT' when it kills the child
110
+ // on timeout; a build error sets a non-zero exit status with killed=false. We do
111
+ // NOT match a bare 'SIGTERM' signal: a sub-process self-terminating with SIGTERM
112
+ // for an unrelated reason would otherwise be mislabelled a timeout.
113
+ function isTimeout(err) {
114
+ return Boolean(err && (err.killed === true || err.code === 'ETIMEDOUT'));
115
+ }
116
+
117
+ // A shell/platform-correct "add this dir to PATH" hint. The resolved binary can
118
+ // live off PATH (cargo -> ~/.cargo/bin); the user needs the line that actually
119
+ // works in THEIR shell — fish rejects POSIX `export`, Windows rejects both.
120
+ function pathHintFor(bin, { env = process.env, platform = process.platform } = {}) {
121
+ // Parse the dir with the TARGET platform's rules, not the host's — so a win32
122
+ // path yields the right dir even when this code runs on a POSIX CI host.
123
+ const dir = (platform === 'win32' ? path.win32 : path.posix).dirname(bin);
124
+ if (platform === 'win32') return `$env:PATH = "${dir};$env:PATH"`;
125
+ const shell = String((env && env.SHELL) || '');
126
+ if (/(^|\/)fish$/.test(shell)) return `fish_add_path ${dir}`;
127
+ return `export PATH="${dir}:$PATH"`;
128
+ }
129
+
62
130
  /**
63
- * rtkStatus() -> { installed, version }. Never throws. Parses `rtk --version`
64
- * ("rtk 0.42.4"). An installed-but-unparseable build yields { installed:true,
65
- * version:null } (a real runtime possibility, handled downstream).
131
+ * rtkStatus() -> { installed, version, bin }. Never throws. Probes `<bin>
132
+ * --version` ("rtk 0.42.4"), default bin "rtk" (on PATH). An installed-but-
133
+ * unparseable build yields { installed:true, version:null }.
66
134
  */
67
- function rtkStatus({ run = execSync } = {}) {
135
+ function rtkStatus({ run = execSync, bin = 'rtk' } = {}) {
68
136
  try {
69
- const out = String(run('rtk --version', { stdio: 'pipe' }) || '');
137
+ const out = String(run(`${bin} --version`, { stdio: 'pipe' }) || '');
70
138
  const m = out.match(/rtk\s+v?([0-9]+\.[0-9]+\.[0-9]+)/i);
71
- return { installed: true, version: m ? m[1] : null };
139
+ return { installed: true, version: m ? m[1] : null, bin };
72
140
  } catch {
73
- return { installed: false, version: null };
141
+ return { installed: false, version: null, bin: null };
142
+ }
143
+ }
144
+
145
+ /**
146
+ * locateRtk() -> a verified { installed, version, bin } by trying the bare name
147
+ * first (on PATH), then resolving across known install dirs (off-PATH). Returns
148
+ * the off-PATH absolute path in `bin` when that is where it was found.
149
+ */
150
+ function locateRtk({ run = execSync, has = commandExists, resolve = resolveBinary, env = process.env } = {}) {
151
+ const onPath = rtkStatus({ run, bin: 'rtk' });
152
+ if (onPath.installed) return onPath;
153
+ // The resolver must never break the never-throws contract; degrade to not-found
154
+ // if it (or a misbehaving injected resolver) throws.
155
+ let resolved = null;
156
+ try {
157
+ resolved = resolve('rtk', { has, env });
158
+ } catch {
159
+ resolved = null;
160
+ }
161
+ if (resolved && resolved !== 'rtk') {
162
+ const s = rtkStatus({ run, bin: resolved });
163
+ if (s.installed) return s;
74
164
  }
165
+ return { installed: false, version: null, bin: null };
75
166
  }
76
167
 
77
168
  /**
@@ -82,16 +173,18 @@ function pickStrategy({ has = commandExists } = {}) {
82
173
  }
83
174
 
84
175
  /**
85
- * doctor() -> a status report for diagnostics / a `byan` doctor surface. `pinned`
86
- * is the version BYAN installs/targets NOT an enforced floor on a pre-existing
87
- * rtk (we do not break a user's own older rtk).
176
+ * doctor() -> a status report for diagnostics / a `byan` doctor surface. Resolves
177
+ * the binary across known dirs so an off-PATH install is reported as installed
178
+ * (with its real path), not as missing. `pinned` is the version BYAN targets —
179
+ * NOT an enforced floor on a pre-existing rtk.
88
180
  */
89
- function doctor({ run = execSync } = {}) {
90
- const s = rtkStatus({ run });
181
+ function doctor({ run = execSync, has = commandExists, resolve = resolveBinary, env = process.env } = {}) {
182
+ const s = locateRtk({ run, has, resolve, env });
91
183
  return {
92
184
  component: 'rtk',
93
185
  installed: s.installed,
94
186
  version: s.version,
187
+ bin: s.bin,
95
188
  pinned: RTK_VERSION,
96
189
  repo: RTK_REPO,
97
190
  hookCommand: 'rtk init -g',
@@ -99,61 +192,88 @@ function doctor({ run = execSync } = {}) {
99
192
  }
100
193
 
101
194
  // Delegate the Claude Code hook wiring to rtk's OWN command (idempotent). This is
102
- // the maintainability win: no manual settings.json merge to keep in sync.
103
- function wireHook({ run, log, installedVia, version }) {
195
+ // the maintainability win: no manual settings.json merge to keep in sync. Wires
196
+ // via the resolved `bin` so an off-PATH install still gets its hook.
197
+ function wireHook({ run, log, installedVia, version, bin = 'rtk', env = process.env, platform = process.platform }) {
198
+ const offPath = bin !== 'rtk';
104
199
  try {
105
- run('rtk init -g', { stdio: 'pipe' });
106
- log(`rtk: ready (${installedVia}, v${version || '?'}) — hook wired via 'rtk init -g'. Restart Claude Code to activate.`);
107
- return { ok: true, synced: true, reason: 'wired', installed: true, installedVia, version, hook: true };
200
+ run(`${bin} init -g`, { stdio: 'pipe' });
201
+ log(`rtk: ready (${installedVia}, v${version || '?'}) — hook wired via '${bin} init -g'. Restart Claude Code to activate.`);
202
+ const r = { ok: true, synced: true, reason: 'wired', installed: true, installedVia, version, hook: true, bin };
203
+ if (offPath) {
204
+ r.pathHint = pathHintFor(bin, { env, platform });
205
+ log(`rtk: note — installed at ${bin}, not on your PATH. Add it: ${r.pathHint}`);
206
+ }
207
+ return r;
108
208
  } catch (err) {
109
- log(`rtk: installed (${installedVia}) but 'rtk init -g' failed (${oneLine(err)}) — run it manually, BYAN unaffected.`);
110
- return { ok: true, synced: false, reason: 'hook-failed', installed: true, installedVia, version, hook: false };
209
+ log(`rtk: installed (${installedVia}) but '${bin} init -g' failed (${oneLine(err)}) — run it manually, BYAN unaffected.`);
210
+ const r = { ok: true, synced: false, reason: 'hook-failed', installed: true, installedVia, version, hook: false, bin };
211
+ if (offPath) r.pathHint = pathHintFor(bin, { env, platform });
212
+ return r;
111
213
  }
112
214
  }
113
215
 
114
216
  /**
115
217
  * setupRtkIntegration(options) -> { ok, synced, reason, ... }. The single entry.
116
218
  *
117
- * Flow: if rtk is already present, just (re)wire the hook (idempotent). Otherwise
118
- * pick an available installer, delegate the install to rtk, verify, then wire the
119
- * hook. Any missing tool / failed step degrades to a logged no-op — it NEVER
120
- * throws and NEVER fails the surrounding BYAN install. `ok` is therefore always
121
- * true; `synced` says whether rtk ended up wired.
219
+ * Flow: if rtk is already present (on PATH or off-PATH in a known dir), just
220
+ * (re)wire the hook (idempotent). Otherwise pick an available installer, delegate
221
+ * the install (BOUNDED by a timeout + VISIBLE via inherited stdio), LOCATE the
222
+ * binary, verify, then wire the hook. Any missing tool / failed / timed-out step
223
+ * degrades to a logged no-op it NEVER throws and NEVER fails the surrounding
224
+ * BYAN install. `ok` is therefore always true; `synced` says whether rtk ended
225
+ * up wired. An off-PATH install carries a `pathHint`.
122
226
  *
123
227
  * @param {object} [o]
124
- * @param {Function} [o.run] command runner (default execSync) — injected in tests
125
- * @param {Function} [o.has] PATH probe (default commandExists) — injected in tests
126
- * @param {Function} [o.log] one-line breadcrumb sink (default no-op)
228
+ * @param {Function} [o.run] command runner (default execSync) — injected in tests
229
+ * @param {Function} [o.has] PATH probe (default commandExists) — injected in tests
230
+ * @param {Function} [o.resolve] binary resolver (default resolveBinary) — injected in tests
231
+ * @param {Function} [o.log] one-line breadcrumb sink (default no-op)
232
+ * @param {object} [o.env] environment (default process.env) — for the timeout override
127
233
  */
128
- function setupRtkIntegration({ run = execSync, has = commandExists, log = () => {} } = {}) {
129
- const before = rtkStatus({ run });
234
+ function setupRtkIntegration({
235
+ run = execSync,
236
+ has = commandExists,
237
+ resolve = resolveBinary,
238
+ log = () => {},
239
+ env = process.env,
240
+ platform = process.platform,
241
+ } = {}) {
242
+ const before = locateRtk({ run, has, resolve, env });
130
243
  if (before.installed) {
131
- return wireHook({ run, log, installedVia: 'already-present', version: before.version });
244
+ return wireHook({ run, log, installedVia: 'already-present', version: before.version, bin: before.bin, env, platform });
132
245
  }
133
246
 
134
247
  const strat = pickStrategy({ has });
135
248
  if (!strat) {
136
- log('rtk: no installer found (brew / cargo / curl) — skipped. BYAN install unaffected.');
249
+ log('rtk: no installer found (brew / curl / cargo) — skipped. BYAN install unaffected.');
137
250
  return { ok: true, synced: false, reason: 'no-installer', installed: false, hook: false };
138
251
  }
139
252
 
140
- // Breadcrumb BEFORE the (silent, possibly slow) delegated install so the user
141
- // is not left staring at a frozen prompt while rtk's installer runs.
142
- log(`rtk: installing via ${strat.id} (pinned ${RTK_TAG}; this can take a moment)...`);
253
+ // Breadcrumb BEFORE the delegated install. stdio is INHERITED so the installer's
254
+ // own progress streams to the user (no frozen line), bounded by a timeout.
255
+ const timeoutMs = timeoutFor(strat, { env });
256
+ log(`rtk: installing via ${strat.id} (pinned ${RTK_TAG}; up to ${Math.round(timeoutMs / MIN)}min, Ctrl+C is safe)...`);
143
257
  try {
144
- run(strat.cmd, { stdio: 'pipe' });
258
+ run(strat.cmd, { stdio: 'inherit', timeout: timeoutMs });
145
259
  } catch (err) {
260
+ if (isTimeout(err)) {
261
+ log(`rtk: install via ${strat.id} exceeded ${Math.round(timeoutMs / MIN)}min — stopped (a background build may still continue). Retry with 'npm run setup-rtk' (or widen BYAN_RTK_TIMEOUT_MS). BYAN unaffected.`);
262
+ return { ok: true, synced: false, reason: `install-timeout:${strat.id}`, installed: false, hook: false };
263
+ }
146
264
  log(`rtk: install via ${strat.id} failed (${oneLine(err)}) — skipped gracefully.`);
147
265
  return { ok: true, synced: false, reason: `install-failed:${strat.id}`, installed: false, hook: false };
148
266
  }
149
267
 
150
- const after = rtkStatus({ run });
268
+ // The binary may be installed but OFF PATH (cargo -> ~/.cargo/bin). Resolve it
269
+ // before declaring failure, and wire it by its real location.
270
+ const after = locateRtk({ run, has, resolve, env });
151
271
  if (!after.installed) {
152
272
  log(`rtk: install via ${strat.id} ran but 'rtk --version' did not confirm — skipped.`);
153
273
  return { ok: true, synced: false, reason: 'install-unverified', installed: false, hook: false };
154
274
  }
155
275
 
156
- return wireHook({ run, log, installedVia: strat.id, version: after.version });
276
+ return wireHook({ run, log, installedVia: strat.id, version: after.version, bin: after.bin, env, platform });
157
277
  }
158
278
 
159
279
  /**
@@ -171,12 +291,18 @@ function shouldOfferRtk({ env = process.env, isTTY = !!(process.stdin && process
171
291
  module.exports = {
172
292
  setupRtkIntegration,
173
293
  rtkStatus,
294
+ locateRtk,
174
295
  pickStrategy,
175
296
  installStrategies,
297
+ timeoutFor,
298
+ isTimeout,
299
+ pathHintFor,
176
300
  doctor,
177
301
  shouldOfferRtk,
178
302
  RTK_VERSION,
179
303
  RTK_TAG,
180
304
  RTK_INSTALL_SH,
181
305
  RTK_REPO,
306
+ RTK_TIMEOUT_MS,
307
+ RTK_DEFAULT_TIMEOUT_MS,
182
308
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.29.1",
3
+ "version": "2.29.2",
4
4
  "description": "BYAN - Intelligent AI agent installer with multi-platform native support (Claude Code, Codex/OpenCode)",
5
5
  "bin": {
6
6
  "create-byan-agent": "bin/create-byan-agent-v2.js"
@@ -16,14 +16,18 @@ const { setupRtkIntegration, doctor } = require('./lib/rtk-integration');
16
16
 
17
17
  function main() {
18
18
  console.log(chalk.cyan('\nRTK token optimizer (rtk-ai/rtk) — optional native component'));
19
- console.log(chalk.gray(' Cuts dev-command tokens 60-90%; wires its own Claude Code hook.\n'));
19
+ console.log(chalk.gray(' Cuts dev-command tokens 60-90%; wires its own Claude Code hook.'));
20
+ console.log(chalk.gray(' Progress streams below. The cargo fallback compiles from source (minutes); Ctrl+C is safe.\n'));
20
21
 
21
22
  const result = setupRtkIntegration({ log: (m) => console.log(chalk.gray(' ' + m)) });
22
23
 
23
24
  if (result.synced) {
24
25
  console.log(chalk.green(`\n rtk ready (${result.installedVia}, v${result.version || '?'}). Restart Claude Code to activate.`));
26
+ if (result.pathHint) console.log(chalk.yellow(` rtk is not on your PATH — add it: ${result.pathHint}`));
25
27
  } else if (result.reason === 'no-installer') {
26
- console.log(chalk.yellow('\n No installer found (brew / cargo / curl). Install one, then re-run `npm run setup-rtk`.'));
28
+ console.log(chalk.yellow('\n No installer found (brew / curl / cargo). Install one, then re-run `npm run setup-rtk`.'));
29
+ } else if (result.pathHint) {
30
+ console.log(chalk.yellow(`\n rtk found off-PATH (${result.reason}) — add it then re-run: ${result.pathHint}`));
27
31
  } else {
28
32
  console.log(chalk.yellow(`\n rtk not wired (${result.reason}). BYAN is unaffected; you can retry later.`));
29
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.29.1",
3
+ "version": "2.29.2",
4
4
  "description": "BYAN v2.8 - Intelligent AI agent creator with ELO trust system + scientific fact-check + Hermes universal dispatcher + native Claude Code integration (hooks, skills, MCP server). Multi-platform (Claude Code, Codex). Merise Agile + TDD + 71 Mantras. ~54% LLM cost savings.",
5
5
  "main": "src/index.js",
6
6
  "bin": {