@phnx-labs/agents-cli 1.22.42 → 1.22.44
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 +149 -0
- package/README.md +15 -13
- package/dist/bootstrap.js +33 -13
- package/dist/cli/command-registry.d.ts +0 -8
- package/dist/cli/command-registry.js +1 -25
- package/dist/commands/alias.d.ts +7 -4
- package/dist/commands/alias.js +33 -8
- package/dist/commands/apply.d.ts +7 -10
- package/dist/commands/apply.js +15 -35
- package/dist/commands/artifacts.js +4 -5
- package/dist/commands/audit.d.ts +6 -6
- package/dist/commands/audit.js +12 -12
- package/dist/commands/auth.js +14 -6
- package/dist/commands/beta.d.ts +7 -1
- package/dist/commands/beta.js +16 -9
- package/dist/commands/commands.js +1 -1
- package/dist/commands/computer.js +20 -6
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.js +21 -9
- package/dist/commands/events.d.ts +2 -2
- package/dist/commands/events.js +6 -5
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +11 -6
- package/dist/commands/factory.d.ts +1 -1
- package/dist/commands/feed.js +1 -30
- package/dist/commands/fleet-capture.d.ts +1 -1
- package/dist/commands/fleet-capture.js +4 -5
- package/dist/commands/harness.js +4 -52
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +3 -2
- package/dist/commands/menubar.js +58 -1
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/org.d.ts +5 -0
- package/dist/commands/org.js +83 -29
- package/dist/commands/output.js +2 -2
- package/dist/commands/profiles.js +2 -2
- package/dist/commands/prune.js +5 -4
- package/dist/commands/routines.js +43 -25
- package/dist/commands/routines.test-fixture.d.ts +86 -0
- package/dist/commands/routines.test-fixture.js +345 -0
- package/dist/commands/sessions-picker.js +124 -122
- package/dist/commands/sessions.test-fixture.d.ts +24 -0
- package/dist/commands/sessions.test-fixture.js +224 -0
- package/dist/commands/setup.js +9 -1
- package/dist/commands/share.d.ts +9 -10
- package/dist/commands/share.js +68 -50
- package/dist/commands/skills.js +1 -1
- package/dist/commands/snapshot.d.ts +1 -1
- package/dist/commands/snapshot.js +4 -4
- package/dist/commands/ssh.js +15 -62
- package/dist/commands/status.d.ts +4 -2
- package/dist/commands/status.js +10 -7
- package/dist/commands/subagents.js +1 -1
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +29 -2
- package/dist/commands/view.js +11 -2
- package/dist/commands/webhook.js +1 -1
- package/dist/lib/accounting/rotate.d.ts +5 -3
- package/dist/lib/accounting/rotate.js +36 -7
- package/dist/lib/accounting/usage.d.ts +18 -4
- package/dist/lib/accounting/usage.js +138 -27
- package/dist/lib/agent-spec/agents.d.ts +7 -7
- package/dist/lib/agent-spec/agents.js +15 -15
- package/dist/lib/analytics/mix-commands.d.ts +6 -28
- package/dist/lib/analytics/mix-commands.js +14 -40
- package/dist/lib/audit/log.d.ts +2 -2
- package/dist/lib/audit/log.js +2 -2
- package/dist/lib/beta.js +1 -1
- package/dist/lib/browser/domain-skills.d.ts +26 -6
- package/dist/lib/browser/domain-skills.js +81 -43
- package/dist/lib/cloud/rush.d.ts +0 -15
- package/dist/lib/cloud/rush.js +0 -35
- package/dist/lib/crabbox/runtimes.d.ts +6 -2
- package/dist/lib/crabbox/runtimes.js +21 -5
- package/dist/lib/daemon/daemon.js +97 -58
- package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
- package/dist/lib/daemon/daemon.test-fixture.js +71 -0
- package/dist/lib/daemon/runner.d.ts +18 -1
- package/dist/lib/daemon/runner.js +105 -11
- package/dist/lib/daemon-ticks.d.ts +38 -1
- package/dist/lib/daemon-ticks.js +52 -5
- package/dist/lib/device-config.js +1 -2
- package/dist/lib/devices/discovery-policy.d.ts +3 -3
- package/dist/lib/devices/discovery-policy.js +3 -3
- package/dist/lib/devices/doctor-findings.js +1 -1
- package/dist/lib/devices/fleet.d.ts +4 -8
- package/dist/lib/devices/fleet.js +3 -15
- package/dist/lib/devices/health-report.js +4 -4
- package/dist/lib/devices/pool.d.ts +0 -6
- package/dist/lib/devices/pool.js +0 -6
- package/dist/lib/devices/registry.d.ts +0 -15
- package/dist/lib/devices/registry.js +0 -9
- package/dist/lib/devices/stats-cache.d.ts +12 -0
- package/dist/lib/devices/stats-cache.js +24 -5
- package/dist/lib/drift-sync.d.ts +1 -1
- package/dist/lib/drift-sync.js +2 -2
- package/dist/lib/drift.d.ts +1 -1
- package/dist/lib/exec.js +13 -3
- package/dist/lib/fleet/capture.d.ts +1 -1
- package/dist/lib/fleet/manifest.js +1 -1
- package/dist/lib/fleet/remote-login.d.ts +2 -2
- package/dist/lib/fleet/remote-login.js +3 -4
- package/dist/lib/format.d.ts +9 -0
- package/dist/lib/format.js +16 -0
- package/dist/lib/github/pr-mergeable.d.ts +49 -0
- package/dist/lib/github/pr-mergeable.js +157 -0
- package/dist/lib/github/pr-verdict.d.ts +67 -0
- package/dist/lib/github/pr-verdict.js +72 -0
- package/dist/lib/harness/adapter.d.ts +6 -5
- package/dist/lib/hosts/dispatch.d.ts +2 -0
- package/dist/lib/hosts/dispatch.js +8 -1
- package/dist/lib/hosts/passthrough.d.ts +10 -0
- package/dist/lib/hosts/passthrough.js +28 -7
- package/dist/lib/hosts/providers/devices.js +1 -13
- package/dist/lib/hosts/ready.d.ts +12 -3
- package/dist/lib/hosts/ready.js +27 -12
- package/dist/lib/hosts/registry.d.ts +4 -5
- package/dist/lib/hosts/registry.js +3 -7
- package/dist/lib/hosts/session-index.js +1 -1
- package/dist/lib/installations/shims.js +1 -1
- package/dist/lib/installations/versions.d.ts +40 -0
- package/dist/lib/installations/versions.js +78 -4
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
- package/dist/lib/menubar/install-menubar.d.ts +109 -0
- package/dist/lib/menubar/install-menubar.js +218 -12
- package/dist/lib/monitors/config.d.ts +2 -2
- package/dist/lib/monitors/config.js +3 -2
- package/dist/lib/origin-machine.d.ts +18 -0
- package/dist/lib/origin-machine.js +34 -0
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/perf/spool.js +1 -1
- package/dist/lib/prix-account.d.ts +2 -1
- package/dist/lib/prix-account.js +2 -1
- package/dist/lib/profiles.d.ts +7 -0
- package/dist/lib/profiles.js +26 -1
- package/dist/lib/remote-agents-json.js +1 -7
- package/dist/lib/routines-placement.d.ts +1 -1
- package/dist/lib/routines-placement.js +1 -1
- package/dist/lib/sandbox.d.ts +24 -0
- package/dist/lib/sandbox.js +101 -0
- package/dist/lib/scheduling/routines.d.ts +18 -9
- package/dist/lib/scheduling/routines.js +16 -8
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
- package/dist/lib/secrets/agent.js +9 -1
- package/dist/lib/secrets/index.js +1 -1
- package/dist/lib/secrets/list-filter.js +1 -1
- package/dist/lib/service-manifest.d.ts +20 -0
- package/dist/lib/service-manifest.js +34 -0
- package/dist/lib/session/active.d.ts +4 -16
- package/dist/lib/session/active.js +2 -6
- package/dist/lib/session/cloud.js +1 -1
- package/dist/lib/session/db.d.ts +13 -98
- package/dist/lib/session/db.js +5 -12
- package/dist/lib/session/discover.d.ts +5 -81
- package/dist/lib/session/discover.js +5 -5
- package/dist/lib/session/fork.js +1 -1
- package/dist/lib/session/live-metadata.js +1 -1
- package/dist/lib/session/origin-machine.d.ts +1 -18
- package/dist/lib/session/origin-machine.js +4 -34
- package/dist/lib/session/relative-time.d.ts +1 -44
- package/dist/lib/session/relative-time.js +4 -120
- package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote/remote-bundle.js +61 -0
- package/dist/lib/session/remote/remote-list.d.ts +148 -0
- package/dist/lib/session/remote/remote-list.js +607 -0
- package/dist/lib/session/remote/remote.d.ts +132 -0
- package/dist/lib/session/remote/remote.js +314 -0
- package/dist/lib/session/remote/watch.d.ts +101 -0
- package/dist/lib/session/remote/watch.js +241 -0
- package/dist/lib/session/remote-bundle.d.ts +1 -12
- package/dist/lib/session/remote-bundle.js +3 -61
- package/dist/lib/session/remote-list.d.ts +1 -148
- package/dist/lib/session/remote-list.js +3 -607
- package/dist/lib/session/remote.d.ts +1 -132
- package/dist/lib/session/remote.js +3 -314
- package/dist/lib/session/short-id.d.ts +1 -17
- package/dist/lib/session/short-id.js +4 -20
- package/dist/lib/session/watch.d.ts +1 -101
- package/dist/lib/session/watch.js +3 -242
- package/dist/lib/session/width.d.ts +1 -29
- package/dist/lib/session/width.js +4 -101
- package/dist/lib/share/config.d.ts +12 -2
- package/dist/lib/share/config.js +47 -11
- package/dist/lib/share/delete.js +1 -1
- package/dist/lib/share/publish.js +1 -1
- package/dist/lib/share/worker-template.js +2 -2
- package/dist/lib/smart-launch.d.ts +4 -4
- package/dist/lib/smart-launch.js +8 -18
- package/dist/lib/snapshot.d.ts +2 -2
- package/dist/lib/snapshot.js +1 -1
- package/dist/lib/staleness/index.d.ts +3 -1
- package/dist/lib/staleness/index.js +21 -1
- package/dist/lib/staleness/types.d.ts +11 -0
- package/dist/lib/staleness/writers/commands.js +13 -4
- package/dist/lib/staleness/writers/hooks.js +3 -1
- package/dist/lib/staleness/writers/mcp.d.ts +0 -7
- package/dist/lib/staleness/writers/mcp.js +16 -1
- package/dist/lib/staleness/writers/rules.js +1 -1
- package/dist/lib/staleness/writers/skills.js +3 -1
- package/dist/lib/staleness/writers/subagents.d.ts +0 -9
- package/dist/lib/staleness/writers/subagents.js +18 -1
- package/dist/lib/staleness/writers/types.d.ts +10 -0
- package/dist/lib/startup/command-registry.d.ts +10 -1
- package/dist/lib/startup/command-registry.js +26 -7
- package/dist/lib/startup/root-command.d.ts +18 -1
- package/dist/lib/startup/root-command.js +18 -1
- package/dist/lib/state.js +7 -0
- package/dist/lib/teams/placement-probe.js +28 -13
- package/dist/lib/teams/scheduler.d.ts +3 -1
- package/dist/lib/teams/scheduler.js +1 -1
- package/dist/lib/text/relative-time.d.ts +44 -0
- package/dist/lib/text/relative-time.js +120 -0
- package/dist/lib/text/short-id.d.ts +17 -0
- package/dist/lib/text/short-id.js +20 -0
- package/dist/lib/text/width.d.ts +29 -0
- package/dist/lib/text/width.js +101 -0
- package/dist/lib/triggers/handlers.d.ts +7 -0
- package/dist/lib/triggers/handlers.js +8 -0
- package/dist/lib/triggers/webhook.d.ts +1 -0
- package/dist/lib/triggers/webhook.js +2 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/view-types.d.ts +7 -0
- package/package.json +2 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/serve.d.ts +0 -10
- package/dist/commands/serve.js +0 -68
- package/dist/commands/tickets.d.ts +0 -2
- package/dist/commands/tickets.js +0 -43
- package/dist/commands/trends.d.ts +0 -10
- package/dist/commands/trends.js +0 -12
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/observe-aliases.d.ts +0 -30
- package/dist/lib/observe-aliases.js +0 -38
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/serve/control.d.ts +0 -95
- package/dist/lib/serve/control.js +0 -260
- package/dist/lib/serve/data.d.ts +0 -81
- package/dist/lib/serve/data.js +0 -91
- package/dist/lib/serve/page.d.ts +0 -7
- package/dist/lib/serve/page.js +0 -140
- package/dist/lib/serve/server.d.ts +0 -80
- package/dist/lib/serve/server.js +0 -145
- package/dist/lib/serve/stream.d.ts +0 -43
- package/dist/lib/serve/stream.js +0 -116
- package/dist/lib/serve/token.d.ts +0 -35
- package/dist/lib/serve/token.js +0 -85
- package/dist/lib/tickets/list.d.ts +0 -53
- package/dist/lib/tickets/list.js +0 -153
package/dist/lib/daemon-ticks.js
CHANGED
|
@@ -17,11 +17,51 @@ export function isFreshFleetAuthSnapshot(value, minimumCapturedAt) {
|
|
|
17
17
|
&& value.authRows.length > 0
|
|
18
18
|
&& value.authRows.every(authRow => authRow.health.checkedAt >= minimumCapturedAt);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* How stale a cached auth verdict may get before the periodic tick re-probes.
|
|
22
|
+
*
|
|
23
|
+
* The auth verdict rides the same rate-limited `/api/oauth/usage` endpoint as
|
|
24
|
+
* the usage probe. Firing it every 3-minute tick on every fleet device drove one
|
|
25
|
+
* per-account request quota to a permanent 429, and the shared backoff then
|
|
26
|
+
* parked usage fleet-wide and froze the usage cache (RUSH-2998). Re-probing at
|
|
27
|
+
* most every 20 minutes cuts that endpoint traffic ~5x while still catching a
|
|
28
|
+
* revocation within one window — and every device keeps a REAL verdict (not a
|
|
29
|
+
* degraded "unverified"), so `agents devices ping --strict` and the run
|
|
30
|
+
* auth-preflight keep working on every host, unlike a publisher/subscriber split
|
|
31
|
+
* that would blind every non-primary box to revocation. Fleet status still
|
|
32
|
+
* publishes every tick — it does not ride that endpoint.
|
|
33
|
+
*/
|
|
34
|
+
export const AUTH_PROBE_MAX_AGE_MS = 20 * 60_000;
|
|
35
|
+
/**
|
|
36
|
+
* True when every cached auth row was probed within {@link AUTH_PROBE_MAX_AGE_MS}
|
|
37
|
+
* — i.e. reusing them would not let a verdict get staler than one probe window.
|
|
38
|
+
* Empty cache is never fresh (nothing to reuse). Pure — unit-tested.
|
|
39
|
+
*/
|
|
40
|
+
export function isCachedFleetAuthProbeFresh(authRows, now, maxAgeMs = AUTH_PROBE_MAX_AGE_MS) {
|
|
41
|
+
return authRows.length > 0 && authRows.every((r) => now - r.health.checkedAt < maxAgeMs);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether the tick may reuse the cached auth verdict instead of re-probing the
|
|
45
|
+
* rate-limited endpoint. `force` (an on-demand `agents devices ping`) ALWAYS
|
|
46
|
+
* re-probes — the whole point of the command is a genuinely live verdict, and
|
|
47
|
+
* missing this `!force` is exactly how a `--strict` check silently passed a
|
|
48
|
+
* revoked account (the second `runFleetPing` call site, RUSH-2998). Pure —
|
|
49
|
+
* unit-tested so that inversion cannot regress unnoticed.
|
|
50
|
+
*/
|
|
51
|
+
export function shouldReuseCachedAuthProbe(force, cached, now, maxAgeMs = AUTH_PROBE_MAX_AGE_MS) {
|
|
52
|
+
return !force && isCachedFleetAuthProbeFresh(cached, now, maxAgeMs);
|
|
53
|
+
}
|
|
20
54
|
/**
|
|
21
55
|
* Fleet cache warm: publish THIS host's row for the caches `agents fleet
|
|
22
56
|
* status` / `agents devices list` read (PUBLISH-OWN / READ-UNION, RUSH-2061).
|
|
57
|
+
*
|
|
58
|
+
* `force` is set by on-demand callers (`agents devices ping`) that must return a
|
|
59
|
+
* genuinely live verdict; the periodic daemon tick leaves it unset so it reuses a
|
|
60
|
+
* recent verdict per {@link AUTH_PROBE_MAX_AGE_MS} instead of hammering the
|
|
61
|
+
* rate-limited endpoint every 3 minutes (RUSH-2998).
|
|
23
62
|
*/
|
|
24
|
-
export async function refreshLocalFleetAuthState() {
|
|
63
|
+
export async function refreshLocalFleetAuthState(opts) {
|
|
64
|
+
const force = opts?.force === true;
|
|
25
65
|
const { machineId } = await import('./machine-id.js');
|
|
26
66
|
const { probeLocalFleetAuth, readFleetAuthRows, writeFleetAuthRows } = await import('./auth-health.js');
|
|
27
67
|
const { getCliVersion } = await import('./version.js');
|
|
@@ -40,11 +80,18 @@ export async function refreshLocalFleetAuthState() {
|
|
|
40
80
|
return { row, authRows: readFleetAuthRows(self) };
|
|
41
81
|
},
|
|
42
82
|
// A recent daemon publication is the completed result, not a reason to probe
|
|
43
|
-
// every provider a second time.
|
|
44
|
-
|
|
83
|
+
// every provider a second time. An on-demand caller (force) never accepts a
|
|
84
|
+
// cached snapshot — it must return a genuinely live verdict.
|
|
85
|
+
isCompleted: (value) => !force && isFreshFleetAuthSnapshot(value, minimumCapturedAt),
|
|
45
86
|
refresh: async () => {
|
|
46
|
-
|
|
47
|
-
|
|
87
|
+
// Re-probe the rate-limited /oauth/usage endpoint at most every
|
|
88
|
+
// AUTH_PROBE_MAX_AGE_MS; reuse the last real verdict in between (RUSH-2998).
|
|
89
|
+
// Fleet status publishes every tick regardless — it does not ride that endpoint.
|
|
90
|
+
const cached = readFleetAuthRows(self);
|
|
91
|
+
const reuse = shouldReuseCachedAuthProbe(force, cached, requestedAt);
|
|
92
|
+
const authRows = reuse ? cached : await probeLocalFleetAuth({ cliVersion: getCliVersion() });
|
|
93
|
+
if (!reuse)
|
|
94
|
+
writeFleetAuthRows(self, authRows);
|
|
48
95
|
const row = await publishLocalFleetStatus(self);
|
|
49
96
|
return { row, authRows };
|
|
50
97
|
},
|
|
@@ -243,8 +243,7 @@ export const CONFIG_KEYS = [
|
|
|
243
243
|
type: 'string',
|
|
244
244
|
description: "What this device is for, fleet-wide: 'worker' (a box agents run on) or 'personal' (a machine you sit at — never " +
|
|
245
245
|
'picked automatically). Marking ANY device worker turns automatic placement into an allowlist: `--device auto` then ' +
|
|
246
|
-
'picks only from the marked workers.
|
|
247
|
-
'and is excluded from placement by that role, not this key.)',
|
|
246
|
+
'picks only from the marked workers.',
|
|
248
247
|
validate: (v) => DEVICE_ROLES.includes(v)
|
|
249
248
|
? null
|
|
250
249
|
: `role must be one of ${DEVICE_ROLES.join(' | ')}.`,
|
|
@@ -19,9 +19,9 @@ export declare function loadDeviceDiscoveryPolicies(): Map<string, DeviceDiscove
|
|
|
19
19
|
* Only devices with an EXPLICIT entry in the synced policy are touched. A
|
|
20
20
|
* device absent from the map is left alone, whether or not the map itself is
|
|
21
21
|
* defined — the map is not treated as authoritative-by-omission, because not
|
|
22
|
-
* every local registration path writes a policy entry (
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* every local registration path writes a policy entry (daemon/umbrella
|
|
23
|
+
* auto-refresh, and any device registered before this feature shipped).
|
|
24
|
+
* Wiping those on the next `agents repo pull user` would be
|
|
25
25
|
* silent, fleet-wide data loss for a decision (e.g. "ignore this one iPad")
|
|
26
26
|
* that never meant to touch them.
|
|
27
27
|
*/
|
|
@@ -48,9 +48,9 @@ export function loadDeviceDiscoveryPolicies() {
|
|
|
48
48
|
* Only devices with an EXPLICIT entry in the synced policy are touched. A
|
|
49
49
|
* device absent from the map is left alone, whether or not the map itself is
|
|
50
50
|
* defined — the map is not treated as authoritative-by-omission, because not
|
|
51
|
-
* every local registration path writes a policy entry (
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* every local registration path writes a policy entry (daemon/umbrella
|
|
52
|
+
* auto-refresh, and any device registered before this feature shipped).
|
|
53
|
+
* Wiping those on the next `agents repo pull user` would be
|
|
54
54
|
* silent, fleet-wide data loss for a decision (e.g. "ignore this one iPad")
|
|
55
55
|
* that never meant to touch them.
|
|
56
56
|
*/
|
|
@@ -42,7 +42,7 @@ import { AGENTS, ALL_AGENT_IDS, supportsAccountInspection } from '../agents.js';
|
|
|
42
42
|
import { blocksLocalScripts } from '../platform/winpath.js';
|
|
43
43
|
import { loginHint } from '../signin-badge.js';
|
|
44
44
|
import { CONFIG_ENV_ISOLATED_AGENTS } from '../installations/shims.js';
|
|
45
|
-
import { padToWidth, stringWidth } from '../
|
|
45
|
+
import { padToWidth, stringWidth } from '../text/width.js';
|
|
46
46
|
import { windowsSshEnrollmentProblem } from './windows-ssh-enrollment.js';
|
|
47
47
|
const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
|
|
48
48
|
/** Agents with NO per-version credential isolation: their login is shared across
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* throws (misconfigured auth, etc.) become `failed` rows — they never abort
|
|
8
8
|
* the remaining devices.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import type { DeviceProfile, DeviceRegistry } from './registry.js';
|
|
11
11
|
import type { DeviceStats } from './health.js';
|
|
12
|
-
export type FleetSkipReason = 'offline' | 'no-address'
|
|
12
|
+
export type FleetSkipReason = 'offline' | 'no-address';
|
|
13
13
|
/** npm dist-tags / semver pins only — rejects shell metacharacters. */
|
|
14
14
|
export declare const FLEET_VERSION_RE: RegExp;
|
|
15
15
|
export interface FleetTarget {
|
|
@@ -39,7 +39,6 @@ export interface FanOutDeviceResult<T> {
|
|
|
39
39
|
/**
|
|
40
40
|
* Classify each registered device for a fleet operation.
|
|
41
41
|
*
|
|
42
|
-
* - Control-only device (a cockpit, e.g. a paired iPhone) → skip `control`
|
|
43
42
|
* - Tailscale-offline → skip `offline`
|
|
44
43
|
* - No address → skip `no-address`
|
|
45
44
|
* - Everything else is a target (including this machine, reached over ssh when
|
|
@@ -48,10 +47,7 @@ export interface FanOutDeviceResult<T> {
|
|
|
48
47
|
export declare function planFleetTargets(reg: DeviceRegistry): FleetTarget[];
|
|
49
48
|
/**
|
|
50
49
|
* Remote fan-out targets for the fleet health/drift gates (`fleet status`,
|
|
51
|
-
* `doctor --check --devices`): every planned device except this machine
|
|
52
|
-
* cockpits. A control device never runs agents (mirrors doctor's fan-out, which
|
|
53
|
-
* drops it via `isControlDevice`), so counting it as unreachable/drift would make
|
|
54
|
-
* the CI gate fail on every run for a fleet that merely has a cockpit registered.
|
|
50
|
+
* `doctor --check --devices`): every planned device except this machine.
|
|
55
51
|
* Offline / no-address devices are kept — those are genuine faults a gate should
|
|
56
52
|
* surface — so their `skip` reason still flows through as an `unreachable` row.
|
|
57
53
|
*/
|
|
@@ -72,7 +68,7 @@ export declare function remoteFleetTargets(planned: FleetTarget[], self: string)
|
|
|
72
68
|
* minutes is the only false-negative window — it renders `unreachable` until the
|
|
73
69
|
* next stats warm, which beats letting it hang the status glance for 45s.
|
|
74
70
|
*
|
|
75
|
-
* An existing skip (
|
|
71
|
+
* An existing skip (offline/no-address from {@link planFleetTargets})
|
|
76
72
|
* always wins — those are classified before any probe.
|
|
77
73
|
*/
|
|
78
74
|
export declare function fleetHealthSkip(currentSkip: FleetSkipReason | string | undefined, stats: DeviceStats | undefined): FleetSkipReason | string | undefined;
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* the remaining devices.
|
|
9
9
|
*/
|
|
10
10
|
import { spawnSync } from 'child_process';
|
|
11
|
-
import { isControlDevice } from './registry.js';
|
|
12
11
|
import { isSelfHost } from './self-host.js';
|
|
13
12
|
import { buildSshInvocation, sshTargetFor, writeAskpassShim } from './connect.js';
|
|
14
13
|
/** npm dist-tags / semver pins only — rejects shell metacharacters. */
|
|
@@ -16,7 +15,6 @@ export const FLEET_VERSION_RE = /^[A-Za-z0-9._-]+$/;
|
|
|
16
15
|
/**
|
|
17
16
|
* Classify each registered device for a fleet operation.
|
|
18
17
|
*
|
|
19
|
-
* - Control-only device (a cockpit, e.g. a paired iPhone) → skip `control`
|
|
20
18
|
* - Tailscale-offline → skip `offline`
|
|
21
19
|
* - No address → skip `no-address`
|
|
22
20
|
* - Everything else is a target (including this machine, reached over ssh when
|
|
@@ -26,11 +24,6 @@ export function planFleetTargets(reg) {
|
|
|
26
24
|
const names = Object.keys(reg).sort();
|
|
27
25
|
return names.map((name) => {
|
|
28
26
|
const device = reg[name];
|
|
29
|
-
// A control device drives the fleet but never runs agents — never a target
|
|
30
|
-
// for update/run/stats, whatever its platform reads as.
|
|
31
|
-
if (isControlDevice(device)) {
|
|
32
|
-
return { device, skip: 'control' };
|
|
33
|
-
}
|
|
34
27
|
if (device.tailscale && !device.tailscale.online) {
|
|
35
28
|
return { device, skip: 'offline' };
|
|
36
29
|
}
|
|
@@ -45,10 +38,7 @@ export function planFleetTargets(reg) {
|
|
|
45
38
|
}
|
|
46
39
|
/**
|
|
47
40
|
* Remote fan-out targets for the fleet health/drift gates (`fleet status`,
|
|
48
|
-
* `doctor --check --devices`): every planned device except this machine
|
|
49
|
-
* cockpits. A control device never runs agents (mirrors doctor's fan-out, which
|
|
50
|
-
* drops it via `isControlDevice`), so counting it as unreachable/drift would make
|
|
51
|
-
* the CI gate fail on every run for a fleet that merely has a cockpit registered.
|
|
41
|
+
* `doctor --check --devices`): every planned device except this machine.
|
|
52
42
|
* Offline / no-address devices are kept — those are genuine faults a gate should
|
|
53
43
|
* surface — so their `skip` reason still flows through as an `unreachable` row.
|
|
54
44
|
*/
|
|
@@ -57,7 +47,7 @@ export function remoteFleetTargets(planned, self) {
|
|
|
57
47
|
// device referenced by its dnsName slipped past the bare name check and got a
|
|
58
48
|
// remote version+doctor dial back to THIS box, which orphaned on timeout and
|
|
59
49
|
// piled up (RUSH-2114). `isSelfHost` matches every alias the box answers to.
|
|
60
|
-
return planned.filter((t) => t.device.name !== self && !isSelfHost(t.device.name)
|
|
50
|
+
return planned.filter((t) => t.device.name !== self && !isSelfHost(t.device.name));
|
|
61
51
|
}
|
|
62
52
|
/**
|
|
63
53
|
* Decide whether a fleet-health target should skip the expensive version+doctor
|
|
@@ -75,7 +65,7 @@ export function remoteFleetTargets(planned, self) {
|
|
|
75
65
|
* minutes is the only false-negative window — it renders `unreachable` until the
|
|
76
66
|
* next stats warm, which beats letting it hang the status glance for 45s.
|
|
77
67
|
*
|
|
78
|
-
* An existing skip (
|
|
68
|
+
* An existing skip (offline/no-address from {@link planFleetTargets})
|
|
79
69
|
* always wins — those are classified before any probe.
|
|
80
70
|
*/
|
|
81
71
|
export function fleetHealthSkip(currentSkip, stats) {
|
|
@@ -92,8 +82,6 @@ export function skipLabel(reason) {
|
|
|
92
82
|
return 'offline';
|
|
93
83
|
case 'no-address':
|
|
94
84
|
return 'no address';
|
|
95
|
-
case 'control':
|
|
96
|
-
return 'control device';
|
|
97
85
|
}
|
|
98
86
|
}
|
|
99
87
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { padToWidth, stringWidth, terminalWidth, truncateToWidth } from '../
|
|
2
|
+
import { padToWidth, stringWidth, terminalWidth, truncateToWidth } from '../text/width.js';
|
|
3
3
|
import { fmtBytes, headroom } from './health.js';
|
|
4
4
|
import { formatCheckedAge } from '../auth-health.js';
|
|
5
5
|
import { compareFleetInventories } from './fleet-divergence.js';
|
|
@@ -314,7 +314,7 @@ export function buildFleetAttentionItems(report, now = Date.now()) {
|
|
|
314
314
|
const seen = row.lastSeen ? ` · last seen ${formatCheckedAge(Date.parse(row.lastSeen), now)}` : '';
|
|
315
315
|
items.push({ glyph: 'offline', subject: row.name, detail: `offline${seen}`, fix: 'check the box' });
|
|
316
316
|
}
|
|
317
|
-
// 2) Boxes that need `agents apply` — merge config drift and a stark CLI gap
|
|
317
|
+
// 2) Boxes that need `agents fleet apply` — merge config drift and a stark CLI gap
|
|
318
318
|
// into ONE item per box (both are fixed by the same command, so don't
|
|
319
319
|
// double-list). An offline box's config/CLI is unknowable, so skip it.
|
|
320
320
|
for (const row of report.devices) {
|
|
@@ -327,7 +327,7 @@ export function buildFleetAttentionItems(report, now = Date.now()) {
|
|
|
327
327
|
if (stark)
|
|
328
328
|
reasons.push(`only ${stark.installed} of ${stark.total} agent CLIs installed`);
|
|
329
329
|
if (reasons.length > 0) {
|
|
330
|
-
items.push({ glyph: 'warn', subject: row.name, detail: reasons.join(' · '), fix: `agents apply ${row.name}` });
|
|
330
|
+
items.push({ glyph: 'warn', subject: row.name, detail: reasons.join(' · '), fix: `agents fleet apply --device ${row.name}` });
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
333
|
// 3) Version skew across the fleet — one line.
|
|
@@ -354,7 +354,7 @@ export function buildFleetAttentionItems(report, now = Date.now()) {
|
|
|
354
354
|
glyph: 'warn',
|
|
355
355
|
subject: w.devices[0] ?? 'fleet',
|
|
356
356
|
detail: w.message.replace(`${w.devices[0]} `, ''),
|
|
357
|
-
fix: `agents apply ${w.devices[0] ?? ''}`.trim(),
|
|
357
|
+
fix: `agents fleet apply --device ${w.devices[0] ?? ''}`.trim(),
|
|
358
358
|
});
|
|
359
359
|
}
|
|
360
360
|
return items;
|
|
@@ -19,12 +19,6 @@
|
|
|
19
19
|
*
|
|
20
20
|
* `auto.pool all` turns the allowlist off; `personal` stays excluded, because a
|
|
21
21
|
* machine the user sits at is marked precisely so agents stay off it.
|
|
22
|
-
*
|
|
23
|
-
* Paired cockpits (an iPhone/iPad, `role: control` in the device registry, set
|
|
24
|
-
* by `agents devices pair-ios`) are excluded by the CALLER that reads the
|
|
25
|
-
* registry — `listOnlineDeviceNames` in `lib/smart-launch.ts` — not here. That
|
|
26
|
-
* role is machine-local by nature and already has a home; duplicating it in the
|
|
27
|
-
* shared config would be a second store for one concept.
|
|
28
22
|
*/
|
|
29
23
|
import { type AutoPoolMode, type ConfiguredDeviceRole } from '../device-config.js';
|
|
30
24
|
export interface AutoPoolOptions {
|
package/dist/lib/devices/pool.js
CHANGED
|
@@ -19,12 +19,6 @@
|
|
|
19
19
|
*
|
|
20
20
|
* `auto.pool all` turns the allowlist off; `personal` stays excluded, because a
|
|
21
21
|
* machine the user sits at is marked precisely so agents stay off it.
|
|
22
|
-
*
|
|
23
|
-
* Paired cockpits (an iPhone/iPad, `role: control` in the device registry, set
|
|
24
|
-
* by `agents devices pair-ios`) are excluded by the CALLER that reads the
|
|
25
|
-
* registry — `listOnlineDeviceNames` in `lib/smart-launch.ts` — not here. That
|
|
26
|
-
* role is machine-local by nature and already has a home; duplicating it in the
|
|
27
|
-
* shared config would be a second store for one concept.
|
|
28
22
|
*/
|
|
29
23
|
import { autoPoolMode, listConfiguredDeviceRoles } from '../device-config.js';
|
|
30
24
|
import { normalizeHost } from '../machine-id.js';
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
/** Operating-system family of a device, used to pick the remote shell. */
|
|
2
2
|
export type DevicePlatform = 'windows' | 'linux' | 'macos' | 'unknown';
|
|
3
|
-
/**
|
|
4
|
-
* What a device is *for*. A `worker` (the default) can run agents — it's dialed
|
|
5
|
-
* over SSH for sessions and eligible for run/team placement. A `control` device
|
|
6
|
-
* is a cockpit that drives the fleet but never executes agents itself (an
|
|
7
|
-
* iPhone/iPad running the companion app): it appears in the fleet but is never
|
|
8
|
-
* dialed for SSH nor scheduled work. Absent `role` means `worker`.
|
|
9
|
-
*/
|
|
10
|
-
export type DeviceRole = 'worker' | 'control';
|
|
11
3
|
/** Remote shell dialect derived from the platform. */
|
|
12
4
|
export type DeviceShell = 'powershell' | 'posix';
|
|
13
5
|
/** How `agents ssh` authenticates to a device. Both are first-class, fully
|
|
@@ -73,15 +65,9 @@ export interface DeviceProfile {
|
|
|
73
65
|
* cached {@link DeviceTailscale.online} snapshot when rendering online/offline,
|
|
74
66
|
* because the live probe reflects whether the box answered right now. */
|
|
75
67
|
reachability?: DeviceReachability;
|
|
76
|
-
/** What the device is for. Absent means `worker` (see {@link DeviceRole}). */
|
|
77
|
-
role?: DeviceRole;
|
|
78
68
|
createdAt: string;
|
|
79
69
|
updatedAt: string;
|
|
80
70
|
}
|
|
81
|
-
/** A device's effective role, defaulting to `worker` when unset. */
|
|
82
|
-
export declare function deviceRole(d: DeviceProfile): DeviceRole;
|
|
83
|
-
/** True for a control-only device (a cockpit) that must never be dialed/scheduled. */
|
|
84
|
-
export declare function isControlDevice(d: DeviceProfile): boolean;
|
|
85
71
|
/**
|
|
86
72
|
* Whether a fan-out should dial this device, honouring the preference stated on
|
|
87
73
|
* {@link DeviceProfile.reachability}: the live SSH probe wins over the cached
|
|
@@ -127,7 +113,6 @@ export interface DeviceInput {
|
|
|
127
113
|
auth?: DeviceAuth;
|
|
128
114
|
tailscale?: DeviceTailscale;
|
|
129
115
|
reachability?: DeviceReachability;
|
|
130
|
-
role?: DeviceRole;
|
|
131
116
|
}
|
|
132
117
|
/**
|
|
133
118
|
* Create the device if absent, otherwise merge the supplied fields into the
|
|
@@ -19,14 +19,6 @@ import * as path from 'path';
|
|
|
19
19
|
import lockfile from 'proper-lockfile';
|
|
20
20
|
import { getDevicesRegistryPath, getDevicesIgnoredPath } from '../state.js';
|
|
21
21
|
import { atomicWriteJsonSync } from '../fs-atomic.js';
|
|
22
|
-
/** A device's effective role, defaulting to `worker` when unset. */
|
|
23
|
-
export function deviceRole(d) {
|
|
24
|
-
return d.role ?? 'worker';
|
|
25
|
-
}
|
|
26
|
-
/** True for a control-only device (a cockpit) that must never be dialed/scheduled. */
|
|
27
|
-
export function isControlDevice(d) {
|
|
28
|
-
return deviceRole(d) === 'control';
|
|
29
|
-
}
|
|
30
22
|
/**
|
|
31
23
|
* Whether a fan-out should dial this device, honouring the preference stated on
|
|
32
24
|
* {@link DeviceProfile.reachability}: the live SSH probe wins over the cached
|
|
@@ -198,7 +190,6 @@ export async function upsertDevice(name, input) {
|
|
|
198
190
|
auth: input.auth ?? prev?.auth ?? { method: 'key' },
|
|
199
191
|
tailscale: input.tailscale ?? prev?.tailscale,
|
|
200
192
|
reachability: input.reachability ?? prev?.reachability,
|
|
201
|
-
role: input.role ?? prev?.role,
|
|
202
193
|
createdAt: prev?.createdAt ?? now,
|
|
203
194
|
updatedAt: now,
|
|
204
195
|
};
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { probeFleetStats, probeLocalStats, type DeviceStats } from './health.js';
|
|
2
2
|
import type { DeviceProfile } from './registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* Freshness bound for a cached row. Matches the agent-count mirror's window
|
|
5
|
+
* (`AGENT_STATUS_STALE_MS` in `commands/ssh.ts`) so both columns of
|
|
6
|
+
* `devices list` / `fleet status` share one staleness model. A row older than
|
|
7
|
+
* this is never served as current — it is re-probed live (and the probe result
|
|
8
|
+
* rewrites the cache, unreachable boxes included).
|
|
9
|
+
*/
|
|
10
|
+
export declare const STATS_STALE_MS: number;
|
|
11
|
+
/** True when a cached row is still within {@link STATS_STALE_MS}. */
|
|
12
|
+
export declare function isFreshDeviceStats(stats: DeviceStats, now?: number): boolean;
|
|
3
13
|
/** Read the whole cache (best-effort; a missing/corrupt file yields an empty map). */
|
|
4
14
|
export declare function readStatsCache(): Record<string, DeviceStats>;
|
|
5
15
|
/**
|
|
@@ -27,6 +37,8 @@ export interface LoadFleetStatsOptions {
|
|
|
27
37
|
probeLocal?: typeof probeLocalStats;
|
|
28
38
|
readCache?: typeof readStatsCache;
|
|
29
39
|
writeCache?: typeof writeStatsCache;
|
|
40
|
+
/** Clock override for tests (drives the {@link STATS_STALE_MS} bound). */
|
|
41
|
+
now?: number;
|
|
30
42
|
}
|
|
31
43
|
/**
|
|
32
44
|
* Load fleet stats cache-first. See the module doc for the default vs
|
|
@@ -15,16 +15,32 @@
|
|
|
15
15
|
* - **`--refresh` / `--live`:** skip the cache and live-probe every device,
|
|
16
16
|
* rewriting the cache.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* Every cached row is bounded by {@link STATS_STALE_MS}: a row older than the
|
|
19
|
+
* window is treated exactly like a missing one — re-probed live this call and
|
|
20
|
+
* rewritten — so the default read is itself the cache's writer and a value can
|
|
21
|
+
* never fossilize at the last manual `--refresh`. (The daemon used to warm this
|
|
22
|
+
* cache every ~3 min; RUSH-2061 removed that N² fleet probe, which left the
|
|
23
|
+
* unbounded read serving multi-day-old rows as current fleet state — the
|
|
24
|
+
* scheduler, `--device auto`, and the session-start banner all read these
|
|
25
|
+
* numbers. See #2666.)
|
|
22
26
|
*/
|
|
23
27
|
import * as fs from 'fs';
|
|
24
28
|
import * as path from 'path';
|
|
25
29
|
import { getCacheDir } from '../state.js';
|
|
26
30
|
import { probeFleetStats, probeLocalStats } from './health.js';
|
|
27
31
|
const CACHE_FILE = '.fleet-stats.json';
|
|
32
|
+
/**
|
|
33
|
+
* Freshness bound for a cached row. Matches the agent-count mirror's window
|
|
34
|
+
* (`AGENT_STATUS_STALE_MS` in `commands/ssh.ts`) so both columns of
|
|
35
|
+
* `devices list` / `fleet status` share one staleness model. A row older than
|
|
36
|
+
* this is never served as current — it is re-probed live (and the probe result
|
|
37
|
+
* rewrites the cache, unreachable boxes included).
|
|
38
|
+
*/
|
|
39
|
+
export const STATS_STALE_MS = 3 * 60_000;
|
|
40
|
+
/** True when a cached row is still within {@link STATS_STALE_MS}. */
|
|
41
|
+
export function isFreshDeviceStats(stats, now = Date.now()) {
|
|
42
|
+
return now - stats.fetchedAt <= STATS_STALE_MS;
|
|
43
|
+
}
|
|
28
44
|
function cacheFilePath() {
|
|
29
45
|
return path.join(getCacheDir(), CACHE_FILE);
|
|
30
46
|
}
|
|
@@ -71,6 +87,7 @@ export async function loadFleetStats(devices, opts = {}) {
|
|
|
71
87
|
const readCache = opts.readCache ?? readStatsCache;
|
|
72
88
|
const writeCache = opts.writeCache ?? writeStatsCache;
|
|
73
89
|
const self = opts.selfName;
|
|
90
|
+
const now = opts.now ?? Date.now();
|
|
74
91
|
const cache = opts.forceRefresh ? {} : readCache();
|
|
75
92
|
const stats = new Map();
|
|
76
93
|
const toProbe = [];
|
|
@@ -82,11 +99,13 @@ export async function loadFleetStats(devices, opts = {}) {
|
|
|
82
99
|
continue;
|
|
83
100
|
}
|
|
84
101
|
const cached = cache[d.name];
|
|
85
|
-
if (cached) {
|
|
102
|
+
if (cached && isFreshDeviceStats(cached, now)) {
|
|
86
103
|
stats.set(d.name, cached);
|
|
87
104
|
servedFromCache = true;
|
|
88
105
|
}
|
|
89
106
|
else {
|
|
107
|
+
// Missing OR beyond the staleness bound: a stale number rendered as
|
|
108
|
+
// current is worse than the probe's cost, so re-probe live (#2666).
|
|
90
109
|
toProbe.push(d);
|
|
91
110
|
}
|
|
92
111
|
}
|
package/dist/lib/drift-sync.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive drift-sync flow — the single "we detected drift, want to fix it?"
|
|
3
|
-
* action, shared by `agents status`, `agents doctor`, and the menu-bar "NEEDS
|
|
3
|
+
* action, shared by `agents sync status`, `agents doctor`, and the menu-bar "NEEDS
|
|
4
4
|
* SYNC" row.
|
|
5
5
|
*
|
|
6
6
|
* It composes existing pieces, re-implementing nothing:
|
package/dist/lib/drift-sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive drift-sync flow — the single "we detected drift, want to fix it?"
|
|
3
|
-
* action, shared by `agents status`, `agents doctor`, and the menu-bar "NEEDS
|
|
3
|
+
* action, shared by `agents sync status`, `agents doctor`, and the menu-bar "NEEDS
|
|
4
4
|
* SYNC" row.
|
|
5
5
|
*
|
|
6
6
|
* It composes existing pieces, re-implementing nothing:
|
|
@@ -115,7 +115,7 @@ export async function promptDriftSync(opts = {}) {
|
|
|
115
115
|
if (opts.yes || !isInteractiveTerminal()) {
|
|
116
116
|
if (!opts.yes) {
|
|
117
117
|
// Non-TTY without --yes: report, don't act, don't throw.
|
|
118
|
-
console.log(chalk.gray('\nRun `agents status --yes` to sync, or `agents status` in a terminal to choose.'));
|
|
118
|
+
console.log(chalk.gray('\nRun `agents sync status --yes` to sync, or `agents sync status` in a terminal to choose.'));
|
|
119
119
|
return base;
|
|
120
120
|
}
|
|
121
121
|
const systemPulled = await pullSystem(status);
|
package/dist/lib/drift.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export interface DriftSummary {
|
|
|
62
62
|
* True when the install is out of sync: any installed version is stale,
|
|
63
63
|
* never-synced, carries unwired hooks, or has a broken generated hook runtime,
|
|
64
64
|
* OR a source layer is behind origin.
|
|
65
|
-
* `agents doctor` surfaces it as "run `agents status`"; `agents doctor --check`
|
|
65
|
+
* `agents doctor` surfaces it as "run `agents sync status`"; `agents doctor --check`
|
|
66
66
|
* maps it to a non-zero exit. Orphans are a `prune` concern, not sync drift, so they do
|
|
67
67
|
* NOT set this flag (mirrors the sync-status engine: an orphan alone never
|
|
68
68
|
* flags needsSync).
|
package/dist/lib/exec.js
CHANGED
|
@@ -37,6 +37,8 @@ import { applyAddDirs } from './add-dir.js';
|
|
|
37
37
|
import { applyActiveRulesPresetAtRun } from './rules/run-sync.js';
|
|
38
38
|
import { resolveHarnessAdapter, stripForeignConfigDir } from './harness/index.js';
|
|
39
39
|
import { resolveConfigVersion } from './harness/exec-config-version.js';
|
|
40
|
+
import { getAccountInfo } from './agents.js';
|
|
41
|
+
import { getUsageLookupKey, noteClaudeSessionLimit, parseClaudeSessionLimitReset } from './accounting/usage.js';
|
|
40
42
|
/**
|
|
41
43
|
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
42
44
|
*
|
|
@@ -2083,12 +2085,20 @@ export async function runWithFallback(options) {
|
|
|
2083
2085
|
}
|
|
2084
2086
|
throw err;
|
|
2085
2087
|
}
|
|
2086
|
-
|
|
2088
|
+
const output = `${result.stderr}\n${result.stdout}`;
|
|
2089
|
+
const sessionLimitReset = agent === 'claude' ? parseClaudeSessionLimitReset(output) : null;
|
|
2090
|
+
if (sessionLimitReset && version) {
|
|
2091
|
+
const account = await getAccountInfo(agent, getVersionHomePath(agent, version));
|
|
2092
|
+
const usageKey = getUsageLookupKey(account);
|
|
2093
|
+
if (usageKey)
|
|
2094
|
+
noteClaudeSessionLimit(usageKey, sessionLimitReset);
|
|
2095
|
+
}
|
|
2096
|
+
if (result.exitCode === 0 && !sessionLimitReset)
|
|
2087
2097
|
return 0;
|
|
2088
2098
|
const isLast = i === chain.length - 1;
|
|
2089
2099
|
if (isLast)
|
|
2090
|
-
return result.exitCode;
|
|
2091
|
-
if (!detectRateLimit(result.stderr) && !detectRateLimit(result.stdout)) {
|
|
2100
|
+
return result.exitCode || 1;
|
|
2101
|
+
if (!sessionLimitReset && !detectRateLimit(result.stderr) && !detectRateLimit(result.stdout)) {
|
|
2092
2102
|
return result.exitCode;
|
|
2093
2103
|
}
|
|
2094
2104
|
const next = chain[i + 1];
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { FleetManifest, FleetDefaults } from './types.js';
|
|
17
17
|
export interface CaptureInputs {
|
|
18
|
-
/** Registered
|
|
18
|
+
/** Registered device names to record (the roster — names only). */
|
|
19
19
|
devices: string[];
|
|
20
20
|
/** Optional per-device agent specs (e.g. from `--from-pins`), keyed by name. */
|
|
21
21
|
agentsByDevice?: Record<string, string[]>;
|
|
@@ -124,7 +124,7 @@ export function readFleetFile(filePath) {
|
|
|
124
124
|
}
|
|
125
125
|
const fleet = doc.fleet;
|
|
126
126
|
if (fleet === undefined) {
|
|
127
|
-
throw new Error(`${filePath} has no fleet: block. Add one to declare the profile (see \`agents apply --help\`).`);
|
|
127
|
+
throw new Error(`${filePath} has no fleet: block. Add one to declare the profile (see \`agents fleet apply --help\`).`);
|
|
128
128
|
}
|
|
129
129
|
return parseFleetManifest(fleet);
|
|
130
130
|
}
|
|
@@ -63,7 +63,7 @@ export interface SelectLoginOptions {
|
|
|
63
63
|
*
|
|
64
64
|
* A pair is pending when its cached verdict {@link isPendingVerdict} (or
|
|
65
65
|
* `includeLoggedIn` forces it). Agents with no defined login flow are dropped
|
|
66
|
-
* silently (nothing to log into).
|
|
66
|
+
* silently (nothing to log into).
|
|
67
67
|
*/
|
|
68
68
|
export declare function selectLoginTargets(devices: DeviceProfile[], agents: (AgentId | string)[], cache: Record<string, AuthHealth>, opts?: SelectLoginOptions): PendingLogin[];
|
|
69
69
|
/**
|
|
@@ -146,7 +146,7 @@ export interface DetectOptions {
|
|
|
146
146
|
includeLoggedIn?: boolean;
|
|
147
147
|
}
|
|
148
148
|
/**
|
|
149
|
-
* Resolve the online
|
|
149
|
+
* Resolve the online fleet devices, then compute pending logins
|
|
150
150
|
* from the shared auth-health cache (populated by `agents fleet ping`). Thin I/O
|
|
151
151
|
* over the pure {@link selectLoginTargets}. Devices/agents can be narrowed by the
|
|
152
152
|
* caller's flags; the default agent set is every agent with a defined flow.
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
* human works through the earlier ones.
|
|
28
28
|
*/
|
|
29
29
|
import * as http from 'http';
|
|
30
|
-
import { isControlDevice } from '../devices/registry.js';
|
|
31
30
|
import { loadDevices } from '../devices/registry.js';
|
|
32
31
|
import { deviceIdentityArgs, fleetDialTarget } from '../devices/connect.js';
|
|
33
32
|
import { planFleetTargets } from '../devices/fleet.js';
|
|
@@ -117,7 +116,7 @@ function worstCachedVerdict(cache, host, agent) {
|
|
|
117
116
|
*
|
|
118
117
|
* A pair is pending when its cached verdict {@link isPendingVerdict} (or
|
|
119
118
|
* `includeLoggedIn` forces it). Agents with no defined login flow are dropped
|
|
120
|
-
* silently (nothing to log into).
|
|
119
|
+
* silently (nothing to log into).
|
|
121
120
|
*/
|
|
122
121
|
export function selectLoginTargets(devices, agents, cache, opts = {}) {
|
|
123
122
|
const out = [];
|
|
@@ -408,7 +407,7 @@ export async function driveRemoteLogin(target, flow, driver, opts = {}, extraSsh
|
|
|
408
407
|
}
|
|
409
408
|
}
|
|
410
409
|
/**
|
|
411
|
-
* Resolve the online
|
|
410
|
+
* Resolve the online fleet devices, then compute pending logins
|
|
412
411
|
* from the shared auth-health cache (populated by `agents fleet ping`). Thin I/O
|
|
413
412
|
* over the pure {@link selectLoginTargets}. Devices/agents can be narrowed by the
|
|
414
413
|
* caller's flags; the default agent set is every agent with a defined flow.
|
|
@@ -417,7 +416,7 @@ export async function detectPending(opts = {}) {
|
|
|
417
416
|
const reg = await loadDevices();
|
|
418
417
|
const self = machineId();
|
|
419
418
|
const online = planFleetTargets(reg)
|
|
420
|
-
.filter((t) => !t.skip && t.device.name !== self
|
|
419
|
+
.filter((t) => !t.skip && t.device.name !== self)
|
|
421
420
|
.map((t) => t.device);
|
|
422
421
|
const deviceFilter = opts.devices && opts.devices.length > 0 ? new Set(opts.devices) : null;
|
|
423
422
|
const devices = deviceFilter ? online.filter((d) => deviceFilter.has(d.name)) : online;
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -27,6 +27,15 @@ export declare function formatDie(msg: string, opts?: DieOptions): {
|
|
|
27
27
|
* keep the original red-stderr behavior.
|
|
28
28
|
*/
|
|
29
29
|
export declare function die(msg: string, code?: number, opts?: DieOptions): never;
|
|
30
|
+
/**
|
|
31
|
+
* Await a command action and turn a thrown Error into a clean `die(message)`
|
|
32
|
+
* instead of Node's raw stack dump — bootstrap's parseAsync catch deliberately
|
|
33
|
+
* rethrows plain Errors as engineering bugs, so a command whose helpers throw
|
|
34
|
+
* user-actionable errors (auth, org) wraps its `.action(...)` call site with
|
|
35
|
+
* this. The helpers keep throwing so tests can assert on them. Pass `json`
|
|
36
|
+
* so a `--json` caller still gets the structured `{"error"}` payload.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runOrDie(fn: () => void | Promise<void>, opts?: DieOptions): Promise<void>;
|
|
30
39
|
/**
|
|
31
40
|
* `die()` with a structured friction event attached. Use this at CLI error
|
|
32
41
|
* chokepoints so the nightly routine can classify and rank recurring failures
|
package/dist/lib/format.js
CHANGED
|
@@ -44,6 +44,22 @@ export function die(msg, code = 1, opts = {}) {
|
|
|
44
44
|
console.error(text);
|
|
45
45
|
process.exit(code);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Await a command action and turn a thrown Error into a clean `die(message)`
|
|
49
|
+
* instead of Node's raw stack dump — bootstrap's parseAsync catch deliberately
|
|
50
|
+
* rethrows plain Errors as engineering bugs, so a command whose helpers throw
|
|
51
|
+
* user-actionable errors (auth, org) wraps its `.action(...)` call site with
|
|
52
|
+
* this. The helpers keep throwing so tests can assert on them. Pass `json`
|
|
53
|
+
* so a `--json` caller still gets the structured `{"error"}` payload.
|
|
54
|
+
*/
|
|
55
|
+
export async function runOrDie(fn, opts = {}) {
|
|
56
|
+
try {
|
|
57
|
+
await fn();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
die(err instanceof Error ? err.message : String(err), 1, opts);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
47
63
|
/**
|
|
48
64
|
* `die()` with a structured friction event attached. Use this at CLI error
|
|
49
65
|
* chokepoints so the nightly routine can classify and rank recurring failures
|