@rynx-ai/daemon 0.1.0 → 0.1.9

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.
Files changed (89) hide show
  1. package/dist/browser-cli-args.d.ts +20 -0
  2. package/dist/browser-cli-args.js +100 -0
  3. package/dist/browser-runner-session-context.d.ts +24 -0
  4. package/dist/browser-runner-session-context.js +113 -0
  5. package/dist/cdp-keyboard-input.d.ts +6 -0
  6. package/dist/cdp-keyboard-input.js +209 -0
  7. package/dist/channel-store.d.ts +0 -6
  8. package/dist/channel-store.js +2 -18
  9. package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
  10. package/dist/chrome-for-testing-release-resolver.js +214 -0
  11. package/dist/chrome-for-testing-store.d.ts +85 -0
  12. package/dist/chrome-for-testing-store.js +836 -0
  13. package/dist/cli.js +467 -30
  14. package/dist/control-client.d.ts +113 -0
  15. package/dist/control-client.js +792 -0
  16. package/dist/control-deps.js +37 -6
  17. package/dist/control-endpoint.d.ts +14 -0
  18. package/dist/control-endpoint.js +124 -0
  19. package/dist/control-link-store.d.ts +16 -0
  20. package/dist/control-link-store.js +53 -0
  21. package/dist/daemon-owner.d.ts +14 -0
  22. package/dist/daemon-owner.js +104 -0
  23. package/dist/daemon-server.d.ts +12 -2
  24. package/dist/daemon-server.js +559 -28
  25. package/dist/db.js +140 -10
  26. package/dist/desktop-browser-host-client.d.ts +34 -0
  27. package/dist/desktop-browser-host-client.js +413 -0
  28. package/dist/direct-runtime-authenticator.d.ts +13 -0
  29. package/dist/direct-runtime-authenticator.js +127 -0
  30. package/dist/direct-runtime-config.d.ts +20 -0
  31. package/dist/direct-runtime-config.js +101 -0
  32. package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
  33. package/dist/embedded-browser-profile-cleanup.js +338 -0
  34. package/dist/headless-browser-host.d.ts +14 -0
  35. package/dist/headless-browser-host.js +2156 -0
  36. package/dist/index-daemon.js +28 -5
  37. package/dist/installation-id.d.ts +33 -0
  38. package/dist/installation-id.js +692 -0
  39. package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
  40. package/dist/migrations/cleanup-legacy-sessions.js +10 -28
  41. package/dist/plugin-cli.d.ts +30 -0
  42. package/dist/plugin-cli.js +270 -0
  43. package/dist/plugin-host-rpc.d.ts +85 -0
  44. package/dist/plugin-host-rpc.js +878 -0
  45. package/dist/plugin-install-restart.d.ts +13 -0
  46. package/dist/plugin-install-restart.js +24 -0
  47. package/dist/plugin-installer.d.ts +36 -15
  48. package/dist/plugin-installer.js +420 -155
  49. package/dist/plugin-package.d.ts +38 -0
  50. package/dist/plugin-package.js +553 -0
  51. package/dist/plugin-runtime-control.d.ts +2 -0
  52. package/dist/plugin-runtime-control.js +37 -0
  53. package/dist/plugin-store.d.ts +37 -8
  54. package/dist/plugin-store.js +80 -15
  55. package/dist/plugin-supervisor.d.ts +160 -0
  56. package/dist/plugin-supervisor.js +1143 -0
  57. package/dist/pm2.d.ts +3 -1
  58. package/dist/pm2.js +55 -13
  59. package/dist/provider-cli-service.d.ts +46 -0
  60. package/dist/provider-cli-service.js +510 -0
  61. package/dist/registry.d.ts +6 -19
  62. package/dist/registry.js +22 -87
  63. package/dist/remote-runtime-access-store.d.ts +61 -0
  64. package/dist/remote-runtime-access-store.js +386 -0
  65. package/dist/remote-runtime-admin.d.ts +32 -0
  66. package/dist/remote-runtime-admin.js +107 -0
  67. package/dist/remote-runtime-connection-manager.d.ts +92 -0
  68. package/dist/remote-runtime-connection-manager.js +1409 -0
  69. package/dist/remote-runtime-credential-store.d.ts +45 -0
  70. package/dist/remote-runtime-credential-store.js +639 -0
  71. package/dist/remote-runtime-target-control.d.ts +47 -0
  72. package/dist/remote-runtime-target-control.js +433 -0
  73. package/dist/remote-runtime-target-store.d.ts +53 -0
  74. package/dist/remote-runtime-target-store.js +307 -0
  75. package/dist/runtime-share-addresses.d.ts +21 -0
  76. package/dist/runtime-share-addresses.js +50 -0
  77. package/dist/session-browser-capability-store.d.ts +13 -0
  78. package/dist/session-browser-capability-store.js +58 -0
  79. package/dist/session-emulator-binding-store.d.ts +10 -0
  80. package/dist/session-emulator-binding-store.js +61 -0
  81. package/dist/session-launch-operations.d.ts +41 -0
  82. package/dist/session-launch-operations.js +124 -0
  83. package/dist/session-meta-store.d.ts +3 -2
  84. package/dist/session-meta-store.js +57 -7
  85. package/dist/setup.js +9 -4
  86. package/dist/skills-catalog.js +8 -4
  87. package/dist/update.d.ts +11 -2
  88. package/dist/update.js +0 -27
  89. package/package.json +18 -10
