@phnx-labs/agents-cli 1.22.35 → 1.22.37
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 +666 -2
- package/README.md +75 -35
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.d.ts +15 -0
- package/dist/bootstrap.js +1211 -0
- package/dist/commands/accounts.d.ts +4 -11
- package/dist/commands/accounts.js +159 -27
- package/dist/commands/attach.js +17 -4
- package/dist/commands/audit.d.ts +8 -8
- package/dist/commands/audit.js +57 -47
- package/dist/commands/browser.js +15 -11
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +0 -11
- package/dist/commands/computer-actions.js +33 -0
- package/dist/commands/computer-sessions-picker.d.ts +21 -0
- package/dist/commands/computer-sessions-picker.js +141 -0
- package/dist/commands/computer.d.ts +16 -0
- package/dist/commands/computer.js +37 -0
- package/dist/commands/config.d.ts +17 -0
- package/dist/commands/config.js +401 -0
- package/dist/commands/cp.d.ts +84 -0
- package/dist/commands/cp.js +231 -0
- package/dist/commands/daemon.js +243 -14
- package/dist/commands/detach-core.d.ts +1 -1
- package/dist/commands/detach-core.js +12 -2
- package/dist/commands/detach.js +1 -1
- package/dist/commands/doctor.js +64 -5
- package/dist/commands/events.d.ts +24 -12
- package/dist/commands/events.js +144 -28
- package/dist/commands/exec.js +85 -41
- package/dist/commands/feed.d.ts +1 -1
- package/dist/commands/feed.js +16 -8
- package/dist/commands/focus.d.ts +33 -1
- package/dist/commands/focus.js +177 -17
- package/dist/commands/funnel.d.ts +1 -1
- package/dist/commands/go.js +9 -8
- package/dist/commands/harness-wizard.d.ts +1 -0
- package/dist/commands/harness-wizard.js +18 -20
- package/dist/commands/harness.d.ts +2 -0
- package/dist/commands/harness.js +59 -26
- package/dist/commands/hooks.js +0 -11
- package/dist/commands/inspect.d.ts +94 -1
- package/dist/commands/inspect.js +866 -40
- package/dist/commands/lease.d.ts +4 -4
- package/dist/commands/lease.js +6 -6
- package/dist/commands/login.js +0 -13
- package/dist/commands/logs.d.ts +2 -2
- package/dist/commands/logs.js +2 -2
- package/dist/commands/mcp.js +7 -2
- package/dist/commands/memory.js +7 -2
- package/dist/commands/message.d.ts +1 -1
- package/dist/commands/message.js +37 -6
- package/dist/commands/models.js +4 -0
- package/dist/commands/monitors.d.ts +0 -1
- package/dist/commands/monitors.js +199 -16
- package/dist/commands/open.d.ts +18 -0
- package/dist/commands/open.js +57 -0
- package/dist/commands/packages.js +2 -107
- package/dist/commands/plugins.js +6 -3
- package/dist/commands/profiles.d.ts +1 -0
- package/dist/commands/profiles.js +16 -160
- package/dist/commands/projects.d.ts +28 -0
- package/dist/commands/projects.js +125 -5
- package/dist/commands/reconnect.js +9 -3
- package/dist/commands/repo.js +42 -1
- package/dist/commands/resource-view.d.ts +17 -0
- package/dist/commands/resource-view.js +36 -14
- package/dist/commands/resume.js +9 -2
- package/dist/commands/routines.d.ts +0 -2
- package/dist/commands/routines.js +473 -289
- package/dist/commands/secrets.js +52 -9
- package/dist/commands/sessions-export.d.ts +47 -0
- package/dist/commands/sessions-export.js +116 -6
- package/dist/commands/sessions-import.d.ts +17 -0
- package/dist/commands/sessions-import.js +114 -6
- package/dist/commands/sessions-migrate.d.ts +27 -0
- package/dist/commands/sessions-migrate.js +50 -9
- package/dist/commands/sessions-picker.js +23 -4
- package/dist/commands/sessions-resume.d.ts +5 -3
- package/dist/commands/sessions-resume.js +30 -8
- package/dist/commands/sessions-watch.d.ts +2 -0
- package/dist/commands/sessions-watch.js +53 -0
- package/dist/commands/sessions.d.ts +66 -3
- package/dist/commands/sessions.js +298 -47
- package/dist/commands/set.d.ts +1 -1
- package/dist/commands/set.js +2 -2
- package/dist/commands/setup-browser.js +2 -2
- package/dist/commands/setup-mine.js +0 -1
- package/dist/commands/setup-preferences.js +1 -1
- package/dist/commands/setup-share.js +34 -4
- package/dist/commands/setup.d.ts +5 -0
- package/dist/commands/setup.js +26 -0
- package/dist/commands/share.d.ts +67 -0
- package/dist/commands/share.js +275 -12
- package/dist/commands/skills.js +0 -11
- package/dist/commands/snapshot.js +1 -1
- package/dist/commands/ssh.d.ts +18 -0
- package/dist/commands/ssh.js +549 -210
- package/dist/commands/sync.d.ts +5 -0
- package/dist/commands/sync.js +212 -32
- package/dist/commands/teams.d.ts +13 -0
- package/dist/commands/teams.js +187 -30
- package/dist/commands/tickets.d.ts +2 -0
- package/dist/commands/tickets.js +43 -0
- package/dist/commands/update.js +2 -4
- package/dist/commands/usage.js +5 -3
- package/dist/commands/versions.js +11 -5
- package/dist/commands/view.js +15 -7
- package/dist/commands/webhook.d.ts +2 -2
- package/dist/commands/webhook.js +7 -7
- package/dist/index.d.ts +18 -2
- package/dist/index.js +53 -1264
- package/dist/lib/account-catalog.d.ts +18 -0
- package/dist/lib/account-catalog.js +38 -0
- package/dist/lib/account-provider-registry.d.ts +18 -0
- package/dist/lib/account-provider-registry.js +59 -0
- package/dist/lib/account-registry.d.ts +40 -0
- package/dist/lib/account-registry.js +239 -0
- package/dist/lib/account-schema.d.ts +44 -0
- package/dist/lib/account-schema.js +91 -0
- package/dist/lib/account-state-service.d.ts +21 -0
- package/dist/lib/account-state-service.js +60 -0
- package/dist/lib/activity.d.ts +1 -1
- package/dist/lib/activity.js +8 -1
- package/dist/lib/add-dir.d.ts +80 -0
- package/dist/lib/add-dir.js +241 -0
- package/dist/lib/agent-cli-commands.d.ts +11 -0
- package/dist/lib/agent-cli-commands.js +30 -0
- package/dist/lib/agents.js +44 -36
- package/dist/lib/answer-router.js +1 -1
- package/dist/lib/audit/log.d.ts +10 -27
- package/dist/lib/audit/log.js +20 -33
- package/dist/lib/auth-health.d.ts +9 -2
- package/dist/lib/auth-health.js +42 -9
- package/dist/lib/auto-pull-worker.js +7 -3
- package/dist/lib/auto-pull.d.ts +44 -3
- package/dist/lib/auto-pull.js +88 -5
- package/dist/lib/binary-shadow.d.ts +28 -0
- package/dist/lib/binary-shadow.js +121 -0
- package/dist/lib/brand.js +9 -3
- package/dist/lib/browser/ipc.d.ts +30 -0
- package/dist/lib/browser/ipc.js +81 -4
- package/dist/lib/browser/profiles.d.ts +3 -2
- package/dist/lib/browser/profiles.js +65 -22
- package/dist/lib/browser/service.d.ts +4 -0
- package/dist/lib/browser/service.js +30 -3
- package/dist/lib/browser/sessions-list.d.ts +22 -2
- package/dist/lib/browser/sessions-list.js +54 -6
- package/dist/lib/browser/stream.d.ts +2 -0
- package/dist/lib/browser/stream.js +1 -0
- package/dist/lib/browser/types.d.ts +19 -0
- package/dist/lib/byok-usage.d.ts +9 -2
- package/dist/lib/byok-usage.js +133 -31
- package/dist/lib/cli-entry.d.ts +11 -0
- package/dist/lib/cli-entry.js +46 -0
- package/dist/lib/cloud/host.d.ts +1 -1
- package/dist/lib/cloud/host.js +2 -2
- package/dist/lib/cloud/registry.js +1 -1
- package/dist/lib/cloud/types.d.ts +1 -1
- package/dist/lib/commands.d.ts +2 -0
- package/dist/lib/commands.js +2 -0
- package/dist/lib/computer/sessions-list.d.ts +150 -0
- package/dist/lib/computer/sessions-list.js +363 -0
- package/dist/lib/config-keys.d.ts +86 -0
- package/dist/lib/config-keys.js +193 -0
- package/dist/lib/config-machine-keys.d.ts +12 -0
- package/dist/lib/config-machine-keys.js +17 -0
- package/dist/lib/config-transfer.js +3 -4
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/cli.js +3 -3
- package/dist/lib/daemon-health.d.ts +22 -0
- package/dist/lib/daemon-health.js +33 -3
- package/dist/lib/daemon-services.d.ts +50 -0
- package/dist/lib/daemon-services.js +147 -0
- package/dist/lib/daemon-ticks.d.ts +35 -66
- package/dist/lib/daemon-ticks.js +77 -159
- package/dist/lib/daemon.d.ts +72 -4
- package/dist/lib/daemon.js +792 -202
- package/dist/lib/deeplink/register.d.ts +49 -0
- package/dist/lib/deeplink/register.js +253 -0
- package/dist/lib/deeplink/url.d.ts +34 -0
- package/dist/lib/deeplink/url.js +78 -0
- package/dist/lib/device-config.d.ts +124 -29
- package/dist/lib/device-config.js +346 -141
- package/dist/lib/devices/config-migration.d.ts +42 -0
- package/dist/lib/devices/config-migration.js +203 -0
- package/dist/lib/devices/connect.js +14 -7
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +20 -1
- package/dist/lib/devices/harness-inventory.d.ts +11 -2
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/health.d.ts +1 -1
- package/dist/lib/devices/health.js +1 -1
- package/dist/lib/devices/pending.d.ts +22 -1
- package/dist/lib/devices/pending.js +67 -2
- package/dist/lib/devices/registry.d.ts +0 -25
- package/dist/lib/devices/registry.js +2 -83
- package/dist/lib/devices/resolve-profile.d.ts +20 -0
- package/dist/lib/devices/resolve-profile.js +44 -0
- package/dist/lib/devices/rollout-verify.d.ts +101 -0
- package/dist/lib/devices/rollout-verify.js +193 -0
- package/dist/lib/devices/ssh-config.js +10 -6
- package/dist/lib/event-families.d.ts +28 -0
- package/dist/lib/event-families.js +159 -0
- package/dist/lib/event-stream.d.ts +9 -1
- package/dist/lib/event-stream.js +26 -9
- package/dist/lib/events.d.ts +9 -1
- package/dist/lib/events.js +21 -6
- package/dist/lib/exec.d.ts +30 -0
- package/dist/lib/exec.js +219 -121
- package/dist/lib/feed-broadcast.d.ts +25 -0
- package/dist/lib/feed-broadcast.js +33 -0
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +12 -2
- package/dist/lib/fleet/capture.js +15 -0
- package/dist/lib/fleet/manifest.js +9 -0
- package/dist/lib/fleet/types.d.ts +9 -0
- package/dist/lib/fleet-status.js +10 -8
- package/dist/lib/git.d.ts +20 -6
- package/dist/lib/git.js +121 -17
- package/dist/lib/hosts/dispatch.d.ts +8 -3
- package/dist/lib/hosts/dispatch.js +29 -3
- package/dist/lib/hosts/logs.d.ts +2 -2
- package/dist/lib/hosts/logs.js +2 -2
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.d.ts +2 -3
- package/dist/lib/hosts/passthrough.js +10 -24
- package/dist/lib/hosts/progress.js +2 -2
- package/dist/lib/hosts/providers/devices.d.ts +3 -3
- package/dist/lib/hosts/providers/devices.js +18 -12
- package/dist/lib/hosts/ready.d.ts +5 -1
- package/dist/lib/hosts/ready.js +9 -3
- package/dist/lib/hosts/reconcile.d.ts +2 -2
- package/dist/lib/hosts/reconcile.js +2 -2
- package/dist/lib/hosts/registry.js +14 -9
- package/dist/lib/hosts/remote-cmd.js +1 -1
- package/dist/lib/hosts/remote-os.d.ts +9 -6
- package/dist/lib/hosts/remote-os.js +13 -6
- package/dist/lib/hosts/routing-flag.d.ts +23 -0
- package/dist/lib/hosts/routing-flag.js +41 -0
- package/dist/lib/hosts/run-target.js +1 -1
- package/dist/lib/hosts/session-index.js +1 -1
- package/dist/lib/hosts/tasks.d.ts +6 -6
- package/dist/lib/hosts/tasks.js +4 -4
- package/dist/lib/hosts/types.js +1 -2
- package/dist/lib/installations/resolve.js +2 -8
- package/dist/lib/isolation-boundary-report.js +0 -1
- package/dist/lib/linear-cache.d.ts +7 -0
- package/dist/lib/linear-cache.js +31 -4
- package/dist/lib/linear-project-counts.d.ts +1 -1
- package/dist/lib/linear-project-counts.js +2 -3
- package/dist/lib/linear-projects.js +1 -1
- package/dist/lib/loop.d.ts +2 -2
- package/dist/lib/loop.js +1 -1
- package/dist/lib/mailbox-target.d.ts +1 -1
- package/dist/lib/mailbox-target.js +1 -1
- package/dist/lib/manifest.js +7 -4
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/snapshot.d.ts +3 -2
- package/dist/lib/menubar/snapshot.js +6 -2
- package/dist/lib/migrate-fold.d.ts +33 -0
- package/dist/lib/migrate-fold.js +130 -0
- package/dist/lib/migrate.d.ts +34 -34
- package/dist/lib/migrate.js +106 -128
- package/dist/lib/model-tier-overrides.d.ts +1 -1
- package/dist/lib/model-tier-overrides.js +15 -2
- package/dist/lib/monitors/config.d.ts +17 -3
- package/dist/lib/monitors/config.js +52 -18
- package/dist/lib/monitors/engine.d.ts +32 -1
- package/dist/lib/monitors/engine.js +94 -11
- package/dist/lib/monitors/fingerprint.d.ts +46 -0
- package/dist/lib/monitors/fingerprint.js +100 -0
- package/dist/lib/monitors/remote.d.ts +47 -0
- package/dist/lib/monitors/remote.js +81 -0
- package/dist/lib/monitors/state.d.ts +39 -0
- package/dist/lib/monitors/state.js +56 -0
- package/dist/lib/onepassword.d.ts +17 -0
- package/dist/lib/onepassword.js +52 -22
- package/dist/lib/open-url.js +1 -1
- package/dist/lib/picker.d.ts +14 -2
- package/dist/lib/picker.js +35 -13
- package/dist/lib/placement.d.ts +1 -1
- package/dist/lib/placement.js +1 -1
- package/dist/lib/pr-land-detach.d.ts +32 -0
- package/dist/lib/pr-land-detach.js +79 -0
- package/dist/lib/profiles-presets.js +13 -0
- package/dist/lib/profiles.d.ts +2 -0
- package/dist/lib/profiles.js +44 -1
- package/dist/lib/project-focus.d.ts +1 -1
- package/dist/lib/project-focus.js +1 -1
- package/dist/lib/project-probe.d.ts +4 -1
- package/dist/lib/project-probe.js +5 -4
- package/dist/lib/project-resources.js +2 -18
- package/dist/lib/project-root.d.ts +16 -0
- package/dist/lib/project-root.js +20 -2
- package/dist/lib/projects.d.ts +20 -1
- package/dist/lib/projects.js +78 -1
- package/dist/lib/refresh-coordinator.d.ts +24 -0
- package/dist/lib/refresh-coordinator.js +52 -0
- package/dist/lib/registry.d.ts +2 -2
- package/dist/lib/registry.js +1 -1
- package/dist/lib/resource-aliases.d.ts +13 -0
- package/dist/lib/resource-aliases.js +26 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +16 -0
- package/dist/lib/resources.js +66 -10
- package/dist/lib/rotate.d.ts +3 -4
- package/dist/lib/rotate.js +4 -5
- package/dist/lib/routine-activation.d.ts +22 -2
- package/dist/lib/routine-activation.js +63 -16
- package/dist/lib/routine-context.d.ts +9 -2
- package/dist/lib/routine-context.js +21 -6
- package/dist/lib/routines-project.d.ts +58 -48
- package/dist/lib/routines-project.js +190 -159
- package/dist/lib/routines.d.ts +41 -6
- package/dist/lib/routines.js +92 -34
- package/dist/lib/run-defaults.d.ts +5 -0
- package/dist/lib/run-defaults.js +11 -1
- package/dist/lib/runner.d.ts +10 -8
- package/dist/lib/runner.js +122 -39
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +10 -3
- package/dist/lib/secrets/agent.js +43 -8
- package/dist/lib/secrets/bundles.d.ts +1 -0
- package/dist/lib/secrets/bundles.js +92 -80
- package/dist/lib/secrets/index.js +32 -9
- package/dist/lib/secrets/install-helper.d.ts +4 -4
- package/dist/lib/secrets/install-helper.js +4 -4
- package/dist/lib/secrets/push.d.ts +40 -6
- package/dist/lib/secrets/push.js +58 -11
- package/dist/lib/secrets/read-backoff.d.ts +1 -1
- package/dist/lib/secrets/read-backoff.js +1 -1
- package/dist/lib/secrets/remote.d.ts +24 -0
- package/dist/lib/secrets/remote.js +54 -4
- package/dist/lib/self-update.d.ts +108 -0
- package/dist/lib/self-update.js +276 -1
- package/dist/lib/session/active.d.ts +89 -5
- package/dist/lib/session/active.js +165 -10
- package/dist/lib/session/actor-sidecar.d.ts +1 -1
- package/dist/lib/session/actor-sidecar.js +2 -1
- package/dist/lib/session/db.d.ts +176 -2
- package/dist/lib/session/db.js +484 -17
- package/dist/lib/session/discover.js +27 -4
- package/dist/lib/session/hook-sessions.js +2 -2
- package/dist/lib/session/host-link.d.ts +2 -2
- package/dist/lib/session/host-link.js +2 -2
- package/dist/lib/session/pid-registry.d.ts +21 -0
- package/dist/lib/session/pid-registry.js +57 -0
- package/dist/lib/session/remote-active.js +15 -1
- package/dist/lib/session/remote.js +2 -2
- package/dist/lib/session/session-cache.d.ts +14 -7
- package/dist/lib/session/session-cache.js +51 -9
- package/dist/lib/session/state.d.ts +2 -2
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/sync/config.d.ts +10 -10
- package/dist/lib/session/sync/config.js +10 -10
- package/dist/lib/session/sync/r2.d.ts +34 -0
- package/dist/lib/session/sync/r2.js +123 -0
- package/dist/lib/session/throughput.d.ts +1 -1
- package/dist/lib/session/throughput.js +1 -1
- package/dist/lib/session/types.d.ts +23 -1
- package/dist/lib/session/types.js +15 -0
- package/dist/lib/session/viewing-in.d.ts +1 -1
- package/dist/lib/session/viewing-in.js +1 -1
- package/dist/lib/session/watch.d.ts +101 -0
- package/dist/lib/session/watch.js +242 -0
- package/dist/lib/share/config.d.ts +4 -0
- package/dist/lib/share/config.js +1 -0
- package/dist/lib/share/provision.d.ts +38 -0
- package/dist/lib/share/provision.js +50 -0
- package/dist/lib/share/publish.d.ts +40 -1
- package/dist/lib/share/publish.js +116 -3
- package/dist/lib/share/worker-template.js +71 -6
- package/dist/lib/shims.d.ts +17 -3
- package/dist/lib/shims.js +98 -26
- package/dist/lib/signin-badge.js +3 -2
- package/dist/lib/skills.js +2 -0
- package/dist/lib/smart-launch.d.ts +8 -8
- package/dist/lib/smart-launch.js +80 -59
- package/dist/lib/ssh-exec.d.ts +1 -1
- package/dist/lib/ssh-exec.js +1 -1
- package/dist/lib/staleness/prune.d.ts +88 -0
- package/dist/lib/staleness/prune.js +69 -0
- package/dist/lib/staleness/registry.d.ts +1 -1
- package/dist/lib/staleness/writers/commands.js +34 -2
- package/dist/lib/staleness/writers/hooks.js +16 -0
- package/dist/lib/staleness/writers/kinds.d.ts +6 -0
- package/dist/lib/staleness/writers/skills.js +19 -0
- package/dist/lib/staleness/writers/subagents.d.ts +2 -3
- package/dist/lib/staleness/writers/subagents.js +0 -4
- package/dist/lib/staleness/writers/types.d.ts +32 -0
- package/dist/lib/startup/command-registry.d.ts +6 -12
- package/dist/lib/startup/command-registry.js +11 -31
- package/dist/lib/startup/dev-build.d.ts +10 -0
- package/dist/lib/startup/dev-build.js +13 -1
- package/dist/lib/startup/spellcheck.d.ts +18 -0
- package/dist/lib/startup/spellcheck.js +45 -0
- package/dist/lib/state.d.ts +36 -9
- package/dist/lib/state.js +96 -46
- package/dist/lib/subagents-registry.d.ts +0 -7
- package/dist/lib/subagents-registry.js +38 -65
- package/dist/lib/subagents.d.ts +0 -33
- package/dist/lib/subagents.js +0 -75
- package/dist/lib/sync-umbrella.js +1 -1
- package/dist/lib/teams/agents.d.ts +41 -1
- package/dist/lib/teams/agents.js +187 -9
- package/dist/lib/teams/api.d.ts +11 -1
- package/dist/lib/teams/api.js +11 -2
- package/dist/lib/teams/delivery.d.ts +41 -0
- package/dist/lib/teams/delivery.js +60 -0
- package/dist/lib/teams/registry.d.ts +14 -0
- package/dist/lib/teams/registry.js +51 -1
- package/dist/lib/teams/remoteWorktree.d.ts +19 -0
- package/dist/lib/teams/remoteWorktree.js +29 -0
- package/dist/lib/teams/scheduler.d.ts +1 -1
- package/dist/lib/teams/scheduler.js +1 -1
- package/dist/lib/teams/worktree.d.ts +24 -0
- package/dist/lib/teams/worktree.js +42 -0
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +10 -1
- package/dist/lib/terminal/backends/vscodium-agent.js +16 -5
- package/dist/lib/terminal/engine.d.ts +4 -0
- package/dist/lib/terminal/engine.js +8 -2
- package/dist/lib/terminal/types.d.ts +21 -2
- package/dist/lib/testdata/refresh-coordinator-worker.d.ts +1 -0
- package/dist/lib/testdata/refresh-coordinator-worker.js +23 -0
- package/dist/lib/tickets/list.d.ts +53 -0
- package/dist/lib/tickets/list.js +153 -0
- package/dist/lib/tmux/orphan-reap.d.ts +320 -0
- package/dist/lib/tmux/orphan-reap.js +644 -0
- package/dist/lib/tmux/session.d.ts +68 -3
- package/dist/lib/tmux/session.js +95 -5
- package/dist/lib/triggers/handlers.d.ts +20 -0
- package/dist/lib/triggers/handlers.js +6 -1
- package/dist/lib/types.d.ts +46 -34
- package/dist/lib/usage-fleet.d.ts +32 -0
- package/dist/lib/usage-fleet.js +125 -0
- package/dist/lib/usage-refresh.d.ts +3 -3
- package/dist/lib/usage-refresh.js +19 -16
- package/dist/lib/usage.d.ts +51 -47
- package/dist/lib/usage.js +145 -153
- package/dist/lib/version-duplicates.js +2 -2
- package/dist/lib/versions.d.ts +56 -13
- package/dist/lib/versions.js +195 -45
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +3 -3
- package/dist/lib/watchdog/rotate.d.ts +6 -6
- package/dist/lib/watchdog/rotate.js +6 -6
- package/dist/lib/workflows.d.ts +1 -1
- package/dist/lib/yaml-io.d.ts +47 -0
- package/dist/lib/yaml-io.js +55 -0
- package/package.json +2 -1
- package/scripts/install-helper.js +2 -2
- package/scripts/postinstall.js +23 -20
- package/dist/commands/defaults.d.ts +0 -7
- package/dist/commands/defaults.js +0 -107
- package/dist/commands/export.d.ts +0 -11
- package/dist/commands/export.js +0 -215
- package/dist/commands/helper.d.ts +0 -12
- package/dist/commands/helper.js +0 -87
- package/dist/commands/hosts.d.ts +0 -11
- package/dist/commands/hosts.js +0 -330
- package/dist/commands/lock.d.ts +0 -12
- package/dist/commands/lock.js +0 -70
- package/dist/commands/pull.d.ts +0 -17
- package/dist/commands/pull.js +0 -39
- package/dist/commands/push.d.ts +0 -14
- package/dist/commands/push.js +0 -30
- package/dist/commands/wallet.d.ts +0 -20
- package/dist/commands/wallet.js +0 -216
- package/dist/commands/worktree.d.ts +0 -19
- package/dist/commands/worktree.js +0 -272
- package/dist/lib/account-labels.d.ts +0 -24
- package/dist/lib/account-labels.js +0 -72
- package/dist/lib/auto-dispatch-linear.d.ts +0 -18
- package/dist/lib/auto-dispatch-linear.js +0 -107
- package/dist/lib/auto-dispatch-provider.d.ts +0 -10
- package/dist/lib/auto-dispatch-provider.js +0 -30
- package/dist/lib/auto-dispatch.d.ts +0 -93
- package/dist/lib/auto-dispatch.js +0 -128
- package/dist/lib/export.d.ts +0 -72
- package/dist/lib/export.js +0 -269
- package/dist/lib/lock.d.ts +0 -93
- package/dist/lib/lock.js +0 -207
- package/dist/lib/wallet/index.d.ts +0 -78
- package/dist/lib/wallet/index.js +0 -253
package/dist/commands/ssh.js
CHANGED
|
@@ -21,7 +21,8 @@ import { machineId } from '../lib/session/sync/config.js';
|
|
|
21
21
|
import { isDeviceAuto, resolveDeviceAffinity } from '../lib/smart-launch.js';
|
|
22
22
|
import { registerFleetCaptureCommand } from './fleet-capture.js';
|
|
23
23
|
import { registerFleetApplyAlias } from './apply.js';
|
|
24
|
-
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored,
|
|
24
|
+
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
|
|
25
|
+
import { resolveDeviceProfile } from '../lib/devices/resolve-profile.js';
|
|
25
26
|
import { collectReachabilityWriteBacks, deviceOnlineState } from '../lib/devices/reachability.js';
|
|
26
27
|
import { addControlToken } from '../lib/serve/token.js';
|
|
27
28
|
import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
|
|
@@ -36,6 +37,7 @@ import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, deviceIdentity
|
|
|
36
37
|
import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
|
|
37
38
|
import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
|
|
38
39
|
import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
40
|
+
import { isRolloutSuccess, verifyFleetRollout, } from '../lib/devices/rollout-verify.js';
|
|
39
41
|
import { fleetCapacity, fmtBytes, headroom, } from '../lib/devices/health.js';
|
|
40
42
|
import { buildFleetHealthReport, renderFleetMatrix, renderFleetSummary, renderFleetWarnings, } from '../lib/devices/health-report.js';
|
|
41
43
|
import { loadFleetStats, readStatsCache } from '../lib/devices/stats-cache.js';
|
|
@@ -43,19 +45,24 @@ import { collectLocalFleetInventory } from '../lib/devices/fleet-inventory.js';
|
|
|
43
45
|
import { checkSyncStatus, countOrphans } from '../lib/drift.js';
|
|
44
46
|
import { checkAllClis } from '../lib/teams/agents.js';
|
|
45
47
|
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
48
|
+
import { listTasks, resolveTaskRef } from '../lib/hosts/tasks.js';
|
|
49
|
+
import { reconcileRunningTasks } from '../lib/hosts/reconcile.js';
|
|
50
|
+
import { stopDispatchedTask } from '../lib/hosts/dispatch.js';
|
|
51
|
+
import { terminalWidth, truncateToWidth } from '../lib/session/width.js';
|
|
46
52
|
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
47
53
|
import { ALL_AGENT_IDS } from '../lib/agents.js';
|
|
48
54
|
import { collectLocalHarnessInventory, groupByAccount, renderAccountsMatrix, renderHarnessMatrix, } from '../lib/devices/harness-inventory.js';
|
|
49
55
|
import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js';
|
|
50
|
-
import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort } from './lease.js';
|
|
51
|
-
import { authCellColor, formatCheckedAge, isDeadVerdict,
|
|
56
|
+
import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort, registerLeaseCommand } from './lease.js';
|
|
57
|
+
import { authCellColor, formatCheckedAge, isDeadVerdict, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
|
|
52
58
|
import { runFleetLogin } from '../lib/fleet/remote-login.js';
|
|
53
|
-
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from '../lib/device-config.js';
|
|
54
|
-
import { setHelpSections } from '../lib/help.js';
|
|
59
|
+
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue, configKeySpec } from '../lib/device-config.js';
|
|
60
|
+
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
55
61
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
56
62
|
* command is running on so it stands out from the rest of the tailnet.
|
|
57
|
-
* `isInteractive` marks the configured interactive host (`devices
|
|
63
|
+
* `isInteractive` marks the configured interactive host (`devices config <name> interactive.host`). */
|
|
58
64
|
function deviceSummary(d, isSelf = false, stats, isInteractive = false) {
|
|
65
|
+
d = resolveDeviceProfile(d);
|
|
59
66
|
const addr = hostNameFor(d) ?? chalk.gray('no address');
|
|
60
67
|
// Prefer a fresh live verdict (this run's probe, else the written-back
|
|
61
68
|
// reachability) over the stale tailscale.online snapshot (RUSH-1965).
|
|
@@ -113,7 +120,9 @@ function renderDeviceTable(reg, names, self, statsMap, full = false, interactive
|
|
|
113
120
|
chalk.gray('headroom');
|
|
114
121
|
lines.push(head);
|
|
115
122
|
for (const name of names) {
|
|
116
|
-
|
|
123
|
+
// Effective profile: the operator's central config (ssh.*/platform/user)
|
|
124
|
+
// overlays the discovery record, so the table shows what would be dialed.
|
|
125
|
+
const d = resolveDeviceProfile(reg[name]);
|
|
117
126
|
const isSelf = name === self;
|
|
118
127
|
const marker = isSelf ? chalk.cyan('▸ ') : ' ';
|
|
119
128
|
const label = isSelf ? chalk.bold.cyan(name.padEnd(16)) : chalk.bold(name.padEnd(16));
|
|
@@ -180,7 +189,7 @@ export function renderLeasedBoxesSection(boxes, nowSecs) {
|
|
|
180
189
|
chalk.gray(fmtIdleShort(b, nowSecs).padEnd(12)) +
|
|
181
190
|
chalk.gray(fmtExpiresShort(b, nowSecs)));
|
|
182
191
|
}
|
|
183
|
-
lines.push(chalk.gray(' Reuse a box with `agents run --box <slug>` · stop with `agents lease stop <slug>`'));
|
|
192
|
+
lines.push(chalk.gray(' Reuse a box with `agents run --box <slug>` · stop with `agents devices lease stop <slug>`'));
|
|
184
193
|
return lines;
|
|
185
194
|
}
|
|
186
195
|
/** The leased-box rows for the devices list, or [] when crabbox can't be read. */
|
|
@@ -309,26 +318,51 @@ async function runInteractiveDeviceSync() {
|
|
|
309
318
|
].filter(Boolean);
|
|
310
319
|
console.log(parts.join(chalk.gray(' · ')));
|
|
311
320
|
}
|
|
312
|
-
/**
|
|
313
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Print a per-device result table for fleet update/run.
|
|
323
|
+
*
|
|
324
|
+
* `verifications` is supplied only by the rollout (`agents fleet update`), which
|
|
325
|
+
* re-probes what `agents` resolves to on each upgraded box. A box that upgraded
|
|
326
|
+
* with `exit 0` but still resolves to another copy — the dev-install shadow of
|
|
327
|
+
* RUSH-2446 — is rendered `stale` / `unverified`, counted as **not upgraded**,
|
|
328
|
+
* and makes the command exit non-zero. An `exit 0` alone never reads as `ok` on
|
|
329
|
+
* a rollout again.
|
|
330
|
+
*/
|
|
331
|
+
function printFleetResults(results, verifications) {
|
|
314
332
|
const nameW = Math.max(8, ...results.map((r) => r.name.length));
|
|
315
333
|
console.log(chalk.bold('DEVICE'.padEnd(nameW)) + ' ' +
|
|
316
|
-
chalk.bold('STATUS'.padEnd(
|
|
334
|
+
chalk.bold('STATUS'.padEnd(10)) + ' ' +
|
|
317
335
|
chalk.bold('DETAIL'));
|
|
336
|
+
let notUpgraded = 0;
|
|
318
337
|
for (const r of results) {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
338
|
+
const verified = r.status === 'ok' ? verifications?.get(r.name) : undefined;
|
|
339
|
+
if (verified && !isRolloutSuccess(verified.verdict))
|
|
340
|
+
notUpgraded++;
|
|
341
|
+
const label = r.status === 'skipped' ? chalk.gray('skipped'.padEnd(10)) :
|
|
342
|
+
r.status === 'failed' ? chalk.red('failed'.padEnd(10)) :
|
|
343
|
+
verified === undefined ? chalk.green('ok'.padEnd(10)) :
|
|
344
|
+
verified.verdict === 'on-target' ? chalk.green('ok'.padEnd(10)) :
|
|
345
|
+
verified.verdict === 'unverified' ? chalk.yellow('unverified'.padEnd(10)) :
|
|
346
|
+
chalk.red('stale'.padEnd(10));
|
|
322
347
|
const detail = r.status === 'skipped' ? chalk.gray(skipLabel(r.reason)) :
|
|
323
348
|
r.status === 'failed' ? chalk.red(r.detail || `exit ${r.code ?? '?'}`) :
|
|
324
|
-
chalk.gray(r.code === 0 ? 'exit 0' : '')
|
|
325
|
-
|
|
349
|
+
verified === undefined ? chalk.gray(r.code === 0 ? 'exit 0' : '') :
|
|
350
|
+
verified.verdict === 'on-target' ? chalk.gray(verified.detail) :
|
|
351
|
+
verified.verdict === 'unverified' ? chalk.yellow(verified.detail) :
|
|
352
|
+
chalk.red(verified.detail);
|
|
353
|
+
console.log(`${r.name.padEnd(nameW)} ${label} ${detail}`);
|
|
326
354
|
}
|
|
327
|
-
const ok = results.filter((r) => r.status === 'ok').length;
|
|
328
355
|
const failed = results.filter((r) => r.status === 'failed').length;
|
|
329
356
|
const skipped = results.filter((r) => r.status === 'skipped').length;
|
|
330
|
-
|
|
331
|
-
|
|
357
|
+
const ok = results.filter((r) => r.status === 'ok').length - notUpgraded;
|
|
358
|
+
const parts = [`${ok} ok`, `${failed} failed`, `${skipped} skipped`];
|
|
359
|
+
if (verifications)
|
|
360
|
+
parts.splice(1, 0, `${notUpgraded} not upgraded`);
|
|
361
|
+
console.log(chalk.gray(parts.join(' · ')));
|
|
362
|
+
if (notUpgraded > 0) {
|
|
363
|
+
console.log(chalk.yellow('A box that upgraded but still resolves elsewhere runs OLD code — remove the stale install that owns the `agents` name on that box, or reorder PATH. `agents doctor` names it.'));
|
|
364
|
+
}
|
|
365
|
+
if (failed > 0 || notUpgraded > 0)
|
|
332
366
|
process.exitCode = 1;
|
|
333
367
|
}
|
|
334
368
|
async function localHealthRow(self, stats) {
|
|
@@ -386,6 +420,16 @@ async function probeRemoteHealth(target) {
|
|
|
386
420
|
inventory: parsed.fleet,
|
|
387
421
|
};
|
|
388
422
|
}
|
|
423
|
+
/** Effective device-scoped config, keyed by the canonical YAML key. */
|
|
424
|
+
function deviceConfigJson(name) {
|
|
425
|
+
const config = {};
|
|
426
|
+
for (const entry of listConfig({ device: name })) {
|
|
427
|
+
if (entry.spec.scope !== 'device' || entry.value === undefined)
|
|
428
|
+
continue;
|
|
429
|
+
config[entry.spec.yamlKey] = entry.value;
|
|
430
|
+
}
|
|
431
|
+
return Object.keys(config).length > 0 ? config : undefined;
|
|
432
|
+
}
|
|
389
433
|
async function runFleetStatus(opts) {
|
|
390
434
|
const reg = await loadDevices();
|
|
391
435
|
const self = machineId();
|
|
@@ -418,7 +462,7 @@ async function runFleetStatus(opts) {
|
|
|
418
462
|
const remoteTargets = remoteFleetTargets(planned, self)
|
|
419
463
|
.map((t) => ({
|
|
420
464
|
name: t.device.name,
|
|
421
|
-
platform: t.device.platform,
|
|
465
|
+
platform: resolveDeviceProfile(t.device).platform,
|
|
422
466
|
// Fail fast: gate the expensive version+doctor dials on the reachability
|
|
423
467
|
// verdict the cheap stats probe already computed one step earlier. A box
|
|
424
468
|
// it found unreachable skips straight to an `unreachable` row instead of
|
|
@@ -513,7 +557,20 @@ async function runFleetStatus(opts) {
|
|
|
513
557
|
}
|
|
514
558
|
const report = buildFleetHealthReport(rows, new Date(), { self });
|
|
515
559
|
if (opts.json) {
|
|
516
|
-
|
|
560
|
+
const interactiveHost = getConfigValue('interactive.host').value;
|
|
561
|
+
console.log(JSON.stringify({
|
|
562
|
+
...report,
|
|
563
|
+
devices: report.devices.map((row) => {
|
|
564
|
+
const registered = reg[row.name];
|
|
565
|
+
const config = deviceConfigJson(row.name);
|
|
566
|
+
return {
|
|
567
|
+
...row,
|
|
568
|
+
profile: registered ? resolveDeviceProfile(registered) : { name: row.name },
|
|
569
|
+
interactive: row.name === interactiveHost,
|
|
570
|
+
...(config ? { config } : {}),
|
|
571
|
+
};
|
|
572
|
+
}),
|
|
573
|
+
}, null, 2));
|
|
517
574
|
}
|
|
518
575
|
else if (opts.verbose) {
|
|
519
576
|
// Full grid: the auth/CLI/sync/version columns and the warnings rollup.
|
|
@@ -575,11 +632,10 @@ export async function raceFleetPingDeadline(fanOut, remoteTargets, overallTimeou
|
|
|
575
632
|
}
|
|
576
633
|
async function runFleetPing(opts) {
|
|
577
634
|
const self = machineId();
|
|
578
|
-
const
|
|
635
|
+
const { refreshLocalFleetAuthState } = await import('../lib/daemon-ticks.js');
|
|
579
636
|
// --local: probe just this host. Used both directly and as the fan-out worker.
|
|
580
637
|
if (opts.local) {
|
|
581
|
-
const rows = await
|
|
582
|
-
writeFleetAuthRows(self, rows);
|
|
638
|
+
const { authRows: rows } = await refreshLocalFleetAuthState();
|
|
583
639
|
if (opts.json) {
|
|
584
640
|
console.log(JSON.stringify({ host: self, rows }));
|
|
585
641
|
}
|
|
@@ -596,12 +652,11 @@ async function runFleetPing(opts) {
|
|
|
596
652
|
const reg = await loadDevices();
|
|
597
653
|
const planned = planFleetTargets(reg);
|
|
598
654
|
const results = [];
|
|
599
|
-
const localRows = await
|
|
600
|
-
writeFleetAuthRows(self, localRows);
|
|
655
|
+
const { authRows: localRows } = await refreshLocalFleetAuthState();
|
|
601
656
|
results.push({ host: self, rows: localRows });
|
|
602
657
|
const remoteTargets = remoteFleetTargets(planned, self).map((t) => ({
|
|
603
658
|
name: t.device.name,
|
|
604
|
-
platform: t.device.platform,
|
|
659
|
+
platform: resolveDeviceProfile(t.device).platform,
|
|
605
660
|
skip: t.skip,
|
|
606
661
|
dialTarget: fleetDialTarget(t.device),
|
|
607
662
|
extraSshArgs: deviceIdentityArgs(t.device),
|
|
@@ -671,7 +726,7 @@ async function probeRemoteHarnesses(target, refresh) {
|
|
|
671
726
|
* the local and remote rows. Bounded by the same per-device + overall deadlines
|
|
672
727
|
* as `fleet ping`, so one unreachable box can never stall the glance.
|
|
673
728
|
*/
|
|
674
|
-
async function collectFleetHarnesses(opts) {
|
|
729
|
+
export async function collectFleetHarnesses(opts) {
|
|
675
730
|
const self = machineId();
|
|
676
731
|
const want = opts.devices?.length ? new Set(opts.devices) : null;
|
|
677
732
|
const results = [];
|
|
@@ -683,7 +738,7 @@ async function collectFleetHarnesses(opts) {
|
|
|
683
738
|
const planned = planFleetTargets(reg);
|
|
684
739
|
let remoteTargets = remoteFleetTargets(planned, self).map((t) => ({
|
|
685
740
|
name: t.device.name,
|
|
686
|
-
platform: t.device.platform,
|
|
741
|
+
platform: resolveDeviceProfile(t.device).platform,
|
|
687
742
|
skip: t.skip,
|
|
688
743
|
dialTarget: fleetDialTarget(t.device),
|
|
689
744
|
extraSshArgs: deviceIdentityArgs(t.device),
|
|
@@ -736,7 +791,7 @@ async function runDevicesHarnesses(opts) {
|
|
|
736
791
|
for (const line of renderHarnessMatrix(results))
|
|
737
792
|
console.log(line);
|
|
738
793
|
}
|
|
739
|
-
async function runDevicesAccounts(opts) {
|
|
794
|
+
export async function runDevicesAccounts(opts) {
|
|
740
795
|
if (opts.local) {
|
|
741
796
|
const rows = await collectLocalHarnessInventory({ agents: opts.agents, refresh: opts.refresh });
|
|
742
797
|
if (opts.json)
|
|
@@ -880,26 +935,45 @@ function registerDevicesCommands(program) {
|
|
|
880
935
|
const devicesCmd = program
|
|
881
936
|
.command('devices')
|
|
882
937
|
.alias('fleet')
|
|
883
|
-
.description('Registry of SSH device profiles (platform, user, address, auth), self-populated from Tailscale. Alias: fleet.')
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
938
|
+
.description('Registry of SSH device profiles (platform, user, address, auth), self-populated from Tailscale. Alias: fleet.');
|
|
939
|
+
setHelpSections(devicesCmd, {
|
|
940
|
+
examples: `
|
|
941
|
+
Discover & register:
|
|
942
|
+
agents devices sync # pick which tailscale nodes to keep (TTY)
|
|
943
|
+
agents devices sync --yes # register all non-ignored nodes
|
|
944
|
+
agents devices ignore ipad165 # dismiss a node so it's never re-suggested
|
|
945
|
+
|
|
946
|
+
Inspect:
|
|
947
|
+
agents devices list # what's registered (★ = interactive host)
|
|
948
|
+
agents devices status # live reachability + load
|
|
949
|
+
agents devices ping # quick liveness probe
|
|
950
|
+
agents devices lease list # disposable crabbox devices available for reuse
|
|
951
|
+
|
|
952
|
+
Configure a device:
|
|
953
|
+
agents devices config mac-mini # settings menu (TTY) / print (piped)
|
|
954
|
+
agents devices config mac-mini agents.max-concurrent 4
|
|
955
|
+
agents devices config mac-mini scheduler.enabled off
|
|
956
|
+
agents devices config mac-mini notes "runs the releases"
|
|
957
|
+
agents devices config win-mini ssh.auth password
|
|
958
|
+
agents devices config worker ssh.identity-file ~/.ssh/worker_ed25519
|
|
959
|
+
agents devices config mac-mini auto-launch.enabled off
|
|
960
|
+
agents devices config mac-mini interactive.host zion # where agents show YOU artifacts
|
|
961
|
+
agents devices render --write # write ~/.ssh/config.d/agents include
|
|
900
962
|
|
|
901
|
-
|
|
902
|
-
|
|
963
|
+
Fleet operations:
|
|
964
|
+
agents fleet update # roll out latest agents-cli everywhere
|
|
965
|
+
agents fleet run uname -a # run a command on every online device
|
|
966
|
+
`,
|
|
967
|
+
notes: '`agents fleet` is an alias for `agents devices` — same subcommands.',
|
|
968
|
+
});
|
|
969
|
+
registerLeaseCommand(devicesCmd);
|
|
970
|
+
registerCommandGroups(devicesCmd, [
|
|
971
|
+
{ title: 'Discover & register', names: ['sync', 'register', 'add', 'ignore', 'unignore', 'rm'] },
|
|
972
|
+
{ title: 'Inspect', names: ['list', 'show', 'status', 'ping', 'harnesses', 'accounts'] },
|
|
973
|
+
{ title: 'Disposable devices', names: ['lease'] },
|
|
974
|
+
{ title: 'Configure a device', names: ['config', 'render'] },
|
|
975
|
+
{ title: 'Fleet operations', names: ['update', 'run', 'login', 'pair-ios', 'capture', 'apply'] },
|
|
976
|
+
]);
|
|
903
977
|
devicesCmd
|
|
904
978
|
.command('sync')
|
|
905
979
|
.description('Ingest `tailscale status --json` into device profiles. In a terminal, opens a checkbox to register/unregister nodes; with --yes, registers every non-ignored node.')
|
|
@@ -972,14 +1046,247 @@ Typical workflow:
|
|
|
972
1046
|
}
|
|
973
1047
|
console.log(chalk.green(`No longer ignoring '${name}'`) + chalk.gray(' — run `agents devices sync` to register it.'));
|
|
974
1048
|
});
|
|
1049
|
+
// ─── devices config (unified settings surface) ────────────────────────────
|
|
1050
|
+
//
|
|
1051
|
+
// ONE command for every per-device setting: `agents devices config <name>
|
|
1052
|
+
// [key] [value] [--unset] [--json]`. The retired subcommands (configure,
|
|
1053
|
+
// note, set-interactive, set, enable/disable/prefer/unprefer) are hidden
|
|
1054
|
+
// tombstones below — each prints a deprecation notice on STDERR (so a --json
|
|
1055
|
+
// consumer's stdout stays parseable) and delegates to this same engine,
|
|
1056
|
+
// preserving its old output shape and exit codes.
|
|
1057
|
+
/** Parse a raw CLI string into a config key's typed value (bool/int pass validation, strings verbatim). */
|
|
1058
|
+
const parseConfigValueInput = (spec, raw) => {
|
|
1059
|
+
switch (spec.type) {
|
|
1060
|
+
case 'int': {
|
|
1061
|
+
const n = Number(raw);
|
|
1062
|
+
if (!Number.isInteger(n))
|
|
1063
|
+
throw new Error(`Config key '${spec.name}' expects an integer, got '${raw}'.`);
|
|
1064
|
+
return n;
|
|
1065
|
+
}
|
|
1066
|
+
case 'bool': {
|
|
1067
|
+
if (raw === 'on' || raw === 'true')
|
|
1068
|
+
return true;
|
|
1069
|
+
if (raw === 'off' || raw === 'false')
|
|
1070
|
+
return false;
|
|
1071
|
+
throw new Error(`Config key '${spec.name}' expects on/off (or true/false), got '${raw}'.`);
|
|
1072
|
+
}
|
|
1073
|
+
default:
|
|
1074
|
+
return raw;
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
const writeJson = (payload) => {
|
|
1078
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
1079
|
+
};
|
|
1080
|
+
/** The device-scope config entries for `name` (what the config surface edits). */
|
|
1081
|
+
const deviceConfigEntries = (name) => listConfig({ device: name }).filter((e) => e.spec.scope === 'device');
|
|
1082
|
+
/** Print the full resolved config for a device (bare invocation, non-menu). */
|
|
1083
|
+
const printDevicesConfig = (name, json) => {
|
|
1084
|
+
const entries = deviceConfigEntries(name);
|
|
1085
|
+
if (json) {
|
|
1086
|
+
const config = {};
|
|
1087
|
+
for (const e of entries)
|
|
1088
|
+
if (e.value !== undefined)
|
|
1089
|
+
config[e.spec.name] = e.value;
|
|
1090
|
+
writeJson({ device: name, config });
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
console.log(chalk.bold(`Config for '${name}'`));
|
|
1094
|
+
for (const e of entries) {
|
|
1095
|
+
const value = e.value === undefined ? chalk.gray('— (default)') : chalk.cyan(JSON.stringify(e.value));
|
|
1096
|
+
console.log(` ${e.spec.name.padEnd(24)} ${value}${chalk.gray(` ${e.spec.description}`)}`);
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
/**
|
|
1100
|
+
* The `devices config` engine — shared by the config command's action and
|
|
1101
|
+
* every tombstone. `quiet` performs the write with no output (a tombstone
|
|
1102
|
+
* that prints its own legacy shape). Throws on a bad key/value; callers map
|
|
1103
|
+
* that to exit 1.
|
|
1104
|
+
*/
|
|
1105
|
+
const runDevicesConfig = async (name, key, valueParts, opts) => {
|
|
1106
|
+
const spec = key ? configKeySpec(key) : undefined; // unknown key → throw listing the valid keys
|
|
1107
|
+
// User-scope keys (interactive.host) are stored centrally — the device name
|
|
1108
|
+
// is syntax only, so an unregistered name is not an error for them.
|
|
1109
|
+
if (!spec || spec.scope === 'device')
|
|
1110
|
+
await mustGetDevice(name);
|
|
1111
|
+
if (opts.unset) {
|
|
1112
|
+
if (!spec)
|
|
1113
|
+
throw new Error('--unset needs a key: agents devices config <name> <key> --unset');
|
|
1114
|
+
unsetConfigValue(spec.name, { device: name });
|
|
1115
|
+
if (opts.quiet)
|
|
1116
|
+
return;
|
|
1117
|
+
if (opts.json)
|
|
1118
|
+
writeJson({ device: name, key: spec.name, value: null });
|
|
1119
|
+
else
|
|
1120
|
+
console.log(chalk.green(`Unset ${spec.name}`) + chalk.gray(` on '${name}' — back to the default.`));
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (spec && valueParts.length > 0) {
|
|
1124
|
+
let value;
|
|
1125
|
+
if (spec.type === 'string-list') {
|
|
1126
|
+
// List keys (notes) APPEND — one entry per invocation.
|
|
1127
|
+
const existing = getConfigValue(spec.name, { device: name }).value ?? [];
|
|
1128
|
+
value = [...existing, valueParts.join(' ')];
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
value = parseConfigValueInput(spec, valueParts.join(' '));
|
|
1132
|
+
}
|
|
1133
|
+
setConfigValue(spec.name, value, { device: name });
|
|
1134
|
+
if (opts.quiet)
|
|
1135
|
+
return;
|
|
1136
|
+
if (opts.json)
|
|
1137
|
+
writeJson({ device: name, key: spec.name, value });
|
|
1138
|
+
else
|
|
1139
|
+
console.log(chalk.green(`Set ${spec.name} = ${JSON.stringify(value)}`) + chalk.gray(` on '${name}'.`));
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (spec) {
|
|
1143
|
+
const entry = getConfigValue(spec.name, { device: name });
|
|
1144
|
+
if (opts.quiet)
|
|
1145
|
+
return;
|
|
1146
|
+
if (opts.json) {
|
|
1147
|
+
writeJson({ device: name, key: spec.name, value: entry.value ?? null });
|
|
1148
|
+
}
|
|
1149
|
+
else if (entry.value === undefined) {
|
|
1150
|
+
console.log(` ${spec.name.padEnd(24)} ${chalk.gray('— (default)')}${chalk.gray(` ${spec.description}`)}`);
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
console.log(` ${spec.name.padEnd(24)} ${chalk.cyan(JSON.stringify(entry.value))}${chalk.gray(` ${spec.description}`)}`);
|
|
1154
|
+
}
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
// Bare: TTY → the interactive settings menu; piped/--json → print.
|
|
1158
|
+
if (opts.json || !isInteractiveTerminal()) {
|
|
1159
|
+
printDevicesConfig(name, Boolean(opts.json));
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
await runDevicesConfigMenu(name);
|
|
1163
|
+
};
|
|
1164
|
+
/** The interactive settings menu: pick a key, edit it, repeat. TTY-only. */
|
|
1165
|
+
const runDevicesConfigMenu = async (name) => {
|
|
1166
|
+
const { select, input, confirm } = await import('@inquirer/prompts');
|
|
1167
|
+
const DONE = '__done__';
|
|
1168
|
+
try {
|
|
1169
|
+
for (;;) {
|
|
1170
|
+
const entries = deviceConfigEntries(name);
|
|
1171
|
+
const picked = await select({
|
|
1172
|
+
message: `Config for '${name}' — pick a key to edit:`,
|
|
1173
|
+
pageSize: Math.min(entries.length + 1, 20),
|
|
1174
|
+
choices: [
|
|
1175
|
+
...entries.map((e) => {
|
|
1176
|
+
const value = e.value !== undefined
|
|
1177
|
+
? chalk.cyan(JSON.stringify(e.value))
|
|
1178
|
+
: chalk.gray(e.spec.defaultValue !== undefined
|
|
1179
|
+
? `default: ${JSON.stringify(e.spec.defaultValue)}`
|
|
1180
|
+
: 'unset (default)');
|
|
1181
|
+
return { value: e.spec.name, name: `${e.spec.name.padEnd(24)} ${value} ${chalk.gray(e.spec.description)}` };
|
|
1182
|
+
}),
|
|
1183
|
+
{ value: DONE, name: 'Done' },
|
|
1184
|
+
],
|
|
1185
|
+
});
|
|
1186
|
+
if (picked === DONE)
|
|
1187
|
+
return;
|
|
1188
|
+
const spec = configKeySpec(picked);
|
|
1189
|
+
if (spec.type === 'bool') {
|
|
1190
|
+
const current = getConfigValue(picked, { device: name }).value;
|
|
1191
|
+
const next = await confirm({
|
|
1192
|
+
message: `${picked} — enable?`,
|
|
1193
|
+
default: current ?? spec.defaultValue ?? true,
|
|
1194
|
+
});
|
|
1195
|
+
setConfigValue(picked, next, { device: name });
|
|
1196
|
+
console.log(chalk.green(`Set ${picked} = ${next}`) + chalk.gray(` on '${name}'.`));
|
|
1197
|
+
}
|
|
1198
|
+
else if (spec.type === 'string-list') {
|
|
1199
|
+
const text = await input({ message: `${picked} — append an entry (empty to go back):` });
|
|
1200
|
+
if (text.trim().length > 0) {
|
|
1201
|
+
const existing = getConfigValue(picked, { device: name }).value ?? [];
|
|
1202
|
+
setConfigValue(picked, [...existing, text.trim()], { device: name });
|
|
1203
|
+
console.log(chalk.green(`Noted on '${name}':`) + ` ${text.trim()}`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
const current = getConfigValue(picked, { device: name }).value;
|
|
1208
|
+
const raw = await input({
|
|
1209
|
+
message: `${picked}:`,
|
|
1210
|
+
default: current === undefined ? undefined : String(current),
|
|
1211
|
+
});
|
|
1212
|
+
if (raw.trim().length === 0)
|
|
1213
|
+
continue;
|
|
1214
|
+
const value = parseConfigValueInput(spec, raw.trim());
|
|
1215
|
+
setConfigValue(spec.name, value, { device: name });
|
|
1216
|
+
console.log(chalk.green(`Set ${spec.name} = ${JSON.stringify(value)}`) + chalk.gray(` on '${name}'.`));
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
catch (err) {
|
|
1221
|
+
if (isPromptCancelled(err))
|
|
1222
|
+
return; // ctrl-c / esc — leave the menu quietly
|
|
1223
|
+
throw err;
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
const configCmd = devicesCmd
|
|
1227
|
+
.command('config <name> [key] [value...]')
|
|
1228
|
+
.description('Get, set, or unset a device’s settings (scheduler, agent cap, ssh overrides, auto-launch, notes). ' +
|
|
1229
|
+
'Bare opens an interactive settings menu (TTY) or prints the resolved config (piped). ' +
|
|
1230
|
+
'Stored centrally in ~/.agents/agents.yaml under fleet.devices.<name>.config — synced, so any box can configure any device.')
|
|
1231
|
+
.option('--unset', 'reset the key to its default (removes the stored value)')
|
|
1232
|
+
.option('--json', 'output machine-readable JSON')
|
|
1233
|
+
.action(async (name, key, valueParts, opts) => {
|
|
1234
|
+
try {
|
|
1235
|
+
await runDevicesConfig(name, key, valueParts ?? [], opts);
|
|
1236
|
+
}
|
|
1237
|
+
catch (err) {
|
|
1238
|
+
console.error(chalk.red(err.message));
|
|
1239
|
+
process.exit(1);
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
setHelpSections(configCmd, {
|
|
1243
|
+
examples: `
|
|
1244
|
+
agents devices config mac-mini # settings menu (TTY) / print config (piped)
|
|
1245
|
+
agents devices config mac-mini agents.max-concurrent 4 # cap concurrent agents
|
|
1246
|
+
agents devices config mac-mini scheduler.enabled off # no routines firing there
|
|
1247
|
+
agents devices config mac-mini scheduler.enabled # read one key back
|
|
1248
|
+
agents devices config mac-mini scheduler.enabled --unset # back to the default
|
|
1249
|
+
agents devices config mac-mini notes "runs the releases" # append an operator note
|
|
1250
|
+
agents devices config win-mini ssh.auth password # password auth…
|
|
1251
|
+
agents devices config win-mini ssh.bundle muqsit # …from this secrets bundle
|
|
1252
|
+
agents devices config worker ssh.identity-file ~/.ssh/worker_ed25519
|
|
1253
|
+
agents devices config mac-mini auto-launch.enabled off # exclude from AGI EXT auto-launch
|
|
1254
|
+
agents devices config mac-mini auto-launch.preferred on # boost in auto-launch ranking
|
|
1255
|
+
agents devices config zion interactive.host zion # user scope: where agents show YOU artifacts
|
|
1256
|
+
agents devices config mac-mini --json # machine-readable
|
|
1257
|
+
`,
|
|
1258
|
+
notes: `
|
|
1259
|
+
Keys: agents.max-concurrent, scheduler.enabled, daemon.enabled,
|
|
1260
|
+
watchdog.enabled, browser.remote-control, browser.profile, notes,
|
|
1261
|
+
ssh.user, ssh.auth (key|password), ssh.bundle, ssh.bundle-key,
|
|
1262
|
+
ssh.identity-file, platform (windows|linux|macos|unknown),
|
|
1263
|
+
auto-launch.enabled, auto-launch.preferred — plus the user-scope
|
|
1264
|
+
interactive.host (stored centrally; the device name is syntax only).
|
|
1265
|
+
|
|
1266
|
+
Booleans take on/off (or true/false). 'notes' appends one entry per
|
|
1267
|
+
invocation. Values land in ~/.agents/agents.yaml under
|
|
1268
|
+
fleet.devices.<name>.config and sync with 'agents repo push/pull'.
|
|
1269
|
+
ssh.* / platform / user overlay the discovered registry profile at dial
|
|
1270
|
+
time. scheduler.enabled / daemon.enabled take effect when the daemon
|
|
1271
|
+
reloads or restarts on that device.
|
|
1272
|
+
|
|
1273
|
+
The retired subcommands still work and forward here: configure, note,
|
|
1274
|
+
set, set-interactive, enable, disable, prefer, unprefer.
|
|
1275
|
+
`,
|
|
1276
|
+
});
|
|
1277
|
+
/** Deprecation notice for a retired subcommand — STDERR only, so a --json consumer's stdout stays parseable. */
|
|
1278
|
+
const configTombstoneNotice = (retired, replacement) => {
|
|
1279
|
+
console.error(chalk.yellow(`Deprecated: "agents devices ${retired}" is now "agents devices ${replacement}". Running that for you.\n`));
|
|
1280
|
+
};
|
|
975
1281
|
devicesCmd
|
|
976
|
-
.command('enable <name>')
|
|
977
|
-
.description('Allow a registered device to be auto-picked by Factory agent launches.')
|
|
1282
|
+
.command('enable <name>', { hidden: true })
|
|
978
1283
|
.action(async (name) => {
|
|
979
1284
|
try {
|
|
1285
|
+
configTombstoneNotice('enable <name>', 'config <name> auto-launch.enabled on');
|
|
980
1286
|
await mustGetDevice(name);
|
|
981
|
-
|
|
982
|
-
|
|
1287
|
+
// Back to the default (enabled) = remove the key.
|
|
1288
|
+
await runDevicesConfig(name, 'auto-launch.enabled', [], { unset: true, quiet: true });
|
|
1289
|
+
console.log(chalk.green(`Enabled '${name}'`) + chalk.gray(' for AGI EXT auto-launch.'));
|
|
983
1290
|
}
|
|
984
1291
|
catch (err) {
|
|
985
1292
|
console.error(chalk.red(err.message));
|
|
@@ -987,13 +1294,13 @@ Typical workflow:
|
|
|
987
1294
|
}
|
|
988
1295
|
});
|
|
989
1296
|
devicesCmd
|
|
990
|
-
.command('disable <name>')
|
|
991
|
-
.description('Exclude a registered device from Factory auto-launch. It can still be picked manually via (Pick Host).')
|
|
1297
|
+
.command('disable <name>', { hidden: true })
|
|
992
1298
|
.action(async (name) => {
|
|
993
1299
|
try {
|
|
1300
|
+
configTombstoneNotice('disable <name>', 'config <name> auto-launch.enabled off');
|
|
994
1301
|
await mustGetDevice(name);
|
|
995
|
-
await
|
|
996
|
-
console.log(chalk.green(`Disabled '${name}'`) + chalk.gray(' for
|
|
1302
|
+
await runDevicesConfig(name, 'auto-launch.enabled', ['off'], { quiet: true });
|
|
1303
|
+
console.log(chalk.green(`Disabled '${name}'`) + chalk.gray(' for AGI EXT auto-launch.'));
|
|
997
1304
|
}
|
|
998
1305
|
catch (err) {
|
|
999
1306
|
console.error(chalk.red(err.message));
|
|
@@ -1001,13 +1308,13 @@ Typical workflow:
|
|
|
1001
1308
|
}
|
|
1002
1309
|
});
|
|
1003
1310
|
devicesCmd
|
|
1004
|
-
.command('prefer <name>')
|
|
1005
|
-
.description('Boost a registered device in Factory auto-launch ranking.')
|
|
1311
|
+
.command('prefer <name>', { hidden: true })
|
|
1006
1312
|
.action(async (name) => {
|
|
1007
1313
|
try {
|
|
1314
|
+
configTombstoneNotice('prefer <name>', 'config <name> auto-launch.preferred on');
|
|
1008
1315
|
await mustGetDevice(name);
|
|
1009
|
-
await
|
|
1010
|
-
console.log(chalk.green(`Preferred '${name}'`) + chalk.gray(' for
|
|
1316
|
+
await runDevicesConfig(name, 'auto-launch.preferred', ['on'], { quiet: true });
|
|
1317
|
+
console.log(chalk.green(`Preferred '${name}'`) + chalk.gray(' for AGI EXT auto-launch.'));
|
|
1011
1318
|
}
|
|
1012
1319
|
catch (err) {
|
|
1013
1320
|
console.error(chalk.red(err.message));
|
|
@@ -1015,30 +1322,31 @@ Typical workflow:
|
|
|
1015
1322
|
}
|
|
1016
1323
|
});
|
|
1017
1324
|
devicesCmd
|
|
1018
|
-
.command('unprefer <name>')
|
|
1019
|
-
.description('Remove the auto-launch preference boost from a device.')
|
|
1325
|
+
.command('unprefer <name>', { hidden: true })
|
|
1020
1326
|
.action(async (name) => {
|
|
1021
1327
|
try {
|
|
1328
|
+
configTombstoneNotice('unprefer <name>', 'config <name> auto-launch.preferred off');
|
|
1022
1329
|
await mustGetDevice(name);
|
|
1023
|
-
|
|
1024
|
-
|
|
1330
|
+
// Back to the default (not preferred) = remove the key.
|
|
1331
|
+
await runDevicesConfig(name, 'auto-launch.preferred', [], { unset: true, quiet: true });
|
|
1332
|
+
console.log(chalk.green(`No longer preferring '${name}'`) + chalk.gray(' for AGI EXT auto-launch.'));
|
|
1025
1333
|
}
|
|
1026
1334
|
catch (err) {
|
|
1027
1335
|
console.error(chalk.red(err.message));
|
|
1028
1336
|
process.exit(1);
|
|
1029
1337
|
}
|
|
1030
1338
|
});
|
|
1031
|
-
|
|
1032
|
-
.command('set-interactive [name]')
|
|
1033
|
-
.description('Get or set the interactive host — the one device that shows YOU artifacts (browser opens, dashboards, rendered plans). Stored fleet-wide as config.interactiveHost in central agents.yaml.')
|
|
1339
|
+
devicesCmd
|
|
1340
|
+
.command('set-interactive [name]', { hidden: true })
|
|
1034
1341
|
.option('--unset', 'clear the interactive host')
|
|
1035
1342
|
.option('--json', 'output machine-readable JSON')
|
|
1036
1343
|
.action(async (name, opts) => {
|
|
1037
1344
|
try {
|
|
1345
|
+
configTombstoneNotice('set-interactive [name]', 'config <name> interactive.host <name>');
|
|
1038
1346
|
if (opts.unset) {
|
|
1039
1347
|
unsetConfigValue('interactive.host');
|
|
1040
1348
|
if (opts.json)
|
|
1041
|
-
|
|
1349
|
+
writeJson({ interactiveHost: null });
|
|
1042
1350
|
else
|
|
1043
1351
|
console.log(chalk.green('Cleared the interactive host.'));
|
|
1044
1352
|
return;
|
|
@@ -1047,20 +1355,20 @@ Typical workflow:
|
|
|
1047
1355
|
await mustGetDevice(name);
|
|
1048
1356
|
setConfigValue('interactive.host', name);
|
|
1049
1357
|
if (opts.json)
|
|
1050
|
-
|
|
1358
|
+
writeJson({ interactiveHost: name });
|
|
1051
1359
|
else
|
|
1052
1360
|
console.log(chalk.green(`Interactive host: '${name}'`) + chalk.gray(' — agents show you artifacts there. Clear with --unset.'));
|
|
1053
1361
|
return;
|
|
1054
1362
|
}
|
|
1055
1363
|
const current = getConfigValue('interactive.host').value;
|
|
1056
1364
|
if (opts.json) {
|
|
1057
|
-
|
|
1365
|
+
writeJson({ interactiveHost: current ?? null });
|
|
1058
1366
|
}
|
|
1059
1367
|
else if (current) {
|
|
1060
1368
|
console.log(`${chalk.bold('Interactive host:')} ${chalk.cyan(current)}`);
|
|
1061
1369
|
}
|
|
1062
1370
|
else {
|
|
1063
|
-
console.log(chalk.gray("No interactive host set. Set one with 'agents devices
|
|
1371
|
+
console.log(chalk.gray("No interactive host set. Set one with 'agents devices config <name> interactive.host <name>'."));
|
|
1064
1372
|
}
|
|
1065
1373
|
}
|
|
1066
1374
|
catch (err) {
|
|
@@ -1068,123 +1376,65 @@ Typical workflow:
|
|
|
1068
1376
|
process.exit(1);
|
|
1069
1377
|
}
|
|
1070
1378
|
});
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
agents devices set-interactive --unset # back to no interactive host
|
|
1076
|
-
agents devices set-interactive --json # machine-readable (for skills)
|
|
1077
|
-
`,
|
|
1078
|
-
notes: `
|
|
1079
|
-
The interactive host answers "which online macOS device do I show this on?"
|
|
1080
|
-
so skills stop guessing. It is marked ★ interactive in 'agents devices list'.
|
|
1081
|
-
The value syncs fleet-wide (central agents.yaml, config.interactiveHost);
|
|
1082
|
-
per-device settings live under 'agents devices configure' instead.
|
|
1083
|
-
`,
|
|
1084
|
-
});
|
|
1085
|
-
const configureCmd = devicesCmd
|
|
1086
|
-
.command('configure <name>')
|
|
1087
|
-
.description('Get or set per-device config: --max-agents, --scheduler. Written to ~/.agents/devices/<name>/agents.yaml (works for any device — the devices/ tree syncs). Unset = default behavior.')
|
|
1088
|
-
.option('--max-agents <n>', 'cap concurrent agents (Factory auto-launch counts device-wide; teams placement counts the team’s roster on the device)')
|
|
1089
|
-
.option('--scheduler <on|off>', 'allow the routines scheduler (daemon) to fire on this device (takes effect on daemon reload/restart)')
|
|
1379
|
+
devicesCmd
|
|
1380
|
+
.command('configure <name>', { hidden: true })
|
|
1381
|
+
.option('--max-agents <n>', 'cap concurrent agents')
|
|
1382
|
+
.option('--scheduler <on|off>', 'allow the routines scheduler (daemon) to fire on this device')
|
|
1090
1383
|
.option('--json', 'output machine-readable JSON')
|
|
1091
1384
|
.action(async (name, opts) => {
|
|
1092
1385
|
try {
|
|
1386
|
+
configTombstoneNotice('configure <name> [--max-agents N] [--scheduler on|off]', 'config <name> <key> <value>');
|
|
1093
1387
|
await mustGetDevice(name);
|
|
1094
|
-
const parseOnOff = (flag, raw) => {
|
|
1095
|
-
if (raw === 'on')
|
|
1096
|
-
return true;
|
|
1097
|
-
if (raw === 'off')
|
|
1098
|
-
return false;
|
|
1099
|
-
throw new Error(`--${flag} expects 'on' or 'off', got '${raw}'.`);
|
|
1100
|
-
};
|
|
1101
1388
|
const writes = [];
|
|
1102
|
-
if (opts.maxAgents !== undefined)
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1108
|
-
if (opts.scheduler !== undefined)
|
|
1109
|
-
writes.push(['scheduler.enabled', parseOnOff('scheduler', opts.scheduler)]);
|
|
1110
|
-
if (writes.length > 0) {
|
|
1111
|
-
for (const [key, value] of writes)
|
|
1112
|
-
setConfigValue(key, value, { device: name });
|
|
1113
|
-
if (!opts.json) {
|
|
1114
|
-
for (const [key, value] of writes) {
|
|
1115
|
-
console.log(chalk.green(`Set ${key} = ${JSON.stringify(value)}`) + chalk.gray(` on '${name}'.`));
|
|
1116
|
-
}
|
|
1389
|
+
if (opts.maxAgents !== undefined)
|
|
1390
|
+
writes.push(['agents.max-concurrent', opts.maxAgents]);
|
|
1391
|
+
if (opts.scheduler !== undefined) {
|
|
1392
|
+
if (opts.scheduler !== 'on' && opts.scheduler !== 'off') {
|
|
1393
|
+
throw new Error(`--scheduler expects 'on' or 'off', got '${opts.scheduler}'.`);
|
|
1117
1394
|
}
|
|
1395
|
+
writes.push(['scheduler.enabled', opts.scheduler]);
|
|
1118
1396
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
if (opts.json) {
|
|
1122
|
-
const config = {};
|
|
1123
|
-
for (const e of entries)
|
|
1124
|
-
if (e.value !== undefined)
|
|
1125
|
-
config[e.spec.name] = e.value;
|
|
1126
|
-
process.stdout.write(JSON.stringify({ device: name, config }, null, 2) + '\n');
|
|
1127
|
-
}
|
|
1128
|
-
else {
|
|
1129
|
-
console.log(chalk.bold(`Config for '${name}'`));
|
|
1130
|
-
for (const e of entries) {
|
|
1131
|
-
const value = e.value === undefined ? chalk.gray('— (default)') : chalk.cyan(JSON.stringify(e.value));
|
|
1132
|
-
console.log(` ${e.spec.name.padEnd(24)} ${value}${chalk.gray(` ${e.spec.description}`)}`);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1397
|
+
for (const [key, value] of writes) {
|
|
1398
|
+
await runDevicesConfig(name, key, [value], { quiet: Boolean(opts.json) });
|
|
1135
1399
|
}
|
|
1400
|
+
if (opts.json)
|
|
1401
|
+
printDevicesConfig(name, true);
|
|
1402
|
+
else if (writes.length === 0)
|
|
1403
|
+
printDevicesConfig(name, false);
|
|
1136
1404
|
}
|
|
1137
1405
|
catch (err) {
|
|
1138
1406
|
console.error(chalk.red(err.message));
|
|
1139
1407
|
process.exit(1);
|
|
1140
1408
|
}
|
|
1141
1409
|
});
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
agents devices configure mac-mini --max-agents 4 # cap concurrent agents
|
|
1145
|
-
agents devices configure mac-mini --scheduler off # no routines firing there
|
|
1146
|
-
agents devices configure mac-mini # print its current config
|
|
1147
|
-
agents devices configure mac-mini --json # machine-readable
|
|
1148
|
-
`,
|
|
1149
|
-
notes: `
|
|
1150
|
-
Run it on any machine for any device: the value lands in
|
|
1151
|
-
~/.agents/devices/<name>/agents.yaml locally and reaches the device on the
|
|
1152
|
-
next 'agents repo push/pull'. Unset keys keep today's behavior.
|
|
1153
|
-
--scheduler takes effect when the daemon reloads or restarts on that
|
|
1154
|
-
device ('agents routines start' / the reload a 'routines add' sends).
|
|
1155
|
-
For the default browser profile use 'agents browser profiles set-default
|
|
1156
|
-
<name>'; for free-form text use 'agents devices note'.
|
|
1157
|
-
`,
|
|
1158
|
-
});
|
|
1159
|
-
const noteCmd = devicesCmd
|
|
1160
|
-
.command('note <name> [text...]')
|
|
1161
|
-
.description('Append a free-form note to a device (repeat to append more). No text prints the notes; --clear empties them.')
|
|
1410
|
+
devicesCmd
|
|
1411
|
+
.command('note <name> [text...]', { hidden: true })
|
|
1162
1412
|
.option('--clear', 'remove all notes from the device')
|
|
1163
1413
|
.option('--json', 'output machine-readable JSON')
|
|
1164
1414
|
.action(async (name, text, opts) => {
|
|
1165
1415
|
try {
|
|
1416
|
+
configTombstoneNotice('note <name> [text...]', 'config <name> notes <text>');
|
|
1166
1417
|
await mustGetDevice(name);
|
|
1167
1418
|
if (opts.clear) {
|
|
1168
|
-
|
|
1419
|
+
await runDevicesConfig(name, 'notes', [], { unset: true, quiet: true });
|
|
1169
1420
|
if (opts.json)
|
|
1170
|
-
|
|
1421
|
+
writeJson({ device: name, notes: [] });
|
|
1171
1422
|
else
|
|
1172
1423
|
console.log(chalk.green(`Cleared notes on '${name}'.`));
|
|
1173
1424
|
return;
|
|
1174
1425
|
}
|
|
1175
1426
|
if (text.length > 0) {
|
|
1176
|
-
|
|
1177
|
-
const notes =
|
|
1178
|
-
setConfigValue('notes', notes, { device: name });
|
|
1427
|
+
await runDevicesConfig(name, 'notes', text, { quiet: true });
|
|
1428
|
+
const notes = getConfigValue('notes', { device: name }).value ?? [];
|
|
1179
1429
|
if (opts.json)
|
|
1180
|
-
|
|
1430
|
+
writeJson({ device: name, notes });
|
|
1181
1431
|
else
|
|
1182
1432
|
console.log(chalk.green(`Noted on '${name}':`) + ` ${text.join(' ')}`);
|
|
1183
1433
|
return;
|
|
1184
1434
|
}
|
|
1185
1435
|
const notes = getConfigValue('notes', { device: name }).value ?? [];
|
|
1186
1436
|
if (opts.json) {
|
|
1187
|
-
|
|
1437
|
+
writeJson({ device: name, notes });
|
|
1188
1438
|
}
|
|
1189
1439
|
else if (notes.length > 0) {
|
|
1190
1440
|
console.log(chalk.bold(`Notes for '${name}'`));
|
|
@@ -1192,7 +1442,7 @@ Typical workflow:
|
|
|
1192
1442
|
console.log(` ${chalk.gray('•')} ${n}`);
|
|
1193
1443
|
}
|
|
1194
1444
|
else {
|
|
1195
|
-
console.log(chalk.gray(`No notes on '${name}'. Add one with 'agents devices
|
|
1445
|
+
console.log(chalk.gray(`No notes on '${name}'. Add one with 'agents devices config ${name} notes "..."'.`));
|
|
1196
1446
|
}
|
|
1197
1447
|
}
|
|
1198
1448
|
catch (err) {
|
|
@@ -1200,45 +1450,15 @@ Typical workflow:
|
|
|
1200
1450
|
process.exit(1);
|
|
1201
1451
|
}
|
|
1202
1452
|
});
|
|
1203
|
-
setHelpSections(noteCmd, {
|
|
1204
|
-
examples: `
|
|
1205
|
-
agents devices note mac-mini "runs the releases — don't reboot"
|
|
1206
|
-
agents devices note mac-mini "4 displays attached" # appends a second note
|
|
1207
|
-
agents devices note mac-mini # print its notes
|
|
1208
|
-
agents devices note mac-mini --clear # drop them all
|
|
1209
|
-
`,
|
|
1210
|
-
notes: `
|
|
1211
|
-
Notes are operator memory for a box (why it exists, what to never do to
|
|
1212
|
-
it). They sync like every other device doc; 'agents devices list --json'
|
|
1213
|
-
carries them under config.notes.
|
|
1214
|
-
`,
|
|
1215
|
-
});
|
|
1216
|
-
/** Device-scope config block for `list --json`, keyed by yamlKey (set keys only). */
|
|
1217
|
-
const deviceConfigJson = (name) => {
|
|
1218
|
-
const config = {};
|
|
1219
|
-
for (const entry of listConfig({ device: name })) {
|
|
1220
|
-
if (entry.spec.scope !== 'device' || entry.value === undefined)
|
|
1221
|
-
continue;
|
|
1222
|
-
config[entry.spec.yamlKey] = entry.value;
|
|
1223
|
-
}
|
|
1224
|
-
return Object.keys(config).length > 0 ? config : undefined;
|
|
1225
|
-
};
|
|
1226
1453
|
const runList = async (opts = {}) => {
|
|
1227
1454
|
const reg = await loadDevices();
|
|
1228
1455
|
const names = Object.keys(reg).sort();
|
|
1229
1456
|
const interactiveHost = getConfigValue('interactive.host').value;
|
|
1230
|
-
if (opts.json) {
|
|
1231
|
-
// Registry + local device docs, always fast — the Factory extension polls
|
|
1232
|
-
// this path. Each row carries its device-scope `config` (maxAgents,
|
|
1233
|
-
// schedulerEnabled, notes, defaultBrowserProfile — set keys
|
|
1234
|
-
// only) and an `interactive` flag for the configured interactive host.
|
|
1235
|
-
process.stdout.write(JSON.stringify(names.map((n) => {
|
|
1236
|
-
const config = deviceConfigJson(n);
|
|
1237
|
-
return { ...reg[n], interactive: n === interactiveHost, ...(config ? { config } : {}) };
|
|
1238
|
-
}), null, 2) + '\n');
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
1457
|
if (names.length === 0) {
|
|
1458
|
+
if (opts.json) {
|
|
1459
|
+
process.stdout.write('[]\n');
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1242
1462
|
console.log(chalk.gray("No devices. Run 'agents devices sync' or 'agents devices add <name> <user@host>'."));
|
|
1243
1463
|
return;
|
|
1244
1464
|
}
|
|
@@ -1272,6 +1492,19 @@ Typical workflow:
|
|
|
1272
1492
|
if (statsMap)
|
|
1273
1493
|
await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
|
|
1274
1494
|
}
|
|
1495
|
+
if (opts.json) {
|
|
1496
|
+
process.stdout.write(JSON.stringify(names.map((name) => {
|
|
1497
|
+
const config = deviceConfigJson(name);
|
|
1498
|
+
const health = statsMap?.get(name);
|
|
1499
|
+
return {
|
|
1500
|
+
...resolveDeviceProfile(reg[name]),
|
|
1501
|
+
interactive: name === interactiveHost,
|
|
1502
|
+
...(config ? { config } : {}),
|
|
1503
|
+
...(health ? { health: { ...health, headroom: headroom(health) } } : {}),
|
|
1504
|
+
};
|
|
1505
|
+
}), null, 2) + '\n');
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1275
1508
|
console.log(chalk.bold(`Devices (${names.length})`));
|
|
1276
1509
|
for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full, interactiveHost))
|
|
1277
1510
|
console.log(line);
|
|
@@ -1295,7 +1528,7 @@ Typical workflow:
|
|
|
1295
1528
|
.command('list')
|
|
1296
1529
|
.alias('ls')
|
|
1297
1530
|
.description('List registered devices with platform, address, reachability, and live resource headroom.')
|
|
1298
|
-
.option('--json', 'output
|
|
1531
|
+
.option('--json', 'output effective device profiles, config, and health as a JSON array')
|
|
1299
1532
|
.option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
|
|
1300
1533
|
.option('--refresh', 'force a live probe of every device, bypassing the cache')
|
|
1301
1534
|
.option('--live', 'alias of --refresh (shorter to type)')
|
|
@@ -1444,8 +1677,7 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
|
|
|
1444
1677
|
}
|
|
1445
1678
|
});
|
|
1446
1679
|
devicesCmd
|
|
1447
|
-
.command('set <name>')
|
|
1448
|
-
.description('Update fields on an existing device (platform, user, auth).')
|
|
1680
|
+
.command('set <name>', { hidden: true })
|
|
1449
1681
|
.option('--platform <platform>', 'windows | linux | macos')
|
|
1450
1682
|
.option('--user <user>', 'login user')
|
|
1451
1683
|
.option('--auth <method>', 'key | password')
|
|
@@ -1455,21 +1687,35 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
|
|
|
1455
1687
|
.option('--clear-identity-file', 'return key auth to ssh-agent/default-key discovery')
|
|
1456
1688
|
.action(async (name, opts) => {
|
|
1457
1689
|
try {
|
|
1690
|
+
configTombstoneNotice('set <name> [--platform|--user|--auth|--bundle|--bundle-key|--identity-file …]', 'config <name> <ssh.*|platform> <value>');
|
|
1458
1691
|
const existing = await mustGetDevice(name);
|
|
1459
|
-
const nextMethod = opts.auth ?? existing.auth.method;
|
|
1692
|
+
const nextMethod = opts.auth ?? resolveDeviceProfile(existing).auth.method;
|
|
1460
1693
|
if (opts.identityFile && nextMethod !== 'key') {
|
|
1461
1694
|
throw new Error('--identity-file requires key auth; pass --auth key in the same command.');
|
|
1462
1695
|
}
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
:
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1696
|
+
const writes = [];
|
|
1697
|
+
if (opts.platform)
|
|
1698
|
+
writes.push({ key: 'platform', value: opts.platform });
|
|
1699
|
+
if (opts.user)
|
|
1700
|
+
writes.push({ key: 'ssh.user', value: opts.user });
|
|
1701
|
+
if (opts.auth)
|
|
1702
|
+
writes.push({ key: 'ssh.auth', value: opts.auth });
|
|
1703
|
+
if (opts.bundle)
|
|
1704
|
+
writes.push({ key: 'ssh.bundle', value: opts.bundle });
|
|
1705
|
+
if (opts.bundleKey)
|
|
1706
|
+
writes.push({ key: 'ssh.bundle-key', value: opts.bundleKey });
|
|
1707
|
+
if (opts.identityFile)
|
|
1708
|
+
writes.push({ key: 'ssh.identity-file', value: opts.identityFile });
|
|
1709
|
+
if (opts.clearIdentityFile)
|
|
1710
|
+
writes.push({ key: 'ssh.identity-file' }); // unset
|
|
1711
|
+
if (writes.length === 0) {
|
|
1712
|
+
printDevicesConfig(name, false);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
for (const w of writes) {
|
|
1716
|
+
await runDevicesConfig(name, w.key, w.value !== undefined ? [w.value] : [], { unset: w.value === undefined, quiet: true });
|
|
1717
|
+
}
|
|
1718
|
+
const d = resolveDeviceProfile((await mustGetDevice(name)));
|
|
1473
1719
|
console.log(chalk.green(`Updated device '${name}'`) + chalk.gray(` (auth: ${d.auth.method}${d.auth.bundle ? ` via ${d.auth.bundle}` : ''})`));
|
|
1474
1720
|
}
|
|
1475
1721
|
catch (err) {
|
|
@@ -1550,8 +1796,25 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
|
|
|
1550
1796
|
});
|
|
1551
1797
|
devicesCmd
|
|
1552
1798
|
.command('update')
|
|
1553
|
-
.description('Roll out agents-cli to every online registered device (`agents upgrade --yes` on each). Offline devices are skipped.')
|
|
1799
|
+
.description('Roll out agents-cli to every online registered device (`agents upgrade --yes` on each), then verify each box actually runs the new version. Offline devices are skipped.')
|
|
1554
1800
|
.argument('[version]', 'Target version or dist-tag (default: latest)')
|
|
1801
|
+
.addHelpText('after', `
|
|
1802
|
+
Examples:
|
|
1803
|
+
agents fleet update # roll out latest, then verify each box
|
|
1804
|
+
agents fleet update 1.22.35 # pin the target version
|
|
1805
|
+
agents devices update # same command under the devices group
|
|
1806
|
+
|
|
1807
|
+
After each upgrade the rollout asks the box what \`agents\` resolves to and what
|
|
1808
|
+
version that copy reports. A box that upgraded with exit 0 but still resolves to
|
|
1809
|
+
another install — a stale copy in a second node prefix, a Homebrew shim, or a
|
|
1810
|
+
hand-made link that sits earlier on PATH than the npm global — is reported
|
|
1811
|
+
\`stale\` with its resolved path, counted as NOT upgraded, and makes the command
|
|
1812
|
+
exit non-zero. Remove the install that owns the name, or reorder PATH on that
|
|
1813
|
+
box; \`agents doctor\` names it.
|
|
1814
|
+
|
|
1815
|
+
A box whose probe cannot answer (no POSIX shell, e.g. Windows) is reported
|
|
1816
|
+
\`unverified\` rather than counted as a success.
|
|
1817
|
+
`)
|
|
1555
1818
|
.action(async (version) => {
|
|
1556
1819
|
let cmd;
|
|
1557
1820
|
try {
|
|
@@ -1568,8 +1831,13 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
|
|
|
1568
1831
|
return;
|
|
1569
1832
|
}
|
|
1570
1833
|
console.log(chalk.gray(`Running \`${cmd.join(' ')}\` on ${targets.filter((t) => !t.skip).length} online device(s)…`));
|
|
1571
|
-
const
|
|
1572
|
-
|
|
1834
|
+
const self = machineId();
|
|
1835
|
+
const results = runFleet(targets, cmd, { self });
|
|
1836
|
+
// `exit 0` from the upgrade only proves the npm global moved. Ask each box
|
|
1837
|
+
// what `agents` actually resolves to before calling the rollout a success
|
|
1838
|
+
// (RUSH-2446) — a dev install shadowing the global keeps running old code.
|
|
1839
|
+
const verifications = verifyFleetRollout(targets, results, version, { self });
|
|
1840
|
+
printFleetResults(results, verifications);
|
|
1573
1841
|
});
|
|
1574
1842
|
devicesCmd
|
|
1575
1843
|
.command('run <cmd...>')
|
|
@@ -1590,6 +1858,74 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
|
|
|
1590
1858
|
const results = runFleet(targets, cmd, { self: machineId() });
|
|
1591
1859
|
printFleetResults(results);
|
|
1592
1860
|
});
|
|
1861
|
+
devicesCmd
|
|
1862
|
+
.command('ps')
|
|
1863
|
+
.description('List agent tasks dispatched to devices with `agents run --device <name> --no-follow`. Reconciles each still-`running` record against the remote before listing. View a log with `agents logs <id>`.')
|
|
1864
|
+
.option('--json', 'Output JSON')
|
|
1865
|
+
.action((opts) => doDeviceTaskPs(!!opts.json));
|
|
1866
|
+
devicesCmd
|
|
1867
|
+
.command('stop <id>')
|
|
1868
|
+
.alias('kill')
|
|
1869
|
+
.description('Terminate a running dispatched task from this machine (SIGTERM the remote process group; marks it failed/143).')
|
|
1870
|
+
.action((id) => doDeviceTaskStop(id));
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* `agents devices ps` — list tasks dispatched to devices (`agents run --device
|
|
1874
|
+
* <name> --no-follow`). Heals any 'running' record whose local follower died
|
|
1875
|
+
* (dropped connection, laptop sleep) against the remote `.exit` before listing,
|
|
1876
|
+
* so a finished run never shows stuck at 'running'.
|
|
1877
|
+
*/
|
|
1878
|
+
async function doDeviceTaskPs(json) {
|
|
1879
|
+
const tasks = reconcileRunningTasks(listTasks());
|
|
1880
|
+
if (json) {
|
|
1881
|
+
console.log(JSON.stringify(tasks, null, 2));
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
if (tasks.length === 0) {
|
|
1885
|
+
console.log(chalk.gray('No dispatched tasks yet. Dispatch one: agents run <agent> "<task>" --device <name> --no-follow'));
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
const cols = terminalWidth();
|
|
1889
|
+
console.log(chalk.bold('ID').padEnd(11) + chalk.bold('NAME').padEnd(16) + chalk.bold('DEVICE').padEnd(16) + chalk.bold('AGENT').padEnd(10) + chalk.bold('STATUS').padEnd(11) + chalk.bold('PROMPT'));
|
|
1890
|
+
for (const t of tasks) {
|
|
1891
|
+
const status = t.status === 'completed' ? chalk.green(t.status) : t.status === 'failed' ? chalk.red(t.status) : chalk.yellow(t.status);
|
|
1892
|
+
const nameCol = truncateToWidth(t.name ?? chalk.gray('-'), 15).padEnd(16);
|
|
1893
|
+
const promptCol = truncateToWidth(t.prompt, Math.max(12, cols - (11 + 16 + 16 + 10 + 11)));
|
|
1894
|
+
console.log(t.id.padEnd(11) + nameCol + t.host.padEnd(16) + t.agent.padEnd(10) + status.padEnd(11) + promptCol);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
/** `agents devices stop <id>` — terminate a running dispatched task from the origin machine. */
|
|
1898
|
+
async function doDeviceTaskStop(ref) {
|
|
1899
|
+
// Heal first so we don't try to kill a process that already exited.
|
|
1900
|
+
const current = resolveTaskRef(ref);
|
|
1901
|
+
if (!current) {
|
|
1902
|
+
console.log(chalk.red(`Unknown task "${ref}".`));
|
|
1903
|
+
process.exitCode = 1;
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const task = reconcileRunningTasks([current])[0] ?? current;
|
|
1907
|
+
if (task.status !== 'running') {
|
|
1908
|
+
console.log(chalk.gray(`Task ${task.id} is already ${task.status}` + (task.exitCode !== undefined ? ` (exit ${task.exitCode})` : '') + '.'));
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
try {
|
|
1912
|
+
const stopped = stopDispatchedTask(task);
|
|
1913
|
+
const statusColor = stopped.status === 'completed' ? chalk.green : chalk.yellow;
|
|
1914
|
+
const exitNote = stopped.exitCode === 143
|
|
1915
|
+
? 'exit 143 / SIGTERM'
|
|
1916
|
+
: stopped.exitCode !== undefined
|
|
1917
|
+
? `exit ${stopped.exitCode}`
|
|
1918
|
+
: stopped.status;
|
|
1919
|
+
console.log(chalk.green(`Stopped ${stopped.id}`) +
|
|
1920
|
+
chalk.gray(` on ${stopped.host}`) +
|
|
1921
|
+
' ' + statusColor(stopped.status) +
|
|
1922
|
+
chalk.gray(` (${exitNote})`));
|
|
1923
|
+
console.log(chalk.gray(`Logs: agents logs ${stopped.id}`));
|
|
1924
|
+
}
|
|
1925
|
+
catch (err) {
|
|
1926
|
+
console.error(chalk.red(err?.message ?? err));
|
|
1927
|
+
process.exitCode = 1;
|
|
1928
|
+
}
|
|
1593
1929
|
}
|
|
1594
1930
|
/** Register the `agents ssh` smart wrapper. */
|
|
1595
1931
|
function registerSshWrapper(program) {
|
|
@@ -1644,14 +1980,17 @@ the target when it exists, else the remote home. Same portable-cwd rule as
|
|
|
1644
1980
|
// `user@device` (same device, login user overridden — dialed via its
|
|
1645
1981
|
// Tailscale route, not LAN DNS), or an ad-hoc `user@host`/`host` literal.
|
|
1646
1982
|
// A bare unregistered alias still errors as "Unknown device".
|
|
1647
|
-
const
|
|
1648
|
-
if (!
|
|
1983
|
+
const resolvedTarget = await resolveDeviceTarget(target);
|
|
1984
|
+
if (!resolvedTarget) {
|
|
1649
1985
|
// Not a registered device — it may be a leased crabbox box slug. ssh into
|
|
1650
1986
|
// it directly (crabbox@<tailnet|ip>:2222) before giving up.
|
|
1651
1987
|
trySshLeasedBox(target, cmd); // exits the process on a match
|
|
1652
1988
|
console.error(chalk.red(`Unknown device '${target}'. See 'agents devices list'.`));
|
|
1653
1989
|
process.exit(1);
|
|
1654
1990
|
}
|
|
1991
|
+
// The effective profile: central config (ssh.*/platform/user) overlaid on
|
|
1992
|
+
// the discovery record.
|
|
1993
|
+
const device = resolveDeviceProfile(resolvedTarget);
|
|
1655
1994
|
// Preflight: a device Tailscale last saw offline would otherwise hang
|
|
1656
1995
|
// for the full ConnectTimeout. Fail fast with a clear message instead.
|
|
1657
1996
|
if (device.tailscale && !device.tailscale.online) {
|