@phnx-labs/agents-cli 1.20.67 → 1.20.69
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 +100 -0
- package/README.md +14 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/check.js +96 -2
- package/dist/commands/computer.d.ts +26 -0
- package/dist/commands/computer.js +173 -149
- package/dist/commands/doctor.js +37 -9
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +126 -6
- package/dist/commands/fork.d.ts +9 -0
- package/dist/commands/fork.js +67 -0
- package/dist/commands/plugins.js +11 -2
- package/dist/commands/repo.js +47 -2
- package/dist/commands/run-account-picker.d.ts +14 -0
- package/dist/commands/run-account-picker.js +131 -0
- package/dist/commands/setup-browser.d.ts +18 -0
- package/dist/commands/setup-browser.js +142 -0
- package/dist/commands/setup-computer.d.ts +19 -0
- package/dist/commands/setup-computer.js +133 -0
- package/dist/commands/setup-share.d.ts +17 -0
- package/dist/commands/setup-share.js +85 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +58 -1
- package/dist/commands/share.d.ts +15 -0
- package/dist/commands/share.js +10 -4
- package/dist/commands/ssh.js +267 -2
- package/dist/commands/teams.d.ts +11 -0
- package/dist/commands/teams.js +39 -1
- package/dist/commands/view.d.ts +4 -14
- package/dist/commands/view.js +39 -36
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +21 -5
- package/dist/lib/agents.js +59 -24
- package/dist/lib/auth-health.d.ts +103 -0
- package/dist/lib/auth-health.js +232 -0
- package/dist/lib/browser/chrome.d.ts +9 -0
- package/dist/lib/browser/chrome.js +22 -0
- package/dist/lib/browser/profiles.js +4 -25
- package/dist/lib/cli-entry.d.ts +23 -0
- package/dist/lib/cli-entry.js +116 -0
- package/dist/lib/computer/download.d.ts +51 -0
- package/dist/lib/computer/download.js +145 -0
- package/dist/lib/computer-rpc.js +9 -3
- package/dist/lib/daemon.d.ts +2 -2
- package/dist/lib/daemon.js +8 -89
- package/dist/lib/devices/connect.d.ts +15 -0
- package/dist/lib/devices/connect.js +17 -5
- package/dist/lib/devices/fleet.d.ts +23 -0
- package/dist/lib/devices/fleet.js +38 -0
- package/dist/lib/devices/health-report.d.ts +46 -0
- package/dist/lib/devices/health-report.js +159 -0
- package/dist/lib/devices/health.d.ts +14 -4
- package/dist/lib/devices/health.js +49 -8
- package/dist/lib/devices/terminfo.d.ts +44 -0
- package/dist/lib/devices/terminfo.js +167 -0
- package/dist/lib/git.d.ts +10 -0
- package/dist/lib/git.js +23 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/ready.js +5 -3
- package/dist/lib/hosts/remote-cmd.d.ts +12 -0
- package/dist/lib/hosts/remote-cmd.js +17 -1
- package/dist/lib/mcp.d.ts +21 -0
- package/dist/lib/mcp.js +278 -13
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/resources/mcp.js +20 -342
- package/dist/lib/rotate.d.ts +12 -8
- package/dist/lib/rotate.js +22 -23
- package/dist/lib/routines.d.ts +13 -0
- package/dist/lib/routines.js +21 -0
- package/dist/lib/runner.js +18 -40
- package/dist/lib/secrets/agent.js +11 -15
- package/dist/lib/session/fork.d.ts +32 -0
- package/dist/lib/session/fork.js +101 -0
- package/dist/lib/share/capture.js +21 -3
- package/dist/lib/signin-badge.d.ts +41 -0
- package/dist/lib/signin-badge.js +64 -0
- package/dist/lib/ssh-exec.d.ts +5 -0
- package/dist/lib/ssh-exec.js +55 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/usage.d.ts +40 -3
- package/dist/lib/usage.js +147 -16
- package/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type DeviceStats } from './health.js';
|
|
2
|
+
export interface FleetCliStatus {
|
|
3
|
+
installed: boolean;
|
|
4
|
+
path: string | null;
|
|
5
|
+
error: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface FleetSyncStatus {
|
|
8
|
+
agent: string;
|
|
9
|
+
version: string;
|
|
10
|
+
status: 'fresh' | 'stale' | 'never-synced';
|
|
11
|
+
isDefault: boolean;
|
|
12
|
+
divergence?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface FleetOrphanStatus {
|
|
15
|
+
agent: string;
|
|
16
|
+
version: string;
|
|
17
|
+
commands: number;
|
|
18
|
+
skills: number;
|
|
19
|
+
hooks: number;
|
|
20
|
+
}
|
|
21
|
+
export interface FleetHealthRow {
|
|
22
|
+
name: string;
|
|
23
|
+
platform?: string;
|
|
24
|
+
version?: string | null;
|
|
25
|
+
stats?: DeviceStats;
|
|
26
|
+
error?: string;
|
|
27
|
+
skipped?: string;
|
|
28
|
+
clis: Record<string, FleetCliStatus>;
|
|
29
|
+
sync: FleetSyncStatus[];
|
|
30
|
+
orphans: FleetOrphanStatus[];
|
|
31
|
+
}
|
|
32
|
+
export interface FleetWarning {
|
|
33
|
+
kind: 'unreachable' | 'drift' | 'cli' | 'version-skew';
|
|
34
|
+
devices: string[];
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
export interface FleetHealthReport {
|
|
38
|
+
generatedAt: string;
|
|
39
|
+
devices: FleetHealthRow[];
|
|
40
|
+
warnings: FleetWarning[];
|
|
41
|
+
hasWarnings: boolean;
|
|
42
|
+
hasDrift: boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare function buildFleetHealthReport(rows: FleetHealthRow[], now?: Date): FleetHealthReport;
|
|
45
|
+
export declare function renderFleetWarnings(report: FleetHealthReport): string[];
|
|
46
|
+
export declare function renderFleetMatrix(report: FleetHealthReport): string[];
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { padToWidth, terminalWidth, truncateToWidth } from '../session/width.js';
|
|
3
|
+
import { fmtBytes, headroom } from './health.js';
|
|
4
|
+
function driftRows(row) {
|
|
5
|
+
return row.sync.filter((s) => s.status !== 'fresh');
|
|
6
|
+
}
|
|
7
|
+
function installedCliCount(row) {
|
|
8
|
+
const statuses = Object.values(row.clis);
|
|
9
|
+
return {
|
|
10
|
+
installed: statuses.filter((s) => s.installed).length,
|
|
11
|
+
total: statuses.length,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function buildFleetHealthReport(rows, now = new Date()) {
|
|
15
|
+
const warnings = [];
|
|
16
|
+
const unreachable = rows
|
|
17
|
+
.filter((r) => r.error || r.skipped)
|
|
18
|
+
.map((r) => r.name);
|
|
19
|
+
if (unreachable.length > 0) {
|
|
20
|
+
warnings.push({
|
|
21
|
+
kind: 'unreachable',
|
|
22
|
+
devices: unreachable,
|
|
23
|
+
message: `${unreachable.length} device${unreachable.length === 1 ? '' : 's'} unreachable or skipped`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const drifted = rows.filter((r) => driftRows(r).length > 0).map((r) => r.name);
|
|
27
|
+
if (drifted.length > 0) {
|
|
28
|
+
warnings.push({
|
|
29
|
+
kind: 'drift',
|
|
30
|
+
devices: drifted,
|
|
31
|
+
message: `${drifted.length} device${drifted.length === 1 ? '' : 's'} have sync drift`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const cliIssues = rows
|
|
35
|
+
.filter((r) => {
|
|
36
|
+
const counts = installedCliCount(r);
|
|
37
|
+
return counts.total > 0 && counts.installed < counts.total;
|
|
38
|
+
})
|
|
39
|
+
.map((r) => r.name);
|
|
40
|
+
if (cliIssues.length > 0) {
|
|
41
|
+
warnings.push({
|
|
42
|
+
kind: 'cli',
|
|
43
|
+
devices: cliIssues,
|
|
44
|
+
message: `${cliIssues.length} device${cliIssues.length === 1 ? ' is' : 's are'} missing one or more agent CLIs`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const versions = new Map();
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
if (!row.version)
|
|
50
|
+
continue;
|
|
51
|
+
const list = versions.get(row.version) ?? [];
|
|
52
|
+
list.push(row.name);
|
|
53
|
+
versions.set(row.version, list);
|
|
54
|
+
}
|
|
55
|
+
if (versions.size > 1) {
|
|
56
|
+
warnings.push({
|
|
57
|
+
kind: 'version-skew',
|
|
58
|
+
devices: rows.filter((r) => r.version).map((r) => r.name),
|
|
59
|
+
message: `agents-cli version skew: ${Array.from(versions.keys()).sort().join(', ')}`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
generatedAt: now.toISOString(),
|
|
64
|
+
devices: rows,
|
|
65
|
+
warnings,
|
|
66
|
+
hasWarnings: warnings.length > 0,
|
|
67
|
+
hasDrift: drifted.length > 0 || unreachable.length > 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function statusGlyph(row) {
|
|
71
|
+
if (row.error || row.skipped)
|
|
72
|
+
return chalk.red('○');
|
|
73
|
+
if (driftRows(row).length > 0)
|
|
74
|
+
return chalk.yellow('◐');
|
|
75
|
+
return chalk.green('●');
|
|
76
|
+
}
|
|
77
|
+
function headroomLabel(row) {
|
|
78
|
+
const h = headroom(row.stats);
|
|
79
|
+
switch (h) {
|
|
80
|
+
case 'idle':
|
|
81
|
+
return chalk.green('idle');
|
|
82
|
+
case 'light':
|
|
83
|
+
return chalk.green('light');
|
|
84
|
+
case 'busy':
|
|
85
|
+
return chalk.yellow('busy');
|
|
86
|
+
case 'loaded':
|
|
87
|
+
return chalk.red('loaded');
|
|
88
|
+
case 'unknown':
|
|
89
|
+
return chalk.gray('unknown');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function driftLabel(row) {
|
|
93
|
+
if (row.error)
|
|
94
|
+
return chalk.red('probe failed');
|
|
95
|
+
if (row.skipped)
|
|
96
|
+
return chalk.gray(row.skipped);
|
|
97
|
+
const drift = driftRows(row);
|
|
98
|
+
if (drift.length === 0)
|
|
99
|
+
return chalk.green('fresh');
|
|
100
|
+
const stale = drift.filter((r) => r.status === 'stale').length;
|
|
101
|
+
const cold = drift.filter((r) => r.status === 'never-synced').length;
|
|
102
|
+
const parts = [];
|
|
103
|
+
if (stale)
|
|
104
|
+
parts.push(`${stale} stale`);
|
|
105
|
+
if (cold)
|
|
106
|
+
parts.push(`${cold} cold`);
|
|
107
|
+
return chalk.yellow(parts.join(' · '));
|
|
108
|
+
}
|
|
109
|
+
function cliLabel(row) {
|
|
110
|
+
if (row.error || row.skipped)
|
|
111
|
+
return chalk.gray('-');
|
|
112
|
+
const { installed, total } = installedCliCount(row);
|
|
113
|
+
if (total === 0)
|
|
114
|
+
return chalk.gray('none');
|
|
115
|
+
return installed === total ? chalk.green(`${installed}/${total}`) : chalk.yellow(`${installed}/${total}`);
|
|
116
|
+
}
|
|
117
|
+
function loadLabel(stats) {
|
|
118
|
+
if (!stats?.reachable)
|
|
119
|
+
return chalk.gray('-');
|
|
120
|
+
const load = stats.loadPercent === undefined ? '-' : `${Math.round(stats.loadPercent)}%`;
|
|
121
|
+
const mem = stats.memPercent === undefined ? '-' : `${Math.round(stats.memPercent)}%`;
|
|
122
|
+
return `${load}/${mem}`;
|
|
123
|
+
}
|
|
124
|
+
export function renderFleetWarnings(report) {
|
|
125
|
+
if (report.warnings.length === 0)
|
|
126
|
+
return [chalk.green('Fleet warnings: none')];
|
|
127
|
+
return [
|
|
128
|
+
chalk.bold(`Fleet warnings (${report.warnings.length})`),
|
|
129
|
+
...report.warnings.map((w) => ` ${chalk.yellow(w.kind.padEnd(12))} ${w.message} ${chalk.gray(`(${w.devices.join(', ')})`)}`),
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
export function renderFleetMatrix(report) {
|
|
133
|
+
if (report.devices.length === 0)
|
|
134
|
+
return [chalk.gray('No registered devices. Run `agents devices` to register some.')];
|
|
135
|
+
const nameW = Math.min(22, Math.max(6, ...report.devices.map((r) => r.name.length)));
|
|
136
|
+
const versionW = Math.min(14, Math.max(7, ...report.devices.map((r) => (r.version ?? '-').length)));
|
|
137
|
+
const width = terminalWidth();
|
|
138
|
+
// 4 = leading " " + the per-row status glyph + its trailing space (rows prefix
|
|
139
|
+
// ` ${statusGlyph} `; the header reserves the same 4 cols so every column lines up).
|
|
140
|
+
const fixed = 4 + nameW + 2 + 8 + 2 + 9 + 2 + 9 + 2 + versionW + 2 + 9 + 2 + 9;
|
|
141
|
+
const noteW = Math.max(12, width - fixed);
|
|
142
|
+
const lines = [
|
|
143
|
+
chalk.bold('Fleet status'),
|
|
144
|
+
chalk.gray(` ${padToWidth('Device', nameW)} ${padToWidth('OS', 8)} ${padToWidth('Health', 9)} ${padToWidth('Sync', 9)} ${padToWidth('CLI', 9)} ${padToWidth('Version', versionW)} ${padToWidth('Load/Mem', 9)} Note`),
|
|
145
|
+
];
|
|
146
|
+
for (const row of report.devices) {
|
|
147
|
+
const note = row.error ?? row.skipped ?? (row.orphans.length > 0 ? `${row.orphans.length} orphaned version${row.orphans.length === 1 ? '' : 's'}` : '');
|
|
148
|
+
lines.push(` ${statusGlyph(row)} ${padToWidth(truncateToWidth(row.name, nameW), nameW)} ` +
|
|
149
|
+
`${padToWidth(truncateToWidth(row.platform ?? '-', 8), 8)} ` +
|
|
150
|
+
`${padToWidth(headroomLabel(row), 9)} ` +
|
|
151
|
+
`${padToWidth(driftLabel(row), 9)} ` +
|
|
152
|
+
`${padToWidth(cliLabel(row), 9)} ` +
|
|
153
|
+
`${padToWidth(truncateToWidth(row.version ?? '-', versionW), versionW)} ` +
|
|
154
|
+
`${padToWidth(loadLabel(row.stats), 9)} ` +
|
|
155
|
+
chalk.gray(truncateToWidth(note || `free ${fmtBytes(row.stats?.memFreeBytes)}`, noteW)));
|
|
156
|
+
}
|
|
157
|
+
lines.push(chalk.gray(' ● fresh · ◐ drift · ○ unreachable/skipped'));
|
|
158
|
+
return lines;
|
|
159
|
+
}
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Device resource probing for `agents devices list`.
|
|
3
3
|
*
|
|
4
4
|
* One SSH round-trip per device gathers load average, memory pressure, and core
|
|
5
|
-
* count (mac + linux
|
|
6
|
-
*
|
|
7
|
-
* degrades to "no stats" instead
|
|
5
|
+
* count (mac + linux via a POSIX snippet, windows via a CIM one-liner), parsed
|
|
6
|
+
* into a {@link DeviceStats}. Probes run in parallel with a bounded timeout so
|
|
7
|
+
* the list stays responsive — a slow or hung box degrades to "no stats" instead
|
|
8
|
+
* of blocking the whole table.
|
|
8
9
|
*
|
|
9
10
|
* The parsers are pure and unit-tested (health.test.ts). They mirror the ones in
|
|
10
11
|
* the Factory extension (apps/factory/src/core/deviceHealth.ts) — kept as a
|
|
@@ -14,12 +15,17 @@ import type { DeviceProfile } from './registry.js';
|
|
|
14
15
|
/** Default per-device probe budget. Short enough that the list never hangs on a
|
|
15
16
|
* wedged box, long enough for a cold relayed SSH handshake. */
|
|
16
17
|
export declare const PROBE_TIMEOUT_MS = 2500;
|
|
18
|
+
/** Windows probe budget. The first CIM query of a PowerShell session pays a
|
|
19
|
+
* "Preparing modules for first use" cost on top of PowerShell startup, which
|
|
20
|
+
* routinely blows the 2.5s POSIX budget on a relayed connection. */
|
|
21
|
+
export declare const WIN_PROBE_TIMEOUT_MS = 6000;
|
|
17
22
|
export interface DeviceStats {
|
|
18
23
|
host: string;
|
|
19
24
|
reachable: boolean;
|
|
20
25
|
loadAvg1?: number;
|
|
21
26
|
ncpu?: number;
|
|
22
|
-
/**
|
|
27
|
+
/** Load normalized to core count (the "has room" number): loadAvg1 / ncpu *
|
|
28
|
+
* 100 on mac/linux, CPU utilization % directly on windows (no loadAvg1). */
|
|
23
29
|
loadPercent?: number;
|
|
24
30
|
memPercent?: number;
|
|
25
31
|
memTotalBytes?: number;
|
|
@@ -43,6 +49,10 @@ export declare function parseNcpu(out: string): {
|
|
|
43
49
|
};
|
|
44
50
|
/** Assemble a DeviceStats from the three snippet sections. */
|
|
45
51
|
export declare function parseProbeOutput(host: string, stdout: string, fetchedAt: number): DeviceStats;
|
|
52
|
+
/** Assemble a DeviceStats from the windows one-liner. A missing marker line
|
|
53
|
+
* (e.g. CIM unavailable) keeps `reachable: true` — ssh answered — with no
|
|
54
|
+
* numbers, mirroring how garbage POSIX output degrades. */
|
|
55
|
+
export declare function parseWinProbeOutput(host: string, stdout: string, fetchedAt: number): DeviceStats;
|
|
46
56
|
export interface FleetCapacity {
|
|
47
57
|
reachable: number;
|
|
48
58
|
cores: number;
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Device resource probing for `agents devices list`.
|
|
3
3
|
*
|
|
4
4
|
* One SSH round-trip per device gathers load average, memory pressure, and core
|
|
5
|
-
* count (mac + linux
|
|
6
|
-
*
|
|
7
|
-
* degrades to "no stats" instead
|
|
5
|
+
* count (mac + linux via a POSIX snippet, windows via a CIM one-liner), parsed
|
|
6
|
+
* into a {@link DeviceStats}. Probes run in parallel with a bounded timeout so
|
|
7
|
+
* the list stays responsive — a slow or hung box degrades to "no stats" instead
|
|
8
|
+
* of blocking the whole table.
|
|
8
9
|
*
|
|
9
10
|
* The parsers are pure and unit-tested (health.test.ts). They mirror the ones in
|
|
10
11
|
* the Factory extension (apps/factory/src/core/deviceHealth.ts) — kept as a
|
|
@@ -15,10 +16,21 @@ import { buildSshInvocation, writeAskpassShim } from './connect.js';
|
|
|
15
16
|
/** Default per-device probe budget. Short enough that the list never hangs on a
|
|
16
17
|
* wedged box, long enough for a cold relayed SSH handshake. */
|
|
17
18
|
export const PROBE_TIMEOUT_MS = 2_500;
|
|
19
|
+
/** Windows probe budget. The first CIM query of a PowerShell session pays a
|
|
20
|
+
* "Preparing modules for first use" cost on top of PowerShell startup, which
|
|
21
|
+
* routinely blows the 2.5s POSIX budget on a relayed connection. */
|
|
22
|
+
export const WIN_PROBE_TIMEOUT_MS = 6_000;
|
|
18
23
|
const SEP = '---AGSTAT---';
|
|
19
24
|
/** One-shot remote snapshot: load, then memory (mac vm_stat else linux
|
|
20
25
|
* meminfo), then core count (linux nproc else mac hw.ncpu). */
|
|
21
26
|
const PROBE_SNIPPET = `uptime; echo ${SEP}; (vm_stat 2>/dev/null || cat /proc/meminfo 2>/dev/null); echo ${SEP}; (nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null)`;
|
|
27
|
+
/** Windows equivalent, one labeled line via CIM. PowerShell 5.1-safe: no `||`
|
|
28
|
+
* chaining, plain string concatenation. `LoadPercentage` is $null on some
|
|
29
|
+
* hosts/VMs, which concatenates to an empty field — the parser treats that as
|
|
30
|
+
* "no load signal" and headroom falls back to memory pressure alone.
|
|
31
|
+
* wrapRemoteCommand base64-encodes this for powershell-shell devices, so the
|
|
32
|
+
* quoting survives ssh intact. */
|
|
33
|
+
const WIN_PROBE_SNIPPET = `$os = Get-CimInstance Win32_OperatingSystem; $cpu = (Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average; Write-Output ('AGWINSTAT load=' + $cpu + ' freeKb=' + $os.FreePhysicalMemory + ' totalKb=' + $os.TotalVisibleMemorySize + ' ncpu=' + $env:NUMBER_OF_PROCESSORS)`;
|
|
22
34
|
/** Compact human byte size: 512M, 64G, 1.5T (binary units, ≤1 decimal). */
|
|
23
35
|
export function fmtBytes(bytes) {
|
|
24
36
|
if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0)
|
|
@@ -106,6 +118,28 @@ export function parseProbeOutput(host, stdout, fetchedAt) {
|
|
|
106
118
|
fetchedAt,
|
|
107
119
|
};
|
|
108
120
|
}
|
|
121
|
+
/** Assemble a DeviceStats from the windows one-liner. A missing marker line
|
|
122
|
+
* (e.g. CIM unavailable) keeps `reachable: true` — ssh answered — with no
|
|
123
|
+
* numbers, mirroring how garbage POSIX output degrades. */
|
|
124
|
+
export function parseWinProbeOutput(host, stdout, fetchedAt) {
|
|
125
|
+
const m = stdout.match(/AGWINSTAT load=([0-9.]*) freeKb=([0-9]+) totalKb=([0-9]+) ncpu=([0-9]+)/);
|
|
126
|
+
if (!m)
|
|
127
|
+
return { host, reachable: true, fetchedAt };
|
|
128
|
+
const loadPercent = m[1] === '' ? undefined : parseFloat(m[1]);
|
|
129
|
+
const freeKb = parseInt(m[2], 10);
|
|
130
|
+
const totalKb = parseInt(m[3], 10);
|
|
131
|
+
const ncpu = parseInt(m[4], 10);
|
|
132
|
+
return {
|
|
133
|
+
host,
|
|
134
|
+
reachable: true,
|
|
135
|
+
ncpu: Number.isFinite(ncpu) && ncpu > 0 ? ncpu : undefined,
|
|
136
|
+
loadPercent: loadPercent !== undefined && Number.isFinite(loadPercent) ? loadPercent : undefined,
|
|
137
|
+
memPercent: totalKb > 0 ? Math.max(0, Math.min(100, ((totalKb - freeKb) / totalKb) * 100)) : undefined,
|
|
138
|
+
memTotalBytes: totalKb > 0 ? totalKb * 1024 : undefined,
|
|
139
|
+
memFreeBytes: totalKb > 0 ? freeKb * 1024 : undefined,
|
|
140
|
+
fetchedAt,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
109
143
|
/** Sum cores and memory across reachable devices for the summary footer. */
|
|
110
144
|
export function fleetCapacity(statsList) {
|
|
111
145
|
const cap = { reachable: 0, cores: 0, memTotalBytes: 0, memFreeBytes: 0 };
|
|
@@ -139,6 +173,7 @@ export function headroom(stats) {
|
|
|
139
173
|
export function probeDeviceStats(device, opts = {}) {
|
|
140
174
|
const host = device.name;
|
|
141
175
|
const fetchedAt = opts.now ?? Date.now();
|
|
176
|
+
const isWin = device.shell === 'powershell';
|
|
142
177
|
let args;
|
|
143
178
|
let env;
|
|
144
179
|
try {
|
|
@@ -146,16 +181,21 @@ export function probeDeviceStats(device, opts = {}) {
|
|
|
146
181
|
// buildSshInvocation joins the cmd with spaces and hands the string to the
|
|
147
182
|
// remote login shell, which evaluates the snippet's `;`/`||` directly — no
|
|
148
183
|
// `sh -c` wrapper needed (and a wrapper would only re-quote the first token).
|
|
149
|
-
|
|
184
|
+
// For powershell devices it base64-encodes the snippet instead.
|
|
185
|
+
({ args, env } = buildSshInvocation(device, [isWin ? WIN_PROBE_SNIPPET : PROBE_SNIPPET], shim));
|
|
150
186
|
}
|
|
151
187
|
catch {
|
|
152
188
|
return Promise.resolve({ host, reachable: false, fetchedAt });
|
|
153
189
|
}
|
|
154
190
|
return new Promise((resolve) => {
|
|
155
|
-
execFile('ssh', args, {
|
|
191
|
+
execFile('ssh', args, {
|
|
192
|
+
encoding: 'utf-8',
|
|
193
|
+
env: { ...process.env, ...env },
|
|
194
|
+
timeout: opts.timeoutMs ?? (isWin ? WIN_PROBE_TIMEOUT_MS : PROBE_TIMEOUT_MS),
|
|
195
|
+
}, (err, stdout) => {
|
|
156
196
|
if (err || !stdout)
|
|
157
197
|
return resolve({ host, reachable: false, fetchedAt });
|
|
158
|
-
resolve(parseProbeOutput(host, stdout, fetchedAt));
|
|
198
|
+
resolve(isWin ? parseWinProbeOutput(host, stdout, fetchedAt) : parseProbeOutput(host, stdout, fetchedAt));
|
|
159
199
|
});
|
|
160
200
|
});
|
|
161
201
|
}
|
|
@@ -164,11 +204,12 @@ export function probeDeviceStats(device, opts = {}) {
|
|
|
164
204
|
* from itself. */
|
|
165
205
|
export function probeLocalStats(host, opts = {}) {
|
|
166
206
|
const fetchedAt = opts.now ?? Date.now();
|
|
207
|
+
const isWin = process.platform === 'win32';
|
|
167
208
|
return new Promise((resolve) => {
|
|
168
|
-
execFile('sh', ['-c', PROBE_SNIPPET], { encoding: 'utf-8', timeout: opts.timeoutMs ?? PROBE_TIMEOUT_MS }, (err, stdout) => {
|
|
209
|
+
execFile(isWin ? 'powershell' : 'sh', isWin ? ['-NoProfile', '-Command', WIN_PROBE_SNIPPET] : ['-c', PROBE_SNIPPET], { encoding: 'utf-8', timeout: opts.timeoutMs ?? (isWin ? WIN_PROBE_TIMEOUT_MS : PROBE_TIMEOUT_MS) }, (err, stdout) => {
|
|
169
210
|
if (err || !stdout)
|
|
170
211
|
return resolve({ host, reachable: false, fetchedAt });
|
|
171
|
-
resolve(parseProbeOutput(host, stdout, fetchedAt));
|
|
212
|
+
resolve(isWin ? parseWinProbeOutput(host, stdout, fetchedAt) : parseProbeOutput(host, stdout, fetchedAt));
|
|
172
213
|
});
|
|
173
214
|
});
|
|
174
215
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type DeviceProfile } from './registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decide whether an interactive login warrants a terminfo push. Pure so the
|
|
4
|
+
* gating logic is unit-testable without touching ssh or the filesystem.
|
|
5
|
+
*
|
|
6
|
+
* @param interactive true only for a real login (no remote command) on a human tty.
|
|
7
|
+
*/
|
|
8
|
+
export declare function shouldSyncTerminfo(params: {
|
|
9
|
+
term?: string;
|
|
10
|
+
shell: DeviceProfile['shell'];
|
|
11
|
+
interactive: boolean;
|
|
12
|
+
}): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Cache key for a device: the remote **user@host**, not just the host. terminfo
|
|
15
|
+
* is compiled into the *per-user* `~/.terminfo`, so `alice@box` and `bob@box`
|
|
16
|
+
* are distinct sync targets — keying on host alone would let the first user's
|
|
17
|
+
* stamp suppress the second user's (never-installed) sync. Mirrors
|
|
18
|
+
* {@link sshTargetFor}'s user handling; falls back to the device name when the
|
|
19
|
+
* address is unresolved.
|
|
20
|
+
*/
|
|
21
|
+
export declare function terminfoHostKey(device: Pick<DeviceProfile, 'user' | 'name'>, addr: string | undefined): string;
|
|
22
|
+
/** True when a fresh successful-sync stamp exists for this host+TERM. */
|
|
23
|
+
export declare function terminfoSynced(host: string, term: string, cacheRoot?: string): boolean;
|
|
24
|
+
/** Record a successful sync so repeat logins skip the push. Best-effort. */
|
|
25
|
+
export declare function markTerminfoSynced(host: string, term: string, cacheRoot?: string): void;
|
|
26
|
+
/** Export the local terminfo source for a TERM, or null if it can't be produced. */
|
|
27
|
+
export declare function localTerminfoSource(term: string): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Best-effort: ensure `device` has the terminfo entry for the local `$TERM`
|
|
30
|
+
* before an interactive login. Never throws; returns whether a push succeeded
|
|
31
|
+
* (false when skipped, already-cached, or failed). `sshArgs`/`sshEnv` are the
|
|
32
|
+
* SAME host-key + auth options the real login uses, minus the interactive tty —
|
|
33
|
+
* so a password-auth device resolves through the askpass shim without a second
|
|
34
|
+
* human prompt.
|
|
35
|
+
*/
|
|
36
|
+
export declare function syncTerminfoToDevice(opts: {
|
|
37
|
+
device: DeviceProfile;
|
|
38
|
+
host: string;
|
|
39
|
+
term: string | undefined;
|
|
40
|
+
/** ssh argv for a non-interactive `tic -x -` exec against this device. */
|
|
41
|
+
sshArgs: string[];
|
|
42
|
+
/** Env overlay for that ssh (askpass wiring for password devices). */
|
|
43
|
+
sshEnv: Record<string, string>;
|
|
44
|
+
}): boolean;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminfo propagation for `agents ssh` interactive logins.
|
|
3
|
+
*
|
|
4
|
+
* Modern terminals (Ghostty, kitty, Alacritty, WezTerm, foot, rio) advertise a
|
|
5
|
+
* custom `TERM` (e.g. `xterm-ghostty`) whose terminfo entry ships with the
|
|
6
|
+
* terminal, not with the remote host's ncurses. SSH into a box that lacks the
|
|
7
|
+
* entry and the session is subtly broken: wrong backspace, missing colors, a
|
|
8
|
+
* garbled clear/alt-screen. Ghostty's own shell integration fixes this for the
|
|
9
|
+
* bare `ssh` command, but `agents ssh` (Tailscale-relayed, spawned directly)
|
|
10
|
+
* bypasses that wrapper — so we handle it here.
|
|
11
|
+
*
|
|
12
|
+
* Strategy: on an interactive POSIX login, if the local `$TERM` is one the
|
|
13
|
+
* remote is unlikely to have, export it locally (`infocmp -x`) and compile it on
|
|
14
|
+
* the remote (`tic -x -`, writes to the user's `~/.terminfo`, no sudo). This is
|
|
15
|
+
* the canonical, terminal-agnostic technique.
|
|
16
|
+
*
|
|
17
|
+
* Two invariants keep it safe and cheap:
|
|
18
|
+
* - **Fail-safe.** Every failure is swallowed. A push that errors, times out,
|
|
19
|
+
* or hits a remote without `tic` leaves the user exactly where they are today
|
|
20
|
+
* — it never blocks or delays the actual login beyond the short push timeout.
|
|
21
|
+
* - **Cached.** A successful sync stamps a local marker keyed by host+TERM, so
|
|
22
|
+
* only the first login to each host pays the one extra round-trip; every
|
|
23
|
+
* repeat login is zero-cost.
|
|
24
|
+
*/
|
|
25
|
+
import { spawnSync } from 'child_process';
|
|
26
|
+
import * as fs from 'fs';
|
|
27
|
+
import * as path from 'path';
|
|
28
|
+
import { getCacheDir } from '../state.js';
|
|
29
|
+
/**
|
|
30
|
+
* Terminfo names that ncurses ships essentially everywhere, so pushing them is
|
|
31
|
+
* wasted work. Anything NOT in this set (the exotic terminal entries) is a sync
|
|
32
|
+
* candidate. Kept deliberately conservative: when unsure, we push — `tic` is
|
|
33
|
+
* idempotent and the result is cached.
|
|
34
|
+
*/
|
|
35
|
+
const UNIVERSAL_TERMS = new Set([
|
|
36
|
+
'dumb',
|
|
37
|
+
'ansi',
|
|
38
|
+
'vt100',
|
|
39
|
+
'vt102',
|
|
40
|
+
'vt220',
|
|
41
|
+
'linux',
|
|
42
|
+
'cygwin',
|
|
43
|
+
'xterm',
|
|
44
|
+
'xterm-color',
|
|
45
|
+
'xterm-16color',
|
|
46
|
+
'xterm-256color',
|
|
47
|
+
'screen',
|
|
48
|
+
'screen-256color',
|
|
49
|
+
'tmux',
|
|
50
|
+
'tmux-256color',
|
|
51
|
+
'rxvt',
|
|
52
|
+
'rxvt-unicode',
|
|
53
|
+
'rxvt-unicode-256color',
|
|
54
|
+
]);
|
|
55
|
+
/** How long a successful sync suppresses re-syncing the same host+TERM. */
|
|
56
|
+
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
57
|
+
/** Cap on the push so a stalled remote can't delay the login indefinitely. */
|
|
58
|
+
const PUSH_TIMEOUT_MS = 8000;
|
|
59
|
+
/**
|
|
60
|
+
* Decide whether an interactive login warrants a terminfo push. Pure so the
|
|
61
|
+
* gating logic is unit-testable without touching ssh or the filesystem.
|
|
62
|
+
*
|
|
63
|
+
* @param interactive true only for a real login (no remote command) on a human tty.
|
|
64
|
+
*/
|
|
65
|
+
export function shouldSyncTerminfo(params) {
|
|
66
|
+
if (!params.interactive)
|
|
67
|
+
return false;
|
|
68
|
+
if (params.shell === 'powershell')
|
|
69
|
+
return false; // Windows console ignores terminfo
|
|
70
|
+
const term = params.term?.trim();
|
|
71
|
+
if (!term)
|
|
72
|
+
return false;
|
|
73
|
+
if (UNIVERSAL_TERMS.has(term))
|
|
74
|
+
return false;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Cache key for a device: the remote **user@host**, not just the host. terminfo
|
|
79
|
+
* is compiled into the *per-user* `~/.terminfo`, so `alice@box` and `bob@box`
|
|
80
|
+
* are distinct sync targets — keying on host alone would let the first user's
|
|
81
|
+
* stamp suppress the second user's (never-installed) sync. Mirrors
|
|
82
|
+
* {@link sshTargetFor}'s user handling; falls back to the device name when the
|
|
83
|
+
* address is unresolved.
|
|
84
|
+
*/
|
|
85
|
+
export function terminfoHostKey(device, addr) {
|
|
86
|
+
const host = addr ?? device.name;
|
|
87
|
+
return device.user ? `${device.user}@${host}` : host;
|
|
88
|
+
}
|
|
89
|
+
/** Directory holding per-host+TERM sync stamps. `cacheRoot` override is for tests. */
|
|
90
|
+
function stampDir(cacheRoot) {
|
|
91
|
+
return path.join(cacheRoot ?? getCacheDir(), 'devices', 'terminfo');
|
|
92
|
+
}
|
|
93
|
+
/** Filesystem-safe stamp name for a host+TERM pair. */
|
|
94
|
+
function stampFile(host, term, cacheRoot) {
|
|
95
|
+
const safe = `${host}__${term}`.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
96
|
+
return path.join(stampDir(cacheRoot), safe);
|
|
97
|
+
}
|
|
98
|
+
/** True when a fresh successful-sync stamp exists for this host+TERM. */
|
|
99
|
+
export function terminfoSynced(host, term, cacheRoot) {
|
|
100
|
+
try {
|
|
101
|
+
const st = fs.statSync(stampFile(host, term, cacheRoot));
|
|
102
|
+
return Date.now() - st.mtimeMs < CACHE_TTL_MS;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Record a successful sync so repeat logins skip the push. Best-effort. */
|
|
109
|
+
export function markTerminfoSynced(host, term, cacheRoot) {
|
|
110
|
+
try {
|
|
111
|
+
fs.mkdirSync(stampDir(cacheRoot), { recursive: true });
|
|
112
|
+
fs.writeFileSync(stampFile(host, term, cacheRoot), `${new Date().toISOString()}\n`);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* a cache write failure just means we re-sync next time — never fatal */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Export the local terminfo source for a TERM, or null if it can't be produced. */
|
|
119
|
+
export function localTerminfoSource(term) {
|
|
120
|
+
try {
|
|
121
|
+
const res = spawnSync('infocmp', ['-x', term], {
|
|
122
|
+
encoding: 'utf8',
|
|
123
|
+
timeout: 4000,
|
|
124
|
+
});
|
|
125
|
+
if (res.status === 0 && res.stdout && res.stdout.trim().length > 0) {
|
|
126
|
+
return res.stdout;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* infocmp missing or errored — nothing we can push */
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Best-effort: ensure `device` has the terminfo entry for the local `$TERM`
|
|
136
|
+
* before an interactive login. Never throws; returns whether a push succeeded
|
|
137
|
+
* (false when skipped, already-cached, or failed). `sshArgs`/`sshEnv` are the
|
|
138
|
+
* SAME host-key + auth options the real login uses, minus the interactive tty —
|
|
139
|
+
* so a password-auth device resolves through the askpass shim without a second
|
|
140
|
+
* human prompt.
|
|
141
|
+
*/
|
|
142
|
+
export function syncTerminfoToDevice(opts) {
|
|
143
|
+
const term = opts.term?.trim();
|
|
144
|
+
if (!term)
|
|
145
|
+
return false;
|
|
146
|
+
if (terminfoSynced(opts.host, term))
|
|
147
|
+
return false;
|
|
148
|
+
const source = localTerminfoSource(term);
|
|
149
|
+
if (!source)
|
|
150
|
+
return false;
|
|
151
|
+
try {
|
|
152
|
+
const res = spawnSync('ssh', opts.sshArgs, {
|
|
153
|
+
input: source,
|
|
154
|
+
env: { ...process.env, ...opts.sshEnv },
|
|
155
|
+
timeout: PUSH_TIMEOUT_MS,
|
|
156
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
157
|
+
});
|
|
158
|
+
if (res.status === 0) {
|
|
159
|
+
markTerminfoSynced(opts.host, term);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
/* connection failed / timed out — fail-safe, login proceeds unaffected */
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -95,6 +95,16 @@ export declare function getGitHubUsername(): Promise<string | null>;
|
|
|
95
95
|
* Get the remote URL for origin in a git repo.
|
|
96
96
|
*/
|
|
97
97
|
export declare function getRemoteUrl(repoPath: string): Promise<string | null>;
|
|
98
|
+
/**
|
|
99
|
+
* Canonical `host/owner/repo` form of a git remote, transport-agnostic, so the
|
|
100
|
+
* same repo cloned over SSH vs HTTPS compares equal. Strips protocol, any
|
|
101
|
+
* `user@`, a trailing `.git`, and folds the scp-style `host:owner/repo` colon to
|
|
102
|
+
* a slash. Lower-cased. Used to decide whether an existing checkout is "the same
|
|
103
|
+
* repo" as a requested source before adopting it.
|
|
104
|
+
*/
|
|
105
|
+
export declare function canonicalGitRemote(url: string): string;
|
|
106
|
+
/** True when two git remote URLs point at the same repo across transport forms. */
|
|
107
|
+
export declare function sameGitRemote(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
98
108
|
/**
|
|
99
109
|
* Set the remote URL for origin in a git repo.
|
|
100
110
|
*/
|
package/dist/lib/git.js
CHANGED
|
@@ -365,6 +365,29 @@ export async function getRemoteUrl(repoPath) {
|
|
|
365
365
|
return null;
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Canonical `host/owner/repo` form of a git remote, transport-agnostic, so the
|
|
370
|
+
* same repo cloned over SSH vs HTTPS compares equal. Strips protocol, any
|
|
371
|
+
* `user@`, a trailing `.git`, and folds the scp-style `host:owner/repo` colon to
|
|
372
|
+
* a slash. Lower-cased. Used to decide whether an existing checkout is "the same
|
|
373
|
+
* repo" as a requested source before adopting it.
|
|
374
|
+
*/
|
|
375
|
+
export function canonicalGitRemote(url) {
|
|
376
|
+
return url
|
|
377
|
+
.trim()
|
|
378
|
+
.replace(/\/+$/, '') // trailing slashes first, so a trailing-slash-after-.git still strips
|
|
379
|
+
.replace(/\.git$/i, '')
|
|
380
|
+
.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '') // strip scheme (https://, ssh://, git://)
|
|
381
|
+
.replace(/^[^@/]+@/, '') // strip user@ (git@, ssh user)
|
|
382
|
+
.replace(':', '/') // scp-style host:owner/repo → host/owner/repo (first colon only)
|
|
383
|
+
.toLowerCase();
|
|
384
|
+
}
|
|
385
|
+
/** True when two git remote URLs point at the same repo across transport forms. */
|
|
386
|
+
export function sameGitRemote(a, b) {
|
|
387
|
+
if (!a || !b)
|
|
388
|
+
return false;
|
|
389
|
+
return canonicalGitRemote(a) === canonicalGitRemote(b);
|
|
390
|
+
}
|
|
368
391
|
/**
|
|
369
392
|
* Set the remote URL for origin in a git repo.
|
|
370
393
|
*/
|
package/dist/lib/hosts/option.js
CHANGED
|
@@ -14,7 +14,7 @@ export function addHostOption(cmd) {
|
|
|
14
14
|
return cmd
|
|
15
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('--device <name>', 'Alias of --host: run this command on a registered device (from `agents devices`).')
|
|
17
|
-
.option('--remote-cwd <dir>',
|
|
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.');
|
|
20
20
|
}
|
package/dist/lib/hosts/ready.js
CHANGED
|
@@ -10,7 +10,7 @@ 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';
|
|
13
|
+
import { remoteShellFor, buildWindowsAgentsCommand, encodePowershell, powershellQuote, POWERSHELL_PROGRESS_SILENCE } from './remote-cmd.js';
|
|
14
14
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
15
15
|
/** Resolve this CLI's own version by walking up to the nearest package.json. */
|
|
16
16
|
export function localCliVersion() {
|
|
@@ -75,7 +75,8 @@ export function remoteAgentsVersion(target, os) {
|
|
|
75
75
|
* equivalents (`Select-Object -Last`, `Test-Path`). Pure/exported. */
|
|
76
76
|
export function buildBootstrapCommand(spec, os) {
|
|
77
77
|
if (remoteShellFor(os) === 'powershell') {
|
|
78
|
-
const script =
|
|
78
|
+
const script = `${POWERSHELL_PROGRESS_SILENCE}; ` +
|
|
79
|
+
`npm install -g ${powershellQuote(spec)} 2>&1 | Select-Object -Last 3; ` +
|
|
79
80
|
`if (-not (Test-Path "$HOME/.agents/.system")) { agents setup 2>&1 | Select-Object -Last 3 }; ` +
|
|
80
81
|
`agents --version`;
|
|
81
82
|
return `powershell -NoProfile -EncodedCommand ${encodePowershell(script)}`;
|
|
@@ -110,7 +111,8 @@ const READY_MARKER = '@@AGENTS_READY@@';
|
|
|
110
111
|
*/
|
|
111
112
|
export function buildReadyProbeCommand(os) {
|
|
112
113
|
if (remoteShellFor(os) === 'powershell') {
|
|
113
|
-
const script =
|
|
114
|
+
const script = `${POWERSHELL_PROGRESS_SILENCE}; ` +
|
|
115
|
+
`agents --version 2>$null; Write-Output "${READY_MARKER}"; ` +
|
|
114
116
|
`agents view 2>$null; if ($LASTEXITCODE -ne 0) { agents list 2>$null }`;
|
|
115
117
|
return `powershell -NoProfile -EncodedCommand ${encodePowershell(script)}`;
|
|
116
118
|
}
|