package/dist/cli.js CHANGED
@@ -3,7 +3,8 @@
3
3
  * `rynx` — the daemon CLI.
4
4
  *
5
5
  * rynx start | stop | restart | status | logs process lifecycle (pm2)
6
- * rynx plugin add <spec> | ls | rm <name> | enable <name> | disable <name>
6
+ * rynx plugin manage install|list|uninstall|enable|disable ...
7
+ * rynx plugin <plugin-id> <plugin-command> ...
7
8
  * rynx agent list | show <name> | add <name> | rm <name>
8
9
  *
9
10
  * The CLI owns process lifecycle, plugin install/registry, and the file-based
@@ -19,14 +20,22 @@ import { createRequire } from "node:module";
19
20
  import path from "node:path";
20
21
  import { confirm, isCancel } from "@clack/prompts";
21
22
  import { listAgentSpecs, loadAgentSpec, loadConfig, rynxAgentsDir } from "@rynx-ai/core";
23
+ import { encodePairingCode } from "@rynx-ai/protocol/direct-runtime";
22
24
  import { agentSpecPath, removeAgentSpec, scaffoldAgentSpec } from "./agent-file.js";
25
+ import { parseBrowserCliArgs, resolveBrowserCliTarget, } from "./browser-cli-args.js";
26
+ import { resolveChromeForTestingRelease } from "./chrome-for-testing-release-resolver.js";
27
+ import { createChromeForTestingArtifactStore } from "./chrome-for-testing-store.js";
23
28
  import { dbPath } from "./db.js";
24
29
  import { cleanupLegacySessions } from "./migrations/cleanup-legacy-sessions.js";
30
+ import { PluginCliUsageError, runPluginCliCommand } from "./plugin-cli.js";
25
31
  import { installPlugin, uninstallPlugin } from "./plugin-installer.js";
26
- import { getPlugin, listPlugins, setPluginEnabled } from "./plugin-store.js";
27
- import { startDaemon, statusDaemon, stopDaemon, streamLogs } from "./pm2.js";
32
+ import { restartAfterPluginInstall } from "./plugin-install-restart.js";
33
+ import { getPlugin, isRunnablePluginRecord, listPlugins, setPluginEnabled } from "./plugin-store.js";
34
+ import { ensureResidentPluginRuntime, reloadResidentPluginRuntime, } from "./plugin-runtime-control.js";
35
+ import { isPm2DaemonOnline, startDaemon, statusDaemon, stopDaemon, streamLogs } from "./pm2.js";
28
36
  import { runDoctor, runSetup } from "./setup.js";
29
37
  import { runUpdate } from "./update.js";
