@phnx-labs/agents-cli 1.20.78 → 1.20.83
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 +595 -0
- package/README.md +26 -26
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +9 -0
- package/dist/commands/activity.js +153 -24
- package/dist/commands/apply.js +11 -4
- package/dist/commands/cli.js +1 -1
- package/dist/commands/commands.d.ts +1 -1
- package/dist/commands/commands.js +7 -6
- package/dist/commands/defaults.js +3 -11
- package/dist/commands/doctor.d.ts +66 -0
- package/dist/commands/doctor.js +596 -33
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +24 -5
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +87 -11
- package/dist/commands/harness.js +2 -2
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +8 -4
- package/dist/commands/logs.js +6 -0
- package/dist/commands/menubar.js +12 -0
- package/dist/commands/models.js +1 -1
- package/dist/commands/packages.js +6 -6
- package/dist/commands/permissions.js +1 -1
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.js +4 -4
- package/dist/commands/repo.d.ts +78 -0
- package/dist/commands/repo.js +187 -11
- package/dist/commands/routines.d.ts +8 -1
- package/dist/commands/routines.js +241 -77
- package/dist/commands/rules.js +13 -12
- package/dist/commands/secrets.d.ts +3 -1
- package/dist/commands/secrets.js +100 -32
- package/dist/commands/sessions-inject.d.ts +7 -4
- package/dist/commands/sessions-inject.js +18 -18
- package/dist/commands/sessions-picker.js +32 -3
- package/dist/commands/sessions-resume.d.ts +16 -0
- package/dist/commands/sessions-resume.js +14 -13
- package/dist/commands/sessions.d.ts +115 -0
- package/dist/commands/sessions.js +189 -35
- package/dist/commands/set.d.ts +15 -0
- package/dist/commands/set.js +79 -0
- package/dist/commands/setup-fleet.d.ts +21 -0
- package/dist/commands/setup-fleet.js +201 -0
- package/dist/commands/setup-secrets.d.ts +20 -0
- package/dist/commands/setup-secrets.js +223 -0
- package/dist/commands/setup.js +17 -3
- package/dist/commands/skills.js +1 -1
- package/dist/commands/sync.js +7 -2
- package/dist/commands/teams-picker.js +0 -1
- package/dist/commands/teams.d.ts +33 -1
- package/dist/commands/teams.js +208 -44
- package/dist/commands/versions.js +10 -5
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +11 -0
- package/dist/index.js +52 -8
- package/dist/lib/acp/harnesses.js +0 -7
- package/dist/lib/activity.d.ts +156 -0
- package/dist/lib/activity.js +282 -0
- package/dist/lib/agents.d.ts +9 -2
- package/dist/lib/agents.js +72 -63
- package/dist/lib/browser/service.js +3 -0
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/capabilities.js +6 -2
- package/dist/lib/crabbox/lease.js +2 -2
- package/dist/lib/crabbox/setup-copy.d.ts +4 -4
- package/dist/lib/crabbox/setup-copy.js +4 -4
- package/dist/lib/daemon.d.ts +18 -0
- package/dist/lib/daemon.js +47 -6
- package/dist/lib/devices/fleet.d.ts +1 -1
- package/dist/lib/devices/fleet.js +1 -1
- package/dist/lib/doctor-diff.d.ts +20 -0
- package/dist/lib/doctor-diff.js +6 -1
- package/dist/lib/drift.d.ts +31 -11
- package/dist/lib/drift.js +58 -7
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +10 -1
- package/dist/lib/exec.d.ts +7 -0
- package/dist/lib/exec.js +69 -11
- package/dist/lib/feed-post.d.ts +28 -1
- package/dist/lib/feed-post.js +110 -2
- package/dist/lib/fleet/apply.js +8 -1
- package/dist/lib/fleet/auth-sync.d.ts +18 -1
- package/dist/lib/fleet/auth-sync.js +25 -11
- package/dist/lib/fleet/remote-login.js +5 -0
- package/dist/lib/git.d.ts +14 -14
- package/dist/lib/git.js +41 -44
- package/dist/lib/hooks.d.ts +71 -0
- package/dist/lib/hooks.js +245 -95
- package/dist/lib/hosts/dispatch.d.ts +28 -1
- package/dist/lib/hosts/dispatch.js +33 -5
- package/dist/lib/hosts/option.js +2 -2
- package/dist/lib/hosts/passthrough.d.ts +22 -1
- package/dist/lib/hosts/passthrough.js +238 -13
- package/dist/lib/hosts/run-target.d.ts +2 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/hq/floor.d.ts +1 -1
- package/dist/lib/hq/floor.js +6 -1
- package/dist/lib/mcp.js +3 -71
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +24 -0
- package/dist/lib/menubar/install-menubar.js +52 -2
- package/dist/lib/menubar/notify-desktop.d.ts +15 -0
- package/dist/lib/menubar/notify-desktop.js +41 -5
- package/dist/lib/merged-resources.d.ts +11 -0
- package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
- package/dist/lib/permissions.d.ts +0 -32
- package/dist/lib/permissions.js +4 -157
- package/dist/lib/picker.js +1 -1
- package/dist/lib/platform/process.d.ts +16 -5
- package/dist/lib/platform/process.js +54 -0
- package/dist/lib/plugins.d.ts +0 -8
- package/dist/lib/plugins.js +4 -110
- package/dist/lib/project-resources.js +4 -1
- package/dist/lib/refresh.d.ts +5 -3
- package/dist/lib/refresh.js +7 -5
- package/dist/lib/resources/commands.js +4 -1
- package/dist/lib/resources/mcp.js +0 -4
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +1 -5
- package/dist/lib/resources/rules.js +4 -1
- package/dist/lib/resources/skills.js +4 -1
- package/dist/lib/resources/subagents.js +4 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +33 -0
- package/dist/lib/routines.js +122 -0
- package/dist/lib/run-defaults.d.ts +1 -0
- package/dist/lib/run-defaults.js +9 -0
- package/dist/lib/runner.d.ts +0 -12
- package/dist/lib/runner.js +49 -10
- package/dist/lib/scheduler.js +16 -4
- 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 +16 -0
- package/dist/lib/secrets/agent.js +105 -3
- package/dist/lib/secrets/audit.d.ts +46 -0
- package/dist/lib/secrets/audit.js +56 -0
- package/dist/lib/secrets/bundles.d.ts +11 -8
- package/dist/lib/secrets/bundles.js +31 -23
- package/dist/lib/secrets/index.d.ts +2 -1
- package/dist/lib/secrets/index.js +8 -2
- package/dist/lib/secrets/remote.d.ts +75 -2
- package/dist/lib/secrets/remote.js +139 -5
- package/dist/lib/secrets/sync.js +3 -3
- package/dist/lib/session/active.d.ts +128 -26
- package/dist/lib/session/active.js +192 -53
- package/dist/lib/session/actor-sidecar.d.ts +23 -0
- package/dist/lib/session/actor-sidecar.js +101 -0
- package/dist/lib/session/db.d.ts +4 -1
- package/dist/lib/session/db.js +65 -8
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +13 -0
- package/dist/lib/session/hook-sessions.d.ts +9 -0
- package/dist/lib/session/hook-sessions.js +57 -8
- package/dist/lib/session/pid-registry.d.ts +13 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/types.d.ts +16 -0
- package/dist/lib/staleness/detectors/commands.js +2 -2
- package/dist/lib/staleness/detectors/permissions.js +1 -45
- package/dist/lib/staleness/writers/commands.js +4 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -2
- package/dist/lib/startup/command-registry.js +2 -4
- package/dist/lib/state.js +57 -1
- package/dist/lib/subagents-registry.js +5 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -12
- package/dist/lib/teams/agents.d.ts +0 -2
- package/dist/lib/teams/agents.js +10 -27
- package/dist/lib/teams/index.d.ts +1 -1
- package/dist/lib/teams/index.js +1 -1
- package/dist/lib/types.d.ts +17 -9
- package/dist/lib/usage.d.ts +28 -5
- package/dist/lib/usage.js +271 -8
- package/dist/lib/versions.js +7 -1
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +25 -13
- package/package.json +1 -2
- package/dist/commands/check.d.ts +0 -15
- package/dist/commands/check.js +0 -180
- package/dist/commands/resources.d.ts +0 -5
package/dist/lib/hooks.js
CHANGED
|
@@ -13,9 +13,8 @@ import * as os from 'os';
|
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as yaml from 'yaml';
|
|
15
15
|
import * as TOML from 'smol-toml';
|
|
16
|
-
import { AGENTS, agentConfigDirName } from './agents.js';
|
|
16
|
+
import { AGENTS, agentConfigDirName, isAgentHardDeprecated } from './agents.js';
|
|
17
17
|
import { supports, explainSkip, capableAgents } from './capabilities.js';
|
|
18
|
-
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
|
|
19
18
|
import { getHooksDir as getSystemHooksDir, getUserHooksDir, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getTrashHooksDir, getEnabledExtraRepos, getResolvedRulesDir, getUserRulesDir } from './state.js';
|
|
20
19
|
import { collectSubruleHooksFromState } from './rules/compose.js';
|
|
21
20
|
function getCentralHooksDir() { return getUserHooksDir(); }
|
|
@@ -147,9 +146,113 @@ function isStaleSiblingVersionCommand(command, current) {
|
|
|
147
146
|
const id = versionHomeIdentity(command);
|
|
148
147
|
return id !== null && id.agent === current.agent && id.version !== current.version;
|
|
149
148
|
}
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
function hookResourceName(command) {
|
|
150
|
+
const withoutArgs = command.trim().split(/\s+/)[0];
|
|
151
|
+
return path.basename(withoutArgs).replace(/\.[^.]+$/, '');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Collapse version-scoped registrations by logical resource identity rather
|
|
155
|
+
* than absolute command path. When the same resource is present in several
|
|
156
|
+
* homes, the active home's command wins even if it appears later. Commands
|
|
157
|
+
* outside version homes are user-owned and remain untouched.
|
|
158
|
+
*/
|
|
159
|
+
export function deduplicateVersionHookCommands(commands, activeVersionHome) {
|
|
160
|
+
const active = versionHomeIdentity(activeVersionHome);
|
|
161
|
+
if (!active)
|
|
162
|
+
return [...commands];
|
|
163
|
+
const selected = new Map();
|
|
164
|
+
const passthrough = [];
|
|
165
|
+
for (const command of commands) {
|
|
166
|
+
const identity = versionHomeIdentity(command);
|
|
167
|
+
if (!identity || identity.agent !== active.agent) {
|
|
168
|
+
passthrough.push(command);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const key = `${identity.agent}\0${hookResourceName(command)}`;
|
|
172
|
+
const candidateIsActive = identity.version === active.version;
|
|
173
|
+
const prior = selected.get(key);
|
|
174
|
+
if (!prior || (!prior.active && candidateIsActive)) {
|
|
175
|
+
selected.set(key, { command, active: candidateIsActive });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return [...passthrough, ...Array.from(selected.values(), ({ command }) => command)];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Collapse a matcher group's hook entries down to the deduplicated command
|
|
182
|
+
* multiset {@link deduplicateVersionHookCommands} returns, preserving order and
|
|
183
|
+
* the concrete entry objects. The deduped list is honored as a per-command
|
|
184
|
+
* budget, NOT a membership set: two identical active-version entries collapse to
|
|
185
|
+
* one because the budget for that command is one. (A membership `Set.has` test
|
|
186
|
+
* would keep both — the bug this replaces.) Passthrough (user / non-version-home)
|
|
187
|
+
* commands keep their original multiplicity, so genuine user hooks are untouched.
|
|
188
|
+
*/
|
|
189
|
+
function collapseVersionHookEntries(entries, activeVersionHome) {
|
|
190
|
+
const budget = new Map();
|
|
191
|
+
for (const command of deduplicateVersionHookCommands(entries.map((e) => e.command), activeVersionHome)) {
|
|
192
|
+
budget.set(command, (budget.get(command) ?? 0) + 1);
|
|
193
|
+
}
|
|
194
|
+
return entries.filter((e) => {
|
|
195
|
+
const remaining = budget.get(e.command) ?? 0;
|
|
196
|
+
if (remaining <= 0)
|
|
197
|
+
return false;
|
|
198
|
+
budget.set(e.command, remaining - 1);
|
|
199
|
+
return true;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
import { getEffectiveHome, getVersionHomePath, listInstalledVersions, resolveVersion } from './versions.js';
|
|
203
|
+
import { generateHookShim, getHookShimPath, isValidHookShimName, parseCacheConfig, removeHookShim } from './hooks/cache.js';
|
|
152
204
|
import { getHookShimsDir } from './state.js';
|
|
205
|
+
function hookContentHash(scriptPath) {
|
|
206
|
+
return crypto.createHash('sha256').update(fs.readFileSync(scriptPath)).digest('hex');
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Inspect every installed hooks-capable harness without assuming a native
|
|
210
|
+
* settings format. Same-name copies with one content hash are installation
|
|
211
|
+
* noise; multiple hashes are drift. The active version is always selected as
|
|
212
|
+
* the authoritative copy when it participates in the group.
|
|
213
|
+
*/
|
|
214
|
+
export function inspectDuplicateVersionHooks(cwd = process.cwd()) {
|
|
215
|
+
const byResource = new Map();
|
|
216
|
+
for (const { agent, version } of iterHooksCapableVersions()) {
|
|
217
|
+
const activeVersion = resolveVersion(agent, cwd);
|
|
218
|
+
for (const entry of listHooksInVersionHome(agent, version)) {
|
|
219
|
+
let hash;
|
|
220
|
+
try {
|
|
221
|
+
hash = hookContentHash(entry.scriptPath);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const copy = {
|
|
227
|
+
agent,
|
|
228
|
+
version,
|
|
229
|
+
name: entry.name,
|
|
230
|
+
path: entry.scriptPath,
|
|
231
|
+
hash,
|
|
232
|
+
active: version === activeVersion,
|
|
233
|
+
};
|
|
234
|
+
const key = `${agent}\0${entry.name}`;
|
|
235
|
+
const copies = byResource.get(key) ?? [];
|
|
236
|
+
copies.push(copy);
|
|
237
|
+
byResource.set(key, copies);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const findings = [];
|
|
241
|
+
for (const copies of byResource.values()) {
|
|
242
|
+
if (copies.length < 2)
|
|
243
|
+
continue;
|
|
244
|
+
copies.sort((a, b) => a.version.localeCompare(b.version, undefined, { numeric: true }));
|
|
245
|
+
const authoritative = copies.find((copy) => copy.active) ?? copies[copies.length - 1];
|
|
246
|
+
findings.push({
|
|
247
|
+
agent: authoritative.agent,
|
|
248
|
+
name: authoritative.name,
|
|
249
|
+
kind: new Set(copies.map((copy) => copy.hash)).size === 1 ? 'duplicate' : 'drift',
|
|
250
|
+
authoritative,
|
|
251
|
+
copies,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return findings.sort((a, b) => `${a.agent}/${a.name}`.localeCompare(`${b.agent}/${b.name}`));
|
|
255
|
+
}
|
|
153
256
|
/**
|
|
154
257
|
* Resolve the command path to register for a hook.
|
|
155
258
|
*
|
|
@@ -488,6 +591,130 @@ export function getVersionHooksDir(agent, version) {
|
|
|
488
591
|
export function listHooksInVersionHome(agent, version) {
|
|
489
592
|
return listHookEntriesFromDir(getVersionHooksDir(agent, version));
|
|
490
593
|
}
|
|
594
|
+
// ─── wiring inspection (settings.json family: claude, droid) ──────────────────
|
|
595
|
+
/**
|
|
596
|
+
* Agents whose hooks register through {@link registerHooksForClaude} — a native
|
|
597
|
+
* settings.json shaped `hooks[event] = [{ matcher, hooks: [{ command }] }]`, with
|
|
598
|
+
* no event renaming. These are the only agents this read-only wiring inspector
|
|
599
|
+
* understands; every other harness uses a divergent config format and/or event
|
|
600
|
+
* map (Gemini/Antigravity settings.json variants, Codex config.toml, the OpenCode
|
|
601
|
+
* plugin, …), so it reports them unsupported rather than risk a false verdict.
|
|
602
|
+
*/
|
|
603
|
+
const SETTINGS_JSON_HOOK_FAMILY = ['claude', 'droid'];
|
|
604
|
+
/**
|
|
605
|
+
* Verify that every hook the manifest says should be wired for a (claude|droid)
|
|
606
|
+
* version is actually REFERENCED in that version's native settings.json — not
|
|
607
|
+
* merely present as a file on disk.
|
|
608
|
+
*
|
|
609
|
+
* `agents doctor` compares hook FILES against source (see diffHooks in
|
|
610
|
+
* doctor-diff.ts) but never checks the wiring, so a hook whose script is
|
|
611
|
+
* byte-identical to source yet missing from settings.json's PreToolUse/Stop/…
|
|
612
|
+
* array reads as "ok" while it never fires. This closes that blind spot.
|
|
613
|
+
*
|
|
614
|
+
* Read-only by construction: it mirrors registerHooksForClaude's command
|
|
615
|
+
* resolution WITHOUT the shim-generation / chmod side effects that
|
|
616
|
+
* resolveHookCommand performs, so it never mutates the version home.
|
|
617
|
+
*/
|
|
618
|
+
export function checkVersionHookWiring(agent, version) {
|
|
619
|
+
if (!AGENTS[agent].supportsHooks || !SETTINGS_JSON_HOOK_FAMILY.includes(agent)) {
|
|
620
|
+
return { supported: false, unwired: [] };
|
|
621
|
+
}
|
|
622
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
623
|
+
const settingsPath = path.join(versionHome, agentConfigDirName(agent), 'settings.json');
|
|
624
|
+
const localHooksDir = getVersionHooksDir(agent, version);
|
|
625
|
+
// Resolve ONLY to a script that was actually synced for THIS agent+version: the
|
|
626
|
+
// copy in the version home hooks dir, or an absolute subrule-dir path (those are
|
|
627
|
+
// registered in place, never copied). Deliberately NO central-source fallback —
|
|
628
|
+
// sync wires `selectHookManifest(parseHookManifest(), hooksToSync)` where
|
|
629
|
+
// hooksToSync is the per-agent selected set (versions.ts), so a hook the manifest
|
|
630
|
+
// declares but that was never synced into THIS version home (e.g. a claude-scoped
|
|
631
|
+
// hook viewed for droid) must not be expected here. A missing-but-declared hook
|
|
632
|
+
// is a FILE gap that diffHooks reports as `missing`, not a wiring gap.
|
|
633
|
+
// No ensureExecutable — that chmods, and this path must not touch disk.
|
|
634
|
+
const resolveScript = (script) => {
|
|
635
|
+
if (path.isAbsolute(script) && fs.existsSync(script))
|
|
636
|
+
return script;
|
|
637
|
+
return resolveContainedHookPath(localHooksDir, script);
|
|
638
|
+
};
|
|
639
|
+
// Read-only mirror of resolveHookCommand — same command string, no shim write.
|
|
640
|
+
const expectedCommand = (name, hookDef) => {
|
|
641
|
+
const scriptPath = resolveScript(hookDef.script);
|
|
642
|
+
if (!scriptPath)
|
|
643
|
+
return null;
|
|
644
|
+
if (!isValidHookShimName(name))
|
|
645
|
+
return null;
|
|
646
|
+
const cache = parseCacheConfig(hookDef.cache);
|
|
647
|
+
const hasMatches = hookDef.matches != null && Object.keys(hookDef.matches).length > 0;
|
|
648
|
+
if (!cache && !hasMatches)
|
|
649
|
+
return toPortableCommand(scriptPath);
|
|
650
|
+
return toPortableCommand(getHookShimPath(name));
|
|
651
|
+
};
|
|
652
|
+
const manifest = parseHookManifest({ warn: false });
|
|
653
|
+
const expected = [];
|
|
654
|
+
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
655
|
+
if (!hookDef.events || hookDef.events.length === 0)
|
|
656
|
+
continue;
|
|
657
|
+
const command = expectedCommand(name, hookDef);
|
|
658
|
+
if (!command)
|
|
659
|
+
continue; // script unresolved — a file gap, reported by diffHooks
|
|
660
|
+
// Mirror registerHooksForClaude: a hook registers under the matcher group
|
|
661
|
+
// `hookDef.matcher || ''` for each of its events.
|
|
662
|
+
const matcher = hookDef.matcher || '';
|
|
663
|
+
for (const event of hookDef.events)
|
|
664
|
+
expected.push({ name, event, matcher, command });
|
|
665
|
+
}
|
|
666
|
+
if (!fs.existsSync(settingsPath)) {
|
|
667
|
+
return {
|
|
668
|
+
supported: true,
|
|
669
|
+
settingsPath,
|
|
670
|
+
expected: expected.length,
|
|
671
|
+
settingsMissing: expected.length > 0,
|
|
672
|
+
unwired: [],
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
let config;
|
|
676
|
+
try {
|
|
677
|
+
config = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
return {
|
|
681
|
+
supported: true,
|
|
682
|
+
settingsPath,
|
|
683
|
+
expected: expected.length,
|
|
684
|
+
settingsUnparseable: true,
|
|
685
|
+
unwired: [],
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
// Command strings actually referenced, keyed by (event, matcher) — a hook wired
|
|
689
|
+
// under the WRONG matcher group must NOT read as wired, so scope by matcher and
|
|
690
|
+
// not just by event.
|
|
691
|
+
const wiredByGroup = new Map();
|
|
692
|
+
const groupKey = (event, matcher) => `${event}\n${matcher}`;
|
|
693
|
+
const hooks = config.hooks && typeof config.hooks === 'object'
|
|
694
|
+
? config.hooks
|
|
695
|
+
: {};
|
|
696
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
697
|
+
if (!Array.isArray(groups))
|
|
698
|
+
continue;
|
|
699
|
+
for (const group of groups) {
|
|
700
|
+
if (!group || !Array.isArray(group.hooks))
|
|
701
|
+
continue;
|
|
702
|
+
const matcher = typeof group.matcher === 'string' ? group.matcher : '';
|
|
703
|
+
const key = groupKey(event, matcher);
|
|
704
|
+
let cmds = wiredByGroup.get(key);
|
|
705
|
+
if (!cmds) {
|
|
706
|
+
cmds = new Set();
|
|
707
|
+
wiredByGroup.set(key, cmds);
|
|
708
|
+
}
|
|
709
|
+
for (const h of group.hooks) {
|
|
710
|
+
if (h && typeof h.command === 'string')
|
|
711
|
+
cmds.add(h.command);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const unwired = expected.filter((e) => !wiredByGroup.get(groupKey(e.event, e.matcher))?.has(e.command));
|
|
716
|
+
return { supported: true, settingsPath, expected: expected.length, unwired };
|
|
717
|
+
}
|
|
491
718
|
/**
|
|
492
719
|
* Check if a hook installed in a specific version matches central content.
|
|
493
720
|
*/
|
|
@@ -936,6 +1163,9 @@ function sweepOrphanShims(manifest) {
|
|
|
936
1163
|
}
|
|
937
1164
|
}
|
|
938
1165
|
export function registerHooksToSettings(agentId, versionHome, hookManifest, agentsDirOverride) {
|
|
1166
|
+
if (isAgentHardDeprecated(agentId)) {
|
|
1167
|
+
return { registered: [], errors: [] };
|
|
1168
|
+
}
|
|
939
1169
|
const manifest = hookManifest || parseHookManifest();
|
|
940
1170
|
if (Object.keys(manifest).length === 0) {
|
|
941
1171
|
if (agentId === 'opencode') {
|
|
@@ -995,9 +1225,6 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
995
1225
|
if (agentId === 'codex') {
|
|
996
1226
|
return registerHooksForCodex(versionHome, manifest, resolveScript, managedPrefixes);
|
|
997
1227
|
}
|
|
998
|
-
if (agentId === 'gemini') {
|
|
999
|
-
return registerHooksForGemini(versionHome, manifest, resolveScript, managedPrefixes);
|
|
1000
|
-
}
|
|
1001
1228
|
if (agentId === 'antigravity') {
|
|
1002
1229
|
return registerHooksForAntigravity(versionHome, manifest, resolveScript, managedPrefixes);
|
|
1003
1230
|
}
|
|
@@ -1183,16 +1410,6 @@ const ANTIGRAVITY_EVENT_MAP = {
|
|
|
1183
1410
|
Stop: 'on_loop_stop',
|
|
1184
1411
|
OnError: 'on_error',
|
|
1185
1412
|
};
|
|
1186
|
-
/**
|
|
1187
|
-
* Gemini has no native UserPromptSubmit event — map it to BeforeAgent,
|
|
1188
|
-
* the closest lifecycle phase that fires before the model sees the prompt.
|
|
1189
|
-
* Note: gemini's BeforeAgent can only APPEND via additionalContext — it
|
|
1190
|
-
* cannot replace the prompt. The hook script branches on caller to emit
|
|
1191
|
-
* the correct protocol.
|
|
1192
|
-
*/
|
|
1193
|
-
const GEMINI_EVENT_MAP = {
|
|
1194
|
-
UserPromptSubmit: 'BeforeAgent',
|
|
1195
|
-
};
|
|
1196
1413
|
function registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes, configDirName = '.claude') {
|
|
1197
1414
|
const registered = [];
|
|
1198
1415
|
const errors = [];
|
|
@@ -1238,8 +1455,9 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
|
|
|
1238
1455
|
for (const group of eventEntries) {
|
|
1239
1456
|
if (!group.hooks)
|
|
1240
1457
|
continue;
|
|
1241
|
-
|
|
1458
|
+
const managed = group.hooks.filter((h) => (!isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command)) &&
|
|
1242
1459
|
!isStaleSiblingVersionCommand(h.command, currentVh));
|
|
1460
|
+
group.hooks = collapseVersionHookEntries(managed, versionHome);
|
|
1243
1461
|
}
|
|
1244
1462
|
}
|
|
1245
1463
|
// Remove empty matcher groups left after cleanup
|
|
@@ -1387,12 +1605,18 @@ function registerHooksForCodex(versionHome, manifest, resolveScript, managedPref
|
|
|
1387
1605
|
if (resolved)
|
|
1388
1606
|
currentManifestPaths.add(resolved);
|
|
1389
1607
|
}
|
|
1608
|
+
// Identity of the version home being synced. Codex stores hooks in a shared
|
|
1609
|
+
// hooks.json per CODEX_HOME, but agents-cli registers version-scoped command
|
|
1610
|
+
// paths; without this, old version entries keep firing after upgrades.
|
|
1611
|
+
const currentVh = versionHomeIdentity(versionHome);
|
|
1390
1612
|
// Remove stale entries from all event groups
|
|
1391
1613
|
for (const eventGroups of Object.values(hooksFile.hooks)) {
|
|
1392
1614
|
for (const group of eventGroups) {
|
|
1393
1615
|
if (!group.hooks)
|
|
1394
1616
|
continue;
|
|
1395
|
-
|
|
1617
|
+
const managed = group.hooks.filter((h) => (!isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command)) &&
|
|
1618
|
+
!isStaleSiblingVersionCommand(h.command, currentVh));
|
|
1619
|
+
group.hooks = collapseVersionHookEntries(managed, versionHome);
|
|
1396
1620
|
}
|
|
1397
1621
|
}
|
|
1398
1622
|
for (const [event, eventGroups] of Object.entries(hooksFile.hooks)) {
|
|
@@ -1436,7 +1660,8 @@ function registerHooksForCodex(versionHome, manifest, resolveScript, managedPref
|
|
|
1436
1660
|
group.hooks = [];
|
|
1437
1661
|
}
|
|
1438
1662
|
const existingIdx = group.hooks.findIndex((h) => h.command === commandPath);
|
|
1439
|
-
const
|
|
1663
|
+
const eventTimeout = event === 'SessionEnd' ? Math.min(timeout, 3) : timeout;
|
|
1664
|
+
const hookEntry = { type: 'command', command: commandPath, timeout: eventTimeout };
|
|
1440
1665
|
if (existingIdx >= 0) {
|
|
1441
1666
|
group.hooks[existingIdx] = hookEntry;
|
|
1442
1667
|
}
|
|
@@ -1529,81 +1754,6 @@ function registerHooksForCodex(versionHome, manifest, resolveScript, managedPref
|
|
|
1529
1754
|
}
|
|
1530
1755
|
return { registered, errors };
|
|
1531
1756
|
}
|
|
1532
|
-
function registerHooksForGemini(versionHome, manifest, resolveScript, managedPrefixes) {
|
|
1533
|
-
const registered = [];
|
|
1534
|
-
const errors = [];
|
|
1535
|
-
const settingsPath = path.join(versionHome, '.gemini', 'settings.json');
|
|
1536
|
-
try {
|
|
1537
|
-
updateGeminiSettings(settingsPath, (config) => {
|
|
1538
|
-
setGeminiAutoUpdateDisabled(config);
|
|
1539
|
-
if (!config.hooks || typeof config.hooks !== 'object') {
|
|
1540
|
-
config.hooks = {};
|
|
1541
|
-
}
|
|
1542
|
-
const hooks = config.hooks;
|
|
1543
|
-
const currentManifestPaths = new Set();
|
|
1544
|
-
for (const [hookName, hookDef] of Object.entries(manifest)) {
|
|
1545
|
-
if (!hookDef.events || hookDef.events.length === 0)
|
|
1546
|
-
continue;
|
|
1547
|
-
const resolved = resolveHookCommand(hookName, hookDef, resolveScript);
|
|
1548
|
-
if (resolved)
|
|
1549
|
-
currentManifestPaths.add(resolved);
|
|
1550
|
-
}
|
|
1551
|
-
for (const eventEntries of Object.values(hooks)) {
|
|
1552
|
-
if (!Array.isArray(eventEntries))
|
|
1553
|
-
continue;
|
|
1554
|
-
for (const group of eventEntries) {
|
|
1555
|
-
if (!group.hooks)
|
|
1556
|
-
continue;
|
|
1557
|
-
group.hooks = group.hooks.filter((h) => !isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command));
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
for (const [event, eventEntries] of Object.entries(hooks)) {
|
|
1561
|
-
if (!Array.isArray(eventEntries))
|
|
1562
|
-
continue;
|
|
1563
|
-
hooks[event] = eventEntries.filter((g) => g.hooks && g.hooks.length > 0);
|
|
1564
|
-
}
|
|
1565
|
-
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
1566
|
-
if (!hookDef.events || hookDef.events.length === 0)
|
|
1567
|
-
continue;
|
|
1568
|
-
const commandPath = resolveHookCommand(name, hookDef, resolveScript);
|
|
1569
|
-
if (!commandPath) {
|
|
1570
|
-
errors.push(`${name}: script not found in user or system hooks dir`);
|
|
1571
|
-
continue;
|
|
1572
|
-
}
|
|
1573
|
-
const timeoutMs = (hookDef.timeout || 600) * 1000;
|
|
1574
|
-
for (const event of hookDef.events) {
|
|
1575
|
-
const geminiEvent = GEMINI_EVENT_MAP[event] ?? event;
|
|
1576
|
-
if (!hooks[geminiEvent]) {
|
|
1577
|
-
hooks[geminiEvent] = [];
|
|
1578
|
-
}
|
|
1579
|
-
const eventEntries = hooks[geminiEvent];
|
|
1580
|
-
const matcher = hookDef.matcher || '';
|
|
1581
|
-
let matcherGroup = eventEntries.find((e) => (e.matcher || '') === matcher);
|
|
1582
|
-
if (!matcherGroup) {
|
|
1583
|
-
matcherGroup = { matcher, hooks: [] };
|
|
1584
|
-
eventEntries.push(matcherGroup);
|
|
1585
|
-
}
|
|
1586
|
-
if (!matcherGroup.hooks) {
|
|
1587
|
-
matcherGroup.hooks = [];
|
|
1588
|
-
}
|
|
1589
|
-
const existingIdx = matcherGroup.hooks.findIndex((h) => h.command === commandPath);
|
|
1590
|
-
const hookEntry = { name, type: 'command', command: commandPath, timeout: timeoutMs };
|
|
1591
|
-
if (existingIdx >= 0) {
|
|
1592
|
-
matcherGroup.hooks[existingIdx] = hookEntry;
|
|
1593
|
-
}
|
|
1594
|
-
else {
|
|
1595
|
-
matcherGroup.hooks.push(hookEntry);
|
|
1596
|
-
}
|
|
1597
|
-
registered.push(`${name} -> ${geminiEvent}`);
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
});
|
|
1601
|
-
}
|
|
1602
|
-
catch (err) {
|
|
1603
|
-
errors.push(`Failed to write gemini settings.json: ${err.message}`);
|
|
1604
|
-
}
|
|
1605
|
-
return { registered, errors };
|
|
1606
|
-
}
|
|
1607
1757
|
/**
|
|
1608
1758
|
* Register hooks into antigravity's (agy) settings.json. Unlike gemini, agy uses
|
|
1609
1759
|
* a flat per-event array of `{ command }` entries (no matcher groups). Events
|
|
@@ -11,6 +11,18 @@
|
|
|
11
11
|
import type { Host } from './types.js';
|
|
12
12
|
import { type HostTask } from './tasks.js';
|
|
13
13
|
import { type HostCredentials } from './credentials.js';
|
|
14
|
+
/**
|
|
15
|
+
* Derive the remote directory to mirror from the local cwd, for a host run the
|
|
16
|
+
* caller gave no `--cwd`/`--remote-cwd`.
|
|
17
|
+
*
|
|
18
|
+
* Without this a `--host` run lands in the remote `$HOME`, so an agent launched
|
|
19
|
+
* from a repo starts with no project context and the user has to `cd` by hand.
|
|
20
|
+
* Only a cwd under the LOCAL home is mirrored — that is the part with a
|
|
21
|
+
* meaningful remote analogue (`~/src/x` re-roots onto the remote home). A path
|
|
22
|
+
* outside home returns undefined: `/opt/thing` on this box says nothing about
|
|
23
|
+
* the target's filesystem, so the run keeps the remote home.
|
|
24
|
+
*/
|
|
25
|
+
export declare function deriveMirroredCwd(localCwd: string): string | undefined;
|
|
14
26
|
/**
|
|
15
27
|
* Build a `cd <dir> && ` prefix that resolves on the REMOTE host.
|
|
16
28
|
*
|
|
@@ -18,8 +30,19 @@ import { type HostCredentials } from './credentials.js';
|
|
|
18
30
|
* the local one (`/home/<me>` vs `/Users/<me>`). We emit an unquoted `"$HOME"`
|
|
19
31
|
* for that segment — the remote login shell expands it — and shell-quote the
|
|
20
32
|
* remainder. Any other path (absolute or relative) is quoted verbatim.
|
|
33
|
+
*
|
|
34
|
+
* `mirror` marks a directory the caller DERIVED from the local cwd rather than
|
|
35
|
+
* one the user asked for (see `deriveMirroredCwd`). The same repo checked out at
|
|
36
|
+
* the same home-relative path on both boxes is the common fleet layout, so
|
|
37
|
+
* mirroring lands the remote agent in the project instead of `$HOME`. It is a
|
|
38
|
+
* best-effort mirror by definition — the host may simply not have that checkout
|
|
39
|
+
* — so a missing directory falls back to the remote home instead of failing the
|
|
40
|
+
* run. An explicit `--cwd`/`--remote-cwd` is never mirrored: the user named that
|
|
41
|
+
* directory, so a missing one must surface as a `cd` error.
|
|
21
42
|
*/
|
|
22
|
-
export declare function remoteCdPrefix(remoteCwd?: string
|
|
43
|
+
export declare function remoteCdPrefix(remoteCwd?: string, opts?: {
|
|
44
|
+
mirror?: boolean;
|
|
45
|
+
}): string;
|
|
23
46
|
/**
|
|
24
47
|
* Merge the resolved actor's provenance env UNDER a caller-supplied env, so every
|
|
25
48
|
* remote `agents …` invocation forwards `AGENTS_ACTOR*` / `GIT_*` across the SSH
|
|
@@ -103,6 +126,8 @@ export interface DispatchOptions {
|
|
|
103
126
|
/** Native-CLI passthrough (everything after `--`), appended last. */
|
|
104
127
|
passthroughArgs?: string[];
|
|
105
128
|
remoteCwd?: string;
|
|
129
|
+
/** `remoteCwd` was derived from the local cwd — mirror it, don't fail on it. */
|
|
130
|
+
mirrorCwd?: boolean;
|
|
106
131
|
/**
|
|
107
132
|
* Force the remote run's NEW session to use this exact id (Claude only, via
|
|
108
133
|
* `agents run --session-id`). Captured on the task record so the run is
|
|
@@ -165,6 +190,8 @@ export interface InteractiveDispatchOptions {
|
|
|
165
190
|
/** Route through the Agent Client Protocol. */
|
|
166
191
|
acp?: boolean;
|
|
167
192
|
remoteCwd?: string;
|
|
193
|
+
/** `remoteCwd` was derived from the local cwd — mirror it, don't fail on it. */
|
|
194
|
+
mirrorCwd?: boolean;
|
|
168
195
|
sessionId?: string;
|
|
169
196
|
name?: string;
|
|
170
197
|
resume?: string;
|
|
@@ -19,6 +19,7 @@ import { saveTask, updateTask, terminalPatch } from './tasks.js';
|
|
|
19
19
|
import { followHostTask } from './progress.js';
|
|
20
20
|
import { wrapHostCommandWithCredentials } from './credentials.js';
|
|
21
21
|
import { hostKeyCheckingOpts } from '../devices/known-hosts.js';
|
|
22
|
+
import { toRemotePortable } from '../project-root.js';
|
|
22
23
|
// Use $HOME (not ~) so the path is correct whether or not it's quoted and
|
|
23
24
|
// regardless of the run's cwd. Task ids are 8 hex chars, so these paths are
|
|
24
25
|
// injection-safe to interpolate unquoted into remote commands.
|
|
@@ -39,6 +40,21 @@ function homeRemainder(p) {
|
|
|
39
40
|
return p.slice(6);
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Derive the remote directory to mirror from the local cwd, for a host run the
|
|
45
|
+
* caller gave no `--cwd`/`--remote-cwd`.
|
|
46
|
+
*
|
|
47
|
+
* Without this a `--host` run lands in the remote `$HOME`, so an agent launched
|
|
48
|
+
* from a repo starts with no project context and the user has to `cd` by hand.
|
|
49
|
+
* Only a cwd under the LOCAL home is mirrored — that is the part with a
|
|
50
|
+
* meaningful remote analogue (`~/src/x` re-roots onto the remote home). A path
|
|
51
|
+
* outside home returns undefined: `/opt/thing` on this box says nothing about
|
|
52
|
+
* the target's filesystem, so the run keeps the remote home.
|
|
53
|
+
*/
|
|
54
|
+
export function deriveMirroredCwd(localCwd) {
|
|
55
|
+
const portable = toRemotePortable(localCwd);
|
|
56
|
+
return homeRemainder(portable) === null ? undefined : portable;
|
|
57
|
+
}
|
|
42
58
|
/**
|
|
43
59
|
* Build a `cd <dir> && ` prefix that resolves on the REMOTE host.
|
|
44
60
|
*
|
|
@@ -46,15 +62,26 @@ function homeRemainder(p) {
|
|
|
46
62
|
* the local one (`/home/<me>` vs `/Users/<me>`). We emit an unquoted `"$HOME"`
|
|
47
63
|
* for that segment — the remote login shell expands it — and shell-quote the
|
|
48
64
|
* remainder. Any other path (absolute or relative) is quoted verbatim.
|
|
65
|
+
*
|
|
66
|
+
* `mirror` marks a directory the caller DERIVED from the local cwd rather than
|
|
67
|
+
* one the user asked for (see `deriveMirroredCwd`). The same repo checked out at
|
|
68
|
+
* the same home-relative path on both boxes is the common fleet layout, so
|
|
69
|
+
* mirroring lands the remote agent in the project instead of `$HOME`. It is a
|
|
70
|
+
* best-effort mirror by definition — the host may simply not have that checkout
|
|
71
|
+
* — so a missing directory falls back to the remote home instead of failing the
|
|
72
|
+
* run. An explicit `--cwd`/`--remote-cwd` is never mirrored: the user named that
|
|
73
|
+
* directory, so a missing one must surface as a `cd` error.
|
|
49
74
|
*/
|
|
50
|
-
export function remoteCdPrefix(remoteCwd) {
|
|
75
|
+
export function remoteCdPrefix(remoteCwd, opts = {}) {
|
|
51
76
|
if (!remoteCwd)
|
|
52
77
|
return '';
|
|
53
78
|
const rest = homeRemainder(remoteCwd);
|
|
54
79
|
if (rest === '')
|
|
55
80
|
return 'cd "$HOME" && ';
|
|
56
|
-
if (rest !== null)
|
|
57
|
-
|
|
81
|
+
if (rest !== null) {
|
|
82
|
+
const dir = `"$HOME"/${shellQuote(rest)}`;
|
|
83
|
+
return opts.mirror ? `{ cd ${dir} || cd "$HOME"; } && ` : `cd ${dir} && `;
|
|
84
|
+
}
|
|
58
85
|
return `cd ${shellQuote(remoteCwd)} && `;
|
|
59
86
|
}
|
|
60
87
|
/**
|
|
@@ -190,7 +217,7 @@ async function launchDetached(host, target, opts) {
|
|
|
190
217
|
// resolved actor provenance first so the detached remote run inherits it
|
|
191
218
|
// instead of re-resolving from this box's SSH_CONNECTION (RUSH-2028).
|
|
192
219
|
const invocation = ['agents', ...opts.forwardedArgs].map(shellQuote).join(' ');
|
|
193
|
-
const cwd = remoteCdPrefix(opts.remoteCwd);
|
|
220
|
+
const cwd = remoteCdPrefix(opts.remoteCwd, { mirror: opts.mirrorCwd });
|
|
194
221
|
const actorExports = posixEnvExports(withActorEnv());
|
|
195
222
|
const prelude = actorExports ? `${actorExports}; ` : '';
|
|
196
223
|
let inner = `${prelude}${cwd}${invocation} > ${remoteLog} 2>&1; echo $? > ${remoteExit}`;
|
|
@@ -385,7 +412,7 @@ export async function runInteractiveOnHost(host, opts) {
|
|
|
385
412
|
for (const w of warnings)
|
|
386
413
|
process.stderr.write(`[hosts] warning: ${w}\n`);
|
|
387
414
|
const invocation = ['agents', ...buildInteractiveRunForwardedArgs(opts)].map(shellQuote).join(' ');
|
|
388
|
-
const cwd = remoteCdPrefix(opts.remoteCwd);
|
|
415
|
+
const cwd = remoteCdPrefix(opts.remoteCwd, { mirror: opts.mirrorCwd });
|
|
389
416
|
// Forward actor provenance so the interactive remote run inherits it rather
|
|
390
417
|
// than re-resolving from this box's SSH_CONNECTION (RUSH-2028).
|
|
391
418
|
const actorExports = posixEnvExports(withActorEnv());
|
|
@@ -413,6 +440,7 @@ export async function dispatchToHost(host, opts) {
|
|
|
413
440
|
return launchDetached(host, target, {
|
|
414
441
|
forwardedArgs: buildRunForwardedArgs(opts),
|
|
415
442
|
remoteCwd: opts.remoteCwd,
|
|
443
|
+
mirrorCwd: opts.mirrorCwd,
|
|
416
444
|
follow: opts.follow,
|
|
417
445
|
timeoutMs: opts.timeoutMs,
|
|
418
446
|
agentLabel: opts.agent,
|
package/dist/lib/hosts/option.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
/** Attach the standard `--host` flag family to a command and return it (chainable). */
|
|
13
13
|
export function addHostOption(cmd) {
|
|
14
14
|
return cmd
|
|
15
|
-
.option('-H, --host <name>', 'Run this command on another machine over SSH instead of locally — a device, a registered host,
|
|
16
|
-
.option('--device <name>', 'Alias of --host: run this command on a registered device (from `agents devices`).')
|
|
15
|
+
.option('-H, --host <name>', 'Run this command on another machine over SSH instead of locally — a device, a registered host, user@host, or `all` to fan out across every registered device. See `agents devices` / `agents hosts`.')
|
|
16
|
+
.option('--device <name>', 'Alias of --host: run this command on a registered device (from `agents devices`), or `all` to run it across the whole fleet.')
|
|
17
17
|
.option('--remote-cwd <dir>', "Working directory on the host for --host runs. Resolves on the REMOTE host — pass a '$HOME'-relative path (single-quoted so your local shell doesn't expand it) or a valid remote absolute path; a local ~ expands here and won't exist there (/Users/you vs /home/you). No effect on 'teams add'.")
|
|
18
18
|
.option('--no-tty', 'Force non-interactive output for --host runs even from a terminal.')
|
|
19
19
|
.option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.');
|
|
@@ -16,8 +16,28 @@
|
|
|
16
16
|
* table or, when `--host`/`--device` is present, exits with a clear
|
|
17
17
|
* "not supported" message — never commander's raw `unknown option`.
|
|
18
18
|
*/
|
|
19
|
+
import { type DeviceRegistry } from '../devices/registry.js';
|
|
20
|
+
import { runLocalCommand, runOnDevice } from '../devices/fleet.js';
|
|
21
|
+
/** Per-command remote behaviour. Absence from this map = not host-routable here. */
|
|
22
|
+
interface RemoteSpec {
|
|
23
|
+
/** Flags appended when running non-interactively (no local TTY / `--no-tty`). */
|
|
24
|
+
nonInteractive?: string[];
|
|
25
|
+
}
|
|
19
26
|
/** Pull the value of `--host`/`-H`/`--remote-cwd` (any form) out of an argv. */
|
|
20
27
|
export declare function flagValue(args: string[], long: string, short?: string): string | undefined;
|
|
28
|
+
/** Injectable dependencies for {@link runFleetPassthrough} — used by tests. */
|
|
29
|
+
export interface FleetPassthroughOptions {
|
|
30
|
+
/** Override the device registry loader (tests). */
|
|
31
|
+
loadDevices?: () => Promise<DeviceRegistry>;
|
|
32
|
+
/** Override the per-device runner (tests). Defaults to `runOnDevice`. */
|
|
33
|
+
runner?: typeof runOnDevice;
|
|
34
|
+
/** Override the local runner for the self device (tests). Defaults to `runLocalCommand`. */
|
|
35
|
+
localRunner?: typeof runLocalCommand;
|
|
36
|
+
/** Override this machine's id (tests). Defaults to `machineId()`. */
|
|
37
|
+
self?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Run `agents <command> …` across every registered device and render the roster. */
|
|
40
|
+
export declare function runFleetPassthrough(command: string, allArgs: string[], spec: RemoteSpec, opts?: FleetPassthroughOptions): Promise<boolean>;
|
|
21
41
|
/**
|
|
22
42
|
* Route `agents <command> … --host <name>` to a remote if the command is
|
|
23
43
|
* host-routable and a `--host` (or its `--device` alias) was given. Returns
|
|
@@ -30,4 +50,5 @@ export declare function flagValue(args: string[], long: string, short?: string):
|
|
|
30
50
|
* @param command the resolved subcommand name (`process.argv`'s first non-flag).
|
|
31
51
|
* @param allArgs `process.argv.slice(2)` — the command name followed by its args.
|
|
32
52
|
*/
|
|
33
|
-
export declare function maybeRunOnHost(command: string, allArgs: string[]): Promise<boolean>;
|
|
53
|
+
export declare function maybeRunOnHost(command: string, allArgs: string[], opts?: FleetPassthroughOptions): Promise<boolean>;
|
|
54
|
+
export {};
|