@phnx-labs/agents-cli 1.22.78 → 1.22.80

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.
Files changed (90) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +28 -1
  3. package/dist/bootstrap.js +40 -12
  4. package/dist/commands/accounts.d.ts +22 -0
  5. package/dist/commands/accounts.js +158 -35
  6. package/dist/commands/config.js +37 -0
  7. package/dist/commands/exec.js +21 -10
  8. package/dist/commands/update.js +169 -18
  9. package/dist/commands/versions.d.ts +11 -0
  10. package/dist/commands/versions.js +30 -4
  11. package/dist/commands/view.d.ts +69 -3
  12. package/dist/commands/view.js +235 -75
  13. package/dist/index.js +35 -2
  14. package/dist/lib/account-catalog.d.ts +97 -1
  15. package/dist/lib/account-catalog.js +134 -6
  16. package/dist/lib/account-registry.d.ts +43 -0
  17. package/dist/lib/account-registry.js +97 -2
  18. package/dist/lib/accounting/rotate.d.ts +2 -2
  19. package/dist/lib/accounting/rotate.js +5 -5
  20. package/dist/lib/accounts/auth-operation-lock.d.ts +9 -0
  21. package/dist/lib/accounts/auth-operation-lock.js +55 -0
  22. package/dist/lib/accounts/connect.d.ts +170 -0
  23. package/dist/lib/accounts/connect.js +383 -0
  24. package/dist/lib/capabilities.js +2 -0
  25. package/dist/lib/commands.js +2 -0
  26. package/dist/lib/config-keys.d.ts +11 -2
  27. package/dist/lib/config-keys.js +21 -1
  28. package/dist/lib/daemon/daemon.js +5 -0
  29. package/dist/lib/daemon/harness-update-service.d.ts +110 -0
  30. package/dist/lib/daemon/harness-update-service.js +216 -0
  31. package/dist/lib/daemon-services.d.ts +1 -1
  32. package/dist/lib/daemon-services.js +5 -0
  33. package/dist/lib/device-config.d.ts +0 -1
  34. package/dist/lib/device-config.js +50 -5
  35. package/dist/lib/exec.js +26 -2
  36. package/dist/lib/fs-atomic.d.ts +2 -0
  37. package/dist/lib/fs-atomic.js +2 -0
  38. package/dist/lib/hooks/install.js +7 -2
  39. package/dist/lib/installations/active-check.d.ts +48 -0
  40. package/dist/lib/installations/active-check.js +84 -0
  41. package/dist/lib/installations/index.d.ts +5 -1
  42. package/dist/lib/installations/index.js +4 -0
  43. package/dist/lib/installations/installation-lock.d.ts +6 -0
  44. package/dist/lib/installations/installation-lock.js +29 -0
  45. package/dist/lib/installations/launch-gate.d.ts +69 -0
  46. package/dist/lib/installations/launch-gate.js +133 -0
  47. package/dist/lib/installations/native-command.d.ts +5 -0
  48. package/dist/lib/installations/native-command.js +52 -0
  49. package/dist/lib/installations/shims.d.ts +8 -2
  50. package/dist/lib/installations/shims.js +105 -2
  51. package/dist/lib/installations/store.d.ts +5 -1
  52. package/dist/lib/installations/store.js +24 -3
  53. package/dist/lib/installations/strategies.js +55 -35
  54. package/dist/lib/installations/types.d.ts +17 -0
  55. package/dist/lib/installations/update-cancellation.d.ts +82 -0
  56. package/dist/lib/installations/update-cancellation.js +122 -0
  57. package/dist/lib/installations/update-policy.d.ts +69 -0
  58. package/dist/lib/installations/update-policy.js +114 -0
  59. package/dist/lib/installations/update-runtime.d.ts +118 -0
  60. package/dist/lib/installations/update-runtime.js +321 -0
  61. package/dist/lib/installations/update.d.ts +25 -0
  62. package/dist/lib/installations/update.js +141 -2
  63. package/dist/lib/installations/versions.d.ts +1 -0
  64. package/dist/lib/installations/versions.js +166 -131
  65. package/dist/lib/platform/process.d.ts +3 -1
  66. package/dist/lib/platform/process.js +2 -2
  67. package/dist/lib/staleness/detectors/commands.d.ts +1 -2
  68. package/dist/lib/staleness/detectors/hooks.d.ts +1 -2
  69. package/dist/lib/staleness/detectors/mcp.d.ts +1 -2
  70. package/dist/lib/staleness/detectors/permissions.d.ts +1 -2
  71. package/dist/lib/staleness/detectors/plugins.d.ts +1 -7
  72. package/dist/lib/staleness/detectors/rules.d.ts +1 -2
  73. package/dist/lib/staleness/detectors/skills.d.ts +1 -2
  74. package/dist/lib/staleness/detectors/subagents.d.ts +1 -7
  75. package/dist/lib/staleness/detectors/workflows.d.ts +1 -2
  76. package/dist/lib/staleness/writers/commands.d.ts +1 -2
  77. package/dist/lib/staleness/writers/hooks.d.ts +1 -2
  78. package/dist/lib/staleness/writers/mcp.d.ts +1 -2
  79. package/dist/lib/staleness/writers/permissions.d.ts +1 -12
  80. package/dist/lib/staleness/writers/plugins.d.ts +1 -6
  81. package/dist/lib/staleness/writers/rules.d.ts +1 -2
  82. package/dist/lib/staleness/writers/skills.d.ts +1 -2
  83. package/dist/lib/staleness/writers/subagents.d.ts +1 -2
  84. package/dist/lib/staleness/writers/workflows.d.ts +1 -8
  85. package/dist/lib/state.d.ts +3 -1
  86. package/dist/lib/state.js +38 -13
  87. package/dist/lib/types.d.ts +26 -1
  88. package/dist/lib/types.js +5 -0
  89. package/dist/lib/view-types.d.ts +6 -0
  90. package/package.json +1 -1
