@phnx-labs/agents-cli 1.20.35 → 1.20.36
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
package/dist/lib/hooks.js
CHANGED
|
@@ -68,11 +68,15 @@ function getManagedHookPrefixes() {
|
|
|
68
68
|
* absolute Windows paths break in bash because backslashes are stripped as
|
|
69
69
|
* escape characters, whereas ~/... paths expand correctly via the ~/.claude
|
|
70
70
|
* symlink/junction on both platforms.
|
|
71
|
+
*
|
|
72
|
+
* `home` and `sep` are injectable so the Windows behavior (backslash sep,
|
|
73
|
+
* drive-letter home) is unit-testable on a POSIX CI host — pass sep='\\' to
|
|
74
|
+
* simulate Windows. With the defaults this is byte-identical to reading
|
|
75
|
+
* os.homedir()/path.sep at the call site.
|
|
71
76
|
*/
|
|
72
|
-
function toPortableCommand(absPath) {
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const homeNorm = home.split(path.sep).join('/');
|
|
77
|
+
export function toPortableCommand(absPath, home = os.homedir(), sep = path.sep) {
|
|
78
|
+
const normalized = absPath.split(sep).join('/');
|
|
79
|
+
const homeNorm = home.split(sep).join('/');
|
|
76
80
|
if (normalized.startsWith(homeNorm + '/')) {
|
|
77
81
|
return '~/' + normalized.slice(homeNorm.length + 1);
|
|
78
82
|
}
|
|
@@ -113,8 +117,38 @@ function isManagedHookCommand(command, prefixes) {
|
|
|
113
117
|
}
|
|
114
118
|
return false;
|
|
115
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Per-version-home command detection. Sync copies each hook script into the
|
|
122
|
+
* active version's home and registers the command by that version-scoped path
|
|
123
|
+
* (`~/.agents/.history/versions/<agent>/<version>/home/…`). Because the path
|
|
124
|
+
* embeds the version number, a later version's sync appends a fresh set whose
|
|
125
|
+
* paths never string-match (and thus never prune) the prior version's entries —
|
|
126
|
+
* so entries for every version installed over time pile up in one settings
|
|
127
|
+
* file, and once a version is removed its entries become dead hooks that error
|
|
128
|
+
* on every tool call. `versionHomeIdentity` extracts the `<agent>/<version>` a
|
|
129
|
+
* command (or home path) belongs to so stale sibling-version entries can be
|
|
130
|
+
* pruned. Returns null for any path outside a per-version home (system hooks,
|
|
131
|
+
* the user's own custom hooks) — those are never a prune target.
|
|
132
|
+
*/
|
|
133
|
+
const VERSION_HOME_SEGMENT_RE = /\.history\/versions\/([^/]+)\/([^/]+)\/home(?:\/|$)/;
|
|
134
|
+
function versionHomeIdentity(commandOrPath) {
|
|
135
|
+
const norm = commandOrPath.split(/[\\/]/).join('/');
|
|
136
|
+
const m = VERSION_HOME_SEGMENT_RE.exec(norm);
|
|
137
|
+
return m ? { agent: m[1], version: m[2] } : null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* True when `command` points into a DIFFERENT version home of the same agent as
|
|
141
|
+
* `current` — i.e. a stale entry left behind by an earlier version's sync.
|
|
142
|
+
* Non-version-home commands (system + user-custom hooks) always return false.
|
|
143
|
+
*/
|
|
144
|
+
function isStaleSiblingVersionCommand(command, current) {
|
|
145
|
+
if (!current)
|
|
146
|
+
return false;
|
|
147
|
+
const id = versionHomeIdentity(command);
|
|
148
|
+
return id !== null && id.agent === current.agent && id.version !== current.version;
|
|
149
|
+
}
|
|
116
150
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
|
|
117
|
-
import { generateHookShim, parseCacheConfig, removeHookShim } from './hooks/cache.js';
|
|
151
|
+
import { generateHookShim, isValidHookShimName, parseCacheConfig, removeHookShim } from './hooks/cache.js';
|
|
118
152
|
import { getHookShimsDir } from './state.js';
|
|
119
153
|
/**
|
|
120
154
|
* Resolve the command path to register for a hook.
|
|
@@ -128,6 +162,8 @@ function resolveHookCommand(name, hookDef, resolveScript) {
|
|
|
128
162
|
const scriptPath = resolveScript(hookDef.script);
|
|
129
163
|
if (!scriptPath)
|
|
130
164
|
return null;
|
|
165
|
+
if (!isValidHookShimName(name))
|
|
166
|
+
return null;
|
|
131
167
|
const cache = parseCacheConfig(hookDef.cache);
|
|
132
168
|
if (!cache) {
|
|
133
169
|
// No caching opted in — make sure a previously generated shim from an
|
|
@@ -715,6 +751,24 @@ export function parseHookManifest(opts = {}) {
|
|
|
715
751
|
}
|
|
716
752
|
catch { /* skip unreadable manifest */ }
|
|
717
753
|
}
|
|
754
|
+
// Extra-repo layer: hooks: section of each enabled extra repo's agents.yaml.
|
|
755
|
+
// Sits above system but below user, mirroring resolveHookScriptPath's
|
|
756
|
+
// first-found order (user > extra > system). Without this layer the script
|
|
757
|
+
// path of an extra-repo hook resolves but its events never register (#602).
|
|
758
|
+
// Earlier extras win over later ones, so iterate in reverse: the last write
|
|
759
|
+
// for a given name comes from the earliest-registered repo.
|
|
760
|
+
for (const { dir } of [...getEnabledExtraRepos()].reverse()) {
|
|
761
|
+
const extraMetaPath = path.join(dir, 'agents.yaml');
|
|
762
|
+
if (!fs.existsSync(extraMetaPath))
|
|
763
|
+
continue;
|
|
764
|
+
try {
|
|
765
|
+
const meta = yaml.parse(fs.readFileSync(extraMetaPath, 'utf-8'));
|
|
766
|
+
if (meta?.hooks)
|
|
767
|
+
for (const [name, def] of Object.entries(meta.hooks))
|
|
768
|
+
merged[name] = def;
|
|
769
|
+
}
|
|
770
|
+
catch { /* skip unreadable extra-repo manifest */ }
|
|
771
|
+
}
|
|
718
772
|
// User layer: hooks: section of agents.yaml.
|
|
719
773
|
const userMetaPath = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
720
774
|
if (fs.existsSync(userMetaPath)) {
|
|
@@ -989,15 +1043,21 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
|
|
|
989
1043
|
if (resolved)
|
|
990
1044
|
currentManifestPaths.add(resolved);
|
|
991
1045
|
}
|
|
992
|
-
//
|
|
993
|
-
//
|
|
1046
|
+
// Identity of the version home being synced. Used to prune entries that point
|
|
1047
|
+
// at a DIFFERENT version home of the same agent (see isStaleSiblingVersionCommand).
|
|
1048
|
+
const currentVh = versionHomeIdentity(versionHome);
|
|
1049
|
+
// Remove stale entries: any hook command under a managed root that isn't in
|
|
1050
|
+
// the current manifest is a leftover from a renamed/deleted hook script; any
|
|
1051
|
+
// command pointing at a sibling version's home is a leftover from that
|
|
1052
|
+
// version's sync (its version-scoped path never matches the current set).
|
|
994
1053
|
for (const eventEntries of Object.values(hooks)) {
|
|
995
1054
|
if (!Array.isArray(eventEntries))
|
|
996
1055
|
continue;
|
|
997
1056
|
for (const group of eventEntries) {
|
|
998
1057
|
if (!group.hooks)
|
|
999
1058
|
continue;
|
|
1000
|
-
group.hooks = group.hooks.filter((h) => !isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command))
|
|
1059
|
+
group.hooks = group.hooks.filter((h) => (!isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command)) &&
|
|
1060
|
+
!isStaleSiblingVersionCommand(h.command, currentVh));
|
|
1001
1061
|
}
|
|
1002
1062
|
}
|
|
1003
1063
|
// Remove empty matcher groups left after cleanup
|
|
@@ -1049,6 +1109,65 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
|
|
|
1049
1109
|
}
|
|
1050
1110
|
return { registered, errors };
|
|
1051
1111
|
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Prune every Claude-family (`settings.json`) hook entry whose command lives
|
|
1114
|
+
* under a removed version's home
|
|
1115
|
+
* (`~/.agents/.history/versions/<agent>/<removedVersion>/home/…`).
|
|
1116
|
+
*
|
|
1117
|
+
* `agents remove <agent>@<version>` soft-deletes the version's files but leaves
|
|
1118
|
+
* the hook entries other version homes registered against it — dead hooks that
|
|
1119
|
+
* error on every tool call ("No such file or directory") until the next sync.
|
|
1120
|
+
* This clears them from a remaining version's settings immediately. Only the
|
|
1121
|
+
* removed version's entries are touched; the current version's entries, system
|
|
1122
|
+
* hooks, and the user's own custom hooks are left intact. Returns the number of
|
|
1123
|
+
* entries removed.
|
|
1124
|
+
*/
|
|
1125
|
+
export function pruneVersionHomeHookEntriesFromSettings(settingsPath, agent, removedVersion) {
|
|
1126
|
+
if (!fs.existsSync(settingsPath))
|
|
1127
|
+
return 0;
|
|
1128
|
+
let config;
|
|
1129
|
+
try {
|
|
1130
|
+
config = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
return 0;
|
|
1134
|
+
}
|
|
1135
|
+
if (!config.hooks || typeof config.hooks !== 'object')
|
|
1136
|
+
return 0;
|
|
1137
|
+
const hooks = config.hooks;
|
|
1138
|
+
let removed = 0;
|
|
1139
|
+
for (const eventEntries of Object.values(hooks)) {
|
|
1140
|
+
if (!Array.isArray(eventEntries))
|
|
1141
|
+
continue;
|
|
1142
|
+
for (const group of eventEntries) {
|
|
1143
|
+
if (!group.hooks)
|
|
1144
|
+
continue;
|
|
1145
|
+
const before = group.hooks.length;
|
|
1146
|
+
group.hooks = group.hooks.filter((h) => {
|
|
1147
|
+
const id = versionHomeIdentity(h.command);
|
|
1148
|
+
return !(id !== null && id.agent === agent && id.version === removedVersion);
|
|
1149
|
+
});
|
|
1150
|
+
removed += before - group.hooks.length;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (removed === 0)
|
|
1154
|
+
return 0;
|
|
1155
|
+
// Drop matcher groups left empty by the prune.
|
|
1156
|
+
for (const [event, eventEntries] of Object.entries(hooks)) {
|
|
1157
|
+
if (!Array.isArray(eventEntries))
|
|
1158
|
+
continue;
|
|
1159
|
+
hooks[event] = eventEntries.filter((g) => g.hooks && g.hooks.length > 0);
|
|
1160
|
+
}
|
|
1161
|
+
try {
|
|
1162
|
+
fs.writeFileSync(settingsPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
// Best-effort cleanup: a write failure leaves the dead entry to be pruned
|
|
1166
|
+
// on the next sync (see isStaleSiblingVersionCommand).
|
|
1167
|
+
return 0;
|
|
1168
|
+
}
|
|
1169
|
+
return removed;
|
|
1170
|
+
}
|
|
1052
1171
|
function registerHooksForCodex(versionHome, manifest, resolveScript, managedPrefixes) {
|
|
1053
1172
|
const registered = [];
|
|
1054
1173
|
const errors = [];
|
|
@@ -21,10 +21,25 @@ export interface DispatchOptions {
|
|
|
21
21
|
mode?: string;
|
|
22
22
|
model?: string;
|
|
23
23
|
remoteCwd?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Force the remote run's NEW session to use this exact id (Claude only, via
|
|
26
|
+
* `agents run --session-id`). Captured on the task record so the run is
|
|
27
|
+
* resumable by id. Mutually exclusive with `resume`.
|
|
28
|
+
*/
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
/** Resume an existing session on the host by id (via `agents run --resume`). */
|
|
31
|
+
resume?: string;
|
|
24
32
|
/** Stream progress and block until completion (default true). */
|
|
25
33
|
follow?: boolean;
|
|
26
34
|
timeoutMs?: number;
|
|
27
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the remote `agents run …` argv for a host dispatch. Pure so the
|
|
38
|
+
* session-id / resume flag wiring is unit-testable without an SSH round-trip.
|
|
39
|
+
* `--session-id` and `--resume` are mutually exclusive (the CLI rejects both);
|
|
40
|
+
* resume wins when — defensively — both are set.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildRunForwardedArgs(opts: DispatchOptions): string[];
|
|
28
43
|
/** Dispatch an `agents run <agent> "<prompt>"` onto a host (the `run --host` path). */
|
|
29
44
|
export declare function dispatchToHost(host: Host, opts: DispatchOptions): Promise<DispatchResult>;
|
|
30
45
|
export interface CommandDispatchOptions {
|
|
@@ -12,6 +12,8 @@ import { randomUUID } from 'crypto';
|
|
|
12
12
|
import { sshExec, shellQuote } from '../ssh-exec.js';
|
|
13
13
|
import { sshTargetFor } from './types.js';
|
|
14
14
|
import { ensureHostReady } from './ready.js';
|
|
15
|
+
import { remoteShellFor } from './remote-cmd.js';
|
|
16
|
+
import { resolveRemoteOsSync } from './remote-os.js';
|
|
15
17
|
import { saveTask, updateTask, terminalPatch } from './tasks.js';
|
|
16
18
|
import { followHostTask } from './progress.js';
|
|
17
19
|
// Use $HOME (not ~) so the path is correct whether or not it's quoted and
|
|
@@ -22,8 +24,22 @@ const REMOTE_DIR = '$HOME/.agents/.cache/hosts';
|
|
|
22
24
|
* The launch + task-record + optional follow core. Both `dispatchToHost` (run)
|
|
23
25
|
* and `dispatchAgentsCommand` (teams) build their `forwardedArgs` and call here,
|
|
24
26
|
* so the nohup/exit-file/offset-tail machinery lives in exactly one place.
|
|
27
|
+
*
|
|
28
|
+
* Windows remotes are refused up front: the detached launch is only half the
|
|
29
|
+
* contract — the follow/reconcile layer (`progress.ts`/`reconcile.ts`) offset-
|
|
30
|
+
* tails the log with POSIX `tail -c`/`printf`/`cat`/`stat`, which do not exist
|
|
31
|
+
* in cmd.exe/PowerShell. Shipping the launch alone would leave `run --host
|
|
32
|
+
* <windows>` dispatching but hanging on follow forever, so we fail fast with an
|
|
33
|
+
* actionable message instead. Read-only `--host` commands (view/sessions/…) run
|
|
34
|
+
* a single round-trip with no follow protocol and DO work against Windows.
|
|
25
35
|
*/
|
|
26
36
|
async function launchDetached(host, target, opts) {
|
|
37
|
+
if (remoteShellFor(resolveRemoteOsSync(host.name)) === 'powershell') {
|
|
38
|
+
throw new Error(`Detached dispatch to Windows host "${host.name}" is not supported yet — the run ` +
|
|
39
|
+
`follow/reconcile layer is POSIX-only (offset-tails the remote log with tail/cat/stat). ` +
|
|
40
|
+
`Read-only --host commands (view, sessions, usage, cost, doctor, list, teams) do work ` +
|
|
41
|
+
`against Windows.`);
|
|
42
|
+
}
|
|
27
43
|
const id = randomUUID().slice(0, 8);
|
|
28
44
|
const remoteLog = `${REMOTE_DIR}/${id}.log`;
|
|
29
45
|
const remoteExit = `${REMOTE_DIR}/${id}.exit`;
|
|
@@ -45,6 +61,7 @@ async function launchDetached(host, target, opts) {
|
|
|
45
61
|
agent: opts.agentLabel,
|
|
46
62
|
prompt: opts.promptLabel,
|
|
47
63
|
pid: Number.isFinite(pid) ? pid : undefined,
|
|
64
|
+
sessionId: opts.sessionId,
|
|
48
65
|
remoteLog,
|
|
49
66
|
remoteExit,
|
|
50
67
|
status: 'running',
|
|
@@ -67,24 +84,40 @@ async function launchDetached(host, target, opts) {
|
|
|
67
84
|
const finished = exitCode === -1 ? task : (updateTask(id, terminalPatch(exitCode)) ?? task);
|
|
68
85
|
return { task: finished, exitCode };
|
|
69
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Build the remote `agents run …` argv for a host dispatch. Pure so the
|
|
89
|
+
* session-id / resume flag wiring is unit-testable without an SSH round-trip.
|
|
90
|
+
* `--session-id` and `--resume` are mutually exclusive (the CLI rejects both);
|
|
91
|
+
* resume wins when — defensively — both are set.
|
|
92
|
+
*/
|
|
93
|
+
export function buildRunForwardedArgs(opts) {
|
|
94
|
+
const args = ['run', opts.agent, opts.prompt, '--quiet'];
|
|
95
|
+
if (opts.mode)
|
|
96
|
+
args.push('--mode', opts.mode);
|
|
97
|
+
if (opts.model)
|
|
98
|
+
args.push('--model', opts.model);
|
|
99
|
+
if (opts.resume)
|
|
100
|
+
args.push('--resume', opts.resume);
|
|
101
|
+
else if (opts.sessionId)
|
|
102
|
+
args.push('--session-id', opts.sessionId);
|
|
103
|
+
return args;
|
|
104
|
+
}
|
|
70
105
|
/** Dispatch an `agents run <agent> "<prompt>"` onto a host (the `run --host` path). */
|
|
71
106
|
export async function dispatchToHost(host, opts) {
|
|
72
107
|
const target = sshTargetFor(host);
|
|
73
108
|
const { warnings } = ensureHostReady(host, { agent: opts.agent });
|
|
74
109
|
for (const w of warnings)
|
|
75
110
|
process.stderr.write(`[hosts] warning: ${w}\n`);
|
|
76
|
-
const forwardedArgs = ['run', opts.agent, opts.prompt, '--quiet'];
|
|
77
|
-
if (opts.mode)
|
|
78
|
-
forwardedArgs.push('--mode', opts.mode);
|
|
79
|
-
if (opts.model)
|
|
80
|
-
forwardedArgs.push('--model', opts.model);
|
|
81
111
|
return launchDetached(host, target, {
|
|
82
|
-
forwardedArgs,
|
|
112
|
+
forwardedArgs: buildRunForwardedArgs(opts),
|
|
83
113
|
remoteCwd: opts.remoteCwd,
|
|
84
114
|
follow: opts.follow,
|
|
85
115
|
timeoutMs: opts.timeoutMs,
|
|
86
116
|
agentLabel: opts.agent,
|
|
87
117
|
promptLabel: opts.prompt,
|
|
118
|
+
// On resume the remote session keeps its existing id; record that id so the
|
|
119
|
+
// task stays mapped to the same session.
|
|
120
|
+
sessionId: opts.resume ?? opts.sessionId,
|
|
88
121
|
});
|
|
89
122
|
}
|
|
90
123
|
/**
|
package/dist/lib/hosts/logs.js
CHANGED
|
@@ -11,6 +11,7 @@ import chalk from 'chalk';
|
|
|
11
11
|
import { loadTask, localLogPath, updateTask, terminalPatch } from './tasks.js';
|
|
12
12
|
import { followHostTask } from './progress.js';
|
|
13
13
|
import { reconcileTask } from './reconcile.js';
|
|
14
|
+
import { sshExecRaw } from '../ssh-exec.js';
|
|
14
15
|
/** Show (or follow, when running) a dispatched host task's combined-stdout log. */
|
|
15
16
|
export async function showHostTaskLog(id, follow) {
|
|
16
17
|
const task = loadTask(id);
|
|
@@ -39,7 +40,35 @@ export async function showHostTaskLog(id, follow) {
|
|
|
39
40
|
process.stdout.write(fs.readFileSync(localLogPath(id), 'utf-8'));
|
|
40
41
|
}
|
|
41
42
|
catch {
|
|
42
|
-
|
|
43
|
+
// No local log — task was dispatched with --no-follow. Fetch from the remote
|
|
44
|
+
// on demand and cache locally so subsequent calls are instant.
|
|
45
|
+
const remote = fetchAndCacheRemoteLog(task);
|
|
46
|
+
if (remote !== null) {
|
|
47
|
+
process.stdout.write(remote);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
process.stdout.write(chalk.gray('(no local log captured for this task)\n'));
|
|
51
|
+
}
|
|
43
52
|
}
|
|
44
53
|
return { found: true, exitCode: 0 };
|
|
45
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Fetch a task's remote log over SSH, write it to the local mirror path (for
|
|
57
|
+
* future instant reads), and return its content. Returns null when the host is
|
|
58
|
+
* unreachable or the remote log is empty/absent.
|
|
59
|
+
*
|
|
60
|
+
* `remoteLog` is a $HOME-prefixed path with a safe (hex) basename — intentionally
|
|
61
|
+
* unquoted so the remote shell expands $HOME, matching the contract in reconcile.ts
|
|
62
|
+
* and progress.ts.
|
|
63
|
+
*/
|
|
64
|
+
function fetchAndCacheRemoteLog(task) {
|
|
65
|
+
const res = sshExecRaw(task.target, `cat ${task.remoteLog} 2>/dev/null`, { timeoutMs: 30000, multiplex: true });
|
|
66
|
+
if (res.code !== 0 || res.stdout.length === 0)
|
|
67
|
+
return null;
|
|
68
|
+
// The hosts cache dir already exists (saveTask created it) — write is best-effort.
|
|
69
|
+
try {
|
|
70
|
+
fs.writeFileSync(localLogPath(task.id), res.stdout);
|
|
71
|
+
}
|
|
72
|
+
catch { /* best-effort cache */ }
|
|
73
|
+
return res.stdout;
|
|
74
|
+
}
|
package/dist/lib/hosts/option.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
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 a registered host
|
|
15
|
+
.option('-H, --host <name>', 'Run this command on another machine over SSH instead of locally — a device, a registered host, or user@host. See `agents devices` / `agents hosts`.')
|
|
16
16
|
.option('--remote-cwd <dir>', 'Working directory on the host for --host runs.')
|
|
17
17
|
.option('--no-tty', 'Force non-interactive output for --host runs even from a terminal.')
|
|
18
18
|
.option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.');
|
|
@@ -22,6 +22,7 @@ import { resolveHost, resolveHostByCap } from './registry.js';
|
|
|
22
22
|
import { sshTargetFor } from './types.js';
|
|
23
23
|
import { dispatchAgentsCommand } from './dispatch.js';
|
|
24
24
|
import { stripRoutingFlags, buildRemoteAgentsInvocation, HOST_ROUTING_SPECS, } from './remote-cmd.js';
|
|
25
|
+
import { resolveRemoteOsSync } from './remote-os.js';
|
|
25
26
|
import { machineId } from '../session/sync/config.js';
|
|
26
27
|
const REMOTE_PASSTHROUGH = {
|
|
27
28
|
view: {},
|
|
@@ -32,6 +33,7 @@ const REMOTE_PASSTHROUGH = {
|
|
|
32
33
|
list: {},
|
|
33
34
|
sync: { nonInteractive: ['--yes'] },
|
|
34
35
|
teams: {},
|
|
36
|
+
message: {},
|
|
35
37
|
};
|
|
36
38
|
/** `--no-tty` is stripped like the routing flags but carries no value. */
|
|
37
39
|
const STRIP_SPECS = [...HOST_ROUTING_SPECS, { long: 'no-tty', takesValue: false }];
|
|
@@ -130,7 +132,7 @@ export async function maybeRunOnHost(command, allArgs) {
|
|
|
130
132
|
}
|
|
131
133
|
return true;
|
|
132
134
|
}
|
|
133
|
-
const remoteCmd = buildRemoteAgentsInvocation(forwarded, remoteCwd);
|
|
135
|
+
const remoteCmd = buildRemoteAgentsInvocation(forwarded, remoteCwd, resolveRemoteOsSync(host.name));
|
|
134
136
|
const code = sshStream(target, remoteCmd, { tty: interactive, multiplex: true });
|
|
135
137
|
if (code === 255) {
|
|
136
138
|
console.error(chalk.red(`${host.name}: unreachable over SSH (asleep, offline, or host key changed?).`) +
|
|
@@ -8,15 +8,30 @@
|
|
|
8
8
|
import type { Host } from './types.js';
|
|
9
9
|
/** Resolve this CLI's own version by walking up to the nearest package.json. */
|
|
10
10
|
export declare function localCliVersion(): string | null;
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/** ssh command that confirms reachability and echoes the OS. POSIX boxes answer
|
|
12
|
+
* `uname -s`; a Windows target has no `uname` (ssh lands in cmd.exe/PowerShell),
|
|
13
|
+
* so it runs a tiny PowerShell probe. Pure/exported so both branches are
|
|
14
|
+
* unit-testable without ssh. */
|
|
15
|
+
export declare function buildProbeCommand(os?: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Reachability + OS probe over ssh. `os` is the caller's hint (device-registry
|
|
18
|
+
* platform / enrolled `HostEntry.os`); when it marks the host Windows we take
|
|
19
|
+
* the PowerShell path and report that known platform, otherwise POSIX + uname.
|
|
20
|
+
*/
|
|
21
|
+
export declare function probeHost(target: string, os?: string): {
|
|
13
22
|
reachable: boolean;
|
|
14
23
|
os?: string;
|
|
15
24
|
};
|
|
16
|
-
/**
|
|
17
|
-
export declare function
|
|
25
|
+
/** ssh command that prints the remote agents-cli version. Pure/exported. */
|
|
26
|
+
export declare function buildRemoteVersionCommand(os?: string): string;
|
|
27
|
+
/** Remote agents-cli version (PATH-resolved on the remote), or null if not installed. */
|
|
28
|
+
export declare function remoteAgentsVersion(target: string, os?: string): string | null;
|
|
29
|
+
/** ssh command that installs/upgrades agents-cli to `spec` then `agents setup`.
|
|
30
|
+
* PowerShell has no `tail`/`[ -d ]`/`||`, so the Windows branch uses native
|
|
31
|
+
* equivalents (`Select-Object -Last`, `Test-Path`). Pure/exported. */
|
|
32
|
+
export declare function buildBootstrapCommand(spec: string, os?: string): string;
|
|
18
33
|
/** Install (or upgrade to) a specific agents-cli version on the remote, then `agents setup`. */
|
|
19
|
-
export declare function bootstrapAgentsCli(target: string, version: string | null): {
|
|
34
|
+
export declare function bootstrapAgentsCli(target: string, version: string | null, os?: string): {
|
|
20
35
|
ok: boolean;
|
|
21
36
|
output: string;
|
|
22
37
|
};
|
|
@@ -36,7 +51,15 @@ export interface ReadyProbe {
|
|
|
36
51
|
* rather than the exit code, so a command that ran-but-failed is never mistaken
|
|
37
52
|
* for a dead connection (only ssh's own failure drops the sentinel).
|
|
38
53
|
*/
|
|
39
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The one-shot readiness command (version + sentinel + agent listing). The
|
|
56
|
+
* Windows branch emits the sentinel with `Write-Output` and branches on
|
|
57
|
+
* `$LASTEXITCODE` (no `printf`/`||`); `parseReadyProbe` keys off the sentinel
|
|
58
|
+
* substring, so the missing leading newline vs the POSIX `printf '\n…'` form is
|
|
59
|
+
* absorbed by its `.trim()`. Pure/exported so both branches are unit-testable.
|
|
60
|
+
*/
|
|
61
|
+
export declare function buildReadyProbeCommand(os?: string): string;
|
|
62
|
+
export declare function readyProbe(target: string, os?: string): ReadyProbe;
|
|
40
63
|
/** Pure parser for `readyProbe` output (unit-tested without ssh). */
|
|
41
64
|
export declare function parseReadyProbe(stdout: string): ReadyProbe;
|
|
42
65
|
/** True if `view` output lists the named agent (word-boundary, case-insensitive). */
|
package/dist/lib/hosts/ready.js
CHANGED
|
@@ -10,6 +10,8 @@ import * as path from 'path';
|
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { sshExec, shellQuote } from '../ssh-exec.js';
|
|
12
12
|
import { sshTargetFor } from './types.js';
|
|
13
|
+
import { remoteShellFor, buildWindowsAgentsCommand, encodePowershell, powershellQuote } from './remote-cmd.js';
|
|
14
|
+
import { resolveRemoteOsSync } from './remote-os.js';
|
|
13
15
|
/** Resolve this CLI's own version by walking up to the nearest package.json. */
|
|
14
16
|
export function localCliVersion() {
|
|
15
17
|
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -30,29 +32,63 @@ export function localCliVersion() {
|
|
|
30
32
|
}
|
|
31
33
|
return null;
|
|
32
34
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
/** ssh command that confirms reachability and echoes the OS. POSIX boxes answer
|
|
36
|
+
* `uname -s`; a Windows target has no `uname` (ssh lands in cmd.exe/PowerShell),
|
|
37
|
+
* so it runs a tiny PowerShell probe. Pure/exported so both branches are
|
|
38
|
+
* unit-testable without ssh. */
|
|
39
|
+
export function buildProbeCommand(os) {
|
|
40
|
+
if (remoteShellFor(os) === 'powershell') {
|
|
41
|
+
return `powershell -NoProfile -EncodedCommand ${encodePowershell('[System.Environment]::OSVersion.Platform.ToString()')}`;
|
|
42
|
+
}
|
|
43
|
+
return 'uname -s 2>/dev/null || echo unknown';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reachability + OS probe over ssh. `os` is the caller's hint (device-registry
|
|
47
|
+
* platform / enrolled `HostEntry.os`); when it marks the host Windows we take
|
|
48
|
+
* the PowerShell path and report that known platform, otherwise POSIX + uname.
|
|
49
|
+
*/
|
|
50
|
+
export function probeHost(target, os) {
|
|
51
|
+
const r = sshExec(target, buildProbeCommand(os), { timeoutMs: 12000 });
|
|
36
52
|
if (r.code !== 0)
|
|
37
53
|
return { reachable: false };
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
if (remoteShellFor(os) === 'powershell')
|
|
55
|
+
return { reachable: true, os };
|
|
56
|
+
const uname = r.stdout.trim();
|
|
57
|
+
return { reachable: true, os: uname && uname !== 'unknown' ? uname : undefined };
|
|
40
58
|
}
|
|
41
|
-
/**
|
|
42
|
-
export function
|
|
43
|
-
|
|
59
|
+
/** ssh command that prints the remote agents-cli version. Pure/exported. */
|
|
60
|
+
export function buildRemoteVersionCommand(os) {
|
|
61
|
+
return remoteShellFor(os) === 'powershell'
|
|
62
|
+
? buildWindowsAgentsCommand({ args: ['--version'] })
|
|
63
|
+
: 'bash -lc "agents --version 2>/dev/null"';
|
|
64
|
+
}
|
|
65
|
+
/** Remote agents-cli version (PATH-resolved on the remote), or null if not installed. */
|
|
66
|
+
export function remoteAgentsVersion(target, os) {
|
|
67
|
+
const r = sshExec(target, buildRemoteVersionCommand(os), { timeoutMs: 20000 });
|
|
44
68
|
if (r.code !== 0)
|
|
45
69
|
return null;
|
|
46
70
|
const v = r.stdout.trim();
|
|
47
71
|
return v || null;
|
|
48
72
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
73
|
+
/** ssh command that installs/upgrades agents-cli to `spec` then `agents setup`.
|
|
74
|
+
* PowerShell has no `tail`/`[ -d ]`/`||`, so the Windows branch uses native
|
|
75
|
+
* equivalents (`Select-Object -Last`, `Test-Path`). Pure/exported. */
|
|
76
|
+
export function buildBootstrapCommand(spec, os) {
|
|
77
|
+
if (remoteShellFor(os) === 'powershell') {
|
|
78
|
+
const script = `npm install -g ${powershellQuote(spec)} 2>&1 | Select-Object -Last 3; ` +
|
|
79
|
+
`if (-not (Test-Path "$HOME/.agents/.system")) { agents setup 2>&1 | Select-Object -Last 3 }; ` +
|
|
80
|
+
`agents --version`;
|
|
81
|
+
return `powershell -NoProfile -EncodedCommand ${encodePowershell(script)}`;
|
|
82
|
+
}
|
|
52
83
|
const script = `npm install -g ${shellQuote(spec)} 2>&1 | tail -3; ` +
|
|
53
84
|
`if [ ! -d ~/.agents/.system ]; then agents setup 2>&1 | tail -3 || true; fi; ` +
|
|
54
85
|
`agents --version`;
|
|
55
|
-
|
|
86
|
+
return `bash -lc ${shellQuote(script)}`;
|
|
87
|
+
}
|
|
88
|
+
/** Install (or upgrade to) a specific agents-cli version on the remote, then `agents setup`. */
|
|
89
|
+
export function bootstrapAgentsCli(target, version, os) {
|
|
90
|
+
const spec = version ? `@phnx-labs/agents-cli@${version}` : '@phnx-labs/agents-cli';
|
|
91
|
+
const r = sshExec(target, buildBootstrapCommand(spec, os), { timeoutMs: 300000 });
|
|
56
92
|
return { ok: r.code === 0, output: (r.stdout + r.stderr).trim() };
|
|
57
93
|
}
|
|
58
94
|
/** Sentinel splitting the version output from the agent listing in one probe. */
|
|
@@ -65,10 +101,25 @@ const READY_MARKER = '@@AGENTS_READY@@';
|
|
|
65
101
|
* rather than the exit code, so a command that ran-but-failed is never mistaken
|
|
66
102
|
* for a dead connection (only ssh's own failure drops the sentinel).
|
|
67
103
|
*/
|
|
68
|
-
|
|
104
|
+
/**
|
|
105
|
+
* The one-shot readiness command (version + sentinel + agent listing). The
|
|
106
|
+
* Windows branch emits the sentinel with `Write-Output` and branches on
|
|
107
|
+
* `$LASTEXITCODE` (no `printf`/`||`); `parseReadyProbe` keys off the sentinel
|
|
108
|
+
* substring, so the missing leading newline vs the POSIX `printf '\n…'` form is
|
|
109
|
+
* absorbed by its `.trim()`. Pure/exported so both branches are unit-testable.
|
|
110
|
+
*/
|
|
111
|
+
export function buildReadyProbeCommand(os) {
|
|
112
|
+
if (remoteShellFor(os) === 'powershell') {
|
|
113
|
+
const script = `agents --version 2>$null; Write-Output "${READY_MARKER}"; ` +
|
|
114
|
+
`agents view 2>$null; if ($LASTEXITCODE -ne 0) { agents list 2>$null }`;
|
|
115
|
+
return `powershell -NoProfile -EncodedCommand ${encodePowershell(script)}`;
|
|
116
|
+
}
|
|
69
117
|
const script = `agents --version 2>/dev/null; printf '\\n${READY_MARKER}\\n'; ` +
|
|
70
118
|
`agents view 2>/dev/null || agents list 2>/dev/null`;
|
|
71
|
-
|
|
119
|
+
return `bash -lc ${shellQuote(script)}`;
|
|
120
|
+
}
|
|
121
|
+
export function readyProbe(target, os) {
|
|
122
|
+
const r = sshExec(target, buildReadyProbeCommand(os), { timeoutMs: 20000 });
|
|
72
123
|
return parseReadyProbe(r.stdout);
|
|
73
124
|
}
|
|
74
125
|
/** Pure parser for `readyProbe` output (unit-tested without ssh). */
|
|
@@ -92,7 +143,7 @@ export function viewHasAgent(view, agent) {
|
|
|
92
143
|
*/
|
|
93
144
|
export function ensureHostReady(host, opts) {
|
|
94
145
|
const target = sshTargetFor(host);
|
|
95
|
-
const probe = readyProbe(target);
|
|
146
|
+
const probe = readyProbe(target, host.os ?? resolveRemoteOsSync(host.name));
|
|
96
147
|
if (!probe.reachable) {
|
|
97
148
|
throw new Error(`Host "${host.name}" (${target}) is not reachable over SSH. Check it's online and key auth works.`);
|
|
98
149
|
}
|
|
@@ -11,8 +11,25 @@ export declare function getAllProviders(): HostProvider[];
|
|
|
11
11
|
/** Every host across all registered providers, deduped by name (first wins). */
|
|
12
12
|
export declare function listAllHosts(): Promise<Host[]>;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Thrown when a device resolves but can't be used as an offload target because
|
|
15
|
+
* it authenticates with a password. The offload path runs over `sshExec`, whose
|
|
16
|
+
* `SSH_OPTS` force `BatchMode=yes` (no password prompts), so only key / ssh-config
|
|
17
|
+
* auth can carry a `--host` run. Named so the top-level catch prints the message
|
|
18
|
+
* cleanly instead of a stack trace.
|
|
19
|
+
*/
|
|
20
|
+
export declare class DeviceOffloadUnsupportedError extends Error {
|
|
21
|
+
constructor(name: string);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a host name to a single host, or null if unknown. Resolution order:
|
|
25
|
+
* 1. host providers — the `agents hosts` registry (agents.yaml overlay + ssh-config)
|
|
26
|
+
* 2. the devices registry (`agents devices`) — a machine registered once with
|
|
27
|
+
* `agents devices sync` is reachable by `--host`/`--device` with no second enroll
|
|
28
|
+
* 3. an ad-hoc `user@host` (must contain `@`, validated) — nothing to register
|
|
29
|
+
*
|
|
30
|
+
* A bare unknown name returns null so capability-tag routing (`resolveHostByCap`)
|
|
31
|
+
* stays reachable: `--host gpu` must fall through to a cap lookup, not be misread
|
|
32
|
+
* as an ad-hoc target.
|
|
16
33
|
*/
|
|
17
34
|
export declare function resolveHost(name: string): Promise<Host | null>;
|
|
18
35
|
/**
|