38
+ import { createResidentRemoteRuntimePairingOffer, callManagedRuntimeBrowser, callResidentRuntime, forgetResidentRuntimeTarget, getResidentRuntimeLocalBrowserEndpoint, listResidentRemoteRuntimeClients, listResidentRuntimeTargets, pairResidentRuntimeTarget, revokeResidentRemoteRuntimeClient, readOptionalManagedRuntimeBrowserCredential, testResidentRuntimeTarget, } from "./control-client.js";
30
39
  const USAGE = `Usage: rynx <command>
31
40
 
32
41
  Setup:
@@ -41,15 +50,15 @@ Lifecycle:
41
50
  logs tail the server logs
42
51
  update [version] upgrade @rynx-ai/daemon (npm) + restart (--check: report only)
43
52
 
44
- Plugins (~/.rynx/rynx.db · channel plugins):
45
- plugin ls list installed plugins
46
- plugin add <spec> [--force]
47
- install from an npm spec or a local directory
48
- (local is copied into ~/.rynx/plugins; --force overwrites a same-named plugin)
49
- plugin rm <name> uninstall a plugin
50
- plugin enable <name> enable / disable an installed plugin
51
- plugin disable <name>
52
- (plugin changes apply on \`rynx restart\`)
53
+ Plugins (static manifest + isolated runner):
54
+ plugin manage list
55
+ plugin manage install <npm|path|git|url> [--force] [--grant-all] [--allow-scripts] [--restart]
56
+ plugin manage update <id> [--grant-all] [--allow-scripts]
57
+ plugin manage uninstall <id>
58
+ plugin manage enable|disable <id>
59
+ manage arbitrary installed plugins
60
+ plugin <id> <command> run a command declared by that plugin
61
+ (\`manage\` is a reserved plugin id)
53
62
 
54
63
  Agents (~/.rynx/agents/<id>/agent.json — dir name is the id):
55
64
  agent list list agents (id · runtime · model · [display name])
@@ -67,7 +76,44 @@ Maintenance:
67
76
 
68
77
  Emulator:
69
78
  emulator <args…> passthrough to the bundled \`rynx-emulator\` CLI
70
- (same commands/flags; run \`rynx emulator\` for its help)
79
+ (use --runtime <local|daemon-id> for bounded unary
80
+ remote control; run \`rynx emulator\` for its help)
81
+
82
+ Remote Runtime:
83
+ runtime share --address <host|ws-url> [--label <label>] [--json]
84
+ on the remote host, print one revocable pairing link
85
+ runtime pair [...] compatibility alias for runtime share
86
+ runtime add --pairing-code <rynx://...> [--name <name>]
87
+ pair, verify and save a remote daemon
88
+ (stdin remains supported for scripts)
89
+ runtime list [--json] list the synthetic local target and paired daemons
90
+ runtime test <local|daemon-id> [--json]
91
+ authenticate and read the selected daemon status
92
+ runtime forget <daemon-id>
93
+ remove one local target and its client credential
94
+ runtime clients list [--json]
95
+ runtime clients revoke <grant-id>
96
+ inspect/revoke clients authorized by this daemon
97
+
98
+ Browser (one Browser per Runtime Session):
99
+ browser install [--channel <stable|beta|dev|canary> | --version <x.x.x.x>] [--json]
100
+ install and activate managed Chrome for Testing
101
+ browser update [--channel <stable|beta|dev|canary>] [--json]
102
+ resolve and activate the current channel build
103
+ browser version [--json]
104
+ show the active managed Browser engine
105
+ browser clean [--json] remove inactive managed builds while retaining active,
106
+ rollback, and live Headless Browser builds
107
+ browser open [url] [--session <id>] [--runtime <local|daemon-id>] [--json]
108
+ open the selected Runtime Session's managed Browser
109
+ browser status [--session <id>] [--runtime <local|daemon-id>] [--json]
110
+ show Browser generation, state and active Page
111
+ browser pages [--session <id>] [--runtime <local|daemon-id>] [--json]
112
+ list the Browser's opaque Pages
113
+ browser endpoint [--ensure] [--session <local-id>] [--json]
114
+ print Runtime-local CDP; --ensure opens it when absent
115
+ browser close [--session <id>] [--runtime <local|daemon-id>] [--json]
116
+ close the current Browser generation
71
117
 
72
118
  Channel instances (a channel + an agent) are configured in the web control
73
119
  console (run \`rynx start\`, then open the printed URL).
@@ -76,9 +122,8 @@ function fail(message) {
76
122
  console.error(message);
77
123
  process.exit(2);
78
124
  }
79
- async function pluginCommand(sub, arg) {
125
+ async function pluginManageCommand(sub, arg) {
80
126
  switch (sub) {
81
- case "ls":
82
127
  case "list": {
83
128
  const plugins = listPlugins();
84
129
  console.log(`Plugins (db: ${dbPath()})`);
@@ -87,24 +132,30 @@ async function pluginCommand(sub, arg) {
87
132
  return;
88
133
  }
89
134
  for (const p of plugins) {
90
- const state = p.enabled ? "enabled" : "disabled";
135
+ const state = !isRunnablePluginRecord(p)
136
+ ? "reinstall-required"
137
+ : p.enabled
138
+ ? "enabled"
139
+ : "disabled";
91
140
  console.log(` ${p.name.padEnd(16)} ${(p.version ?? "-").padEnd(10)} ${p.source.padEnd(6)} ${state}`);
92
141
  }
93
142
  return;
94
143
  }
95
- case "add":
96
144
  case "install": {
97
145
  if (!arg)
98
- fail("plugin add: missing spec (an npm package or a local directory)");
146
+ fail("plugin manage install: missing npm/path/git/url source");
99
147
  const force = process.argv.includes("--force");
148
+ const grantAll = process.argv.includes("--grant-all");
149
+ const allowScripts = process.argv.includes("--allow-scripts");
100
150
  const result = await installPlugin(arg, (line) => console.log(line), {
151
+ allowScripts,
101
152
  onConflict: async (existing) => {
102
153
  if (force)
103
154
  return true;
104
155
  const desc = `${existing.source}${existing.version ? ` @${existing.version}` : ""}`;
105
156
  if (!process.stdin.isTTY) {
106
157
  fail(`plugin "${existing.name}" already installed (${desc}). ` +
107
- `Re-run with --force, or \`rynx plugin rm ${existing.name}\` first.`);
158
+ `Re-run with --force, or \`rynx plugin manage uninstall ${existing.name}\` first.`);
108
159
  }
