@phnx-labs/agents-cli 1.22.98 → 1.22.100
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 +14 -0
- package/README.md +16 -7
- package/dist/commands/setup-secrets.d.ts +18 -11
- package/dist/commands/setup-secrets.js +50 -18
- package/dist/lib/account-catalog.js +20 -4
- package/dist/lib/accounts/slots.d.ts +19 -0
- package/dist/lib/accounts/slots.js +61 -1
- package/dist/lib/auth-health.d.ts +79 -5
- package/dist/lib/auth-health.js +133 -33
- package/dist/lib/browser/arc-discovery.d.ts +1 -1
- package/dist/lib/browser/arc-discovery.js +14 -4
- package/dist/lib/daemon/auth-sync-service.d.ts +21 -4
- package/dist/lib/daemon/auth-sync-service.js +49 -21
- package/dist/lib/daemon/usage-sync-service.d.ts +26 -7
- package/dist/lib/daemon/usage-sync-service.js +35 -8
- package/dist/lib/daemon-ticks.d.ts +22 -5
- package/dist/lib/daemon-ticks.js +38 -13
- package/dist/lib/fleet-shared-repo-sync.d.ts +7 -0
- package/dist/lib/fleet-shared-repo-sync.js +66 -3
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/reconcile-and-repair.d.ts +8 -0
- package/dist/lib/reconcile-and-repair.js +32 -1
- package/package.json +1 -1
package/dist/lib/daemon-ticks.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* by the cross-process refresh lease so an explicit CLI refresh and the daemon
|
|
15
15
|
* timer converge on the same published result.
|
|
16
16
|
*/
|
|
17
|
-
import { AUTH_PROBE_MAX_AGE_MS } from './auth-health.js';
|
|
17
|
+
import { AUTH_PROBE_MAX_AGE_MS, authTargetKey } from './auth-health.js';
|
|
18
18
|
export { AUTH_PROBE_MAX_AGE_MS } from './auth-health.js';
|
|
19
19
|
export function isFreshFleetAuthSnapshot(value, minimumCapturedAt) {
|
|
20
20
|
return value.row.capturedAt >= minimumCapturedAt
|
|
@@ -36,12 +36,32 @@ export function isFreshFleetAuthSnapshot(value, minimumCapturedAt) {
|
|
|
36
36
|
* publishes every tick — it does not ride that endpoint.
|
|
37
37
|
*/
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* The cached rows still backed by a home on this device, by `authTargetKey`.
|
|
40
|
+
*
|
|
41
|
+
* A row for an uninstalled version is an ORPHAN: the probe enumerates installed
|
|
42
|
+
* homes + account slots, so nothing re-probes it and its `checkedAt` is frozen
|
|
43
|
+
* at whatever the last probe left. Pure — unit-tested.
|
|
44
|
+
*/
|
|
45
|
+
export function installedAuthRows(authRows, installedTargets) {
|
|
46
|
+
return authRows.filter((r) => installedTargets.has(authTargetKey(r.agent, r.version)));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* True when every cached auth row FOR AN INSTALLED HOME was probed within
|
|
50
|
+
* {@link AUTH_PROBE_MAX_AGE_MS} — i.e. reusing them would not let a verdict get
|
|
51
|
+
* staler than one probe window. No installed row is never fresh (nothing to
|
|
52
|
+
* reuse). Pure — unit-tested.
|
|
53
|
+
*
|
|
54
|
+
* Orphan rows are excluded rather than counted stale (PHNX-4051). Counting them
|
|
55
|
+
* made this permanently false on any box that had ever uninstalled a version:
|
|
56
|
+
* the orphan can never be re-probed, so the tick live-probed the rate-limited
|
|
57
|
+
* `/api/oauth/usage` for every account every 3 minutes, re-arming the per-account
|
|
58
|
+
* 429 backoff and parking the usage refresher — the RUSH-2998 failure the reuse
|
|
59
|
+
* window exists to prevent. Observed on yosemite-m0 (rows dated Sep 2 / Sep 6 for
|
|
60
|
+
* uninstalled Claude versions).
|
|
42
61
|
*/
|
|
43
|
-
export function isCachedFleetAuthProbeFresh(authRows, now, maxAgeMs = AUTH_PROBE_MAX_AGE_MS) {
|
|
44
|
-
|
|
62
|
+
export function isCachedFleetAuthProbeFresh(authRows, now, installedTargets, maxAgeMs = AUTH_PROBE_MAX_AGE_MS) {
|
|
63
|
+
const installed = installedAuthRows(authRows, installedTargets);
|
|
64
|
+
return installed.length > 0 && installed.every((r) => now - r.health.checkedAt < maxAgeMs);
|
|
45
65
|
}
|
|
46
66
|
/**
|
|
47
67
|
* Whether the tick may reuse the cached auth verdict instead of re-probing the
|
|
@@ -51,8 +71,8 @@ export function isCachedFleetAuthProbeFresh(authRows, now, maxAgeMs = AUTH_PROBE
|
|
|
51
71
|
* revoked account (the second `runFleetPing` call site, RUSH-2998). Pure —
|
|
52
72
|
* unit-tested so that inversion cannot regress unnoticed.
|
|
53
73
|
*/
|
|
54
|
-
export function shouldReuseCachedAuthProbe(force, cached, now, maxAgeMs = AUTH_PROBE_MAX_AGE_MS) {
|
|
55
|
-
return !force && isCachedFleetAuthProbeFresh(cached, now, maxAgeMs);
|
|
74
|
+
export function shouldReuseCachedAuthProbe(force, cached, now, installedTargets, maxAgeMs = AUTH_PROBE_MAX_AGE_MS) {
|
|
75
|
+
return !force && isCachedFleetAuthProbeFresh(cached, now, installedTargets, maxAgeMs);
|
|
56
76
|
}
|
|
57
77
|
/**
|
|
58
78
|
* Fleet cache warm: publish THIS host's row for the caches `agents fleet
|
|
@@ -67,7 +87,7 @@ export async function refreshLocalFleetAuthState(opts) {
|
|
|
67
87
|
const force = opts?.force === true;
|
|
68
88
|
const signal = opts?.signal;
|
|
69
89
|
const { machineId } = await import('./machine-id.js');
|
|
70
|
-
const { probeLocalFleetAuth, readFleetAuthRows, writeFleetAuthRows } = await import('./auth-health.js');
|
|
90
|
+
const { probeLocalFleetAuth, readFleetAuthRows, writeFleetAuthRows, localAuthTargetKeys } = await import('./auth-health.js');
|
|
71
91
|
const { getCliVersion } = await import('./version.js');
|
|
72
92
|
const self = machineId();
|
|
73
93
|
const requestedAt = Date.now();
|
|
@@ -91,11 +111,16 @@ export async function refreshLocalFleetAuthState(opts) {
|
|
|
91
111
|
// Re-probe the rate-limited /oauth/usage endpoint at most every
|
|
92
112
|
// AUTH_PROBE_MAX_AGE_MS; reuse the last real verdict in between (RUSH-2998).
|
|
93
113
|
// Fleet status publishes every tick regardless — it does not ride that endpoint.
|
|
114
|
+
const installed = localAuthTargetKeys();
|
|
94
115
|
const cached = readFleetAuthRows(self);
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
116
|
+
const live = installedAuthRows(cached, installed);
|
|
117
|
+
const reuse = shouldReuseCachedAuthProbe(force, cached, requestedAt, installed);
|
|
118
|
+
const authRows = reuse ? live : await probeLocalFleetAuth({ cliVersion: getCliVersion(), forceLive: force, signal });
|
|
119
|
+
// Write when we probed, and ALSO when the cache holds orphan rows for homes
|
|
120
|
+
// that are gone: a reusing tick is the common case, so leaving the prune on
|
|
121
|
+
// the probe branch would keep them in `agents view` indefinitely (PHNX-4051).
|
|
122
|
+
if (!reuse || live.length !== cached.length)
|
|
123
|
+
writeFleetAuthRows(self, authRows, installed);
|
|
99
124
|
const row = await publishLocalFleetStatus(self);
|
|
100
125
|
return { row, authRows };
|
|
101
126
|
},
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export declare const FLEET_SHARED_REPO_SYNC_DEADLINE_MS = 45000;
|
|
2
2
|
export declare const FLEET_SHARED_REPO_KILL_GRACE_MS = 250;
|
|
3
|
+
export declare const FLEET_SHARED_REPO_OUTPUT_MAX_BYTES: number;
|
|
4
|
+
/**
|
|
5
|
+
* When the shared-state exchange last completed successfully, in epoch ms, or
|
|
6
|
+
* `null` if it never has on this box (no marker yet) or the marker is unreadable.
|
|
7
|
+
* Reads the default daemon-dir marker the periodic exchange writes.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readLastSuccessfulExchangeMs(): number | null;
|
|
3
10
|
export interface BoundedProcessResult {
|
|
4
11
|
code: number | null;
|
|
5
12
|
stdout: string;
|
|
@@ -19,9 +19,46 @@ import { machineId } from './session/sync/config.js';
|
|
|
19
19
|
import { getDaemonDir, getUserAgentsDir } from './state.js';
|
|
20
20
|
export const FLEET_SHARED_REPO_SYNC_DEADLINE_MS = 45_000;
|
|
21
21
|
export const FLEET_SHARED_REPO_KILL_GRACE_MS = 250;
|
|
22
|
-
const FLEET_SHARED_REPO_OUTPUT_MAX_BYTES = 1024 * 1024;
|
|
22
|
+
export const FLEET_SHARED_REPO_OUTPUT_MAX_BYTES = 1024 * 1024;
|
|
23
23
|
const FLEET_SHARED_REPO_PUSH_ATTEMPTS = 3;
|
|
24
24
|
const FLEET_SHARED_REPO_REBASE_CLEANUP_RESERVE_MS = 5_000;
|
|
25
|
+
/**
|
|
26
|
+
* The exchange records its last SUCCESSFUL completion beside its lock so a
|
|
27
|
+
* sibling service (auth-sync) can gate work on the freshness of the peer state
|
|
28
|
+
* this exchange delivers into the local checkout (PHNX-4051). auth-sync's
|
|
29
|
+
* credential pushes read the peer verdicts the LAST usage-sync exchange wrote; if
|
|
30
|
+
* that exchange has not landed within a tick interval, those verdicts are stale
|
|
31
|
+
* and pushing off them would act on a peer that may already hold the key.
|
|
32
|
+
*/
|
|
33
|
+
function exchangeMarkerPath(lockPath) {
|
|
34
|
+
return `${lockPath}.last-success`;
|
|
35
|
+
}
|
|
36
|
+
function writeExchangeMarker(lockPath, nowMs = Date.now()) {
|
|
37
|
+
try {
|
|
38
|
+
const p = exchangeMarkerPath(lockPath);
|
|
39
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
40
|
+
fs.writeFileSync(p, JSON.stringify({ at: nowMs }), 'utf-8');
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Best-effort telemetry — never fail the exchange over its own marker.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* When the shared-state exchange last completed successfully, in epoch ms, or
|
|
48
|
+
* `null` if it never has on this box (no marker yet) or the marker is unreadable.
|
|
49
|
+
* Reads the default daemon-dir marker the periodic exchange writes.
|
|
50
|
+
*/
|
|
51
|
+
export function readLastSuccessfulExchangeMs() {
|
|
52
|
+
try {
|
|
53
|
+
const lockPath = path.join(getDaemonDir(), 'fleet-shared-repo-sync');
|
|
54
|
+
const raw = fs.readFileSync(exchangeMarkerPath(lockPath), 'utf-8');
|
|
55
|
+
const at = JSON.parse(raw).at;
|
|
56
|
+
return typeof at === 'number' && Number.isFinite(at) ? at : null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
25
62
|
function signalProcessTree(child, signal) {
|
|
26
63
|
const pid = child.pid;
|
|
27
64
|
if (!pid)
|
|
@@ -177,9 +214,30 @@ function retainedAutostash(stashList, objectId) {
|
|
|
177
214
|
* cannot create a fresh collision — so nothing is silently destroyed. Then let
|
|
178
215
|
* the rebase check out origin's version. A file we cannot clear is left in
|
|
179
216
|
* place; the rebase may still abort, no worse than today and never losing data.
|
|
217
|
+
*
|
|
218
|
+
* The scan is scoped to the TRACKED ROOTS origin carries — the top-level entries
|
|
219
|
+
* of `git ls-tree --name-only origin/<branch>` — NOT the whole working tree
|
|
220
|
+
* (PHNX-4051). Only a file origin tracks can be overwritten by the rebase
|
|
221
|
+
* checkout, so a tracked root is the tightest superset of the collidable paths:
|
|
222
|
+
* every path origin tracks stays in scope (a stale untracked `projects/rush.yaml`
|
|
223
|
+
* under a tracked `projects/` root is still cleared), while an untracked-ONLY tree
|
|
224
|
+
* origin never tracks — artifacts-cli's revision store
|
|
225
|
+
* (`~/.agents/artifact-history/`, ~11k files / >1 MiB of paths) is the live case —
|
|
226
|
+
* is never a tracked root, so it never enters the `git ls-files --others` listing.
|
|
227
|
+
* An unbounded listing there blows past {@link FLEET_SHARED_REPO_OUTPUT_MAX_BYTES}
|
|
228
|
+
* and kills the exchange on every tick; scoping to the tracked roots keeps the
|
|
229
|
+
* PHNX-3923 coverage whole while an unrelated untracked tree cannot push the
|
|
230
|
+
* listing over the cap. `ls-tree`'s own output is bounded — it lists only the root
|
|
231
|
+
* entries, never the recursive tree.
|
|
180
232
|
*/
|
|
181
233
|
async function clearCollidingUntracked(git, root, branch) {
|
|
182
|
-
const
|
|
234
|
+
const tree = await git(['ls-tree', '--name-only', '-z', `origin/${branch}`]);
|
|
235
|
+
if (tree.code !== 0)
|
|
236
|
+
return { error: tree };
|
|
237
|
+
const trackedRoots = tree.stdout.split('\0').filter(Boolean);
|
|
238
|
+
if (trackedRoots.length === 0)
|
|
239
|
+
return { cleared: 0, backedUp: [], backupDir: null };
|
|
240
|
+
const others = await git(['ls-files', '--others', '--exclude-standard', '-z', '--', ...trackedRoots]);
|
|
183
241
|
if (others.code !== 0)
|
|
184
242
|
return { error: others };
|
|
185
243
|
const relPaths = others.stdout.split('\0').filter(Boolean);
|
|
@@ -403,7 +461,12 @@ export async function syncFleetSharedStateRepo(options = {}) {
|
|
|
403
461
|
retries: { retries: 20, factor: 1, minTimeout: 100, maxTimeout: 100 },
|
|
404
462
|
onCompromised: logAndContinueOnLockCompromised('fleet shared repo sync'),
|
|
405
463
|
});
|
|
406
|
-
|
|
464
|
+
const result = await performFleetSharedRepoSync(root, device, timeoutMs);
|
|
465
|
+
// Record freshness only on a real success, beside the lock so a test-redirected
|
|
466
|
+
// lockPath keeps its marker isolated too (PHNX-4051).
|
|
467
|
+
if (result.success)
|
|
468
|
+
writeExchangeMarker(lockPath);
|
|
469
|
+
return result;
|
|
407
470
|
}
|
|
408
471
|
catch (err) {
|
|
409
472
|
return {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* helper release now, off this table entirely.
|
|
37
37
|
*/
|
|
38
38
|
export const HELPER_RELEASES = {
|
|
39
|
-
menubar: { tagPrefix: 'menubar', floor: '1.1.
|
|
39
|
+
menubar: { tagPrefix: 'menubar', floor: '1.1.4' },
|
|
40
40
|
'computer-mac': { tagPrefix: 'computer-mac', floor: '1.0.0' },
|
|
41
41
|
// The Windows helper is a bare .exe, not an .app bundle, so it does not share
|
|
42
42
|
// helper-download.ts's zip/codesign/notarize machinery -- but it has the same
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { AgentId } from './types.js';
|
|
20
20
|
import { type HealResult } from './heal.js';
|
|
21
|
+
import { type SlotProjection } from './accounts/slots.js';
|
|
21
22
|
import { type HookRuntimeRepairReport } from './hooks/install.js';
|
|
22
23
|
import { type RemediateStaleInstallsResult, type FindAgentsCliInstallsOptions } from './self-update.js';
|
|
23
24
|
export interface HookRewireResult {
|
|
@@ -36,6 +37,13 @@ export interface RepairAfterSyncReport {
|
|
|
36
37
|
hookRuntimeRepair: HookRuntimeRepairReport;
|
|
37
38
|
/** Only populated on the umbrella (no-agent) path; null otherwise. */
|
|
38
39
|
staleInstallPurge: RemediateStaleInstallsResult | null;
|
|
40
|
+
/**
|
|
41
|
+
* Account slots re-projected from their harness's default version home
|
|
42
|
+
* (PHNX-3940): one entry per slot, with the artifacts pruned because the
|
|
43
|
+
* source no longer carries them. Errors are per-harness sentences.
|
|
44
|
+
*/
|
|
45
|
+
slotProjection: SlotProjection[];
|
|
46
|
+
slotProjectionErrors: string[];
|
|
39
47
|
}
|
|
40
48
|
/**
|
|
41
49
|
* Test/override hook that scopes the destructive stale-CLI purge to injected
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AGENTS, ALL_AGENT_IDS } from './agents.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { heal, healChangedAnything } from './heal.js';
|
|
4
|
+
import { projectAccountSlots } from './accounts/slots.js';
|
|
4
5
|
import { checkVersionHookWiring, registerHooksToSettings, repairManagedHookRuntimeArtifacts, } from './hooks/install.js';
|
|
5
6
|
import { getVersionHomePath, listInstalledVersions, isVersionIsolated, } from './installations/versions.js';
|
|
6
7
|
import { invalidateDoctorOverviewCache } from './devices/doctor-overview-cache.js';
|
|
@@ -126,11 +127,27 @@ export async function repairAfterSync(opts) {
|
|
|
126
127
|
const staleInstallPurge = (!opts.agent && opts.pruneClis === true)
|
|
127
128
|
? purgeStaleAgentsCliCopies(opts.purgeInjection)
|
|
128
129
|
: null;
|
|
130
|
+
// Account slots follow the version home (PHNX-3940). `agents accounts add`
|
|
131
|
+
// projects a slot once; without this every later sync would update the
|
|
132
|
+
// version home and leave each slot on the resources it was cloned with.
|
|
133
|
+
const slotProjection = [];
|
|
134
|
+
const slotProjectionErrors = [];
|
|
135
|
+
const slotAgents = opts.agent ? [opts.agent] : ALL_AGENT_IDS.filter((a) => listInstalledVersions(a).length > 0);
|
|
136
|
+
for (const agent of slotAgents) {
|
|
137
|
+
try {
|
|
138
|
+
slotProjection.push(...projectAccountSlots(agent));
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
slotProjectionErrors.push(`${agent}: ${err.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
129
144
|
const report = {
|
|
130
145
|
heal: healResult,
|
|
131
146
|
hookRewire,
|
|
132
147
|
hookRuntimeRepair,
|
|
133
148
|
staleInstallPurge,
|
|
149
|
+
slotProjection,
|
|
150
|
+
slotProjectionErrors,
|
|
134
151
|
};
|
|
135
152
|
if (repairChangedAnything(report))
|
|
136
153
|
invalidateDoctorOverviewCache();
|
|
@@ -142,7 +159,9 @@ export function repairChangedAnything(report) {
|
|
|
142
159
|
report.hookRewire.some((r) => r.rewired > 0 || r.remaining > 0 || r.failure !== undefined) ||
|
|
143
160
|
report.hookRuntimeRepair.attempts.length > 0 ||
|
|
144
161
|
(report.staleInstallPurge !== null &&
|
|
145
|
-
(report.staleInstallPurge.removed.length > 0 || report.staleInstallPurge.failed.length > 0))
|
|
162
|
+
(report.staleInstallPurge.removed.length > 0 || report.staleInstallPurge.failed.length > 0)) ||
|
|
163
|
+
report.slotProjection.length > 0 ||
|
|
164
|
+
report.slotProjectionErrors.length > 0);
|
|
146
165
|
}
|
|
147
166
|
/**
|
|
148
167
|
* True when the RECONCILE repair left a per-version problem a human must fix — an
|
|
@@ -174,6 +193,8 @@ export function repairAfterSyncJson(report) {
|
|
|
174
193
|
hookRewire: report.hookRewire,
|
|
175
194
|
hookRuntimeRepair: report.hookRuntimeRepair,
|
|
176
195
|
staleInstallPurge: report.staleInstallPurge,
|
|
196
|
+
slotProjection: report.slotProjection,
|
|
197
|
+
slotProjectionErrors: report.slotProjectionErrors,
|
|
177
198
|
hadFailures: repairHadFailures(report),
|
|
178
199
|
};
|
|
179
200
|
}
|
|
@@ -264,4 +285,14 @@ export function renderRepairAfterSync(report, log = (s) => console.log(s)) {
|
|
|
264
285
|
renderHookRuntimeRepairText(report.hookRuntimeRepair, log);
|
|
265
286
|
if (report.staleInstallPurge)
|
|
266
287
|
renderStaleInstallPurgeText(report.staleInstallPurge, log);
|
|
288
|
+
renderSlotProjectionText(report, log);
|
|
289
|
+
}
|
|
290
|
+
function renderSlotProjectionText(report, log) {
|
|
291
|
+
for (const slot of report.slotProjection) {
|
|
292
|
+
const pruned = slot.pruned.length > 0 ? chalk.gray(` — removed ${slot.pruned.join(', ')}`) : '';
|
|
293
|
+
log(` ${chalk.green('slot')} account ${chalk.bold(slot.name)} ${chalk.gray(`← ${slot.from}`)}${pruned}`);
|
|
294
|
+
}
|
|
295
|
+
for (const err of report.slotProjectionErrors) {
|
|
296
|
+
log(` ${chalk.yellow('slot')} ${err}`);
|
|
297
|
+
}
|
|
267
298
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.100",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|