@phnx-labs/agents-cli 1.20.21 → 1.20.22

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.
@@ -12,6 +12,31 @@
12
12
  * to PATH resolution.
13
13
  */
14
14
  export declare const NPM_PACKAGE_NAME = "@phnx-labs/agents-cli";
15
+ export type PackageManager = 'npm' | 'bun';
16
+ /**
17
+ * The directory bun installs global packages into:
18
+ * <BUN_INSTALL>/install/global (BUN_INSTALL defaults to ~/.bun)
19
+ *
20
+ * A globally-installed scoped package then lives at
21
+ * `<bunGlobalDir>/node_modules/@phnx-labs/agents-cli` — note there is NO `lib`
22
+ * segment, unlike npm's POSIX layout. That single difference is why an
23
+ * npm-based upgrade silently misses a bun install (see deriveGlobalPrefix).
24
+ */
25
+ export declare function bunGlobalDir(): string;
26
+ /**
27
+ * Identify which package manager owns the install at `packageRoot`, so the
28
+ * upgrade can shell out to the one that actually replaces this copy.
29
+ *
30
+ * bun lays a global package out as `<bunGlobalDir>/node_modules/<scoped pkg>`,
31
+ * so the prefix (the parent of `node_modules`) is the bun global dir itself.
32
+ * Everything else — npm's `<prefix>/lib/node_modules` and the Windows
33
+ * `<prefix>/node_modules` — is treated as npm.
34
+ *
35
+ * Detection is path-based (no subprocess): it matches the resolved bun global
36
+ * dir from BUN_INSTALL/$HOME, and falls back to the structural `.bun/install/
37
+ * global` tail for a relocated BUN_INSTALL not exported into this process.
38
+ */
39
+ export declare function detectPackageManager(packageRoot: string): PackageManager;
15
40
  export interface UpdateCheckCache {
16
41
  lastCheck: number;
17
42
  latestVersion: string;
@@ -49,6 +74,15 @@ export declare function deriveGlobalPrefix(packageRoot: string): string;
49
74
  * refreshAliasShims().
50
75
  */
51
76
  export declare function installPackageIntoPrefix(spec: string, prefix: string): Promise<void>;
77
+ /**
78
+ * Install `spec` into bun's global store with `bun add -g`. bun writes to
79
+ * `<bunGlobalDir>/node_modules/<pkg>`, which is exactly the running package
80
+ * root for a bun install — so verifyInstalledVersion() sees the new version
81
+ * in place. bun skips untrusted lifecycle scripts, so the caller refreshes
82
+ * alias shims afterwards via refreshAliasShims() rather than relying on the
83
+ * package's postinstall hook.
84
+ */
85
+ export declare function installPackageWithBun(spec: string): Promise<void>;
52
86
  /** Read the version field of the package.json at `packageRoot`, fresh from disk. */
53
87
  export declare function readInstalledVersion(packageRoot: string): string;
54
88
  /**
@@ -12,10 +12,55 @@
12
12
  * to PATH resolution.
13
13
  */
14
14
  import * as fs from 'fs';
15
+ import * as os from 'os';
15
16
  import * as path from 'path';
16
17
  import { spawnSync } from 'child_process';
17
18
  import { compareVersions } from './versions.js';
18
19
  export const NPM_PACKAGE_NAME = '@phnx-labs/agents-cli';
20
+ /**
21
+ * The directory bun installs global packages into:
22
+ * <BUN_INSTALL>/install/global (BUN_INSTALL defaults to ~/.bun)
23
+ *
24
+ * A globally-installed scoped package then lives at
25
+ * `<bunGlobalDir>/node_modules/@phnx-labs/agents-cli` — note there is NO `lib`
26
+ * segment, unlike npm's POSIX layout. That single difference is why an
27
+ * npm-based upgrade silently misses a bun install (see deriveGlobalPrefix).
28
+ */
29
+ export function bunGlobalDir() {
30
+ const bunInstall = process.env.BUN_INSTALL || path.join(os.homedir(), '.bun');
31
+ return path.join(bunInstall, 'install', 'global');
32
+ }
33
+ /**
34
+ * Identify which package manager owns the install at `packageRoot`, so the
35
+ * upgrade can shell out to the one that actually replaces this copy.
36
+ *
37
+ * bun lays a global package out as `<bunGlobalDir>/node_modules/<scoped pkg>`,
38
+ * so the prefix (the parent of `node_modules`) is the bun global dir itself.
39
+ * Everything else — npm's `<prefix>/lib/node_modules` and the Windows
40
+ * `<prefix>/node_modules` — is treated as npm.
41
+ *
42
+ * Detection is path-based (no subprocess): it matches the resolved bun global
43
+ * dir from BUN_INSTALL/$HOME, and falls back to the structural `.bun/install/
44
+ * global` tail for a relocated BUN_INSTALL not exported into this process.
45
+ */
46
+ export function detectPackageManager(packageRoot) {
47
+ const resolved = path.resolve(packageRoot);
48
+ const prefix = path.dirname(path.dirname(path.dirname(resolved))); // strip <scope>/<pkg>/node_modules
49
+ if (prefix === path.resolve(bunGlobalDir()))
50
+ return 'bun';
51
+ const parts = prefix.split(path.sep);
52
+ const n = parts.length;
53
+ if (n >= 3 && parts[n - 1] === 'global' && parts[n - 2] === 'install' && parts[n - 3] === '.bun') {
54
+ return 'bun';
55
+ }
56
+ return 'npm';
57
+ }
58
+ /** The shell command a user can run by hand to reproduce the upgrade for `manager`. */
59
+ function manualInstallHint(manager, packageRoot, spec) {
60
+ if (manager === 'bun')
61
+ return `bun add -g ${spec}`;
62
+ return `npm install -g --prefix ${deriveGlobalPrefix(packageRoot)} ${spec}`;
63
+ }
19
64
  /** Read the cached update-check state from disk. Returns null if the file is missing or corrupt. */
20
65
  export function readUpdateCache(file) {
21
66
  try {
@@ -102,6 +147,20 @@ export async function installPackageIntoPrefix(spec, prefix) {
102
147
  const execFileAsync = promisify(execFile);
103
148
  await execFileAsync('npm', ['install', '-g', '--prefix', prefix, spec, '--ignore-scripts']);
104
149
  }
150
+ /**
151
+ * Install `spec` into bun's global store with `bun add -g`. bun writes to
152
+ * `<bunGlobalDir>/node_modules/<pkg>`, which is exactly the running package
153
+ * root for a bun install — so verifyInstalledVersion() sees the new version
154
+ * in place. bun skips untrusted lifecycle scripts, so the caller refreshes
155
+ * alias shims afterwards via refreshAliasShims() rather than relying on the
156
+ * package's postinstall hook.
157
+ */
158
+ export async function installPackageWithBun(spec) {
159
+ const { execFile } = await import('child_process');
160
+ const { promisify } = await import('util');
161
+ const execFileAsync = promisify(execFile);
162
+ await execFileAsync('bun', ['add', '-g', spec]);
163
+ }
105
164
  /** Read the version field of the package.json at `packageRoot`, fresh from disk. */
106
165
  export function readInstalledVersion(packageRoot) {
107
166
  return JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8')).version;
@@ -113,8 +172,10 @@ export function readInstalledVersion(packageRoot) {
113
172
  export function verifyInstalledVersion(packageRoot, expectedVersion) {
114
173
  const actual = readInstalledVersion(packageRoot);
115
174
  if (actual !== expectedVersion) {
116
- throw new Error(`npm reported success but ${packageRoot} is still ${actual} (expected ${expectedVersion}). ` +
117
- `Run manually: npm install -g --prefix ${deriveGlobalPrefix(packageRoot)} ${NPM_PACKAGE_NAME}@${expectedVersion}`);
175
+ const manager = detectPackageManager(packageRoot);
176
+ const hint = manualInstallHint(manager, packageRoot, `${NPM_PACKAGE_NAME}@${expectedVersion}`);
177
+ throw new Error(`the package manager reported success but ${packageRoot} is still ${actual} (expected ${expectedVersion}). ` +
178
+ `Run manually: ${hint}`);
118
179
  }
119
180
  }
120
181
  /**
@@ -5,6 +5,7 @@
5
5
  * configuration schemas, resource tracking, registry types, and permission
6
6
  * formats for each supported agent.
7
7
  */
8
+ import type { CloudProviderId } from './cloud/types.js';
8
9
  /** Unique identifier for a supported AI coding agent. */
9
10
  export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'roo' | 'antigravity' | 'grok' | 'kimi' | 'droid';
10
11
  /** How `agents run <agent>` chooses an installed version when none is pinned. */
@@ -83,6 +84,13 @@ export interface AgentConfig {
83
84
  variableSyntax: string;
84
85
  supportsHooks: boolean;
85
86
  nativeAgentsSkillsDir?: boolean;
87
+ /**
88
+ * This agent's *own* cloud backend. `agents cloud run --agent <id>` routes
89
+ * here when no `--provider` is given (precedence: --provider > this >
90
+ * cloud.default_provider > rush). Undefined means the agent has no native
91
+ * cloud and falls back to the configured default.
92
+ */
93
+ cloudProvider?: CloudProviderId;
86
94
  capabilities: {
87
95
  hooks: Capability;
88
96
  mcp: Capability;
@@ -5,3 +5,14 @@
5
5
  * get stale behavior without this check.
6
6
  */
7
7
  export declare function getCliVersion(): string;
8
+ /**
9
+ * Read the version from package.json on disk every call, bypassing the cache.
10
+ *
11
+ * `getCliVersion()` memoizes the version a long-running process *started* with.
12
+ * After `npm i -g` overwrites the install in place, the on-disk package.json
13
+ * changes but the running process keeps its old in-memory code. Comparing this
14
+ * fresh read against the cached startup value is how a daemon/broker detects it
15
+ * is now stale and should reload onto the new code (self-healing). Returns
16
+ * 'unknown' on any error.
17
+ */
18
+ export declare function getCliVersionFresh(): string;
@@ -23,3 +23,23 @@ export function getCliVersion() {
23
23
  }
24
24
  return cached;
25
25
  }
26
+ /**
27
+ * Read the version from package.json on disk every call, bypassing the cache.
28
+ *
29
+ * `getCliVersion()` memoizes the version a long-running process *started* with.
30
+ * After `npm i -g` overwrites the install in place, the on-disk package.json
31
+ * changes but the running process keeps its old in-memory code. Comparing this
32
+ * fresh read against the cached startup value is how a daemon/broker detects it
33
+ * is now stale and should reload onto the new code (self-healing). Returns
34
+ * 'unknown' on any error.
35
+ */
36
+ export function getCliVersionFresh() {
37
+ try {
38
+ const pkgPath = path.join(__dirname, '..', '..', 'package.json');
39
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
40
+ return String(pkg.version || 'unknown');
41
+ }
42
+ catch {
43
+ return 'unknown';
44
+ }
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.21",
3
+ "version": "1.20.22",
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",
@@ -25,6 +25,7 @@
25
25
  "dist/**/*.d.ts",
26
26
  "dist/**/*.json",
27
27
  "dist/lib/secrets/Agents CLI.app/**",
28
+ "dist/lib/menubar/MenubarHelper.app/**",
28
29
  "scripts/postinstall.js",
29
30
  "scripts/install-helper.js",
30
31
  "CHANGELOG.md",
@@ -43,7 +44,7 @@
43
44
  "url": "https://github.com/phnx-labs/agents-cli/issues"
44
45
  },
45
46
  "scripts": {
46
- "build": "tsc && rm -rf 'dist/lib/secrets/AgentsKeychain.app' 'dist/lib/secrets/Agents CLI.app' && ([ \"$(uname)\" = \"Darwin\" ] && cp -R 'bin/Agents CLI.app' 'dist/lib/secrets/Agents CLI.app' || true)",
47
+ "build": "tsc && rm -rf 'dist/lib/secrets/AgentsKeychain.app' 'dist/lib/secrets/Agents CLI.app' 'dist/lib/menubar/MenubarHelper.app' && ([ \"$(uname)\" = \"Darwin\" ] && cp -R 'bin/Agents CLI.app' 'dist/lib/secrets/Agents CLI.app' || true) && ([ \"$(uname)\" = \"Darwin\" ] && [ -d 'bin/MenubarHelper.app' ] && mkdir -p 'dist/lib/menubar' && cp -R 'bin/MenubarHelper.app' 'dist/lib/menubar/MenubarHelper.app' || true)",
47
48
  "prepack": "scripts/verify-keychain-helper.sh",
48
49
  "postinstall": "node scripts/postinstall.js",
49
50
  "dev": "tsx src/index.ts",
@@ -239,6 +239,8 @@ To enable version-aware shims, add this to your shell config:
239
239
  console.log(` Installed shorthands: ${written.join(', ')}`);
240
240
  }
241
241
 
242
+ await healLongRunningProcesses();
243
+
242
244
  const version = getVersion();
243
245
  if (version) {
244
246
  const section = getChangelogSection(version);
@@ -250,6 +252,39 @@ To enable version-aware shims, add this to your shell config:
250
252
  }
251
253
  }
252
254
 
255
+ /**
256
+ * Self-heal long-running processes onto the just-installed code (macOS).
257
+ *
258
+ * The root cause behind stale-behavior bugs is a daemon/broker that keeps
259
+ * running pre-upgrade code for days. An in-place `npm i -g` swaps the files but
260
+ * not the running processes — so we bounce them here, the one moment we know the
261
+ * code just changed. Best-effort and non-fatal: a failure must never break the
262
+ * install. Skipped in CI and when AGENTS_NO_HEAL=1.
263
+ */
264
+ async function healLongRunningProcesses() {
265
+ if (process.platform !== 'darwin') return;
266
+ if (process.env.CI || process.env.AGENTS_NO_HEAL === '1') return;
267
+ // Routines daemon: restart so it reloads new code (e.g. picks up keychain
268
+ // read-memoization / broker fast-path that a stale daemon wouldn't have).
269
+ try {
270
+ const d = await import('../dist/lib/daemon.js');
271
+ if (d.isDaemonRunning?.()) {
272
+ d.stopDaemon?.();
273
+ d.startDaemon?.();
274
+ console.log(' Restarted the routines daemon onto this version.');
275
+ }
276
+ } catch { /* best effort */ }
277
+ // Persistent secrets-agent broker: kickstart so launchd relaunches it on the
278
+ // new code. No-op if the service isn't installed; never blocks.
279
+ try {
280
+ const a = await import('../dist/lib/secrets/agent.js');
281
+ if (a.secretsAgentServiceInstalled?.()) {
282
+ a.kickstartSecretsAgentService?.();
283
+ console.log(' Reloaded the secrets-agent service onto this version.');
284
+ }
285
+ } catch { /* best effort */ }
286
+ }
287
+
253
288
  main().catch((err) => {
254
289
  console.error(err);
255
290
  process.exit(0);