@phnx-labs/agents-cli 1.20.73 → 1.20.76
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 +989 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +31 -1
- package/dist/commands/exec.js +31 -3
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +87 -9
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +15 -2
- package/dist/commands/secrets.js +44 -21
- package/dist/commands/sessions-browser.js +1 -1
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.js +16 -1
- package/dist/commands/sessions.d.ts +36 -13
- package/dist/commands/sessions.js +210 -40
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/setup.js +6 -1
- package/dist/commands/ssh.js +71 -35
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +51 -11
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +99 -31
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +14 -4
- package/dist/lib/activity.js +17 -5
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +56 -0
- package/dist/lib/agents.js +133 -7
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +60 -0
- package/dist/lib/daemon.js +18 -2
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +21 -0
- package/dist/lib/devices/fleet.js +26 -0
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +66 -0
- package/dist/lib/exec.js +195 -6
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +2 -0
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +13 -5
- package/dist/lib/hosts/run-target.js +26 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +37 -2
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +25 -0
- package/dist/lib/import.js +107 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/menubar/install-menubar.d.ts +20 -0
- package/dist/lib/menubar/install-menubar.js +45 -1
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +7 -2
- package/dist/lib/overdue.js +18 -11
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/project-resources.js +20 -5
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routines.d.ts +26 -0
- package/dist/lib/routines.js +19 -2
- package/dist/lib/runner.js +104 -7
- package/dist/lib/secrets/agent.d.ts +66 -1
- package/dist/lib/secrets/agent.js +154 -90
- package/dist/lib/secrets/bundles.d.ts +34 -0
- package/dist/lib/secrets/bundles.js +179 -19
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +21 -3
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +114 -7
- package/dist/lib/session/active.js +323 -52
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +48 -0
- package/dist/lib/session/db.js +238 -14
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +465 -2
- package/dist/lib/session/discover.js +1648 -468
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +130 -30
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +22 -10
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.js +6 -2
- package/dist/lib/shims.d.ts +27 -1
- package/dist/lib/shims.js +88 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +15 -36
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +48 -2
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +22 -2
- package/dist/lib/versions.js +98 -10
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -270,10 +270,15 @@ On a terminal, `agents sessions --active` (and a bare `agents sessions`) open th
|
|
|
270
270
|
| `d` | device (cycles) | `--device` |
|
|
271
271
|
| `p` | this repo ↔ all dirs | `--all` |
|
|
272
272
|
| `w` | time window | `--since` |
|
|
273
|
+
| `tab` | toggle the preview pane | — |
|
|
273
274
|
| `⏎` | resume / attach | `resume` / `focus` |
|
|
274
275
|
| `y` | copy the equivalent command | `--print-cmd` |
|
|
275
276
|
|
|
276
|
-
Filters **stack** (they AND together), the active set shows in the header, and the highlighted row previews below. Because every hotkey has a flag, the view you build by hand is a real command: press `y` (or run `--print-cmd`) to get the exact `ag sessions …` line — explore interactively, hand the line to an agent. Piped output, `--json`, or `--no-interactive` keep the plain listing for scripts. Peek without opening the pager with `agents sessions <id> --preview`.
|
|
277
|
+
Filters **stack** (they AND together), the active set shows in the header, and the highlighted row **previews below by default** (`tab` hides it) — prompt, activity, last response, plus a links line where the worked-on ticket and the PR the session opened are **clickable** (OSC 8 hyperlinks: the ticket jumps to Linear, the `PR#` to GitHub, in terminals that support them). The Linear workspace is resolved from `LINEAR_WORKSPACE` or the linear-cli config, so tickets stay plain text when it's unknown. Because every hotkey has a flag, the view you build by hand is a real command: press `y` (or run `--print-cmd`) to get the exact `ag sessions …` line — explore interactively, hand the line to an agent. Piped output, `--json`, or `--no-interactive` keep the plain listing for scripts. Peek without opening the pager with `agents sessions <id> --preview`.
|
|
278
|
+
|
|
279
|
+
| before — preview hidden | after — preview open + clickable links |
|
|
280
|
+
| --- | --- |
|
|
281
|
+
|  |  |
|
|
277
282
|
|
|
278
283
|
Each live session resolves to `working`, `waiting_input` (with why -- a question, a plan review, or a permission prompt), or `idle`, alongside badges for the PR it opened, the worktree it sits in, and the ticket it's working. `agents sessions focus [id]` attaches the live pane in place -- the tmux split locally or over SSH, or its Ghostty tab -- and falls back to a fresh tab + resume when the terminal is gone.
|
|
279
284
|
|
|
@@ -292,6 +297,17 @@ agents sessions resume --host zion --tmux # resume on another machine over SSH
|
|
|
292
297
|
|
|
293
298
|
`agents sessions resume` reopens sessions in whatever terminal you're in -- auto-detected across iTerm, Ghostty, tmux, and the VSCodium agent-terminal, or forced with `--iterm` / `--ghostty` / `--tmux` / `--vscodium`. Back them with **tmux** and the runs turn durable: detach, close your editor, reboot the GUI -- the session is still alive to `agents tmux attach`. The whole `agents tmux` subsystem (persistent multiplexer sessions that survive editor restarts and can be shared with other tools) sits underneath.
|
|
294
299
|
|
|
300
|
+
### Send an agent to the background — and bring it back
|
|
301
|
+
|
|
302
|
+
Running 30 agents and drowning in terminal tabs? `agents sessions detach <id>` stops a session's interactive process and keeps it working **headless** in the background -- it drives its task to done unattended, no tab, lower cost. `agents sessions attach <id>` brings it back: version-pinned resume into a live TUI, the same session and full history (including whatever it did while backgrounded).
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
agents sessions detach a1b2c3d4 # go headless in the background, keep working
|
|
306
|
+
agents sessions attach a1b2c3d4 # resume it interactively, right here
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Both are agent-agnostic -- they route through the same `agents run --resume` path (native resume for Claude/Codex, `/continue` replay for the rest). `agents sessions --active` marks each session's `presence` -- `attached` (you're watching it), `background` (running headless), or `parked` (its background run finished) -- so the menu bar and Factory show where every agent is. In the Factory extension, **Agents: Detach** (`Cmd/Ctrl+K B`) and **Agents: Attach** (`Cmd/Ctrl+K A`) do the same over the focused terminal.
|
|
310
|
+
|
|
295
311
|
---
|
|
296
312
|
|
|
297
313
|
## Control the fleet
|
|
@@ -319,6 +335,7 @@ agents feed --flat # one row per agent (legacy)
|
|
|
319
335
|
agents feed --host mac-mini # scope the view to one or more hosts
|
|
320
336
|
agents feed --local # skip the SSH fan-out
|
|
321
337
|
agents feed --json # blocks stamped with their outcome key
|
|
338
|
+
agents feed post "halfway done" # agent status post (auto session identity)
|
|
322
339
|
```
|
|
323
340
|
|
|
324
341
|
Top-level questions and waiting notifications publish one atomic open-block record per session, including the mailbox id, host, runtime, and every answer option. The default view collapses agents under the **outcome** they serve (Linear ticket, PR, worktree slug, or Unassigned) so a 1,100-agent fleet reads as dozens of deliverables. Answered, resumed, and stopped blocks clear automatically; Task subagents are excluded. The rendered reply command uses the same mailbox id with `agents message`, so the decision routes back to the agent that asked it.
|
|
@@ -436,7 +453,8 @@ agents sync --host gpu-box # make the remote machine current
|
|
|
436
453
|
agents doctor --devices # readiness matrix for every registered device
|
|
437
454
|
agents doctor --devices --json # machine-readable fleet readiness
|
|
438
455
|
agents doctor --device mac-mini # same matrix, scoped to one device
|
|
439
|
-
agents fleet status #
|
|
456
|
+
agents fleet status # online/offline rollup + NEEDS ATTENTION + OS-grouped rows (cache-first)
|
|
457
|
+
agents fleet status --verbose # full per-device auth/CLI/sync/version grid
|
|
440
458
|
agents fleet status --live # force a live resource probe (alias of --refresh)
|
|
441
459
|
agents fleet status --json --strict # scriptable fleet health gate
|
|
442
460
|
agents check --devices # CI drift gate across every registered device
|
|
@@ -468,13 +486,21 @@ this machine locally plus any device missing from the cache; pass `--refresh` (o
|
|
|
468
486
|
shorter `--live`) to force a full live probe of every box. Cache-served output notes its
|
|
469
487
|
age (`updated 2m ago — pass --refresh (--live) for a live probe`).
|
|
470
488
|
|
|
471
|
-
`agents fleet status`
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
489
|
+
`agents fleet status` answers "is my fleet OK?" at a glance: a one-line rollup
|
|
490
|
+
(`● N online · ○ M offline`), a short **NEEDS ATTENTION** list where every item names
|
|
491
|
+
the command that fixes it (offline → `check the box`, config drift or a stark CLI gap →
|
|
492
|
+
`agents apply <box>`, version skew → `agents upgrade --fleet`), then quiet per-device
|
|
493
|
+
rows grouped by OS (macOS / Linux / Windows) showing just `name · capacity · load/mem ·
|
|
494
|
+
version`, with this machine flagged `▸ … ← this machine`. A healthy fleet reads in a few
|
|
495
|
+
lines; orphaned versions are demoted to a one-line `agents prune` nudge in the footer.
|
|
496
|
+
|
|
497
|
+
Pass `--verbose` for the full per-device grid — the **Auth column** (which agent accounts
|
|
498
|
+
are actually logged in, per device, read from the auth-health cache — no network), plus
|
|
499
|
+
the CLI-readiness and sync-drift columns. The Auth column has four buckets so it never
|
|
500
|
+
cries wolf: `●live` (verified), `·present` (signed in but the agent has no live-probe
|
|
501
|
+
endpoint — e.g. codex/grok — benign), `◐degraded` (soft/self-healing: expired-but-refreshing,
|
|
502
|
+
rate-limited), and `○revoked` (server rejected — re-login now). Only `○` means a real
|
|
503
|
+
re-login is needed. Run `agents fleet ping` to force a live re-verification across the fleet.
|
|
478
504
|
|
|
479
505
|
**Hosts** (`agents hosts`) are git-synced dispatch targets in `agents.yaml`; **devices** (`agents devices`) are your Tailscale machines in a local registry. Both ride SSH and feed one host pool: devices appear in `agents hosts list` and capability routing without a second enrollment. On `--host` runs every `agents run` option is either forwarded (`--effort --env --timeout --loop …`), rejected loud (`--secrets` never crosses SSH implicitly), or consumed locally — nothing silently drops. See [docs/00-concepts.md](apps/cli/docs/00-concepts.md#devices--hosts).
|
|
480
506
|
|
|
@@ -1218,7 +1244,7 @@ Agents are defined in [src/lib/agents.ts](apps/cli/src/lib/agents.ts) -- each is
|
|
|
1218
1244
|
| [`apps/cli`](apps/cli) | **The CLI** (this README) — version management, config sync, sessions, teams, cloud, browser, computer, secrets. |
|
|
1219
1245
|
| [`apps/factory`](apps/factory) | **Factory** — a VS Code extension that spawns agent terminals as tabs and adds the Factory Floor dashboard. A separate product with its own publish identity. |
|
|
1220
1246
|
| [`native/computer-mac`](native/computer-mac) · [`native/computer-win`](native/computer-win) | Native backends behind `agents computer` — Swift (macOS Accessibility + screen capture) and C#/.NET (Windows UI Automation). |
|
|
1221
|
-
| [`packages/session-tracker`](packages/session-tracker) | The `SessionStart` hook that writes live-session state the
|
|
1247
|
+
| [`packages/session-tracker`](packages/session-tracker) | The `SessionStart` hook that writes live-session state the **Factory extension** reads back (`apps/factory/src/core/liveSession.ts`) — not the CLI, which reads transcripts. |
|
|
1222
1248
|
|
|
1223
1249
|
## Contributing
|
|
1224
1250
|
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/commands/apply.js
CHANGED
|
@@ -18,7 +18,8 @@ import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
|
|
|
18
18
|
import { ensureDevicesRegistered } from '../lib/devices/sync.js';
|
|
19
19
|
import { readFleetFile, resolveDesired } from '../lib/fleet/manifest.js';
|
|
20
20
|
import { snapshotAuth, materializeAuth, parseAuthBundle, KEYCHAIN_BOUND_ON_MAC } from '../lib/fleet/auth-sync.js';
|
|
21
|
-
import { agentIdOf, diffFleet, probeDevice, runFleetApply, pool, sourceHome, } from '../lib/fleet/apply.js';
|
|
21
|
+
import { agentIdOf, diffFleet, probeDevice, runFleetApply, pool, sourceHome, expandAllSpecs, rosterNeedsVersions, } from '../lib/fleet/apply.js';
|
|
22
|
+
import { listInstalledVersions } from '../lib/versions.js';
|
|
22
23
|
/** Version of the running agents-cli — the fleet target version. */
|
|
23
24
|
function localCliVersion() {
|
|
24
25
|
try {
|
|
@@ -92,7 +93,7 @@ function renderPlan(plan) {
|
|
|
92
93
|
const agentsCell = row.probe.reachable
|
|
93
94
|
? (() => {
|
|
94
95
|
const add = row.actions.filter((a) => a.kind === 'add-agent');
|
|
95
|
-
return add.length === 0 ? chalk.green(`ok ${row.desired.agents.length}/${row.desired.agents.length}`) : chalk.cyan('+ ' + add.map((a) => a.agent).join(','));
|
|
96
|
+
return add.length === 0 ? chalk.green(`ok ${row.desired.agents.length}/${row.desired.agents.length}`) : chalk.cyan('+ ' + add.map((a) => a.spec ?? a.agent).join(','));
|
|
96
97
|
})()
|
|
97
98
|
: chalk.gray('-');
|
|
98
99
|
const configCell = row.probe.reachable
|
|
@@ -178,6 +179,15 @@ async function runApply(opts) {
|
|
|
178
179
|
if (desired.length === 0)
|
|
179
180
|
throw new Error(`Device '${opts.device}' is not a target in this manifest.`);
|
|
180
181
|
}
|
|
182
|
+
// --agent overrides the roster for the targeted device(s): install exactly these
|
|
183
|
+
// specs instead of the manifest's. `claude@all` expands to every claude version
|
|
184
|
+
// installed on THIS machine, so a fresh box inherits the same version set. Pair
|
|
185
|
+
// with --device to seed one box; without it, every targeted device gets the set.
|
|
186
|
+
if (opts.agent && opts.agent.length > 0) {
|
|
187
|
+
const specs = expandAllSpecs(opts.agent, (id) => listInstalledVersions(id));
|
|
188
|
+
desired = desired.map((d) => ({ ...d, agents: specs }));
|
|
189
|
+
console.log(chalk.gray(`Roster override (--agent): ${specs.join(', ')}`));
|
|
190
|
+
}
|
|
181
191
|
if (opts.login === false)
|
|
182
192
|
desired = desired.map((d) => ({ ...d, login: 'skip' }));
|
|
183
193
|
if (desired.length === 0) {
|
|
@@ -200,8 +210,9 @@ async function runApply(opts) {
|
|
|
200
210
|
};
|
|
201
211
|
// Probe every target device in parallel.
|
|
202
212
|
const nameToProfile = new Map(desired.map((d) => [d.device, registry[d.device]]));
|
|
213
|
+
const withVersions = rosterNeedsVersions(desired);
|
|
203
214
|
console.log(chalk.gray(`Probing ${desired.length} device(s)…`));
|
|
204
|
-
const probeList = await pool(desired, 6, async (d) => probeDevice(nameToProfile.get(d.device)));
|
|
215
|
+
const probeList = await pool(desired, 6, async (d) => probeDevice(nameToProfile.get(d.device), { withVersions }));
|
|
205
216
|
const probes = new Map(probeList.map((p) => [p.device, p]));
|
|
206
217
|
const targetCliVersion = localCliVersion();
|
|
207
218
|
let plan = diffFleet(desired, probes, { targetCliVersion, sourceAuth, secretsBundles: manifest.secrets?.bundles });
|
|
@@ -280,6 +291,7 @@ export function configureApplyCommand(cmd) {
|
|
|
280
291
|
.option('--dry-run', 'Alias for --plan')
|
|
281
292
|
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
282
293
|
.option('--device <name>', 'Scope the apply to a single device')
|
|
294
|
+
.option('--agent <specs...>', 'Override the roster for targeted device(s): install these specs instead of the manifest\'s. Use `claude@all` to replicate every version installed on this machine.')
|
|
283
295
|
.option('--only <dims>', 'Limit to dimensions: comma list of agents,config,login')
|
|
284
296
|
.option('--no-login', 'Do not propagate logins')
|
|
285
297
|
.addOption(new Option('--recv-auth', 'internal: receive an auth bundle on stdin').hideHelp())
|
|
@@ -309,6 +321,12 @@ export function registerApplyCommand(program) {
|
|
|
309
321
|
|
|
310
322
|
# One device only
|
|
311
323
|
agents apply --device yosemite-s1 -y
|
|
324
|
+
|
|
325
|
+
# Replicate every claude version on this machine onto a fresh box
|
|
326
|
+
agents apply --agent claude@all --device yosemite-s0 -y
|
|
327
|
+
|
|
328
|
+
# Install a specific pinned version on one device
|
|
329
|
+
agents apply --agent claude@2.1.207 --device yosemite-s0
|
|
312
330
|
`,
|
|
313
331
|
});
|
|
314
332
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents sessions attach <id>` — bring a backgrounded (or parked) agent session
|
|
3
|
+
* back to the foreground: stop its headless continuation, then resume it
|
|
4
|
+
* interactively in this terminal. The resume is version-pinned native resume
|
|
5
|
+
* (claude/codex) or a `/continue` replay (others) — the same session, full
|
|
6
|
+
* history, the exact inverse of `agents sessions detach`.
|
|
7
|
+
*/
|
|
8
|
+
import type { Command } from 'commander';
|
|
9
|
+
export declare function registerAttachCommand(program: Command): void;
|
|
10
|
+
export declare function attachAction(id: string): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { discoverSessions } from '../lib/session/discover.js';
|
|
3
|
+
import { resumeSessionInPlace } from './sessions.js';
|
|
4
|
+
import { readDetachRecord, clearDetachRecord, isHeadlessAlive } from '../lib/session/detached.js';
|
|
5
|
+
export function registerAttachCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('attach')
|
|
8
|
+
.argument('<id>', 'Short or full id of the backgrounded/parked session to resume interactively')
|
|
9
|
+
.description('Bring a backgrounded agent to the foreground — resume it interactively here')
|
|
10
|
+
.action(async (id) => {
|
|
11
|
+
await attachAction(id);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export async function attachAction(id) {
|
|
15
|
+
const q = id.toLowerCase();
|
|
16
|
+
// Rich meta carries the pinned version + origin cwd the resume needs.
|
|
17
|
+
const metas = await discoverSessions({ all: true, since: '90d', limit: 2000 });
|
|
18
|
+
const meta = metas.find((m) => m.id === id) ?? metas.find((m) => m.id.toLowerCase().startsWith(q));
|
|
19
|
+
if (!meta) {
|
|
20
|
+
console.error(chalk.red(`No session matching "${id}".`));
|
|
21
|
+
console.error(chalk.gray(' See your sessions: agents sessions'));
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// If it's backgrounded, stop the headless continuation first so two processes
|
|
26
|
+
// don't resume the same transcript at once.
|
|
27
|
+
const rec = readDetachRecord(meta.id);
|
|
28
|
+
if (rec) {
|
|
29
|
+
if (isHeadlessAlive(rec)) {
|
|
30
|
+
try {
|
|
31
|
+
process.kill(rec.headlessPid, 'SIGTERM');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* already gone */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
clearDetachRecord(meta.id);
|
|
38
|
+
}
|
|
39
|
+
console.log(chalk.gray(`Attaching ${meta.agent} ${meta.id.slice(0, 8)} — resuming interactively…`));
|
|
40
|
+
await resumeSessionInPlace(meta);
|
|
41
|
+
}
|
|
@@ -644,8 +644,8 @@ function registerSetupCommand(program) {
|
|
|
644
644
|
console.log('');
|
|
645
645
|
console.log('Next steps:');
|
|
646
646
|
console.log(' 1. Grant TCC permissions (one-time):');
|
|
647
|
-
console.log(' System Settings > Privacy & Security > Accessibility — add Computer
|
|
648
|
-
console.log(' System Settings > Privacy & Security > Screen Recording — add Computer
|
|
647
|
+
console.log(' System Settings > Privacy & Security > Accessibility — add Agents Computer');
|
|
648
|
+
console.log(' System Settings > Privacy & Security > Screen Recording — add Agents Computer');
|
|
649
649
|
console.log(' 2. Whitelist the apps the daemon may drive. Add a YAML under ~/.agents/permissions/groups/:');
|
|
650
650
|
console.log(' name: computer');
|
|
651
651
|
console.log(' allow:');
|
|
@@ -687,7 +687,7 @@ function registerStartCommand(program) {
|
|
|
687
687
|
}
|
|
688
688
|
if (!trusted) {
|
|
689
689
|
console.log('');
|
|
690
|
-
console.log('Grant Accessibility + Screen Recording to Computer
|
|
690
|
+
console.log('Grant Accessibility + Screen Recording to Agents Computer, then run `agents computer start` again.');
|
|
691
691
|
}
|
|
692
692
|
});
|
|
693
693
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure core of `agents sessions detach` — the nudge, the resumed-run argv, and the id
|
|
3
|
+
* resolver. Kept import-light (only a type import) so it is unit-tested without
|
|
4
|
+
* loading the live-session discovery graph.
|
|
5
|
+
*/
|
|
6
|
+
import type { ActiveSession } from '../lib/session/active.js';
|
|
7
|
+
/**
|
|
8
|
+
* The prompt the backgrounded run resumes with. Its whole job is to stop a
|
|
9
|
+
* now-unwatched agent from stalling on a confirmation nobody will answer: it
|
|
10
|
+
* tells the agent it is headless and to drive to done, or to stop and state a
|
|
11
|
+
* blocker (which surfaces as a waiting session the user can `attach`).
|
|
12
|
+
*/
|
|
13
|
+
export declare const BACKGROUND_NUDGE: string;
|
|
14
|
+
/**
|
|
15
|
+
* Build the argv for the headless continuation `detach` spawns. Agent-agnostic
|
|
16
|
+
* and version-pinned by construction — it goes through the same `agents run
|
|
17
|
+
* --resume` path `attach` reverses.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildBackgroundArgv(agent: string, sessionId: string, cwd?: string): string[];
|
|
20
|
+
/** What to do with a resolved session, given which machine we're on. */
|
|
21
|
+
export type DetachTarget = {
|
|
22
|
+
kind: 'local';
|
|
23
|
+
sessionId: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'remote';
|
|
26
|
+
machine: string;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'refuse';
|
|
30
|
+
reason: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Decide how to detach a resolved session. Cloud and team sessions are refused
|
|
34
|
+
* (they have their own lifecycles); a session on another machine is delegated to
|
|
35
|
+
* that host over SSH — never stopped/resumed locally, since its pid and tmux
|
|
36
|
+
* socket only mean something where it actually runs. Pure, so every branch is
|
|
37
|
+
* unit-tested without a live pool. Mirrors `focus`/`jumpTo`'s remote branch.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveDetachTarget(s: ActiveSession, self: string): DetachTarget;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve `<id>` to exactly one live session by prefix. Mirrors `focus`'s match
|
|
42
|
+
* so the two verbs accept the same ids.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveOne(activeById: Map<string, ActiveSession>, id: string): ActiveSession | {
|
|
45
|
+
error: string;
|
|
46
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prompt the backgrounded run resumes with. Its whole job is to stop a
|
|
3
|
+
* now-unwatched agent from stalling on a confirmation nobody will answer: it
|
|
4
|
+
* tells the agent it is headless and to drive to done, or to stop and state a
|
|
5
|
+
* blocker (which surfaces as a waiting session the user can `attach`).
|
|
6
|
+
*/
|
|
7
|
+
export const BACKGROUND_NUDGE = "You've been sent to the background — nobody is watching this session now. " +
|
|
8
|
+
'Continue the current task and drive it to completion end-to-end. ' +
|
|
9
|
+
"Don't ask for confirmation; make the reasonable call and keep going. " +
|
|
10
|
+
'If you genuinely cannot proceed safely, stop and state the blocker plainly in one message.';
|
|
11
|
+
/**
|
|
12
|
+
* Build the argv for the headless continuation `detach` spawns. Agent-agnostic
|
|
13
|
+
* and version-pinned by construction — it goes through the same `agents run
|
|
14
|
+
* --resume` path `attach` reverses.
|
|
15
|
+
*/
|
|
16
|
+
export function buildBackgroundArgv(agent, sessionId, cwd) {
|
|
17
|
+
const argv = ['run', agent, BACKGROUND_NUDGE, '--resume', sessionId, '--headless'];
|
|
18
|
+
if (cwd)
|
|
19
|
+
argv.push('--cwd', cwd);
|
|
20
|
+
return argv;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Decide how to detach a resolved session. Cloud and team sessions are refused
|
|
24
|
+
* (they have their own lifecycles); a session on another machine is delegated to
|
|
25
|
+
* that host over SSH — never stopped/resumed locally, since its pid and tmux
|
|
26
|
+
* socket only mean something where it actually runs. Pure, so every branch is
|
|
27
|
+
* unit-tested without a live pool. Mirrors `focus`/`jumpTo`'s remote branch.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveDetachTarget(s, self) {
|
|
30
|
+
if (s.context === 'cloud') {
|
|
31
|
+
return { kind: 'refuse', reason: 'Cloud sessions run remotely and cannot be detached from here.' };
|
|
32
|
+
}
|
|
33
|
+
if (s.context === 'teams') {
|
|
34
|
+
return {
|
|
35
|
+
kind: 'refuse',
|
|
36
|
+
reason: 'That session is managed by its team — stop it with `agents teams`, not `detach`.',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const sessionId = s.sessionId ?? '';
|
|
40
|
+
if (!sessionId) {
|
|
41
|
+
return { kind: 'refuse', reason: 'That session has no id to resume, so it cannot be detached.' };
|
|
42
|
+
}
|
|
43
|
+
if (s.machine && s.machine !== self) {
|
|
44
|
+
return { kind: 'remote', machine: s.machine, sessionId };
|
|
45
|
+
}
|
|
46
|
+
return { kind: 'local', sessionId };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve `<id>` to exactly one live session by prefix. Mirrors `focus`'s match
|
|
50
|
+
* so the two verbs accept the same ids.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveOne(activeById, id) {
|
|
53
|
+
const q = id.toLowerCase();
|
|
54
|
+
const matches = [...activeById.values()].filter((s) => (s.sessionId ?? '').toLowerCase().startsWith(q));
|
|
55
|
+
if (matches.length === 0)
|
|
56
|
+
return { error: `No live session matching "${id}".` };
|
|
57
|
+
if (matches.length > 1) {
|
|
58
|
+
return { error: `"${id}" is ambiguous (${matches.length} live matches). Use more of the id.` };
|
|
59
|
+
}
|
|
60
|
+
return matches[0];
|
|
61
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents sessions detach <id>` — send a live agent session to the background:
|
|
3
|
+
* stop its interactive process and continue it headless, unattended, so it drives
|
|
4
|
+
* its task to completion without holding a terminal. Bring it back with
|
|
5
|
+
* `agents sessions attach`. A sibling of `sessions focus`/`resume` on the session
|
|
6
|
+
* lifecycle axis.
|
|
7
|
+
*
|
|
8
|
+
* Agent-agnostic and version-pinned by construction: it re-invokes
|
|
9
|
+
* `agents run <agent> "<nudge>" --resume <id> --headless`, which resolves the
|
|
10
|
+
* session's originating version and uses the agent's native resume (claude/codex)
|
|
11
|
+
* or a `/continue` replay (others) — the same path `attach` reverses.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { openSync, closeSync, mkdirSync } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import { gatherLiveTargets } from './go.js';
|
|
18
|
+
import { killSession } from '../lib/tmux/index.js';
|
|
19
|
+
import { getDefaultSocketPath } from '../lib/tmux/paths.js';
|
|
20
|
+
import { getAgentsInvocation } from '../lib/daemon.js';
|
|
21
|
+
import { captureProcessStartTime } from '../lib/pty-server.js';
|
|
22
|
+
import { writeDetachRecord } from '../lib/session/detached.js';
|
|
23
|
+
import { getLogsDir } from '../lib/state.js';
|
|
24
|
+
import { runOnPeer } from '../lib/session/remote-list.js';
|
|
25
|
+
import { buildBackgroundArgv, resolveDetachTarget, resolveOne } from './detach-core.js';
|
|
26
|
+
export function registerDetachCommand(program) {
|
|
27
|
+
program
|
|
28
|
+
.command('detach')
|
|
29
|
+
.argument('<id>', 'Short or full id of the live session to background')
|
|
30
|
+
.option('--local', 'Only this machine (skip the cross-host sweep)')
|
|
31
|
+
.description('Send a live agent to the background — stop its terminal, keep it working headless')
|
|
32
|
+
.action(async (id, opts) => {
|
|
33
|
+
await detachAction(id, opts);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Stop the interactive process so it can't fight the headless resume over the
|
|
38
|
+
* same transcript. tmux-hosted sessions are killed by session (closes the pane
|
|
39
|
+
* and ends the agent); a plain process is SIGTERM'd by pid. Either way we then
|
|
40
|
+
* wait for the process to actually exit before returning, so the headless
|
|
41
|
+
* continuation never starts writing the transcript while the old process is
|
|
42
|
+
* still flushing it.
|
|
43
|
+
*/
|
|
44
|
+
async function stopInteractive(s) {
|
|
45
|
+
if (s.tmuxTarget) {
|
|
46
|
+
const name = s.tmuxTarget.split(':')[0];
|
|
47
|
+
await killSession(name, getDefaultSocketPath());
|
|
48
|
+
}
|
|
49
|
+
else if (s.pid && s.pid > 0) {
|
|
50
|
+
try {
|
|
51
|
+
process.kill(s.pid, 'SIGTERM');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return; /* already gone — nothing to wait on */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (s.pid && s.pid > 0)
|
|
58
|
+
await waitForExit(s.pid);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Poll until `pid` is gone (SIGTERM already sent), escalating to SIGKILL past the
|
|
62
|
+
* deadline so a stuck process can't wedge the detach. Bounded so `detach` never
|
|
63
|
+
* hangs waiting on a process that ignores signals.
|
|
64
|
+
*/
|
|
65
|
+
async function waitForExit(pid, timeoutMs = 5000) {
|
|
66
|
+
const deadline = Date.now() + timeoutMs;
|
|
67
|
+
for (;;) {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return; /* gone */
|
|
73
|
+
}
|
|
74
|
+
if (Date.now() >= deadline) {
|
|
75
|
+
try {
|
|
76
|
+
process.kill(pid, 'SIGKILL');
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
/* raced to exit */
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Where a backgrounded continuation's stdout/stderr lands, by convention, so a
|
|
88
|
+
* crash after detach is debuggable — there is no terminal watching it, and
|
|
89
|
+
* presence only tells you the pid is gone, not whether the run finished or died.
|
|
90
|
+
*/
|
|
91
|
+
function backgroundLogPath(sessionId) {
|
|
92
|
+
return path.join(getLogsDir(), `detach-${sessionId.slice(0, 8)}.log`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Spawn the headless continuation detached, resolving to its pid for tracking.
|
|
96
|
+
* Its output is redirected to `logFile` rather than discarded, so a fast-failing
|
|
97
|
+
* background run leaves a trail.
|
|
98
|
+
*/
|
|
99
|
+
function spawnHeadless(command, args, logFile) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
mkdirSync(path.dirname(logFile), { recursive: true });
|
|
102
|
+
const fd = openSync(logFile, 'a');
|
|
103
|
+
const child = spawn(command, args, { detached: true, stdio: ['ignore', fd, fd], env: process.env });
|
|
104
|
+
child.once('spawn', () => {
|
|
105
|
+
const pid = child.pid ?? 0;
|
|
106
|
+
child.unref();
|
|
107
|
+
try {
|
|
108
|
+
closeSync(fd);
|
|
109
|
+
}
|
|
110
|
+
catch { /* fd handed to the child */ }
|
|
111
|
+
resolve(pid);
|
|
112
|
+
});
|
|
113
|
+
child.once('error', (err) => {
|
|
114
|
+
try {
|
|
115
|
+
closeSync(fd);
|
|
116
|
+
}
|
|
117
|
+
catch { /* never opened for the child */ }
|
|
118
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export async function detachAction(id, opts = {}) {
|
|
123
|
+
const { self, activeById } = await gatherLiveTargets(!!opts.local, { includeCloud: true });
|
|
124
|
+
const resolved = resolveOne(activeById, id);
|
|
125
|
+
if ('error' in resolved) {
|
|
126
|
+
console.error(chalk.red(resolved.error));
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const s = resolved;
|
|
131
|
+
const target = resolveDetachTarget(s, self);
|
|
132
|
+
// Cloud, team, and id-less sessions can't be backgrounded from here.
|
|
133
|
+
if (target.kind === 'refuse') {
|
|
134
|
+
console.error(chalk.red(target.reason));
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const short = target.sessionId.slice(0, 8);
|
|
139
|
+
// A session on another host: its pid and tmux socket only mean something where
|
|
140
|
+
// it runs, so detach it *there* over SSH — never kill/resume locally. Mirrors
|
|
141
|
+
// focus/jumpTo's remote branch.
|
|
142
|
+
if (target.kind === 'remote') {
|
|
143
|
+
console.log(chalk.gray(`${short} lives on ${target.machine} — detaching it there over SSH…`));
|
|
144
|
+
const rc = await runOnPeer(['sessions', 'detach', target.sessionId, '--local'], target.machine);
|
|
145
|
+
if (rc === 'no-target') {
|
|
146
|
+
console.error(chalk.red(`Can't reach ${target.machine} to detach ${short}.`));
|
|
147
|
+
process.exitCode = 1;
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Local: stop the interactive process, then continue it headless, detached —
|
|
152
|
+
// version-pinned via the existing `agents run --resume` path.
|
|
153
|
+
const sessionId = target.sessionId;
|
|
154
|
+
const agent = s.kind;
|
|
155
|
+
await stopInteractive(s);
|
|
156
|
+
const logFile = backgroundLogPath(sessionId);
|
|
157
|
+
const inv = getAgentsInvocation(buildBackgroundArgv(agent, sessionId, s.cwd));
|
|
158
|
+
const pid = await spawnHeadless(inv.command, inv.args, logFile);
|
|
159
|
+
// Record it so `attach` and `agents ls --active` know it's backgrounded.
|
|
160
|
+
writeDetachRecord({
|
|
161
|
+
sessionId,
|
|
162
|
+
agent,
|
|
163
|
+
cwd: s.cwd,
|
|
164
|
+
headlessPid: pid,
|
|
165
|
+
headlessStartTime: captureProcessStartTime(pid),
|
|
166
|
+
detachedAtMs: Date.now(),
|
|
167
|
+
});
|
|
168
|
+
console.log(chalk.green(`◒ Backgrounded ${agent} ${short}`) +
|
|
169
|
+
chalk.gray(` — running headless (pid ${pid}). Bring it back: agents sessions attach ${short}`));
|
|
170
|
+
console.log(chalk.gray(` logs: ${logFile}`));
|
|
171
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { IsolationBoundaryError } from '../lib/shims.js';
|
|
2
|
+
import { explainIsolationBoundary } from '../lib/isolation-boundary-report.js';
|
|
1
3
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
2
4
|
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
3
5
|
import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
|
|
@@ -20,6 +22,7 @@ import { listCliStatus } from '../lib/cli-resources.js';
|
|
|
20
22
|
import { setHelpSections } from '../lib/help.js';
|
|
21
23
|
import { heal, healChangedAnything } from '../lib/heal.js';
|
|
22
24
|
import { blocksLocalScripts, getEffectiveExecutionPolicy } from '../lib/platform/winpath.js';
|
|
25
|
+
import { scanUserRcFiles, rcSecretWarningLines } from '../lib/secrets/rc-hygiene.js';
|
|
23
26
|
import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
|
|
24
27
|
import * as fs from 'fs';
|
|
25
28
|
const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
|
|
@@ -175,6 +178,10 @@ function renderOverviewText(clis, syncRows, orphanRows, hostClis, signIn) {
|
|
|
175
178
|
// generated `agents.ps1` launcher — postinstall diagnoses it interactively,
|
|
176
179
|
// but a non-interactive install never sees that. Surface it here too.
|
|
177
180
|
renderExecPolicyAdvisory();
|
|
181
|
+
// Credentials exported from a shell rc file leak into every process's
|
|
182
|
+
// environment (readable via /proc/<pid>/environ) — the RUSH-1968 class. Flag
|
|
183
|
+
// them here so the master passphrase never silently lives in `~/.zshenv` again.
|
|
184
|
+
renderRcHygieneAdvisory();
|
|
178
185
|
}
|
|
179
186
|
// ─── windows execution-policy advisory ─────────────────────────────────────────
|
|
180
187
|
/**
|
|
@@ -197,6 +204,19 @@ export function execPolicyWarningLines(platform, policy) {
|
|
|
197
204
|
'The agents.cmd shim still works regardless of the policy.',
|
|
198
205
|
];
|
|
199
206
|
}
|
|
207
|
+
function renderRcHygieneAdvisory() {
|
|
208
|
+
const findings = scanUserRcFiles();
|
|
209
|
+
const lines = rcSecretWarningLines(findings);
|
|
210
|
+
if (lines.length === 0)
|
|
211
|
+
return;
|
|
212
|
+
console.log();
|
|
213
|
+
console.log(chalk.bold('Secrets in shell config'));
|
|
214
|
+
const [headline, ...rest] = lines;
|
|
215
|
+
console.log(` ${chalk.yellow('warn ')} ${headline}`);
|
|
216
|
+
for (const line of rest) {
|
|
217
|
+
console.log(chalk.gray(` ${line}`));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
200
220
|
function renderExecPolicyAdvisory() {
|
|
201
221
|
// Only probe the policy on Windows — getEffectiveExecutionPolicy() spawns
|
|
202
222
|
// powershell, which is a wasted (doomed) process on POSIX where the advisory
|
|
@@ -690,7 +710,17 @@ export function registerDoctorCommand(program) {
|
|
|
690
710
|
// adoptShadowingLauncher resolves the launcher itself (PATH shadow, then
|
|
691
711
|
// the durable ~/.local/bin symlink), so it forces the take-over even when
|
|
692
712
|
// this shell's PATH already has the shim first.
|
|
693
|
-
|
|
713
|
+
let result;
|
|
714
|
+
try {
|
|
715
|
+
result = adoptShadowingLauncher(agent);
|
|
716
|
+
}
|
|
717
|
+
catch (err) {
|
|
718
|
+
if (err instanceof IsolationBoundaryError) {
|
|
719
|
+
explainIsolationBoundary(err);
|
|
720
|
+
process.exit(1);
|
|
721
|
+
}
|
|
722
|
+
throw err;
|
|
723
|
+
}
|
|
694
724
|
if (result.adopted) {
|
|
695
725
|
console.log(chalk.green(`Adopted ${AGENTS[agent].cliCommand} launcher (${result.launcher} -> shim). Original recorded for --release; version management now wins regardless of PATH order.`));
|
|
696
726
|
}
|