@phnx-labs/agents-cli 1.20.74 → 1.20.77
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 +1055 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +83 -13
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +286 -15
- package/dist/commands/secrets.js +102 -37
- package/dist/commands/sessions-browser.js +12 -2
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.d.ts +14 -1
- package/dist/commands/sessions-picker.js +184 -16
- package/dist/commands/sessions.d.ts +56 -14
- package/dist/commands/sessions.js +322 -62
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +110 -37
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +17 -6
- package/dist/lib/activity.js +466 -40
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.d.ts +10 -1
- package/dist/lib/daemon.js +114 -11
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +37 -1
- package/dist/lib/devices/fleet.js +36 -2
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +82 -0
- package/dist/lib/exec.js +218 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +16 -0
- package/dist/lib/hosts/dispatch.js +26 -4
- package/dist/lib/hosts/passthrough.js +9 -2
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +32 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +17 -5
- package/dist/lib/hosts/run-target.js +30 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +40 -5
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +29 -0
- package/dist/lib/menubar/install-menubar.js +59 -1
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +6 -2
- package/dist/lib/overdue.js +10 -36
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +94 -0
- package/dist/lib/routines.js +93 -9
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +242 -20
- package/dist/lib/sandbox.js +10 -0
- package/dist/lib/secrets/account-token.d.ts +20 -0
- package/dist/lib/secrets/account-token.js +64 -0
- package/dist/lib/secrets/agent.d.ts +74 -4
- package/dist/lib/secrets/agent.js +181 -107
- package/dist/lib/secrets/bundles.d.ts +54 -6
- package/dist/lib/secrets/bundles.js +230 -34
- package/dist/lib/secrets/index.d.ts +26 -3
- package/dist/lib/secrets/index.js +42 -12
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/session-store.d.ts +6 -2
- package/dist/lib/secrets/session-store.js +65 -15
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +124 -7
- package/dist/lib/session/active.js +390 -56
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +54 -0
- package/dist/lib/session/db.js +320 -19
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +478 -3
- package/dist/lib/session/discover.js +1740 -472
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +141 -32
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +40 -26
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.d.ts +4 -0
- package/dist/lib/session/state.js +99 -13
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +8 -4
- package/dist/lib/state.js +21 -37
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +47 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
|
@@ -37,7 +37,7 @@ export async function runComputerWizard() {
|
|
|
37
37
|
return false;
|
|
38
38
|
}
|
|
39
39
|
// 1. Download + verify + install the signed, notarized helper.
|
|
40
|
-
console.log(chalk.bold('Installing the Computer
|
|
40
|
+
console.log(chalk.bold('Installing the Agents Computer helper...'));
|
|
41
41
|
try {
|
|
42
42
|
await installComputerHelperMacLocal();
|
|
43
43
|
}
|
|
@@ -57,7 +57,7 @@ export async function runComputerWizard() {
|
|
|
57
57
|
}
|
|
58
58
|
// 3. Guide the two permission grants if not already trusted.
|
|
59
59
|
if (!trusted) {
|
|
60
|
-
console.log(chalk.bold('\nGrant two permissions to "Computer
|
|
60
|
+
console.log(chalk.bold('\nGrant two permissions to "Agents Computer" (one-time):'));
|
|
61
61
|
console.log(' 1. ' + chalk.cyan('Accessibility') + chalk.dim(' — lets it click/type'));
|
|
62
62
|
console.log(' 2. ' + chalk.cyan('Screen Recording') + chalk.dim(' — lets it screenshot windows'));
|
|
63
63
|
console.log(chalk.dim('\nOpening System Settings > Privacy & Security ...'));
|
package/dist/commands/ssh.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* rsync / `agents sessions --host` resolve the same logical names.
|
|
11
11
|
*/
|
|
12
12
|
import type { Command } from 'commander';
|
|
13
|
+
import { type FanOutDeviceTarget } from '../lib/devices/fleet.js';
|
|
13
14
|
import { type CrabboxBox } from '../lib/crabbox/cli.js';
|
|
14
15
|
/**
|
|
15
16
|
* Live "Leased boxes" section for `agents devices` (F4, RUSH-1923), computed
|
|
@@ -19,5 +20,17 @@ import { type CrabboxBox } from '../lib/crabbox/cli.js';
|
|
|
19
20
|
* the row formatting is deterministic in tests.
|
|
20
21
|
*/
|
|
21
22
|
export declare function renderLeasedBoxesSection(boxes: CrabboxBox[], nowSecs: number): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Race `fanOut` against an overall wall-clock deadline (RUSH-2041).
|
|
25
|
+
*
|
|
26
|
+
* If `fanOut` settles first the result passes through unchanged. If the
|
|
27
|
+
* deadline fires first every pending remote target is mapped to `failed` (or
|
|
28
|
+
* `skipped` for pre-skipped targets), so callers always get a complete result
|
|
29
|
+
* array and the command exits promptly rather than hanging.
|
|
30
|
+
*
|
|
31
|
+
* Exported so the unit test can exercise the real path with a hanging probe
|
|
32
|
+
* instead of reimplementing the logic.
|
|
33
|
+
*/
|
|
34
|
+
export declare function raceFleetPingDeadline<T, Target extends FanOutDeviceTarget>(fanOut: Promise<import('../lib/devices/fleet.js').FanOutDeviceResult<T>[]>, remoteTargets: Target[], overallTimeoutMs: number): Promise<import('../lib/devices/fleet.js').FanOutDeviceResult<T>[]>;
|
|
22
35
|
/** Register both `agents ssh` and `agents devices`. */
|
|
23
36
|
export declare function registerSshCommands(program: Command): void;
|
package/dist/commands/ssh.js
CHANGED
|
@@ -20,21 +20,22 @@ import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secret
|
|
|
20
20
|
import { machineId } from '../lib/session/sync/config.js';
|
|
21
21
|
import { registerFleetCaptureCommand } from './fleet-capture.js';
|
|
22
22
|
import { registerFleetApplyAlias } from './apply.js';
|
|
23
|
-
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
|
|
23
|
+
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
|
|
24
|
+
import { collectReachabilityWriteBacks, deviceOnlineState } from '../lib/devices/reachability.js';
|
|
24
25
|
import { addControlToken } from '../lib/serve/token.js';
|
|
25
26
|
import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
|
|
26
27
|
import { nodeToDeviceInput, parseTailscaleStatus, tailscaleStatusJson, } from '../lib/devices/tailscale.js';
|
|
27
|
-
import { localLoginUser, planDeviceReconciliation, runDeviceSync, withDefaultUser } from '../lib/devices/sync.js';
|
|
28
|
+
import { defaultPickerChecked, localLoginUser, planDeviceReconciliation, runDeviceSync, withDefaultUser } from '../lib/devices/sync.js';
|
|
28
29
|
import { resolveDeviceTarget, splitUserHost } from '../lib/devices/resolve-target.js';
|
|
29
30
|
import { clearPendingSentinel } from '../lib/devices/pending.js';
|
|
30
31
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
31
32
|
import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
32
|
-
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
33
|
+
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, ASKPASS_AGENT_ONLY_ENV, buildSshInvocation, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
33
34
|
import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
|
|
34
35
|
import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
|
|
35
|
-
import { fanOutDevices, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
36
|
+
import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
36
37
|
import { fleetCapacity, fmtBytes, headroom, } from '../lib/devices/health.js';
|
|
37
|
-
import { buildFleetHealthReport, renderFleetMatrix, renderFleetWarnings, } from '../lib/devices/health-report.js';
|
|
38
|
+
import { buildFleetHealthReport, renderFleetMatrix, renderFleetSummary, renderFleetWarnings, } from '../lib/devices/health-report.js';
|
|
38
39
|
import { loadFleetStats, readStatsCache } from '../lib/devices/stats-cache.js';
|
|
39
40
|
import { checkSyncStatus, countOrphans } from '../lib/drift.js';
|
|
40
41
|
import { checkAllClis } from '../lib/teams/agents.js';
|
|
@@ -47,14 +48,17 @@ import { authCellColor, formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, re
|
|
|
47
48
|
import { runFleetLogin } from '../lib/fleet/remote-login.js';
|
|
48
49
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
49
50
|
* command is running on so it stands out from the rest of the tailnet. */
|
|
50
|
-
function deviceSummary(d, isSelf = false) {
|
|
51
|
+
function deviceSummary(d, isSelf = false, stats) {
|
|
51
52
|
const addr = hostNameFor(d) ?? chalk.gray('no address');
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
// Prefer a fresh live verdict (this run's probe, else the written-back
|
|
54
|
+
// reachability) over the stale tailscale.online snapshot (RUSH-1965).
|
|
55
|
+
const state = deviceOnlineState(d, stats);
|
|
56
|
+
const online = state === 'online'
|
|
57
|
+
? chalk.green('online')
|
|
58
|
+
: state === 'offline'
|
|
59
|
+
? chalk.gray('offline')
|
|
60
|
+
: chalk.gray('unknown');
|
|
61
|
+
const reach = state === 'online' && d.tailscale && !d.tailscale.direct ? chalk.yellow(' (relayed)') : '';
|
|
58
62
|
const marker = isSelf ? chalk.cyan('▸ ') : ' ';
|
|
59
63
|
const name = isSelf ? chalk.bold.cyan(d.name.padEnd(16)) : chalk.bold(d.name.padEnd(16));
|
|
60
64
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
@@ -106,8 +110,11 @@ function renderDeviceTable(reg, names, self, statsMap, full = false) {
|
|
|
106
110
|
const marker = isSelf ? chalk.cyan('▸ ') : ' ';
|
|
107
111
|
const label = isSelf ? chalk.bold.cyan(name.padEnd(16)) : chalk.bold(name.padEnd(16));
|
|
108
112
|
const plat = String(d.platform).padEnd(8);
|
|
109
|
-
const offline = d.tailscale && !d.tailscale.online;
|
|
110
113
|
const stats = statsMap.get(name);
|
|
114
|
+
// Prefer this run's live probe, then the written-back verdict, over the
|
|
115
|
+
// stale tailscale.online snapshot — so a reachable box never renders
|
|
116
|
+
// "offline" while its live load/mem sit one column over (RUSH-1965).
|
|
117
|
+
const offline = deviceOnlineState(d, stats) === 'offline';
|
|
111
118
|
if (offline) {
|
|
112
119
|
lines.push(`${marker}${label}${plat} ${chalk.gray('offline')}`);
|
|
113
120
|
continue;
|
|
@@ -243,13 +250,16 @@ async function runInteractiveDeviceSync() {
|
|
|
243
250
|
// Everything not already dismissed starts checked, so pressing Enter keeps
|
|
244
251
|
// the fleet as-is (matching what auto-sync would register). Unchecking a
|
|
245
252
|
// device removes it AND dismisses it so auto-sync never re-adds it.
|
|
253
|
+
// Sharee nodes (shared in by another user) start unchecked unless already
|
|
254
|
+
// registered — registering one must be a deliberate check, never the
|
|
255
|
+
// default Enter.
|
|
246
256
|
message: 'Your fleet — uncheck a device to remove and stop suggesting it:',
|
|
247
257
|
pageSize: Math.min(nodes.length, 20),
|
|
248
258
|
choices: nodes.map((n) => {
|
|
249
|
-
const flags = [n.platform, n.online ? undefined : 'offline', ignored.has(n.name) ? 'ignored' : undefined]
|
|
259
|
+
const flags = [n.platform, n.online ? undefined : 'offline', n.sharee ? 'shared' : undefined, ignored.has(n.name) ? 'ignored' : undefined]
|
|
250
260
|
.filter(Boolean)
|
|
251
261
|
.join(', ');
|
|
252
|
-
return { value: n.name, name: `${n.name} ${chalk.gray(`(${flags})`)}`, checked:
|
|
262
|
+
return { value: n.name, name: `${n.name} ${chalk.gray(`(${flags})`)}`, checked: defaultPickerChecked(n, registered, ignored) };
|
|
253
263
|
}),
|
|
254
264
|
});
|
|
255
265
|
}
|
|
@@ -347,26 +357,23 @@ async function runFleetStatus(opts) {
|
|
|
347
357
|
const statsMap = opts.stats === false
|
|
348
358
|
? new Map()
|
|
349
359
|
: (await loadFleetStats(probeable, { forceRefresh, selfName: self })).stats;
|
|
360
|
+
// Persist the live probe's reachability verdict so the online/offline word is
|
|
361
|
+
// read from a fresh probe, not a stale tailscale snapshot (RUSH-1965).
|
|
362
|
+
// Best-effort: a registry write must never break the status render.
|
|
363
|
+
await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
|
|
350
364
|
const rows = [localHealthRow(self, statsMap.get(self))];
|
|
351
365
|
const remoteTargets = remoteFleetTargets(planned, self)
|
|
352
366
|
.map((t) => ({
|
|
353
367
|
name: t.device.name,
|
|
354
368
|
platform: t.device.platform,
|
|
355
|
-
|
|
369
|
+
// Fail fast: gate the expensive version+doctor dials on the reachability
|
|
370
|
+
// verdict the cheap stats probe already computed one step earlier. A box
|
|
371
|
+
// it found unreachable skips straight to an `unreachable` row instead of
|
|
372
|
+
// burning 15s+30s per box — so one genuinely-offline device can't stall
|
|
373
|
+
// the matrix for ~60s (RUSH-1964). See {@link fleetHealthSkip} for why
|
|
374
|
+
// this is trusted on the default path, not just under `--refresh`.
|
|
375
|
+
skip: fleetHealthSkip(t.skip, statsMap.get(t.device.name)),
|
|
356
376
|
dialTarget: fleetDialTarget(t.device),
|
|
357
|
-
// Fail fast: the stats probe already tried this box with the same
|
|
358
|
-
// (registry) address. If it came back unreachable, don't spend another
|
|
359
|
-
// 15s+30s on version+doctor — skip straight to an unreachable row so one
|
|
360
|
-
// genuinely-offline device can't stall the whole matrix.
|
|
361
|
-
//
|
|
362
|
-
// Only trust this on a FRESH probe (`--refresh`/`--live`). loadFleetStats
|
|
363
|
-
// is cache-first (daemon-warmed ~3min), so a box that came back online
|
|
364
|
-
// since the last warm would otherwise be skipped to "unreachable" without
|
|
365
|
-
// a live attempt — a false negative. On a default run we still do the live
|
|
366
|
-
// probe (fast now that it dials the registry address).
|
|
367
|
-
...(forceRefresh && statsMap.get(t.device.name)?.reachable === false && !t.skip
|
|
368
|
-
? { skip: 'unreachable' }
|
|
369
|
-
: {}),
|
|
370
377
|
}));
|
|
371
378
|
const remote = await fanOutDevices(remoteTargets, probeRemoteHealth);
|
|
372
379
|
for (const result of remote) {
|
|
@@ -400,18 +407,32 @@ async function runFleetStatus(opts) {
|
|
|
400
407
|
for (const row of rows) {
|
|
401
408
|
if (!row.auth)
|
|
402
409
|
row.auth = summarizeHostAuth(authCache, row.name);
|
|
410
|
+
// Resolve the online/offline verdict and last-seen once (RUSH-1966) so the
|
|
411
|
+
// summary view reads one truth — the same `deviceOnlineState` ordering the
|
|
412
|
+
// registry write-back uses — instead of re-deriving it from error/skipped.
|
|
413
|
+
const profile = reg[row.name];
|
|
414
|
+
if (profile) {
|
|
415
|
+
row.online = deviceOnlineState(profile, statsMap.get(row.name));
|
|
416
|
+
row.lastSeen = profile.tailscale?.lastSeen ?? profile.reachability?.checkedAt;
|
|
417
|
+
}
|
|
403
418
|
}
|
|
404
419
|
const report = buildFleetHealthReport(rows);
|
|
405
420
|
if (opts.json) {
|
|
406
421
|
console.log(JSON.stringify(report, null, 2));
|
|
407
422
|
}
|
|
408
|
-
else {
|
|
423
|
+
else if (opts.verbose) {
|
|
424
|
+
// Full grid: the auth/CLI/sync/version columns and the warnings rollup.
|
|
409
425
|
for (const line of renderFleetWarnings(report))
|
|
410
426
|
console.log(line);
|
|
411
427
|
console.log();
|
|
412
428
|
for (const line of renderFleetMatrix(report))
|
|
413
429
|
console.log(line);
|
|
414
430
|
}
|
|
431
|
+
else {
|
|
432
|
+
// Default: rollup + NEEDS ATTENTION + OS-grouped quiet rows + footer.
|
|
433
|
+
for (const line of renderFleetSummary(report, { self }))
|
|
434
|
+
console.log(line);
|
|
435
|
+
}
|
|
415
436
|
if (opts.strict && report.hasWarnings)
|
|
416
437
|
process.exitCode = 1;
|
|
417
438
|
}
|
|
@@ -420,13 +441,43 @@ async function probeRemoteAuth(target) {
|
|
|
420
441
|
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
421
442
|
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
422
443
|
const cmd = buildRemoteAgentsInvocation(['devices', 'ping', '--local', '--json'], undefined, isWin ? 'windows' : undefined, env);
|
|
423
|
-
const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs:
|
|
444
|
+
const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true });
|
|
424
445
|
if (res.code !== 0) {
|
|
425
446
|
throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
|
|
426
447
|
}
|
|
427
448
|
const parsed = JSON.parse(res.stdout);
|
|
428
449
|
return parsed.rows ?? [];
|
|
429
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Race `fanOut` against an overall wall-clock deadline (RUSH-2041).
|
|
453
|
+
*
|
|
454
|
+
* If `fanOut` settles first the result passes through unchanged. If the
|
|
455
|
+
* deadline fires first every pending remote target is mapped to `failed` (or
|
|
456
|
+
* `skipped` for pre-skipped targets), so callers always get a complete result
|
|
457
|
+
* array and the command exits promptly rather than hanging.
|
|
458
|
+
*
|
|
459
|
+
* Exported so the unit test can exercise the real path with a hanging probe
|
|
460
|
+
* instead of reimplementing the logic.
|
|
461
|
+
*/
|
|
462
|
+
export async function raceFleetPingDeadline(fanOut, remoteTargets, overallTimeoutMs) {
|
|
463
|
+
const overallDeadline = new Promise((_, reject) => setTimeout(() => reject(new Error('fleet ping overall deadline exceeded')), overallTimeoutMs));
|
|
464
|
+
try {
|
|
465
|
+
return await Promise.race([fanOut, overallDeadline]);
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
// Overall deadline hit before all devices settled — mark every pending
|
|
469
|
+
// device as failed so the command exits promptly. Individual probes that
|
|
470
|
+
// already settled are not retrievable (Promise.all internals), so we
|
|
471
|
+
// record all remote targets as timed out / skipped.
|
|
472
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
473
|
+
return remoteTargets.map((t) => ({
|
|
474
|
+
name: t.name,
|
|
475
|
+
status: t.skip ? 'skipped' : 'failed',
|
|
476
|
+
reason: t.skip,
|
|
477
|
+
error: t.skip ? undefined : errMsg,
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
430
481
|
async function runFleetPing(opts) {
|
|
431
482
|
const self = machineId();
|
|
432
483
|
const cliVersion = getCliVersion();
|
|
@@ -463,9 +514,16 @@ async function runFleetPing(opts) {
|
|
|
463
514
|
const spinner = isInteractiveTerminal() && !opts.json
|
|
464
515
|
? ora(`Pinging ${probeable} device${probeable === 1 ? '' : 's'}…`).start()
|
|
465
516
|
: undefined;
|
|
517
|
+
// Per-device: 15 s (matches the version probe budget; enough for the ~8 s
|
|
518
|
+
// provider-fetch inside the remote local auth probe, with headroom).
|
|
519
|
+
// Overall: 30 s hard cap so the command can never outlast a reasonable
|
|
520
|
+
// budget when several devices are simultaneously unreachable (RUSH-2041).
|
|
521
|
+
const FLEET_PING_DEVICE_TIMEOUT_MS = 15_000;
|
|
522
|
+
const FLEET_PING_OVERALL_TIMEOUT_MS = 30_000;
|
|
466
523
|
let remote;
|
|
467
524
|
try {
|
|
468
|
-
|
|
525
|
+
const fanOut = fanOutDevices(remoteTargets, probeRemoteAuth, { perDeviceTimeoutMs: FLEET_PING_DEVICE_TIMEOUT_MS });
|
|
526
|
+
remote = await raceFleetPingDeadline(fanOut, remoteTargets, FLEET_PING_OVERALL_TIMEOUT_MS);
|
|
469
527
|
}
|
|
470
528
|
finally {
|
|
471
529
|
spinner?.stop();
|
|
@@ -737,6 +795,10 @@ Typical workflow:
|
|
|
737
795
|
finally {
|
|
738
796
|
spinner?.stop();
|
|
739
797
|
}
|
|
798
|
+
// Write the live verdict back so this and every other consumer read one
|
|
799
|
+
// reachability truth instead of the stale tailscale snapshot (RUSH-1965).
|
|
800
|
+
if (statsMap)
|
|
801
|
+
await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
|
|
740
802
|
}
|
|
741
803
|
console.log(chalk.bold(`Devices (${names.length})`));
|
|
742
804
|
for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full))
|
|
@@ -766,14 +828,20 @@ Typical workflow:
|
|
|
766
828
|
.action(runList);
|
|
767
829
|
devicesCmd
|
|
768
830
|
.command('status')
|
|
769
|
-
.description('
|
|
831
|
+
.description('Fleet health at a glance: online/offline rollup, a NEEDS ATTENTION list (each with its fix command), and quiet per-device rows grouped by OS. Use --verbose for the full auth/CLI/sync grid.')
|
|
770
832
|
.option('--json', 'output machine-readable JSON')
|
|
771
833
|
.option('--strict', 'exit non-zero when any device has drift or is unreachable')
|
|
772
834
|
.option('--no-stats', 'skip the live resource probe')
|
|
773
835
|
.option('--refresh', 'force a live probe of every device, bypassing the cache')
|
|
774
836
|
.option('--live', 'alias of --refresh (shorter to type)')
|
|
775
|
-
.
|
|
776
|
-
|
|
837
|
+
.option('--verbose', 'show the full per-device auth/CLI/sync/version grid instead of the summary')
|
|
838
|
+
.action(async (opts, cmd) => {
|
|
839
|
+
// The root program also defines a global `--verbose`; commander binds a
|
|
840
|
+
// shared long flag to the program, not the leaf. Read the effective value
|
|
841
|
+
// from the merged globals so `fleet status --verbose` works at either level
|
|
842
|
+
// (same pattern as `fleet ping --verbose`).
|
|
843
|
+
const verbose = opts.verbose ?? Boolean(cmd.optsWithGlobals().verbose);
|
|
844
|
+
await runFleetStatus({ ...opts, verbose });
|
|
777
845
|
});
|
|
778
846
|
devicesCmd
|
|
779
847
|
.command('ping')
|
|
@@ -1014,7 +1082,7 @@ secrets bundle via an askpass shim — the password never touches argv.
|
|
|
1014
1082
|
// `user@device` (same device, login user overridden — dialed via its
|
|
1015
1083
|
// Tailscale route, not LAN DNS), or an ad-hoc `user@host`/`host` literal.
|
|
1016
1084
|
// A bare unregistered alias still errors as "Unknown device".
|
|
1017
|
-
const device = resolveDeviceTarget(name
|
|
1085
|
+
const device = await resolveDeviceTarget(name);
|
|
1018
1086
|
if (!device) {
|
|
1019
1087
|
// Not a registered device — it may be a leased crabbox box slug. ssh into
|
|
1020
1088
|
// it directly (crabbox@<tailnet|ip>:2222) before giving up.
|
|
@@ -1076,8 +1144,13 @@ async function runAskpass() {
|
|
|
1076
1144
|
console.error(`askpass: ${ASKPASS_BUNDLE_ENV} not set`);
|
|
1077
1145
|
process.exit(1);
|
|
1078
1146
|
}
|
|
1147
|
+
// A read-only stats probe sets ASKPASS_AGENT_ONLY_ENV to force a broker-only
|
|
1148
|
+
// resolve even under a TTY — so `agents devices` never pops Touch ID just to
|
|
1149
|
+
// render load/mem for an uncached password-auth device (RUSH-1970). Otherwise
|
|
1150
|
+
// fall back to the headless-context heuristic.
|
|
1151
|
+
const agentOnly = process.env[ASKPASS_AGENT_ONLY_ENV] === '1' || isHeadlessSecretsContext();
|
|
1079
1152
|
try {
|
|
1080
|
-
const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', keys: [key], keyMode: 'storage', agentOnly
|
|
1153
|
+
const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', keys: [key], keyMode: 'storage', agentOnly });
|
|
1081
1154
|
const value = env[key];
|
|
1082
1155
|
if (value === undefined) {
|
|
1083
1156
|
console.error(`askpass: key '${key}' not found in bundle '${bundle}'`);
|
package/dist/commands/status.js
CHANGED
|
@@ -13,6 +13,7 @@ import { AGENTS } from '../lib/agents.js';
|
|
|
13
13
|
import { setHelpSections } from '../lib/help.js';
|
|
14
14
|
import { computeSyncStatus } from '../lib/sync-status.js';
|
|
15
15
|
import { promptDriftSync } from '../lib/drift-sync.js';
|
|
16
|
+
import { resolveConfiguredModel } from '../lib/models.js';
|
|
16
17
|
import { resolveSurface } from './utils.js';
|
|
17
18
|
const agentName = (id) => AGENTS[id]?.name ?? id;
|
|
18
19
|
function versionSummary(v) {
|
|
@@ -75,8 +76,15 @@ export function registerStatusCommand(program) {
|
|
|
75
76
|
console.log(chalk.gray(' (no installed agent versions)'));
|
|
76
77
|
}
|
|
77
78
|
for (const v of status.agents) {
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// Model sits right beside the version, same priority (no label).
|
|
80
|
+
const model = resolveConfiguredModel(v.agent, v.version)?.model;
|
|
81
|
+
const defaultTag = v.isDefault ? ' (default)' : '';
|
|
82
|
+
const plain = `${agentName(v.agent)}@${v.version}${model ? ` · ${model}` : ''}${defaultTag}`;
|
|
83
|
+
const shown = `${agentName(v.agent)}@${v.version}`
|
|
84
|
+
+ (model ? ` ${chalk.gray('·')} ${chalk.yellow(model)}` : '')
|
|
85
|
+
+ (v.isDefault ? chalk.gray(' (default)') : '');
|
|
86
|
+
const pad = ' '.repeat(Math.max(1, 34 - plain.length));
|
|
87
|
+
console.log(` ${shown}${pad}${versionSummary(v)}`);
|
|
80
88
|
}
|
|
81
89
|
if (status.totals.orphan > 0) {
|
|
82
90
|
console.log(chalk.gray(` (${status.totals.orphan} orphan${status.totals.orphan === 1 ? '' : 's'} — run \`agents prune cleanup\`)`));
|
|
@@ -7,6 +7,7 @@ import { select, confirm, checkbox } from '@inquirer/prompts';
|
|
|
7
7
|
import { AGENTS, ALL_AGENT_IDS, accountOrgBadge, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, isSelfUpdatingAgent, } from '../lib/agents.js';
|
|
8
8
|
import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
9
9
|
import { viewAction } from './view.js';
|
|
10
|
+
import { resolveConfiguredModel, formatAgentIdentity } from '../lib/models.js';
|
|
10
11
|
import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
|
|
11
12
|
import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, isOldestInstalled, getGlobalDefault, setGlobalDefault, markVersionIsolated, setIsolatedDefault, isVersionIsolated, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
|
|
12
13
|
import { carryForwardSettings } from '../lib/settings-manifest.js';
|
|
@@ -410,7 +411,10 @@ export function registerVersionsCommands(program) {
|
|
|
410
411
|
spinner.text = msg;
|
|
411
412
|
});
|
|
412
413
|
if (result.success) {
|
|
413
|
-
|
|
414
|
+
const installedVer = result.installedVersion || version;
|
|
415
|
+
const installedModel = resolveConfiguredModel(agentConfig.id, installedVer)?.model;
|
|
416
|
+
const installedIdentity = formatAgentIdentity(`${agentLabel(agentConfig.id)}@${result.installedVersion}`, installedModel ? chalk.yellow(installedModel) : null);
|
|
417
|
+
spinner.succeed(`Installed ${installedIdentity}`);
|
|
414
418
|
const installedVersion = result.installedVersion || version;
|
|
415
419
|
// Track the concrete version so a `--project` pin records it instead
|
|
416
420
|
// of the `latest`/`oldest` alias.
|
|
@@ -756,8 +760,9 @@ export function registerVersionsCommands(program) {
|
|
|
756
760
|
manifest.agents[agentId] = finalVersion;
|
|
757
761
|
writeManifest(process.cwd(), manifest);
|
|
758
762
|
const projEmail = await getAccountEmail(agentId, getVersionHomePath(agentId, finalVersion));
|
|
759
|
-
const
|
|
760
|
-
|
|
763
|
+
const projModel = resolveConfiguredModel(agentId, finalVersion)?.model;
|
|
764
|
+
const projIdentity = formatAgentIdentity(chalk.green(`${agentLabel(agentConfig.id)}@${finalVersion}`), projModel ? chalk.yellow(projModel) : null, projEmail ? chalk.cyan(projEmail) : null);
|
|
765
|
+
console.log(`Set ${projIdentity} for this project`);
|
|
761
766
|
}
|
|
762
767
|
else {
|
|
763
768
|
// Smart resource detection: compare available vs ACTUALLY synced (source of truth: files, not tracking)
|
|
@@ -895,15 +900,19 @@ export function registerVersionsCommands(program) {
|
|
|
895
900
|
switchHomeFileSymlinks(agentId, finalVersion);
|
|
896
901
|
warnIfShimShadowed(agentId);
|
|
897
902
|
const useEmail = await getAccountEmail(agentId, getVersionHomePath(agentId, finalVersion));
|
|
898
|
-
const
|
|
903
|
+
const useModel = resolveConfiguredModel(agentId, finalVersion)?.model;
|
|
904
|
+
const useModelStr = useModel ? chalk.yellow(useModel) : null;
|
|
905
|
+
const useAcctStr = useEmail ? chalk.cyan(useEmail) : null;
|
|
899
906
|
// Self-updating agents are one binary; `use` only swaps the config
|
|
900
907
|
// symlink (the profile), it does not change which binary runs — so say
|
|
901
908
|
// "profile", not "version", to match what actually happened.
|
|
902
909
|
if (isSelfUpdatingAgent(agentId)) {
|
|
903
|
-
|
|
910
|
+
const identity = formatAgentIdentity(chalk.green(agentLabel(agentConfig.id)), useModelStr, useAcctStr);
|
|
911
|
+
console.log(`Switched ${identity} to config profile ${chalk.green(finalVersion)}`);
|
|
904
912
|
}
|
|
905
913
|
else {
|
|
906
|
-
|
|
914
|
+
const identity = formatAgentIdentity(chalk.green(`${agentLabel(agentConfig.id)}@${finalVersion}`), useModelStr, useAcctStr);
|
|
915
|
+
console.log(`Set ${identity} as global default`);
|
|
907
916
|
}
|
|
908
917
|
}
|
|
909
918
|
}
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { Command } from 'commander';
|
|
|
10
10
|
import { accountDisplayLabel } from '../lib/agents.js';
|
|
11
11
|
import type { AccountInfo } from '../lib/agents.js';
|
|
12
12
|
import type { AgentId } from '../lib/types.js';
|
|
13
|
+
import { type ConfiguredModelSource } from '../lib/models.js';
|
|
13
14
|
import { type ProfileSummary } from '../lib/profiles.js';
|
|
14
15
|
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
15
16
|
export declare const accountColumnLabel: typeof accountDisplayLabel;
|
|
@@ -51,6 +52,10 @@ export interface ViewJsonVersion {
|
|
|
51
52
|
}>;
|
|
52
53
|
lastActive: string | null;
|
|
53
54
|
path: string;
|
|
55
|
+
configuredModel?: {
|
|
56
|
+
model: string;
|
|
57
|
+
source: ConfiguredModelSource;
|
|
58
|
+
} | null;
|
|
54
59
|
/** Present only when --resources / --detailed (or --json with a section
|
|
55
60
|
* flag) is passed: this version's resource inventory with git sync-state. */
|
|
56
61
|
resources?: VersionResourcesJson;
|
package/dist/commands/view.js
CHANGED
|
@@ -23,6 +23,7 @@ import { getCentralRulesFileName } from '../lib/rules/rules.js';
|
|
|
23
23
|
import { composeRulesFromState } from '../lib/rules/compose.js';
|
|
24
24
|
import { getConfiguredRunStrategy } from '../lib/rotate.js';
|
|
25
25
|
import { resolveRunDefaults } from '../lib/run-defaults.js';
|
|
26
|
+
import { resolveConfiguredModel } from '../lib/models.js';
|
|
26
27
|
import { listProfiles, profileSummary } from '../lib/profiles.js';
|
|
27
28
|
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
28
29
|
import { confirm } from '@inquirer/prompts';
|
|
@@ -419,6 +420,10 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
419
420
|
let maxPlanWidth = 3;
|
|
420
421
|
let maxUsageWidth = 0;
|
|
421
422
|
let maxStatusWidth = 0;
|
|
423
|
+
// The configured model sits right after the version, at the same priority.
|
|
424
|
+
// Resolve once here (fs + catalog reads) and reuse in the render loop.
|
|
425
|
+
let maxModelWidth = 0;
|
|
426
|
+
const modelByKey = new Map();
|
|
422
427
|
for (const agentId of versionManaged) {
|
|
423
428
|
const versions = listInstalledVersions(agentId);
|
|
424
429
|
const globalDefault = getGlobalDefault(agentId);
|
|
@@ -431,6 +436,11 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
431
436
|
maxEmail = Math.max(maxEmail, accountLabel.length);
|
|
432
437
|
if (info?.plan)
|
|
433
438
|
maxPlanWidth = Math.max(maxPlanWidth, info.plan.length);
|
|
439
|
+
const model = resolveConfiguredModel(agentId, v)?.model;
|
|
440
|
+
if (model) {
|
|
441
|
+
modelByKey.set(`${agentId}:${v}`, model);
|
|
442
|
+
maxModelWidth = Math.max(maxModelWidth, model.length);
|
|
443
|
+
}
|
|
434
444
|
}
|
|
435
445
|
// Profile rows share these columns with version rows so they line up.
|
|
436
446
|
for (const profile of profilesByAgent.get(agentId) ?? []) {
|
|
@@ -499,6 +509,11 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
499
509
|
const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
500
510
|
// Build columns, trimming trailing whitespace when columns are empty
|
|
501
511
|
const parts = [` ${label}`];
|
|
512
|
+
// Configured model — same priority as the version, right beside it.
|
|
513
|
+
if (maxModelWidth > 0) {
|
|
514
|
+
const model = modelByKey.get(`${agentId}:${version}`) ?? '';
|
|
515
|
+
parts.push(chalk.yellow(model.padEnd(maxModelWidth)));
|
|
516
|
+
}
|
|
502
517
|
const hasEmail = !!vInfo?.email;
|
|
503
518
|
const signedIn = !!vInfo?.signedIn;
|
|
504
519
|
const usageUnavailable = agentReportsUsage(agentId) && signedIn && !usageInfo?.snapshot;
|
|
@@ -508,12 +523,11 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
508
523
|
// Otherwise it reflects install time (misleading "just now" for fresh installs).
|
|
509
524
|
const activeStr = vInfo && hasEmail ? formatLastActive(vInfo.lastActive) : '';
|
|
510
525
|
const hasActive = activeStr.length > 0;
|
|
526
|
+
// The model now has its own column above; keep only the run mode here.
|
|
511
527
|
const runDefaults = resolveRunDefaults(agentId, version);
|
|
512
528
|
const runDefaultBits = [];
|
|
513
529
|
if (runDefaults.mode)
|
|
514
530
|
runDefaultBits.push(`mode:${runDefaults.mode}`);
|
|
515
|
-
if (runDefaults.model)
|
|
516
|
-
runDefaultBits.push(`model:${runDefaults.model}`);
|
|
517
531
|
if (!hasEmail && !hasUsage && !signedIn) {
|
|
518
532
|
// Installed but never signed in
|
|
519
533
|
parts.push(chalk.gray('(logged out — log in with: ' + loginHint(agentId) + ')'));
|
|
@@ -555,10 +569,12 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
555
569
|
// No status badge, no last-active — profiles don't accumulate usage state.
|
|
556
570
|
for (const profile of profilesByAgent.get(agentId) ?? []) {
|
|
557
571
|
const nameCol = chalk.cyan(profile.name.padEnd(maxVerLabel));
|
|
572
|
+
// Pad the model column so profile rows line up with version rows.
|
|
573
|
+
const modelPad = maxModelWidth > 0 ? `${' '.repeat(maxModelWidth)} ` : '';
|
|
558
574
|
const authCol = chalk.gray(profile.auth.padEnd(maxEmail));
|
|
559
575
|
const usageEquivalent = profileKindAndModel(profile.model, maxPlanWidth);
|
|
560
576
|
const usagePad = ' '.repeat(Math.max(0, maxUsageWidth - visibleWidth(usageEquivalent)));
|
|
561
|
-
console.log(` ${nameCol} ${authCol} ${chalk.gray(usageEquivalent + usagePad)}`);
|
|
577
|
+
console.log(` ${nameCol} ${modelPad}${authCol} ${chalk.gray(usageEquivalent + usagePad)}`);
|
|
562
578
|
if (showPaths) {
|
|
563
579
|
console.log(chalk.gray(` ${profile.path}`));
|
|
564
580
|
}
|
|
@@ -881,9 +897,12 @@ async function showAgentResources(agentId, requestedVersion, filter) {
|
|
|
881
897
|
const accountLabel = accountColumnLabel(accountInfo);
|
|
882
898
|
const emailStr = accountLabel ? chalk.cyan(` ${accountLabel}`) : '';
|
|
883
899
|
const status = chalk.green(version);
|
|
900
|
+
// Configured model sits right beside the version, same priority (no label).
|
|
901
|
+
const configuredModel = resolveConfiguredModel(agentId, version);
|
|
902
|
+
const modelStr = configuredModel ? chalk.yellow(` ${configuredModel.model}`) : '';
|
|
884
903
|
const usageStr = formatUsageSummary(usageInfo.snapshot?.plan ?? accountInfo.plan, null);
|
|
885
904
|
const usagePart = usageStr ? ` ${usageStr}` : '';
|
|
886
|
-
console.log(` ${colorAgent(agentId)(AGENTS[agentId].name.padEnd(14))} ${status}${emailStr}${usagePart}`);
|
|
905
|
+
console.log(` ${colorAgent(agentId)(AGENTS[agentId].name.padEnd(14))} ${status}${modelStr}${emailStr}${usagePart}`);
|
|
887
906
|
const usageLines = formatUsageSection(usageInfo);
|
|
888
907
|
if (usageLines.length > 0) {
|
|
889
908
|
console.log();
|
|
@@ -1221,6 +1240,7 @@ async function collectAgentsJson(filterAgentId, resourceSections) {
|
|
|
1221
1240
|
: [],
|
|
1222
1241
|
lastActive: info.lastActive ? info.lastActive.toISOString() : null,
|
|
1223
1242
|
path: getVersionDir(agentId, version),
|
|
1243
|
+
configuredModel: resolveConfiguredModel(agentId, version),
|
|
1224
1244
|
};
|
|
1225
1245
|
if (wantResources && resourceSync) {
|
|
1226
1246
|
entry.resources = collectVersionResources(agentId, home, process.cwd(), cliStates?.[agentId]?.installed ?? false, resourceSections, resourceSync);
|
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
*
|
|
9
9
|
* agents watchdog one tick, dry — prints what it WOULD nudge/skip and why
|
|
10
10
|
* agents watchdog --nudge one tick, actually injects (explicit opt-in)
|
|
11
|
-
* agents watchdog --watch
|
|
11
|
+
* agents watchdog --watch manual poll loop (dry unless --nudge)
|
|
12
12
|
* agents watchdog --json machine-readable tick output (for the menu-bar)
|
|
13
|
-
* agents watchdog enable|disable
|
|
13
|
+
* agents watchdog enable|disable turn the always-on watchdog routine on/off
|
|
14
14
|
* agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
|
|
15
|
+
*
|
|
16
|
+
* The always-on watchdog is a daemon-fired ROUTINE, not a private sentinel + a
|
|
17
|
+
* hand-rolled loop: `enable` creates/enables a `watchdog` command routine
|
|
18
|
+
* (`agents watchdog --nudge` every couple of minutes) and reloads the daemon;
|
|
19
|
+
* `disable` pauses it. See ../lib/watchdog/routine.ts.
|
|
15
20
|
*/
|
|
16
21
|
import type { Command } from 'commander';
|
|
17
22
|
/** Register the `agents watchdog` command tree. */
|