109
160
  const yes = await confirm({
110
161
  message: `插件 "${existing.name}" 已存在(${desc}),覆盖吗?`,
@@ -112,20 +163,52 @@ async function pluginCommand(sub, arg) {
112
163
  });
113
164
  return !isCancel(yes) && yes === true;
114
165
  },
166
+ approveCapabilities: (manifest, existing) => approvePluginCapabilities(manifest, existing?.grants ?? [], grantAll),
115
167
  });
116
168
  if (result.skipped) {
117
169
  console.log(`已取消,保留现有插件 "${result.name}"。`);
118
170
  }
119
171
  else {
120
- console.log(`Plugin "${result.name}" (${result.source}${result.version ? ` @${result.version}` : ""}) installed. Run \`rynx restart\` to apply.`);
172
+ console.log(`Plugin "${result.name}" (${result.source}${result.version ? ` @${result.version}` : ""}) installed.`);
173
+ await restartAfterPluginInstall({
174
+ restart: process.argv.includes("--restart"),
175
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
176
+ }, {
177
+ isDaemonOnline: isPm2DaemonOnline,
178
+ confirmRestart: async () => {
179
+ const answer = await confirm({
180
+ message: "Restart Rynx now to apply the plugin?",
181
+ initialValue: true,
182
+ });
183
+ return !isCancel(answer) && answer === true;
184
+ },
185
+ restartDaemon: () => startDaemon({ force: true }),
186
+ log: (message) => console.log(message),
187
+ });
121
188
  }
122
189
  return;
123
190
  }
