@phnx-labs/agents-cli 1.20.32 → 1.20.34
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 +28 -2
- package/dist/commands/commands.js +3 -3
- package/dist/commands/computer-actions.js +1 -0
- package/dist/commands/computer.d.ts +23 -0
- package/dist/commands/computer.js +45 -3
- package/dist/commands/cost.js +2 -2
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +51 -2
- package/dist/commands/exec.js +56 -1
- package/dist/commands/hooks.js +3 -3
- package/dist/commands/import.js +1 -1
- package/dist/commands/inspect.js +13 -17
- package/dist/commands/mcp.js +3 -3
- package/dist/commands/permissions.js +3 -3
- package/dist/commands/rules.js +3 -3
- package/dist/commands/secrets-migrate.js +23 -11
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +53 -1
- package/dist/commands/sessions.js +18 -1
- package/dist/commands/skills.js +3 -3
- package/dist/commands/status.d.ts +12 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/sync.js +2 -2
- package/dist/commands/teams.js +77 -18
- package/dist/commands/usage.js +2 -2
- package/dist/commands/utils.d.ts +8 -0
- package/dist/commands/utils.js +20 -0
- package/dist/commands/versions.js +4 -3
- package/dist/commands/view.d.ts +39 -0
- package/dist/commands/view.js +217 -84
- package/dist/commands/workflows.js +3 -3
- package/dist/index.js +16 -2
- package/dist/lib/acp/harnesses.d.ts +1 -1
- package/dist/lib/acp/harnesses.js +2 -2
- package/dist/lib/agent-spec/index.d.ts +18 -0
- package/dist/lib/agent-spec/index.js +35 -0
- package/dist/lib/agent-spec/primitives.d.ts +28 -0
- package/dist/lib/agent-spec/primitives.js +57 -0
- package/dist/lib/agent-spec/provider.d.ts +2 -0
- package/dist/lib/agent-spec/provider.js +9 -0
- package/dist/lib/agent-spec/resolve.d.ts +33 -0
- package/dist/lib/agent-spec/resolve.js +174 -0
- package/dist/lib/agent-spec/types.d.ts +57 -0
- package/dist/lib/agent-spec/types.js +18 -0
- package/dist/lib/agents.d.ts +12 -0
- package/dist/lib/agents.js +115 -32
- package/dist/lib/browser/chrome.js +20 -0
- package/dist/lib/browser/drivers/ssh.d.ts +19 -0
- package/dist/lib/browser/drivers/ssh.js +18 -3
- package/dist/lib/crabbox/cli.d.ts +98 -0
- package/dist/lib/crabbox/cli.js +218 -0
- package/dist/lib/crabbox/lease.d.ts +41 -0
- package/dist/lib/crabbox/lease.js +73 -0
- package/dist/lib/crabbox/runtimes.d.ts +57 -0
- package/dist/lib/crabbox/runtimes.js +109 -0
- package/dist/lib/doctor-diff.js +29 -2
- package/dist/lib/drift-sync.d.ts +43 -0
- package/dist/lib/drift-sync.js +179 -0
- package/dist/lib/exec.d.ts +15 -0
- package/dist/lib/exec.js +21 -11
- package/dist/lib/hosts/dispatch.d.ts +27 -10
- package/dist/lib/hosts/dispatch.js +55 -19
- package/dist/lib/hosts/option.d.ts +14 -0
- package/dist/lib/hosts/option.js +19 -0
- package/dist/lib/hosts/passthrough.d.ts +30 -0
- package/dist/lib/hosts/passthrough.js +141 -0
- package/dist/lib/hosts/remote-cmd.d.ts +36 -0
- package/dist/lib/hosts/remote-cmd.js +56 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/platform/winpath.d.ts +31 -2
- package/dist/lib/platform/winpath.js +133 -24
- package/dist/lib/pwsh.d.ts +11 -0
- package/dist/lib/pwsh.js +13 -0
- 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 +42 -1
- package/dist/lib/secrets/agent.js +89 -11
- package/dist/lib/secrets/bundles.js +61 -21
- package/dist/lib/secrets/filestore.js +31 -1
- package/dist/lib/secrets/index.d.ts +44 -1
- package/dist/lib/secrets/index.js +108 -10
- package/dist/lib/secrets/linux.d.ts +14 -0
- package/dist/lib/secrets/linux.js +21 -0
- package/dist/lib/secrets/windows.d.ts +74 -0
- package/dist/lib/secrets/windows.js +440 -0
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +18 -1
- package/dist/lib/session/provenance.d.ts +56 -0
- package/dist/lib/session/provenance.js +157 -0
- package/dist/lib/shims.d.ts +20 -0
- package/dist/lib/shims.js +53 -20
- package/dist/lib/ssh-exec.d.ts +22 -0
- package/dist/lib/ssh-exec.js +59 -2
- package/dist/lib/ssh-tunnel.d.ts +0 -5
- package/dist/lib/ssh-tunnel.js +65 -8
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-status.d.ts +102 -0
- package/dist/lib/sync-status.js +135 -0
- package/dist/lib/teams/agents.d.ts +24 -0
- package/dist/lib/teams/agents.js +30 -1
- package/dist/lib/types.d.ts +20 -1
- package/dist/lib/usage.d.ts +30 -0
- package/dist/lib/usage.js +159 -2
- package/dist/lib/versions.d.ts +2 -4
- package/dist/lib/versions.js +7 -20
- package/package.json +2 -1
- package/dist/lib/agent-spec.d.ts +0 -36
- package/dist/lib/agent-spec.js +0 -157
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime detection + picker + credential-script builder for `agents run --lease`.
|
|
3
|
+
*
|
|
4
|
+
* The picker asks which coding-agent runtime(s) to provision on a leased box.
|
|
5
|
+
* The default selection is whatever the user is currently signed into locally
|
|
6
|
+
* (via `getAccountInfo`, the same source `agents view` uses). The chosen runtimes
|
|
7
|
+
* drive both what gets installed on the box and which auth token file is copied
|
|
8
|
+
* over — the token contents ride the uploaded `--script-stdin` body, never argv.
|
|
9
|
+
*
|
|
10
|
+
* SECURITY: copying a runtime's auth token to an ephemeral cloud box is a
|
|
11
|
+
* credential transfer. It is strictly opt-in (a confirm prompt in the command
|
|
12
|
+
* layer), the token never appears in argv/`ps`, and `--lease` one-shot runs tear
|
|
13
|
+
* the box down afterward so the credential's lifetime is bounded by the run.
|
|
14
|
+
*/
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import { getAccountInfo } from '../agents.js';
|
|
19
|
+
export const LEASE_RUNTIMES = [
|
|
20
|
+
{ id: 'claude', label: 'Claude Code', localCandidates: ['.claude/.claude.json', '.claude.json'], remote: '.claude.json' },
|
|
21
|
+
{ id: 'codex', label: 'Codex CLI', localCandidates: ['.codex/auth.json'], remote: '.codex/auth.json' },
|
|
22
|
+
{ id: 'gemini', label: 'Gemini CLI', localCandidates: ['.gemini/google_accounts.json'], remote: '.gemini/google_accounts.json' },
|
|
23
|
+
{ id: 'grok', label: 'Grok CLI', localCandidates: ['.grok/auth.json'], remote: '.grok/auth.json' },
|
|
24
|
+
];
|
|
25
|
+
/** First existing candidate path under the real home, or null. */
|
|
26
|
+
function findLocalCred(cred) {
|
|
27
|
+
const home = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
28
|
+
for (const rel of cred.localCandidates) {
|
|
29
|
+
const p = path.join(home, rel);
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(p))
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* unreadable — skip */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** Which lease-capable runtimes the user is signed into on this machine. */
|
|
41
|
+
export async function detectSignedInRuntimes() {
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const cred of LEASE_RUNTIMES) {
|
|
44
|
+
let info;
|
|
45
|
+
try {
|
|
46
|
+
info = await getAccountInfo(cred.id);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
info = null;
|
|
50
|
+
}
|
|
51
|
+
out.push({
|
|
52
|
+
id: cred.id,
|
|
53
|
+
label: cred.label,
|
|
54
|
+
email: info?.email ?? null,
|
|
55
|
+
signedIn: !!info?.signedIn,
|
|
56
|
+
credPath: findLocalCred(cred),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Interactive checkbox: which runtimes to provision on the box. Defaults to the
|
|
63
|
+
* signed-in ones. Runtimes with no local credential are shown disabled.
|
|
64
|
+
* `prompt` is injected so tests don't require a TTY.
|
|
65
|
+
*/
|
|
66
|
+
export async function pickRuntimes(detected, prompt) {
|
|
67
|
+
const choices = detected.map((d) => ({
|
|
68
|
+
name: `${d.label}${d.email ? ` (${d.email})` : d.signedIn ? ' (signed in)' : ''}`,
|
|
69
|
+
value: d.id,
|
|
70
|
+
checked: d.signedIn && !!d.credPath,
|
|
71
|
+
disabled: d.credPath ? false : 'no local credential — sign in first',
|
|
72
|
+
}));
|
|
73
|
+
if (prompt)
|
|
74
|
+
return prompt(choices);
|
|
75
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
76
|
+
return checkbox({ message: 'Provision which runtime(s) on the leased box?', choices });
|
|
77
|
+
}
|
|
78
|
+
// A long random sentinel makes an accidental (or malicious) collision with a
|
|
79
|
+
// token's contents effectively impossible, so the quoted heredoc can never be
|
|
80
|
+
// closed early by the credential body.
|
|
81
|
+
const CRED_EOF = 'AGENTS_LEASE_CRED_EOF_9f3c1a7b5e2d4068';
|
|
82
|
+
/**
|
|
83
|
+
* Build a bash snippet that writes each picked runtime's token file to the box's
|
|
84
|
+
* home-level config path (0600), from the token contents read locally. Returns
|
|
85
|
+
* `''` when no runtimes were selected. The snippet is meant to be embedded in
|
|
86
|
+
* the `--script-stdin` body (never argv).
|
|
87
|
+
*/
|
|
88
|
+
export function buildCredentialScript(picked, detected) {
|
|
89
|
+
const byId = new Map(detected.map((d) => [d.id, d]));
|
|
90
|
+
const parts = [];
|
|
91
|
+
for (const id of picked) {
|
|
92
|
+
const d = byId.get(id);
|
|
93
|
+
const cred = LEASE_RUNTIMES.find((c) => c.id === id);
|
|
94
|
+
if (!d?.credPath || !cred)
|
|
95
|
+
continue;
|
|
96
|
+
let contents;
|
|
97
|
+
try {
|
|
98
|
+
contents = fs.readFileSync(d.credPath, 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const dir = path.posix.dirname(cred.remote);
|
|
104
|
+
const mkdir = dir && dir !== '.' ? `mkdir -p "$HOME/${dir}"\n` : '';
|
|
105
|
+
parts.push(`${mkdir}cat > "$HOME/${cred.remote}" <<'${CRED_EOF}'\n${contents}${contents.endsWith('\n') ? '' : '\n'}${CRED_EOF}\n` +
|
|
106
|
+
`chmod 600 "$HOME/${cred.remote}"`);
|
|
107
|
+
}
|
|
108
|
+
return parts.join('\n');
|
|
109
|
+
}
|
package/dist/lib/doctor-diff.js
CHANGED
|
@@ -29,6 +29,8 @@ import { pluginInstallDir, repairableManifestFields } from './plugin-marketplace
|
|
|
29
29
|
import { markdownToToml } from './convert.js';
|
|
30
30
|
import { resolveImports, supportsRulesImports } from './rules/compile.js';
|
|
31
31
|
import { listCommandsInVersionHome, getVersionCommandsDir } from './commands.js';
|
|
32
|
+
import { shouldInstallCommandAsSkill, commandSkillMatches, commandSkillName } from './command-skills.js';
|
|
33
|
+
import { supports } from './capabilities.js';
|
|
32
34
|
import { listSkillsInVersionHome, getVersionSkillsDir } from './skills.js';
|
|
33
35
|
import { listHooksInVersionHome, listHookEntriesFromDir } from './hooks.js';
|
|
34
36
|
const RULES_DOC_FILENAME = 'README.md';
|
|
@@ -87,6 +89,15 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
|
87
89
|
const isToml = agentConfig.format === 'toml';
|
|
88
90
|
const ext = isToml ? '.toml' : '.md';
|
|
89
91
|
const homeDir = getVersionCommandsDir(agent, version);
|
|
92
|
+
// Command-as-skill agents (kimi, codex>=0.117, grok) install every command as a
|
|
93
|
+
// SKILL wrapper at <agentDir>/skills/<cmd>/SKILL.md, not a native command file.
|
|
94
|
+
// The compare must follow that or every command false-reports as drifted.
|
|
95
|
+
const asSkill = shouldInstallCommandAsSkill(agent, version);
|
|
96
|
+
// Agents that hold commands neither natively nor as skills (e.g. goose) must not
|
|
97
|
+
// report source commands as "missing" — they structurally can't take them.
|
|
98
|
+
if (!asSkill && !supports(agent, 'commands', version).ok)
|
|
99
|
+
return [];
|
|
100
|
+
const agentDir = path.join(getVersionHomePath(agent, version), agentConfigDirName(agent));
|
|
90
101
|
const installed = new Set(listCommandsInVersionHome(agent, version));
|
|
91
102
|
const layerBases = buildLayerBases(cwd, 'commands', { excludeProject });
|
|
92
103
|
const sourceByName = new Map();
|
|
@@ -113,11 +124,24 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
|
113
124
|
const seen = new Set();
|
|
114
125
|
for (const [name, src] of sourceByName) {
|
|
115
126
|
seen.add(name);
|
|
116
|
-
const homePath = path.join(homeDir, `${name}${ext}`);
|
|
117
127
|
if (!installed.has(name)) {
|
|
118
128
|
rows.push({ kind: 'commands', name, status: 'missing', source: src.layer, sourcePath: src.path });
|
|
119
129
|
continue;
|
|
120
130
|
}
|
|
131
|
+
if (asSkill) {
|
|
132
|
+
// Compare against the installed command-skill wrapper, not a native path.
|
|
133
|
+
const matches = commandSkillMatches(agentDir, name, src.path);
|
|
134
|
+
rows.push({
|
|
135
|
+
kind: 'commands',
|
|
136
|
+
name,
|
|
137
|
+
status: matches ? 'ok' : 'diff',
|
|
138
|
+
source: src.layer,
|
|
139
|
+
sourcePath: src.path,
|
|
140
|
+
homePath: path.join(agentDir, 'skills', commandSkillName(name), 'SKILL.md'),
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const homePath = path.join(homeDir, `${name}${ext}`);
|
|
121
145
|
const installedContent = readSafe(homePath);
|
|
122
146
|
const sourceContent = readSafe(src.path);
|
|
123
147
|
if (installedContent == null || sourceContent == null) {
|
|
@@ -138,7 +162,10 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
|
138
162
|
for (const name of installed) {
|
|
139
163
|
if (seen.has(name))
|
|
140
164
|
continue;
|
|
141
|
-
|
|
165
|
+
const extraHome = asSkill
|
|
166
|
+
? path.join(agentDir, 'skills', commandSkillName(name), 'SKILL.md')
|
|
167
|
+
: path.join(homeDir, `${name}${ext}`);
|
|
168
|
+
rows.push({ kind: 'commands', name, status: 'extra', homePath: extraHome });
|
|
142
169
|
}
|
|
143
170
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
144
171
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive drift-sync flow — the single "we detected drift, want to fix it?"
|
|
3
|
+
* action, shared by `agents status`, `agents doctor`, and the menu-bar "NEEDS
|
|
4
|
+
* SYNC" row.
|
|
5
|
+
*
|
|
6
|
+
* It composes existing pieces, re-implementing nothing:
|
|
7
|
+
* - computeSyncStatus() — the unified detection engine (sync-status.ts)
|
|
8
|
+
* - pullRepo() — fast-forward the `.system` repo (git.ts)
|
|
9
|
+
* - promptAgentVersionSelection() — the "which agent types / versions?" picker
|
|
10
|
+
* - heal({ mode: 'full' }) — the reconcile engine `doctor --fix` uses
|
|
11
|
+
*
|
|
12
|
+
* Combined flow (one confirmation): if `.system` is behind AND resources drifted,
|
|
13
|
+
* a single "Sync all detected" both pulls `.system` and reconciles the chosen
|
|
14
|
+
* version homes. The security posture is preserved — the `.system` pull only ever
|
|
15
|
+
* happens on an explicit user choice here, never silently (see auto-pull-worker.ts
|
|
16
|
+
* for why system auto-pull is off by default).
|
|
17
|
+
*/
|
|
18
|
+
import { type VersionHealResult } from './heal.js';
|
|
19
|
+
import { type UnifiedSyncStatus } from './sync-status.js';
|
|
20
|
+
export interface DriftSyncOptions {
|
|
21
|
+
cwd?: string;
|
|
22
|
+
/** Reconcile everything detected with no prompts — the "kick it" path, also the
|
|
23
|
+
* non-TTY / menu-bar-launched-with-flag behavior. Pulls `.system` if behind. */
|
|
24
|
+
yes?: boolean;
|
|
25
|
+
/** Pre-computed status, to avoid a second scan when the caller already has one. */
|
|
26
|
+
status?: UnifiedSyncStatus;
|
|
27
|
+
/** Skip the drift summary — set by callers that already printed their own. */
|
|
28
|
+
quiet?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface DriftSyncResult {
|
|
31
|
+
systemBehindBefore: number;
|
|
32
|
+
systemPulled: boolean;
|
|
33
|
+
healed: VersionHealResult[];
|
|
34
|
+
/** True when the user declined at the prompt (nothing was changed). */
|
|
35
|
+
cancelled: boolean;
|
|
36
|
+
/** True when there was no drift to act on in the first place. */
|
|
37
|
+
nothingToDo: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The unified "drift detected — sync now?" flow. Returns a structured result so
|
|
41
|
+
* callers (menu-bar, doctor) can report without re-scanning.
|
|
42
|
+
*/
|
|
43
|
+
export declare function promptDriftSync(opts?: DriftSyncOptions): Promise<DriftSyncResult>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive drift-sync flow — the single "we detected drift, want to fix it?"
|
|
3
|
+
* action, shared by `agents status`, `agents doctor`, and the menu-bar "NEEDS
|
|
4
|
+
* SYNC" row.
|
|
5
|
+
*
|
|
6
|
+
* It composes existing pieces, re-implementing nothing:
|
|
7
|
+
* - computeSyncStatus() — the unified detection engine (sync-status.ts)
|
|
8
|
+
* - pullRepo() — fast-forward the `.system` repo (git.ts)
|
|
9
|
+
* - promptAgentVersionSelection() — the "which agent types / versions?" picker
|
|
10
|
+
* - heal({ mode: 'full' }) — the reconcile engine `doctor --fix` uses
|
|
11
|
+
*
|
|
12
|
+
* Combined flow (one confirmation): if `.system` is behind AND resources drifted,
|
|
13
|
+
* a single "Sync all detected" both pulls `.system` and reconciles the chosen
|
|
14
|
+
* version homes. The security posture is preserved — the `.system` pull only ever
|
|
15
|
+
* happens on an explicit user choice here, never silently (see auto-pull-worker.ts
|
|
16
|
+
* for why system auto-pull is off by default).
|
|
17
|
+
*/
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
20
|
+
import { AGENTS } from './agents.js';
|
|
21
|
+
import { pullRepo } from './git.js';
|
|
22
|
+
import { heal } from './heal.js';
|
|
23
|
+
import { promptAgentVersionSelection } from './versions.js';
|
|
24
|
+
import { isInteractiveTerminal, isPromptCancelled } from '../commands/utils.js';
|
|
25
|
+
import { computeSyncStatus, } from './sync-status.js';
|
|
26
|
+
const agentName = (id) => AGENTS[id]?.name ?? id;
|
|
27
|
+
/** "claude@2.1.170 2 drifted · 1 missing" for one version. */
|
|
28
|
+
function versionLine(v) {
|
|
29
|
+
const bits = [];
|
|
30
|
+
if (v.counts.drifted)
|
|
31
|
+
bits.push(`${v.counts.drifted} drifted`);
|
|
32
|
+
if (v.counts.missing)
|
|
33
|
+
bits.push(`${v.counts.missing} missing`);
|
|
34
|
+
const label = `${agentName(v.agent)}@${v.version}`;
|
|
35
|
+
return ` ${label.padEnd(28)} ${chalk.yellow(bits.join(' · '))}`;
|
|
36
|
+
}
|
|
37
|
+
/** Render the drift summary (system freshness + each version owed a sync). */
|
|
38
|
+
function renderSummary(status, needing) {
|
|
39
|
+
console.log(chalk.bold('\nSync status'));
|
|
40
|
+
if (status.system.behind > 0) {
|
|
41
|
+
console.log(` ${'.system repo'.padEnd(28)} ${chalk.yellow(`${status.system.behind} commit${status.system.behind === 1 ? '' : 's'} behind`)} ${chalk.gray('— pull recommended')}`);
|
|
42
|
+
}
|
|
43
|
+
for (const v of needing)
|
|
44
|
+
console.log(versionLine(v));
|
|
45
|
+
if (status.totals.orphan > 0) {
|
|
46
|
+
console.log(chalk.gray(` (${status.totals.orphan} orphan${status.totals.orphan === 1 ? '' : 's'} — run \`agents prune cleanup\`)`));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Fast-forward the `.system` repo. Returns whether it actually moved. */
|
|
50
|
+
async function pullSystem(status) {
|
|
51
|
+
if (status.system.behind <= 0)
|
|
52
|
+
return false;
|
|
53
|
+
const res = await pullRepo(status.system.dir);
|
|
54
|
+
if (res.success) {
|
|
55
|
+
console.log(chalk.green(`Pulled .system (+${status.system.behind}).`));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
console.log(chalk.red(`Could not pull .system: ${res.error ?? 'unknown error'}`));
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
/** Reconcile a set of versions grouped by agent via the shared heal engine. */
|
|
62
|
+
async function healVersions(versionsByAgent, cwd) {
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const [agent, versions] of versionsByAgent) {
|
|
65
|
+
if (versions.length === 0)
|
|
66
|
+
continue;
|
|
67
|
+
const res = await heal({ mode: 'full', cwd, agent, versions });
|
|
68
|
+
out.push(...res.versions);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** Report which agents received what after a heal. */
|
|
73
|
+
function reportHealed(healed) {
|
|
74
|
+
const touched = healed.filter((v) => v.healed.length > 0);
|
|
75
|
+
if (touched.length === 0) {
|
|
76
|
+
console.log(chalk.gray('Nothing to reconcile — homes already matched sources.'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const total = touched.reduce((n, v) => n + v.healed.length, 0);
|
|
80
|
+
const agents = [...new Set(touched.map((v) => agentName(v.agent)))].join(', ');
|
|
81
|
+
console.log(chalk.green(`Synced ${total} resource${total === 1 ? '' : 's'} to ${agents}.`));
|
|
82
|
+
}
|
|
83
|
+
function groupNeeding(needing) {
|
|
84
|
+
const m = new Map();
|
|
85
|
+
for (const v of needing) {
|
|
86
|
+
const list = m.get(v.agent) ?? [];
|
|
87
|
+
list.push(v.version);
|
|
88
|
+
m.set(v.agent, list);
|
|
89
|
+
}
|
|
90
|
+
return m;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The unified "drift detected — sync now?" flow. Returns a structured result so
|
|
94
|
+
* callers (menu-bar, doctor) can report without re-scanning.
|
|
95
|
+
*/
|
|
96
|
+
export async function promptDriftSync(opts = {}) {
|
|
97
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
98
|
+
const status = opts.status ?? (await computeSyncStatus({ cwd }));
|
|
99
|
+
const needing = status.agents.filter((a) => a.needsSync);
|
|
100
|
+
const systemBehind = status.system.behind;
|
|
101
|
+
const base = {
|
|
102
|
+
systemBehindBefore: systemBehind,
|
|
103
|
+
systemPulled: false,
|
|
104
|
+
healed: [],
|
|
105
|
+
cancelled: false,
|
|
106
|
+
nothingToDo: false,
|
|
107
|
+
};
|
|
108
|
+
if (systemBehind <= 0 && needing.length === 0) {
|
|
109
|
+
console.log(chalk.green('Everything is in sync.'));
|
|
110
|
+
return { ...base, nothingToDo: true };
|
|
111
|
+
}
|
|
112
|
+
if (!opts.quiet)
|
|
113
|
+
renderSummary(status, needing);
|
|
114
|
+
// Non-interactive OR explicit --yes: reconcile everything detected.
|
|
115
|
+
if (opts.yes || !isInteractiveTerminal()) {
|
|
116
|
+
if (!opts.yes) {
|
|
117
|
+
// Non-TTY without --yes: report, don't act, don't throw.
|
|
118
|
+
console.log(chalk.gray('\nRun `agents status --yes` to sync, or `agents status` in a terminal to choose.'));
|
|
119
|
+
return base;
|
|
120
|
+
}
|
|
121
|
+
const systemPulled = await pullSystem(status);
|
|
122
|
+
const healed = await healVersions(groupNeeding(needing), cwd);
|
|
123
|
+
reportHealed(healed);
|
|
124
|
+
return { ...base, systemPulled, healed };
|
|
125
|
+
}
|
|
126
|
+
// Interactive gate.
|
|
127
|
+
let choice;
|
|
128
|
+
try {
|
|
129
|
+
choice = await select({
|
|
130
|
+
message: 'Sync now?',
|
|
131
|
+
choices: [
|
|
132
|
+
{ name: 'Sync all detected', value: 'all' },
|
|
133
|
+
{ name: 'Choose agents & resources', value: 'choose' },
|
|
134
|
+
{ name: 'No', value: 'no' },
|
|
135
|
+
],
|
|
136
|
+
default: 'all',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
if (isPromptCancelled(err))
|
|
141
|
+
return { ...base, cancelled: true };
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
if (choice === 'no')
|
|
145
|
+
return { ...base, cancelled: true };
|
|
146
|
+
if (choice === 'all') {
|
|
147
|
+
const systemPulled = await pullSystem(status);
|
|
148
|
+
const healed = await healVersions(groupNeeding(needing), cwd);
|
|
149
|
+
reportHealed(healed);
|
|
150
|
+
return { ...base, systemPulled, healed };
|
|
151
|
+
}
|
|
152
|
+
// choice === 'choose': optional .system pull, then per-agent/version selection.
|
|
153
|
+
let systemPulled = false;
|
|
154
|
+
if (systemBehind > 0) {
|
|
155
|
+
try {
|
|
156
|
+
const pull = await confirm({ message: `Pull .system (${systemBehind} behind) first?`, default: true });
|
|
157
|
+
if (pull)
|
|
158
|
+
systemPulled = await pullSystem(status);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
if (!isPromptCancelled(err))
|
|
162
|
+
throw err;
|
|
163
|
+
return { ...base, systemPulled, cancelled: true };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const needingAgents = [...new Set(needing.map((a) => a.agent))];
|
|
167
|
+
let selection;
|
|
168
|
+
try {
|
|
169
|
+
selection = await promptAgentVersionSelection(needingAgents);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
if (isPromptCancelled(err))
|
|
173
|
+
return { ...base, systemPulled, cancelled: true };
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
const healed = await healVersions(selection.versionSelections, cwd);
|
|
177
|
+
reportHealed(healed);
|
|
178
|
+
return { ...base, systemPulled, healed };
|
|
179
|
+
}
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -189,6 +189,21 @@ export declare function nativeResume(agent: AgentId): boolean;
|
|
|
189
189
|
export declare function buildExecCommand(options: ExecOptions): string[];
|
|
190
190
|
/** Spawn an agent and return its exit code. Convenience wrapper over spawnAgent. */
|
|
191
191
|
export declare function execAgent(options: ExecOptions): Promise<number>;
|
|
192
|
+
/**
|
|
193
|
+
* Resolve how to spawn a shim target for a platform. Pure — testable on any host.
|
|
194
|
+
*
|
|
195
|
+
* POSIX always execs the binary directly (no shell). On Windows a bare
|
|
196
|
+
* (non-absolute) name or a `.cmd` companion goes through the shell so cmd.exe
|
|
197
|
+
* resolves it via PATHEXT — the common, `.cmd`-present path; an absolute `.cmd`
|
|
198
|
+
* or extensionless path is exec'd through the shell / directly. npm always ships
|
|
199
|
+
* a `<cmd>.cmd` companion on Windows, so the runnable target `execShimPassthrough`
|
|
200
|
+
* hands us is the `.cmd` (never a bare `.ps1`).
|
|
201
|
+
*/
|
|
202
|
+
export declare function resolveShimSpawn(platform: NodeJS.Platform, binary: string, extraArgs: string[]): {
|
|
203
|
+
command: string;
|
|
204
|
+
args: string[];
|
|
205
|
+
shell: boolean;
|
|
206
|
+
};
|
|
192
207
|
/**
|
|
193
208
|
* Transparent passthrough exec for generated shims — the node-side delegate that
|
|
194
209
|
* Windows `.cmd` shims call. Resolves the active version (explicit pin, else
|
package/dist/lib/exec.js
CHANGED
|
@@ -374,15 +374,6 @@ export const AGENT_COMMANDS = {
|
|
|
374
374
|
edit: [],
|
|
375
375
|
},
|
|
376
376
|
},
|
|
377
|
-
roo: {
|
|
378
|
-
base: ['roo'],
|
|
379
|
-
promptFlag: 'positional',
|
|
380
|
-
modeFlags: {
|
|
381
|
-
plan: ['--mode', 'architect'],
|
|
382
|
-
edit: ['--mode', 'code'],
|
|
383
|
-
},
|
|
384
|
-
modelFlag: '--model',
|
|
385
|
-
},
|
|
386
377
|
// TODO: --output-format json is documented but currently broken upstream
|
|
387
378
|
// ("flags provided but not defined: -output-format"). Track resolution at
|
|
388
379
|
// https://github.com/google-antigravity/antigravity-cli/issues/7 before
|
|
@@ -665,6 +656,25 @@ export async function execAgent(options) {
|
|
|
665
656
|
const { exitCode } = await spawnAgent(options);
|
|
666
657
|
return exitCode;
|
|
667
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Resolve how to spawn a shim target for a platform. Pure — testable on any host.
|
|
661
|
+
*
|
|
662
|
+
* POSIX always execs the binary directly (no shell). On Windows a bare
|
|
663
|
+
* (non-absolute) name or a `.cmd` companion goes through the shell so cmd.exe
|
|
664
|
+
* resolves it via PATHEXT — the common, `.cmd`-present path; an absolute `.cmd`
|
|
665
|
+
* or extensionless path is exec'd through the shell / directly. npm always ships
|
|
666
|
+
* a `<cmd>.cmd` companion on Windows, so the runnable target `execShimPassthrough`
|
|
667
|
+
* hands us is the `.cmd` (never a bare `.ps1`).
|
|
668
|
+
*/
|
|
669
|
+
export function resolveShimSpawn(platform, binary, extraArgs) {
|
|
670
|
+
if (platform === 'win32') {
|
|
671
|
+
// Use win32 path semantics regardless of the host running this (the platform
|
|
672
|
+
// is the parameter, not process.platform) so `C:\...` reads as absolute.
|
|
673
|
+
const useShell = !path.win32.isAbsolute(binary) || binary.endsWith('.cmd');
|
|
674
|
+
return { command: binary, args: extraArgs, shell: useShell };
|
|
675
|
+
}
|
|
676
|
+
return { command: binary, args: extraArgs, shell: false };
|
|
677
|
+
}
|
|
668
678
|
/**
|
|
669
679
|
* Transparent passthrough exec for generated shims — the node-side delegate that
|
|
670
680
|
* Windows `.cmd` shims call. Resolves the active version (explicit pin, else
|
|
@@ -691,9 +701,9 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
691
701
|
// mode/effort are required by ExecOptions but unused by buildExecEnv (which only
|
|
692
702
|
// derives the per-version config-dir env); pass the agent's default to satisfy the type.
|
|
693
703
|
const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto' });
|
|
694
|
-
const
|
|
704
|
+
const { command, args, shell } = resolveShimSpawn(process.platform, binary, [...launchArgs, ...rawArgs]);
|
|
695
705
|
return new Promise((resolve) => {
|
|
696
|
-
const child = spawn(
|
|
706
|
+
const child = spawn(command, args, { cwd, stdio: 'inherit', env, shell });
|
|
697
707
|
child.on('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
698
708
|
child.on('error', (err) => {
|
|
699
709
|
process.stderr.write(`agents: failed to launch ${agent}: ${err.message}\n`);
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dispatch a headless
|
|
2
|
+
* Dispatch a headless `agents …` command onto a host over SSH.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
* log and its exit code to a sibling `.exit` file, so progress survives a
|
|
6
|
-
* connection (followed via offset-tail in progress.ts). This is the
|
|
7
|
-
* the
|
|
4
|
+
* The command is launched detached (`nohup … &`) writing combined output to a
|
|
5
|
+
* remote log and its exit code to a sibling `.exit` file, so progress survives a
|
|
6
|
+
* dropped connection (followed via offset-tail in progress.ts). This is the
|
|
7
|
+
* offload win: the process/thread/file fan-out happens on the host, not the
|
|
8
|
+
* laptop. `agents run` uses it; `agents teams start --watch --host` reuses the
|
|
9
|
+
* same core so a remote team supervisor keeps running after you disconnect.
|
|
8
10
|
*/
|
|
9
11
|
import type { Host } from './types.js';
|
|
10
12
|
import { type HostTask } from './tasks.js';
|
|
13
|
+
export interface DispatchResult {
|
|
14
|
+
task: HostTask;
|
|
15
|
+
/** Exit code when followed; undefined when detached (--no-follow). */
|
|
16
|
+
exitCode?: number;
|
|
17
|
+
}
|
|
11
18
|
export interface DispatchOptions {
|
|
12
19
|
agent: string;
|
|
13
20
|
prompt: string;
|
|
@@ -18,9 +25,19 @@ export interface DispatchOptions {
|
|
|
18
25
|
follow?: boolean;
|
|
19
26
|
timeoutMs?: number;
|
|
20
27
|
}
|
|
21
|
-
|
|
22
|
-
task: HostTask;
|
|
23
|
-
/** Exit code when followed; undefined when detached (--no-follow). */
|
|
24
|
-
exitCode?: number;
|
|
25
|
-
}
|
|
28
|
+
/** Dispatch an `agents run <agent> "<prompt>"` onto a host (the `run --host` path). */
|
|
26
29
|
export declare function dispatchToHost(host: Host, opts: DispatchOptions): Promise<DispatchResult>;
|
|
30
|
+
export interface CommandDispatchOptions {
|
|
31
|
+
/** `agents …` args (command name first), already stripped of routing flags. */
|
|
32
|
+
forwardedArgs: string[];
|
|
33
|
+
remoteCwd?: string;
|
|
34
|
+
follow?: boolean;
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Dispatch an arbitrary long-running `agents <command>` onto a host detached —
|
|
39
|
+
* used for `teams start --watch --host`, whose supervisor must outlive the SSH
|
|
40
|
+
* connection. Reachability is assumed (the caller has already resolved the host);
|
|
41
|
+
* a launch failure surfaces the remote stderr.
|
|
42
|
+
*/
|
|
43
|
+
export declare function dispatchAgentsCommand(host: Host, opts: CommandDispatchOptions): Promise<DispatchResult>;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dispatch a headless
|
|
2
|
+
* Dispatch a headless `agents …` command onto a host over SSH.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
* log and its exit code to a sibling `.exit` file, so progress survives a
|
|
6
|
-
* connection (followed via offset-tail in progress.ts). This is the
|
|
7
|
-
* the
|
|
4
|
+
* The command is launched detached (`nohup … &`) writing combined output to a
|
|
5
|
+
* remote log and its exit code to a sibling `.exit` file, so progress survives a
|
|
6
|
+
* dropped connection (followed via offset-tail in progress.ts). This is the
|
|
7
|
+
* offload win: the process/thread/file fan-out happens on the host, not the
|
|
8
|
+
* laptop. `agents run` uses it; `agents teams start --watch --host` reuses the
|
|
9
|
+
* same core so a remote team supervisor keeps running after you disconnect.
|
|
8
10
|
*/
|
|
9
11
|
import { randomUUID } from 'crypto';
|
|
10
12
|
import { sshExec, shellQuote } from '../ssh-exec.js';
|
|
@@ -16,25 +18,22 @@ import { followHostTask } from './progress.js';
|
|
|
16
18
|
// regardless of the run's cwd. Task ids are 8 hex chars, so these paths are
|
|
17
19
|
// injection-safe to interpolate unquoted into remote commands.
|
|
18
20
|
const REMOTE_DIR = '$HOME/.agents/.cache/hosts';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The launch + task-record + optional follow core. Both `dispatchToHost` (run)
|
|
23
|
+
* and `dispatchAgentsCommand` (teams) build their `forwardedArgs` and call here,
|
|
24
|
+
* so the nohup/exit-file/offset-tail machinery lives in exactly one place.
|
|
25
|
+
*/
|
|
26
|
+
async function launchDetached(host, target, opts) {
|
|
24
27
|
const id = randomUUID().slice(0, 8);
|
|
25
28
|
const remoteLog = `${REMOTE_DIR}/${id}.log`;
|
|
26
29
|
const remoteExit = `${REMOTE_DIR}/${id}.exit`;
|
|
27
30
|
// Inner command run under a login shell so PATH resolves `agents`.
|
|
28
|
-
const
|
|
29
|
-
if (opts.mode)
|
|
30
|
-
runParts.push('--mode', shellQuote(opts.mode));
|
|
31
|
-
if (opts.model)
|
|
32
|
-
runParts.push('--model', shellQuote(opts.model));
|
|
31
|
+
const invocation = ['agents', ...opts.forwardedArgs].map(shellQuote).join(' ');
|
|
33
32
|
const cwd = opts.remoteCwd ? `cd ${shellQuote(opts.remoteCwd)} && ` : '';
|
|
34
|
-
const inner = `${cwd}${
|
|
33
|
+
const inner = `${cwd}${invocation} > ${remoteLog} 2>&1; echo $? > ${remoteExit}`;
|
|
35
34
|
// Outer: ensure dir, launch detached under bash -lc, print the PID.
|
|
36
35
|
const launch = `mkdir -p ${REMOTE_DIR}; nohup bash -lc ${shellQuote(inner)} >/dev/null 2>&1 & echo $!`;
|
|
37
|
-
const res = sshExec(target, launch, { timeoutMs: 30000 });
|
|
36
|
+
const res = sshExec(target, launch, { timeoutMs: 30000, multiplex: true });
|
|
38
37
|
if (res.code !== 0) {
|
|
39
38
|
throw new Error(`Failed to launch on "${host.name}": ${(res.stderr || res.stdout).trim() || 'ssh error'}`);
|
|
40
39
|
}
|
|
@@ -43,8 +42,8 @@ export async function dispatchToHost(host, opts) {
|
|
|
43
42
|
id,
|
|
44
43
|
host: host.name,
|
|
45
44
|
target,
|
|
46
|
-
agent: opts.
|
|
47
|
-
prompt: opts.
|
|
45
|
+
agent: opts.agentLabel,
|
|
46
|
+
prompt: opts.promptLabel,
|
|
48
47
|
pid: Number.isFinite(pid) ? pid : undefined,
|
|
49
48
|
remoteLog,
|
|
50
49
|
remoteExit,
|
|
@@ -69,3 +68,40 @@ export async function dispatchToHost(host, opts) {
|
|
|
69
68
|
});
|
|
70
69
|
return { task: finished ?? task, exitCode };
|
|
71
70
|
}
|
|
71
|
+
/** Dispatch an `agents run <agent> "<prompt>"` onto a host (the `run --host` path). */
|
|
72
|
+
export async function dispatchToHost(host, opts) {
|
|
73
|
+
const target = sshTargetFor(host);
|
|
74
|
+
const { warnings } = ensureHostReady(host, { agent: opts.agent });
|
|
75
|
+
for (const w of warnings)
|
|
76
|
+
process.stderr.write(`[hosts] warning: ${w}\n`);
|
|
77
|
+
const forwardedArgs = ['run', opts.agent, opts.prompt, '--quiet'];
|
|
78
|
+
if (opts.mode)
|
|
79
|
+
forwardedArgs.push('--mode', opts.mode);
|
|
80
|
+
if (opts.model)
|
|
81
|
+
forwardedArgs.push('--model', opts.model);
|
|
82
|
+
return launchDetached(host, target, {
|
|
83
|
+
forwardedArgs,
|
|
84
|
+
remoteCwd: opts.remoteCwd,
|
|
85
|
+
follow: opts.follow,
|
|
86
|
+
timeoutMs: opts.timeoutMs,
|
|
87
|
+
agentLabel: opts.agent,
|
|
88
|
+
promptLabel: opts.prompt,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Dispatch an arbitrary long-running `agents <command>` onto a host detached —
|
|
93
|
+
* used for `teams start --watch --host`, whose supervisor must outlive the SSH
|
|
94
|
+
* connection. Reachability is assumed (the caller has already resolved the host);
|
|
95
|
+
* a launch failure surfaces the remote stderr.
|
|
96
|
+
*/
|
|
97
|
+
export async function dispatchAgentsCommand(host, opts) {
|
|
98
|
+
const target = sshTargetFor(host);
|
|
99
|
+
return launchDetached(host, target, {
|
|
100
|
+
forwardedArgs: opts.forwardedArgs,
|
|
101
|
+
remoteCwd: opts.remoteCwd,
|
|
102
|
+
follow: opts.follow,
|
|
103
|
+
timeoutMs: opts.timeoutMs,
|
|
104
|
+
agentLabel: opts.forwardedArgs[0] ?? 'agents',
|
|
105
|
+
promptLabel: opts.forwardedArgs.join(' '),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `--host` option registrar. Every command that can run on a remote host
|
|
3
|
+
* declares the flag through here, so its spelling, help text, and companions
|
|
4
|
+
* (`--remote-cwd`, `--no-tty`, `--any`) stay identical everywhere and show up in
|
|
5
|
+
* each command's `--help`.
|
|
6
|
+
*
|
|
7
|
+
* The flags are consumed centrally by `maybeRunOnHost` (passthrough.ts) *before*
|
|
8
|
+
* commander parses, so for a real remote run the local action never sees them.
|
|
9
|
+
* Registering them here still matters: it documents the flag and keeps the local
|
|
10
|
+
* fall-through (e.g. `--host <this-machine>`) from erroring on an unknown option.
|
|
11
|
+
*/
|
|
12
|
+
import type { Command } from 'commander';
|
|
13
|
+
/** Attach the standard `--host` flag family to a command and return it (chainable). */
|
|
14
|
+
export declare function addHostOption(cmd: Command): Command;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `--host` option registrar. Every command that can run on a remote host
|
|
3
|
+
* declares the flag through here, so its spelling, help text, and companions
|
|
4
|
+
* (`--remote-cwd`, `--no-tty`, `--any`) stay identical everywhere and show up in
|
|
5
|
+
* each command's `--help`.
|
|
6
|
+
*
|
|
7
|
+
* The flags are consumed centrally by `maybeRunOnHost` (passthrough.ts) *before*
|
|
8
|
+
* commander parses, so for a real remote run the local action never sees them.
|
|
9
|
+
* Registering them here still matters: it documents the flag and keeps the local
|
|
10
|
+
* fall-through (e.g. `--host <this-machine>`) from erroring on an unknown option.
|
|
11
|
+
*/
|
|
12
|
+
/** Attach the standard `--host` flag family to a command and return it (chainable). */
|
|
13
|
+
export function addHostOption(cmd) {
|
|
14
|
+
return cmd
|
|
15
|
+
.option('-H, --host <name>', 'Run this command on a registered host (or user@host) over SSH instead of locally. See `agents hosts`.')
|
|
16
|
+
.option('--remote-cwd <dir>', 'Working directory on the host for --host runs.')
|
|
17
|
+
.option('--no-tty', 'Force non-interactive output for --host runs even from a terminal.')
|
|
18
|
+
.option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.');
|
|
19
|
+
}
|