@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.
- package/CHANGELOG.md +15 -0
- package/README.md +28 -1
- package/dist/bootstrap.js +40 -12
- package/dist/commands/accounts.d.ts +22 -0
- package/dist/commands/accounts.js +158 -35
- package/dist/commands/config.js +37 -0
- package/dist/commands/exec.js +21 -10
- package/dist/commands/update.js +169 -18
- package/dist/commands/versions.d.ts +11 -0
- package/dist/commands/versions.js +30 -4
- package/dist/commands/view.d.ts +69 -3
- package/dist/commands/view.js +235 -75
- package/dist/index.js +35 -2
- package/dist/lib/account-catalog.d.ts +97 -1
- package/dist/lib/account-catalog.js +134 -6
- package/dist/lib/account-registry.d.ts +43 -0
- package/dist/lib/account-registry.js +97 -2
- package/dist/lib/accounting/rotate.d.ts +2 -2
- package/dist/lib/accounting/rotate.js +5 -5
- package/dist/lib/accounts/auth-operation-lock.d.ts +9 -0
- package/dist/lib/accounts/auth-operation-lock.js +55 -0
- package/dist/lib/accounts/connect.d.ts +170 -0
- package/dist/lib/accounts/connect.js +383 -0
- package/dist/lib/capabilities.js +2 -0
- package/dist/lib/commands.js +2 -0
- package/dist/lib/config-keys.d.ts +11 -2
- package/dist/lib/config-keys.js +21 -1
- package/dist/lib/daemon/daemon.js +5 -0
- package/dist/lib/daemon/harness-update-service.d.ts +110 -0
- package/dist/lib/daemon/harness-update-service.js +216 -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 +0 -1
- package/dist/lib/device-config.js +50 -5
- package/dist/lib/exec.js +26 -2
- package/dist/lib/fs-atomic.d.ts +2 -0
- package/dist/lib/fs-atomic.js +2 -0
- package/dist/lib/hooks/install.js +7 -2
- package/dist/lib/installations/active-check.d.ts +48 -0
- package/dist/lib/installations/active-check.js +84 -0
- package/dist/lib/installations/index.d.ts +5 -1
- package/dist/lib/installations/index.js +4 -0
- package/dist/lib/installations/installation-lock.d.ts +6 -0
- package/dist/lib/installations/installation-lock.js +29 -0
- package/dist/lib/installations/launch-gate.d.ts +69 -0
- package/dist/lib/installations/launch-gate.js +133 -0
- package/dist/lib/installations/native-command.d.ts +5 -0
- package/dist/lib/installations/native-command.js +52 -0
- package/dist/lib/installations/shims.d.ts +8 -2
- package/dist/lib/installations/shims.js +105 -2
- package/dist/lib/installations/store.d.ts +5 -1
- package/dist/lib/installations/store.js +24 -3
- package/dist/lib/installations/strategies.js +55 -35
- package/dist/lib/installations/types.d.ts +17 -0
- package/dist/lib/installations/update-cancellation.d.ts +82 -0
- package/dist/lib/installations/update-cancellation.js +122 -0
- package/dist/lib/installations/update-policy.d.ts +69 -0
- package/dist/lib/installations/update-policy.js +114 -0
- package/dist/lib/installations/update-runtime.d.ts +118 -0
- package/dist/lib/installations/update-runtime.js +321 -0
- package/dist/lib/installations/update.d.ts +25 -0
- package/dist/lib/installations/update.js +141 -2
- package/dist/lib/installations/versions.d.ts +1 -0
- package/dist/lib/installations/versions.js +166 -131
- package/dist/lib/platform/process.d.ts +3 -1
- package/dist/lib/platform/process.js +2 -2
- package/dist/lib/staleness/detectors/commands.d.ts +1 -2
- package/dist/lib/staleness/detectors/hooks.d.ts +1 -2
- package/dist/lib/staleness/detectors/mcp.d.ts +1 -2
- package/dist/lib/staleness/detectors/permissions.d.ts +1 -2
- package/dist/lib/staleness/detectors/plugins.d.ts +1 -7
- package/dist/lib/staleness/detectors/rules.d.ts +1 -2
- package/dist/lib/staleness/detectors/skills.d.ts +1 -2
- package/dist/lib/staleness/detectors/subagents.d.ts +1 -7
- package/dist/lib/staleness/detectors/workflows.d.ts +1 -2
- package/dist/lib/staleness/writers/commands.d.ts +1 -2
- package/dist/lib/staleness/writers/hooks.d.ts +1 -2
- package/dist/lib/staleness/writers/mcp.d.ts +1 -2
- package/dist/lib/staleness/writers/permissions.d.ts +1 -12
- package/dist/lib/staleness/writers/plugins.d.ts +1 -6
- package/dist/lib/staleness/writers/rules.d.ts +1 -2
- package/dist/lib/staleness/writers/skills.d.ts +1 -2
- package/dist/lib/staleness/writers/subagents.d.ts +1 -2
- package/dist/lib/staleness/writers/workflows.d.ts +1 -8
- package/dist/lib/state.d.ts +3 -1
- package/dist/lib/state.js +38 -13
- package/dist/lib/types.d.ts +26 -1
- package/dist/lib/types.js +5 -0
- package/dist/lib/view-types.d.ts +6 -0
- package/package.json +1 -1
package/dist/commands/exec.js
CHANGED
|
@@ -2218,13 +2218,21 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2218
2218
|
process.exit(1);
|
|
2219
2219
|
}
|
|
2220
2220
|
const { resolveAccountVersion } = await import('../lib/accounting/rotate.js');
|
|
2221
|
-
|
|
2221
|
+
const { nativeAccountHome } = await import('../lib/account-registry.js');
|
|
2222
|
+
const { readMeta } = await import('../lib/state.js');
|
|
2223
|
+
accountConfigVersion = await resolveAccountVersion(agent, spawnAccount.identityKey, nativeAccountHome(spawnAccount.id, readMeta())) ?? undefined;
|
|
2222
2224
|
if (!accountConfigVersion) {
|
|
2223
2225
|
console.error(chalk.red(`No installed ${spawnAccount.agent} version is signed in as the identity labeled '${spawnAccount.name}'. Sign in as that identity, or label a different account.`));
|
|
2224
2226
|
process.exit(1);
|
|
2225
2227
|
}
|
|
2228
|
+
// An account-only run executes the account's own stable installation.
|
|
2229
|
+
// This keeps its model/mode configuration and binary update policy
|
|
2230
|
+
// together. An explicit installation or profile still controls the
|
|
2231
|
+
// executable independently; configVersion continues to select auth.
|
|
2232
|
+
if (!version && !fromProfile)
|
|
2233
|
+
version = accountConfigVersion;
|
|
2226
2234
|
if (!options.quiet)
|
|
2227
|
-
process.stderr.write(chalk.gray(`[agents] account '${spawnAccount.name}' · ${agent}
|
|
2235
|
+
process.stderr.write(chalk.gray(`[agents] account '${spawnAccount.name}' · ${agent}\n`));
|
|
2228
2236
|
}
|
|
2229
2237
|
else {
|
|
2230
2238
|
accountEnv = spawnAccount.env;
|
|
@@ -2666,11 +2674,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2666
2674
|
// Login preflight (advisory, warn + continue). On a local INTERACTIVE
|
|
2667
2675
|
// launch, probe whether this agent's account has a credential and print a
|
|
2668
2676
|
// one-line warning if it looks logged out — so you find out BEFORE the TUI
|
|
2669
|
-
// opens, not after typing a prompt and getting "/login" back.
|
|
2670
|
-
// account
|
|
2671
|
-
//
|
|
2672
|
-
// correct for HOME-global credential agents (grok/codex) where a per-version
|
|
2673
|
-
// home would false-negative. It can still false-negative for opaque
|
|
2677
|
+
// opens, not after typing a prompt and getting "/login" back. Inspect the
|
|
2678
|
+
// selected account home, not an unrelated global login. This file-based
|
|
2679
|
+
// read makes no Keychain ACL prompt. It can still false-negative for opaque
|
|
2674
2680
|
// credentials, so this NEVER blocks — it warns and launches anyway. Skipped
|
|
2675
2681
|
// for --json/--quiet, when a rotation already picked a signed-in account,
|
|
2676
2682
|
// and via --no-auth-check / AGENTS_NO_AUTH_CHECK=1. (--device/--lease return
|
|
@@ -2688,12 +2694,13 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2688
2694
|
// `signInLaunch` means the zero-healthy path already reported this exact
|
|
2689
2695
|
// account as logged out and named the login command, so re-probing here
|
|
2690
2696
|
// only prints a second, near-identical warning.
|
|
2691
|
-
rotated: !!rotationResult || accountPickerRequested || signInLaunch,
|
|
2697
|
+
rotated: !!rotationResult || accountPickerRequested || signInLaunch || spawnAccount?.kind === 'provider',
|
|
2692
2698
|
});
|
|
2693
2699
|
if (preflight) {
|
|
2694
2700
|
try {
|
|
2695
2701
|
const { getAccountInfo } = await import('../lib/agents.js');
|
|
2696
|
-
const
|
|
2702
|
+
const authVersion = accountConfigVersion ?? version;
|
|
2703
|
+
const info = await getAccountInfo(agent, authVersion ? getVersionHomePath(agent, authVersion) : undefined);
|
|
2697
2704
|
// Claude authenticates interactively from a per-version setup-token on a
|
|
2698
2705
|
// keychain-less worker (the shim's .oauth_token fallback), which the
|
|
2699
2706
|
// native-credential probe above can't see — so don't warn "logged out" when
|
|
@@ -2706,7 +2713,11 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2706
2713
|
authedViaSetupToken = resolveClaudeSetupToken(getVersionHomePath('claude', version)) !== null;
|
|
2707
2714
|
}
|
|
2708
2715
|
if (!info.signedIn && !authedViaSetupToken) {
|
|
2709
|
-
|
|
2716
|
+
const { connectSupported } = await import('../lib/accounts/connect.js');
|
|
2717
|
+
const hint = connectSupported(agent)
|
|
2718
|
+
? `agents accounts connect ${agent}${spawnAccount?.kind === 'native' ? ` ${spawnAccount.name}` : ''}`
|
|
2719
|
+
: loginHint(agent);
|
|
2720
|
+
process.stderr.write(chalk.yellow(`${agent} looks logged out — sign in with: ${hint}. Launching anyway...\n`));
|
|
2710
2721
|
}
|
|
2711
2722
|
}
|
|
2712
2723
|
catch {
|
package/dist/commands/update.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { formatAgentError, resolveAgentName } from '../lib/agents.js';
|
|
2
|
+
import { AGENTS, formatAgentError, resolveAgentName } from '../lib/agents.js';
|
|
3
3
|
import { setHelpSections } from '../lib/help.js';
|
|
4
|
-
import { describeInstallation, listInstallations, resolveInstallation, selectUpdateStrategy, supportsPinnedUpdate, updateInstallation, } from '../lib/installations/index.js';
|
|
4
|
+
import { describeInstallation, effectiveUpdatePolicy, listInstallations, planAutoUpdates, resolveInstallation, runAutoUpdatePass, selectUpdateStrategy, supportsPinnedUpdate, updateInstallation, } from '../lib/installations/index.js';
|
|
5
5
|
/**
|
|
6
6
|
* Split `<agent>[@<selector>]`. The selector names an INSTALLATION — its frozen
|
|
7
7
|
* label, or the release it currently carries — never a release to install; that
|
|
@@ -38,11 +38,16 @@ function printOutcome(outcome, json) {
|
|
|
38
38
|
fromRelease: outcome.fromRelease,
|
|
39
39
|
toRelease: outcome.toRelease,
|
|
40
40
|
unchanged: outcome.unchanged,
|
|
41
|
+
deferred: outcome.deferred,
|
|
41
42
|
alsoUpdated: outcome.alsoUpdated.map(serialize),
|
|
42
43
|
}, null, 2));
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
const name = `${outcome.installation.agent}@${outcome.installation.label}`;
|
|
47
|
+
if (outcome.deferred) {
|
|
48
|
+
console.log(chalk.yellow(`${name}: ${outcome.deferred} Still on release ${outcome.installation.releaseVersion}.`));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
46
51
|
if (outcome.unchanged) {
|
|
47
52
|
console.log(chalk.gray(`${name} is already on release ${outcome.toRelease}.`));
|
|
48
53
|
return;
|
|
@@ -78,37 +83,174 @@ function fail(err) {
|
|
|
78
83
|
console.error(chalk.red(err.message));
|
|
79
84
|
process.exitCode = 1;
|
|
80
85
|
}
|
|
86
|
+
function serializePlanEntry(entry) {
|
|
87
|
+
const wouldUpdate = entry.eligible && !entry.deferred
|
|
88
|
+
&& !!entry.targetRelease && entry.targetRelease !== entry.currentRelease;
|
|
89
|
+
return {
|
|
90
|
+
agent: entry.agent,
|
|
91
|
+
label: entry.installation.label,
|
|
92
|
+
currentRelease: entry.currentRelease,
|
|
93
|
+
targetRelease: entry.targetRelease,
|
|
94
|
+
policy: entry.policy,
|
|
95
|
+
eligible: entry.eligible,
|
|
96
|
+
deferred: entry.deferred,
|
|
97
|
+
wouldUpdate,
|
|
98
|
+
reason: entry.reason,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** `--check`'s dry-run output: eligibility/current/target/policy/deferral, machine-readable with `--json`. */
|
|
102
|
+
function printPlan(plan, json) {
|
|
103
|
+
const rows = plan.map(serializePlanEntry);
|
|
104
|
+
if (json) {
|
|
105
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (rows.length === 0) {
|
|
109
|
+
console.log(chalk.gray('No managed installations to check.'));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log(chalk.bold('Automatic-update plan\n'));
|
|
113
|
+
for (const row of rows) {
|
|
114
|
+
const name = `${row.agent}@${row.label}`;
|
|
115
|
+
const status = row.wouldUpdate
|
|
116
|
+
? chalk.green(`would update ${row.currentRelease} -> ${row.targetRelease}`)
|
|
117
|
+
: row.deferred
|
|
118
|
+
? chalk.yellow(`deferred — ${row.reason}`)
|
|
119
|
+
: !row.eligible
|
|
120
|
+
? chalk.gray(`not eligible — ${row.reason}`)
|
|
121
|
+
: chalk.gray('already current');
|
|
122
|
+
console.log(` ${chalk.cyan(name.padEnd(28))} policy=${row.policy.padEnd(7)} ${status}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* `--to` also decides the installation's update policy going forward: a
|
|
127
|
+
* concrete release is a manual pin (excluded from the automatic pass until
|
|
128
|
+
* explicitly unpinned), `latest` (the default) unpins it. Returns `null` when
|
|
129
|
+
* `--to` was not passed at all, meaning: don't touch the stored policy.
|
|
130
|
+
*/
|
|
131
|
+
function policyForTo(to) {
|
|
132
|
+
if (to === undefined)
|
|
133
|
+
return null;
|
|
134
|
+
return to === 'latest' ? 'latest' : 'pinned';
|
|
135
|
+
}
|
|
136
|
+
async function updateOne(agent, installation, options) {
|
|
137
|
+
const strategy = selectUpdateStrategy(agent);
|
|
138
|
+
if (!options.json && !strategy.transactional) {
|
|
139
|
+
console.log(chalk.yellow(`${agent} installs one vendor-managed binary, so this update cannot be staged or rolled back; `
|
|
140
|
+
+ `a failure leaves whatever its installer wrote.`));
|
|
141
|
+
}
|
|
142
|
+
if (!options.json) {
|
|
143
|
+
console.log(chalk.gray(`Updating ${agent}@${describeInstallation(installation)} via the ${strategy.id} strategy...`));
|
|
144
|
+
}
|
|
145
|
+
const outcome = await updateInstallation(installation, {
|
|
146
|
+
to: options.to,
|
|
147
|
+
updatePolicy: policyForTo(options.to) ?? undefined,
|
|
148
|
+
onProgress: options.json ? undefined : (message) => console.log(chalk.gray(` ${message}`)),
|
|
149
|
+
});
|
|
150
|
+
return outcome;
|
|
151
|
+
}
|
|
81
152
|
export function registerUpdateCommand(program) {
|
|
82
153
|
const update = program
|
|
83
154
|
.command('update [target]')
|
|
84
155
|
.description('Move a frozen agent installation to a new release, keeping its name and every reference to it')
|
|
85
|
-
.option('--to <release>', 'Release to move to: latest (default), oldest, or an exact version')
|
|
156
|
+
.option('--to <release>', 'Release to move to: latest (default, and unpins), oldest, or an exact version (pins)')
|
|
86
157
|
.option('--json', 'Machine-readable result')
|
|
158
|
+
.option('--check', 'Dry run: report eligibility/current/target/policy/deferral without changing anything')
|
|
159
|
+
.option('--auto', 'Run the same automatic-update pass the daemon runs, across every managed harness (ignores <target>)')
|
|
87
160
|
.action(async (target, options) => {
|
|
88
161
|
try {
|
|
162
|
+
if (options.auto || (options.check && !target)) {
|
|
163
|
+
if (options.check) {
|
|
164
|
+
printPlan(await planAutoUpdates(), !!options.json);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const result = await runAutoUpdatePass({
|
|
168
|
+
onProgress: options.json ? undefined : (message) => console.log(chalk.gray(` ${message}`)),
|
|
169
|
+
});
|
|
170
|
+
if (options.json) {
|
|
171
|
+
console.log(JSON.stringify({
|
|
172
|
+
plan: result.plan.map(serializePlanEntry),
|
|
173
|
+
outcomes: result.outcomes.map((o) => ({
|
|
174
|
+
agent: o.entry.agent,
|
|
175
|
+
label: o.entry.installation.label,
|
|
176
|
+
outcome: o.outcome && {
|
|
177
|
+
fromRelease: o.outcome.fromRelease,
|
|
178
|
+
toRelease: o.outcome.toRelease,
|
|
179
|
+
unchanged: o.outcome.unchanged,
|
|
180
|
+
deferred: o.outcome.deferred,
|
|
181
|
+
},
|
|
182
|
+
error: o.error,
|
|
183
|
+
})),
|
|
184
|
+
}, null, 2));
|
|
185
|
+
}
|
|
186
|
+
else if (result.outcomes.length === 0) {
|
|
187
|
+
console.log(chalk.gray('Nothing eligible to update this pass.'));
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
for (const o of result.outcomes) {
|
|
191
|
+
const name = `${o.entry.agent}@${o.entry.installation.label}`;
|
|
192
|
+
if (o.error)
|
|
193
|
+
console.error(chalk.red(`${name}: ${o.error}`));
|
|
194
|
+
else if (o.outcome)
|
|
195
|
+
printOutcome(o.outcome, false);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (result.outcomes.some((o) => o.error))
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
89
202
|
if (!target) {
|
|
90
|
-
throw new Error('Which agent? Use: agents update <agent>[@<installed-version>]');
|
|
203
|
+
throw new Error('Which agent? Use: agents update <agent>[@<installed-version>], or agents update --auto.');
|
|
91
204
|
}
|
|
92
205
|
const { agent, selector } = parseTarget(target);
|
|
206
|
+
if (options.check) {
|
|
207
|
+
const plan = (await planAutoUpdates({ agents: [agent] })).filter((entry) => !selector || entry.installation.label === selector || entry.installation.releaseVersion === selector);
|
|
208
|
+
if (selector && plan.length === 0) {
|
|
209
|
+
throw new Error(`No ${AGENTS[agent].name} installation matches '${selector}'.`);
|
|
210
|
+
}
|
|
211
|
+
printPlan(plan, !!options.json);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
93
214
|
if (options.to && options.to !== 'latest' && !supportsPinnedUpdate(agent)) {
|
|
94
215
|
// Fail loud at the boundary rather than installing the current release
|
|
95
216
|
// and reporting it as the pin that was asked for.
|
|
96
217
|
throw new Error(`${agent} is a single self-updating binary with no pinnable releases — drop --to, or pass --to latest.`);
|
|
97
218
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
219
|
+
if (selector) {
|
|
220
|
+
const installation = await resolveInstallation(agent, selector);
|
|
221
|
+
const outcome = await updateOne(agent, installation, options);
|
|
222
|
+
printOutcome(outcome, !!options.json);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// Bare `<agent>`, no `@label`: act on every installation of that agent
|
|
226
|
+
// that isn't explicitly pinned — the bulk case. A single installation
|
|
227
|
+
// keeps the old single-target behavior even if it happens to be pinned,
|
|
228
|
+
// since naming the agent with only one installed copy is unambiguous
|
|
229
|
+
// about which one is meant; `--to` still applies to it either way.
|
|
230
|
+
const installations = listInstallations(agent);
|
|
231
|
+
if (installations.length === 0) {
|
|
232
|
+
throw new Error(`No ${AGENTS[agent].name} installations are managed by agents-cli. Install one with: agents add ${agent}@latest`);
|
|
233
|
+
}
|
|
234
|
+
const targets = installations.length === 1
|
|
235
|
+
? installations
|
|
236
|
+
: installations.filter((i) => effectiveUpdatePolicy(i) !== 'pinned');
|
|
237
|
+
if (installations.length > 1 && targets.length === 0) {
|
|
238
|
+
throw new Error(`Every ${AGENTS[agent].name} installation is pinned to a concrete release. `
|
|
239
|
+
+ `Select one explicitly: agents update ${agent}@<label> --to latest`);
|
|
103
240
|
}
|
|
104
|
-
|
|
105
|
-
|
|
241
|
+
let anyError = false;
|
|
242
|
+
for (const installation of targets) {
|
|
243
|
+
try {
|
|
244
|
+
const outcome = await updateOne(agent, installation, options);
|
|
245
|
+
printOutcome(outcome, !!options.json);
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
anyError = true;
|
|
249
|
+
fail(err);
|
|
250
|
+
}
|
|
106
251
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
onProgress: options.json ? undefined : (message) => console.log(chalk.gray(` ${message}`)),
|
|
110
|
-
});
|
|
111
|
-
printOutcome(outcome, !!options.json);
|
|
252
|
+
if (anyError)
|
|
253
|
+
process.exitCode = 1;
|
|
112
254
|
}
|
|
113
255
|
catch (err) {
|
|
114
256
|
fail(err);
|
|
@@ -136,11 +278,20 @@ export function registerUpdateCommand(program) {
|
|
|
136
278
|
examples: `agents update list claude
|
|
137
279
|
agents update claude@2.0.65
|
|
138
280
|
agents update claude@2.0.65 --to 2.1.220
|
|
139
|
-
agents update claude@2.0.65 --
|
|
281
|
+
agents update claude@2.0.65 --to latest
|
|
282
|
+
agents update claude
|
|
283
|
+
agents update --check
|
|
284
|
+
agents update claude --check --json
|
|
285
|
+
agents update --auto`,
|
|
140
286
|
notes: 'An installation keeps its name for life; only the release inside it moves. That is why a default, a project pin, '
|
|
141
287
|
+ 'a routine version, or a profile that names claude@2.0.65 keeps working after you update it. '
|
|
142
288
|
+ 'The selector matches either the installation name or the release it currently carries — when two installations '
|
|
143
289
|
+ 'share a release, select one by its installation name. '
|
|
144
|
-
+ 'The new release is fetched and launched before it replaces the working one, so a bad release leaves your agent running.'
|
|
290
|
+
+ 'The new release is fetched and launched before it replaces the working one, so a bad release leaves your agent running. '
|
|
291
|
+
+ 'A bare `<agent>` (no @label) updates every one of its installations that is not pinned; `--to <release>` pins the '
|
|
292
|
+
+ 'installations it touches, `--to latest` unpins them. Pinned and manual/vendor-managed installations are also skipped '
|
|
293
|
+
+ 'by the automatic background pass (agents config set updates.auto / updates.<agent>.auto). `--check` reports what '
|
|
294
|
+
+ '`--auto` would do without changing anything; `--auto` (no target) runs the exact pass the daemon runs, across every '
|
|
295
|
+
+ 'harness.',
|
|
145
296
|
});
|
|
146
297
|
}
|
|
@@ -7,5 +7,16 @@
|
|
|
7
7
|
* switching, resource sync prompts, and project-level version pinning.
|
|
8
8
|
*/
|
|
9
9
|
import type { Command } from 'commander';
|
|
10
|
+
/** Bare installs reuse an account home; explicit labels keep their expert semantics. */
|
|
11
|
+
export declare function planAccountFirstInstall(input: {
|
|
12
|
+
spec: string;
|
|
13
|
+
supported: boolean;
|
|
14
|
+
isolated: boolean;
|
|
15
|
+
labels: string[];
|
|
16
|
+
defaultLabel: string | null;
|
|
17
|
+
}): {
|
|
18
|
+
existingLabel?: string;
|
|
19
|
+
installationLabel?: string;
|
|
20
|
+
};
|
|
10
21
|
/** Register `agents add`, `agents prune`, `agents remove`, and `agents use`. */
|
|
11
22
|
export declare function registerVersionsCommands(program: Command): void;
|
|
@@ -15,6 +15,15 @@ import { tryAutoPullSystemRepo } from '../lib/git.js';
|
|
|
15
15
|
import { getAgentsDir, getTrashVersionsDir } from '../lib/state.js';
|
|
16
16
|
import { setHelpSections } from '../lib/help.js';
|
|
17
17
|
import { updateSessionFilePaths } from '../lib/session/db.js';
|
|
18
|
+
import { connectSupported } from '../lib/accounts/connect.js';
|
|
19
|
+
/** Bare installs reuse an account home; explicit labels keep their expert semantics. */
|
|
20
|
+
export function planAccountFirstInstall(input) {
|
|
21
|
+
if (!input.supported || input.isolated || input.spec.includes('@'))
|
|
22
|
+
return {};
|
|
23
|
+
const existingLabel = input.defaultLabel && input.labels.includes(input.defaultLabel)
|
|
24
|
+
? input.defaultLabel : input.labels[0];
|
|
25
|
+
return existingLabel ? { existingLabel } : { installationLabel: 'main' };
|
|
26
|
+
}
|
|
18
27
|
/**
|
|
19
28
|
* After removeVersion soft-deletes a version dir to trash, rewrite session
|
|
20
29
|
* file_path entries in the DB so reads still work from the new trash location.
|
|
@@ -317,7 +326,7 @@ export function registerVersionsCommands(program) {
|
|
|
317
326
|
notes: `
|
|
318
327
|
- The first version you install becomes the default automatically.
|
|
319
328
|
- 'add' does NOT change the default if a default already exists. Use 'agents use' to switch.
|
|
320
|
-
-
|
|
329
|
+
- Bare Claude/Codex installs reuse your existing home. Connect another account with 'agents accounts connect <agent> [name]'.
|
|
321
330
|
- --isolated installs a self-contained copy: it never sets the default, never creates the bare '<agent>' shim, and never backs up or symlinks your real ~/.<agent>. Run it with 'agents run <agent>@<version>' and remove it with 'agents remove <agent>@<version> --isolated'. Mutually exclusive with --project.
|
|
322
331
|
`,
|
|
323
332
|
});
|
|
@@ -376,7 +385,19 @@ export function registerVersionsCommands(program) {
|
|
|
376
385
|
// Check if already installed (resolve 'latest'/'oldest' against npm first)
|
|
377
386
|
let alreadyInstalled = false;
|
|
378
387
|
let installedAsVersion = version;
|
|
379
|
-
|
|
388
|
+
const accountInstall = planAccountFirstInstall({
|
|
389
|
+
spec, supported: connectSupported(agent), isolated: !!isIsolated,
|
|
390
|
+
labels: listInstalledVersions(agent).filter(label => !isVersionIsolated(agent, label)),
|
|
391
|
+
defaultLabel: getGlobalDefault(agent),
|
|
392
|
+
});
|
|
393
|
+
if (accountInstall.existingLabel) {
|
|
394
|
+
alreadyInstalled = true;
|
|
395
|
+
installedAsVersion = accountInstall.existingLabel;
|
|
396
|
+
}
|
|
397
|
+
else if (accountInstall.installationLabel) {
|
|
398
|
+
// A fresh managed home has a stable label, independent of npm's release.
|
|
399
|
+
}
|
|
400
|
+
else if (version === 'latest') {
|
|
380
401
|
const latestCheck = await isLatestInstalled(agent);
|
|
381
402
|
if (latestCheck.installed && latestCheck.version) {
|
|
382
403
|
alreadyInstalled = true;
|
|
@@ -407,7 +428,12 @@ export function registerVersionsCommands(program) {
|
|
|
407
428
|
finalizeIsolatedInstall(agent, installedAsVersion);
|
|
408
429
|
continue;
|
|
409
430
|
}
|
|
410
|
-
console.log(chalk.gray(
|
|
431
|
+
console.log(chalk.gray(accountInstall.existingLabel
|
|
432
|
+
? `${agentLabel(agentConfig.id)} is already installed. Your account home is unchanged.`
|
|
433
|
+
: `${agentLabel(agentConfig.id)}@${installedAsVersion} already installed`));
|
|
434
|
+
if (accountInstall.existingLabel) {
|
|
435
|
+
console.log(chalk.gray(` Accounts: agents view ${agent}. Update now: agents update ${agent}.`));
|
|
436
|
+
}
|
|
411
437
|
// Ensure shim exists (in case it was deleted or needs updating)
|
|
412
438
|
createShim(agent);
|
|
413
439
|
}
|
|
@@ -415,7 +441,7 @@ export function registerVersionsCommands(program) {
|
|
|
415
441
|
const spinner = ora(`Installing ${agentLabel(agentConfig.id)}@${version}...`).start();
|
|
416
442
|
const result = await installVersion(agent, version, (msg) => {
|
|
417
443
|
spinner.text = msg;
|
|
418
|
-
});
|
|
444
|
+
}, accountInstall.installationLabel ? { installationLabel: accountInstall.installationLabel } : undefined);
|
|
419
445
|
if (result.success) {
|
|
420
446
|
const installedVer = result.installedVersion || version;
|
|
421
447
|
const installedModel = resolveConfiguredModel(agentConfig.id, installedVer)?.model;
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { AccountInfo } from '../lib/agents.js';
|
|
|
12
12
|
import type { AgentId } from '../lib/types.js';
|
|
13
13
|
import type { FormatUsageSummaryOpts, UsageInfo } from '../lib/accounting/usage.js';
|
|
14
14
|
import { type ConfiguredDeviceRole } from '../lib/device-config.js';
|
|
15
|
+
import { type NativeAccountCatalogRow } from '../lib/account-catalog.js';
|
|
15
16
|
import type { ResourceSection, ViewJsonAgent } from '../lib/view-types.js';
|
|
16
17
|
export type { ResourceItemJson, ResourceSection, SyncState, VersionResourcesJson, ViewJsonAgent, ViewJsonVersion } from '../lib/view-types.js';
|
|
17
18
|
import { type ProfileSummary } from '../lib/profiles.js';
|
|
@@ -26,6 +27,8 @@ export declare const accountColumnLabel: typeof accountDisplayLabel;
|
|
|
26
27
|
* display label when the identity is unnamed.
|
|
27
28
|
*/
|
|
28
29
|
export declare function namedAccountColumnLabel(agentId: AgentId, info: AccountInfo | undefined): string;
|
|
30
|
+
/** Human account identity; release labels belong only in installation diagnostics. */
|
|
31
|
+
export declare function nativeAccountViewLabel(row: Pick<NativeAccountCatalogRow, 'name' | 'display'>): string;
|
|
29
32
|
export interface AccountOrderedVersion {
|
|
30
33
|
version: string;
|
|
31
34
|
email: string | null;
|
|
@@ -92,6 +95,23 @@ export declare function parseResourceSections(options: {
|
|
|
92
95
|
export declare function collectAgentsJson(filterAgentId?: AgentId, resourceSections?: Set<ResourceSection>, opts?: {
|
|
93
96
|
forceRefresh?: boolean;
|
|
94
97
|
}): Promise<ViewJsonAgent[]>;
|
|
98
|
+
interface PrunePlanEntry {
|
|
99
|
+
agentId: AgentId;
|
|
100
|
+
version: string;
|
|
101
|
+
email: string;
|
|
102
|
+
keeper: string;
|
|
103
|
+
isDefault: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* 'duplicate' — older version sharing an email with a newer install.
|
|
106
|
+
* 'home-leftover' — home-only dir left over after a previous removeVersion;
|
|
107
|
+
* no binary, but transcripts may still live here.
|
|
108
|
+
*/
|
|
109
|
+
reason: 'duplicate' | 'home-leftover';
|
|
110
|
+
}
|
|
111
|
+
interface AgentPrunePlan {
|
|
112
|
+
agentId: AgentId;
|
|
113
|
+
toPrune: PrunePlanEntry[];
|
|
114
|
+
}
|
|
95
115
|
/**
|
|
96
116
|
* Identity key for duplicate-install detection. Prefers accountKey — which
|
|
97
117
|
* encodes account AND org — over the bare email: two installs can share an
|
|
@@ -101,10 +121,55 @@ export declare function collectAgentsJson(filterAgentId?: AgentId, resourceSecti
|
|
|
101
121
|
* identity key. Null when there is no usable identity.
|
|
102
122
|
*/
|
|
103
123
|
export declare function pruneGroupKey(info: Pick<AccountInfo, 'accountKey' | 'email'>): string | null;
|
|
124
|
+
/** One installed home, reduced to the fields duplicate detection needs. */
|
|
125
|
+
export interface PruneCandidate {
|
|
126
|
+
/** The version-dir label (opaque slot id). */
|
|
127
|
+
version: string;
|
|
128
|
+
/** The RUNNING release inside the home (installation.releaseVersion), or the label. */
|
|
129
|
+
release: string;
|
|
130
|
+
email: string | null;
|
|
131
|
+
/** Present only once the home's native identity is captured (claude: account+org). */
|
|
132
|
+
accountKey: string | null;
|
|
133
|
+
signedIn: boolean;
|
|
134
|
+
hasBinary: boolean;
|
|
135
|
+
}
|
|
136
|
+
/** A duplicate home to retire, and the keeper it collapses into. */
|
|
137
|
+
export interface DuplicatePruneEntry {
|
|
138
|
+
version: string;
|
|
139
|
+
email: string;
|
|
140
|
+
keeper: string;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* PURE duplicate-home detection: given every installed home for one agent, decide
|
|
144
|
+
* which are redundant duplicates of the SAME logical account and which single home
|
|
145
|
+
* to keep per account. Extracted from buildAgentPrunePlan so the keeper/merge rules
|
|
146
|
+
* are unit-tested without touching disk.
|
|
147
|
+
*
|
|
148
|
+
* Two rules, both conservative:
|
|
149
|
+
* - GROUPING. A home with a captured `accountKey` groups by it (account+org, so a
|
|
150
|
+
* personal Max and a Team seat on the same email stay separate). A home with NO
|
|
151
|
+
* accountKey folds into a same-email account ONLY when a sibling home for that
|
|
152
|
+
* email IS identified — that email-only home is an incompletely-captured re-login
|
|
153
|
+
* of the known account, not a distinct seat (PHNX-3887). Otherwise it groups by
|
|
154
|
+
* its bare email and only ever collapses against another equally-bare home.
|
|
155
|
+
* - KEEPER. Within a group the keeper is the home that best represents the account:
|
|
156
|
+
* captured identity first, then a signed-in credential, then the newest RUNNING
|
|
157
|
+
* release, then the highest dir label as a stable tiebreak. This is deliberately
|
|
158
|
+
* NOT "highest dir-name semver" — in the wild that is the freshly re-logged-in
|
|
159
|
+
* duplicate that never captured its identity or usage, and keeping it while
|
|
160
|
+
* trashing the identified home is the exact bug this replaces.
|
|
161
|
+
*/
|
|
162
|
+
export declare function planDuplicatePrune(candidates: PruneCandidate[]): DuplicatePruneEntry[];
|
|
163
|
+
export declare function executePrunePlan(plan: AgentPrunePlan): Promise<Array<{
|
|
164
|
+
agent: AgentId;
|
|
165
|
+
version: string;
|
|
166
|
+
}>>;
|
|
104
167
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
168
|
+
* Consolidate to one home per logical account: retire the redundant duplicate
|
|
169
|
+
* homes an account accumulated, keeping the single home that best represents it
|
|
170
|
+
* (see {@link planDuplicatePrune} for the keeper/merge rules). When the retired
|
|
171
|
+
* duplicate holds the global default, the default is first repointed onto the
|
|
172
|
+
* keeper, so consolidation collapses it instead of leaving it pinned.
|
|
108
173
|
*
|
|
109
174
|
* When filterAgentId is set, prunes that agent first, then cascades: after
|
|
110
175
|
* each agent, offers the next agent with duplicates. User answering "no"
|
|
@@ -117,6 +182,7 @@ export declare function pruneDuplicates(filterAgentId: AgentId | undefined, yes:
|
|
|
117
182
|
*/
|
|
118
183
|
export declare function viewAction(agentArg?: string, options?: {
|
|
119
184
|
json?: boolean;
|
|
185
|
+
versions?: boolean;
|
|
120
186
|
prune?: boolean;
|
|
121
187
|
yes?: boolean;
|
|
122
188
|
dryRun?: boolean;
|