@phnx-labs/agents-cli 1.20.73 → 1.20.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +989 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +31 -1
- package/dist/commands/exec.js +31 -3
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +87 -9
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +15 -2
- package/dist/commands/secrets.js +44 -21
- package/dist/commands/sessions-browser.js +1 -1
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.js +16 -1
- package/dist/commands/sessions.d.ts +36 -13
- package/dist/commands/sessions.js +210 -40
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/setup.js +6 -1
- package/dist/commands/ssh.js +71 -35
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +51 -11
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +99 -31
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +14 -4
- package/dist/lib/activity.js +17 -5
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +56 -0
- package/dist/lib/agents.js +133 -7
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +60 -0
- package/dist/lib/daemon.js +18 -2
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +21 -0
- package/dist/lib/devices/fleet.js +26 -0
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +66 -0
- package/dist/lib/exec.js +195 -6
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +2 -0
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +13 -5
- package/dist/lib/hosts/run-target.js +26 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +37 -2
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +25 -0
- package/dist/lib/import.js +107 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/menubar/install-menubar.d.ts +20 -0
- package/dist/lib/menubar/install-menubar.js +45 -1
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +7 -2
- package/dist/lib/overdue.js +18 -11
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/project-resources.js +20 -5
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routines.d.ts +26 -0
- package/dist/lib/routines.js +19 -2
- package/dist/lib/runner.js +104 -7
- package/dist/lib/secrets/agent.d.ts +66 -1
- package/dist/lib/secrets/agent.js +154 -90
- package/dist/lib/secrets/bundles.d.ts +34 -0
- package/dist/lib/secrets/bundles.js +179 -19
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +21 -3
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +114 -7
- package/dist/lib/session/active.js +323 -52
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +48 -0
- package/dist/lib/session/db.js +238 -14
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +465 -2
- package/dist/lib/session/discover.js +1648 -468
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +130 -30
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +22 -10
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.js +6 -2
- package/dist/lib/shims.d.ts +27 -1
- package/dist/lib/shims.js +88 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +15 -36
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +48 -2
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +22 -2
- package/dist/lib/versions.js +98 -10
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
|
@@ -54,6 +54,32 @@ export function planFleetTargets(reg) {
|
|
|
54
54
|
export function remoteFleetTargets(planned, self) {
|
|
55
55
|
return planned.filter((t) => t.device.name !== self && t.skip !== 'control');
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Decide whether a fleet-health target should skip the expensive version+doctor
|
|
59
|
+
* dials (`agents fleet status`). The cheap stats probe (~2.5s, same registry
|
|
60
|
+
* address) has already tried this box one step earlier. If it came back
|
|
61
|
+
* unreachable, dialing `agents --version` (15s) + `agents doctor --json` (30s)
|
|
62
|
+
* would almost certainly fail the same way — just 45s slower — so one
|
|
63
|
+
* genuinely-offline box would stall the whole matrix (the ~60s hang, RUSH-1964).
|
|
64
|
+
*
|
|
65
|
+
* We trust the stats verdict on the DEFAULT path, not only under
|
|
66
|
+
* `--refresh`/`--live`: `probeDeviceStats` and the version/doctor dials share
|
|
67
|
+
* one ssh path (`fleetDialTarget`), so reachability is not per-probe. The
|
|
68
|
+
* verdict is either freshly probed this run or daemon-warmed (~3min) with the
|
|
69
|
+
* live write-back from RUSH-1965, so a box that came online in the last few
|
|
70
|
+
* minutes is the only false-negative window — it renders `unreachable` until the
|
|
71
|
+
* next stats warm, which beats letting it hang the status glance for 45s.
|
|
72
|
+
*
|
|
73
|
+
* An existing skip (control/offline/no-address from {@link planFleetTargets})
|
|
74
|
+
* always wins — those are classified before any probe.
|
|
75
|
+
*/
|
|
76
|
+
export function fleetHealthSkip(currentSkip, stats) {
|
|
77
|
+
if (currentSkip)
|
|
78
|
+
return currentSkip;
|
|
79
|
+
if (stats?.reachable === false)
|
|
80
|
+
return 'unreachable';
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
57
83
|
/** Human label for a skip reason. */
|
|
58
84
|
export function skipLabel(reason) {
|
|
59
85
|
switch (reason) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type DeviceStats } from './health.js';
|
|
2
2
|
import { type HostAuthSummary } from '../auth-health.js';
|
|
3
|
+
import type { OnlineState } from './reachability.js';
|
|
3
4
|
export interface FleetCliStatus {
|
|
4
5
|
installed: boolean;
|
|
5
6
|
path: string | null;
|
|
@@ -32,6 +33,13 @@ export interface FleetHealthRow {
|
|
|
32
33
|
/** Cached auth-health rollup for this host (the Auth column). Undefined when
|
|
33
34
|
* the host has never been probed (`agents fleet ping`) or the cache is cold. */
|
|
34
35
|
auth?: HostAuthSummary;
|
|
36
|
+
/** Resolved online/offline verdict (from {@link deviceOnlineState}). Populated
|
|
37
|
+
* by `runFleetStatus` for the summary view; undefined in the raw grid. */
|
|
38
|
+
online?: OnlineState;
|
|
39
|
+
/** When this box was last seen reachable (ISO), for the "last seen …" note on
|
|
40
|
+
* an offline row. Sourced from the registry's tailscale snapshot / reachability
|
|
41
|
+
* verdict. Undefined when never recorded. */
|
|
42
|
+
lastSeen?: string;
|
|
35
43
|
}
|
|
36
44
|
export interface FleetWarning {
|
|
37
45
|
kind: 'unreachable' | 'drift' | 'cli' | 'version-skew';
|
|
@@ -55,3 +63,33 @@ export declare function renderFleetMatrix(report: FleetHealthReport): string[];
|
|
|
55
63
|
* table carries no timestamped data at all (nothing to date).
|
|
56
64
|
*/
|
|
57
65
|
export declare function freshnessFooter(rows: FleetHealthRow[], now?: number): string | null;
|
|
66
|
+
/** Collapse a long dev build (`0.0.0-dev.<sha>[-dirty]`) to `dev`/`dev-dirty`;
|
|
67
|
+
* released semver is shown verbatim. Keeps the version column narrow and stops
|
|
68
|
+
* a single dev box from widening every row. */
|
|
69
|
+
export declare function shortVersion(version: string | null | undefined): string;
|
|
70
|
+
/** OS bucket label for grouping. Anything unrecognized falls under "Other". */
|
|
71
|
+
export declare function platformGroupLabel(platform: string | undefined): 'macOS' | 'Linux' | 'Windows' | 'Other';
|
|
72
|
+
export interface FleetAttentionItem {
|
|
73
|
+
/** Leading mark: `○` offline, `⚠` config/CLI/version issue. */
|
|
74
|
+
glyph: 'offline' | 'warn';
|
|
75
|
+
subject: string;
|
|
76
|
+
detail: string;
|
|
77
|
+
/** The exact command (or instruction) that fixes it. */
|
|
78
|
+
fix: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The actionable problems only — each with the command that fixes it. Order:
|
|
82
|
+
* offline boxes, CLI gaps, active-version config drift, then version skew.
|
|
83
|
+
* A healthy fleet returns `[]` (the caller prints an all-clear line).
|
|
84
|
+
*/
|
|
85
|
+
export declare function buildFleetAttentionItems(report: FleetHealthReport, now?: number): FleetAttentionItem[];
|
|
86
|
+
/**
|
|
87
|
+
* The default `agents fleet status` view (RUSH-1966): a one-line rollup, a
|
|
88
|
+
* NEEDS ATTENTION list of only the actionable problems (each with its fix
|
|
89
|
+
* command), quiet per-device rows grouped by OS, and an honest footer. The full
|
|
90
|
+
* auth/CLI/sync grid moves behind `--verbose` ({@link renderFleetMatrix}).
|
|
91
|
+
*/
|
|
92
|
+
export declare function renderFleetSummary(report: FleetHealthReport, opts?: {
|
|
93
|
+
self?: string;
|
|
94
|
+
now?: number;
|
|
95
|
+
}): string[];
|
|
@@ -226,3 +226,217 @@ export function freshnessFooter(rows, now = Date.now()) {
|
|
|
226
226
|
return null;
|
|
227
227
|
return ` updated ${parts.join(' · ')} — pass --refresh (--live) for a live probe`;
|
|
228
228
|
}
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Summary view (default): rollup + NEEDS ATTENTION + OS groups + footer.
|
|
231
|
+
// The full grid above is kept for `--verbose`. (RUSH-1966)
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
/** Collapse a long dev build (`0.0.0-dev.<sha>[-dirty]`) to `dev`/`dev-dirty`;
|
|
234
|
+
* released semver is shown verbatim. Keeps the version column narrow and stops
|
|
235
|
+
* a single dev box from widening every row. */
|
|
236
|
+
export function shortVersion(version) {
|
|
237
|
+
if (!version)
|
|
238
|
+
return '—';
|
|
239
|
+
const m = version.match(/-dev\b/);
|
|
240
|
+
if (m)
|
|
241
|
+
return /dirty/.test(version) ? 'dev-dirty' : 'dev';
|
|
242
|
+
return version;
|
|
243
|
+
}
|
|
244
|
+
/** OS bucket label for grouping. Anything unrecognized falls under "Other". */
|
|
245
|
+
export function platformGroupLabel(platform) {
|
|
246
|
+
const p = (platform ?? '').toLowerCase();
|
|
247
|
+
if (p === 'macos' || p === 'darwin')
|
|
248
|
+
return 'macOS';
|
|
249
|
+
if (p === 'linux')
|
|
250
|
+
return 'Linux';
|
|
251
|
+
if (p.startsWith('win'))
|
|
252
|
+
return 'Windows';
|
|
253
|
+
return 'Other';
|
|
254
|
+
}
|
|
255
|
+
const GROUP_ORDER = ['macOS', 'Linux', 'Windows', 'Other'];
|
|
256
|
+
/** Non-fresh sync rows for the ACTIVE (default) version only — the drift that a
|
|
257
|
+
* running install actually feels, not stale/cold counts across old orphans. */
|
|
258
|
+
function activeDriftRows(row) {
|
|
259
|
+
return row.sync.filter((s) => s.isDefault && s.status !== 'fresh');
|
|
260
|
+
}
|
|
261
|
+
function isOffline(row) {
|
|
262
|
+
// Prefer the resolved verdict; fall back to a probe error/skip when the caller
|
|
263
|
+
// didn't populate it (e.g. a raw report). Only a positive 'online' is "up".
|
|
264
|
+
if (row.online)
|
|
265
|
+
return row.online !== 'online';
|
|
266
|
+
return Boolean(row.error || row.skipped);
|
|
267
|
+
}
|
|
268
|
+
/** A box with a real offline verdict — as opposed to `unknown` (registered but
|
|
269
|
+
* never addressed/probed). Only a genuine offline is a "check the box" item;
|
|
270
|
+
* an unknown box is unconfigured, not down. */
|
|
271
|
+
function isGenuinelyOffline(row) {
|
|
272
|
+
return row.online === 'offline' || (!row.online && Boolean(row.error || row.skipped));
|
|
273
|
+
}
|
|
274
|
+
/** A CLI count is only worth flagging when it's stark — the box is missing more
|
|
275
|
+
* than two-thirds of the known agent CLIs (e.g. 1/9), which signals a
|
|
276
|
+
* broken/half-set-up box. A normal partial install (a box that just doesn't run
|
|
277
|
+
* every agent) is not a problem, so this deliberately does NOT fire at 4/9 or
|
|
278
|
+
* 6/9. The full count stays visible under `--verbose`. */
|
|
279
|
+
function starkCliGap(row) {
|
|
280
|
+
const { installed, total } = installedCliCount(row);
|
|
281
|
+
return total > 0 && installed * 3 < total ? { installed, total } : null;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The actionable problems only — each with the command that fixes it. Order:
|
|
285
|
+
* offline boxes, CLI gaps, active-version config drift, then version skew.
|
|
286
|
+
* A healthy fleet returns `[]` (the caller prints an all-clear line).
|
|
287
|
+
*/
|
|
288
|
+
export function buildFleetAttentionItems(report, now = Date.now()) {
|
|
289
|
+
const items = [];
|
|
290
|
+
// 1) Genuinely-offline boxes (not `unknown`/unconfigured), each individually.
|
|
291
|
+
for (const row of report.devices) {
|
|
292
|
+
if (!isGenuinelyOffline(row))
|
|
293
|
+
continue;
|
|
294
|
+
const seen = row.lastSeen ? ` · last seen ${formatCheckedAge(Date.parse(row.lastSeen), now)}` : '';
|
|
295
|
+
items.push({ glyph: 'offline', subject: row.name, detail: `offline${seen}`, fix: 'check the box' });
|
|
296
|
+
}
|
|
297
|
+
// 2) Boxes that need `agents apply` — merge config drift and a stark CLI gap
|
|
298
|
+
// into ONE item per box (both are fixed by the same command, so don't
|
|
299
|
+
// double-list). An offline box's config/CLI is unknowable, so skip it.
|
|
300
|
+
for (const row of report.devices) {
|
|
301
|
+
if (isOffline(row))
|
|
302
|
+
continue;
|
|
303
|
+
const reasons = [];
|
|
304
|
+
if (activeDriftRows(row).length > 0)
|
|
305
|
+
reasons.push('config drift');
|
|
306
|
+
const stark = starkCliGap(row);
|
|
307
|
+
if (stark)
|
|
308
|
+
reasons.push(`only ${stark.installed} of ${stark.total} agent CLIs installed`);
|
|
309
|
+
if (reasons.length > 0) {
|
|
310
|
+
items.push({ glyph: 'warn', subject: row.name, detail: reasons.join(' · '), fix: `agents apply ${row.name}` });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// 3) Version skew across the fleet — one line.
|
|
314
|
+
const skew = report.warnings.find((w) => w.kind === 'version-skew');
|
|
315
|
+
if (skew) {
|
|
316
|
+
const counts = new Map();
|
|
317
|
+
for (const row of report.devices) {
|
|
318
|
+
if (!row.version)
|
|
319
|
+
continue;
|
|
320
|
+
const v = shortVersion(row.version);
|
|
321
|
+
counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
322
|
+
}
|
|
323
|
+
const summary = Array.from(counts.entries())
|
|
324
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
325
|
+
.map(([v, n]) => `${n}× ${v}`)
|
|
326
|
+
.join(' · ');
|
|
327
|
+
items.push({ glyph: 'warn', subject: 'version skew', detail: summary, fix: 'agents upgrade --fleet' });
|
|
328
|
+
}
|
|
329
|
+
return items;
|
|
330
|
+
}
|
|
331
|
+
/** Right-aligned `<content>` on the same line as `left`, clamped so it never
|
|
332
|
+
* overlaps the left text on a narrow terminal. */
|
|
333
|
+
function alignRight(left, right, width) {
|
|
334
|
+
const gap = width - stringWidth(left) - stringWidth(right);
|
|
335
|
+
return gap > 1 ? `${left}${' '.repeat(gap)}${right}` : `${left} ${right}`;
|
|
336
|
+
}
|
|
337
|
+
function headroomWord(row) {
|
|
338
|
+
if (isOffline(row))
|
|
339
|
+
return chalk.gray(row.online === 'unknown' ? 'unknown' : 'offline');
|
|
340
|
+
return headroomLabel(row);
|
|
341
|
+
}
|
|
342
|
+
function loadMemCell(row) {
|
|
343
|
+
if (isOffline(row) || !row.stats?.reachable)
|
|
344
|
+
return chalk.gray('—');
|
|
345
|
+
const load = row.stats.loadPercent === undefined ? '—' : `${Math.round(row.stats.loadPercent)}%`;
|
|
346
|
+
const mem = row.stats.memPercent === undefined ? '—' : `${Math.round(row.stats.memPercent)}%`;
|
|
347
|
+
return `${load} / ${mem}`;
|
|
348
|
+
}
|
|
349
|
+
function versionCell(row) {
|
|
350
|
+
if (isOffline(row))
|
|
351
|
+
return chalk.gray('—');
|
|
352
|
+
return shortVersion(row.version);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The default `agents fleet status` view (RUSH-1966): a one-line rollup, a
|
|
356
|
+
* NEEDS ATTENTION list of only the actionable problems (each with its fix
|
|
357
|
+
* command), quiet per-device rows grouped by OS, and an honest footer. The full
|
|
358
|
+
* auth/CLI/sync grid moves behind `--verbose` ({@link renderFleetMatrix}).
|
|
359
|
+
*/
|
|
360
|
+
export function renderFleetSummary(report, opts = {}) {
|
|
361
|
+
const now = opts.now ?? Date.now();
|
|
362
|
+
const width = terminalWidth();
|
|
363
|
+
const rows = report.devices;
|
|
364
|
+
if (rows.length === 0)
|
|
365
|
+
return [chalk.gray('No registered devices. Run `agents devices` to register some.')];
|
|
366
|
+
const online = rows.filter((r) => !isOffline(r)).length;
|
|
367
|
+
const offline = rows.length - online;
|
|
368
|
+
const oldestStats = oldestAcross(rows, (r) => r.stats?.fetchedAt);
|
|
369
|
+
const cachedAge = oldestStats != null ? `cached ${formatCheckedAge(oldestStats, now)}` : null;
|
|
370
|
+
const rollupRight = [opts.self, cachedAge].filter(Boolean).join(' · ');
|
|
371
|
+
const rollupLeft = `${chalk.bold('Fleet')} ${chalk.green('●')} ${online} online ${chalk.gray('○')} ${offline} offline`;
|
|
372
|
+
const lines = [rollupRight ? alignRight(rollupLeft, chalk.gray(rollupRight), width) : rollupLeft, ''];
|
|
373
|
+
const items = buildFleetAttentionItems(report, now);
|
|
374
|
+
if (items.length === 0) {
|
|
375
|
+
lines.push(chalk.green('Everything looks healthy.'), '');
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
lines.push(chalk.bold(`NEEDS ATTENTION (${items.length})`));
|
|
379
|
+
const subjW = Math.max(...items.map((i) => i.subject.length));
|
|
380
|
+
const detailW = Math.max(...items.map((i) => stringWidth(i.detail)));
|
|
381
|
+
for (const it of items) {
|
|
382
|
+
const glyph = it.glyph === 'offline' ? chalk.gray('○') : chalk.yellow('⚠');
|
|
383
|
+
const subject = it.glyph === 'offline' ? chalk.gray(it.subject) : it.subject;
|
|
384
|
+
lines.push(` ${glyph} ${padToWidth(subject, subjW)} ${padToWidth(it.detail, detailW)} ${chalk.cyan(`→ ${it.fix}`)}`);
|
|
385
|
+
}
|
|
386
|
+
lines.push('');
|
|
387
|
+
}
|
|
388
|
+
// Per-device rows, grouped by OS. Within a group, this machine floats to the
|
|
389
|
+
// top; the rest stay alphabetical.
|
|
390
|
+
const nameW = Math.min(18, Math.max(6, ...rows.map((r) => r.name.length)));
|
|
391
|
+
const headW = Math.max(...rows.map((r) => stringWidth(headroomWord(r))));
|
|
392
|
+
const loadW = Math.max(...rows.map((r) => stringWidth(loadMemCell(r))));
|
|
393
|
+
const verW = Math.max(7, ...rows.map((r) => stringWidth(versionCell(r))));
|
|
394
|
+
const grouped = new Map();
|
|
395
|
+
for (const r of rows) {
|
|
396
|
+
const g = platformGroupLabel(r.platform);
|
|
397
|
+
(grouped.get(g) ?? grouped.set(g, []).get(g)).push(r);
|
|
398
|
+
}
|
|
399
|
+
for (const group of GROUP_ORDER) {
|
|
400
|
+
const members = grouped.get(group);
|
|
401
|
+
if (!members || members.length === 0)
|
|
402
|
+
continue;
|
|
403
|
+
members.sort((a, b) => (a.name === opts.self ? -1 : b.name === opts.self ? 1 : 0) || a.name.localeCompare(b.name));
|
|
404
|
+
lines.push(chalk.bold(group));
|
|
405
|
+
for (const row of members) {
|
|
406
|
+
const isSelf = row.name === opts.self;
|
|
407
|
+
const prefix = isSelf ? chalk.cyan('▸') : ' ';
|
|
408
|
+
const marks = [];
|
|
409
|
+
if (!isOffline(row)) {
|
|
410
|
+
const stark = starkCliGap(row);
|
|
411
|
+
if (stark)
|
|
412
|
+
marks.push(chalk.yellow(`⚠ CLIs ${stark.installed}/${stark.total}`));
|
|
413
|
+
}
|
|
414
|
+
else if (isGenuinelyOffline(row) && row.lastSeen) {
|
|
415
|
+
marks.push(chalk.gray(`last seen ${formatCheckedAge(Date.parse(row.lastSeen), now)}`));
|
|
416
|
+
}
|
|
417
|
+
if (isSelf)
|
|
418
|
+
marks.push(chalk.cyan('← this machine'));
|
|
419
|
+
const note = marks.length ? ` ${marks.join(' ')}` : '';
|
|
420
|
+
lines.push(` ${prefix} ${padToWidth(truncateToWidth(row.name, nameW), nameW)} ` +
|
|
421
|
+
`${padToWidth(headroomWord(row), headW)} ` +
|
|
422
|
+
`${padToWidth(loadMemCell(row), loadW)} ` +
|
|
423
|
+
`${padToWidth(versionCell(row), verW)}` +
|
|
424
|
+
note);
|
|
425
|
+
}
|
|
426
|
+
lines.push('');
|
|
427
|
+
}
|
|
428
|
+
// Footer: orphan-version nudge (a `prune` concern, not drift), then an honest
|
|
429
|
+
// freshness line naming the cache age and what --live/--verbose add.
|
|
430
|
+
const orphaned = rows.filter((r) => r.orphans.length > 0).length;
|
|
431
|
+
if (orphaned > 0) {
|
|
432
|
+
const subject = orphaned === 1 ? '1 device carries' : `${orphaned} devices carry`;
|
|
433
|
+
lines.push(chalk.gray(`${subject} orphaned versions · run ${chalk.cyan('agents prune')} to reclaim disk`));
|
|
434
|
+
}
|
|
435
|
+
const freshBits = [
|
|
436
|
+
cachedAge,
|
|
437
|
+
'pass --live to re-probe auth + reachability',
|
|
438
|
+
'pass --verbose for the auth/CLI/sync columns',
|
|
439
|
+
].filter(Boolean);
|
|
440
|
+
lines.push(chalk.gray(freshBits.join(' · ')));
|
|
441
|
+
return lines;
|
|
442
|
+
}
|
|
@@ -182,7 +182,10 @@ export function probeDeviceStats(device, opts = {}) {
|
|
|
182
182
|
// remote login shell, which evaluates the snippet's `;`/`||` directly — no
|
|
183
183
|
// `sh -c` wrapper needed (and a wrapper would only re-quote the first token).
|
|
184
184
|
// For powershell devices it base64-encodes the snippet instead.
|
|
185
|
-
|
|
185
|
+
// agentOnly: this is a read-only stats probe. A password-auth device must
|
|
186
|
+
// resolve its bundle broker-only — never force a foreground Touch ID sheet
|
|
187
|
+
// just to render the load/mem columns of `agents devices` (RUSH-1970).
|
|
188
|
+
({ args, env } = buildSshInvocation(device, [isWin ? WIN_PROBE_SNIPPET : PROBE_SNIPPET], shim, {}, { agentOnly: true }));
|
|
186
189
|
}
|
|
187
190
|
catch {
|
|
188
191
|
return Promise.resolve({ host, reachable: false, fetchedAt });
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One reachability resolver for the fleet render (RUSH-1965).
|
|
3
|
+
*
|
|
4
|
+
* "Is this device online?" had three disagreeing answers: the live SSH probe
|
|
5
|
+
* (`DeviceStats.reachable`, display-only), the persisted registry verdict, and
|
|
6
|
+
* the cached `tailscale.online` snapshot that the online/offline word actually
|
|
7
|
+
* read. A box reachable every day but whose tailscale snapshot was stale (or a
|
|
8
|
+
* `via:"manual"` device that never got a tailscale field at all) rendered
|
|
9
|
+
* "offline forever". This module is the single ordering the render + the
|
|
10
|
+
* write-back both go through, so the word and the persisted truth agree.
|
|
11
|
+
*/
|
|
12
|
+
import type { DeviceProfile, DeviceReachability, DeviceRegistry } from './registry.js';
|
|
13
|
+
import type { DeviceStats } from './health.js';
|
|
14
|
+
export type OnlineState = 'online' | 'offline' | 'unknown';
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a device's online/offline state, preferring the freshest signal.
|
|
17
|
+
*
|
|
18
|
+
* Precedence: a live stat from THIS run (the probe that just ran) > the
|
|
19
|
+
* persisted reachability verdict written back by a prior probe > the cached
|
|
20
|
+
* `tailscale.online` snapshot. 'unknown' only when nothing at all is known.
|
|
21
|
+
*/
|
|
22
|
+
export declare function deviceOnlineState(d: DeviceProfile, stats?: DeviceStats): OnlineState;
|
|
23
|
+
/** Build the reachability verdict to persist from a fresh probe stat. Pure. */
|
|
24
|
+
export declare function reachabilityFromStats(d: DeviceProfile, stats: DeviceStats): DeviceReachability;
|
|
25
|
+
/**
|
|
26
|
+
* Collect the reachability verdicts to write back for a fleet's freshly-read
|
|
27
|
+
* stats map. Only devices present in the registry are included (a stat for a
|
|
28
|
+
* name we don't track is dropped rather than resurrected). Pure — the caller
|
|
29
|
+
* hands the result to {@link writeReachability}.
|
|
30
|
+
*/
|
|
31
|
+
export declare function collectReachabilityWriteBacks(reg: DeviceRegistry, statsMap: Map<string, DeviceStats>): Record<string, DeviceReachability>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a device's online/offline state, preferring the freshest signal.
|
|
3
|
+
*
|
|
4
|
+
* Precedence: a live stat from THIS run (the probe that just ran) > the
|
|
5
|
+
* persisted reachability verdict written back by a prior probe > the cached
|
|
6
|
+
* `tailscale.online` snapshot. 'unknown' only when nothing at all is known.
|
|
7
|
+
*/
|
|
8
|
+
export function deviceOnlineState(d, stats) {
|
|
9
|
+
if (stats)
|
|
10
|
+
return stats.reachable ? 'online' : 'offline';
|
|
11
|
+
if (d.reachability)
|
|
12
|
+
return d.reachability.reachable ? 'online' : 'offline';
|
|
13
|
+
if (d.tailscale)
|
|
14
|
+
return d.tailscale.online ? 'online' : 'offline';
|
|
15
|
+
return 'unknown';
|
|
16
|
+
}
|
|
17
|
+
/** Build the reachability verdict to persist from a fresh probe stat. Pure. */
|
|
18
|
+
export function reachabilityFromStats(d, stats) {
|
|
19
|
+
return {
|
|
20
|
+
reachable: stats.reachable,
|
|
21
|
+
via: d.address?.via,
|
|
22
|
+
checkedAt: new Date(stats.fetchedAt).toISOString(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Collect the reachability verdicts to write back for a fleet's freshly-read
|
|
27
|
+
* stats map. Only devices present in the registry are included (a stat for a
|
|
28
|
+
* name we don't track is dropped rather than resurrected). Pure — the caller
|
|
29
|
+
* hands the result to {@link writeReachability}.
|
|
30
|
+
*/
|
|
31
|
+
export function collectReachabilityWriteBacks(reg, statsMap) {
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const [name, stats] of statsMap) {
|
|
34
|
+
const d = reg[name];
|
|
35
|
+
if (!d)
|
|
36
|
+
continue;
|
|
37
|
+
out[name] = reachabilityFromStats(d, stats);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
@@ -40,6 +40,24 @@ export interface DeviceTailscale {
|
|
|
40
40
|
relay?: string;
|
|
41
41
|
lastSeen?: string;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Verdict of the last live SSH reachability probe (RUSH-1965).
|
|
45
|
+
*
|
|
46
|
+
* The live `agents devices` probe already learns whether a box answered right
|
|
47
|
+
* now; this persists that answer so the online/offline word is read from a
|
|
48
|
+
* fresh probe instead of the potentially-stale {@link DeviceTailscale.online}
|
|
49
|
+
* snapshot (which only `agents devices sync` / the daemon ever writes). A
|
|
50
|
+
* `via:"manual"` device — which never gets a tailscale peer entry — gets a
|
|
51
|
+
* reachability verdict this way too, so it stops rendering "offline forever".
|
|
52
|
+
*/
|
|
53
|
+
export interface DeviceReachability {
|
|
54
|
+
/** Whether the last live probe reached the device. */
|
|
55
|
+
reachable: boolean;
|
|
56
|
+
/** Transport the verdict came through — the address kind used to dial it. */
|
|
57
|
+
via?: DeviceAddress['via'];
|
|
58
|
+
/** ISO-8601 timestamp of the probe that produced this verdict. */
|
|
59
|
+
checkedAt: string;
|
|
60
|
+
}
|
|
43
61
|
/** A single registered device. */
|
|
44
62
|
export interface DeviceProfile {
|
|
45
63
|
name: string;
|
|
@@ -49,6 +67,10 @@ export interface DeviceProfile {
|
|
|
49
67
|
address: DeviceAddress;
|
|
50
68
|
auth: DeviceAuth;
|
|
51
69
|
tailscale?: DeviceTailscale;
|
|
70
|
+
/** Last live SSH-probe reachability verdict (RUSH-1965). Preferred over the
|
|
71
|
+
* cached {@link DeviceTailscale.online} snapshot when rendering online/offline,
|
|
72
|
+
* because the live probe reflects whether the box answered right now. */
|
|
73
|
+
reachability?: DeviceReachability;
|
|
52
74
|
/** What the device is for. Absent means `worker` (see {@link DeviceRole}). */
|
|
53
75
|
role?: DeviceRole;
|
|
54
76
|
createdAt: string;
|
|
@@ -88,6 +110,7 @@ export interface DeviceInput {
|
|
|
88
110
|
address?: DeviceAddress;
|
|
89
111
|
auth?: DeviceAuth;
|
|
90
112
|
tailscale?: DeviceTailscale;
|
|
113
|
+
reachability?: DeviceReachability;
|
|
91
114
|
role?: DeviceRole;
|
|
92
115
|
}
|
|
93
116
|
/**
|
|
@@ -96,6 +119,16 @@ export interface DeviceInput {
|
|
|
96
119
|
* platform so the two can never drift. Returns the resulting profile.
|
|
97
120
|
*/
|
|
98
121
|
export declare function upsertDevice(name: string, input: DeviceInput): Promise<DeviceProfile>;
|
|
122
|
+
/**
|
|
123
|
+
* Persist live reachability verdicts for many devices in one locked pass
|
|
124
|
+
* (RUSH-1965). A no-op verdict (same `reachable` and not newer than what's
|
|
125
|
+
* stored) is skipped, so a cache-served `agents devices` render doesn't churn
|
|
126
|
+
* the registry every call; only a flip or a fresher probe writes. Unlike
|
|
127
|
+
* {@link upsertDevice} this does NOT bump `updatedAt` — reachability is
|
|
128
|
+
* transient liveness (like the tailscale snapshot), not a profile edit. Unknown
|
|
129
|
+
* device names are ignored. Returns the names actually updated.
|
|
130
|
+
*/
|
|
131
|
+
export declare function writeReachability(updates: Record<string, DeviceReachability>): Promise<string[]>;
|
|
99
132
|
/** Remove a device. Returns false if it was not registered. */
|
|
100
133
|
export declare function removeDevice(name: string): Promise<boolean>;
|
|
101
134
|
/** Load the set of ignored node names. Missing file => empty set. A malformed
|
|
@@ -178,6 +178,7 @@ export async function upsertDevice(name, input) {
|
|
|
178
178
|
address: input.address ?? prev?.address ?? { via: 'manual' },
|
|
179
179
|
auth: input.auth ?? prev?.auth ?? { method: 'key' },
|
|
180
180
|
tailscale: input.tailscale ?? prev?.tailscale,
|
|
181
|
+
reachability: input.reachability ?? prev?.reachability,
|
|
181
182
|
role: input.role ?? prev?.role,
|
|
182
183
|
createdAt: prev?.createdAt ?? now,
|
|
183
184
|
updatedAt: now,
|
|
@@ -187,6 +188,42 @@ export async function upsertDevice(name, input) {
|
|
|
187
188
|
return merged;
|
|
188
189
|
});
|
|
189
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Persist live reachability verdicts for many devices in one locked pass
|
|
193
|
+
* (RUSH-1965). A no-op verdict (same `reachable` and not newer than what's
|
|
194
|
+
* stored) is skipped, so a cache-served `agents devices` render doesn't churn
|
|
195
|
+
* the registry every call; only a flip or a fresher probe writes. Unlike
|
|
196
|
+
* {@link upsertDevice} this does NOT bump `updatedAt` — reachability is
|
|
197
|
+
* transient liveness (like the tailscale snapshot), not a profile edit. Unknown
|
|
198
|
+
* device names are ignored. Returns the names actually updated.
|
|
199
|
+
*/
|
|
200
|
+
export async function writeReachability(updates) {
|
|
201
|
+
const names = Object.keys(updates);
|
|
202
|
+
if (names.length === 0)
|
|
203
|
+
return [];
|
|
204
|
+
const p = registryPath();
|
|
205
|
+
return withRegistryLock(p, async () => {
|
|
206
|
+
const reg = await loadDevices();
|
|
207
|
+
const changed = [];
|
|
208
|
+
for (const name of names) {
|
|
209
|
+
const prev = reg[name];
|
|
210
|
+
if (!prev)
|
|
211
|
+
continue; // never resurrect a device the user removed
|
|
212
|
+
const next = updates[name];
|
|
213
|
+
const cur = prev.reachability;
|
|
214
|
+
if (cur &&
|
|
215
|
+
cur.reachable === next.reachable &&
|
|
216
|
+
Date.parse(cur.checkedAt) >= Date.parse(next.checkedAt)) {
|
|
217
|
+
continue; // unchanged verdict, no fresher timestamp — skip the write
|
|
218
|
+
}
|
|
219
|
+
reg[name] = { ...prev, reachability: next };
|
|
220
|
+
changed.push(name);
|
|
221
|
+
}
|
|
222
|
+
if (changed.length > 0)
|
|
223
|
+
await saveDevices(reg);
|
|
224
|
+
return changed;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
190
227
|
/** Remove a device. Returns false if it was not registered. */
|
|
191
228
|
export async function removeDevice(name) {
|
|
192
229
|
const p = registryPath();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { splitUserHost } from '../hosts/registry.js';
|
|
2
|
+
import { type DeviceProfile } from './registry.js';
|
|
3
|
+
export { splitUserHost };
|
|
2
4
|
/** A dialable peer: the ssh target, the machine id used to tag its rows, a
|
|
3
5
|
* display name, and the OS family that picks the remote shell dialect. */
|
|
4
6
|
export interface ResolvedSshTarget {
|
|
@@ -7,35 +9,21 @@ export interface ResolvedSshTarget {
|
|
|
7
9
|
name: string;
|
|
8
10
|
os?: string;
|
|
9
11
|
}
|
|
10
|
-
/** Split a `user@host` / `host` token into its login user (if any) and host part. */
|
|
11
|
-
export declare function splitUserHost(token: string): {
|
|
12
|
-
user?: string;
|
|
13
|
-
host: string;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Resolve one `--host`/`--device` token to a concrete ssh target through the
|
|
17
|
-
* registry. Registry hit → the device's real address + platform (so the machine
|
|
18
|
-
* id, route, and OS all match the auto-discovery sweep), with any `user@`
|
|
19
|
-
* overriding the login account. Miss → a literal `user@host` fallback, its OS
|
|
20
|
-
* taken from the host overlay if enrolled. Returns undefined only when the token
|
|
21
|
-
* fails the shared ssh-target injection guard.
|
|
22
|
-
*/
|
|
23
|
-
export declare function resolveSshTarget(token: string, reg: DeviceRegistry): ResolvedSshTarget | undefined;
|
|
24
12
|
/**
|
|
25
13
|
* Resolve a target token to a full {@link DeviceProfile} for `agents ssh`. Same
|
|
26
|
-
* grammar as
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* returns undefined so the caller reports "Unknown
|
|
32
|
-
*
|
|
14
|
+
* grammar as the fan-out, but returns the whole profile (auth, shell, tailscale
|
|
15
|
+
* metadata) `buildSshInvocation` needs. A registered `name` / `user@device`
|
|
16
|
+
* yields that profile (login user overridden by any `user@`); an ad-hoc
|
|
17
|
+
* `user@host` / IP / FQDN literal yields a synthesized key-auth profile. A bare
|
|
18
|
+
* unregistered alias (no `@`/dot) — or an ssh_config-only alias, which `agents
|
|
19
|
+
* ssh` has never dialed — returns undefined so the caller reports "Unknown
|
|
20
|
+
* device" rather than dialing a literal.
|
|
33
21
|
*/
|
|
34
|
-
export declare function resolveDeviceTarget(token: string
|
|
22
|
+
export declare function resolveDeviceTarget(token: string): Promise<DeviceProfile | undefined>;
|
|
35
23
|
/**
|
|
36
|
-
* Resolve an explicit `--host`/`--device` list to dialable targets
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
24
|
+
* Resolve an explicit `--host`/`--device` list to dialable targets. A token that
|
|
25
|
+
* fails the injection guard or names nothing reachable is skipped with a stderr
|
|
26
|
+
* note (never fatal — one bad token must not blank the fan-out). Shared by every
|
|
27
|
+
* cross-machine fan-out so they can never diverge onto two routes.
|
|
40
28
|
*/
|
|
41
29
|
export declare function resolveExplicitTargets(hosts: string[]): Promise<ResolvedSshTarget[]>;
|