@phnx-labs/agents-cli 1.20.27 → 1.20.29
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 +3 -0
- package/dist/commands/doctor.js +57 -4
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +198 -15
- package/dist/commands/hosts.d.ts +11 -0
- package/dist/commands/hosts.js +229 -0
- package/dist/commands/repo.d.ts +29 -0
- package/dist/commands/repo.js +174 -38
- package/dist/commands/secrets.d.ts +2 -7
- package/dist/commands/secrets.js +108 -29
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +8 -24
- package/dist/commands/ssh.d.ts +14 -0
- package/dist/commands/ssh.js +263 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +22 -5
- package/dist/commands/view.js +27 -11
- package/dist/index.js +3 -1
- package/dist/lib/agents.d.ts +1 -0
- package/dist/lib/agents.js +44 -4
- package/dist/lib/browser/drivers/ssh.d.ts +47 -2
- package/dist/lib/browser/drivers/ssh.js +113 -24
- package/dist/lib/browser/profiles.js +28 -1
- package/dist/lib/browser/runtime-state.js +28 -8
- package/dist/lib/browser/types.d.ts +10 -1
- package/dist/lib/cli-resources.js +10 -1
- package/dist/lib/devices/connect.d.ts +34 -0
- package/dist/lib/devices/connect.js +101 -0
- package/dist/lib/devices/registry.d.ts +78 -0
- package/dist/lib/devices/registry.js +168 -0
- package/dist/lib/devices/ssh-config.d.ts +21 -0
- package/dist/lib/devices/ssh-config.js +33 -0
- package/dist/lib/devices/tailscale.d.ts +31 -0
- package/dist/lib/devices/tailscale.js +126 -0
- package/dist/lib/doctor-diff.d.ts +12 -0
- package/dist/lib/doctor-diff.js +89 -2
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +62 -19
- package/dist/lib/hooks.d.ts +17 -0
- package/dist/lib/hooks.js +127 -3
- package/dist/lib/hosts/dispatch.d.ts +26 -0
- package/dist/lib/hosts/dispatch.js +71 -0
- package/dist/lib/hosts/progress.d.ts +21 -0
- package/dist/lib/hosts/progress.js +49 -0
- package/dist/lib/hosts/providers/local.d.ts +17 -0
- package/dist/lib/hosts/providers/local.js +81 -0
- package/dist/lib/hosts/ready.d.ts +37 -0
- package/dist/lib/hosts/ready.js +88 -0
- package/dist/lib/hosts/registry.d.ts +22 -0
- package/dist/lib/hosts/registry.js +65 -0
- package/dist/lib/hosts/ssh-config.d.ts +37 -0
- package/dist/lib/hosts/ssh-config.js +157 -0
- package/dist/lib/hosts/tasks.d.ts +32 -0
- package/dist/lib/hosts/tasks.js +58 -0
- package/dist/lib/hosts/types.d.ts +51 -0
- package/dist/lib/hosts/types.js +21 -0
- package/dist/lib/loop.d.ts +9 -0
- package/dist/lib/loop.js +13 -1
- package/dist/lib/mcp.js +12 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.js +9 -5
- package/dist/lib/platform/exec.d.ts +10 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/platform/index.d.ts +1 -0
- package/dist/lib/platform/index.js +1 -0
- package/dist/lib/platform/links.d.ts +15 -0
- package/dist/lib/platform/links.js +42 -0
- package/dist/lib/platform/paths.d.ts +18 -0
- package/dist/lib/platform/paths.js +22 -0
- package/dist/lib/platform/posixpath.d.ts +28 -0
- package/dist/lib/platform/posixpath.js +153 -0
- package/dist/lib/plugins.d.ts +10 -0
- package/dist/lib/plugins.js +1 -1
- package/dist/lib/project-launch.js +6 -3
- package/dist/lib/sandbox.js +5 -2
- package/dist/lib/secrets/remote.d.ts +67 -0
- package/dist/lib/secrets/remote.js +133 -0
- package/dist/lib/self-update.js +7 -2
- package/dist/lib/session/db.d.ts +24 -0
- package/dist/lib/session/db.js +80 -5
- package/dist/lib/session/discover.d.ts +28 -0
- package/dist/lib/session/discover.js +303 -4
- package/dist/lib/session/parse.d.ts +7 -0
- package/dist/lib/session/parse.js +110 -0
- package/dist/lib/session/relative-time.d.ts +7 -0
- package/dist/lib/session/relative-time.js +28 -0
- package/dist/lib/session/remote.d.ts +31 -3
- package/dist/lib/session/remote.js +121 -14
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/ssh-exec.d.ts +45 -0
- package/dist/lib/ssh-exec.js +61 -0
- 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 +2 -0
- package/dist/lib/state.js +2 -0
- package/dist/lib/types.d.ts +21 -0
- package/dist/lib/versions.d.ts +6 -2
- package/dist/lib/versions.js +8 -4
- package/package.json +1 -1
- package/scripts/postinstall.js +62 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents devices` (registry) + `agents ssh` (smart wrapper).
|
|
3
|
+
*
|
|
4
|
+
* `agents devices` keeps a registry of SSH device profiles — platform, login
|
|
5
|
+
* user, address, and auth — self-populated from `tailscale status --json`.
|
|
6
|
+
* `agents ssh <name>` then connects through one hardened path: preflight
|
|
7
|
+
* (offline → fail fast instead of a 2-minute hang), platform-aware exec
|
|
8
|
+
* (PowerShell on Windows), and password-from-bundle auth via an askpass shim.
|
|
9
|
+
* Rendering the registry to an ssh_config include also lets plain ssh / scp /
|
|
10
|
+
* rsync / `agents sessions --host` resolve the same logical names.
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from 'child_process';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as os from 'os';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import ora from 'ora';
|
|
18
|
+
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
19
|
+
import { getDevice, loadDevices, removeDevice, upsertDevice, } from '../lib/devices/registry.js';
|
|
20
|
+
import { nodeToDeviceInput, parseTailscaleStatus, tailscaleStatusJson, } from '../lib/devices/tailscale.js';
|
|
21
|
+
import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
22
|
+
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
23
|
+
/** Parse `user@host` or `host` into pieces. */
|
|
24
|
+
function parseTarget(target) {
|
|
25
|
+
const at = target.indexOf('@');
|
|
26
|
+
if (at === -1)
|
|
27
|
+
return { host: target };
|
|
28
|
+
return { user: target.slice(0, at), host: target.slice(at + 1) };
|
|
29
|
+
}
|
|
30
|
+
/** One-line summary of a device for `list`. */
|
|
31
|
+
function deviceSummary(d) {
|
|
32
|
+
const addr = hostNameFor(d) ?? chalk.gray('no address');
|
|
33
|
+
const online = d.tailscale
|
|
34
|
+
? d.tailscale.online
|
|
35
|
+
? chalk.green('online')
|
|
36
|
+
: chalk.gray('offline')
|
|
37
|
+
: chalk.gray('unknown');
|
|
38
|
+
const reach = d.tailscale?.online && !d.tailscale.direct ? chalk.yellow(' (relayed)') : '';
|
|
39
|
+
return ` ${chalk.bold(d.name.padEnd(16))} ${String(d.platform).padEnd(8)} ${(d.user ? d.user + '@' : '') + addr} ${online}${reach}`;
|
|
40
|
+
}
|
|
41
|
+
/** Resolve a device or exit with a clear error. */
|
|
42
|
+
async function mustGetDevice(name) {
|
|
43
|
+
const d = await getDevice(name);
|
|
44
|
+
if (!d) {
|
|
45
|
+
console.error(chalk.red(`Unknown device '${name}'. See 'agents devices list'.`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
return d;
|
|
49
|
+
}
|
|
50
|
+
/** Register the `agents devices` command tree. */
|
|
51
|
+
function registerDevicesCommands(program) {
|
|
52
|
+
const devicesCmd = program
|
|
53
|
+
.command('devices')
|
|
54
|
+
.description('Registry of SSH device profiles (platform, user, address, auth), self-populated from Tailscale.')
|
|
55
|
+
.addHelpText('after', `
|
|
56
|
+
Typical workflow:
|
|
57
|
+
agents devices sync # ingest tailscale nodes (auto-detect platform)
|
|
58
|
+
agents devices list # see what's registered
|
|
59
|
+
agents devices set win-mini --auth password --bundle muqsit
|
|
60
|
+
agents devices render --write # write ~/.ssh/config.d/agents include
|
|
61
|
+
`);
|
|
62
|
+
devicesCmd
|
|
63
|
+
.command('sync')
|
|
64
|
+
.description('Ingest `tailscale status --json` and create/update device profiles (auto-detects platform, address, reachability).')
|
|
65
|
+
.action(async () => {
|
|
66
|
+
const spinner = ora('Reading tailscale status...').start();
|
|
67
|
+
try {
|
|
68
|
+
const nodes = parseTailscaleStatus(tailscaleStatusJson());
|
|
69
|
+
spinner.text = `Updating ${nodes.length} device${nodes.length === 1 ? '' : 's'}...`;
|
|
70
|
+
for (const node of nodes) {
|
|
71
|
+
await upsertDevice(node.name, nodeToDeviceInput(node));
|
|
72
|
+
}
|
|
73
|
+
spinner.succeed(`Synced ${nodes.length} device${nodes.length === 1 ? '' : 's'} from Tailscale`);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
spinner.fail(err.message);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
devicesCmd
|
|
81
|
+
.command('list')
|
|
82
|
+
.alias('ls')
|
|
83
|
+
.description('List registered devices with platform, address, and reachability.')
|
|
84
|
+
.action(async () => {
|
|
85
|
+
const reg = await loadDevices();
|
|
86
|
+
const names = Object.keys(reg).sort();
|
|
87
|
+
if (names.length === 0) {
|
|
88
|
+
console.log(chalk.gray("No devices. Run 'agents devices sync' or 'agents devices add <name> <user@host>'."));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk.bold(`Devices (${names.length})`));
|
|
92
|
+
for (const name of names)
|
|
93
|
+
console.log(deviceSummary(reg[name]));
|
|
94
|
+
});
|
|
95
|
+
devicesCmd
|
|
96
|
+
.command('show <name>')
|
|
97
|
+
.description('Show the full profile for one device.')
|
|
98
|
+
.action(async (name) => {
|
|
99
|
+
const d = await mustGetDevice(name);
|
|
100
|
+
console.log(JSON.stringify(d, null, 2));
|
|
101
|
+
});
|
|
102
|
+
devicesCmd
|
|
103
|
+
.command('add <name> <target>')
|
|
104
|
+
.description('Add a device manually (target is user@host or host).')
|
|
105
|
+
.option('--platform <platform>', 'windows | linux | macos')
|
|
106
|
+
.action(async (name, target, opts) => {
|
|
107
|
+
try {
|
|
108
|
+
const { host, user } = parseTarget(target);
|
|
109
|
+
const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
|
|
110
|
+
const d = await upsertDevice(name, {
|
|
111
|
+
platform: opts.platform ?? undefined,
|
|
112
|
+
user,
|
|
113
|
+
address: { via: 'manual', dnsName: isIp ? undefined : host, ip: isIp ? host : undefined },
|
|
114
|
+
});
|
|
115
|
+
console.log(chalk.green(`Added device '${name}'`) + chalk.gray(` (${d.platform}, ${user ? user + '@' : ''}${host})`));
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.error(chalk.red(err.message));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
devicesCmd
|
|
123
|
+
.command('set <name>')
|
|
124
|
+
.description('Update fields on an existing device (platform, user, auth).')
|
|
125
|
+
.option('--platform <platform>', 'windows | linux | macos')
|
|
126
|
+
.option('--user <user>', 'login user')
|
|
127
|
+
.option('--auth <method>', 'key | password')
|
|
128
|
+
.option('--bundle <bundle>', 'secrets bundle holding the password (for --auth password)')
|
|
129
|
+
.option('--bundle-key <key>', "key within the bundle (default 'password')")
|
|
130
|
+
.action(async (name, opts) => {
|
|
131
|
+
try {
|
|
132
|
+
const existing = await mustGetDevice(name);
|
|
133
|
+
const auth = opts.auth || opts.bundle || opts.bundleKey
|
|
134
|
+
? {
|
|
135
|
+
method: opts.auth ?? existing.auth.method,
|
|
136
|
+
bundle: opts.bundle ?? existing.auth.bundle,
|
|
137
|
+
bundleKey: opts.bundleKey ?? existing.auth.bundleKey,
|
|
138
|
+
}
|
|
139
|
+
: undefined;
|
|
140
|
+
const d = await upsertDevice(name, {
|
|
141
|
+
platform: opts.platform ?? undefined,
|
|
142
|
+
user: opts.user ?? undefined,
|
|
143
|
+
auth,
|
|
144
|
+
});
|
|
145
|
+
console.log(chalk.green(`Updated device '${name}'`) + chalk.gray(` (auth: ${d.auth.method}${d.auth.bundle ? ` via ${d.auth.bundle}` : ''})`));
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.error(chalk.red(err.message));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
devicesCmd
|
|
153
|
+
.command('rm <name>')
|
|
154
|
+
.alias('remove')
|
|
155
|
+
.description('Remove a device from the registry.')
|
|
156
|
+
.action(async (name) => {
|
|
157
|
+
const ok = await removeDevice(name);
|
|
158
|
+
if (!ok) {
|
|
159
|
+
console.error(chalk.red(`Unknown device '${name}'.`));
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
console.log(chalk.green(`Removed device '${name}'`));
|
|
163
|
+
});
|
|
164
|
+
devicesCmd
|
|
165
|
+
.command('render')
|
|
166
|
+
.description('Render the registry to ssh_config. Prints to stdout, or use --write to update ~/.ssh/config.d/agents.')
|
|
167
|
+
.option('--write', 'write to ~/.ssh/config.d/agents instead of printing')
|
|
168
|
+
.action(async (opts) => {
|
|
169
|
+
const reg = await loadDevices();
|
|
170
|
+
const text = renderSshConfig(reg);
|
|
171
|
+
if (!opts.write) {
|
|
172
|
+
process.stdout.write(text);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const dir = path.join(os.homedir(), '.ssh', 'config.d');
|
|
176
|
+
const file = path.join(dir, 'agents');
|
|
177
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
178
|
+
fs.writeFileSync(file, text, { mode: 0o600 });
|
|
179
|
+
console.log(chalk.green(`Wrote ${file}`));
|
|
180
|
+
console.log(chalk.gray('Add this to ~/.ssh/config (once): Include config.d/agents'));
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/** Register the `agents ssh` smart wrapper. */
|
|
184
|
+
function registerSshWrapper(program) {
|
|
185
|
+
const sshCmd = program
|
|
186
|
+
.command('ssh <name> [cmd...]')
|
|
187
|
+
.description('Connect to a registered device. Preflights reachability, picks the right shell, and authenticates (key or password-from-bundle).')
|
|
188
|
+
.allowUnknownOption()
|
|
189
|
+
.addHelpText('after', `
|
|
190
|
+
Examples:
|
|
191
|
+
agents ssh win-mini # interactive login
|
|
192
|
+
agents ssh win-mini hostname # run a command (PowerShell on Windows)
|
|
193
|
+
agents ssh yosemite-s0 uptime # run a command (POSIX)
|
|
194
|
+
|
|
195
|
+
Devices come from 'agents devices'. Password auth pulls the secret from a
|
|
196
|
+
secrets bundle via an askpass shim — the password never touches argv.
|
|
197
|
+
`)
|
|
198
|
+
.action(async (name, cmd) => {
|
|
199
|
+
// Hidden askpass bridge: ssh execs the shim, which re-invokes us here.
|
|
200
|
+
if (name === '__askpass') {
|
|
201
|
+
await runAskpass();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const device = await mustGetDevice(name);
|
|
205
|
+
// Preflight: a device Tailscale last saw offline would otherwise hang
|
|
206
|
+
// for the full ConnectTimeout. Fail fast with a clear message instead.
|
|
207
|
+
if (device.tailscale && !device.tailscale.online) {
|
|
208
|
+
console.error(chalk.red(`Device '${name}' is offline (Tailscale last saw it ${device.tailscale.lastSeen ?? 'a while ago'}).`));
|
|
209
|
+
console.error(chalk.gray("Run 'agents devices sync' to refresh reachability."));
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
if (device.tailscale?.online && !device.tailscale.direct) {
|
|
213
|
+
console.error(chalk.yellow(`Note: connection to '${name}' is relayed (DERP ${device.tailscale.relay ?? '?'}) — expect higher latency.`));
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const shim = writeAskpassShim();
|
|
217
|
+
const { args, env } = buildSshInvocation(device, cmd, shim);
|
|
218
|
+
const res = spawnSync('ssh', args, {
|
|
219
|
+
stdio: 'inherit',
|
|
220
|
+
env: { ...process.env, ...env },
|
|
221
|
+
});
|
|
222
|
+
process.exit(res.status ?? 1);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
console.error(chalk.red(err.message));
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
// Keep the hidden askpass invocation out of help.
|
|
230
|
+
void sshCmd;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The askpass side of password auth. Invoked by the shim (which ssh execs with
|
|
234
|
+
* SSH_ASKPASS): read the target bundle/key from the environment the wrapper
|
|
235
|
+
* set, resolve it through the existing Keychain path, and print the password
|
|
236
|
+
* to stdout for ssh to consume.
|
|
237
|
+
*/
|
|
238
|
+
async function runAskpass() {
|
|
239
|
+
const bundle = process.env[ASKPASS_BUNDLE_ENV];
|
|
240
|
+
const key = process.env[ASKPASS_KEY_ENV] ?? 'password';
|
|
241
|
+
if (!bundle) {
|
|
242
|
+
console.error(`askpass: ${ASKPASS_BUNDLE_ENV} not set`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh' });
|
|
247
|
+
const value = env[key];
|
|
248
|
+
if (value === undefined) {
|
|
249
|
+
console.error(`askpass: key '${key}' not found in bundle '${bundle}'`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
process.stdout.write(value);
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
console.error(`askpass: ${err?.message ?? err}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** Register both `agents ssh` and `agents devices`. */
|
|
260
|
+
export function registerSshCommands(program) {
|
|
261
|
+
registerSshWrapper(program);
|
|
262
|
+
registerDevicesCommands(program);
|
|
263
|
+
}
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* agents sync claude # one agent: uses default/sole installed version
|
|
10
10
|
* agents sync claude@2.1.142 # one agent: explicit version
|
|
11
11
|
* agents sync claude@latest # one agent: newest installed
|
|
12
|
+
* agents sync claude@oldest # one agent: oldest installed
|
|
13
|
+
* agents sync claude@pinned (= claude@default) # one agent: the pinned default version
|
|
12
14
|
* agents sync --agent claude --agent-version 2.1.142 # legacy form, still supported
|
|
13
15
|
*
|
|
14
16
|
* The umbrella stages live in lib/sync-umbrella.ts; this file dispatches to them
|
package/dist/commands/sync.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* agents sync claude # one agent: uses default/sole installed version
|
|
10
10
|
* agents sync claude@2.1.142 # one agent: explicit version
|
|
11
11
|
* agents sync claude@latest # one agent: newest installed
|
|
12
|
+
* agents sync claude@oldest # one agent: oldest installed
|
|
13
|
+
* agents sync claude@pinned (= claude@default) # one agent: the pinned default version
|
|
12
14
|
* agents sync --agent claude --agent-version 2.1.142 # legacy form, still supported
|
|
13
15
|
*
|
|
14
16
|
* The umbrella stages live in lib/sync-umbrella.ts; this file dispatches to them
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
import * as path from 'path';
|
|
29
31
|
import chalk from 'chalk';
|
|
30
32
|
import { agentLabel, resolveAgentName } from '../lib/agents.js';
|
|
31
|
-
import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, listInstalledVersions, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, } from '../lib/versions.js';
|
|
33
|
+
import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, resolveVersionAlias, listInstalledVersions, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, } from '../lib/versions.js';
|
|
32
34
|
import { compileRulesForProject } from '../lib/rules/compile.js';
|
|
33
35
|
import { runLaunchSync } from '../lib/project-launch.js';
|
|
34
36
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -38,7 +40,7 @@ export function registerSyncCommand(program) {
|
|
|
38
40
|
program
|
|
39
41
|
.command('sync [agentSpec]')
|
|
40
42
|
.summary('Make this machine current, or sync resources into one agent')
|
|
41
|
-
.description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude"
|
|
43
|
+
.description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude", "claude@2.1.142", or a selector: @latest / @oldest / @pinned (= @default).\n\nWith NO agent, runs the umbrella verb: fetch remote state (config repos + secrets + sessions) then reconcile it into every installed agent. Scope it with --repos / --secrets / --sessions, --cloud (fetch only), or --local (reconcile only).')
|
|
42
44
|
.option('--agent <agent>', 'Agent identifier (legacy form; prefer the positional spec)')
|
|
43
45
|
.option('--agent-version <version>', 'Version to sync into (legacy form; prefer "agent@version")')
|
|
44
46
|
.option('--project-dir <path>', 'Path to project-level .agents/ directory containing project-scoped resources')
|
|
@@ -116,17 +118,23 @@ async function runSync(agentSpec, opts) {
|
|
|
116
118
|
// ---------- 1. Resolve agent + version ----------
|
|
117
119
|
let agentId;
|
|
118
120
|
let version;
|
|
121
|
+
// A positional @selector typed by the user (latest/oldest/pinned/default/
|
|
122
|
+
// explicit). parseAgentSpec defaults a missing version to 'latest', so a bare
|
|
123
|
+
// `agents sync claude` and `agents sync claude@latest` are indistinguishable
|
|
124
|
+
// after parsing — we only treat the version as a selector when an '@' was
|
|
125
|
+
// actually typed, keeping bare `claude` on the default-version path.
|
|
126
|
+
let selector;
|
|
119
127
|
if (agentSpec) {
|
|
120
128
|
const parsed = parseAgentSpec(agentSpec);
|
|
121
129
|
if (!parsed) {
|
|
122
130
|
errLog(chalk.red(`Invalid agent spec '${agentSpec}'.`));
|
|
123
|
-
errLog(chalk.gray('Examples: claude, claude@2.1.142,
|
|
131
|
+
errLog(chalk.gray('Examples: claude, claude@2.1.142, claude@latest, claude@oldest, claude@pinned'));
|
|
124
132
|
process.exitCode = 1;
|
|
125
133
|
return;
|
|
126
134
|
}
|
|
127
135
|
agentId = parsed.agent;
|
|
128
|
-
if (
|
|
129
|
-
|
|
136
|
+
if (agentSpec.includes('@'))
|
|
137
|
+
selector = parsed.version;
|
|
130
138
|
}
|
|
131
139
|
if (opts.agent) {
|
|
132
140
|
const resolved = resolveAgentName(opts.agent);
|
|
@@ -138,6 +146,8 @@ async function runSync(agentSpec, opts) {
|
|
|
138
146
|
agentId = resolved;
|
|
139
147
|
}
|
|
140
148
|
if (opts.agentVersion) {
|
|
149
|
+
// Legacy flag and the launch-shim hot path (`--agent-version <concrete>`):
|
|
150
|
+
// pass through verbatim. Selector aliases are a positional-spec feature.
|
|
141
151
|
version = opts.agentVersion;
|
|
142
152
|
}
|
|
143
153
|
if (!agentId) {
|
|
@@ -147,6 +157,13 @@ async function runSync(agentSpec, opts) {
|
|
|
147
157
|
return;
|
|
148
158
|
}
|
|
149
159
|
// ---------- 2. Resolve version (project pin → global default → sole installed) ----------
|
|
160
|
+
// A positional @selector wins over the default-resolution below.
|
|
161
|
+
// @latest / @oldest → newest / oldest installed (process.exit if none)
|
|
162
|
+
// @pinned / @default → undefined → fall through to the default path
|
|
163
|
+
// @x.y.z → that version (process.exit if not installed)
|
|
164
|
+
if (selector !== undefined && !version) {
|
|
165
|
+
version = resolveVersionAlias(agentId, selector);
|
|
166
|
+
}
|
|
150
167
|
if (!version) {
|
|
151
168
|
version = resolveVersion(agentId, process.cwd()) || undefined;
|
|
152
169
|
if (!version) {
|
package/dist/commands/view.js
CHANGED
|
@@ -21,6 +21,9 @@ import { listProfiles, profileSummary } from '../lib/profiles.js';
|
|
|
21
21
|
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
22
22
|
import { confirm } from '@inquirer/prompts';
|
|
23
23
|
import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
24
|
+
// Shown in the email column for agents that are signed in but expose no email
|
|
25
|
+
// address locally (Antigravity, Kimi store an opaque OAuth/JWT credential).
|
|
26
|
+
const SIGNED_IN_LABEL = 'signed in';
|
|
24
27
|
/**
|
|
25
28
|
* Group profile summaries by their host harness, optionally filtered to a
|
|
26
29
|
* single agent. Profile YAMLs that fail validation are silently skipped by
|
|
@@ -350,6 +353,8 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
350
353
|
const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
|
|
351
354
|
if (info?.email)
|
|
352
355
|
maxEmail = Math.max(maxEmail, info.email.length);
|
|
356
|
+
else if (info?.signedIn)
|
|
357
|
+
maxEmail = Math.max(maxEmail, SIGNED_IN_LABEL.length);
|
|
353
358
|
if (info?.plan)
|
|
354
359
|
maxPlanWidth = Math.max(maxPlanWidth, info.plan.length);
|
|
355
360
|
}
|
|
@@ -406,6 +411,7 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
406
411
|
// Build columns, trimming trailing whitespace when columns are empty
|
|
407
412
|
const parts = [` ${label}`];
|
|
408
413
|
const hasEmail = !!vInfo?.email;
|
|
414
|
+
const signedIn = !!vInfo?.signedIn;
|
|
409
415
|
const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth);
|
|
410
416
|
const hasUsage = usageStr.length > 0;
|
|
411
417
|
// Only show lastActive for versions with an actual logged-in account.
|
|
@@ -418,14 +424,17 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
418
424
|
runDefaultBits.push(`mode:${runDefaults.mode}`);
|
|
419
425
|
if (runDefaults.model)
|
|
420
426
|
runDefaultBits.push(`model:${runDefaults.model}`);
|
|
421
|
-
if (!hasEmail && !hasUsage) {
|
|
427
|
+
if (!hasEmail && !hasUsage && !signedIn) {
|
|
422
428
|
// Installed but never signed in
|
|
423
429
|
parts.push(chalk.gray('(not signed in — run ' + agent.cliCommand + ' to log in)'));
|
|
424
430
|
}
|
|
425
431
|
else {
|
|
426
|
-
if (hasEmail || hasUsage || hasActive) {
|
|
427
|
-
|
|
428
|
-
|
|
432
|
+
if (hasEmail || hasUsage || hasActive || signedIn) {
|
|
433
|
+
// Signed-in agents without a local email (Antigravity, Kimi) show a
|
|
434
|
+
// "signed in" placeholder so they read as logged in, not blank.
|
|
435
|
+
const display = vInfo?.email || (signedIn ? SIGNED_IN_LABEL : '');
|
|
436
|
+
const emailCol = display.padEnd(maxEmail);
|
|
437
|
+
parts.push(display ? chalk.cyan(emailCol) : ' '.repeat(maxEmail));
|
|
429
438
|
}
|
|
430
439
|
if (hasUsage || hasActive) {
|
|
431
440
|
const usagePad = ' '.repeat(Math.max(0, maxUsageWidth - visibleWidth(usageStr)));
|
|
@@ -503,8 +512,10 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
503
512
|
const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
|
|
504
513
|
const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null);
|
|
505
514
|
const gActiveStr = gInfo ? formatLastActive(gInfo.lastActive) : '';
|
|
506
|
-
if (gInfo?.email || gUsageStr || gActiveStr)
|
|
507
|
-
|
|
515
|
+
if (gInfo?.email || gUsageStr || gActiveStr || gInfo?.signedIn) {
|
|
516
|
+
const gDisplay = gInfo?.email || (gInfo?.signedIn ? SIGNED_IN_LABEL : '');
|
|
517
|
+
parts.push(gDisplay ? chalk.cyan(gDisplay) : '');
|
|
518
|
+
}
|
|
508
519
|
if (gUsageStr || gActiveStr)
|
|
509
520
|
parts.push(gUsageStr);
|
|
510
521
|
const gStatusStr = formatUsageStatusBadge(gInfo?.usageStatus);
|
|
@@ -814,7 +825,11 @@ async function showAgentResources(agentId, requestedVersion, filter) {
|
|
|
814
825
|
cliVersion: version,
|
|
815
826
|
info: accountInfo,
|
|
816
827
|
});
|
|
817
|
-
const emailStr = accountInfo.email
|
|
828
|
+
const emailStr = accountInfo.email
|
|
829
|
+
? chalk.cyan(` ${accountInfo.email}`)
|
|
830
|
+
: accountInfo.signedIn
|
|
831
|
+
? chalk.cyan(` ${SIGNED_IN_LABEL}`)
|
|
832
|
+
: '';
|
|
818
833
|
const status = chalk.green(version);
|
|
819
834
|
const usageStr = formatUsageSummary(accountInfo.plan, null);
|
|
820
835
|
const usagePart = usageStr ? ` ${usageStr}` : '';
|
|
@@ -1006,7 +1021,7 @@ async function collectAgentsJson(filterAgentId) {
|
|
|
1006
1021
|
const entry = {
|
|
1007
1022
|
version,
|
|
1008
1023
|
isDefault: version === globalDefault,
|
|
1009
|
-
signedIn:
|
|
1024
|
+
signedIn: info.signedIn,
|
|
1010
1025
|
email: info.email,
|
|
1011
1026
|
plan: info.plan,
|
|
1012
1027
|
usageStatus: info.usageStatus,
|
|
@@ -1273,9 +1288,10 @@ export async function viewAction(agentArg, options) {
|
|
|
1273
1288
|
console.log(chalk.red(formatAgentError(agentName)));
|
|
1274
1289
|
process.exit(1);
|
|
1275
1290
|
}
|
|
1276
|
-
// Keep 'default' as-is since showAgentResources handles
|
|
1277
|
-
// returns undefined for '
|
|
1278
|
-
|
|
1291
|
+
// Keep 'default'/'pinned' as-is since showAgentResources handles 'default';
|
|
1292
|
+
// resolveVersionAlias returns undefined for both (they're synonyms), which
|
|
1293
|
+
// would otherwise skip the detailed view.
|
|
1294
|
+
const requestedVersion = (parts[1] === 'default' || parts[1] === 'pinned')
|
|
1279
1295
|
? 'default'
|
|
1280
1296
|
: (resolveVersionAlias(agentId, parts[1]) ?? null);
|
|
1281
1297
|
if (prune) {
|
package/dist/index.js
CHANGED
|
@@ -51,7 +51,7 @@ if (IS_DEV_BUILD) {
|
|
|
51
51
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
52
52
|
// first byte of output), the registry maps a command name to a thunk that
|
|
53
53
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
54
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadBrowser, loadComputer, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
|
|
54
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadBrowser, loadComputer, loadHosts, loadSsh, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
|
|
55
55
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
56
56
|
import { IS_WINDOWS } from './lib/platform/index.js';
|
|
57
57
|
// Transparent shim delegate: the generated Windows `.cmd` shims invoke
|
|
@@ -783,6 +783,8 @@ async function registerAllEagerCommands() {
|
|
|
783
783
|
await reg(loadTmux);
|
|
784
784
|
await reg(loadBrowser);
|
|
785
785
|
await reg(loadComputer);
|
|
786
|
+
await reg(loadHosts);
|
|
787
|
+
await reg(loadSsh);
|
|
786
788
|
registerJobsCronAliasCommand(program, 'jobs');
|
|
787
789
|
registerJobsCronAliasCommand(program, 'cron');
|
|
788
790
|
registerUpgradeCommand(program);
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -107,6 +107,7 @@ export interface AccountInfo {
|
|
|
107
107
|
currency: string;
|
|
108
108
|
} | null;
|
|
109
109
|
lastActive: Date | null;
|
|
110
|
+
signedIn: boolean;
|
|
110
111
|
}
|
|
111
112
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
112
113
|
export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;
|
package/dist/lib/agents.js
CHANGED
|
@@ -15,6 +15,7 @@ import * as path from 'path';
|
|
|
15
15
|
import * as os from 'os';
|
|
16
16
|
import * as TOML from 'smol-toml';
|
|
17
17
|
import chalk from 'chalk';
|
|
18
|
+
import { needsWindowsShell } from './platform/index.js';
|
|
18
19
|
import { latestFileMtimeMs } from './fs-walk.js';
|
|
19
20
|
import { damerauLevenshtein } from './fuzzy.js';
|
|
20
21
|
import { getCacheDir, getVersionsDir, getShimsDir, getCliVersionCachePath } from './state.js';
|
|
@@ -782,6 +783,7 @@ export async function getAccountInfo(agentId, home) {
|
|
|
782
783
|
usageStatus: null,
|
|
783
784
|
overageCredits: null,
|
|
784
785
|
lastActive: null,
|
|
786
|
+
signedIn: false,
|
|
785
787
|
};
|
|
786
788
|
const configFiles = {
|
|
787
789
|
claude: path.join(base, '.claude.json'),
|
|
@@ -853,6 +855,7 @@ export async function getAccountInfo(agentId, home) {
|
|
|
853
855
|
usageStatus,
|
|
854
856
|
overageCredits,
|
|
855
857
|
lastActive,
|
|
858
|
+
signedIn: !!email,
|
|
856
859
|
};
|
|
857
860
|
}
|
|
858
861
|
case 'codex': {
|
|
@@ -894,11 +897,13 @@ export async function getAccountInfo(agentId, home) {
|
|
|
894
897
|
usageStatus,
|
|
895
898
|
overageCredits: null,
|
|
896
899
|
lastActive,
|
|
900
|
+
signedIn: !!email,
|
|
897
901
|
};
|
|
898
902
|
}
|
|
899
903
|
case 'gemini': {
|
|
900
904
|
const data = JSON.parse(await fs.promises.readFile(path.join(base, '.gemini', 'google_accounts.json'), 'utf-8'));
|
|
901
|
-
|
|
905
|
+
const email = data.active || null;
|
|
906
|
+
return { ...empty, email, signedIn: !!email, lastActive };
|
|
902
907
|
}
|
|
903
908
|
case 'grok': {
|
|
904
909
|
// Grok stores auth in ~/.grok/auth.json
|
|
@@ -907,12 +912,44 @@ export async function getAccountInfo(agentId, home) {
|
|
|
907
912
|
if (fs.existsSync(authPath)) {
|
|
908
913
|
const data = JSON.parse(await fs.promises.readFile(authPath, 'utf-8'));
|
|
909
914
|
const email = data.email || data.user?.email || data.account?.email || null;
|
|
910
|
-
return { ...empty, email, lastActive };
|
|
915
|
+
return { ...empty, email, signedIn: !!email, lastActive };
|
|
911
916
|
}
|
|
912
917
|
}
|
|
913
918
|
catch { }
|
|
914
919
|
return { ...empty, lastActive };
|
|
915
920
|
}
|
|
921
|
+
case 'antigravity': {
|
|
922
|
+
// Antigravity (`agy`) stores a Google OAuth token at
|
|
923
|
+
// ~/.gemini/antigravity-cli/antigravity-oauth-token. It's a consumer
|
|
924
|
+
// OAuth grant (access + refresh token, no id_token), so there's no email
|
|
925
|
+
// claim to read locally — presence of a refresh token is the only
|
|
926
|
+
// signed-in signal we can derive without a network call.
|
|
927
|
+
const tokenPath = path.join(base, '.gemini', 'antigravity-cli', 'antigravity-oauth-token');
|
|
928
|
+
if (!fs.existsSync(tokenPath))
|
|
929
|
+
return { ...empty, lastActive };
|
|
930
|
+
const data = JSON.parse(await fs.promises.readFile(tokenPath, 'utf-8'));
|
|
931
|
+
const hasToken = typeof data?.token?.refresh_token === 'string' && !!data.token.refresh_token;
|
|
932
|
+
if (!hasToken)
|
|
933
|
+
return { ...empty, lastActive };
|
|
934
|
+
return { ...empty, signedIn: true, lastActive };
|
|
935
|
+
}
|
|
936
|
+
case 'kimi': {
|
|
937
|
+
// Kimi Code stores OAuth credentials at
|
|
938
|
+
// ~/.kimi-code/credentials/kimi-code.json. The access token is a JWT
|
|
939
|
+
// whose payload carries an opaque user_id (no email), so we report
|
|
940
|
+
// signed-in state plus a stable account key for usage dedup.
|
|
941
|
+
const credPath = path.join(base, '.kimi-code', 'credentials', 'kimi-code.json');
|
|
942
|
+
if (!fs.existsSync(credPath))
|
|
943
|
+
return { ...empty, lastActive };
|
|
944
|
+
const data = JSON.parse(await fs.promises.readFile(credPath, 'utf-8'));
|
|
945
|
+
const accessToken = data?.access_token;
|
|
946
|
+
if (typeof accessToken !== 'string' || !accessToken)
|
|
947
|
+
return { ...empty, lastActive };
|
|
948
|
+
const decoded = decodeJwtPayload(accessToken);
|
|
949
|
+
const userId = normalizeIdentityPart(decoded?.user_id ?? decoded?.sub);
|
|
950
|
+
const accountKey = buildIdentityKey(agentId, [['user', userId]]);
|
|
951
|
+
return { ...empty, signedIn: true, accountId: userId, accountKey, lastActive };
|
|
952
|
+
}
|
|
916
953
|
default:
|
|
917
954
|
return { ...empty, lastActive };
|
|
918
955
|
}
|
|
@@ -1151,7 +1188,10 @@ export async function registerMcp(agentId, name, command, scope = 'user', transp
|
|
|
1151
1188
|
}
|
|
1152
1189
|
// When home is specified, override HOME so MCP config writes to the version's config dir
|
|
1153
1190
|
const env = options?.home ? { ...process.env, HOME: options.home } : undefined;
|
|
1154
|
-
|
|
1191
|
+
// On Windows a bare command name / `.cmd` wrapper (the npm-installed agent
|
|
1192
|
+
// CLI) can't be exec'd directly — it needs shell:true for PATHEXT/cmd. Off
|
|
1193
|
+
// Windows this is always false, so the no-shell argv path is unchanged.
|
|
1194
|
+
await execFileAsync(bin, args, { ...(env ? { env } : {}), shell: needsWindowsShell(bin) });
|
|
1155
1195
|
return { success: true };
|
|
1156
1196
|
}
|
|
1157
1197
|
catch (err) {
|
|
@@ -1170,7 +1210,7 @@ export async function unregisterMcp(agentId, name, options) {
|
|
|
1170
1210
|
try {
|
|
1171
1211
|
const bin = options?.binary || agent.cliCommand;
|
|
1172
1212
|
const env = options?.home ? { ...process.env, HOME: options.home } : undefined;
|
|
1173
|
-
await execFileAsync(bin, ['mcp', 'remove', name], env ? { env } :
|
|
1213
|
+
await execFileAsync(bin, ['mcp', 'remove', name], { ...(env ? { env } : {}), shell: needsWindowsShell(bin) });
|
|
1174
1214
|
return { success: true };
|
|
1175
1215
|
}
|
|
1176
1216
|
catch (err) {
|
|
@@ -1,11 +1,56 @@
|
|
|
1
1
|
import { CDPClient } from '../cdp.js';
|
|
2
2
|
import type { BrowserProfile } from '../types.js';
|
|
3
|
+
import { shellQuote } from '../../ssh-exec.js';
|
|
4
|
+
export { shellQuote };
|
|
3
5
|
export interface SSHConnection {
|
|
4
6
|
cdp: CDPClient;
|
|
5
7
|
port: number;
|
|
6
8
|
pid: number;
|
|
7
9
|
cleanup: () => void;
|
|
8
10
|
}
|
|
9
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Which shell dialect the *remote* host speaks. Selected per-endpoint via the
|
|
13
|
+
* `&os=windows` query param on an `ssh://` target. POSIX is the default — the
|
|
14
|
+
* historical behavior for macOS/Linux remotes. Windows remotes run OpenSSH
|
|
15
|
+
* Server with cmd.exe as the default shell, so the launch/teardown command
|
|
16
|
+
* strings differ (no `&` backgrounding, no `lsof`, `.exe` instead of `.app`).
|
|
17
|
+
*/
|
|
18
|
+
export type RemoteOs = 'windows' | 'posix';
|
|
10
19
|
export declare function connectSSH(endpoint: string, profile: BrowserProfile): Promise<SSHConnection>;
|
|
11
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Wrap a PowerShell script as a `-EncodedCommand` invocation. Base64 of the
|
|
22
|
+
* UTF-16LE bytes is a single quote-free token, so it rides through Node spawn
|
|
23
|
+
* → Windows sshd → cmd.exe with zero escaping hazards (hand-quoted
|
|
24
|
+
* `powershell -Command "…"` is fragile the moment a path or URL is involved).
|
|
25
|
+
*/
|
|
26
|
+
export declare function encodePowerShell(script: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* The PowerShell that launches the browser on a Windows remote. Two hard
|
|
29
|
+
* requirements shaped this:
|
|
30
|
+
* 1. The browser must OUTLIVE the ssh session. Windows OpenSSH terminates
|
|
31
|
+
* the session's job tree on disconnect, which reaps both `start /B` and
|
|
32
|
+
* `Start-Process` children (verified against a real box). WMI
|
|
33
|
+
* `Win32_Process.Create` spawns under the WMI provider service instead,
|
|
34
|
+
* so the process survives after we drop the ssh connection and reconnect
|
|
35
|
+
* over the CDP tunnel.
|
|
36
|
+
* 2. A distinct `--user-data-dir` so a fresh instance bound to the debugging
|
|
37
|
+
* port comes up even when the user already has Edge open.
|
|
38
|
+
* CreateProcess ignores App Paths, so we resolve the real `.exe` from the
|
|
39
|
+
* registry at runtime rather than relying on a bare `msedge` name.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildWindowsLaunchScript(browserType: string, port: number, customBinary?: string): string;
|
|
42
|
+
/** The PowerShell that kills whatever holds the CDP port on a Windows remote. */
|
|
43
|
+
export declare function buildWindowsKillScript(port: number): string;
|
|
44
|
+
/**
|
|
45
|
+
* Build the remote command that launches the browser detached with a CDP port.
|
|
46
|
+
* POSIX backgrounds the `.app` binary with `… &`; Windows resolves the exe and
|
|
47
|
+
* spawns it via WMI (encoded PowerShell) so it survives the ssh session.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildLaunchCmd(remoteOs: RemoteOs, browserType: string, port: number, customBinary?: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Build the remote command that kills whatever holds the CDP port.
|
|
52
|
+
* POSIX uses `lsof`+`kill`; Windows uses encoded PowerShell
|
|
53
|
+
* (Get-NetTCPConnection → Stop-Process).
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildKillCmd(remoteOs: RemoteOs, port: number): string;
|
|
56
|
+
export declare function restartRemoteBrowser(user: string, host: string, browserType: string, port: number, remoteOs: RemoteOs, customBinary?: string): Promise<void>;
|