@phnx-labs/agents-cli 1.20.49 → 1.20.50
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 +7 -0
- package/README.md +3 -0
- package/dist/commands/doctor.js +133 -2
- package/dist/commands/teams.js +217 -70
- package/dist/lib/exec.js +12 -4
- package/dist/lib/hosts/passthrough.js +30 -1
- package/dist/lib/hosts/progress.d.ts +31 -0
- package/dist/lib/hosts/progress.js +35 -0
- package/dist/lib/hosts/remote-cmd.d.ts +1 -1
- package/dist/lib/hosts/remote-cmd.js +13 -3
- package/dist/lib/shims.d.ts +17 -0
- package/dist/lib/shims.js +29 -0
- package/dist/lib/teams/agents.d.ts +108 -1
- package/dist/lib/teams/agents.js +511 -11
- package/dist/lib/teams/api.d.ts +7 -1
- package/dist/lib/teams/api.js +5 -2
- package/dist/lib/teams/registry.d.ts +17 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/remoteWorktree.d.ts +57 -0
- package/dist/lib/teams/remoteWorktree.js +213 -0
- package/dist/lib/teams/scheduler.d.ts +29 -0
- package/dist/lib/teams/scheduler.js +78 -0
- package/dist/lib/teams/supervisor.js +7 -0
- package/dist/lib/versions.d.ts +7 -0
- package/dist/lib/versions.js +42 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.20.50
|
|
6
|
+
|
|
7
|
+
- **Distributed agent teams: teammates can now run on different machines across your fleet, not just the box running `teams start`.** A single team can place the backend teammate on a Linux box and the UI teammate on a Mac while one orchestrator still drives the DAG, polls status, and cleans up. One vocabulary, all optional (omit it and teams stay 100% local as before): `teams create --devices a,b,c` (alias `--hosts`) declares a pool the team may auto-schedule onto, `--repo <url|path>` (defaults to the local checkout's `origin`) says how each device gets the code, and `teams add --device X` (alias `--host`) pins one teammate to a host — which needs **no** pool, so "send just one teammate elsewhere" is zero-setup. Placement resolves top-down at launch: explicit `--device` pin → single-device pool (whole team there) → multi-device pool (least-loaded auto-schedule) → local. Remote teammates dispatch over SSH via the existing `agents devices`/host machinery (a third teammate backend beside local and cloud), are monitored by offset-tailing the remote log + `.exit` sentinel, and get the repo auto-provisioned per device (reuse an existing checkout, else clone into `~/.agents/repos/<team>`) with an optional per-teammate git worktree on the host. `teams status`/`teams logs` show each teammate's host and stream its output back with the local mirror capped (~512KB rolling tail) so a 10+-teammate fleet can't blow up the orchestrator. POSIX hosts only in v1 (Windows rejected with a clear message). Source: `apps/cli/src/lib/teams/{scheduler,remoteWorktree,agents,api,supervisor,registry}.ts`, `apps/cli/src/lib/hosts/{progress,passthrough}.ts`, `apps/cli/src/commands/teams.ts`, `apps/cli/docs/teams.md`.
|
|
8
|
+
- **NEW: `agents doctor --devices` shows a cross-device agent-readiness matrix.** `agents doctor` could already run on one remote machine via `--host`, but checking the whole fleet meant running the command once per box. `--devices` fans out `agents teams doctor --json` to every registered device (plus the local machine), renders a device × agent matrix, and emits a stable JSON contract with `--json`. `--device <name>` or `--host <name>` scopes the same matrix to a single machine. The remote probe now bootstraps `PATH` with the canonical shim directories before running, so login shells that haven't sourced interactive rc files no longer report false "not installed" negatives. Source: `apps/cli/src/commands/doctor.ts`, `apps/cli/src/lib/teams/agents.ts`, `apps/cli/src/lib/hosts/{passthrough,remote-cmd}.ts`.
|
|
9
|
+
- **`agents run codex` / `agents teams` now honor your configured Codex model instead of silently defaulting to `gpt-5.3-codex`.** Codex runs under a per-version `CODEX_HOME`, and your `model` preference (`~/.codex/config.toml`) lives only in the version-home that was active when you set it. A dispatch pinned to a different version read a home with no top-level `model`, so Codex fell back to its built-in default — which a ChatGPT-tier account isn't entitled to use, so the run died with `400: The 'gpt-5.3-codex' model is not supported when using Codex with a ChatGPT account` before doing any work, even though `ag view` reported Codex "signed in". When no explicit `--model` is passed, the model is now defaulted (for Codex) to the top-level `model` in your active `~/.codex/config.toml` and forwarded via `--model`; it's read-only (no file writes), so fanning out many parallel runs to one version-home can't race. Verified live on a box where Codex was 100% unusable: the request model changed `gpt-5.3-codex` → `gpt-5.5` and codex@0.142.0 returned successfully. Source: `apps/cli/src/lib/exec.ts`, `apps/cli/src/lib/shims.ts` (`readCodexConfiguredModel`).
|
|
10
|
+
- **Fix: `agents add claude@<version>` now produces a runnable install — it no longer ships a half-built binary that dies with "claude native binary not installed."** `installVersion` runs `npm install --ignore-scripts` (the right posture for the dependency *tree* — never run arbitrary transitive postinstalls), but that also skipped the agent package's OWN postinstall, which for `@anthropic-ai/claude-code` is a required step: the package ships a ~500-byte stub at `bin/claude.exe` plus per-arch native binaries as optional deps, and its `postinstall` (`node install.cjs`) is what copies the correct ~231 MB native binary over the stub. Skipped, every launch died with `Error: claude native binary not installed`. The existing launch-health self-heal (#764, and its Windows/daemon extension) couldn't save it on two counts: the stub reports its breakage *politely* rather than with a raw `ENOENT`, so the probe's missing-binary signature didn't match and the gutted install read as healthy; and the repair path (`ensureAgentRunnable` → clean reinstall) re-ran the same `--ignore-scripts` install, so it never copied the binary either. `installVersion` now runs the **first-party** package's declared `postinstall` after the npm install (scoped to that one package — never the dependency tree, never claude-code's `exit 1` `prepare` guard), best-effort, before the integrity gate. Because `installVersion` is the single choke point for `agents add`, config refresh, run-time heal, and the daemon's proactive heal, this also revives the repair path for the whole class. `isMissingBinarySignature` was additionally widened to recognize the stub's polite phrases (`native binary not installed`, `postinstall did not run`, `optional dependency was not downloaded`) so the self-heal catches this failure mode if a postinstall ever silently no-ops. Verified end-to-end on linux-arm64: `installVersion('claude','2.1.186')` into a clean HOME runs the postinstall automatically, lands the 231,782,112-byte binary (not the stub), and `claude.exe --version` returns `2.1.186 (Claude Code)` — with no manual `install.cjs` step. Source: `apps/cli/src/lib/versions.ts` (`installVersion`, `isMissingBinarySignature`).
|
|
11
|
+
|
|
5
12
|
## 1.20.49
|
|
6
13
|
|
|
7
14
|
- **`agents run --mode plan` no longer hard-fails on agents without a read-only mode (antigravity, cursor, kiro, …).** Those agents have no plan flag, so an explicit or default `--mode plan` used to abort with `does not support 'plan' mode` — breaking multi-agent scripts that pass a uniform plan flag, and diverging from `agents teams add` (default mode `edit`). `resolveMode` now degrades unsupported `plan` to the agent's safest native mode (`capabilities.modes[0]`, typically `edit`), matching the existing `auto` → `edit` degrade. The CLI prints a yellow warning when the user explicitly asked for plan (gray for the implicit default) so the elevation is never silent. `skip` still hard-fails when unsupported. Source: `apps/cli/src/lib/exec.ts`, `apps/cli/src/commands/exec.ts`.
|
package/README.md
CHANGED
|
@@ -307,6 +307,9 @@ agents logs <id> --full # the full raw transcript / stdout (toke
|
|
|
307
307
|
agents logs <id> -f # re-attach to a running one and follow
|
|
308
308
|
agents view claude --host gpu-box # inspect the remote install
|
|
309
309
|
agents sync --host gpu-box # make the remote machine current
|
|
310
|
+
agents doctor --devices # readiness matrix for every registered device
|
|
311
|
+
agents doctor --devices --json # machine-readable fleet readiness
|
|
312
|
+
agents doctor --device mac-mini # same matrix, scoped to one device
|
|
310
313
|
|
|
311
314
|
# Your Tailscale fleet, auto-discovered
|
|
312
315
|
agents devices sync # ingest `tailscale status`
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
2
|
+
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
3
|
+
import { loadDevices } from '../lib/devices/registry.js';
|
|
4
|
+
import { resolveHost } from '../lib/hosts/registry.js';
|
|
5
|
+
import { sshExec } from '../lib/ssh-exec.js';
|
|
6
|
+
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
7
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
2
8
|
import chalk from 'chalk';
|
|
3
|
-
import { checkAllClis } from '../lib/teams/agents.js';
|
|
9
|
+
import { checkAllClis, collectTeamsDoctorData } from '../lib/teams/agents.js';
|
|
4
10
|
import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError } from '../lib/agents.js';
|
|
5
11
|
import { getGlobalDefault, getVersionHomePath, isVersionInstalled, listInstalledVersions, parseAgentSpec, } from '../lib/versions.js';
|
|
6
12
|
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
@@ -203,6 +209,121 @@ function renderExecPolicyAdvisory() {
|
|
|
203
209
|
console.log(chalk.gray(` ${line}`));
|
|
204
210
|
}
|
|
205
211
|
}
|
|
212
|
+
const AGENT_ORDER = ['claude', 'codex', 'kimi', 'grok', 'antigravity', 'opencode', 'cursor', 'gemini', 'droid'];
|
|
213
|
+
function shortAgentHeader(name) {
|
|
214
|
+
return name.slice(0, 4).padEnd(4);
|
|
215
|
+
}
|
|
216
|
+
function agentCell(entry) {
|
|
217
|
+
if (!entry)
|
|
218
|
+
return chalk.gray('- ');
|
|
219
|
+
if (entry.installed) {
|
|
220
|
+
const signedInHint = entry.signedIn ? '*' : ' ';
|
|
221
|
+
return chalk.green(`rdy${signedInHint}`.padEnd(4));
|
|
222
|
+
}
|
|
223
|
+
if (entry.error)
|
|
224
|
+
return chalk.red('err '.padEnd(4));
|
|
225
|
+
return chalk.gray('no '.padEnd(4));
|
|
226
|
+
}
|
|
227
|
+
async function resolveFleetTargets(opts) {
|
|
228
|
+
const singleName = opts.host || opts.device;
|
|
229
|
+
if (singleName) {
|
|
230
|
+
// --device / --host as a single-device filter: resolve through the device
|
|
231
|
+
// registry first, then the general host registry, then ad-hoc user@host.
|
|
232
|
+
const registry = await loadDevices();
|
|
233
|
+
const deviceProfile = registry[singleName];
|
|
234
|
+
if (deviceProfile) {
|
|
235
|
+
return [{
|
|
236
|
+
name: deviceProfile.name,
|
|
237
|
+
sshTarget: deviceProfile.name,
|
|
238
|
+
os: deviceProfile.platform !== 'unknown' ? deviceProfile.platform : undefined,
|
|
239
|
+
}];
|
|
240
|
+
}
|
|
241
|
+
const host = await resolveHost(singleName);
|
|
242
|
+
if (host) {
|
|
243
|
+
return [{ name: singleName, sshTarget: sshTargetFor(host), os: host.os }];
|
|
244
|
+
}
|
|
245
|
+
console.error(chalk.red(`Unknown host or device '${singleName}'.`));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
const registry = await loadDevices();
|
|
249
|
+
const localName = machineId();
|
|
250
|
+
return Object.values(registry)
|
|
251
|
+
.filter((d) => d.name.toLowerCase() !== localName)
|
|
252
|
+
.map((d) => ({
|
|
253
|
+
name: d.name,
|
|
254
|
+
sshTarget: d.name,
|
|
255
|
+
os: d.platform !== 'unknown' ? d.platform : undefined,
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
async function probeFleetTarget(target) {
|
|
259
|
+
const forwarded = ['teams', 'doctor', '--json'];
|
|
260
|
+
const isWin = /^win/i.test((target.os ?? '').trim());
|
|
261
|
+
const remoteCmd = buildRemoteAgentsInvocation(forwarded, undefined, isWin ? 'windows' : undefined,
|
|
262
|
+
// POSIX login shells often lack the shims dir; Windows PowerShell usually
|
|
263
|
+
// has it via the install profile, and our single-quote escaping would
|
|
264
|
+
// prevent $HOME expansion there, so skip the bootstrap on Windows.
|
|
265
|
+
isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
|
|
266
|
+
const res = sshExec(target.sshTarget, remoteCmd, { timeoutMs: 30000, multiplex: true });
|
|
267
|
+
if (res.code !== 0) {
|
|
268
|
+
return {
|
|
269
|
+
name: target.name,
|
|
270
|
+
online: false,
|
|
271
|
+
error: res.timedOut ? 'timed out' : (res.stderr || `exit ${res.code ?? 'unknown'}`),
|
|
272
|
+
agents: {},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const agents = JSON.parse(res.stdout);
|
|
277
|
+
return { name: target.name, online: true, agents };
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
const stderrHint = res.stderr ? ` stderr: ${res.stderr.trim()}` : '';
|
|
281
|
+
return {
|
|
282
|
+
name: target.name,
|
|
283
|
+
online: true,
|
|
284
|
+
error: `invalid JSON (${err?.message ?? 'parse error'})${stderrHint}`,
|
|
285
|
+
agents: {},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function runDevicesDoctor(opts) {
|
|
290
|
+
const singleName = opts.host || opts.device;
|
|
291
|
+
const targets = await resolveFleetTargets(opts);
|
|
292
|
+
const localName = machineId();
|
|
293
|
+
const results = [];
|
|
294
|
+
// Local machine first, directly.
|
|
295
|
+
if (!singleName) {
|
|
296
|
+
results.push({ name: localName, online: true, agents: await collectTeamsDoctorData() });
|
|
297
|
+
}
|
|
298
|
+
// Remote targets in parallel.
|
|
299
|
+
const remoteResults = await Promise.all(targets.map(probeFleetTarget));
|
|
300
|
+
results.push(...remoteResults);
|
|
301
|
+
if (opts.json) {
|
|
302
|
+
console.log(JSON.stringify({ devices: results }, null, 2));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (results.length === 0) {
|
|
306
|
+
console.log(chalk.gray('No registered devices. Run `agents devices` to register some.'));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const agentsToShow = AGENT_ORDER.filter((a) => results.some((r) => r.agents[a] !== undefined));
|
|
310
|
+
console.log(chalk.bold('Agent readiness by device'));
|
|
311
|
+
if (agentsToShow.length === 0) {
|
|
312
|
+
console.log(chalk.gray(' (no agent data collected)'));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const nameWidth = Math.max(...results.map((r) => r.name.length));
|
|
316
|
+
const header = ` ${'Device'.padEnd(nameWidth)} ${agentsToShow.map(shortAgentHeader).join(' ')}`;
|
|
317
|
+
console.log(chalk.gray(header));
|
|
318
|
+
for (const row of results) {
|
|
319
|
+
const status = row.online ? chalk.green('online ') : chalk.red('offline');
|
|
320
|
+
const errorSuffix = row.error ? ` ${chalk.gray(row.error)}` : '';
|
|
321
|
+
const cells = agentsToShow.map((a) => agentCell(row.agents[a])).join(' ');
|
|
322
|
+
console.log(` ${row.name.padEnd(nameWidth)} ${status} ${cells}${errorSuffix}`);
|
|
323
|
+
}
|
|
324
|
+
console.log();
|
|
325
|
+
console.log(chalk.gray(' rdy* = installed and signed in · rdy = installed · no = not installed · err = probe failed · - = offline'));
|
|
326
|
+
}
|
|
206
327
|
function parseTargetArg(arg) {
|
|
207
328
|
const at = arg.indexOf('@');
|
|
208
329
|
const agentPart = at === -1 ? arg : arg.slice(0, at);
|
|
@@ -484,7 +605,9 @@ export function registerDoctorCommand(program) {
|
|
|
484
605
|
.option('--kind <kinds>', 'Restrict to comma-separated resource kinds (commands,skills,hooks,rules,mcp,permissions,subagents,plugins,promptcuts)')
|
|
485
606
|
.option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
|
|
486
607
|
.option('--adopt <agent>', "Take over the agent's native launcher that shadows the shim (symlink it to the version-managed shim; reversible with --release)")
|
|
487
|
-
.option('--release <agent>', 'Undo --adopt: restore the native launcher agents-cli previously adopted')
|
|
608
|
+
.option('--release <agent>', 'Undo --adopt: restore the native launcher agents-cli previously adopted')
|
|
609
|
+
.option('--devices', 'Check agent readiness on every registered device (alias --hosts)')
|
|
610
|
+
.option('--hosts', 'Alias of --devices');
|
|
488
611
|
setHelpSections(doctorCmd, {
|
|
489
612
|
examples: `
|
|
490
613
|
# Overview: CLI availability + sync status + orphans across all defaults
|
|
@@ -511,6 +634,14 @@ export function registerDoctorCommand(program) {
|
|
|
511
634
|
});
|
|
512
635
|
doctorCmd.action(async (target, opts) => {
|
|
513
636
|
const cwd = opts.cwd ? opts.cwd : process.cwd();
|
|
637
|
+
if (opts.devices || opts.hosts) {
|
|
638
|
+
if (target) {
|
|
639
|
+
console.error(chalk.red('Cannot combine --devices with a target argument.'));
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
await runDevicesDoctor(opts);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
514
645
|
// Launcher adoption escape hatch. `--adopt <agent>` forces the take-over
|
|
515
646
|
// even for a non-default agent; `--release <agent>` reverses it.
|
|
516
647
|
if (opts.adopt || opts.release) {
|
package/dist/commands/teams.js
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import * as fs from 'fs/promises';
|
|
3
3
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
-
import { AgentManager,
|
|
5
|
+
import { AgentManager, checkCliSignedIn, collectTeamsDoctorData, getAgentsDir, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
|
|
6
6
|
import { resolveProvider } from '../lib/cloud/registry.js';
|
|
7
7
|
import { runSupervisor } from '../lib/teams/supervisor.js';
|
|
8
8
|
import { debug } from '../lib/teams/debug.js';
|
|
@@ -11,7 +11,15 @@ import { handleSpawn, handleStatus, handleStop, handleTasks, toTaskStatusSummary
|
|
|
11
11
|
import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } from '../lib/teams/registry.js';
|
|
12
12
|
import { setHelpSections } from '../lib/help.js';
|
|
13
13
|
import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
|
|
14
|
-
import {
|
|
14
|
+
import { resolveHost } from '../lib/hosts/registry.js';
|
|
15
|
+
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
16
|
+
import { ensureHostReady } from '../lib/hosts/ready.js';
|
|
17
|
+
import { remoteShellFor } from '../lib/hosts/remote-cmd.js';
|
|
18
|
+
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
19
|
+
import { remoteWorktreeDirty, removeRemoteWorktree, ensureRemoteRepo } from '../lib/teams/remoteWorktree.js';
|
|
20
|
+
import { getRemoteUrl } from '../lib/git.js';
|
|
21
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
22
|
+
import { isVersionInstalled, resolveVersionAlias, resolveVersionAliasLoose } from '../lib/versions.js';
|
|
15
23
|
import { AGENTS, warnAgentDeprecated } from '../lib/agents.js';
|
|
16
24
|
import { discoverSessions, parseTimeFilter, resolveSessionById } from '../lib/session/discover.js';
|
|
17
25
|
import { renderSessionLog } from './sessions.js';
|
|
@@ -486,6 +494,9 @@ function printAgentDetail(a, session) {
|
|
|
486
494
|
if (a.after && a.after.length) {
|
|
487
495
|
console.log(` ${chalk.gray('after ')} ${a.after.join(', ')}`);
|
|
488
496
|
}
|
|
497
|
+
if (a.host) {
|
|
498
|
+
console.log(` ${chalk.gray('host ')} ${chalk.cyan(a.host)}`);
|
|
499
|
+
}
|
|
489
500
|
// If the agent's internal session id differs from ours (non-Claude), show
|
|
490
501
|
// it as a hint for `agents sessions <id>`.
|
|
491
502
|
if (a.remote_session_id && a.remote_session_id !== a.agent_id) {
|
|
@@ -569,7 +580,8 @@ function printAgentSummary(s) {
|
|
|
569
580
|
const duration = s.duration ? `${chalk.gray(' · ')}${chalk.white(s.duration)}` : '';
|
|
570
581
|
const errBadge = s.has_errors ? chalk.red(' !') : '';
|
|
571
582
|
const tools = chalk.gray(` · ${s.tool_count} tools`);
|
|
572
|
-
|
|
583
|
+
const hostBadge = s.host ? chalk.gray(' · on ') + chalk.cyan(s.host) : '';
|
|
584
|
+
console.log(` ${chalk.cyan(handle.padEnd(14))} ${ident.padEnd(11)} ${label}${duration}${tools}${hostBadge}${errBadge}`);
|
|
573
585
|
// Files: counts + basenames. Read is count only.
|
|
574
586
|
const fileLines = [];
|
|
575
587
|
const renderCat = (label, cat) => {
|
|
@@ -965,13 +977,55 @@ export function registerTeamsCommands(program) {
|
|
|
965
977
|
.option('-d, --description <text>', 'One-line summary of what this team is working on')
|
|
966
978
|
.option('--enable-worktrees', 'Each teammate works in its own git worktree (requires --worktree on add)')
|
|
967
979
|
.option('--use-worktree <path>', 'All teammates share this existing worktree path (mutually exclusive with --enable-worktrees)')
|
|
980
|
+
.option('--devices <list>', 'Pool of machines this team may run teammates on (comma-separated). Enables distributed auto-scheduling.')
|
|
981
|
+
.option('--hosts <list>', 'Alias for --devices.')
|
|
982
|
+
.option('--repo <urlOrPath>', 'How each device gets the code (git URL to clone, or a path). Defaults to the local checkout origin.')
|
|
968
983
|
.option('--json', 'Output machine-readable JSON')
|
|
969
984
|
.action(async (team, opts) => {
|
|
970
985
|
try {
|
|
986
|
+
// --devices / --hosts are aliases; commander can't express a two-name
|
|
987
|
+
// option that isn't a short flag, so merge them here. Split on comma,
|
|
988
|
+
// trim, drop blanks, dedupe (preserving first-seen order).
|
|
989
|
+
const rawPool = [opts.devices, opts.hosts].filter(Boolean).join(',');
|
|
990
|
+
const devices = [];
|
|
991
|
+
for (const d of rawPool.split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
992
|
+
if (!devices.includes(d))
|
|
993
|
+
devices.push(d);
|
|
994
|
+
}
|
|
995
|
+
// Validate every pooled device resolves + is POSIX (v1 remote monitor is
|
|
996
|
+
// POSIX-only). A device equal to the local machine is fine (runs local),
|
|
997
|
+
// so skip the resolve/POSIX check for it.
|
|
998
|
+
for (const name of devices) {
|
|
999
|
+
if (name.toLowerCase() === machineId())
|
|
1000
|
+
continue;
|
|
1001
|
+
const host = await resolveHost(name);
|
|
1002
|
+
if (!host) {
|
|
1003
|
+
die(`Couldn't resolve pool device "${name}". Register it with \`agents devices\`, ` +
|
|
1004
|
+
`enroll it with \`agents hosts add ${name}\`, or pass user@host.`);
|
|
1005
|
+
}
|
|
1006
|
+
if (remoteShellFor(host.os ?? resolveRemoteOsSync(host.name)) === 'powershell') {
|
|
1007
|
+
die(`Distributed teams on Windows device "${host.name}" are not supported yet — ` +
|
|
1008
|
+
`the teams remote monitor is POSIX-only. Use a Linux/macOS device.`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
// --repo: how each device gets the code. Default to the local checkout's
|
|
1012
|
+
// origin when a pool is declared and we're inside a git repo, so the user
|
|
1013
|
+
// never hand-manages a path per box. A poolless team leaves repo unset.
|
|
1014
|
+
let repo = opts.repo;
|
|
1015
|
+
if (!repo && devices.length > 0) {
|
|
1016
|
+
const cwd = process.cwd();
|
|
1017
|
+
if (await isGitRepo(cwd)) {
|
|
1018
|
+
const origin = await getRemoteUrl(cwd);
|
|
1019
|
+
if (origin)
|
|
1020
|
+
repo = origin;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
971
1023
|
const meta = await createTeam(team, {
|
|
972
1024
|
description: opts.description,
|
|
973
1025
|
enableWorktrees: opts.enableWorktrees,
|
|
974
1026
|
useWorktree: opts.useWorktree,
|
|
1027
|
+
devices,
|
|
1028
|
+
repo,
|
|
975
1029
|
});
|
|
976
1030
|
if (isJsonMode(opts)) {
|
|
977
1031
|
console.log(JSON.stringify({ team, ...meta }, null, 2));
|
|
@@ -984,6 +1038,10 @@ export function registerTeamsCommands(program) {
|
|
|
984
1038
|
console.log(chalk.gray(` worktrees: per-teammate`));
|
|
985
1039
|
if (meta.use_worktree)
|
|
986
1040
|
console.log(chalk.gray(` worktree: ${meta.use_worktree}`));
|
|
1041
|
+
if (meta.devices && meta.devices.length)
|
|
1042
|
+
console.log(chalk.gray(` devices: ${meta.devices.join(', ')}`));
|
|
1043
|
+
if (meta.repo)
|
|
1044
|
+
console.log(chalk.gray(` repo: ${meta.repo}`));
|
|
987
1045
|
console.log();
|
|
988
1046
|
console.log(chalk.gray('Add your first teammate:'));
|
|
989
1047
|
if (meta.enable_worktrees) {
|
|
@@ -1039,9 +1097,88 @@ export function registerTeamsCommands(program) {
|
|
|
1039
1097
|
die(`--cloud rush requires --repo <owner/repo>`);
|
|
1040
1098
|
}
|
|
1041
1099
|
}
|
|
1100
|
+
// Auto-create the team if it doesn't exist yet (friendlier UX than erroring),
|
|
1101
|
+
// then load its metadata — needed here for the distributed --repo (how each
|
|
1102
|
+
// device gets the code) before we resolve a per-teammate --device pin.
|
|
1103
|
+
await ensureTeam(team);
|
|
1104
|
+
const teamMeta = await getTeam(team);
|
|
1105
|
+
// `--device`/`--host` are aliases (addHostOption registers both). For `teams
|
|
1106
|
+
// add` the passthrough special-cases them as PLACEMENT, not routing, so the
|
|
1107
|
+
// local action reads them here. Reject a conflicting pair.
|
|
1108
|
+
const explicitDevice = (() => {
|
|
1109
|
+
const h = opts.host;
|
|
1110
|
+
const d = opts.device;
|
|
1111
|
+
if (h && d && h !== d) {
|
|
1112
|
+
die('Conflicting --host/--device values — pass just one.');
|
|
1113
|
+
}
|
|
1114
|
+
return h ?? d ?? null;
|
|
1115
|
+
})();
|
|
1116
|
+
// Distributed teams: --device <name> PINS this teammate to a machine over
|
|
1117
|
+
// SSH. Resolve + validate the placement here so a bad target fails at `add`
|
|
1118
|
+
// time, not silently at launch. Persisted (hostName/hostTarget/repoPath) so
|
|
1119
|
+
// startReady()/launchRemoteProcess dispatch over SSH. Unpinned teammates
|
|
1120
|
+
// leave these null — the launch-time scheduler resolves the pool cascade.
|
|
1121
|
+
let hostName = null;
|
|
1122
|
+
let hostTarget = null;
|
|
1123
|
+
let hostRepoPath = null;
|
|
1124
|
+
if (explicitDevice && explicitDevice.toLowerCase() !== machineId()) {
|
|
1125
|
+
if (cloudProviderId) {
|
|
1126
|
+
die(`--device and --cloud are mutually exclusive (two different remote backends). Pick one.`);
|
|
1127
|
+
}
|
|
1128
|
+
const host = await resolveHost(explicitDevice);
|
|
1129
|
+
if (!host) {
|
|
1130
|
+
die(`Couldn't resolve --device "${explicitDevice}". Register it with \`agents devices\`, ` +
|
|
1131
|
+
`enroll it with \`agents hosts add ${explicitDevice}\`, or pass user@host.`);
|
|
1132
|
+
}
|
|
1133
|
+
// POSIX-only in v1: the remote follow/monitor layer offset-tails the log
|
|
1134
|
+
// with tail/cat/kill, which don't exist under PowerShell. Refuse Windows
|
|
1135
|
+
// up front, mirroring dispatch.ts launchDetached.
|
|
1136
|
+
if (remoteShellFor(host.os ?? resolveRemoteOsSync(host.name)) === 'powershell') {
|
|
1137
|
+
die(`Distributed teammates on Windows host "${host.name}" are not supported yet — ` +
|
|
1138
|
+
`the teams remote monitor is POSIX-only (offset-tails the remote log with tail/cat/kill). ` +
|
|
1139
|
+
`Use a Linux/macOS host, or run this teammate locally.`);
|
|
1140
|
+
}
|
|
1141
|
+
try {
|
|
1142
|
+
hostTarget = sshTargetFor(host);
|
|
1143
|
+
}
|
|
1144
|
+
catch (err) {
|
|
1145
|
+
die(`Can't resolve an ssh target for "${host.name}": ${err.message}`);
|
|
1146
|
+
}
|
|
1147
|
+
// Ensure agents-cli is present + version-matched on the host; surface
|
|
1148
|
+
// (not fail on) an agent-not-installed warning, like the run --host path.
|
|
1149
|
+
try {
|
|
1150
|
+
const { warnings } = ensureHostReady(host, { agent: parseTeammate(teammate).agent });
|
|
1151
|
+
for (const w of warnings)
|
|
1152
|
+
process.stderr.write(chalk.yellow(`[teams] warning: ${w}\n`));
|
|
1153
|
+
}
|
|
1154
|
+
catch (err) {
|
|
1155
|
+
die(`Host "${host.name}" is not ready: ${err.message}`);
|
|
1156
|
+
}
|
|
1157
|
+
// Provision the repo on the host from the team's --repo (clone into
|
|
1158
|
+
// ~/.agents/repos/<team> or reuse an existing checkout), resolving to the
|
|
1159
|
+
// ABSOLUTE git root so every later remote command works from an absolute
|
|
1160
|
+
// path (dispatch `cd`, worktree create, polling). When the team has no
|
|
1161
|
+
// --repo (the common "just send one teammate elsewhere" case, created
|
|
1162
|
+
// without a pool), fall back to THIS checkout's origin so the headline case
|
|
1163
|
+
// works with zero extra flags whenever you run `add` inside a git repo.
|
|
1164
|
+
let effectiveRepo = teamMeta?.repo ?? '';
|
|
1165
|
+
if (!effectiveRepo && (await isGitRepo(process.cwd()))) {
|
|
1166
|
+
effectiveRepo = (await getRemoteUrl(process.cwd())) ?? '';
|
|
1167
|
+
}
|
|
1168
|
+
try {
|
|
1169
|
+
hostRepoPath = ensureRemoteRepo(hostTarget, effectiveRepo, team);
|
|
1170
|
+
}
|
|
1171
|
+
catch (err) {
|
|
1172
|
+
die(`Couldn't provision the repo on "${host.name}": ${err.message}\n` +
|
|
1173
|
+
` Set how each device gets the code with: agents teams create ${team} --repo <url|path>`);
|
|
1174
|
+
}
|
|
1175
|
+
hostName = host.name;
|
|
1176
|
+
}
|
|
1042
1177
|
const { agent, version, profileName } = parseTeammate(teammate);
|
|
1043
1178
|
warnAgentDeprecated(agent);
|
|
1044
|
-
|
|
1179
|
+
// Version-installed check is about the LOCAL machine — a distributed (--on)
|
|
1180
|
+
// teammate's agent/version lives on the host, verified by ensureHostReady.
|
|
1181
|
+
if (version && !hostName && !isVersionInstalled(agent, version)) {
|
|
1045
1182
|
die(`${AGENT_NAMES[agent]} ${version} isn't installed.\n` +
|
|
1046
1183
|
` Install it: agents add ${agent}@${version}\n` +
|
|
1047
1184
|
` Or see what's installed (incl. @latest): agents view ${agent}`);
|
|
@@ -1049,7 +1186,8 @@ export function registerTeamsCommands(program) {
|
|
|
1049
1186
|
// Advisory sign-in check: warn but NEVER block. Detection is unreliable
|
|
1050
1187
|
// for opaque-cred agents, so a false negative must not stop a team. Cloud
|
|
1051
1188
|
// dispatch authenticates through the provider, not the local CLI — skip it.
|
|
1052
|
-
|
|
1189
|
+
// Distributed (--on) teammates authenticate on the host, not locally — skip.
|
|
1190
|
+
if (!opts.force && !cloudProviderId && !hostName && !(await checkCliSignedIn(agent))) {
|
|
1053
1191
|
console.error(chalk.yellow(`⚠ ${AGENT_NAMES[agent]} may not be signed in (detection is unreliable). Adding anyway.`) +
|
|
1054
1192
|
chalk.gray(`\n If it fails to start, run \`${AGENTS[agent].cliCommand}\` to log in, or pass --force to silence this.`));
|
|
1055
1193
|
}
|
|
@@ -1058,7 +1196,7 @@ export function registerTeamsCommands(program) {
|
|
|
1058
1196
|
// out-of-credits / signed-out account (see throttleWarningLine). Skip bare
|
|
1059
1197
|
// targets (rotation handles them), profiles (auth-injected account isn't
|
|
1060
1198
|
// the version-home one we can read), and cloud dispatch. Warn, never block.
|
|
1061
|
-
if (!opts.force && !cloudProviderId && !profileName && version) {
|
|
1199
|
+
if (!opts.force && !cloudProviderId && !hostName && !profileName && version) {
|
|
1062
1200
|
const readiness = await checkRunAccountReadiness(agent, version);
|
|
1063
1201
|
if (!readiness.ready)
|
|
1064
1202
|
console.error(throttleWarningLine(agent, version, readiness));
|
|
@@ -1086,15 +1224,34 @@ export function registerTeamsCommands(program) {
|
|
|
1086
1224
|
catch (err) {
|
|
1087
1225
|
die(err.message);
|
|
1088
1226
|
}
|
|
1089
|
-
//
|
|
1090
|
-
await ensureTeam(team);
|
|
1091
|
-
// Check if team has worktrees enabled or a shared worktree
|
|
1092
|
-
const teamMeta = await getTeam(team);
|
|
1227
|
+
// Team already ensured + loaded above (teamMeta) for the --repo provisioning.
|
|
1093
1228
|
const worktreesEnabled = teamMeta?.enable_worktrees ?? false;
|
|
1094
1229
|
const sharedWorktree = teamMeta?.use_worktree ?? null;
|
|
1095
1230
|
let worktreeName = null;
|
|
1096
1231
|
let worktreePath = null;
|
|
1097
|
-
if (
|
|
1232
|
+
if (hostName) {
|
|
1233
|
+
// Distributed teammate: the checkout lives on the host, so we NEVER touch
|
|
1234
|
+
// the local filesystem here. A shared local worktree makes no sense for a
|
|
1235
|
+
// remote teammate; a per-teammate worktree is created ON THE HOST at launch
|
|
1236
|
+
// (createRemoteWorktree in launchRemoteProcess) — we just capture its name.
|
|
1237
|
+
if (sharedWorktree) {
|
|
1238
|
+
die(`Team '${team}' uses a shared local --use-worktree, which can't apply to a --device (remote) teammate.`);
|
|
1239
|
+
}
|
|
1240
|
+
if (worktreesEnabled) {
|
|
1241
|
+
if (!opts.worktree) {
|
|
1242
|
+
die(`Team '${team}' has worktrees enabled. Use --worktree <name> for the remote teammate (created on ${hostName}).`);
|
|
1243
|
+
}
|
|
1244
|
+
if (!opts.name) {
|
|
1245
|
+
die(`Team '${team}' has worktrees enabled. Use --name <name> to identify this teammate.`);
|
|
1246
|
+
}
|
|
1247
|
+
worktreeName = opts.worktree;
|
|
1248
|
+
}
|
|
1249
|
+
else if (opts.worktree) {
|
|
1250
|
+
die(`--worktree requires --enable-worktrees on the team. Recreate the team with: agents teams create ${team} --enable-worktrees`);
|
|
1251
|
+
}
|
|
1252
|
+
// Local cwd stays null — the remote cwd is repoPath / the remote worktree.
|
|
1253
|
+
}
|
|
1254
|
+
else if (sharedWorktree) {
|
|
1098
1255
|
// Team uses a shared worktree for all teammates
|
|
1099
1256
|
const fsp = await import('fs/promises');
|
|
1100
1257
|
try {
|
|
@@ -1133,7 +1290,10 @@ export function registerTeamsCommands(program) {
|
|
|
1133
1290
|
else if (opts.worktree) {
|
|
1134
1291
|
die(`--worktree requires --enable-worktrees on the team. Recreate the team with: agents teams create ${team} --enable-worktrees`);
|
|
1135
1292
|
}
|
|
1136
|
-
|
|
1293
|
+
// Distributed teammates have no LOCAL cwd — their working dir lives on the
|
|
1294
|
+
// host (repoPath / the remote worktree). Local teammates default to the
|
|
1295
|
+
// worktree path, then --cwd, then the current directory.
|
|
1296
|
+
const cwd = hostName ? null : (worktreePath ?? opts.cwd ?? process.cwd());
|
|
1137
1297
|
const mgr = mkManager();
|
|
1138
1298
|
// Factory teammates: prepend the worker-skill preamble to every task
|
|
1139
1299
|
// prompt so implementers/testers/reviewers know about the Ledger, the
|
|
@@ -1182,7 +1342,7 @@ export function registerTeamsCommands(program) {
|
|
|
1182
1342
|
}
|
|
1183
1343
|
}
|
|
1184
1344
|
try {
|
|
1185
|
-
const result = await handleSpawn(mgr, team, agent, effectiveTask, cwd, opts.mode, opts.effort, null, cwd, version, opts.name ?? null, after, opts.model ?? null, envOverrides ?? null, taskType, cloudProviderId, cloudSessionId, opts.repo ?? null, opts.branch ?? null, worktreeName, worktreePath, profileName);
|
|
1345
|
+
const result = await handleSpawn(mgr, team, agent, effectiveTask, cwd, opts.mode, opts.effort, null, cwd, version, opts.name ?? null, after, opts.model ?? null, envOverrides ?? null, taskType, cloudProviderId, cloudSessionId, opts.repo ?? null, opts.branch ?? null, worktreeName, worktreePath, profileName, hostName, hostTarget, hostRepoPath);
|
|
1186
1346
|
if (isJsonMode(opts)) {
|
|
1187
1347
|
console.log(JSON.stringify(result, null, 2));
|
|
1188
1348
|
return;
|
|
@@ -1200,7 +1360,10 @@ export function registerTeamsCommands(program) {
|
|
|
1200
1360
|
console.log(` ${chalk.gray('agent_id')} ${chalk.cyan(shortId(result.agent_id))} ${chalk.gray(`(${result.agent_id})`)}`);
|
|
1201
1361
|
console.log(` ${chalk.gray('status ')} ${statusColor(result.status)(result.status)}`);
|
|
1202
1362
|
console.log(` ${chalk.gray('mode ')} ${opts.mode}`);
|
|
1203
|
-
console.log(` ${chalk.gray('working ')} ${cwd}`);
|
|
1363
|
+
console.log(` ${chalk.gray('working ')} ${hostName ? hostRepoPath : cwd}`);
|
|
1364
|
+
if (hostName) {
|
|
1365
|
+
console.log(` ${chalk.gray('host ')} ${chalk.cyan(hostName)}${chalk.gray(` (${hostTarget})`)}`);
|
|
1366
|
+
}
|
|
1204
1367
|
if (worktreeName) {
|
|
1205
1368
|
console.log(` ${chalk.gray('worktree')} ${chalk.cyan(worktreeName)}`);
|
|
1206
1369
|
}
|
|
@@ -1300,6 +1463,7 @@ export function registerTeamsCommands(program) {
|
|
|
1300
1463
|
started_at: a.startedAt.toISOString(),
|
|
1301
1464
|
cwd: a.cwd,
|
|
1302
1465
|
version: a.version,
|
|
1466
|
+
host: a.hostName,
|
|
1303
1467
|
})) }, null, 2));
|
|
1304
1468
|
return;
|
|
1305
1469
|
}
|
|
@@ -1317,7 +1481,10 @@ export function registerTeamsCommands(program) {
|
|
|
1317
1481
|
console.log(chalk.bold(`Team ${chalk.cyan(team)} ${chalk.gray(`(${agents.length} working)`)}`));
|
|
1318
1482
|
for (const a of agents) {
|
|
1319
1483
|
const ident = a.name || shortId(a.agentId);
|
|
1320
|
-
|
|
1484
|
+
// A distributed teammate has no local pid; show its host + remote pid.
|
|
1485
|
+
const pidStr = a.hostName
|
|
1486
|
+
? chalk.cyan(`on ${a.hostName}`) + (a.remotePid ? chalk.gray(` (pid ${a.remotePid})`) : '')
|
|
1487
|
+
: a.pid ? chalk.yellow(`pid ${a.pid}`) : chalk.gray('pid ?');
|
|
1321
1488
|
const started = chalk.gray(relTime(a.startedAt.toISOString()));
|
|
1322
1489
|
console.log(` ${chalk.magenta(padRight(fullName(a.agentType, a.version), 18))} ${chalk.white(padRight(ident, 20))} ${pidStr} ${started}`);
|
|
1323
1490
|
}
|
|
@@ -1544,13 +1711,24 @@ export function registerTeamsCommands(program) {
|
|
|
1544
1711
|
let worktreeKept = false;
|
|
1545
1712
|
if (agent?.worktreeName && agent?.worktreePath) {
|
|
1546
1713
|
try {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1714
|
+
if (agent.hostName && agent.hostTarget && agent.repoPath) {
|
|
1715
|
+
// Distributed teammate: guard + remove the worktree ON THE HOST.
|
|
1716
|
+
if (remoteWorktreeDirty(agent.hostTarget, agent.worktreePath)) {
|
|
1717
|
+
worktreeKept = true;
|
|
1718
|
+
}
|
|
1719
|
+
else {
|
|
1720
|
+
removeRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName);
|
|
1721
|
+
}
|
|
1550
1722
|
}
|
|
1551
1723
|
else {
|
|
1552
|
-
const
|
|
1553
|
-
|
|
1724
|
+
const dirty = await hasUncommittedChanges(agent.worktreePath);
|
|
1725
|
+
if (dirty) {
|
|
1726
|
+
worktreeKept = true;
|
|
1727
|
+
}
|
|
1728
|
+
else {
|
|
1729
|
+
const baseCwd = process.cwd();
|
|
1730
|
+
await removeWorktree(baseCwd, agent.worktreeName);
|
|
1731
|
+
}
|
|
1554
1732
|
}
|
|
1555
1733
|
}
|
|
1556
1734
|
catch {
|
|
@@ -1667,12 +1845,23 @@ export function registerTeamsCommands(program) {
|
|
|
1667
1845
|
const agent = await mgr.get(a.agent_id);
|
|
1668
1846
|
if (agent?.worktreeName && agent?.worktreePath) {
|
|
1669
1847
|
try {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1848
|
+
if (agent.hostName && agent.hostTarget && agent.repoPath) {
|
|
1849
|
+
// Distributed teammate: guard + remove the worktree ON THE HOST.
|
|
1850
|
+
if (remoteWorktreeDirty(agent.hostTarget, agent.worktreePath)) {
|
|
1851
|
+
keptWorktrees.push(agent.worktreeName);
|
|
1852
|
+
}
|
|
1853
|
+
else {
|
|
1854
|
+
removeRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName);
|
|
1855
|
+
}
|
|
1673
1856
|
}
|
|
1674
1857
|
else {
|
|
1675
|
-
await
|
|
1858
|
+
const dirty = await hasUncommittedChanges(agent.worktreePath);
|
|
1859
|
+
if (dirty) {
|
|
1860
|
+
keptWorktrees.push(agent.worktreeName);
|
|
1861
|
+
}
|
|
1862
|
+
else {
|
|
1863
|
+
await removeWorktree(baseCwd, agent.worktreeName);
|
|
1864
|
+
}
|
|
1676
1865
|
}
|
|
1677
1866
|
}
|
|
1678
1867
|
catch { /* best-effort */ }
|
|
@@ -1778,58 +1967,16 @@ export function registerTeamsCommands(program) {
|
|
|
1778
1967
|
.description('Check which agents are installed and available to join a team. Verifies CLI paths and shows an advisory sign-in hint.')
|
|
1779
1968
|
.option('--json', 'Output machine-readable JSON')
|
|
1780
1969
|
.action(async (opts) => {
|
|
1781
|
-
const
|
|
1782
|
-
// Deep integrity probe. `checkAllClis` reports presence (shim + stub guard),
|
|
1783
|
-
// but a GUTTED native binary (JS wrapper present, platform binary missing —
|
|
1784
|
-
// the codex/kimi optional-dep partial-extract failure) still passes that. So
|
|
1785
|
-
// actually launch the resolved default version and, if it won't run, flip the
|
|
1786
|
-
// agent to not-installed with a repair hint — otherwise doctor says "ready"
|
|
1787
|
-
// and the teammate ENOENTs at spawn. Parallel; win32 is treated as healthy by
|
|
1788
|
-
// verifyInstalledBinaryLaunches.
|
|
1789
|
-
await Promise.all(Object.entries(info).map(async ([name, entry]) => {
|
|
1790
|
-
if (!entry.installed)
|
|
1791
|
-
return;
|
|
1792
|
-
const agent = name;
|
|
1793
|
-
const version = resolveVersion(agent);
|
|
1794
|
-
if (!version)
|
|
1795
|
-
return;
|
|
1796
|
-
const health = await verifyInstalledBinaryLaunches(agent, version);
|
|
1797
|
-
if (!health.ok) {
|
|
1798
|
-
entry.installed = false;
|
|
1799
|
-
entry.path = null;
|
|
1800
|
-
entry.error = `${AGENTS[agent]?.cliCommand ?? name}@${version} is installed but its binary won't launch`
|
|
1801
|
-
+ `${health.detail ? ` (${health.detail})` : ''}. Repair: agents add ${agent}@${version}`;
|
|
1802
|
-
}
|
|
1803
|
-
}));
|
|
1804
|
-
// Advisory enrichment only. Sign-in detection is UNRELIABLE, so it never
|
|
1805
|
-
// changes the authoritative installed/ready column — it annotates. And an
|
|
1806
|
-
// agent that is actually running in a team is treated as signed in
|
|
1807
|
-
// regardless of the probe, so doctor never reports a working agent as
|
|
1808
|
-
// logged out ("don't show wrong stuff").
|
|
1809
|
-
const running = new Set();
|
|
1810
|
-
try {
|
|
1811
|
-
for (const a of await mkManager().listRunning())
|
|
1812
|
-
running.add(a.agentType);
|
|
1813
|
-
}
|
|
1814
|
-
catch { /* no teams yet — leave running empty */ }
|
|
1815
|
-
const auth = {};
|
|
1816
|
-
await Promise.all(Object.entries(info).map(async ([name, entry]) => {
|
|
1817
|
-
const isRunning = running.has(name);
|
|
1818
|
-
const probe = entry.installed && !isRunning ? await checkCliSignedIn(name) : false;
|
|
1819
|
-
auth[name] = resolveSignInAdvisory(entry.installed, isRunning, probe);
|
|
1820
|
-
}));
|
|
1970
|
+
const data = await collectTeamsDoctorData();
|
|
1821
1971
|
if (isJsonMode(opts)) {
|
|
1822
|
-
|
|
1823
|
-
for (const [name, entry] of Object.entries(info))
|
|
1824
|
-
merged[name] = { ...entry, ...auth[name] };
|
|
1825
|
-
console.log(JSON.stringify(merged, null, 2));
|
|
1972
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1826
1973
|
return;
|
|
1827
1974
|
}
|
|
1828
1975
|
console.log(chalk.bold('Who can join a team:'));
|
|
1829
|
-
for (const [name, entry] of Object.entries(
|
|
1976
|
+
for (const [name, entry] of Object.entries(data)) {
|
|
1830
1977
|
const pretty = AGENT_NAMES[name] || name;
|
|
1831
1978
|
if (entry.installed) {
|
|
1832
|
-
const { signedIn, running: isRunning } =
|
|
1979
|
+
const { signedIn, running: isRunning } = entry;
|
|
1833
1980
|
const hint = isRunning
|
|
1834
1981
|
? chalk.gray('in use')
|
|
1835
1982
|
: signedIn
|