@phnx-labs/agents-cli 1.22.88 → 1.22.90

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.
@@ -104,18 +104,22 @@ export declare function resolveMultiInstallInventory(runningRoot: string, runnin
104
104
  /**
105
105
  * The on-disk package root of the copy that is currently running.
106
106
  *
107
- * For a plain JS install this is just `<__dirname>/..`. Under the compiled
108
- * standalone binary (shipped since 1.20.53) it is not: Bun sets `__dirname` to
109
- * its embedded virtual FS, so `<__dirname>/..` yields `/$bunfs` — a path that
110
- * exists nowhere. That phantom value was reported as a second install by the
111
- * multi-install check, and rejected by deriveGlobalPrefix as "not an
112
- * npm-managed install", so every self-upgrade from a compiled copy failed.
107
+ * For a plain JS install, walk up from the CALLING module's directory to the
108
+ * directory whose package.json names this package. Never assume a fixed depth:
109
+ * `<__dirname>/..` is the root only for a module directly under `dist/`, and
110
+ * from `dist/lib/daemon/self-update-service.js` it answered `dist/lib`, which
111
+ * deriveGlobalPrefix rejected as "not an npm-managed install" on every daemon
112
+ * self-update tick (2026-09-07 fleet incident).
113
113
  *
114
- * The physical executable is `process.execPath`, which ships inside the
115
- * package (`<packageRoot>/dist/bin/agents`). Walk up from it to the directory
116
- * whose package.json actually names this package rather than assuming a fixed
117
- * depth, so a change to the dist layout surfaces as a clear throw here instead
118
- * of a wrong prefix that npm would happily install into.
114
+ * Under the compiled standalone binary (shipped since 1.20.53) `__dirname` is
115
+ * Bun's embedded virtual FS, so walking up from it yields `/$bunfs` — a path
116
+ * that exists nowhere. That phantom value was reported as a second install by
117
+ * the multi-install check and rejected by deriveGlobalPrefix, so every
118
+ * self-upgrade from a compiled copy failed. The physical executable is
119
+ * `process.execPath`, which ships inside the package
120
+ * (`<packageRoot>/dist/bin/agents`); walk up from it instead. Either way a
121
+ * change to the dist layout surfaces as a clear throw here rather than a wrong
122
+ * prefix that npm would happily install into.
119
123
  */
120
124
  export declare function resolveRunningPackageRoot(dirname: string, execPath?: string): string;
121
125
  /**
@@ -175,8 +179,30 @@ export declare function installPackageIntoPrefix(spec: string, prefix: string, s
175
179
  * alias shims afterwards via refreshAliasShims() rather than relying on the
176
180
  * package's postinstall hook.
177
181
  *
182
+ * Unlike npm's arborist (see {@link sweepStaleInstallStaging}: retire-rename,
183
+ * then one final rename), bun's write into the package directory is NOT known
184
+ * to be atomic — files may land incrementally. Anything that trusts a version
185
+ * bump on disk written by ANOTHER process (the daemon's stale-install relaunch
186
+ * in `daemon/self-update-service.ts`) must therefore gate on
187
+ * {@link installLooksSettled} rather than on the version alone.
188
+ *
178
189
  * `signal` behaves exactly as documented on {@link installPackageIntoPrefix}.
179
190
  */
191
+ /** How long an install's package.json must have been at rest before a foreign version bump is trusted. */
192
+ export declare const INSTALL_SETTLE_MS = 60000;
193
+ /**
194
+ * True when the install at `packageRoot` looks complete: its package.json has
195
+ * not been modified for at least `settleMs`, and every `bin` entry it declares
196
+ * exists on disk. This is the guard for trusting a version bump that a
197
+ * DIFFERENT process wrote (an operator's `agents` auto-update, `agents
198
+ * upgrade`, the installer) without re-running that process's own verification:
199
+ * npm's reify is an atomic rename, but bun's is not (see
200
+ * {@link installPackageWithBun}), so a reader can catch bun mid-extraction —
201
+ * package.json present, `dist/` still landing. An install finishes in seconds;
202
+ * a minute of quiet plus the bin entry present rules that window out. Any
203
+ * read error means "not settled".
204
+ */
205
+ export declare function installLooksSettled(packageRoot: string, settleMs?: number, now?: number): boolean;
180
206
  export declare function installPackageWithBun(spec: string, signal?: AbortSignal): Promise<void>;
181
207
  /**
182
208
  * Verify a downloaded tarball's bytes against a Subresource Integrity (SRI)
@@ -240,37 +240,59 @@ function isPackageRoot(dir) {
240
240
  /**
241
241
  * The on-disk package root of the copy that is currently running.
242
242
  *
243
- * For a plain JS install this is just `<__dirname>/..`. Under the compiled
244
- * standalone binary (shipped since 1.20.53) it is not: Bun sets `__dirname` to
245
- * its embedded virtual FS, so `<__dirname>/..` yields `/$bunfs` — a path that
246
- * exists nowhere. That phantom value was reported as a second install by the
247
- * multi-install check, and rejected by deriveGlobalPrefix as "not an
248
- * npm-managed install", so every self-upgrade from a compiled copy failed.
243
+ * For a plain JS install, walk up from the CALLING module's directory to the
244
+ * directory whose package.json names this package. Never assume a fixed depth:
245
+ * `<__dirname>/..` is the root only for a module directly under `dist/`, and
246
+ * from `dist/lib/daemon/self-update-service.js` it answered `dist/lib`, which
247
+ * deriveGlobalPrefix rejected as "not an npm-managed install" on every daemon
248
+ * self-update tick (2026-09-07 fleet incident).
249
249
  *
250
- * The physical executable is `process.execPath`, which ships inside the
251
- * package (`<packageRoot>/dist/bin/agents`). Walk up from it to the directory
252
- * whose package.json actually names this package rather than assuming a fixed
253
- * depth, so a change to the dist layout surfaces as a clear throw here instead
254
- * of a wrong prefix that npm would happily install into.
250
+ * Under the compiled standalone binary (shipped since 1.20.53) `__dirname` is
251
+ * Bun's embedded virtual FS, so walking up from it yields `/$bunfs` — a path
252
+ * that exists nowhere. That phantom value was reported as a second install by
253
+ * the multi-install check and rejected by deriveGlobalPrefix, so every
254
+ * self-upgrade from a compiled copy failed. The physical executable is
255
+ * `process.execPath`, which ships inside the package
256
+ * (`<packageRoot>/dist/bin/agents`); walk up from it instead. Either way a
257
+ * change to the dist layout surfaces as a clear throw here rather than a wrong
258
+ * prefix that npm would happily install into.
255
259
  */
256
260
  export function resolveRunningPackageRoot(dirname, execPath = process.execPath) {
257
- const fromDirname = path.resolve(dirname, '..');
258
- if (!isBunVirtualPath(fromDirname))
259
- return fromDirname;
261
+ if (!isBunVirtualPath(dirname)) {
262
+ // Walk up from the CALLING module's directory to the package.json that
263
+ // names this package. The previous `path.resolve(dirname, '..')` was only
264
+ // right for a module one level below the root (dist/bootstrap.js); from
265
+ // dist/lib/daemon/self-update-service.js it answered `dist/lib`, so
266
+ // `deriveGlobalPrefix` threw "not an npm-managed install" on every daemon
267
+ // self-update tick, fleet-wide, and no daemon ever relaunched onto a
268
+ // release (2026-09-07: eight workers still running 1.22.79 code four
269
+ // releases later).
270
+ const found = findPackageRootAbove(dirname);
271
+ if (found)
272
+ return found;
273
+ throw new Error(`Cannot locate the running agents-cli install: no ${NPM_PACKAGE_NAME} package.json above ` +
274
+ `${dirname}. Reinstall with: npm install -g ${NPM_PACKAGE_NAME}`);
275
+ }
260
276
  if (!execPath || isBunVirtualPath(execPath)) {
261
- throw new Error(`Cannot locate the running agents-cli install: __dirname is the Bun virtual path ${fromDirname} ` +
277
+ throw new Error(`Cannot locate the running agents-cli install: __dirname is the Bun virtual path ${dirname} ` +
262
278
  `and process.execPath (${execPath || '(empty)'}) is not a real file. ` +
263
279
  `Reinstall with: npm install -g ${NPM_PACKAGE_NAME}`);
264
280
  }
265
- let dir = path.dirname(path.resolve(execPath));
281
+ const found = findPackageRootAbove(path.dirname(path.resolve(execPath)));
282
+ if (found)
283
+ return found;
284
+ throw new Error(`Cannot locate the running agents-cli install: no ${NPM_PACKAGE_NAME} package.json above ` +
285
+ `${execPath}. Reinstall with: npm install -g ${NPM_PACKAGE_NAME}`);
286
+ }
287
+ /** Nearest ancestor of `start` (inclusive) whose package.json names this package, or null. */
288
+ function findPackageRootAbove(start) {
289
+ let dir = path.resolve(start);
266
290
  for (;;) {
267
291
  if (isPackageRoot(dir))
268
292
  return dir;
269
293
  const parent = path.dirname(dir);
270
- if (parent === dir) {
271
- throw new Error(`Cannot locate the running agents-cli install: no ${NPM_PACKAGE_NAME} package.json above ` +
272
- `${execPath}. Reinstall with: npm install -g ${NPM_PACKAGE_NAME}`);
273
- }
294
+ if (parent === dir)
295
+ return null;
274
296
  dir = parent;
275
297
  }
276
298
  }
@@ -376,8 +398,43 @@ export async function installPackageIntoPrefix(spec, prefix, signal) {
376
398
  * alias shims afterwards via refreshAliasShims() rather than relying on the
377
399
  * package's postinstall hook.
378
400
  *
401
+ * Unlike npm's arborist (see {@link sweepStaleInstallStaging}: retire-rename,
402
+ * then one final rename), bun's write into the package directory is NOT known
403
+ * to be atomic — files may land incrementally. Anything that trusts a version
404
+ * bump on disk written by ANOTHER process (the daemon's stale-install relaunch
405
+ * in `daemon/self-update-service.ts`) must therefore gate on
406
+ * {@link installLooksSettled} rather than on the version alone.
407
+ *
379
408
  * `signal` behaves exactly as documented on {@link installPackageIntoPrefix}.
380
409
  */
410
+ /** How long an install's package.json must have been at rest before a foreign version bump is trusted. */
411
+ export const INSTALL_SETTLE_MS = 60_000;
412
+ /**
413
+ * True when the install at `packageRoot` looks complete: its package.json has
414
+ * not been modified for at least `settleMs`, and every `bin` entry it declares
415
+ * exists on disk. This is the guard for trusting a version bump that a
416
+ * DIFFERENT process wrote (an operator's `agents` auto-update, `agents
417
+ * upgrade`, the installer) without re-running that process's own verification:
418
+ * npm's reify is an atomic rename, but bun's is not (see
419
+ * {@link installPackageWithBun}), so a reader can catch bun mid-extraction —
420
+ * package.json present, `dist/` still landing. An install finishes in seconds;
421
+ * a minute of quiet plus the bin entry present rules that window out. Any
422
+ * read error means "not settled".
423
+ */
424
+ export function installLooksSettled(packageRoot, settleMs = INSTALL_SETTLE_MS, now = Date.now()) {
425
+ try {
426
+ const pkgJsonPath = path.join(packageRoot, 'package.json');
427
+ const stat = fs.statSync(pkgJsonPath);
428
+ if (now - stat.mtimeMs < settleMs)
429
+ return false;
430
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
431
+ const bins = typeof pkg.bin === 'string' ? [pkg.bin] : Object.values(pkg.bin ?? {});
432
+ return bins.every((rel) => fs.existsSync(path.join(packageRoot, rel)));
433
+ }
434
+ catch {
435
+ return false;
436
+ }
437
+ }
381
438
  export async function installPackageWithBun(spec, signal) {
382
439
  const { execFile } = await import('child_process');
383
440
  const { promisify } = await import('util');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.88",
3
+ "version": "1.22.90",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",