@phnx-labs/agents-cli 1.22.56 → 1.22.58
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 +70 -0
- package/README.md +4 -4
- package/dist/bootstrap.js +11 -2
- package/dist/cli/command-registry.d.ts +0 -1
- package/dist/cli/command-registry.js +0 -3
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/exec.js +1 -1
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/hooks.js +4 -4
- package/dist/commands/insights.d.ts +7 -5
- package/dist/commands/insights.js +16 -9
- package/dist/commands/monitors.js +11 -0
- package/dist/commands/perf.d.ts +16 -7
- package/dist/commands/perf.js +29 -20
- package/dist/commands/prune.js +5 -3
- package/dist/commands/routines.d.ts +8 -0
- package/dist/commands/routines.js +57 -3
- package/dist/commands/rules.js +1 -1
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/ssh.js +24 -14
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/commands/trash.d.ts +2 -2
- package/dist/commands/trash.js +2 -6
- package/dist/commands/versions.d.ts +2 -2
- package/dist/commands/versions.js +1 -10
- package/dist/commands/view.d.ts +2 -2
- package/dist/commands/view.js +7 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +14 -0
- package/dist/lib/account-registry.d.ts +5 -1
- package/dist/lib/account-registry.js +47 -14
- package/dist/lib/accounting/capacity.d.ts +18 -7
- package/dist/lib/accounting/capacity.js +19 -8
- package/dist/lib/accounting/usage-ingest.d.ts +1 -0
- package/dist/lib/accounting/usage-ingest.js +75 -0
- package/dist/lib/accounting/usage-sync.d.ts +97 -0
- package/dist/lib/accounting/usage-sync.js +203 -0
- package/dist/lib/accounting/usage.d.ts +48 -2
- package/dist/lib/accounting/usage.js +79 -2
- package/dist/lib/agent-spec/agents.js +1 -1
- package/dist/lib/analytics/mix-commands.d.ts +8 -7
- package/dist/lib/analytics/mix-commands.js +50 -73
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- package/dist/lib/browser/ipc.d.ts +8 -0
- package/dist/lib/browser/ipc.js +87 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +96 -11
- package/dist/lib/browser/sessions-list.js +10 -1
- package/dist/lib/daemon/daemon.js +5 -0
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +95 -53
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -0
- package/dist/lib/daemon/usage-sync-service.js +42 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +17 -6
- package/dist/lib/device-config.js +25 -11
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/devices/pool.d.ts +4 -3
- package/dist/lib/devices/pool.js +13 -5
- package/dist/lib/doctor-diff.js +77 -7
- package/dist/lib/exec.d.ts +6 -41
- package/dist/lib/exec.js +6 -41
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/git.d.ts +13 -1
- package/dist/lib/git.js +36 -7
- package/dist/lib/harness/adapter.d.ts +7 -7
- package/dist/lib/harness/adapters/claude.js +3 -2
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/hosts/remote-cmd.d.ts +9 -0
- package/dist/lib/hosts/remote-cmd.js +22 -0
- package/dist/lib/installations/migrate.d.ts +6 -120
- package/dist/lib/installations/migrate.js +27 -259
- package/dist/lib/installations/shims.d.ts +13 -95
- package/dist/lib/installations/shims.js +22 -139
- package/dist/lib/installations/store.js +1 -1
- package/dist/lib/installations/versions.d.ts +26 -133
- package/dist/lib/installations/versions.js +41 -204
- package/dist/lib/perf/db.d.ts +1 -1
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/refresh.d.ts +9 -0
- package/dist/lib/refresh.js +3 -1
- package/dist/lib/routine-readiness.d.ts +15 -1
- package/dist/lib/routine-readiness.js +41 -0
- package/dist/lib/sandbox.d.ts +4 -1
- package/dist/lib/sandbox.js +30 -1
- package/dist/lib/secrets/agent.d.ts +80 -225
- package/dist/lib/secrets/agent.js +139 -401
- package/dist/lib/secrets/bundles.d.ts +73 -222
- package/dist/lib/secrets/bundles.js +168 -467
- package/dist/lib/secrets/reaper.d.ts +28 -70
- package/dist/lib/secrets/reaper.js +30 -85
- package/dist/lib/secrets/remote.d.ts +42 -129
- package/dist/lib/secrets/remote.js +55 -173
- package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
- package/dist/lib/self-heal/checks/install-staging.js +96 -0
- package/dist/lib/self-heal/registry.js +2 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/self-update.d.ts +23 -0
- package/dist/lib/self-update.js +50 -0
- package/dist/lib/session/active.d.ts +16 -32
- package/dist/lib/session/active.js +10 -68
- package/dist/lib/session/db.d.ts +24 -36
- package/dist/lib/session/db.js +143 -44
- package/dist/lib/session/discover.d.ts +6 -58
- package/dist/lib/session/discover.js +5 -43
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/parse.d.ts +1 -19
- package/dist/lib/session/parse.js +2 -15
- package/dist/lib/session/tool-calls.d.ts +43 -1
- package/dist/lib/session/tool-calls.js +74 -44
- package/dist/lib/session/tool-store.d.ts +33 -2
- package/dist/lib/session/tool-store.js +56 -3
- package/dist/lib/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +8 -2
- package/dist/lib/startup/command-registry.js +12 -4
- package/dist/lib/sync-status.d.ts +22 -0
- package/dist/lib/sync-status.js +27 -0
- package/dist/lib/sync-umbrella.d.ts +9 -0
- package/dist/lib/sync-umbrella.js +21 -2
- package/dist/lib/traces/insights.d.ts +47 -14
- package/dist/lib/traces/insights.js +92 -21
- package/dist/lib/traces/phenotype.d.ts +23 -3
- package/dist/lib/traces/phenotype.js +72 -24
- package/dist/lib/traces/sync.d.ts +15 -0
- package/dist/lib/traces/sync.js +104 -19
- package/dist/lib/traces/worker-template.js +154 -1
- package/package.json +1 -1
package/dist/commands/ssh.js
CHANGED
|
@@ -79,10 +79,14 @@ function deviceSummary(d, isSelf = false, stats, isInteractive = false, roles) {
|
|
|
79
79
|
const marker = isSelf ? chalk.cyan('▸ ') : ' ';
|
|
80
80
|
const name = isSelf ? chalk.bold.cyan(d.name.padEnd(16)) : chalk.bold(d.name.padEnd(16));
|
|
81
81
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
82
|
-
const interactive = isInteractive ? chalk.yellow(' ★ interactive') : '';
|
|
83
82
|
// `roles` defaults to a single-device roster so a fleet-wide `role` default
|
|
84
83
|
// still reaches this device even when it has no per-device doc of its own.
|
|
85
|
-
const
|
|
84
|
+
const roleMap = roles ?? listConfiguredDeviceRoles([d.name]);
|
|
85
|
+
// `personal` already means "the interactive box you sit at", so the star would
|
|
86
|
+
// just repeat it — show it only when the interactive host is NOT a personal box
|
|
87
|
+
// (e.g. an unmarked or worker box deliberately pinned as the artifact target).
|
|
88
|
+
const interactive = isInteractive && roleMap[d.name] !== 'personal' ? chalk.yellow(' ★ interactive') : '';
|
|
89
|
+
const role = roleTag(d.name, roleMap);
|
|
86
90
|
return `${marker}${name} ${String(d.platform).padEnd(8)} ${(d.user ? d.user + '@' : '') + addr} ${online}${reach}${here}${interactive}${role}`;
|
|
87
91
|
}
|
|
88
92
|
/** The fleet-wide role mark, rendered for a device row. Empty when unmarked —
|
|
@@ -95,7 +99,9 @@ function roleTag(name, roles) {
|
|
|
95
99
|
return chalk.green(' worker');
|
|
96
100
|
if (role === 'personal')
|
|
97
101
|
return chalk.yellow(' personal');
|
|
98
|
-
|
|
102
|
+
if (role === 'desktop')
|
|
103
|
+
return chalk.cyan(' desktop');
|
|
104
|
+
return '';
|
|
99
105
|
}
|
|
100
106
|
const HEADROOM_BADGE = {
|
|
101
107
|
idle: chalk.green('○ idle'),
|
|
@@ -257,7 +263,9 @@ export function renderDeviceTable(reg, names, self, statsMap, full = false, inte
|
|
|
257
263
|
: '';
|
|
258
264
|
const badge = HEADROOM_BADGE[headroom(stats)];
|
|
259
265
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
260
|
-
|
|
266
|
+
// A `personal` role already conveys "your interactive seat"; don't double-
|
|
267
|
+
// render the star on it. Keep it for a non-personal pinned interactive host.
|
|
268
|
+
const interactive = name === interactiveHost && deviceRoles[name] !== 'personal' ? chalk.yellow(' ★ interactive') : '';
|
|
261
269
|
lines.push(fitDeviceRow(`${marker}${label}${plat} ${specCell(stats, specWidth)}${load}${mem}${disk}${freeTotal} ${badge}${relay}${here}${interactive}`, role, desc, width));
|
|
262
270
|
}
|
|
263
271
|
// Fleet capacity summary — total cores, how much RAM is free right now, and
|
|
@@ -1449,12 +1457,12 @@ function registerDevicesCommands(program) {
|
|
|
1449
1457
|
*
|
|
1450
1458
|
* A role written here lands in that device's tracked per-device doc
|
|
1451
1459
|
* (`devices/<name>/agents.yaml` `config.role`) and syncs with repo
|
|
1452
|
-
* push/pull. The vocabulary is
|
|
1460
|
+
* push/pull. The vocabulary is `worker | personal | desktop`.
|
|
1453
1461
|
*/
|
|
1454
1462
|
const runDevicesRole = async (name, role, opts) => {
|
|
1455
1463
|
if (!name) {
|
|
1456
1464
|
if (role)
|
|
1457
|
-
throw new Error('Name a device: agents devices role <name> <worker|personal>');
|
|
1465
|
+
throw new Error('Name a device: agents devices role <name> <worker|personal|desktop>');
|
|
1458
1466
|
const reg = await loadDevices();
|
|
1459
1467
|
// The full registered roster, not just online — a fleet-wide `role`
|
|
1460
1468
|
// default must reach a registered device even when it has no per-device
|
|
@@ -1475,14 +1483,14 @@ function registerDevicesCommands(program) {
|
|
|
1475
1483
|
}
|
|
1476
1484
|
else {
|
|
1477
1485
|
for (const [device, r] of marked) {
|
|
1478
|
-
const tint = r === 'worker' ? chalk.green : r === 'personal' ? chalk.yellow : chalk.gray;
|
|
1486
|
+
const tint = r === 'worker' ? chalk.green : r === 'personal' ? chalk.yellow : r === 'desktop' ? chalk.cyan : chalk.gray;
|
|
1479
1487
|
console.log(` ${device.padEnd(20)} ${tint(r)}`);
|
|
1480
1488
|
}
|
|
1481
1489
|
}
|
|
1482
1490
|
console.log();
|
|
1483
1491
|
console.log(chalk.bold('--device auto picks from: ') + (pool.length > 0 ? pool.join(', ') : chalk.red('nothing — no eligible device')));
|
|
1484
1492
|
if (mode === 'all')
|
|
1485
|
-
console.log(chalk.gray('auto.pool=all — worker marks are ignored (
|
|
1493
|
+
console.log(chalk.gray('auto.pool=all — worker marks are ignored (personal and desktop devices are still excluded).'));
|
|
1486
1494
|
return;
|
|
1487
1495
|
}
|
|
1488
1496
|
await mustGetDevice(name);
|
|
@@ -1669,7 +1677,8 @@ function registerDevicesCommands(program) {
|
|
|
1669
1677
|
});
|
|
1670
1678
|
const roleCmd = devicesCmd
|
|
1671
1679
|
.command('role [name] [role]')
|
|
1672
|
-
.description('Show or set what a device is for: worker (agents run here)
|
|
1680
|
+
.description('Show or set what a device is for: worker (agents run here), personal (you sit here), or desktop (a headed ' +
|
|
1681
|
+
'always-on box — the release/credential home). Personal and desktop are never picked automatically. ' +
|
|
1673
1682
|
'Marking any device worker makes `--device auto` an allowlist over the marked workers.')
|
|
1674
1683
|
.option('--clear', 'remove the mark, returning the device to unmarked')
|
|
1675
1684
|
.option('--json', 'output machine-readable JSON')
|
|
@@ -1688,6 +1697,7 @@ function registerDevicesCommands(program) {
|
|
|
1688
1697
|
agents devices role yosemite-s0 worker # agents spin up here
|
|
1689
1698
|
agents devices role yosemite-s1 worker # …and here; auto now rotates over these two only
|
|
1690
1699
|
agents devices role zion personal # your laptop — keep automatic placement off it
|
|
1700
|
+
agents devices role mac-mini desktop # headed always-on box — also kept out of auto placement
|
|
1691
1701
|
agents devices role yosemite-s0 --clear # unmark
|
|
1692
1702
|
agents devices role --json # machine-readable
|
|
1693
1703
|
`,
|
|
@@ -1699,12 +1709,12 @@ function registerDevicesCommands(program) {
|
|
|
1699
1709
|
|
|
1700
1710
|
Effect on '--device auto' (agents run, teams, agents ssh auto, and the AGI
|
|
1701
1711
|
EXT launch commands, which all resolve placement through the CLI):
|
|
1702
|
-
no device marked
|
|
1703
|
-
any worker marked
|
|
1704
|
-
personal
|
|
1712
|
+
no device marked -> every online device, as before
|
|
1713
|
+
any worker marked -> ONLY the marked workers
|
|
1714
|
+
personal / desktop -> never picked, under either state
|
|
1705
1715
|
|
|
1706
|
-
Turn the allowlist off with 'agents config set auto.pool all';
|
|
1707
|
-
|
|
1716
|
+
Turn the allowlist off with 'agents config set auto.pool all'; personal and
|
|
1717
|
+
desktop boxes stay excluded, since that is what the mark is for.
|
|
1708
1718
|
`,
|
|
1709
1719
|
});
|
|
1710
1720
|
const describeCmd = devicesCmd
|
package/dist/commands/status.js
CHANGED
|
@@ -78,7 +78,7 @@ export function registerStatusCommand(syncCmd) {
|
|
|
78
78
|
// A non-git / partial ~/.agents is its own drift state, not a per-agent
|
|
79
79
|
// "N missing" — surface it distinctly so the real problem isn't buried (PHNX-3301).
|
|
80
80
|
if (status.user.notGitRepo) {
|
|
81
|
-
console.log(` ${'~/.agents (user repo)'.padEnd(28)} ${chalk.yellow('not a git repo — will adopt on next `agents repo sync user`')}`);
|
|
81
|
+
console.log(` ${'~/.agents (user repo)'.padEnd(28)} ${chalk.yellow('not a git repo — will adopt on next `agents sync` (or `agents repo sync user`)')}`);
|
|
82
82
|
}
|
|
83
83
|
if (status.agents.length === 0) {
|
|
84
84
|
console.log(chalk.gray(' (no installed agent versions)'));
|
package/dist/commands/sync.js
CHANGED
|
@@ -45,6 +45,7 @@ import { runLaunchSync } from '../lib/project-launch.js';
|
|
|
45
45
|
import { formatKeptProjectResources } from '../lib/project-resources.js';
|
|
46
46
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
47
47
|
import { runUmbrellaSync } from '../lib/sync-umbrella.js';
|
|
48
|
+
import { verifyVersionConverged, formatResidualDrift } from '../lib/sync-status.js';
|
|
48
49
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
49
50
|
import { syncRepoGit, adoptUserRepoIfNeeded, recordUserRepoRemote, resolveUserRepoRemoteUrl } from '../lib/git.js';
|
|
50
51
|
import { getSystemAgentsDir, getUserAgentsDir, getEnabledExtraRepos } from '../lib/state.js';
|
|
@@ -53,6 +54,46 @@ import { registerStatusCommand } from './status.js';
|
|
|
53
54
|
function emitJson(payload) {
|
|
54
55
|
console.log(JSON.stringify(payload));
|
|
55
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Post-reconcile verification (PHNX-3186): after a sync writes into a set of
|
|
59
|
+
* version homes, re-read each home and confirm it now matches its resolved
|
|
60
|
+
* sources. The `agents sync` success line MUST NOT read "reconciled" while the
|
|
61
|
+
* drift it was asked to fix stays put. Any residual drifted/missing resource
|
|
62
|
+
* sets a non-zero exit code and — outside `--json` — names the exact unfixed
|
|
63
|
+
* drift so the operator sees what did not converge instead of a false ✓. Orphans
|
|
64
|
+
* are excluded (sync never removes them). Returns the residual for `--json`.
|
|
65
|
+
*/
|
|
66
|
+
function verifyReconciled(pairs, cwd) {
|
|
67
|
+
const residual = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
for (const { agent, version } of pairs) {
|
|
70
|
+
const key = `${agent}@${version}`;
|
|
71
|
+
if (seen.has(key))
|
|
72
|
+
continue;
|
|
73
|
+
seen.add(key);
|
|
74
|
+
const r = verifyVersionConverged(agent, version, cwd);
|
|
75
|
+
if (r)
|
|
76
|
+
residual.push(r);
|
|
77
|
+
}
|
|
78
|
+
// Residual drift is reported loudly via `ok:false` + the printed ⚠ block, but
|
|
79
|
+
// does NOT change the exit code — matching the declined-write precedent
|
|
80
|
+
// (RUSH-2700). The fleet fan-out (`agents sync --device all`) THROWS on a
|
|
81
|
+
// non-zero peer exit (`hosts/passthrough.ts`), discarding that box's JSON, so a
|
|
82
|
+
// non-zero exit here would hide the very `residualDrift` payload it emitted.
|
|
83
|
+
// Callers that must treat an incomplete sync as failure read `ok`/`residualDrift`
|
|
84
|
+
// from `--json`, exactly as they already do for declines.
|
|
85
|
+
return residual;
|
|
86
|
+
}
|
|
87
|
+
/** Print the residual-drift block naming exactly what did not converge. */
|
|
88
|
+
function printResidual(residual, errLog) {
|
|
89
|
+
if (residual.length === 0)
|
|
90
|
+
return;
|
|
91
|
+
const lines = formatResidualDrift(residual);
|
|
92
|
+
errLog(chalk.yellow(`⚠ sync did not fully reconcile — ${lines.length} resource(s) still drift after writing:`));
|
|
93
|
+
for (const line of lines)
|
|
94
|
+
errLog(chalk.yellow(` ${line}`));
|
|
95
|
+
errLog(chalk.gray(' Re-run the sync; a gap that survives a re-run is a real unreconcilable drift — report it.'));
|
|
96
|
+
}
|
|
56
97
|
/**
|
|
57
98
|
* Translate per-kind CLI flags into a `ResourceSelection` for `buildSelection`.
|
|
58
99
|
* Returns `undefined` when no kind flag was given (caller should use full sync).
|
|
@@ -429,6 +470,7 @@ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
|
|
|
429
470
|
await runInteractiveReconcile(opts, outLog, errLog);
|
|
430
471
|
return;
|
|
431
472
|
}
|
|
473
|
+
const cwd = opts.cwd || process.cwd();
|
|
432
474
|
const flags = {
|
|
433
475
|
repos: opts.repos,
|
|
434
476
|
secrets: opts.secrets,
|
|
@@ -459,10 +501,18 @@ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
|
|
|
459
501
|
// --cloud (fetch-only, no local reconcile).
|
|
460
502
|
if (!opts.cloud)
|
|
461
503
|
evictCentralBrowserProfilesForSync(quiet, json, outLog, errLog);
|
|
504
|
+
// Post-reconcile verification (PHNX-3186): re-read every version the reconcile
|
|
505
|
+
// wrote into and confirm it now matches source. Without this the umbrella
|
|
506
|
+
// printed `✓ sync: reconciled` unconditionally, the exact false-success the
|
|
507
|
+
// ticket reports. Residual drift downgrades the line and sets a non-zero exit.
|
|
508
|
+
const residual = result.reconciled
|
|
509
|
+
? verifyReconciled(result.reconciledVersions.map((r) => ({ agent: r.agent, version: r.version })), cwd)
|
|
510
|
+
: [];
|
|
462
511
|
if (json) {
|
|
463
512
|
emitJson({
|
|
464
|
-
// A refused resource is not a clean sync (RUSH-2700
|
|
465
|
-
|
|
513
|
+
// A refused resource OR residual drift is not a clean sync (RUSH-2700 +
|
|
514
|
+
// PHNX-3186).
|
|
515
|
+
ok: result.declined.length === 0 && residual.length === 0,
|
|
466
516
|
mode: 'umbrella',
|
|
467
517
|
plan: result.plan,
|
|
468
518
|
repos: result.repos,
|
|
@@ -470,6 +520,7 @@ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
|
|
|
470
520
|
devices: result.devices,
|
|
471
521
|
reconciled: result.reconciled,
|
|
472
522
|
declined: result.declined,
|
|
523
|
+
residualDrift: residual,
|
|
473
524
|
});
|
|
474
525
|
return;
|
|
475
526
|
}
|
|
@@ -482,12 +533,18 @@ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
|
|
|
482
533
|
if (result.secrets) {
|
|
483
534
|
parts.push(result.secrets.skipped ? 'secrets skipped' : `secrets ${result.secrets.pulled} pulled`);
|
|
484
535
|
}
|
|
536
|
+
// Only claim "reconciled" when the reconcile actually converged. Residual
|
|
537
|
+
// drift is already printed loudly by verifyReconciled above; reflect it in
|
|
538
|
+
// the one-line summary rather than a bare ✓.
|
|
485
539
|
if (result.reconciled)
|
|
486
|
-
parts.push('reconciled');
|
|
487
|
-
|
|
540
|
+
parts.push(residual.length === 0 ? 'reconciled' : 'reconcile INCOMPLETE');
|
|
541
|
+
const symbol = residual.length === 0 ? chalk.green('✓') : chalk.yellow('⚠');
|
|
542
|
+
const line = `${symbol} sync: ${parts.join(' · ') || 'nothing to do'}`;
|
|
543
|
+
outLog(residual.length === 0 ? chalk.green(line) : chalk.yellow(line));
|
|
488
544
|
const errs = [...(result.repos?.errors ?? []), ...(result.secrets?.errors ?? [])];
|
|
489
545
|
for (const e of errs)
|
|
490
546
|
errLog(chalk.yellow(` ! ${e}`));
|
|
547
|
+
printResidual(residual, errLog);
|
|
491
548
|
}
|
|
492
549
|
}
|
|
493
550
|
catch (err) {
|
|
@@ -700,13 +757,23 @@ async function runSync(agentSpec, repoArg, opts) {
|
|
|
700
757
|
if (!quiet && !json)
|
|
701
758
|
printSyncDetail(result, agentId, v, cwd);
|
|
702
759
|
}
|
|
760
|
+
// Verify each version actually converged; a repo-scoped sync only touched
|
|
761
|
+
// that repo's kinds, so skip verification there (the other layers legitimately
|
|
762
|
+
// still differ and are not this run's responsibility).
|
|
763
|
+
const residual = repoScope
|
|
764
|
+
? []
|
|
765
|
+
: verifyReconciled(versions.map(({ version: v }) => ({ agent: agentId, version: v })), cwd);
|
|
766
|
+
if (!quiet && !json)
|
|
767
|
+
printResidual(residual, errLog);
|
|
703
768
|
if (json) {
|
|
704
769
|
emitJson({
|
|
705
|
-
// Any version that refused a write
|
|
706
|
-
|
|
770
|
+
// Any version that refused a write, or any residual drift after the
|
|
771
|
+
// reconcile, makes the whole run not-ok (RUSH-2700 + PHNX-3186).
|
|
772
|
+
ok: versions.every(({ result }) => result.declined.length === 0) && residual.length === 0,
|
|
707
773
|
mode: 'agent-all',
|
|
708
774
|
agent: agentId,
|
|
709
775
|
repo: repoScope,
|
|
776
|
+
residualDrift: residual,
|
|
710
777
|
...healedPointers,
|
|
711
778
|
versions: versions.map(({ version: v, result }) => ({
|
|
712
779
|
version: v,
|
|
@@ -901,6 +968,11 @@ async function runSync(agentSpec, repoArg, opts) {
|
|
|
901
968
|
return;
|
|
902
969
|
}
|
|
903
970
|
const result = syncResourcesToVersion(agentId, version, selection, { projectDir, cwd, force, allowExecSurfaces: !!opts.allowExecSurfaces });
|
|
971
|
+
// Post-reconcile verification (PHNX-3186). Only for a FULL reconcile
|
|
972
|
+
// (`!selection`): an interactive subset-selection deliberately touched only the
|
|
973
|
+
// picked kinds, so the rest legitimately still differs and is not this run's
|
|
974
|
+
// failure. This is the exact path `agents sync <agent>[@version]` takes.
|
|
975
|
+
const residual = selection ? [] : verifyReconciled([{ agent: agentId, version }], cwd);
|
|
904
976
|
// Compile project-scope rules into the workspace itself so each agent's
|
|
905
977
|
// native loader picks up cwd/<INSTRUCTIONS_FILE>. projectDir is the
|
|
906
978
|
// .agents/ directory; the workspace root is its parent.
|
|
@@ -910,8 +982,11 @@ async function runSync(agentSpec, repoArg, opts) {
|
|
|
910
982
|
projectCompile = compileRulesForProject(projectRoot);
|
|
911
983
|
}
|
|
912
984
|
if (json) {
|
|
985
|
+
const base = agentSyncJson(agentId, version, result);
|
|
913
986
|
emitJson({
|
|
914
|
-
...
|
|
987
|
+
...base,
|
|
988
|
+
ok: base.ok === true && residual.length === 0,
|
|
989
|
+
residualDrift: residual,
|
|
915
990
|
...healedPointers,
|
|
916
991
|
projectCompile: projectCompile
|
|
917
992
|
? {
|
|
@@ -928,6 +1003,7 @@ async function runSync(agentSpec, repoArg, opts) {
|
|
|
928
1003
|
return;
|
|
929
1004
|
// ---------- 6. Detailed output ----------
|
|
930
1005
|
printSyncDetail(result, agentId, version, cwd);
|
|
1006
|
+
printResidual(residual, errLog);
|
|
931
1007
|
if (projectCompile?.compiled) {
|
|
932
1008
|
const linkInfo = projectCompile.symlinks.length > 0
|
|
933
1009
|
? ` (+ ${projectCompile.symlinks.join(', ')})`
|
package/dist/commands/traces.js
CHANGED
|
@@ -109,6 +109,13 @@ async function handleSync(opts) {
|
|
|
109
109
|
parts.push(chalk.dim('nothing new'));
|
|
110
110
|
}
|
|
111
111
|
console.log(parts.join(chalk.dim(' · ')));
|
|
112
|
+
// The per-session data can upload cleanly while the aggregated console shard
|
|
113
|
+
// fails to refresh (e.g. the sessions DB is locked by a running app). That used
|
|
114
|
+
// to be silent, so the console sat stale with no signal here (PHNX-3401).
|
|
115
|
+
if (result.indexError) {
|
|
116
|
+
console.log(chalk.yellow(' ⚠ console index not refreshed') +
|
|
117
|
+
chalk.dim(` (${result.indexError}) — dashboard may be stale; it will retry next sync`));
|
|
118
|
+
}
|
|
112
119
|
}
|
|
113
120
|
async function handleStatus() {
|
|
114
121
|
const ledger = readSyncLedger();
|
package/dist/commands/trash.d.ts
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
import type { Command } from 'commander';
|
|
10
10
|
/**
|
|
11
11
|
* Restore a soft-deleted version back into ~/.agents/.history/versions/.
|
|
12
|
-
*
|
|
12
|
+
* Backs the top-level `agents restore` command.
|
|
13
13
|
* Exits the process with a non-zero code on any failure.
|
|
14
14
|
*/
|
|
15
15
|
export declare function restoreVersion(target: string): void;
|
|
16
16
|
/**
|
|
17
17
|
* Register the top-level `agents restore` command — a shorthand for
|
|
18
|
-
* `agents
|
|
18
|
+
* `agents restore` so users can undo a `remove`/`prune` directly.
|
|
19
19
|
*/
|
|
20
20
|
export declare function registerRestoreCommand(program: Command): void;
|
|
21
21
|
export declare function registerTrashCommands(program: Command): void;
|
package/dist/commands/trash.js
CHANGED
|
@@ -112,7 +112,7 @@ function humanSize(bytes) {
|
|
|
112
112
|
}
|
|
113
113
|
/**
|
|
114
114
|
* Restore a soft-deleted version back into ~/.agents/.history/versions/.
|
|
115
|
-
*
|
|
115
|
+
* Backs the top-level `agents restore` command.
|
|
116
116
|
* Exits the process with a non-zero code on any failure.
|
|
117
117
|
*/
|
|
118
118
|
export function restoreVersion(target) {
|
|
@@ -157,7 +157,7 @@ export function restoreVersion(target) {
|
|
|
157
157
|
}
|
|
158
158
|
/**
|
|
159
159
|
* Register the top-level `agents restore` command — a shorthand for
|
|
160
|
-
* `agents
|
|
160
|
+
* `agents restore` so users can undo a `remove`/`prune` directly.
|
|
161
161
|
*/
|
|
162
162
|
export function registerRestoreCommand(program) {
|
|
163
163
|
program
|
|
@@ -196,8 +196,4 @@ export function registerTrashCommands(program) {
|
|
|
196
196
|
console.log();
|
|
197
197
|
console.log(chalk.gray('Restore with: agents restore <agent>@<version>'));
|
|
198
198
|
});
|
|
199
|
-
trash
|
|
200
|
-
.command('restore <target>')
|
|
201
|
-
.description('Restore a soft-deleted version (e.g. "claude@2.1.110") back to ~/.agents/.history/versions/')
|
|
202
|
-
.action((target) => restoreVersion(target));
|
|
203
199
|
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Version management commands for installing, switching, and removing agent CLIs.
|
|
3
3
|
*
|
|
4
4
|
* Implements `agents add`, `agents prune`, `agents remove` (alias),
|
|
5
|
-
*
|
|
5
|
+
* and `agents use`. Handles npm-based installation,
|
|
6
6
|
* shim creation, config symlink
|
|
7
7
|
* switching, resource sync prompts, and project-level version pinning.
|
|
8
8
|
*/
|
|
9
9
|
import type { Command } from 'commander';
|
|
10
|
-
/** Register `agents add`, `agents prune`, `agents remove`,
|
|
10
|
+
/** Register `agents add`, `agents prune`, `agents remove`, and `agents use`. */
|
|
11
11
|
export declare function registerVersionsCommands(program: Command): void;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { addHostOption } from '../lib/hosts/option.js';
|
|
2
1
|
import chalk from 'chalk';
|
|
3
2
|
import ora from 'ora';
|
|
4
3
|
import * as fs from 'fs';
|
|
@@ -6,7 +5,6 @@ import * as path from 'path';
|
|
|
6
5
|
import { select, confirm, checkbox } from '@inquirer/prompts';
|
|
7
6
|
import { AGENTS, MANAGED_AGENT_IDS, accountOrgBadge, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, isAgentHardDeprecated, hardDeprecationError, isSelfUpdatingAgent, } from '../lib/agents.js';
|
|
8
7
|
import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, isUsageHeadlessScopeError, } from '../lib/accounting/usage.js';
|
|
9
|
-
import { viewAction } from './view.js';
|
|
10
8
|
import { resolveConfiguredModel, formatAgentIdentity } from '../lib/models.js';
|
|
11
9
|
import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
|
|
12
10
|
import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, isOldestInstalled, getGlobalDefault, setGlobalDefault, markVersionIsolated, setIsolatedDefault, isVersionIsolated, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/installations/versions.js';
|
|
@@ -288,7 +286,7 @@ function configureVersionPruneCommand(cmd, commandName) {
|
|
|
288
286
|
});
|
|
289
287
|
cmd.action((specs, options) => versionPruneAction(specs, options, commandName));
|
|
290
288
|
}
|
|
291
|
-
/** Register `agents add`, `agents prune`, `agents remove`,
|
|
289
|
+
/** Register `agents add`, `agents prune`, `agents remove`, and `agents use`. */
|
|
292
290
|
export function registerVersionsCommands(program) {
|
|
293
291
|
const addCmd = program
|
|
294
292
|
.command('add <specs...>')
|
|
@@ -943,11 +941,4 @@ export function registerVersionsCommands(program) {
|
|
|
943
941
|
throw err;
|
|
944
942
|
}
|
|
945
943
|
});
|
|
946
|
-
// Deprecated: use `agents view` instead
|
|
947
|
-
addHostOption(program.command('list [agent]'))
|
|
948
|
-
.description('List installed agent CLI versions')
|
|
949
|
-
.action(async (agentArg) => {
|
|
950
|
-
console.log(chalk.red('Deprecated: "agents list" is now "agents view"\n'));
|
|
951
|
-
await viewAction(agentArg);
|
|
952
|
-
});
|
|
953
944
|
}
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -72,8 +72,8 @@ export declare function renderHarnessBlocks(harnesses: ProfileSummary[], install
|
|
|
72
72
|
/**
|
|
73
73
|
* The USAGE-READ-2 decision, isolated from its runtime inputs so it can be
|
|
74
74
|
* tested directly: a usage read may fall through to the interactive OAuth login
|
|
75
|
-
* ONLY for a foreground human render on a `personal`
|
|
76
|
-
* required — role alone is not sufficient.
|
|
75
|
+
* ONLY for a foreground human render on a headed device (`personal` or
|
|
76
|
+
* `desktop`). Both conditions are required — role alone is not sufficient.
|
|
77
77
|
*/
|
|
78
78
|
export declare function allowInteractiveUsageLogin(role: ConfiguredDeviceRole | undefined, isTTY: boolean): boolean;
|
|
79
79
|
/** Build the set of resource sections to include in `--json` from the
|
package/dist/commands/view.js
CHANGED
|
@@ -9,7 +9,7 @@ import { ambientClaudeToken, loginHint } from '../lib/signin-badge.js';
|
|
|
9
9
|
import { machineId } from '../lib/machine-id.js';
|
|
10
10
|
import { authCacheKey, readAuthHealthCache } from '../lib/auth-health.js';
|
|
11
11
|
import { agentReportsUsage, classifyUsageErrorKind, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageBenignState, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, isUsageHeadlessScopeError, usageErrorForDisplay, } from '../lib/accounting/usage.js';
|
|
12
|
-
import { selfConfiguredDeviceRole } from '../lib/device-config.js';
|
|
12
|
+
import { isHeadedDeviceRole, selfConfiguredDeviceRole } from '../lib/device-config.js';
|
|
13
13
|
import { readManifest } from '../lib/manifest.js';
|
|
14
14
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, isVersionIsolated, getIsolatedDefault, } from '../lib/installations/versions.js';
|
|
15
15
|
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/installations/shims.js';
|
|
@@ -320,17 +320,18 @@ function renderHostClisSection(cwd) {
|
|
|
320
320
|
/**
|
|
321
321
|
* The USAGE-READ-2 decision, isolated from its runtime inputs so it can be
|
|
322
322
|
* tested directly: a usage read may fall through to the interactive OAuth login
|
|
323
|
-
* ONLY for a foreground human render on a `personal`
|
|
324
|
-
* required — role alone is not sufficient.
|
|
323
|
+
* ONLY for a foreground human render on a headed device (`personal` or
|
|
324
|
+
* `desktop`). Both conditions are required — role alone is not sufficient.
|
|
325
325
|
*/
|
|
326
326
|
export function allowInteractiveUsageLogin(role, isTTY) {
|
|
327
|
-
return role
|
|
327
|
+
return isHeadedDeviceRole(role) && isTTY === true;
|
|
328
328
|
}
|
|
329
329
|
/**
|
|
330
330
|
* Whether this `agents view` invocation may fall through to the interactive
|
|
331
331
|
* OAuth login for a usage read (USAGE-READ-2). True only for a foreground human
|
|
332
|
-
* render on a `personal`
|
|
333
|
-
* carrying the `user:profile` scope the usage endpoint needs,
|
|
332
|
+
* render on a headed device (`personal` or `desktop`): the interactive login is
|
|
333
|
+
* the sole credential carrying the `user:profile` scope the usage endpoint needs,
|
|
334
|
+
* and a human
|
|
334
335
|
* running one command is not the unattended-loop revocation risk RUSH-1822
|
|
335
336
|
* fixed. The `--json` path never reaches these render functions (it returns
|
|
336
337
|
* early via `collectAgentsJson`), and a non-TTY (piped/scripted) run is excluded
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* - `__secrets-get` / `__secrets-ping` / `__secrets-lock` (SYNC_* tokens)
|
|
16
16
|
* - `__shim`
|
|
17
17
|
* - `__claude-statusline`
|
|
18
|
+
* - `__usage-ingest` / `__usage-export`
|
|
18
19
|
* - `__daemon-run`
|
|
19
20
|
*
|
|
20
21
|
* The tokens are imported from the leaf module sync-commands.ts — the SAME
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* - `__secrets-get` / `__secrets-ping` / `__secrets-lock` (SYNC_* tokens)
|
|
16
16
|
* - `__shim`
|
|
17
17
|
* - `__claude-statusline`
|
|
18
|
+
* - `__usage-ingest` / `__usage-export`
|
|
18
19
|
* - `__daemon-run`
|
|
19
20
|
*
|
|
20
21
|
* The tokens are imported from the leaf module sync-commands.ts — the SAME
|
|
@@ -75,6 +76,19 @@ if (process.argv[2] === '__claude-statusline') {
|
|
|
75
76
|
const { runClaudeStatusLine } = await import('./lib/claude-statusline.js');
|
|
76
77
|
process.exit(await runClaudeStatusLine());
|
|
77
78
|
}
|
|
79
|
+
// Fleet usage-sync receiver: a headed peer pipes its identity-keyed usage rows to
|
|
80
|
+
// stdin, we merge them newest-wins into this box's cache (PHNX-3392). Above the
|
|
81
|
+
// bootstrap line for the same reason as __claude-statusline — no update check, no
|
|
82
|
+
// detached sync fork, and nothing writes to stdout to corrupt the caller's view.
|
|
83
|
+
if (process.argv[2] === '__usage-ingest') {
|
|
84
|
+
const { runUsageIngest } = await import('./lib/accounting/usage-ingest.js');
|
|
85
|
+
process.exit(await runUsageIngest());
|
|
86
|
+
}
|
|
87
|
+
if (process.argv[2] === '__usage-export') {
|
|
88
|
+
const { exportClaudeUsageCacheRows } = await import('./lib/accounting/usage.js');
|
|
89
|
+
process.stdout.write(JSON.stringify({ v: 1, rows: exportClaudeUsageCacheRows() }));
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
78
92
|
if (process.argv[2] === '__daemon-run') {
|
|
79
93
|
const { runDaemon, log: daemonLog } = await import('./lib/daemon/daemon.js');
|
|
80
94
|
// RUSH-2418: the daemon is the one always-on process here, and it ran with no
|
|
@@ -63,11 +63,15 @@ export declare function unbindAccount(nameOrId: string, target: string): void;
|
|
|
63
63
|
/** Every target bound to `accountId`: this box's device-doc bindings merged over
|
|
64
64
|
* the fleet-shared central bindings (PHNX-3315). */
|
|
65
65
|
export declare function accountBindings(accountId: string, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): string[];
|
|
66
|
+
export interface AccountSelection {
|
|
67
|
+
id: string;
|
|
68
|
+
source: 'explicit' | 'binding' | 'default';
|
|
69
|
+
}
|
|
66
70
|
/** Explicit selection wins over a configured per-harness default. */
|
|
67
71
|
export declare function resolveAccountSelection(explicit: string | undefined, agent: AgentId, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, opts?: {
|
|
68
72
|
useDefault?: boolean;
|
|
69
73
|
target?: string;
|
|
70
|
-
}):
|
|
74
|
+
}): AccountSelection | undefined;
|
|
71
75
|
export interface AddAccountOptions {
|
|
72
76
|
baseUrl?: string;
|
|
73
77
|
}
|
|
@@ -20,6 +20,7 @@ import * as fs from 'node:fs';
|
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import * as crypto from 'node:crypto';
|
|
22
22
|
import * as yaml from 'yaml';
|
|
23
|
+
import chalk from 'chalk';
|
|
23
24
|
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
24
25
|
import { getUserAgentsDir, readMeta, updateMeta } from './state.js';
|
|
25
26
|
import { deleteKeychainToken, getKeychainToken, hasKeychainToken } from './secrets/index.js';
|
|
@@ -307,18 +308,19 @@ export function accountBindings(accountId, meta) {
|
|
|
307
308
|
/** Explicit selection wins over a configured per-harness default. */
|
|
308
309
|
export function resolveAccountSelection(explicit, agent, meta, opts = {}) {
|
|
309
310
|
if (explicit)
|
|
310
|
-
return explicit;
|
|
311
|
+
return { id: explicit, source: 'explicit' };
|
|
311
312
|
// This box's device-doc bindings win over the fleet-shared central bindings
|
|
312
313
|
// (PHNX-3315), so a per-box account attachment resolves without touching the
|
|
313
314
|
// shared file. Defaults are genuinely fleet-shared and stay central.
|
|
314
315
|
const bindings = { ...meta.accounts?.bindings, ...meta.deviceAccounts?.bindings };
|
|
315
316
|
const bound = opts.target ? bindings[opts.target] : undefined;
|
|
316
317
|
if (bound)
|
|
317
|
-
return bound;
|
|
318
|
+
return { id: bound, source: 'binding' };
|
|
318
319
|
const deviceScoped = bindings[agent];
|
|
319
320
|
if (deviceScoped)
|
|
320
|
-
return deviceScoped;
|
|
321
|
-
|
|
321
|
+
return { id: deviceScoped, source: 'binding' };
|
|
322
|
+
const defaulted = opts.useDefault === false ? undefined : meta.accounts?.defaults?.[agent];
|
|
323
|
+
return defaulted ? { id: defaulted, source: 'default' } : undefined;
|
|
322
324
|
}
|
|
323
325
|
function profileConsumers(name, base) {
|
|
324
326
|
const dir = path.join(base, 'profiles');
|
|
@@ -371,19 +373,30 @@ export function renameAccount(oldName, newName, base = getUserAgentsDir()) {
|
|
|
371
373
|
const rows = nativeRowsForNameOrId(meta, oldName);
|
|
372
374
|
if (rows.length) {
|
|
373
375
|
assertUniqueUnifiedName(newName, meta, doc, new Set(rows.map(account => account.id)));
|
|
374
|
-
// Sweep every row for the identity (PHNX-3206) in its owning store (PHNX-3315)
|
|
376
|
+
// Sweep every row for the identity (PHNX-3206) in its owning store (PHNX-3315)
|
|
377
|
+
// and any per-harness default that points to the old name or row ids.
|
|
375
378
|
const rowScope = rows[0].scope;
|
|
379
|
+
const idsToRename = new Set([oldName, ...rows.map(row => row.id)]);
|
|
376
380
|
updateMeta(current => {
|
|
381
|
+
const defaults = { ...current.accounts?.defaults };
|
|
382
|
+
for (const [agent, value] of Object.entries(defaults)) {
|
|
383
|
+
if (idsToRename.has(value))
|
|
384
|
+
defaults[agent] = newName;
|
|
385
|
+
}
|
|
386
|
+
const next = { ...current, accounts: { ...current.accounts, defaults } };
|
|
377
387
|
if (rowScope === 'device') {
|
|
378
388
|
const native = { ...current.deviceAccounts?.native };
|
|
379
389
|
for (const row of rows)
|
|
380
390
|
native[row.id] = { ...native[row.id], name: newName };
|
|
381
|
-
|
|
391
|
+
next.deviceAccounts = { ...current.deviceAccounts, native };
|
|
382
392
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
393
|
+
else {
|
|
394
|
+
const native = { ...current.accounts?.native };
|
|
395
|
+
for (const row of rows)
|
|
396
|
+
native[row.id] = { ...native[row.id], name: newName };
|
|
397
|
+
next.accounts = { ...next.accounts, native };
|
|
398
|
+
}
|
|
399
|
+
return next;
|
|
387
400
|
});
|
|
388
401
|
return;
|
|
389
402
|
}
|
|
@@ -391,6 +404,15 @@ export function renameAccount(oldName, newName, base = getUserAgentsDir()) {
|
|
|
391
404
|
if (!account)
|
|
392
405
|
throw new Error(`Unknown account '${oldName}'.`);
|
|
393
406
|
assertUniqueUnifiedName(newName, meta, doc);
|
|
407
|
+
const idsToRename = new Set([account.name, account.id]);
|
|
408
|
+
updateMeta(current => {
|
|
409
|
+
const defaults = { ...current.accounts?.defaults };
|
|
410
|
+
for (const [agent, value] of Object.entries(defaults)) {
|
|
411
|
+
if (idsToRename.has(value))
|
|
412
|
+
defaults[agent] = newName;
|
|
413
|
+
}
|
|
414
|
+
return { ...current, accounts: { ...current.accounts, defaults } };
|
|
415
|
+
});
|
|
394
416
|
renameBundle(account.name, newName); // moves metadata + secret, preserves ACCOUNT_ID
|
|
395
417
|
renameProfileConsumers(account.name, newName, base);
|
|
396
418
|
}
|
|
@@ -422,7 +444,7 @@ export function removeAccount(name, base = getUserAgentsDir()) {
|
|
|
422
444
|
if (!account)
|
|
423
445
|
throw new Error(`Unknown account '${name}'.`);
|
|
424
446
|
const bindings = accountBindings(account.id, meta);
|
|
425
|
-
const defaults = Object.entries(meta.accounts?.defaults ?? {}).filter(([,
|
|
447
|
+
const defaults = Object.entries(meta.accounts?.defaults ?? {}).filter(([, value]) => value === account.id || value === account.name).map(([agent]) => agent);
|
|
426
448
|
if (bindings.length || defaults.length) {
|
|
427
449
|
const refs = [...bindings.map(target => `binding ${target}`), ...defaults.map(agent => `default ${agent}`)];
|
|
428
450
|
throw new Error(`Account '${account.name}' is still referenced by: ${refs.join(', ')}. Detach or clear those references before removing it.`);
|
|
@@ -505,9 +527,20 @@ export function resolveSpawnAccount(explicit, agent, version, meta, opts = {}) {
|
|
|
505
527
|
const selection = resolveAccountSelection(explicit, agent, meta, { useDefault: opts.useDefault, target });
|
|
506
528
|
if (!selection)
|
|
507
529
|
return null;
|
|
508
|
-
const unified = findUnifiedAccount(selection, meta);
|
|
509
|
-
if (!unified)
|
|
510
|
-
|
|
530
|
+
const unified = findUnifiedAccount(selection.id, meta);
|
|
531
|
+
if (!unified) {
|
|
532
|
+
// A stale per-harness default is a preference, not a hard requirement: the
|
|
533
|
+
// machine stays runnable by falling back to balanced rotation. Bindings and
|
|
534
|
+
// explicit --account are intentional, so they still fail loud.
|
|
535
|
+
if (selection.source === 'default') {
|
|
536
|
+
process.stderr.write(chalk.yellow(`[agents] default account '${selection.id}' for ${agent} no longer exists on this machine; falling back to balanced selection. Clear the stale default with: agents accounts clear-default ${agent}\n`));
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
const remedy = selection.source === 'binding'
|
|
540
|
+
? `Detach the stale binding with: agents accounts detach ${selection.id} ${target}`
|
|
541
|
+
: `Add an account named '${selection.id}' or remove the --account override.`;
|
|
542
|
+
throw new Error(`Unknown account '${selection.id}' for ${agent} harness. ${remedy}`);
|
|
543
|
+
}
|
|
511
544
|
if (unified.kind === 'native') {
|
|
512
545
|
if (unified.agent !== agent) {
|
|
513
546
|
throw new Error(`Account '${unified.name}' is a ${unified.agent} login and cannot authenticate the ${agent} harness.`);
|