@@ -22,7 +22,7 @@ import { discoverPermissionGroups, getActivePermissionPresetName, readPermission
22
22
  import { parseMcpConfigForScan, isProjectMcpTrusted } from '../mcp.js';
23
23
  import { createVersionedAlias, removeVersionedAlias, switchConfigSymlink, getConfigSymlinkVersion, ensureClaudeInsideSymlink, assertIsolationBoundary, isIsolationProtected, } from './shims.js';
24
24
  import { importInstallScriptBinary } from '../import.js';
25
- import { createInstallation, getBinaryPath, getCliVersionFromPath, getGlobalDefault, getIsolatedDefault, getLiveVersion, getVersionDir, getVersionHomePath, invalidateInstalledVersionsCache, invalidateLiveVersionCache, isGlobalBinaryAgent, isVersionInstalled, isVersionIsolated, listInstalledVersions, pickCanonicalGlobalBinaryVersion, resolveGrokFallbackBinary, resolveVersion, } from './store.js';
25
+ import { createInstallation, readInstallation, getBinaryPath, getCliVersionFromPath, getGlobalDefault, getIsolatedDefault, getLiveVersion, getVersionDir, getVersionHomePath, invalidateInstalledVersionsCache, invalidateLiveVersionCache, isGlobalBinaryAgent, isVersionInstalled, isVersionIsolated, listInstalledVersions, pickCanonicalGlobalBinaryVersion, resolveGrokFallbackBinary, resolveVersion, } from './store.js';
26
26
  export * from './store.js';
27
27
  import { INSTALLATION_RECORD_FILE } from './types.js';
28
28
  import { composeWin32CommandLine } from '../platform/index.js';
@@ -34,6 +34,9 @@ import { discoverPlugins, syncPluginToVersion, pluginSupportsAgent, cleanOrphane
34
34
  import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from '../staleness/index.js';
35
35
  import { pruneRemovedResources } from '../staleness/prune.js';
36
36
  import { emit } from '../feed/events.js';
37
+ import { withFileLockAsync } from '../fs-atomic.js';
38
+ import { installationLockTarget, INSTALLATION_LOCK_OPTIONS } from './installation-lock.js';
39
+ import { isInstallationLikelyActive } from './active-check.js';
37
40
  import { safeJoin } from '../paths.js';
38
41
  import { readSkillSourceCommandMarker, shouldAlsoInstallCommandAsSkill, shouldInstallCommandAsSkill, } from '../command-skills.js';
39
42
  import { getWriter, getDetector } from '../staleness/registry.js';
@@ -1031,7 +1034,8 @@ async function checkGrokAccountCollision(installedVersion) {
1031
1034
  /** Install a specific version of an agent. */
1032
1035
  export async function installVersion(agent, version, onProgress, opts) {
1033
1036
  const agentConfig = AGENTS[agent];
1034
- const requestedLabel = version;
1037
+ const requestedLabel = opts?.installationLabel ?? version;
1038
+ const initialUpdatePolicy = version === 'latest' ? 'latest' : 'pinned';
1035
1039
  if (isAgentHardDeprecated(agent)) {
1036
1040
  return { success: false, installedVersion: version, error: hardDeprecationError(agent) };
1037
1041
  }
@@ -1039,6 +1043,20 @@ export async function installVersion(agent, version, onProgress, opts) {
1039
1043
  if (!VERSION_RE.test(version)) {
1040
1044
  throw new Error(`Invalid version: ${JSON.stringify(version)}`);
1041
1045
  }
1046
+ // A caller-supplied installation label decouples the addressable version-dir
1047
+ // name from the vendor release (PHNX-3940): `agents accounts connect` mints an
1048
+ // opaque stable label so ten accounts can share the same upstream `latest`
1049
+ // under distinct isolated homes, each with its own native login. It follows
1050
+ // the same identity/release split the installScript branch already uses via
1051
+ // `requestedLabel`, and must be a real label, never a release alias.
1052
+ if (opts?.installationLabel !== undefined) {
1053
+ if (!VERSION_RE.test(opts.installationLabel)) {
1054
+ throw new Error(`Invalid installation label: ${JSON.stringify(opts.installationLabel)}`);
1055
+ }
1056
+ if (opts.installationLabel === 'latest' || opts.installationLabel === 'oldest') {
1057
+ throw new Error(`Installation label cannot be a release alias ('${opts.installationLabel}').`);
1058
+ }
1059
+ }
1042
1060
  if (!agentConfig.npmPackage) {
1043
1061
  if (!agentConfig.installScript) {
1044
1062
  return { success: false, installedVersion: version, error: 'Agent has no npm package' };
@@ -1149,7 +1167,7 @@ export async function installVersion(agent, version, onProgress, opts) {
1149
1167
  // Freeze this installation's identity. The dir name is its stable label from
1150
1168
  // here on; the release it carries is recorded separately so `agents update`
1151
1169
  // can move the release without invalidating any reference to the label.
1152
- createInstallation(agent, installationLabel, releaseVersion);
1170
+ createInstallation(agent, installationLabel, releaseVersion, initialUpdatePolicy);
1153
1171
  const trackerInstall = await installSessionTrackerHook(agent, installationLabel);
1154
1172
  if (!trackerInstall.installed && trackerInstall.error) {
1155
1173
  console.warn(`agents: SessionStart hook not installed for ${agent}@${installationLabel}: ${trackerInstall.error}`);
@@ -1183,142 +1201,158 @@ export async function installVersion(agent, version, onProgress, opts) {
1183
1201
  }
1184
1202
  version = resolved;
1185
1203
  }
1204
+ // `version` is now the concrete vendor release. The addressable slot is the
1205
+ // caller's opaque installation label when given (connect), else the release
1206
+ // itself — the identity/release split (PHNX-3940). Everything on disk (dir,
1207
+ // alias, record, tracker) keys on `label`; only the npm spec uses `release`.
1208
+ const releaseVersion = version;
1209
+ const label = opts?.installationLabel ?? releaseVersion;
1186
1210
  ensureAgentsDir();
1187
- const versionDir = getVersionDir(agent, version);
1188
- // A `clean` (repair) reinstall wipes a possibly partially-extracted
1189
- // node_modules first. npm treats a present-but-gutted platform package (its
1190
- // package.json landed, its vendored native binary did not) as already
1191
- // installed and would skip re-fetching it — so without this the corrupt
1192
- // vendor/ survives the reinstall and the ENOENT persists. home/ is preserved.
1193
- if (opts?.clean && fs.existsSync(versionDir)) {
1194
- removeInstallArtifacts(versionDir);
1195
- }
1196
- // Create version directory and isolated home
1197
- fs.mkdirSync(versionDir, { recursive: true });
1198
- fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
1199
- // Initialize package.json (only for real npm agents)
1200
- const packageJson = {
1201
- name: `agents-${agent}-${version}`,
1202
- version: '1.0.0',
1203
- private: true,
1204
- };
1205
- fs.writeFileSync(path.join(versionDir, 'package.json'), JSON.stringify(packageJson, null, 2));
1206
- // Install the package. `version` is always concrete here (`latest`/`oldest`
1207
- // were resolved above), so the spec is always pinned. The `@` prefix is
1208
- // load-bearing: it ensures `version` (which VERSION_RE permits to start with
1209
- // `-`) is never passed as a standalone npm CLI flag.
1210
- const packageSpec = `${agentConfig.npmPackage}@${version}`;
1211
- // Set once the install has passed its integrity gate; read after the try so
1212
- // the success path's bookkeeping sits outside the catch's cleanup.
1213
- let healthyVersion;
1214
- try {
1215
- // Check npm is available
1216
- const winShell = process.platform === 'win32';
1217
- try {
1218
- await execFileAsync('npm', ['--version'], { shell: winShell });
1219
- }
1220
- catch {
1221
- return {
1222
- success: false,
1223
- installedVersion: version,
1224
- error: 'npm is not installed. Install Node.js and npm first: https://nodejs.org/',
1225
- };
1211
+ const versionDir = getVersionDir(agent, label);
1212
+ return withFileLockAsync(installationLockTarget(agent, label), async () => {
1213
+ // Installs and repairs mutate the same executable as updates. Hold the same
1214
+ // lock before touching artifacts, even before the first record exists.
1215
+ if (await isInstallationLikelyActive({ agent, label })) {
1216
+ return { success: false, installedVersion: label, error: `${agent} account home ${label} is in use. Retry after its sessions finish.` };
1217
+ }
1218
+ // A `clean` (repair) reinstall wipes a possibly partially-extracted
1219
+ // node_modules first. npm treats a present-but-gutted platform package (its
1220
+ // package.json landed, its vendored native binary did not) as already
1221
+ // installed and would skip re-fetching it — so without this the corrupt
1222
+ // vendor/ survives the reinstall and the ENOENT persists. home/ is preserved.
1223
+ if (opts?.clean && fs.existsSync(versionDir)) {
1224
+ removeInstallArtifacts(versionDir);
1226
1225
  }
1227
- onProgress?.(`Installing ${packageSpec}...`);
1228
- await execFileAsync('npm', ['install', packageSpec, '--ignore-scripts'], { cwd: versionDir, shell: winShell });
1229
- // `version` is concrete (the `latest`/`oldest` aliases were resolved up
1230
- // front), so the package installed directly into its final versioned dir —
1231
- // no post-install rename, and no shared `latest/` dir for a concurrent
1232
- // process to move out from under us.
1233
- const installedVersion = version;
1234
- // Create versioned alias (e.g., claude@2.0.65)
1235
- createVersionedAlias(agent, installedVersion);
1236
- // Claude reads its global config from CLAUDE_CONFIG_DIR/.claude.json —
1237
- // i.e. inside the per-version .claude dir — while the rest of agents-cli
1238
- // manages the home-level file. Symlink INSIDE to OUTSIDE so Claude and
1239
- // agents-cli see the same content.
1240
- if (agent === 'claude') {
1226
+ // Create version directory and isolated home
1227
+ fs.mkdirSync(versionDir, { recursive: true });
1228
+ fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
1229
+ // Initialize package.json (only for real npm agents)
1230
+ const packageJson = {
1231
+ name: `agents-${agent}-${version}`,
1232
+ version: '1.0.0',
1233
+ private: true,
1234
+ };
1235
+ fs.writeFileSync(path.join(versionDir, 'package.json'), JSON.stringify(packageJson, null, 2));
1236
+ // Install the package. `version` is always concrete here (`latest`/`oldest`
1237
+ // were resolved above), so the spec is always pinned. The `@` prefix is
1238
+ // load-bearing: it ensures `version` (which VERSION_RE permits to start with
1239
+ // `-`) is never passed as a standalone npm CLI flag.
1240
+ const packageSpec = `${agentConfig.npmPackage}@${version}`;
1241
+ // Set once the install has passed its integrity gate; read after the try so
1242
+ // the success path's bookkeeping sits outside the catch's cleanup.
1243
+ let healthyVersion;
1244
+ try {
1245
+ // Check npm is available
1246
+ const winShell = process.platform === 'win32';
1241
1247
  try {
1242
- ensureClaudeInsideSymlink(installedVersion);
1248
+ await execFileAsync('npm', ['--version'], { shell: winShell });
1243
1249
  }
1244
1250
  catch {
1245
- /* non-fatal; the install itself succeeded */
1251
+ return {
1252
+ success: false,
1253
+ installedVersion: version,
1254
+ error: 'npm is not installed. Install Node.js and npm first: https://nodejs.org/',
1255
+ };
1246
1256
  }
1247
- }
1248
- // The `npm install` above ran with `--ignore-scripts` — the right posture for
1249
- // the dependency TREE (never run arbitrary transitive postinstalls), but it
1250
- // also skips the agent package's OWN postinstall, which for some agents is a
1251
- // required install step. @anthropic-ai/claude-code ships a ~500-byte stub at
1252
- // `bin/claude.exe` plus per-arch native binaries as optional deps; its
1253
- // `postinstall` (`node install.cjs`) is what copies the correct native binary
1254
- // over the stub. Skip it and every launch dies with "native binary not
1255
- // installed". So run the first-party package's declared postinstall here —
1256
- // scoped to that one package, never `prepare` (claude-code's `prepare` is an
1257
- // unconditional `exit 1` publish guard). Same precedent as the keychain-helper
1258
- // postinstall re-run after a `--ignore-scripts` upgrade (see index.ts).
1259
- // Best-effort: the integrity gate below is the real backstop — a still-broken
1260
- // binary (postinstall failed, or a platform with no published native dep)
1261
- // fails there with the correct message rather than throwing here.
1262
- if (agentConfig.npmPackage) {
1263
- // `installedVersion === version`, so this is exactly `versionDir` — the
1264
- // install landed in its final dir with no rename to chase.
1265
- const pkgRoot = path.join(versionDir, 'node_modules', agentConfig.npmPackage);
1266
- try {
1267
- const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8'));
1268
- const postinstall = pkg?.scripts?.postinstall;
1269
- if (typeof postinstall === 'string' && postinstall.trim()) {
1270
- onProgress?.(`Running ${agentConfig.name} postinstall...`);
1271
- // The declared postinstall is a shell command string (e.g. `node
1272
- // install.cjs`), so it must run through a shell on ALL platforms —
1273
- // shell:true, empty args. cwd is the package root; install.cjs anchors
1274
- // its paths to __dirname, so that is correct and sufficient.
1275
- await execFileAsync(postinstall, [], { cwd: pkgRoot, shell: true });
1257
+ onProgress?.(`Installing ${packageSpec}...`);
1258
+ await execFileAsync('npm', ['install', packageSpec, '--ignore-scripts'], { cwd: versionDir, shell: winShell });
1259
+ // The release installed directly into its final labeled dir (`versionDir` is
1260
+ // keyed on `label`) — no post-install rename, no shared `latest/` dir for a
1261
+ // concurrent process to move out from under us. The addressable slot is the
1262
+ // label; `releaseVersion` is what npm actually staged.
1263
+ const installedVersion = label;
1264
+ // Create versioned alias (e.g., claude@2.0.65, or claude@ins_… for connect)
1265
+ createVersionedAlias(agent, installedVersion);
1266
+ // Claude reads its global config from CLAUDE_CONFIG_DIR/.claude.json —
1267
+ // i.e. inside the per-version .claude dir — while the rest of agents-cli
1268
+ // manages the home-level file. Symlink INSIDE to OUTSIDE so Claude and
1269
+ // agents-cli see the same content.
1270
+ if (agent === 'claude') {
1271
+ try {
1272
+ ensureClaudeInsideSymlink(installedVersion);
1273
+ }
1274
+ catch {
1275
+ /* non-fatal; the install itself succeeded */
1276
1276
  }
1277
1277
  }
1278
- catch {
1279
- /* non-fatal; the integrity gate below catches a still-broken binary */
1278
+ // The `npm install` above ran with `--ignore-scripts` — the right posture for
1279
+ // the dependency TREE (never run arbitrary transitive postinstalls), but it
1280
+ // also skips the agent package's OWN postinstall, which for some agents is a
1281
+ // required install step. @anthropic-ai/claude-code ships a ~500-byte stub at
1282
+ // `bin/claude.exe` plus per-arch native binaries as optional deps; its
1283
+ // `postinstall` (`node install.cjs`) is what copies the correct native binary
1284
+ // over the stub. Skip it and every launch dies with "native binary not
1285
+ // installed". So run the first-party package's declared postinstall here —
1286
+ // scoped to that one package, never `prepare` (claude-code's `prepare` is an
1287
+ // unconditional `exit 1` publish guard). Same precedent as the keychain-helper
1288
+ // postinstall re-run after a `--ignore-scripts` upgrade (see index.ts).
1289
+ // Best-effort: the integrity gate below is the real backstop — a still-broken
1290
+ // binary (postinstall failed, or a platform with no published native dep)
1291
+ // fails there with the correct message rather than throwing here.
1292
+ if (agentConfig.npmPackage) {
1293
+ // The install landed in `versionDir` (the labeled dir) with no rename to
1294
+ // chase, so the package root is exactly there.
1295
+ const pkgRoot = path.join(versionDir, 'node_modules', agentConfig.npmPackage);
1296
+ try {
1297
+ const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8'));
1298
+ const postinstall = pkg?.scripts?.postinstall;
1299
+ if (typeof postinstall === 'string' && postinstall.trim()) {
1300
+ onProgress?.(`Running ${agentConfig.name} postinstall...`);
1301
+ // The declared postinstall is a shell command string (e.g. `node
1302
+ // install.cjs`), so it must run through a shell on ALL platforms —
1303
+ // shell:true, empty args. cwd is the package root; install.cjs anchors
1304
+ // its paths to __dirname, so that is correct and sufficient.
1305
+ await execFileAsync(postinstall, [], { cwd: pkgRoot, shell: true });
1306
+ }
1307
+ }
1308
+ catch {
1309
+ /* non-fatal; the integrity gate below catches a still-broken binary */
1310
+ }
1311
+ }
1312
+ // Integrity gate: confirm the install actually launches, not just that the
1313
+ // JS wrapper landed. A gutted install (wrapper present, native platform
1314
+ // binary missing) otherwise gets silently pinned as the default and crashes
1315
+ // with ENOENT on run. Fail loudly here so `agents add` never records a
1316
+ // broken version as healthy — the caller then won't set it as default.
1317
+ const health = await verifyInstalledBinaryLaunches(agent, installedVersion);
1318
+ if (!health.ok) {
1319
+ if (fs.existsSync(versionDir))
1320
+ removeInstallArtifacts(versionDir);
1321
+ const detail = health.detail ? ` (${health.detail})` : '';
1322
+ emit('version.install', { agent, version: installedVersion, error: `binary failed to launch${detail}` });
1323
+ return {
1324
+ success: false,
1325
+ installedVersion,
1326
+ error: `${agentConfig.name}@${installedVersion} installed but its binary failed to launch${detail}. `
1327
+ + `The install is incomplete — the platform binary is missing. Re-run: agents add ${agent}@${installedVersion}`,
1328
+ };
1280
1329
  }
1330
+ // The install is healthy from here. Identity is frozen AFTER the try (see
1331
+ // below) so a bookkeeping write failure cannot fall into the catch and wipe
1332
+ // a working install.
1333
+ healthyVersion = installedVersion;
1281
1334
  }
1282
- // Integrity gate: confirm the install actually launches, not just that the
1283
- // JS wrapper landed. A gutted install (wrapper present, native platform
1284
- // binary missing) otherwise gets silently pinned as the default and crashes
1285
- // with ENOENT on run. Fail loudly here so `agents add` never records a
1286
- // broken version as healthy — the caller then won't set it as default.
1287
- const health = await verifyInstalledBinaryLaunches(agent, installedVersion);
1288
- if (!health.ok) {
1289
- if (fs.existsSync(versionDir))
1335
+ catch (err) {
1336
+ // Clean up on failure — preserve `home/` in case a prior install left
1337
+ // conversation history behind that we must not wipe on a failed reinstall.
1338
+ if (fs.existsSync(versionDir)) {
1290
1339
  removeInstallArtifacts(versionDir);
1291
- const detail = health.detail ? ` (${health.detail})` : '';
1292
- emit('version.install', { agent, version: installedVersion, error: `binary failed to launch${detail}` });
1293
- return {
1294
- success: false,
1295
- installedVersion,
1296
- error: `${agentConfig.name}@${installedVersion} installed but its binary failed to launch${detail}. `
1297
- + `The install is incomplete — the platform binary is missing. Re-run: agents add ${agent}@${installedVersion}`,
1298
- };
1299
- }
1300
- // The install is healthy from here. Identity is frozen AFTER the try (see
1301
- // below) so a bookkeeping write failure cannot fall into the catch and wipe
1302
- // a working install.
1303
- healthyVersion = installedVersion;
1304
- }
1305
- catch (err) {
1306
- // Clean up on failure — preserve `home/` in case a prior install left
1307
- // conversation history behind that we must not wipe on a failed reinstall.
1308
- if (fs.existsSync(versionDir)) {
1309
- removeInstallArtifacts(versionDir);
1340
+ }
1341
+ emit('version.install', { agent, version, error: err.message });
1342
+ return { success: false, installedVersion: version, error: err.message };
1343
+ }
1344
+ // Freeze this installation's identity (see the installScript branch above).
1345
+ // The label is the frozen identity; the release it carries is recorded
1346
+ // separately so a connect home under an opaque label records the real vendor
1347
+ // release it installed rather than the label string.
1348
+ createInstallation(agent, healthyVersion, releaseVersion, initialUpdatePolicy);
1349
+ const trackerInstall = await installSessionTrackerHook(agent, healthyVersion);
1350
+ if (!trackerInstall.installed && trackerInstall.error) {
1351
+ console.warn(`agents: SessionStart hook not installed for ${agent}@${healthyVersion}: ${trackerInstall.error}`);
1310
1352
  }
1311
- emit('version.install', { agent, version, error: err.message });
1312
- return { success: false, installedVersion: version, error: err.message };
1313
- }
1314
- // Freeze this installation's identity (see the installScript branch above).
1315
- createInstallation(agent, healthyVersion, healthyVersion);
1316
- const trackerInstall = await installSessionTrackerHook(agent, healthyVersion);
1317
- if (!trackerInstall.installed && trackerInstall.error) {
1318
- console.warn(`agents: SessionStart hook not installed for ${agent}@${healthyVersion}: ${trackerInstall.error}`);
1319
- }
1320
- emit('version.install', { agent, version: healthyVersion });
1321
- return { success: true, installedVersion: healthyVersion };
1353
+ emit('version.install', { agent, version: healthyVersion });
1354
+ return { success: true, installedVersion: healthyVersion };
1355
+ }, { ...INSTALLATION_LOCK_OPTIONS, realpath: false });
1322
1356
  }
1323
1357
  // Version-dir entries that are STATE, not install output, and so must survive a
1324
1358
  // clean reinstall: `home/`, the `.isolated` marker that is the single source
@@ -1328,7 +1362,7 @@ export async function installVersion(agent, version, onProgress, opts) {
1328
1362
  // self-heal would hand it a bare `<agent>` shim and a PATH entry. Losing the
1329
1363
  // installation record would mint a NEW id for the same install on its first
1330
1364
  // repair, discarding its release history.
1331
- const PRESERVED_ON_CLEAN_REINSTALL = new Set(['home', '.isolated', INSTALLATION_RECORD_FILE]);
1365
+ const PRESERVED_ON_CLEAN_REINSTALL = new Set(['home', '.isolated', '.launch-leases', INSTALLATION_RECORD_FILE, `${INSTALLATION_RECORD_FILE}.lock`]);
1332
1366
  /**
1333
1367
  * Remove install artifacts from a version directory, preserving `home/` which
1334
1368
  * contains the user's conversation history, sessions, history.jsonl, tasks,
@@ -1339,7 +1373,7 @@ const PRESERVED_ON_CLEAN_REINSTALL = new Set(['home', '.isolated', INSTALLATION_
1339
1373
  */
1340
1374
  function removeInstallArtifacts(versionDir) {
1341
1375
  for (const entry of fs.readdirSync(versionDir)) {
1342
- if (PRESERVED_ON_CLEAN_REINSTALL.has(entry))
1376
+ if (PRESERVED_ON_CLEAN_REINSTALL.has(entry) || entry.startsWith('.rollback-'))
1343
1377
  continue;
1344
1378
  fs.rmSync(path.join(versionDir, entry), { recursive: true, force: true });
1345
1379
  }
@@ -1906,7 +1940,8 @@ export async function ensureAgentRunnable(agent, version, log, opts) {
1906
1940
  // so isolation must be decided from the pre-repair state, not re-read after.
1907
1941
  const targetIsolated = isVersionIsolated(agent, version);
1908
1942
  log?.(`${cfg.name}@${version} is broken (platform binary missing) — repairing…`);
1909
- const repair = await installVersion(agent, version, undefined, { clean: true });
1943
+ const record = readInstallation(agent, version);
1944
+ const repair = await installVersion(agent, record?.releaseVersion ?? version, undefined, { clean: true, installationLabel: version });
1910
1945
  if (repair.success && (await verifyInstalledBinaryLaunches(agent, version)).ok) {
1911
1946
  log?.(`repaired ${cfg.name}@${version}.`);
1912
1947
  return version;
@@ -1914,7 +1949,7 @@ export async function ensureAgentRunnable(agent, version, log, opts) {
1914
1949
  // An isolated copy is walled off from the rest of the setup: repairing it in
1915
1950
  // place is the ONLY thing we may do. No fallback, no install, no default
1916
1951
  // switch — surface the failure and let the caller tell the user.
1917
- if (targetIsolated) {
1952
+ if (targetIsolated || (record && record.label !== record.releaseVersion)) {
1918
1953
  log?.(`${cfg.name}@${version} is an isolated install and could not be repaired — leaving your default ${cfg.name} untouched.`);
1919
1954
  return null;
1920
1955
  }
@@ -78,4 +78,6 @@ export declare function isAlive(pid: number): boolean;
78
78
  * caller there silently ran with NO pid-reuse protection — including
79
79
  * `agents teams stop`, which is how it could SIGKILL an unrelated process group.
80
80
  */
81
- export declare function captureProcessStartTime(pid: number): string | null;
81
+ export declare function captureProcessStartTime(pid: number, opts?: {
82
+ fresh?: boolean;
83
+ }): string | null;
@@ -158,11 +158,11 @@ const startTimeByPid = new Map();
158
158
  * caller there silently ran with NO pid-reuse protection — including
159
159
  * `agents teams stop`, which is how it could SIGKILL an unrelated process group.
160
160
  */
161
- export function captureProcessStartTime(pid) {
161
+ export function captureProcessStartTime(pid, opts = {}) {
162
162
  if (!Number.isInteger(pid) || pid <= 0)
163
163
  return null;
164
164
  const cached = startTimeByPid.get(pid);
165
- if (cached !== undefined)
165
+ if (!opts.fresh && cached !== undefined)
166
166
  return cached;
167
167
  const value = readProcessStartTime(pid);
168
168
  startTimeByPid.set(pid, value);
@@ -1,3 +1,2 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceDetector } from './types.js';
3
- export declare const commandsDetectors: Partial<Record<AgentId, ResourceDetector>>;
2
+ export declare const commandsDetectors: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceDetector>>;
@@ -1,3 +1,2 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceDetector } from './types.js';
3
- export declare const hooksDetectors: Partial<Record<AgentId, ResourceDetector>>;
2
+ export declare const hooksDetectors: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceDetector>>;
@@ -1,3 +1,2 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceDetector } from './types.js';
3
- export declare const mcpDetectors: Partial<Record<AgentId, ResourceDetector>>;
2
+ export declare const mcpDetectors: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceDetector>>;
@@ -1,3 +1,2 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceDetector } from './types.js';
3
- export declare const permissionsDetectors: Partial<Record<AgentId, ResourceDetector>>;
2
+ export declare const permissionsDetectors: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceDetector>>;
@@ -1,8 +1,2 @@
1
- /**
2
- * Plugins detector — for each discovered plugin, ask `isPluginSynced` whether
3
- * its expected artifacts are present in the version home. Mirrors
4
- * versions.ts:541-549.
5
- */
6
- import type { AgentId } from '../../types.js';
7
1
  import type { ResourceDetector } from './types.js';
8
- export declare const pluginsDetectors: Partial<Record<AgentId, ResourceDetector>>;
2
+ export declare const pluginsDetectors: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceDetector>>;
@@ -1,3 +1,2 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceDetector } from './types.js';
3
- export declare const rulesDetectors: Partial<Record<AgentId, ResourceDetector>>;
2
+ export declare const rulesDetectors: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceDetector>>;
@@ -1,5 +1,4 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceDetector } from './types.js';
3
2
  /** Exported for unit tests (RUSH-2320 #2). Production callers: list() below. */
4
3
  export declare function skillDirsMatch(src: string, dest: string): boolean;
5
- export declare const skillsDetectors: Partial<Record<AgentId, ResourceDetector>>;
4
+ export declare const skillsDetectors: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceDetector>>;
@@ -1,8 +1,2 @@
1
- /**
2
- * Subagents detector. The installed-name enumeration for every agent's on-disk
3
- * layout is declared once in the subagent registry; this detector is generic
4
- * and delegates to `listInstalledSubagentNames` instead of a per-agent builder.
5
- */
6
- import type { AgentId } from '../../types.js';
7
1
  import type { ResourceDetector } from './types.js';
8
- export declare const subagentsDetectors: Partial<Record<AgentId, ResourceDetector>>;
2
+ export declare const subagentsDetectors: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceDetector>>;
@@ -1,3 +1,2 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceDetector } from './types.js';
3
- export declare const workflowsDetectors: Partial<Record<AgentId, ResourceDetector>>;
2
+ export declare const workflowsDetectors: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceDetector>>;
@@ -1,3 +1,2 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceWriter } from './types.js';
3
- export declare const commandsWriters: Partial<Record<AgentId, ResourceWriter<string[]>>>;
2
+ export declare const commandsWriters: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceWriter<string[]>>>;
@@ -1,3 +1,2 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceWriter } from './types.js';
3
- export declare const hooksWriters: Partial<Record<AgentId, ResourceWriter<string[]>>>;
2
+ export declare const hooksWriters: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceWriter<string[]>>>;
@@ -1,3 +1,2 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceWriter } from './types.js';
3
- export declare const mcpWriters: Partial<Record<AgentId, ResourceWriter<string[]>>>;
2
+ export declare const mcpWriters: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceWriter<string[]>>>;
@@ -1,13 +1,2 @@
1
- /**
2
- * Permissions writer — selection is a list of permission GROUP names. We
3
- * build the PermissionSet from the discovered groups, then dispatch into the
4
- * per-agent format writer in `lib/permissions.ts:applyPermissionsToVersion`.
5
- *
6
- * The per-agent format work lives in lib/permissions.ts because the format
7
- * conversions (Claude settings.json vs Codex TOML+rules vs Gemini tools vs
8
- * Antigravity permissions{} vs Grok [permission].rules) are tightly coupled
9
- * to the converters defined alongside them.
10
- */
11
- import type { AgentId } from '../../types.js';
12
1
  import type { ResourceWriter } from './types.js';
13
- export declare const permissionsWriters: Partial<Record<AgentId, ResourceWriter<string[]>>>;
2
+ export declare const permissionsWriters: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceWriter<string[]>>>;
@@ -1,7 +1,2 @@
1
- /**
2
- * Plugins writer — thin wrapper around `syncPluginToVersion`. Discovery and
3
- * per-agent format work lives in lib/plugins.ts.
4
- */
5
- import type { AgentId } from '../../types.js';
6
1
  import type { ResourceWriter } from './types.js';
7
- export declare const pluginsWriters: Partial<Record<AgentId, ResourceWriter<string[]>>>;
2
+ export declare const pluginsWriters: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceWriter<string[]>>>;
@@ -1,7 +1,6 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceWriter } from './types.js';
3
2
  export interface RulesSelection {
4
3
  /** Preset name to compose. Empty string falls through to `"default"` in the composer. */
5
4
  preset: string;
6
5
  }
7
- export declare const rulesWriters: Partial<Record<AgentId, ResourceWriter<RulesSelection>>>;
6
+ export declare const rulesWriters: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceWriter<RulesSelection>>>;
@@ -1,3 +1,2 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceWriter } from './types.js';
3
- export declare const skillsWriters: Partial<Record<AgentId, ResourceWriter<string[]>>>;
2
+ export declare const skillsWriters: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceWriter<string[]>>>;
@@ -1,3 +1,2 @@
1
- import type { AgentId } from '../../types.js';
2
1
  import type { ResourceWriter } from './types.js';
3
- export declare const subagentsWriters: Partial<Record<AgentId, ResourceWriter<string[]>>>;
2
+ export declare const subagentsWriters: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceWriter<string[]>>>;
@@ -1,9 +1,2 @@
1
- /**
2
- * Workflows writer — copies each selected workflow into the agent-specific
3
- * on-disk layout via `syncWorkflowToVersion` (Claude: WORKFLOW.md tree under
4
- * `{versionHome}/workflows/`; Grok: native `.rhai` under
5
- * `{versionHome}/.grok/workflows/`).
6
- */
7
- import type { AgentId } from '../../types.js';
8
1
  import type { ResourceWriter } from './types.js';
9
- export declare const workflowsWriters: Partial<Record<AgentId, ResourceWriter<string[]>>>;
2
+ export declare const workflowsWriters: Partial<Record<"claude" | "codex" | "gemini" | "cursor" | "opencode" | "openclaw" | "copilot" | "amp" | "goose" | "antigravity" | "grok" | "kimi" | "droid" | "hermes" | "muse" | "warp", ResourceWriter<string[]>>>;
@@ -475,7 +475,9 @@ export declare function writeMetaUnlocked(meta: Meta): boolean;
475
475
  * mtime check below catches it on the next read (assuming the mtime advanced).
476
476
  * - The cache stores the merged system+user meta; both files' mtimes contribute.
477
477
  */
478
- export declare function readMeta(): Meta;
478
+ export declare function readMeta(options?: {
479
+ migrate?: boolean;
480
+ }): Meta;
479
481
  /** Serialize and write agents.yaml to the user repo, invalidating the in-memory cache. */
480
482
  export declare function writeMeta(meta: Meta): void;
481
483
  /** Update agents.yaml under lock and return the new state. */