124
- case "rm":
125
- case "remove":
191
+ case "update": {
192
+ if (!arg)
193
+ fail("plugin manage update: missing plugin id");
194
+ const existing = getPlugin(arg);
195
+ if (!existing)
196
+ fail(`plugin "${arg}" not found`);
197
+ if (!isRunnablePluginRecord(existing)) {
198
+ fail(`plugin "${arg}" uses the removed legacy format; reinstall it from an explicit source`);
199
+ }
200
+ const result = await installPlugin(existing.spec, (line) => console.log(line), {
201
+ expectedId: existing.name,
202
+ allowScripts: process.argv.includes("--allow-scripts"),
203
+ onConflict: () => true,
204
+ approveCapabilities: (manifest, current) => approvePluginCapabilities(manifest, current?.grants ?? [], process.argv.includes("--grant-all")),
205
+ });
206
+ console.log(`Plugin "${result.name}" updated${result.version ? ` to ${result.version}` : ""}. Run \`rynx restart\` to apply.`);
207
+ return;
208
+ }
126
209
  case "uninstall": {
127
210
  if (!arg)
128
- fail("plugin rm: missing plugin name");
211
+ fail("plugin manage uninstall: missing plugin id");
129
212
  if (!(await uninstallPlugin(arg, (line) => console.log(line))))
130
213
  fail(`plugin "${arg}" not found`);
131
214
  console.log(`Plugin "${arg}" uninstalled. Run \`rynx restart\` to apply.`);
@@ -134,9 +217,12 @@ async function pluginCommand(sub, arg) {
134
217
  case "enable":
135
218
  case "disable": {
136
219
  if (!arg)
137
- fail(`plugin ${sub}: missing plugin name`);
138
- if (!getPlugin(arg))
220
+ fail(`plugin manage ${sub}: missing plugin id`);
221
+ const plugin = getPlugin(arg);
222
+ if (!plugin)
139
223
  fail(`plugin "${arg}" not found`);
224
+ if (!isRunnablePluginRecord(plugin))
225
+ fail(`plugin "${arg}" must be reinstalled before it can be enabled`);
140
226
  setPluginEnabled(arg, sub === "enable");
141
227
  console.log(`Plugin "${arg}" ${sub}d. Run \`rynx restart\` to apply.`);
142
228
  return;
@@ -145,6 +231,69 @@ async function pluginCommand(sub, arg) {
145
231
  fail(USAGE);
146
232
  }
147
233
  }
234
+ async function pluginCommand(args) {
235
+ const [pluginId, command] = args;
236
+ if (pluginId === "manage") {
237
+ await pluginManageCommand(command, args[2]);
238
+ return;
239
+ }
240
+ if (!pluginId)
241
+ fail(USAGE);
242
+ try {
243
+ const result = await runPluginCliCommand(pluginId, args.slice(1));
244
+ if (result.code !== 0) {
245
+ process.exitCode = result.code;
246
+ return;
247
+ }
248
+ const reload = result.effects.some((effect) => effect.effect === "runtime.reload");
249
+ const ensure = result.effects.some((effect) => effect.effect === "runtime.ensure");
250
+ if (reload || ensure) {
251
+ try {
252
+ if (reload)
253
+ await reloadResidentPluginRuntime(pluginId);
254
+ else
255
+ await ensureResidentPluginRuntime(pluginId);
256
+ }
257
+ catch (error) {
258
+ const detail = error instanceof Error ? error.message : String(error);
259
+ throw new Error(reload
260
+ ? `Plugin "${pluginId}" configuration was saved, but its runtime did not become ready: ${detail}`
261
+ : `Plugin "${pluginId}" runtime did not become ready: ${detail}`);
262
+ }
263
+ if (!args.includes("--json"))
264
+ console.log(`Plugin "${pluginId}" runtime is ready.`);
265
+ }
266
+ }
267
+ catch (error) {
268
+ if (error instanceof PluginCliUsageError) {
269
+ console.log(error.message);
270
+ return;
271
+ }
272
+ throw error;
273
+ }
274
+ }
275
+ async function approvePluginCapabilities(manifest, existing, grantAll) {
276
+ const requested = manifest.requestedCapabilities;
277
+ const existingSet = new Set(existing);
278
+ const newCapabilities = requested.filter((capability) => !existingSet.has(capability));
279
+ if (newCapabilities.length === 0)
280
+ return requested.filter((capability) => existingSet.has(capability));
281
+ if (grantAll)
282
+ return requested;
283
+ console.log(`Plugin "${manifest.id}" requests host capabilities:`);
284
+ for (const capability of newCapabilities)
285
+ console.log(` - ${capability}`);
286
+ if (!process.stdin.isTTY) {
287
+ throw new Error("capability approval required; inspect the list and re-run with --grant-all");
288
+ }
289
+ const approved = await confirm({
290
+ message: `Grant these ${newCapabilities.length} capability/capabilities to "${manifest.id}"?`,
291
+ initialValue: false,
292
+ });
293
+ if (isCancel(approved) || approved !== true)
294
+ throw new Error("plugin installation cancelled");
295
+ return requested;
296
+ }
148
297
  async function agentCommand(sub, name) {
149
298
  switch (sub) {
150
299
  case "list": {
@@ -219,6 +368,291 @@ function emulatorPassthrough(args) {
219
368
  process.exit(1);
220
369
  });
221
370
  }
371
+ async function runtimeCommand(args) {
372
+ const [sub, arg] = args;
373
+ const json = args.includes("--json");
374
+ switch (sub) {
375
+ case "share":
376
+ case "pair": {
377
+ const label = optionValue(args, "--label");
378
+ const address = optionValue(args, "--address");
379
+ if (!address) {
380
+ fail("runtime share: pass --address with a host or ws(s) URL reachable by the other device");
381
+ }
382
+ const offer = await createResidentRemoteRuntimePairingOffer({
383
+ ...(label ? { clientLabel: label } : {}),
384
+ address,
385
+ });
386
+ console.log(json ? JSON.stringify(offer, null, 2) : encodePairingCode(offer));
387
+ return;
388
+ }
389
+ case "add": {
390
+ const name = optionValue(args, "--name");
391
+ const code = optionValue(args, "--pairing-code");
392
+ if (code === undefined && process.stdin.isTTY) {
393
+ fail("runtime add: pass --pairing-code or pipe a pairing link on stdin");
394
+ }
395
+ const raw = code ?? await readStdinBounded(64 * 1024);
396
+ if (!raw.trim()) {
397
+ fail("runtime add: pass --pairing-code or pipe a pairing link on stdin");
398
+ }
399
+ let offer = raw.trim();
400
+ if (raw.trim().startsWith("{")) {
401
+ try {
402
+ offer = JSON.parse(raw);
403
+ }
404
+ catch {
405
+ fail("runtime add: stdin is not valid pairing JSON");
406
+ }
407
+ }
408
+ const result = await pairResidentRuntimeTarget(offer, name);
409
+ if (json)
410
+ console.log(JSON.stringify(result, null, 2));
411
+ else {
412
+ console.log(`Paired ${result.target.displayName} (${result.target.daemonId}) via ${result.target.endpoint}.`);
413
+ console.log(`Remote instance: ${result.status.daemonInstanceId}`);
414
+ }
415
+ return;
416
+ }
417
+ case "list": {
418
+ const targets = await listResidentRuntimeTargets();
419
+ if (json) {
420
+ console.log(JSON.stringify({ runtimes: targets }, null, 2));
421
+ return;
422
+ }
423
+ console.log("Runtime targets");
424
+ for (const target of targets) {
425
+ const route = target.binding === "local" ? "local" : target.endpoint;
426
+ console.log(` ${target.selector.padEnd(38)} ${target.displayName} ${route}`);
427
+ }
428
+ return;
429
+ }
430
+ case "test": {
431
+ if (!arg || arg.startsWith("--"))
432
+ fail("runtime test: missing local or daemon id");
433
+ const result = await testResidentRuntimeTarget(arg);
434
+ if (json)
435
+ console.log(JSON.stringify(result, null, 2));
436
+ else {
437
+ console.log(`${result.target.displayName}: connected to ${result.status.daemonId} ` +
438
+ `(instance ${result.status.daemonInstanceId})`);
439
+ }
440
+ return;
441
+ }
442
+ case "forget": {
443
+ if (!arg || arg.startsWith("--"))
444
+ fail("runtime forget: missing daemon id");
445
+ await forgetResidentRuntimeTarget(arg);
446
+ console.log(`Forgot Runtime target ${arg}.`);
447
+ return;
448
+ }
449
+ case "clients": {
450
+ const action = arg;
451
+ if (action === "list") {
452
+ const clients = await listResidentRemoteRuntimeClients();
453
+ if (json) {
454
+ console.log(JSON.stringify({ clients }, null, 2));
455
+ return;
456
+ }
457
+ console.log("Remote Runtime clients");
458
+ if (clients.length === 0)
459
+ console.log(" (none)");
460
+ for (const client of clients) {
461
+ const state = client.revokedAt ? "revoked" : "active";
462
+ console.log(` ${client.grantId.padEnd(38)} ${client.clientLabel ?? client.clientId} ${state}`);
463
+ }
464
+ return;
465
+ }
466
+ if (action === "revoke") {
467
+ const grantId = args[2];
468
+ if (!grantId || grantId.startsWith("--")) {
469
+ fail("runtime clients revoke: missing grant id");
470
+ }
471
+ const result = await revokeResidentRemoteRuntimeClient(grantId);
472
+ console.log(`Revoked ${grantId}; closed ${result.closedConnections} active connection(s).`);
473
+ return;
474
+ }
475
+ fail("runtime clients: expected list or revoke <grant-id>");
476
+ }
477
+ default:
478
+ fail(USAGE);
479
+ }
480
+ }
481
+ async function browserCommand(args) {
482
+ let parsed;
483
+ try {
484
+ parsed = parseBrowserCliArgs(args);
485
+ }
486
+ catch (error) {
487
+ fail(error instanceof Error ? error.message : String(error));
488
+ }
489
+ const sub = parsed.subcommand;
490
+ const json = parsed.json;
491
+ if (sub === "install" || sub === "update") {
492
+ const version = parsed.version;
493
+ const channelValue = parsed.channel;
494
+ if (version && channelValue) {
495
+ fail("browser install: choose either --version or --channel");
496
+ }
497
+ if (sub === "update" && version) {
498
+ fail("browser update: use --channel, or browser install --version for a pinned build");
499
+ }
500
+ const selector = version
501
+ ? { kind: "version", version }
502
+ : { kind: "channel", channel: browserReleaseChannel(channelValue) };
503
+ const resolved = await resolveChromeForTestingRelease(selector);
504
+ const installed = await createChromeForTestingArtifactStore().installResolved(resolved, {
505
+ ...(json ? {} : { onProgress: (message) => console.log(message) }),
506
+ });
507
+ if (json)
508
+ console.log(JSON.stringify({ resolved, installed }, null, 2));
509
+ else
510
+ console.log(`Active Browser engine: Chrome for Testing ${installed.version}.`);
511
+ return;
512
+ }
513
+ if (sub === "version") {
514
+ const installed = createChromeForTestingArtifactStore().readActive();
515
+ if (json)
516
+ console.log(JSON.stringify({ installed: installed ?? null }, null, 2));
517
+ else if (installed) {
518
+ console.log(`Chrome for Testing ${installed.version} · ${installed.hostPlatform} · ${installed.sha256}`);
519
+ }
520
+ else {
521
+ console.log("No managed Chrome for Testing build is installed.");
522
+ }
523
+ return;
524
+ }
525
+ if (sub === "clean") {
526
+ const result = createChromeForTestingArtifactStore().pruneBuilds();
527
+ if (json)
528
+ console.log(JSON.stringify(result, null, 2));
529
+ else if (result.removedBuilds.length === 0) {
530
+ console.log("No inactive Chrome for Testing builds were eligible for removal.");
531
+ }
532
+ else {
533
+ console.log(`Removed ${result.removedBuilds.length} inactive Chrome for Testing build(s).`);
534
+ }
535
+ return;
536
+ }
537
+ if (sub === "endpoint") {
538
+ const target = await resolveBrowserTarget(parsed);
539
+ if (parsed.ensure) {
540
+ await callBrowserRuntime(target, "browser.open", { sessionId: target.sessionId });
541
+ }
542
+ const descriptor = await getResidentRuntimeLocalBrowserEndpoint({
543
+ ...(target.credential ? {} : { sessionId: target.sessionId }),
544
+ });
545
+ console.log(json ? JSON.stringify(descriptor, null, 2) : descriptor.endpoint);
546
+ return;
547
+ }
548
+ const target = await resolveBrowserTarget(parsed);
549
+ const sessionId = target.sessionId;
550
+ if (sub === "open") {
551
+ const state = await callBrowserRuntime(target, "browser.open", {
552
+ sessionId,
553
+ ...(parsed.url ? { url: parsed.url } : {}),
554
+ });
555
+ printBrowserState(state, json);
556
+ return;
557
+ }
558
+ const state = await callBrowserRuntime(target, "browser.state.get", { sessionId });
559
+ if (sub === "status") {
560
+ printBrowserState(state, json);
561
+ return;
562
+ }
563
+ if (sub === "pages") {
564
+ if (json)
565
+ console.log(JSON.stringify({ pages: state?.pages ?? [] }, null, 2));
566
+ else
567
+ printBrowserPages(state);
568
+ return;
569
+ }
570
+ if (!state) {
571
+ if (json)
572
+ console.log(JSON.stringify({ closed: false, browser: null }, null, 2));
573
+ else
574
+ console.log(`No Browser is open for Session ${sessionId}.`);
575
+ return;
576
+ }
577
+ const result = await callBrowserRuntime(target, "browser.close", {
578
+ sessionId,
579
+ browserGeneration: state.browserGeneration,
580
+ });
581
+ console.log(json
582
+ ? JSON.stringify(result, null, 2)
583
+ : `Closed Browser generation ${result.browserGeneration} for Session ${sessionId}.`);
584
+ }
585
+ function browserReleaseChannel(value) {
586
+ const normalized = value === undefined
587
+ ? "Stable"
588
+ : `${value.slice(0, 1).toUpperCase()}${value.slice(1).toLowerCase()}`;
589
+ if (normalized !== "Stable" &&
590
+ normalized !== "Beta" &&
591
+ normalized !== "Dev" &&
592
+ normalized !== "Canary") {
593
+ fail("browser: --channel must be stable, beta, dev, or canary");
594
+ }
595
+ return normalized;
596
+ }
597
+ async function resolveBrowserTarget(args) {
598
+ const credential = await readOptionalManagedRuntimeBrowserCredential();
599
+ try {
600
+ return resolveBrowserCliTarget(args, credential);
601
+ }
602
+ catch (error) {
603
+ fail(error instanceof Error ? error.message : String(error));
604
+ }
605
+ }
606
+ async function callBrowserRuntime(target, method, params) {
607
+ if (target.credential) {
608
+ return callManagedRuntimeBrowser(target.credential, method, params);
609
+ }
610
+ return callResidentRuntime(target.runtimeSelector, method, params);
611
+ }
612
+ function printBrowserState(state, json) {
613
+ if (json) {
614
+ console.log(JSON.stringify({ browser: state }, null, 2));
615
+ return;
616
+ }
617
+ if (!state) {
618
+ console.log("No Browser is open for this Session.");
619
+ return;
620
+ }
621
+ console.log(`Browser ${state.status} · ${state.executionBackend} · generation ${state.browserGeneration}`);
622
+ printBrowserPages(state);
623
+ }
624
+ function printBrowserPages(state) {
625
+ if (!state || state.pages.length === 0) {
626
+ console.log(" (no Pages)");
627
+ return;
628
+ }
629
+ for (const page of state.pages) {
630
+ const marker = page.pageId === state.activePageId ? "*" : " ";
631
+ const title = page.title ? ` ${page.title}` : "";
632
+ console.log(`${marker} ${page.pageId} ${page.url}${title}`);
633
+ }
634
+ }
635
+ function optionValue(args, option) {
636
+ const index = args.indexOf(option);
637
+ if (index < 0)
638
+ return undefined;
639
+ const value = args[index + 1];
640
+ if (!value || value.startsWith("--"))
641
+ fail(`${option}: missing value`);
642
+ return value;
643
+ }
644
+ async function readStdinBounded(maxBytes) {
645
+ const chunks = [];
646
+ let bytes = 0;
647
+ for await (const chunk of process.stdin) {
648
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
649
+ bytes += value.byteLength;
650
+ if (bytes > maxBytes)
651
+ fail(`stdin exceeds ${maxBytes} bytes`);
652
+ chunks.push(value);
653
+ }
654
+ return Buffer.concat(chunks, bytes).toString("utf8");
655
+ }
222
656
  async function main() {
223
657
  const [command, sub, arg] = process.argv.slice(2);
224
658
  switch (command) {
@@ -230,14 +664,11 @@ async function main() {
230
664
  break;
231
665
  case "update": {
232
666
  const rest = process.argv.slice(3);
233
- const resultIdx = rest.indexOf("--result-file");
234
- const resultFile = resultIdx >= 0 ? rest[resultIdx + 1] : undefined;
235
- const version = rest.find((a) => !a.startsWith("--") && a !== resultFile);
667
+ const version = rest.find((a) => !a.startsWith("--"));
236
668
  process.exit(await runUpdate({
237
669
  check: rest.includes("--check"),
238
670
  json: rest.includes("--json"),
239
671
  version,
240
- resultFile,
241
672
  }));
242
673
  break;
243
674
  }
@@ -257,7 +688,7 @@ async function main() {
257
688
  streamLogs();
258
689
  break;
259
690
  case "plugin":
260
- await pluginCommand(sub, arg);
691
+ await pluginCommand(process.argv.slice(3));
261
692
  break;
262
693
  case "agent":
263
694
  await agentCommand(sub, arg);
@@ -268,6 +699,12 @@ async function main() {
268
699
  case "emulator":
269
700
  emulatorPassthrough(process.argv.slice(3));
270
701
  break;
702
+ case "runtime":
703
+ await runtimeCommand(process.argv.slice(3));
704
+ break;
705
+ case "browser":
706
+ await browserCommand(process.argv.slice(3));
707
+ break;
271
708
  case undefined:
272
709
  case "help":
273
710
  case "-h":