@phnx-labs/agents-cli 1.22.46 → 1.22.49
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 +663 -0
- package/LICENSE +102 -182
- package/README.md +96 -49
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +243 -25
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/auth.js +7 -5
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +711 -62
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +26 -24
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +107 -53
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- package/dist/commands/focus.d.ts +14 -0
- package/dist/commands/focus.js +48 -5
- package/dist/commands/go.d.ts +3 -0
- package/dist/commands/go.js +16 -3
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +7 -6
- package/dist/commands/mailboxes.js +4 -3
- package/dist/commands/menubar.js +11 -4
- package/dist/commands/projects.d.ts +16 -1
- package/dist/commands/projects.js +58 -64
- package/dist/commands/resume.js +1 -1
- package/dist/commands/route.js +4 -4
- package/dist/commands/routines.js +4 -3
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +5 -0
- package/dist/commands/setup-secrets.js +23 -0
- package/dist/commands/share.d.ts +20 -5
- package/dist/commands/share.js +152 -52
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +243 -18
- package/dist/commands/teams.js +34 -2
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +184 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +32 -62
- package/dist/commands/watchdog.js +9 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +84 -6
- package/dist/lib/accounting/usage.js +293 -75
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +25 -15
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +40 -9
- package/dist/lib/browser/profiles.d.ts +104 -65
- package/dist/lib/browser/profiles.js +277 -165
- package/dist/lib/browser/registry.d.ts +39 -0
- package/dist/lib/browser/registry.js +149 -0
- package/dist/lib/browser/remote-control.d.ts +28 -2
- package/dist/lib/browser/remote-control.js +35 -3
- package/dist/lib/browser/resolve-target.d.ts +68 -0
- package/dist/lib/browser/resolve-target.js +250 -0
- package/dist/lib/browser/runtime-state.d.ts +44 -11
- package/dist/lib/browser/runtime-state.js +98 -15
- package/dist/lib/browser/service.d.ts +65 -9
- package/dist/lib/browser/service.js +196 -61
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/byok-usage.js +2 -2
- package/dist/lib/claude-statusline.d.ts +29 -0
- package/dist/lib/claude-statusline.js +139 -0
- package/dist/lib/cloud/host.d.ts +1 -1
- package/dist/lib/cloud/host.js +1 -1
- package/dist/lib/cloud/rush.js +1 -1
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/computer/computer-rpc.js +2 -2
- package/dist/lib/computer/download.d.ts +15 -15
- package/dist/lib/computer/download.js +35 -98
- package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
- package/dist/lib/computer/ssh-tunnel.js +12 -22
- package/dist/lib/config-keys.d.ts +6 -11
- package/dist/lib/config-keys.js +18 -19
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +2 -2
- package/dist/lib/crabbox/lease.d.ts +9 -0
- package/dist/lib/crabbox/lease.js +70 -15
- package/dist/lib/crabbox/runtimes.js +1 -1
- package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
- package/dist/lib/daemon/account-state-daemon-service.js +26 -0
- package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
- package/dist/lib/daemon/browser-ipc-service.js +46 -0
- package/dist/lib/daemon/daemon.d.ts +3 -0
- package/dist/lib/daemon/daemon.js +212 -444
- package/dist/lib/daemon/device-probe-service.d.ts +26 -0
- package/dist/lib/daemon/device-probe-service.js +47 -0
- package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
- package/dist/lib/daemon/keychain-reap-service.js +32 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
- package/dist/lib/daemon/monitor-engine-service.js +29 -0
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
- package/dist/lib/daemon/secrets-broker-service.js +66 -0
- package/dist/lib/daemon/self-heal-service.d.ts +27 -0
- package/dist/lib/daemon/self-heal-service.js +48 -0
- package/dist/lib/daemon/service.d.ts +102 -0
- package/dist/lib/daemon/service.js +98 -0
- package/dist/lib/daemon/session-index-service.d.ts +22 -0
- package/dist/lib/daemon/session-index-service.js +40 -0
- package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
- package/dist/lib/daemon/state-dir-check-service.js +63 -0
- package/dist/lib/daemon/supervisor.d.ts +87 -0
- package/dist/lib/daemon/supervisor.js +298 -0
- package/dist/lib/daemon/watchdog-service.d.ts +19 -0
- package/dist/lib/daemon/watchdog-service.js +40 -0
- package/dist/lib/daemon-health.d.ts +15 -0
- package/dist/lib/daemon-health.js +29 -5
- package/dist/lib/daemon-services.d.ts +8 -1
- package/dist/lib/daemon-services.js +55 -0
- package/dist/lib/daemon-ticks.d.ts +13 -2
- package/dist/lib/daemon-ticks.js +22 -20
- package/dist/lib/device-config.d.ts +25 -10
- package/dist/lib/device-config.js +90 -39
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +26 -0
- package/dist/lib/devices/stats-cache.js +59 -7
- package/dist/lib/devices/worker-pick.d.ts +48 -0
- package/dist/lib/devices/worker-pick.js +113 -0
- package/dist/lib/exec.d.ts +70 -14
- package/dist/lib/exec.js +110 -38
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- package/dist/lib/harness/adapters/cursor.js +8 -11
- package/dist/lib/helper-download.d.ts +108 -0
- package/dist/lib/helper-download.js +194 -0
- package/dist/lib/helper-versions.d.ts +52 -0
- package/dist/lib/helper-versions.js +58 -0
- package/dist/lib/hooks/cache.js +19 -0
- package/dist/lib/hooks/install.js +2 -2
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/credentials.d.ts +1 -1
- package/dist/lib/hosts/credentials.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +7 -1
- package/dist/lib/hosts/dispatch.js +24 -5
- package/dist/lib/hosts/passthrough.d.ts +8 -15
- package/dist/lib/hosts/passthrough.js +35 -16
- package/dist/lib/hosts/providers/devices.d.ts +1 -1
- package/dist/lib/hosts/providers/devices.js +1 -1
- package/dist/lib/hosts/reconnect.d.ts +179 -21
- package/dist/lib/hosts/reconnect.js +266 -45
- package/dist/lib/hosts/registry.js +7 -0
- package/dist/lib/installations/migrate.d.ts +4 -5
- package/dist/lib/installations/migrate.js +6 -7
- package/dist/lib/installations/shims.d.ts +1 -1
- package/dist/lib/installations/shims.js +23 -13
- package/dist/lib/installations/versions.js +25 -2
- package/dist/lib/linear-autoclose.d.ts +1 -1
- package/dist/lib/linear-autoclose.js +1 -1
- package/dist/lib/loop.d.ts +2 -2
- package/dist/lib/loop.js +1 -1
- package/dist/lib/menubar/download-menubar.d.ts +47 -0
- package/dist/lib/menubar/download-menubar.js +60 -0
- package/dist/lib/menubar/install-menubar.d.ts +44 -7
- package/dist/lib/menubar/install-menubar.js +105 -23
- package/dist/lib/menubar/notify-desktop.d.ts +3 -3
- package/dist/lib/menubar/notify-desktop.js +6 -6
- package/dist/lib/model-tiers.d.ts +1 -1
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/project-probe.d.ts +1 -1
- package/dist/lib/project-probe.js +1 -1
- package/dist/lib/project-pull.d.ts +1 -1
- package/dist/lib/project-pull.js +2 -2
- package/dist/lib/projects.d.ts +1 -1
- package/dist/lib/projects.js +1 -1
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/sandbox.js +6 -7
- package/dist/lib/secrets/download-keychain.d.ts +47 -0
- package/dist/lib/secrets/download-keychain.js +70 -0
- package/dist/lib/secrets/install-helper.d.ts +20 -1
- package/dist/lib/secrets/install-helper.js +44 -3
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +86 -2
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +142 -8
- package/dist/lib/session/discover.d.ts +23 -1
- package/dist/lib/session/discover.js +44 -10
- package/dist/lib/session/highlights.d.ts +49 -1
- package/dist/lib/session/highlights.js +95 -0
- package/dist/lib/session/host-link.d.ts +16 -2
- package/dist/lib/session/host-link.js +7 -1
- package/dist/lib/session/parse.js +33 -1
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/remote-list.d.ts +9 -0
- package/dist/lib/session/remote/remote-list.js +19 -2
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +17 -3
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +93 -1
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/tool-calls.d.ts +2 -0
- package/dist/lib/session/tool-calls.js +1 -1
- package/dist/lib/session/trajectory-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +19 -0
- package/dist/lib/session/trajectory-html.js +770 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +284 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +384 -0
- package/dist/lib/session/types.d.ts +15 -0
- package/dist/lib/sha256-asset.d.ts +26 -0
- package/dist/lib/sha256-asset.js +41 -0
- package/dist/lib/share/backend.d.ts +95 -0
- package/dist/lib/share/backend.js +139 -0
- package/dist/lib/share/delete.d.ts +6 -2
- package/dist/lib/share/delete.js +8 -9
- package/dist/lib/share/html.d.ts +22 -0
- package/dist/lib/share/html.js +88 -0
- package/dist/lib/share/provision.d.ts +18 -4
- package/dist/lib/share/provision.js +38 -4
- package/dist/lib/share/publish.d.ts +39 -15
- package/dist/lib/share/publish.js +55 -26
- package/dist/lib/share/worker-template.js +171 -11
- package/dist/lib/sqlite.js +16 -3
- package/dist/lib/ssh-exec.d.ts +65 -0
- package/dist/lib/ssh-exec.js +120 -4
- package/dist/lib/startup/command-registry.d.ts +3 -1
- package/dist/lib/startup/command-registry.js +6 -3
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +2 -2
- package/dist/lib/state.js +11 -5
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/terminal/types.js +1 -1
- package/dist/lib/tmux/orphan-reap.js +1 -1
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +204 -27
- package/dist/lib/traces/backend.d.ts +29 -0
- package/dist/lib/traces/backend.js +46 -0
- package/dist/lib/traces/classify.d.ts +52 -0
- package/dist/lib/traces/classify.js +72 -0
- package/dist/lib/traces/config.d.ts +10 -0
- package/dist/lib/traces/config.js +2 -0
- package/dist/lib/traces/provision.d.ts +19 -0
- package/dist/lib/traces/provision.js +30 -0
- package/dist/lib/traces/sync.d.ts +150 -0
- package/dist/lib/traces/sync.js +447 -0
- package/dist/lib/traces/worker-template.d.ts +4 -0
- package/dist/lib/traces/worker-template.js +159 -0
- package/dist/lib/types.d.ts +34 -19
- package/dist/lib/types.js +14 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +18 -4
- package/dist/lib/usage-refresh.js +68 -19
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/dist/lib/workflows.d.ts +1 -1
- package/package.json +5 -7
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -24
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/dist/lib/usage-fleet.d.ts +0 -32
- package/dist/lib/usage-fleet.js +0 -125
package/dist/commands/ssh.js
CHANGED
|
@@ -18,10 +18,12 @@ import ora from 'ora';
|
|
|
18
18
|
import { getCliVersion } from '../lib/version.js';
|
|
19
19
|
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
20
20
|
import { machineId } from '../lib/session/sync/config.js';
|
|
21
|
+
import { assertRegistrableDeviceName } from '../lib/devices/registry.js';
|
|
21
22
|
import { isDeviceAuto, resolveDeviceAffinity } from '../lib/smart-launch.js';
|
|
23
|
+
import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, } from '../lib/devices/interactive-host.js';
|
|
22
24
|
import { registerFleetCaptureCommand } from './fleet-capture.js';
|
|
23
25
|
import { registerFleetApplyAlias } from './apply.js';
|
|
24
|
-
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
|
|
26
|
+
import { addIgnored, getDevice, loadDevices, loadIgnored, loadIgnoredEntries, removeDevice, removeIgnored, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
|
|
25
27
|
import { resolveDeviceProfile } from '../lib/devices/resolve-profile.js';
|
|
26
28
|
import { collectReachabilityWriteBacks, deviceOnlineState } from '../lib/devices/reachability.js';
|
|
27
29
|
import { nodeToDeviceInput, parseTailscaleStatus, tailscaleStatusJson, } from '../lib/devices/tailscale.js';
|
|
@@ -47,7 +49,7 @@ import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
|
47
49
|
import { listTasks, resolveTaskRef } from '../lib/hosts/tasks.js';
|
|
48
50
|
import { reconcileRunningTasks } from '../lib/hosts/reconcile.js';
|
|
49
51
|
import { stopDispatchedTask } from '../lib/hosts/dispatch.js';
|
|
50
|
-
import { terminalWidth, truncateToWidth } from '../lib/session/width.js';
|
|
52
|
+
import { stringWidth, stripAnsi, terminalWidth, truncateToWidth } from '../lib/session/width.js';
|
|
51
53
|
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
52
54
|
import { ALL_AGENT_IDS } from '../lib/agents.js';
|
|
53
55
|
import { collectLocalHarnessInventory, groupByAccount, renderAccountsMatrix, renderHarnessMatrix, } from '../lib/devices/harness-inventory.js';
|
|
@@ -113,30 +115,106 @@ function pctCell(v, width) {
|
|
|
113
115
|
return chalk.yellow(s);
|
|
114
116
|
return chalk.red(s);
|
|
115
117
|
}
|
|
118
|
+
/** Floor for the `spec` column; the real width is measured from the rows.
|
|
119
|
+
*
|
|
120
|
+
* A fixed width cannot work here: `fmtBytes` emits an optional decimal, so a
|
|
121
|
+
* spec string runs from `8c 16G 256G` (11) to `10c 23.5G 460G` (14) depending
|
|
122
|
+
* purely on the hardware behind it. Anything narrower than the widest row
|
|
123
|
+
* pushes load/mem/disk/headroom out of alignment row-to-row AND against the
|
|
124
|
+
* header — which defeats the scannability this column exists for. */
|
|
125
|
+
const SPEC_WIDTH_MIN = 12;
|
|
126
|
+
/** The static hardware as one compact cell — `12c 64G 1T`: cores, total RAM,
|
|
127
|
+
* total root disk via fmtBytes. `—` only when no probe has ever seen the box.
|
|
128
|
+
*
|
|
129
|
+
* Deliberately NOT gated on `reachable`: hardware does not change while a
|
|
130
|
+
* machine is down, so an offline device keeps rendering the spec from its last
|
|
131
|
+
* successful probe (`retainHardwareFacts`, RUSH-3096) rather than blanking a
|
|
132
|
+
* fact that is still true. The volatile columns beside it — load, mem, disk
|
|
133
|
+
* used — stay `—`, and `fleetCapacity` still counts only reachable boxes, so
|
|
134
|
+
* a down machine never contributes to usable capacity.
|
|
135
|
+
*
|
|
136
|
+
* Unpadded; {@link renderDeviceTable} pads to the measured column width. */
|
|
137
|
+
function specText(stats) {
|
|
138
|
+
if (!stats?.ncpu)
|
|
139
|
+
return '—';
|
|
140
|
+
return `${stats.ncpu}c ${fmtBytes(stats.memTotalBytes)} ${fmtBytes(stats.diskTotalBytes)}`;
|
|
141
|
+
}
|
|
142
|
+
/** The widest spec string across the rows actually being rendered, floored at
|
|
143
|
+
* SPEC_WIDTH_MIN so a short fleet still lines up with the header. */
|
|
144
|
+
function specColumnWidth(names, statsMap) {
|
|
145
|
+
return Math.max(SPEC_WIDTH_MIN, ...names.map((n) => stringWidth(specText(statsMap.get(n)))));
|
|
146
|
+
}
|
|
147
|
+
/** Colour + pad one spec cell to the measured column width. */
|
|
148
|
+
function specCell(stats, width) {
|
|
149
|
+
const text = specText(stats);
|
|
150
|
+
return (text === '—' ? chalk.gray : chalk.greenBright)(text.padEnd(width));
|
|
151
|
+
}
|
|
152
|
+
/** The one-line `description` config value per device (absent when unset). */
|
|
153
|
+
function listDeviceDescriptions(names) {
|
|
154
|
+
const out = {};
|
|
155
|
+
for (const n of names) {
|
|
156
|
+
const v = getConfigValue('description', { device: n }).value;
|
|
157
|
+
if (typeof v === 'string' && v.length > 0)
|
|
158
|
+
out[n] = v;
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Fit a device row to the terminal width. The description is the FIRST thing
|
|
164
|
+
* to truncate, then the role tag; the fixed columns (device, platform, spec,
|
|
165
|
+
* load/mem/disk numbers, headroom, and the ▸/★/←/relay markers) never
|
|
166
|
+
* truncate. `role` and `desc` arrive fully rendered (ansi + leading gap).
|
|
167
|
+
*/
|
|
168
|
+
function fitDeviceRow(fixed, role, desc, width) {
|
|
169
|
+
const w = (s) => stringWidth(stripAnsi(s));
|
|
170
|
+
let over = w(fixed) + w(role) + w(desc) - width;
|
|
171
|
+
if (over > 0 && desc) {
|
|
172
|
+
// `desc` is ' ' + gray(text) — truncate the text, keep the gap.
|
|
173
|
+
const text = stripAnsi(desc).slice(2);
|
|
174
|
+
const budget = w(text) - over;
|
|
175
|
+
desc = budget > 0 ? ' ' + chalk.gray(truncateToWidth(text, budget)) : '';
|
|
176
|
+
over = w(fixed) + w(role) + w(desc) - width;
|
|
177
|
+
}
|
|
178
|
+
// A partial role word ("work…") reads as a bug — when it can't fit whole,
|
|
179
|
+
// it drops entirely. The numbers are never touched.
|
|
180
|
+
if (over > 0 && role)
|
|
181
|
+
role = '';
|
|
182
|
+
return fixed + role + desc;
|
|
183
|
+
}
|
|
116
184
|
/**
|
|
117
185
|
* Render the device list. When `statsMap` is provided, resource columns are
|
|
118
|
-
* appended —
|
|
119
|
-
*
|
|
186
|
+
* appended — a `spec` cell (cores / total RAM / total disk), normalized load,
|
|
187
|
+
* memory and disk-used percentages, a headroom badge, and (in `full` mode)
|
|
188
|
+
* free/total memory — with the per-device role and `description` riding the
|
|
189
|
+
* tail, so it's obvious which boxes have room and what each box is for.
|
|
120
190
|
* Without it (probe skipped) the classic reachability line is used. A fleet
|
|
121
191
|
* capacity summary is appended whenever stats were gathered.
|
|
122
192
|
*/
|
|
123
|
-
function renderDeviceTable(reg, names, self, statsMap, full = false, interactiveHost) {
|
|
193
|
+
export function renderDeviceTable(reg, names, self, statsMap, full = false, interactiveHost, opts = {}) {
|
|
124
194
|
if (!statsMap) {
|
|
125
195
|
const roles = listConfiguredDeviceRoles(names);
|
|
126
196
|
return names.map((n) => deviceSummary(reg[n], n === self, undefined, n === interactiveHost, roles));
|
|
127
197
|
}
|
|
128
198
|
const deviceRoles = listConfiguredDeviceRoles(names);
|
|
199
|
+
const specWidth = specColumnWidth(names, statsMap);
|
|
200
|
+
const descriptions = listDeviceDescriptions(names);
|
|
201
|
+
const width = opts.width ?? terminalWidth();
|
|
129
202
|
const lines = [];
|
|
130
203
|
const head = ' ' +
|
|
131
204
|
chalk.gray('device'.padEnd(16)) +
|
|
132
205
|
chalk.gray('platform'.padEnd(8)) +
|
|
133
206
|
' ' +
|
|
134
|
-
|
|
207
|
+
chalk.gray('spec'.padEnd(specWidth)) +
|
|
135
208
|
chalk.gray('load'.padStart(5)) +
|
|
136
209
|
chalk.gray('mem'.padStart(6)) +
|
|
210
|
+
chalk.gray('disk'.padStart(5)) +
|
|
137
211
|
(full ? ' ' + chalk.gray('free/total'.padEnd(12)) : '') +
|
|
138
212
|
' ' +
|
|
139
|
-
chalk.gray('headroom')
|
|
213
|
+
chalk.gray('headroom') +
|
|
214
|
+
' ' +
|
|
215
|
+
chalk.gray('role') +
|
|
216
|
+
' ' +
|
|
217
|
+
chalk.gray('description');
|
|
140
218
|
lines.push(head);
|
|
141
219
|
for (const name of names) {
|
|
142
220
|
// Effective profile: the operator's central config (ssh.*/platform/user)
|
|
@@ -147,18 +225,30 @@ function renderDeviceTable(reg, names, self, statsMap, full = false, interactive
|
|
|
147
225
|
const label = isSelf ? chalk.bold.cyan(name.padEnd(16)) : chalk.bold(name.padEnd(16));
|
|
148
226
|
const plat = String(d.platform).padEnd(8);
|
|
149
227
|
const stats = statsMap.get(name);
|
|
228
|
+
const role = roleTag(name, deviceRoles);
|
|
229
|
+
const desc = descriptions[name] ? ' ' + chalk.gray(descriptions[name]) : '';
|
|
150
230
|
// Prefer this run's live probe, then the written-back verdict, over the
|
|
151
231
|
// stale tailscale.online snapshot — so a reachable box never renders
|
|
152
232
|
// "offline" while its live load/mem sit one column over (RUSH-1965).
|
|
153
233
|
const offline = deviceOnlineState(d, stats) === 'offline';
|
|
154
234
|
if (offline) {
|
|
155
|
-
|
|
235
|
+
// The spec cell rides the offline row: hardware is what the box IS, not
|
|
236
|
+
// what it is doing, so it stays legible while the machine is down
|
|
237
|
+
// (RUSH-3096). The live columns are omitted rather than dashed — there is
|
|
238
|
+
// no current load/mem/disk to report for a box that did not answer.
|
|
239
|
+
//
|
|
240
|
+
// The explicit gap is load-bearing: `specCell` pads to the MEASURED
|
|
241
|
+
// column width, so the widest spec on the fleet pads to nothing and the
|
|
242
|
+
// marker would collide with it ("455Goffline"). An online row gets its
|
|
243
|
+
// separation for free from `pctCell`'s right-alignment; this one has no
|
|
244
|
+
// numeric cell to lean on.
|
|
245
|
+
lines.push(fitDeviceRow(`${marker}${label}${plat} ${specCell(stats, specWidth)} ${chalk.gray('offline')}`, role, desc, width));
|
|
156
246
|
continue;
|
|
157
247
|
}
|
|
158
248
|
const relay = !isSelf && d.tailscale?.online && !d.tailscale.direct ? chalk.yellow(' relay') : '';
|
|
159
|
-
const cores = full ? chalk.gray(String(stats?.ncpu ?? '—').padStart(6)) : '';
|
|
160
249
|
const load = pctCell(stats?.loadPercent, 5);
|
|
161
250
|
const mem = pctCell(stats?.memPercent, 6);
|
|
251
|
+
const disk = pctCell(stats?.diskUsedPercent, 5);
|
|
162
252
|
const freeTotal = full
|
|
163
253
|
? ' ' +
|
|
164
254
|
(stats?.reachable && stats.memTotalBytes
|
|
@@ -168,13 +258,21 @@ function renderDeviceTable(reg, names, self, statsMap, full = false, interactive
|
|
|
168
258
|
const badge = HEADROOM_BADGE[headroom(stats)];
|
|
169
259
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
170
260
|
const interactive = name === interactiveHost ? chalk.yellow(' ★ interactive') : '';
|
|
171
|
-
lines.push(`${marker}${label}${plat} ${
|
|
261
|
+
lines.push(fitDeviceRow(`${marker}${label}${plat} ${specCell(stats, specWidth)}${load}${mem}${disk}${freeTotal} ${badge}${relay}${here}${interactive}`, role, desc, width));
|
|
172
262
|
}
|
|
173
|
-
// Fleet capacity summary — total cores
|
|
263
|
+
// Fleet capacity summary — total cores, how much RAM is free right now, and
|
|
264
|
+
// total free root disk across the reachable fleet.
|
|
174
265
|
const cap = fleetCapacity(statsMap.values());
|
|
175
266
|
if (cap.reachable > 0) {
|
|
176
267
|
const freePct = cap.memTotalBytes > 0 ? Math.round((cap.memFreeBytes / cap.memTotalBytes) * 100) : 0;
|
|
177
|
-
|
|
268
|
+
let diskFreeBytes = 0;
|
|
269
|
+
for (const s of statsMap.values())
|
|
270
|
+
if (s.reachable)
|
|
271
|
+
diskFreeBytes += s.diskFreeBytes ?? 0;
|
|
272
|
+
lines.push(chalk.gray(` Fleet capacity: ${cap.cores} cores · ${fmtBytes(cap.memFreeBytes)} free / ${fmtBytes(cap.memTotalBytes)} RAM (${freePct}% free) · ${fmtBytes(diskFreeBytes)} disk free across ${cap.reachable} reachable device${cap.reachable === 1 ? '' : 's'}`));
|
|
273
|
+
}
|
|
274
|
+
if (opts.ignoredCount) {
|
|
275
|
+
lines.push(chalk.gray(` ${opts.ignoredCount} ignored node${opts.ignoredCount === 1 ? '' : 's'} not listed — 'agents devices ignored'`));
|
|
178
276
|
}
|
|
179
277
|
return lines;
|
|
180
278
|
}
|
|
@@ -975,6 +1073,7 @@ function registerDevicesCommands(program) {
|
|
|
975
1073
|
agents devices sync # pick which tailscale nodes to keep (TTY)
|
|
976
1074
|
agents devices sync --yes # register all non-ignored nodes
|
|
977
1075
|
agents devices ignore ipad165 # dismiss a node so it's never re-suggested
|
|
1076
|
+
agents devices ignored # list dismissed nodes (when / which machine)
|
|
978
1077
|
|
|
979
1078
|
Inspect:
|
|
980
1079
|
agents devices list # what's registered (★ = interactive host)
|
|
@@ -990,6 +1089,7 @@ function registerDevicesCommands(program) {
|
|
|
990
1089
|
agents devices config win-mini ssh.auth password
|
|
991
1090
|
agents devices config worker ssh.identity-file ~/.ssh/worker_ed25519
|
|
992
1091
|
agents devices config mac-mini auto-launch.enabled off
|
|
1092
|
+
agents devices describe mark-1 "gpu box — cuda 12.4" # one-line purpose, shown in the list
|
|
993
1093
|
agents devices config mac-mini interactive.host zion # where agents show YOU artifacts
|
|
994
1094
|
agents devices render --write # write ~/.ssh/config.d/agents include
|
|
995
1095
|
|
|
@@ -1002,10 +1102,10 @@ function registerDevicesCommands(program) {
|
|
|
1002
1102
|
registerLeaseCommand(devicesCmd);
|
|
1003
1103
|
registerSnapshotCommand(devicesCmd);
|
|
1004
1104
|
registerCommandGroups(devicesCmd, [
|
|
1005
|
-
{ title: 'Discover & register', names: ['sync', 'register', 'add', 'ignore', 'unignore', '
|
|
1105
|
+
{ title: 'Discover & register', names: ['sync', 'register', 'add', 'ignore', 'unignore', 'ignored', 'remove'] },
|
|
1006
1106
|
{ title: 'Inspect', names: ['list', 'show', 'status', 'ping', 'harnesses', 'accounts', 'snapshot'] },
|
|
1007
1107
|
{ title: 'Disposable devices', names: ['lease'] },
|
|
1008
|
-
{ title: 'Configure a device', names: ['config', 'render'] },
|
|
1108
|
+
{ title: 'Configure a device', names: ['config', 'describe', 'render'] },
|
|
1009
1109
|
{ title: 'Fleet operations', names: ['update', 'run', 'login', 'capture', 'apply'] },
|
|
1010
1110
|
]);
|
|
1011
1111
|
devicesCmd
|
|
@@ -1085,6 +1185,47 @@ function registerDevicesCommands(program) {
|
|
|
1085
1185
|
setDeviceDiscoveryStatus(name, undefined);
|
|
1086
1186
|
console.log(chalk.green(`No longer ignoring '${name}'`) + chalk.gray(' — run `agents devices sync` to register it.'));
|
|
1087
1187
|
});
|
|
1188
|
+
const ignoredCmd = devicesCmd
|
|
1189
|
+
.command('ignored')
|
|
1190
|
+
.description('List dismissed tailscale nodes — what was dismissed, when, and on which machine.')
|
|
1191
|
+
.option('--json', 'output machine-readable JSON')
|
|
1192
|
+
.action((opts) => {
|
|
1193
|
+
try {
|
|
1194
|
+
const entries = loadIgnoredEntries();
|
|
1195
|
+
if (opts.json) {
|
|
1196
|
+
process.stdout.write(JSON.stringify(entries, null, 2) + '\n');
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (entries.length === 0) {
|
|
1200
|
+
console.log(chalk.gray("No ignored nodes. 'agents devices ignore <name>' dismisses one — it won't be re-suggested."));
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
console.log(chalk.bold(`Ignored nodes (${entries.length})`));
|
|
1204
|
+
for (const e of entries) {
|
|
1205
|
+
const age = formatCheckedAge(Date.parse(e.ignoredAt));
|
|
1206
|
+
console.log(` ${chalk.bold(e.name.padEnd(24))} ${chalk.gray(`${age} · dismissed on ${e.ignoredOn}`)}`);
|
|
1207
|
+
}
|
|
1208
|
+
console.log(chalk.gray("Undo one with 'agents devices unignore <name>'."));
|
|
1209
|
+
}
|
|
1210
|
+
catch (err) {
|
|
1211
|
+
console.error(chalk.red(err.message));
|
|
1212
|
+
process.exit(1);
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
setHelpSections(ignoredCmd, {
|
|
1216
|
+
examples: `
|
|
1217
|
+
agents devices ignored # what was dismissed, when, and on which machine
|
|
1218
|
+
agents devices ignored --json # machine-readable [{ name, ignoredAt, ignoredOn }]
|
|
1219
|
+
agents devices unignore old-laptop # undo a dismissal
|
|
1220
|
+
`,
|
|
1221
|
+
notes: `
|
|
1222
|
+
Dismissals live in the tracked central agents.yaml (fleet.ignored) and
|
|
1223
|
+
sync with 'agents repo push/pull', so a node dismissed on one box stays
|
|
1224
|
+
dismissed everywhere. An ignored node is not a device — it never enters
|
|
1225
|
+
the registry, so 'agents devices list' never shows it; this command is
|
|
1226
|
+
where dismissals are visible.
|
|
1227
|
+
`,
|
|
1228
|
+
});
|
|
1088
1229
|
// ─── devices config (unified settings surface) ────────────────────────────
|
|
1089
1230
|
//
|
|
1090
1231
|
// ONE command for every per-device setting: `agents devices config <name>
|
|
@@ -1461,6 +1602,8 @@ function registerDevicesCommands(program) {
|
|
|
1461
1602
|
`,
|
|
1462
1603
|
notes: `
|
|
1463
1604
|
Keys: role (worker|personal), see 'agents devices role',
|
|
1605
|
+
description (one line saying what the box is for — see
|
|
1606
|
+
'agents devices describe'; renders in the devices list tail),
|
|
1464
1607
|
agents.max-concurrent, scheduler.enabled, daemon.enabled,
|
|
1465
1608
|
watchdog.enabled, tmux.enabled, browser.remote-control,
|
|
1466
1609
|
browser.task-idle-minutes, browser.profile,
|
|
@@ -1529,6 +1672,40 @@ function registerDevicesCommands(program) {
|
|
|
1529
1672
|
box stays excluded, since that is what the mark is for.
|
|
1530
1673
|
`,
|
|
1531
1674
|
});
|
|
1675
|
+
const describeCmd = devicesCmd
|
|
1676
|
+
.command('describe <name> [text...]')
|
|
1677
|
+
.description('Show or set the one-line description of what a device is FOR ("gpu box — cuda 12.4"). ' +
|
|
1678
|
+
'Rendered as the tail column of `agents devices list` and synced fleet-wide. ' +
|
|
1679
|
+
'Same key as `agents devices config <name> description` — one store, two names.')
|
|
1680
|
+
.option('--unset', 'remove the description (the device falls back to the fleet default / unset)')
|
|
1681
|
+
.option('--json', 'output machine-readable JSON')
|
|
1682
|
+
.action(async (name, textParts, opts) => {
|
|
1683
|
+
try {
|
|
1684
|
+
// Thin sugar over the 'description' config key — the same engine that
|
|
1685
|
+
// backs `agents devices config <name> description`, not a second path.
|
|
1686
|
+
await runDevicesConfig(name, 'description', textParts ?? [], opts);
|
|
1687
|
+
}
|
|
1688
|
+
catch (err) {
|
|
1689
|
+
console.error(chalk.red(err.message));
|
|
1690
|
+
process.exit(1);
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
setHelpSections(describeCmd, {
|
|
1694
|
+
examples: `
|
|
1695
|
+
agents devices describe mark-1 "gpu box — cuda 12.4" # set it (80 chars, one line)
|
|
1696
|
+
agents devices describe mark-1 # read it back
|
|
1697
|
+
agents devices describe mark-1 --unset # remove it
|
|
1698
|
+
agents devices describe mark-1 --json # machine-readable
|
|
1699
|
+
agents devices config mark-1 description "gpu box" # equivalent — same store
|
|
1700
|
+
`,
|
|
1701
|
+
notes: `
|
|
1702
|
+
The description is the device-scope 'description' config key: stored in
|
|
1703
|
+
the tracked per-device doc (devices/<name>/agents.yaml config: block),
|
|
1704
|
+
synced fleet-wide with 'agents repo push/pull', and rendered as the tail
|
|
1705
|
+
column of 'agents devices list'. It replaces on each set — for appended
|
|
1706
|
+
long-form scratch use 'agents devices config <name> notes "…"'.
|
|
1707
|
+
`,
|
|
1708
|
+
});
|
|
1532
1709
|
/** Deprecation notice for a retired subcommand — STDERR only, so a --json consumer's stdout stays parseable. */
|
|
1533
1710
|
const configTombstoneNotice = (retired, replacement) => {
|
|
1534
1711
|
console.error(chalk.yellow(`Deprecated: "agents devices ${retired}" is now "agents devices ${replacement}". Running that for you.\n`));
|
|
@@ -1753,6 +1930,7 @@ function registerDevicesCommands(program) {
|
|
|
1753
1930
|
process.stdout.write(JSON.stringify(names.map((name) => {
|
|
1754
1931
|
const config = deviceConfigJson(name);
|
|
1755
1932
|
const health = statsMap?.get(name);
|
|
1933
|
+
const description = getConfigValue('description', { device: name }).value;
|
|
1756
1934
|
return {
|
|
1757
1935
|
...resolveDeviceProfile(reg[name]),
|
|
1758
1936
|
interactive: name === interactiveHost,
|
|
@@ -1760,6 +1938,7 @@ function registerDevicesCommands(program) {
|
|
|
1760
1938
|
// marked (absent when unmarked), `autoPool` is the answer that
|
|
1761
1939
|
// matters to a caller — may `--device auto` pick this box.
|
|
1762
1940
|
...(jsonRoles[name] ? { role: jsonRoles[name] } : {}),
|
|
1941
|
+
...(typeof description === 'string' && description ? { description } : {}),
|
|
1763
1942
|
autoPool: autoPool.has(name),
|
|
1764
1943
|
...(config ? { config } : {}),
|
|
1765
1944
|
...(health ? { health: { ...health, headroom: headroom(health) } } : {}),
|
|
@@ -1768,7 +1947,10 @@ function registerDevicesCommands(program) {
|
|
|
1768
1947
|
return;
|
|
1769
1948
|
}
|
|
1770
1949
|
console.log(chalk.bold(`Devices (${names.length})`));
|
|
1771
|
-
|
|
1950
|
+
// Dismissed nodes are not devices (never in the registry) — surface their
|
|
1951
|
+
// count under the table so a "missing" node is explainable from the list.
|
|
1952
|
+
const ignoredCount = loadIgnoredEntries().length;
|
|
1953
|
+
for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full, interactiveHost, { ignoredCount }))
|
|
1772
1954
|
console.log(line);
|
|
1773
1955
|
if (freshness?.servedFromCache && freshness.oldestFetchedAt != null) {
|
|
1774
1956
|
console.log(chalk.gray(` updated ${formatCheckedAge(freshness.oldestFetchedAt)} — pass --refresh (--live) for a live probe`));
|
|
@@ -1789,7 +1971,7 @@ function registerDevicesCommands(program) {
|
|
|
1789
1971
|
devicesCmd
|
|
1790
1972
|
.command('list')
|
|
1791
1973
|
.alias('ls')
|
|
1792
|
-
.description('List registered devices with platform,
|
|
1974
|
+
.description('List registered devices with platform, spec (cores/RAM/disk), live load/mem/disk headroom, role, and description.')
|
|
1793
1975
|
.option('--json', 'output effective device profiles, config, and health as a JSON array')
|
|
1794
1976
|
.option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
|
|
1795
1977
|
.option('--refresh', 'force a live probe of every device, bypassing the cache')
|
|
@@ -1832,6 +2014,36 @@ function registerDevicesCommands(program) {
|
|
|
1832
2014
|
await runFleetPing({ ...opts, verbose });
|
|
1833
2015
|
});
|
|
1834
2016
|
const csvList = (s) => s ? s.split(',').map((x) => x.trim()).filter(Boolean) : undefined;
|
|
2017
|
+
devicesCmd
|
|
2018
|
+
.command('pick')
|
|
2019
|
+
.description('Print the device automatic placement would choose for offloaded machine work (the suite, a build) — least-loaded, reachable, POSIX, never the box you are sitting at. Writes just the name to stdout so scripts can consume it.')
|
|
2020
|
+
.option('--json', 'output the pick plus every candidate and exclusion reason')
|
|
2021
|
+
.option('--platform <list>', 'comma-separated platforms to allow (default: linux,macos)')
|
|
2022
|
+
.action(async (opts) => {
|
|
2023
|
+
const { resolveWorkerDevice } = await import('../lib/devices/worker-pick.js');
|
|
2024
|
+
let plan;
|
|
2025
|
+
try {
|
|
2026
|
+
plan = await resolveWorkerDevice({ platforms: csvList(opts.platform) });
|
|
2027
|
+
}
|
|
2028
|
+
catch (err) {
|
|
2029
|
+
// Fail loud with the resolver's own message. Never print a device name
|
|
2030
|
+
// we did not actually pick: a caller reading stdout would ship a tree to it.
|
|
2031
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
2032
|
+
process.exitCode = 1;
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
if (opts.json) {
|
|
2036
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
// stdout is the name alone (`box="$(agents devices pick)"`); the human
|
|
2040
|
+
// detail goes to stderr so it never pollutes that capture.
|
|
2041
|
+
const detail = plan.candidates
|
|
2042
|
+
.map((c) => `${c.device}:${c.loadPercent === undefined ? '?' : `${Math.round(c.loadPercent)}%`}`)
|
|
2043
|
+
.join(' ');
|
|
2044
|
+
console.error(chalk.gray(`[agents] worker=${plan.device}${plan.isLocal ? ' (this machine)' : ''} candidates: ${detail}`));
|
|
2045
|
+
console.log(plan.device);
|
|
2046
|
+
});
|
|
1835
2047
|
const harnessInvOpts = (opts) => ({
|
|
1836
2048
|
agents: csvList(opts.agents),
|
|
1837
2049
|
devices: csvList(opts.device ?? opts.devices),
|
|
@@ -1924,6 +2136,10 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
|
|
|
1924
2136
|
.option('--platform <platform>', 'windows | linux | macos')
|
|
1925
2137
|
.action(async (name, target, opts) => {
|
|
1926
2138
|
try {
|
|
2139
|
+
// The one place a device name is CHOSEN rather than observed, so the one
|
|
2140
|
+
// place the reserved-sentinel policy belongs. upsertDevice itself stays
|
|
2141
|
+
// shape-only — `devices sync` feeds it tailnet node names in a loop.
|
|
2142
|
+
assertRegistrableDeviceName(name);
|
|
1927
2143
|
const { host, user } = splitUserHost(target);
|
|
1928
2144
|
const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
|
|
1929
2145
|
const d = await upsertDevice(name, {
|
|
@@ -1987,8 +2203,8 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
|
|
|
1987
2203
|
}
|
|
1988
2204
|
});
|
|
1989
2205
|
devicesCmd
|
|
1990
|
-
.command('
|
|
1991
|
-
.alias('
|
|
2206
|
+
.command('remove <name>')
|
|
2207
|
+
.alias('rm')
|
|
1992
2208
|
.description('Remove a device from the registry.')
|
|
1993
2209
|
.action(async (name) => {
|
|
1994
2210
|
const ok = await removeDevice(name);
|
|
@@ -2190,6 +2406,15 @@ the target when it exists, else the remote home. Same portable-cwd rule as
|
|
|
2190
2406
|
// nothing to resolve "self" to, and mis-report the pick as "Unknown
|
|
2191
2407
|
// device").
|
|
2192
2408
|
let target = name;
|
|
2409
|
+
if (isDeviceInteractive(name)) {
|
|
2410
|
+
const pinned = resolveInteractiveDevice();
|
|
2411
|
+
if (!pinned) {
|
|
2412
|
+
console.error(chalk.red(interactiveUnsetError()));
|
|
2413
|
+
process.exit(1);
|
|
2414
|
+
}
|
|
2415
|
+
process.stderr.write(chalk.gray(`[agents] device=interactive → ${pinned}\n`));
|
|
2416
|
+
target = pinned;
|
|
2417
|
+
}
|
|
2193
2418
|
if (isDeviceAuto(name)) {
|
|
2194
2419
|
const plan = resolveDeviceAffinity({});
|
|
2195
2420
|
if (!plan.host) {
|
package/dist/commands/teams.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } f
|
|
|
18
18
|
import { setHelpSections } from '../lib/help.js';
|
|
19
19
|
import { createWorktree, commitsBehindDefault, isGitRepo, hasUncommittedChanges, removeWorktree, worktreeCheckoutExists, worktreeExists, } from '../lib/teams/worktree.js';
|
|
20
20
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
21
|
+
import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, } from '../lib/devices/interactive-host.js';
|
|
21
22
|
import { isDeviceAuto, resolveDeviceAuto } from '../lib/smart-launch.js';
|
|
22
23
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
23
24
|
import { ensureHostReady } from '../lib/hosts/ready.js';
|
|
@@ -1369,7 +1370,22 @@ export function registerTeamsCommands(program) {
|
|
|
1369
1370
|
// trim, drop blanks, dedupe (preserving first-seen order).
|
|
1370
1371
|
const rawPool = [opts.devices, opts.hosts].filter(Boolean).join(',');
|
|
1371
1372
|
const devices = [];
|
|
1372
|
-
for (const
|
|
1373
|
+
for (const raw of rawPool.split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
1374
|
+
// Resolve `interactive` to the concrete device HERE, the same way the
|
|
1375
|
+
// single `--device` pin does. A pool is a set of specific machines the
|
|
1376
|
+
// scheduler picks between, so persisting the sentinel would let the
|
|
1377
|
+
// pool's membership change silently the next time `interactive.host`
|
|
1378
|
+
// is re-pinned — and every later friction message would quote
|
|
1379
|
+
// "interactive" instead of a box the user can act on.
|
|
1380
|
+
let d = raw;
|
|
1381
|
+
if (isDeviceInteractive(d)) {
|
|
1382
|
+
const pinned = resolveInteractiveDevice();
|
|
1383
|
+
if (!pinned) {
|
|
1384
|
+
dieFriction('teams', 'pool-device-interactive-unset', interactiveUnsetError());
|
|
1385
|
+
}
|
|
1386
|
+
process.stderr.write(chalk.gray(`[teams] pool device=interactive → ${pinned}\n`));
|
|
1387
|
+
d = pinned;
|
|
1388
|
+
}
|
|
1373
1389
|
if (!devices.includes(d))
|
|
1374
1390
|
devices.push(d);
|
|
1375
1391
|
}
|
|
@@ -1444,7 +1460,7 @@ export function registerTeamsCommands(program) {
|
|
|
1444
1460
|
.alias('a')
|
|
1445
1461
|
.description("Add a teammate to work on a task. Runs in background; returns immediately. Use 'status' to check in.")
|
|
1446
1462
|
.option('-n, --name <name>', 'Friendly name for this teammate (e.g. alice). Required if using --after. Unique within team.')
|
|
1447
|
-
.option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (
|
|
1463
|
+
.option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (more autonomous than edit, mechanism per-harness — see 'agents modes <agent>') | skip (bypass all permission prompts). 'full' accepted as alias for skip. Teammates run headless: plan works headless on claude/codex/cursor/droid/opencode; kimi/grok/antigravity have no headless plan mode and auto-downgrade a plan request to auto.`, 'edit')
|
|
1448
1464
|
.option('-e, --effort <effort>', `Reasoning intensity: ${VALID_EFFORTS.join('|')}`, 'medium')
|
|
1449
1465
|
.option('--model <model>', 'Cost tier (cheap|default|best|ultra) or a concrete id (e.g. claude-opus-4-8); tiers resolve per harness+version to a supported model')
|
|
1450
1466
|
.option('--env <key=value>', 'Set an environment variable for this teammate (repeatable for multiple vars)', (val, prev) => [...prev, val], [])
|
|
@@ -1507,6 +1523,22 @@ export function registerTeamsCommands(program) {
|
|
|
1507
1523
|
process.stderr.write(chalk.gray(`[teams] device=auto → ${picked === machineId() ? 'local' : picked}\n`));
|
|
1508
1524
|
explicitDevice = picked;
|
|
1509
1525
|
}
|
|
1526
|
+
// `interactive` needs the same up-front resolution, and for a sharper
|
|
1527
|
+
// reason: `teams add`/`create` bail out of the fleet passthrough
|
|
1528
|
+
// (passthrough.ts, the teams subcommand guard), so the sentinel block
|
|
1529
|
+
// there never runs and this is the only resolver. Left literal, the
|
|
1530
|
+
// local-machine check below compares 'interactive' against the machine id,
|
|
1531
|
+
// takes the remote branch on the pinned box itself, and pins the teammate
|
|
1532
|
+
// to an SSH hop into this same machine — skipping three local preflights
|
|
1533
|
+
// on the way.
|
|
1534
|
+
if (explicitDevice && isDeviceInteractive(explicitDevice)) {
|
|
1535
|
+
const pinned = resolveInteractiveDevice();
|
|
1536
|
+
if (!pinned) {
|
|
1537
|
+
dieFriction('teams', 'device-interactive-unset', interactiveUnsetError());
|
|
1538
|
+
}
|
|
1539
|
+
process.stderr.write(chalk.gray(`[teams] device=interactive → ${pinned === machineId() ? 'local' : pinned}\n`));
|
|
1540
|
+
explicitDevice = pinned;
|
|
1541
|
+
}
|
|
1510
1542
|
// Distributed teams: --device <name> PINS this teammate to a machine over
|
|
1511
1543
|
// SSH. Resolve + validate the placement here so a bad target fails at `add`
|
|
1512
1544
|
// time, not silently at launch. Persisted (hostName/hostTarget/repoPath) so
|