@h-rig/cli 0.0.6-alpha.0

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 (47) hide show
  1. package/README.md +30 -0
  2. package/dist/bin/build-rig-binaries.js +107 -0
  3. package/dist/bin/rig.js +9330 -0
  4. package/dist/src/commands/_authority-runs.js +110 -0
  5. package/dist/src/commands/_connection-state.js +123 -0
  6. package/dist/src/commands/_doctor-checks.js +501 -0
  7. package/dist/src/commands/_operator-view.js +322 -0
  8. package/dist/src/commands/_parsers.js +107 -0
  9. package/dist/src/commands/_paths.js +50 -0
  10. package/dist/src/commands/_pi-install.js +184 -0
  11. package/dist/src/commands/_policy.js +79 -0
  12. package/dist/src/commands/_preflight.js +460 -0
  13. package/dist/src/commands/_probes.js +13 -0
  14. package/dist/src/commands/_run-driver-helpers.js +289 -0
  15. package/dist/src/commands/_server-client.js +364 -0
  16. package/dist/src/commands/_snapshot-upload.js +313 -0
  17. package/dist/src/commands/_task-picker.js +48 -0
  18. package/dist/src/commands/agent.js +497 -0
  19. package/dist/src/commands/browser.js +890 -0
  20. package/dist/src/commands/connect.js +180 -0
  21. package/dist/src/commands/dist.js +402 -0
  22. package/dist/src/commands/doctor.js +511 -0
  23. package/dist/src/commands/github.js +276 -0
  24. package/dist/src/commands/inbox.js +160 -0
  25. package/dist/src/commands/init.js +1254 -0
  26. package/dist/src/commands/inspect.js +174 -0
  27. package/dist/src/commands/inspector.js +256 -0
  28. package/dist/src/commands/plugin.js +167 -0
  29. package/dist/src/commands/profile-and-review.js +178 -0
  30. package/dist/src/commands/queue.js +197 -0
  31. package/dist/src/commands/remote.js +507 -0
  32. package/dist/src/commands/repo-git-harness.js +221 -0
  33. package/dist/src/commands/run.js +753 -0
  34. package/dist/src/commands/server.js +368 -0
  35. package/dist/src/commands/setup.js +681 -0
  36. package/dist/src/commands/task-report-bug.js +1083 -0
  37. package/dist/src/commands/task-run-driver.js +1933 -0
  38. package/dist/src/commands/task.js +1325 -0
  39. package/dist/src/commands/test.js +39 -0
  40. package/dist/src/commands/workspace.js +123 -0
  41. package/dist/src/commands.js +9012 -0
  42. package/dist/src/index.js +9348 -0
  43. package/dist/src/launcher.js +131 -0
  44. package/dist/src/report-bug.js +260 -0
  45. package/dist/src/runner.js +272 -0
  46. package/dist/src/withMutedConsole.js +42 -0
  47. package/package.json +31 -0
@@ -0,0 +1,890 @@
1
+ // @bun
2
+ // packages/cli/src/commands/browser.ts
3
+ import { mkdirSync, rmSync } from "fs";
4
+ import { resolve } from "path";
5
+ import { spawn } from "child_process";
6
+ import { emitKeypressEvents } from "readline";
7
+ import { pathToFileURL } from "url";
8
+ import * as clack2 from "@clack/prompts";
9
+ import pc2 from "picocolors";
10
+
11
+ // packages/cli/src/runner.ts
12
+ import { EventBus } from "@rig/runtime/control-plane/runtime/events";
13
+ import { CliError } from "@rig/runtime/control-plane/errors";
14
+ import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
15
+ import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
16
+ import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
17
+ import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
18
+ import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
19
+ function takeFlag(args, flag) {
20
+ const rest = [];
21
+ let value = false;
22
+ for (const arg of args) {
23
+ if (arg === flag) {
24
+ value = true;
25
+ continue;
26
+ }
27
+ rest.push(arg);
28
+ }
29
+ return { value, rest };
30
+ }
31
+ function takeOption(args, option) {
32
+ const rest = [];
33
+ let value;
34
+ for (let index = 0;index < args.length; index += 1) {
35
+ const current = args[index];
36
+ if (current === option) {
37
+ const next = args[index + 1];
38
+ if (!next || next.startsWith("-")) {
39
+ throw new CliError(`Missing value for ${option}`);
40
+ }
41
+ value = next;
42
+ index += 1;
43
+ continue;
44
+ }
45
+ if (current !== undefined) {
46
+ rest.push(current);
47
+ }
48
+ }
49
+ return { value, rest };
50
+ }
51
+ function requireNoExtraArgs(args, usage) {
52
+ if (args.length > 0) {
53
+ throw new CliError(`Unexpected arguments: ${args.join(" ")}
54
+ Usage: ${usage}`);
55
+ }
56
+ }
57
+
58
+ // packages/cli/src/commands/browser.ts
59
+ import { runCapture as runCapture2 } from "@rig/runtime/control-plane/native/utils";
60
+
61
+ // packages/cli/src/commands/task-report-bug.ts
62
+ import * as clack from "@clack/prompts";
63
+ import pc from "picocolors";
64
+ import {
65
+ appendJsonlRecord,
66
+ readJsonFile,
67
+ readJsonlFile,
68
+ writeJsonFile
69
+ } from "@rig/runtime/control-plane/authority-files";
70
+ import { runCapture, unique } from "@rig/runtime/control-plane/native/utils";
71
+
72
+ // packages/cli/src/commands/_paths.ts
73
+ import { resolveMonorepoRoot } from "@rig/runtime/control-plane/native/utils";
74
+
75
+ // packages/cli/src/commands/task-report-bug.ts
76
+ async function promptBugText(message, defaultValue, options = {}) {
77
+ const result = await clack.text({
78
+ message,
79
+ defaultValue: defaultValue?.trim() ? defaultValue : undefined,
80
+ placeholder: options.placeholder,
81
+ validate: options.required ? (value) => validateRequiredBugPromptValue(value, message) : undefined
82
+ });
83
+ return unwrapClackPrompt(result).trim();
84
+ }
85
+ function validateRequiredBugPromptValue(value, label) {
86
+ return value?.trim() ? undefined : `${label} is required.`;
87
+ }
88
+ async function promptBugConfirm(message, initialValue) {
89
+ const result = await clack.confirm({
90
+ message,
91
+ initialValue
92
+ });
93
+ return unwrapClackPrompt(result);
94
+ }
95
+ function unwrapClackPrompt(result) {
96
+ if (clack.isCancel(result)) {
97
+ clack.cancel("Bug report cancelled.");
98
+ throw new CliError2("Bug report cancelled by user.");
99
+ }
100
+ return result;
101
+ }
102
+
103
+ // packages/cli/src/commands/browser.ts
104
+ function shouldColorizeCliOutput() {
105
+ return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb";
106
+ }
107
+ function browserHelpText(options = {}) {
108
+ const c = pc2.createColors(options.color ?? shouldColorizeCliOutput());
109
+ const title = (value) => c.bold(c.cyan(value));
110
+ const section = (value) => c.bold(c.blue(value));
111
+ const command = (value) => c.green(value);
112
+ const code = (value) => c.yellow(`\`${value}\``);
113
+ return [
114
+ title("rig browser - Rig Browser workstation commands"),
115
+ "",
116
+ section("Purpose:"),
117
+ " Rig Browser is the deterministic browser workstation for browser-heavy agent work.",
118
+ " For hp-next it pins the app preset, persistent profile, state directory, and Chrome DevTools MCP port.",
119
+ " Use it instead of ad hoc personal browser tabs for auth, routing, chunk-load, console, network, and visual debugging.",
120
+ "",
121
+ section("How agents use it inside a run:"),
122
+ ` Browser-required tasks get a Browser section in ${code("rig task info --task <id>")}.`,
123
+ ` The runtime writes browser metadata into ${code("runtime-context.json")}, exports ${code("RIG_BROWSER_*")} env vars,`,
124
+ " and injects browser helper commands onto PATH. Agents should read this runtime contract instead",
125
+ " of guessing app paths, profiles, or ports.",
126
+ "",
127
+ section("Recommended agent sequence:"),
128
+ ` ${command("rig-browser-launch")}`,
129
+ ` ${command("rig-browser-check")}`,
130
+ ` ${command("rig-browser-attach-info")}`,
131
+ ` ${command("npx chrome-devtools-mcp@latest --browser-url=$RIG_BROWSER_ATTACH_URL")}`,
132
+ "",
133
+ section("Runtime helper commands:"),
134
+ ` ${command("rig-browser-launch")}: Launches the task-resolved Rig Browser using the configured`,
135
+ " launch command, profile, state dir, preset, and port. Use at the start of browser-required",
136
+ ` task work. Pass ${code("--dev")} when you need the dev/watch browser command.`,
137
+ ` ${command("rig-browser-check")}: Validates that the helper-launched browser exposes the expected CDP`,
138
+ " endpoint. Use before attaching Chrome DevTools MCP or before blaming app code.",
139
+ ` ${command("rig-browser-attach-info")}: Prints the effective attach URL, profile, base profile, preset,`,
140
+ " and state directory chosen for this runtime. Use this when sharing instructions or attaching",
141
+ " MCP, because isolated runtimes may derive a profile/port from the base config.",
142
+ ` ${command("rig-browser-e2e")}: Runs the browser e2e command through the same workstation contract.`,
143
+ " Use after a browser fix to validate the actual app-owned browser surface.",
144
+ ` ${command("rig-browser-reset-profile")}: Clears the effective browser profile. Use when stale cookies,`,
145
+ " localStorage, auth state, or cache are suspected; do not use when the bug depends on persisted auth.",
146
+ "",
147
+ section("rig browser command family:"),
148
+ ` ${command("rig browser --help")}`,
149
+ ` ${command("rig browser explain")}`,
150
+ ` ${command("rig browser hp-next dev")}: Starts hp-next Rig Browser in dev/watch mode.`,
151
+ " Use while implementing or debugging local frontend changes.",
152
+ ` ${command("rig browser hp-next start")}: Builds/starts the hp-next browser workstation without watch mode.`,
153
+ " Use for stable manual verification or CI-like local checks.",
154
+ ` ${command("rig browser hp-next check")}: Validates the hp-next CDP endpoint on the configured port.`,
155
+ " Use before MCP attach or after relaunching the browser.",
156
+ ` ${command("rig browser hp-next e2e")}: Runs hp-next browser e2e smoke checks through Rig Browser.`,
157
+ " Use before handing off browser/auth/routing fixes.",
158
+ ` ${command("rig browser hp-next reset")}: Resets the hp-next browser profile.`,
159
+ " Use when stale browser state is the suspected cause.",
160
+ ` ${command("rig browser demo")}: Launches a real guided agent/browser demo.`,
161
+ " Use to experience the browser-required agent task workflow end to end.",
162
+ ` ${command("rig browser cdp-probe")}: Runs an isolated CDP attach/evaluate/layout probe.`,
163
+ " Use to distinguish browser-workstation failure from hp-next app failure.",
164
+ ` ${command("rig browser profile-persistence")}: Verifies that a named profile persists browser state.`,
165
+ " Use when debugging login/session persistence.",
166
+ ` ${command("rig browser profile-lock-check")}: Verifies concurrent launches reject the same profile.`,
167
+ " Use when profile ownership or parallel agent collisions are suspected.",
168
+ ` ${command("rig browser smoke-test")}: Runs the browser package smoke test.`,
169
+ " Use for a quick package-level workstation sanity check.",
170
+ "",
171
+ section("Repo script mapping:"),
172
+ ` ${command("rig browser hp-next dev")}: Runs ${code("bun run app:dev:browser:hp-next")}.`,
173
+ ` ${command("rig browser hp-next start")}: Runs ${code("bun run app:start:browser:hp-next")}.`,
174
+ ` ${command("rig browser hp-next check")}: Runs ${code("bun run app:check:browser:hp-next")}.`,
175
+ ` ${command("rig browser hp-next e2e")}: Runs ${code("bun run app:e2e:browser:hp-next")}.`,
176
+ ` ${command("rig browser hp-next reset")}: Runs ${code("bun run app:reset:browser:hp-next")}.`,
177
+ ` ${command("rig browser demo")}: Builds the browser, launches Electron, checks CDP, then attaches to the live page.`,
178
+ ` ${command("rig browser cdp-probe")}: Runs ${code("bun run --filter=@rig/browser cdp-probe")}.`,
179
+ ` ${command("rig browser profile-persistence")}: Runs ${code("bun run --filter=@rig/browser profile-persistence")}.`,
180
+ ` ${command("rig browser profile-lock-check")}: Runs ${code("bun run --filter=@rig/browser profile-lock-check")}.`,
181
+ ` ${command("rig browser smoke-test")}: Runs ${code("bun run --filter=@rig/browser smoke-test")}.`,
182
+ "",
183
+ section("hp-next defaults:"),
184
+ ` Local attach URL: ${code("http://127.0.0.1:9333")}`,
185
+ ` Local preset/profile: ${code("hp-next-local")}`,
186
+ ` Local state dir: ${code(".tmp/rig-browser/hp-next")}`,
187
+ "",
188
+ section("Chrome DevTools MCP:"),
189
+ ` ${command("npx chrome-devtools-mcp@latest --browser-url=http://127.0.0.1:9333")}`,
190
+ "",
191
+ section("Profiles:"),
192
+ " A profile is the browser state bucket: cookies, localStorage, auth session, and cache.",
193
+ " Use hp-next-local to preserve local auth, hp-next-shared for shared-dev, or a bug-specific",
194
+ " profile such as bug-otp-auth when you need a clean/reproducible auth run.",
195
+ "",
196
+ section("First debug targets:"),
197
+ " Inspect page URL/title, console errors, failed network requests, auth cookies/localStorage,",
198
+ " route state, chunk requests, and responsive layout before changing app code."
199
+ ].join(`
200
+ `);
201
+ }
202
+ function browserAgentUsageText() {
203
+ return browserHelpText();
204
+ }
205
+ function browserDemoHelpText(options = {}) {
206
+ const c = pc2.createColors(options.color ?? shouldColorizeCliOutput());
207
+ const section = (value) => c.bold(c.blue(value));
208
+ const command = (value) => c.green(value);
209
+ const code = (value) => c.yellow(`\`${value}\``);
210
+ return [
211
+ c.bold(c.cyan("rig browser demo - real agent/browser walkthrough")),
212
+ "",
213
+ section("What it does:"),
214
+ " Places you in the agent's seat inside a browser-required task run.",
215
+ " It launches a real Rig Browser window, exposes a real CDP endpoint,",
216
+ " asks you to type the same simple commands an agent runs, then prints each command output.",
217
+ " It attaches over the live Chrome DevTools Protocol endpoint and performs the same inspect/mutate workflow an agent uses.",
218
+ "",
219
+ section("Command:"),
220
+ ` ${command("rig browser demo")} ${code("[--port <n>] [--profile <name>] [--state-dir <path>] [--target-url <url>] [--keep-open] [--no-build]")}`,
221
+ "",
222
+ section("Commands you will type:"),
223
+ ` ${command("rig-browser-attach-info")}`,
224
+ ` ${command("rig-browser-launch")}`,
225
+ ` ${command("rig-browser-check")}`,
226
+ ` ${command("npx chrome-devtools-mcp@latest --browser-url=$RIG_BROWSER_ATTACH_URL")}`,
227
+ "",
228
+ section("Real steps:"),
229
+ " 1. Prepare the demo runtime contract and build the browser bundle unless --no-build is passed.",
230
+ " 2. Ask you to type rig-browser-attach-info, then print the resolved runtime metadata.",
231
+ " 3. Ask you to type rig-browser-launch, then launch real Electron with RIG_BROWSER_* env vars.",
232
+ " 4. Ask you to type rig-browser-check, then check /json/version and /json/list on the live CDP endpoint.",
233
+ " 5. Ask you to type the DevTools MCP attach command, then run the live CDP inspect/update action.",
234
+ "",
235
+ section("Default endpoint:"),
236
+ " http://127.0.0.1:9336"
237
+ ].join(`
238
+ `);
239
+ }
240
+ function buildBrowserDemoRuntime(projectRoot, options = {}) {
241
+ const profile = options.profile || `agent-demo-${process.pid}`;
242
+ const port = options.port ?? 9336;
243
+ const stateDir = options.stateDir || `.tmp/rig-browser/${profile}`;
244
+ const targetUrl = options.targetUrl || buildBrowserDemoTargetUrl();
245
+ return {
246
+ attachUrl: `http://127.0.0.1:${port}`,
247
+ browserDir: resolve(projectRoot, "apps/native-app/apps/browser"),
248
+ env: {
249
+ RIG_BROWSER_DEV: "1",
250
+ RIG_STATE_DIR: stateDir,
251
+ T3CODE_STATE_DIR: stateDir,
252
+ RIG_BROWSER_PRESET: "hp-next-local",
253
+ RIG_BROWSER_PROFILE: profile,
254
+ RIG_BROWSER_REMOTE_DEBUGGING_PORT: String(port),
255
+ RIG_BROWSER_TARGET_URL: targetUrl
256
+ },
257
+ helperCommands: [
258
+ "rig-browser-launch",
259
+ "rig-browser-check",
260
+ "rig-browser-attach-info",
261
+ "rig-browser-e2e",
262
+ "rig-browser-reset-profile"
263
+ ],
264
+ port,
265
+ profile,
266
+ stateDir,
267
+ targetUrl
268
+ };
269
+ }
270
+ function buildBrowserDemoTargetUrl() {
271
+ const html = [
272
+ "<!doctype html>",
273
+ "<html>",
274
+ "<head>",
275
+ '<meta charset="utf-8">',
276
+ "<title>Rig Browser Agent Demo</title>",
277
+ "<style>",
278
+ "body{font-family:ui-sans-serif,system-ui;margin:48px;background:#f7efe3;color:#1f2933}",
279
+ ".card{max-width:760px;border:2px solid #1f2933;border-radius:20px;padding:28px;background:white;box-shadow:8px 8px 0 #1f2933}",
280
+ "#status{font-weight:700;color:#8a4b0f}",
281
+ "</style>",
282
+ "</head>",
283
+ "<body>",
284
+ '<main class="card">',
285
+ "<p>Rig Browser task run</p>",
286
+ "<h1>You are the agent now</h1>",
287
+ '<p id="status">Waiting for the agent to attach over CDP.</p>',
288
+ '<p id="details">The CLI will inspect this real page and then update it.</p>',
289
+ "</main>",
290
+ "<script>console.log('rig-browser-demo-page-ready')</script>",
291
+ "</body>",
292
+ "</html>"
293
+ ].join("");
294
+ return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
295
+ }
296
+ function buildBrowserDemoAgentCommands(runtime) {
297
+ const attachWithEnv = "npx chrome-devtools-mcp@latest --browser-url=$RIG_BROWSER_ATTACH_URL";
298
+ const attachWithUrl = `npx chrome-devtools-mcp@latest --browser-url=${runtime.attachUrl}`;
299
+ return {
300
+ attachInfo: {
301
+ primary: "rig-browser-attach-info",
302
+ accepted: ["rig-browser-attach-info"]
303
+ },
304
+ launch: {
305
+ primary: "rig-browser-launch",
306
+ accepted: ["rig-browser-launch"]
307
+ },
308
+ check: {
309
+ primary: "rig-browser-check",
310
+ accepted: ["rig-browser-check"]
311
+ },
312
+ attach: {
313
+ primary: attachWithEnv,
314
+ accepted: [attachWithEnv, attachWithUrl]
315
+ }
316
+ };
317
+ }
318
+ function normalizeBrowserDemoCommand(value) {
319
+ return value.trim().replace(/\s+/g, " ").replace(/--browser-url\s+/g, "--browser-url=").replace(/--browser-url\s*=\s*/g, "--browser-url=");
320
+ }
321
+ function browserDemoCommandMatches(value, command) {
322
+ const normalized = normalizeBrowserDemoCommand(value);
323
+ return command.accepted.map(normalizeBrowserDemoCommand).includes(normalized);
324
+ }
325
+ function browserDemoCommandCompletions(line, command) {
326
+ if (line.trim().length === 0) {
327
+ return [[command.primary], line];
328
+ }
329
+ const normalizedLine = normalizeBrowserDemoCommand(line);
330
+ const matches = command.accepted.filter((accepted) => normalizeBrowserDemoCommand(accepted).startsWith(normalizedLine));
331
+ return [matches.length > 0 ? matches : [command.primary], line];
332
+ }
333
+ function formatBrowserDemoCommandOutput(command, output) {
334
+ const normalizedOutput = output.trim() || "(no output)";
335
+ return `$ ${command}
336
+ ${normalizedOutput}`;
337
+ }
338
+ function printBrowserDemoCommandOutput(command, output) {
339
+ const c = pc2.createColors(shouldColorizeCliOutput());
340
+ console.log("");
341
+ console.log(c.bold("Agent terminal output"));
342
+ console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
343
+ console.log(formatBrowserDemoCommandOutput(command, output));
344
+ console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
345
+ console.log("");
346
+ }
347
+ async function executeBrowser(context, args) {
348
+ const [command = "help", ...rest] = args;
349
+ if (command === "help" || command === "--help" || command === "-h") {
350
+ console.log(browserHelpText());
351
+ return { ok: true, group: "browser", command: "help" };
352
+ }
353
+ if (command === "explain") {
354
+ requireNoExtraArgs(rest, "bun run rig browser explain");
355
+ console.log(browserAgentUsageText());
356
+ return { ok: true, group: "browser", command: "explain" };
357
+ }
358
+ if (command === "demo") {
359
+ return executeBrowserDemo(context, rest);
360
+ }
361
+ if (command === "app" || command === "hp-next") {
362
+ const appSlug = command === "hp-next" ? "hp-next" : process.env.RIG_BROWSER_APP?.trim() || "";
363
+ if (!appSlug) {
364
+ throw new CliError2(`rig browser app: set RIG_BROWSER_APP=<app-slug> to select which project app scripts to invoke.
365
+ ` + `Scripts are invoked as 'bun run app:<mode>:browser:<app-slug>'.
366
+
367
+ ${browserHelpText()}`);
368
+ }
369
+ const [subcommand = "help", ...appRest] = rest;
370
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
371
+ console.log(browserHelpText());
372
+ return { ok: true, group: "browser", command: `${command}-help` };
373
+ }
374
+ const modes = ["dev", "start", "check", "e2e", "reset"];
375
+ if (!modes.includes(subcommand)) {
376
+ throw new CliError2(`Unknown browser ${command} command: ${subcommand}. Valid modes: ${modes.join(", ")}.
377
+
378
+ ${browserHelpText()}`);
379
+ }
380
+ requireNoExtraArgs(appRest, `bun run rig browser ${command} ${subcommand}`);
381
+ await context.runCommand(["bun", "run", `app:${subcommand}:browser:${appSlug}`]);
382
+ return { ok: true, group: "browser", command: `${command}-${subcommand}` };
383
+ }
384
+ const packageScripts = {
385
+ "cdp-probe": "cdp-probe",
386
+ "profile-persistence": "profile-persistence",
387
+ "profile-lock-check": "profile-lock-check",
388
+ "smoke-test": "smoke-test"
389
+ };
390
+ const packageScript = packageScripts[command];
391
+ if (packageScript) {
392
+ requireNoExtraArgs(rest, `bun run rig browser ${command}`);
393
+ await context.runCommand(["bun", "run", "--filter=@rig/browser", packageScript]);
394
+ return { ok: true, group: "browser", command };
395
+ }
396
+ throw new CliError2(`Unknown browser command: ${command}
397
+
398
+ ${browserHelpText()}`);
399
+ }
400
+ async function executeBrowserDemo(context, args) {
401
+ const [maybeHelp] = args;
402
+ if (maybeHelp === "help" || maybeHelp === "--help" || maybeHelp === "-h") {
403
+ console.log(browserDemoHelpText());
404
+ return { ok: true, group: "browser", command: "demo-help" };
405
+ }
406
+ let pending = args;
407
+ const portResult = takeOption(pending, "--port");
408
+ pending = portResult.rest;
409
+ const profileResult = takeOption(pending, "--profile");
410
+ pending = profileResult.rest;
411
+ const stateDirResult = takeOption(pending, "--state-dir");
412
+ pending = stateDirResult.rest;
413
+ const targetUrlResult = takeOption(pending, "--target-url");
414
+ pending = targetUrlResult.rest;
415
+ const keepOpenFlag = takeFlag(pending, "--keep-open");
416
+ pending = keepOpenFlag.rest;
417
+ const noBuildFlag = takeFlag(pending, "--no-build");
418
+ pending = noBuildFlag.rest;
419
+ requireNoExtraArgs(pending, "bun run rig browser demo [--port <n>] [--profile <name>] [--state-dir <path>] [--target-url <url>] [--keep-open] [--no-build]");
420
+ if (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY) {
421
+ throw new CliError2("rig browser demo requires an interactive TTY in text mode.");
422
+ }
423
+ const port = portResult.value ? Number.parseInt(portResult.value, 10) : undefined;
424
+ if (port !== undefined && (!Number.isFinite(port) || port <= 0)) {
425
+ throw new CliError2(`Invalid --port value: ${portResult.value}`);
426
+ }
427
+ const runtime = buildBrowserDemoRuntime(context.projectRoot, {
428
+ ...port !== undefined ? { port } : {},
429
+ ...profileResult.value ? { profile: profileResult.value } : {},
430
+ ...stateDirResult.value ? { stateDir: stateDirResult.value } : {},
431
+ ...targetUrlResult.value ? { targetUrl: targetUrlResult.value } : {}
432
+ });
433
+ clack2.intro("rig browser demo");
434
+ clack2.note([
435
+ "You are now acting as the agent inside a browser-required task run.",
436
+ "Type each simple command when prompted. The CLI will run the real backing action",
437
+ "and print the same kind of output an agent would inspect before the next step."
438
+ ].join(`
439
+ `), "Agent task-run walkthrough");
440
+ clack2.note(formatBrowserDemoRuntime(runtime), "Runtime contract the agent receives");
441
+ const agentCommands = buildBrowserDemoAgentCommands(runtime);
442
+ await promptBrowserDemoAgentCommand("Agent command 1/4: print browser attach info", agentCommands.attachInfo);
443
+ printBrowserDemoCommandOutput(agentCommands.attachInfo.primary, formatBrowserDemoAttachInfoOutput(runtime));
444
+ await promptBrowserDemoAgentCommand("Agent command 2/4: launch Rig Browser", agentCommands.launch);
445
+ if (!noBuildFlag.value) {
446
+ const spinner2 = clack2.spinner();
447
+ spinner2.start("Preparing browser bundle for the launch command");
448
+ try {
449
+ runBrowserDemoBuild(context.projectRoot);
450
+ spinner2.stop("Browser bundle built");
451
+ } catch (error) {
452
+ spinner2.error("Browser bundle build failed");
453
+ throw error;
454
+ }
455
+ }
456
+ const child = await launchBrowserDemo(runtime);
457
+ let keepOpen = keepOpenFlag.value;
458
+ try {
459
+ printBrowserDemoCommandOutput(agentCommands.launch.primary, formatBrowserDemoLaunchOutput(runtime, child.pid));
460
+ await promptBrowserDemoAgentCommand("Agent command 3/4: check live browser endpoint", agentCommands.check);
461
+ const readySpinner = clack2.spinner();
462
+ readySpinner.start(`Waiting for live CDP endpoint at ${runtime.attachUrl}`);
463
+ const { version, pageTarget, targets } = await waitForBrowserDemoReady(runtime, child);
464
+ readySpinner.stop("Live CDP endpoint is ready");
465
+ printBrowserDemoCommandOutput(agentCommands.check.primary, formatBrowserDemoCheckOutput(runtime, version, pageTarget, targets));
466
+ await promptBugText("Look at the launched browser page, then type ready", undefined, { required: true, placeholder: "ready" });
467
+ await promptBrowserDemoAgentCommand("Agent command 4/4: attach DevTools MCP", agentCommands.attach);
468
+ clack2.note([
469
+ "The attach step evaluates JavaScript inside the live browser page.",
470
+ "It records DOM state before the change, changes #status, #details, and the page background,",
471
+ "then records DOM state and layout again so the transcript proves what changed."
472
+ ].join(`
473
+ `), "What mutationEvaluation means");
474
+ const cdpSpinner = clack2.spinner();
475
+ cdpSpinner.start("Agent attaching over WebSocket CDP");
476
+ const cdpResult = await runBrowserDemoCdpAction(pageTarget.webSocketDebuggerUrl);
477
+ cdpSpinner.stop("Agent CDP action completed");
478
+ printBrowserDemoCommandOutput(agentCommands.attach.primary, formatBrowserDemoCdpResult(cdpResult));
479
+ await promptBugConfirm("Did you see the page update after the agent action?", true);
480
+ if (!keepOpen) {
481
+ keepOpen = await promptBugConfirm("Keep the demo browser open after this command exits?", false);
482
+ }
483
+ clack2.outro(keepOpen ? "Demo complete. Browser left open by request." : "Demo complete. Closing browser.");
484
+ return {
485
+ ok: true,
486
+ group: "browser",
487
+ command: "demo",
488
+ details: {
489
+ attachUrl: runtime.attachUrl,
490
+ browser: version.Browser,
491
+ pageTitle: pageTarget.title,
492
+ profile: runtime.profile,
493
+ stateDir: runtime.stateDir,
494
+ cdpResult,
495
+ keptOpen: keepOpen
496
+ }
497
+ };
498
+ } finally {
499
+ if (!keepOpen) {
500
+ await stopBrowserDemo(child);
501
+ }
502
+ }
503
+ }
504
+ function formatBrowserDemoRuntime(runtime) {
505
+ return [
506
+ `RIG_BROWSER_ATTACH_URL=${runtime.attachUrl}`,
507
+ `RIG_BROWSER_PROFILE=${runtime.profile}`,
508
+ `RIG_BROWSER_STATE_DIR=${runtime.stateDir}`,
509
+ `RIG_BROWSER_REMOTE_DEBUGGING_PORT=${runtime.port}`,
510
+ `helpers=${runtime.helperCommands.join(", ")}`
511
+ ].join(`
512
+ `);
513
+ }
514
+ function formatBrowserDemoAttachInfoOutput(runtime) {
515
+ return [
516
+ "Rig Browser",
517
+ " Preset: hp-next-local",
518
+ " Mode: persistent",
519
+ ` State dir: ${runtime.stateDir}`,
520
+ " Default profile: hp-next-local",
521
+ ` Effective profile: ${runtime.profile}`,
522
+ ` Effective attach URL: ${runtime.attachUrl}`,
523
+ " Launch helper: rig-browser-launch",
524
+ " Check helper: rig-browser-check",
525
+ " E2E helper: rig-browser-e2e",
526
+ " Reset helper: rig-browser-reset-profile"
527
+ ].join(`
528
+ `);
529
+ }
530
+ function formatBrowserDemoLaunchOutput(runtime, pid) {
531
+ return [
532
+ "Rig Browser launch requested.",
533
+ ` pid: ${pid ?? "(unknown)"}`,
534
+ ` profile: ${runtime.profile}`,
535
+ ` stateDir: ${runtime.stateDir}`,
536
+ ` attachUrl: ${runtime.attachUrl}`
537
+ ].join(`
538
+ `);
539
+ }
540
+ function formatBrowserDemoCheckOutput(runtime, version, pageTarget, targets) {
541
+ return JSON.stringify({
542
+ attachUrl: runtime.attachUrl,
543
+ pageTarget,
544
+ targets,
545
+ version
546
+ }, null, 2);
547
+ }
548
+ function formatBrowserDemoCdpResult(result) {
549
+ return JSON.stringify({
550
+ title: result.title ?? null,
551
+ mutationExplanation: {
552
+ command: "Runtime.evaluate",
553
+ summary: "The agent executes JavaScript inside the live page through CDP, not against a mock.",
554
+ changes: [
555
+ "reads the DOM before mutation",
556
+ "changes #status, #details, and the page background",
557
+ "returns the browser-side mutation result",
558
+ "reads the DOM and layout again after mutation"
559
+ ],
560
+ payloadGuide: {
561
+ beforeDomState: "DOM state captured before Runtime.evaluate mutates the page.",
562
+ mutationEvaluation: "The exact Runtime.evaluate response returned by the browser-side mutation function.",
563
+ afterDomState: "DOM state captured after mutation to prove the page changed.",
564
+ consoleEvents: "Runtime.consoleAPICalled events observed during the attach session.",
565
+ layoutMetrics: "Full Page.getLayoutMetrics response from the live page."
566
+ }
567
+ },
568
+ beforeDomState: result.beforeDomState,
569
+ mutationEvaluation: result.mutationEvaluation,
570
+ afterDomState: result.afterDomState,
571
+ consoleEvents: result.consoleEvents,
572
+ layoutMetrics: result.layoutMetrics
573
+ }, null, 2);
574
+ }
575
+ function runBrowserDemoBuild(projectRoot) {
576
+ const result = runCapture2(["bun", "run", "--filter=@rig/browser", "build"], projectRoot);
577
+ if (result.exitCode !== 0) {
578
+ const details = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join(`
579
+ `);
580
+ throw new CliError2(`Browser demo build failed.${details ? `
581
+ ${details}` : ""}`, result.exitCode);
582
+ }
583
+ }
584
+ async function promptBrowserDemoAgentCommand(message, command) {
585
+ for (;; ) {
586
+ const value = await readBrowserDemoAgentCommand(message, command);
587
+ if (browserDemoCommandMatches(value, command)) {
588
+ return normalizeBrowserDemoCommand(value);
589
+ }
590
+ clack2.log.warn(`Expected one of:
591
+ ${command.accepted.map((accepted) => ` ${accepted}`).join(`
592
+ `)}`);
593
+ }
594
+ }
595
+ async function readBrowserDemoAgentCommand(message, command) {
596
+ const c = pc2.createColors(shouldColorizeCliOutput());
597
+ const alternatives = command.accepted.filter((accepted) => normalizeBrowserDemoCommand(accepted) !== normalizeBrowserDemoCommand(command.primary));
598
+ console.log("");
599
+ console.log(`${c.cyan("\u25C6")} ${c.bold(message)}`);
600
+ console.log(` ${c.dim("Type the command, or press Tab to fill the placeholder:")}`);
601
+ console.log(` ${c.dim(command.primary)}`);
602
+ if (alternatives.length > 0) {
603
+ console.log(` ${c.dim("Also accepted:")}`);
604
+ for (const alternative of alternatives) {
605
+ console.log(` ${c.dim(alternative)}`);
606
+ }
607
+ }
608
+ return readBrowserDemoCommandLine(command);
609
+ }
610
+ function readBrowserDemoCommandLine(command) {
611
+ const stdin = process.stdin;
612
+ const stdout = process.stdout;
613
+ const wasRaw = Boolean(stdin.isRaw);
614
+ let line = "";
615
+ const render = () => {
616
+ stdout.write(`\r\x1B[2K> ${line}`);
617
+ };
618
+ return new Promise((resolveInput, rejectInput) => {
619
+ const cleanup = () => {
620
+ stdin.off("keypress", onKeypress);
621
+ if (stdin.isTTY) {
622
+ stdin.setRawMode(wasRaw);
623
+ }
624
+ };
625
+ const completeLine = () => {
626
+ const [matches] = browserDemoCommandCompletions(line, command);
627
+ line = matches[0] ?? command.primary;
628
+ render();
629
+ };
630
+ const onKeypress = (characters, key) => {
631
+ if (key.ctrl && key.name === "c") {
632
+ cleanup();
633
+ stdout.write(`
634
+ `);
635
+ rejectInput(new CliError2("Browser demo cancelled."));
636
+ return;
637
+ }
638
+ if (key.name === "return" || key.name === "enter") {
639
+ cleanup();
640
+ stdout.write(`
641
+ `);
642
+ resolveInput(line);
643
+ return;
644
+ }
645
+ if (key.name === "tab") {
646
+ completeLine();
647
+ return;
648
+ }
649
+ if (key.name === "backspace") {
650
+ line = line.slice(0, -1);
651
+ render();
652
+ return;
653
+ }
654
+ if (key.ctrl && key.name === "u") {
655
+ line = "";
656
+ render();
657
+ return;
658
+ }
659
+ if (!key.ctrl && !key.meta && characters) {
660
+ line += characters;
661
+ render();
662
+ }
663
+ };
664
+ emitKeypressEvents(stdin);
665
+ stdin.resume();
666
+ stdin.on("keypress", onKeypress);
667
+ if (stdin.isTTY) {
668
+ stdin.setRawMode(true);
669
+ }
670
+ render();
671
+ });
672
+ }
673
+ async function launchBrowserDemo(runtime) {
674
+ rmSync(resolve(runtime.browserDir, runtime.stateDir), { recursive: true, force: true });
675
+ mkdirSync(resolve(runtime.browserDir, runtime.stateDir), { recursive: true });
676
+ const launcherPath = resolve(runtime.browserDir, "scripts/electron-launcher.mjs");
677
+ const launcher = await import(pathToFileURL(launcherPath).href);
678
+ const electronPath = await launcher.resolveElectronPath();
679
+ const env = { ...process.env, ...runtime.env };
680
+ delete env.ELECTRON_RUN_AS_NODE;
681
+ const child = spawn(electronPath, ["dist-electron/main.js"], {
682
+ cwd: runtime.browserDir,
683
+ env,
684
+ stdio: ["ignore", "pipe", "pipe"]
685
+ });
686
+ child.stdout?.on("data", () => {});
687
+ child.stderr?.on("data", () => {});
688
+ return child;
689
+ }
690
+ async function waitForBrowserDemoReady(runtime, child) {
691
+ const version = await waitForBrowserDemo(() => fetchBrowserDemoJson(runtime, "/json/version"), child, "CDP /json/version");
692
+ const { targets, pageTarget } = await waitForBrowserDemo(async () => {
693
+ const nextTargets = await fetchBrowserDemoJson(runtime, "/json/list");
694
+ const nextPageTarget = findBrowserDemoPageTarget(runtime, nextTargets);
695
+ if (!nextPageTarget) {
696
+ throw new Error("demo page target is not ready yet");
697
+ }
698
+ return { targets: nextTargets, pageTarget: nextPageTarget };
699
+ }, child, "CDP demo page target");
700
+ if (!version.Browser) {
701
+ throw new CliError2("Browser demo reached CDP but /json/version did not include Browser metadata.");
702
+ }
703
+ return { version, targets, pageTarget };
704
+ }
705
+ function findBrowserDemoPageTarget(runtime, targets) {
706
+ const attachableTargets = targets.filter((target) => target.type === "page" && typeof target.webSocketDebuggerUrl === "string");
707
+ return attachableTargets.find((target) => target.url === runtime.targetUrl && target.title === "Rig Browser Agent Demo") ?? attachableTargets.find((target) => target.title === "Rig Browser Agent Demo") ?? null;
708
+ }
709
+ async function waitForBrowserDemo(check, child, label, timeoutMs = 18000) {
710
+ const startedAt = Date.now();
711
+ let lastError = null;
712
+ for (;; ) {
713
+ if (child.exitCode !== null || child.signalCode !== null) {
714
+ throw new CliError2(`Rig Browser exited before ${label} became ready.`);
715
+ }
716
+ try {
717
+ return await check();
718
+ } catch (error) {
719
+ lastError = error;
720
+ if (Date.now() - startedAt > timeoutMs) {
721
+ throw new CliError2(`${label} did not become ready within ${timeoutMs}ms: ${String(lastError)}`);
722
+ }
723
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 150));
724
+ }
725
+ }
726
+ }
727
+ async function fetchBrowserDemoJson(runtime, path) {
728
+ const response = await fetch(`${runtime.attachUrl}${path}`);
729
+ if (!response.ok) {
730
+ throw new Error(`${path} returned ${response.status}`);
731
+ }
732
+ return response.json();
733
+ }
734
+ async function runBrowserDemoCdpAction(wsUrl) {
735
+ return withBrowserDemoCdpSession(wsUrl, async ({ send, consoleEvents }) => {
736
+ await send("Runtime.enable");
737
+ await send("Page.enable");
738
+ const beforeDomState = await send("Runtime.evaluate", {
739
+ expression: [
740
+ "(() => ({",
741
+ "title: document.title,",
742
+ "status: document.querySelector('#status')?.textContent ?? null,",
743
+ "details: document.querySelector('#details')?.textContent ?? null,",
744
+ "bodyBackground: getComputedStyle(document.body).backgroundColor,",
745
+ "url: location.href",
746
+ "}))()"
747
+ ].join(""),
748
+ returnByValue: true
749
+ });
750
+ if (beforeDomState.exceptionDetails) {
751
+ throw new Error(`Runtime.evaluate before state failed: ${JSON.stringify(beforeDomState)}`);
752
+ }
753
+ const mutationEvaluation = await send("Runtime.evaluate", {
754
+ expression: [
755
+ "(() => {",
756
+ "console.log('rig-browser-demo-agent-action');",
757
+ "const status = document.querySelector('#status');",
758
+ "const details = document.querySelector('#details');",
759
+ "if (status) status.textContent='Agent CDP command executed.';",
760
+ "if (details) details.textContent='The CLI attached over the live browser debugging endpoint and changed this page.';",
761
+ "document.body.style.background='#e4f7ea';",
762
+ "return {",
763
+ "title: document.title,",
764
+ "status: status?.textContent ?? null,",
765
+ "details: details?.textContent ?? null,",
766
+ "bodyBackground: getComputedStyle(document.body).backgroundColor,",
767
+ "url: location.href",
768
+ "};",
769
+ "})()"
770
+ ].join(""),
771
+ returnByValue: true
772
+ });
773
+ if (mutationEvaluation.exceptionDetails) {
774
+ throw new Error(`Runtime.evaluate mutation failed: ${JSON.stringify(mutationEvaluation)}`);
775
+ }
776
+ const afterDomState = await send("Runtime.evaluate", {
777
+ expression: [
778
+ "(() => ({",
779
+ "title: document.title,",
780
+ "status: document.querySelector('#status')?.textContent ?? null,",
781
+ "details: document.querySelector('#details')?.textContent ?? null,",
782
+ "bodyBackground: getComputedStyle(document.body).backgroundColor,",
783
+ "url: location.href",
784
+ "}))()"
785
+ ].join(""),
786
+ returnByValue: true
787
+ });
788
+ if (afterDomState.exceptionDetails) {
789
+ throw new Error(`Runtime.evaluate after state failed: ${JSON.stringify(afterDomState)}`);
790
+ }
791
+ const layoutMetrics = await send("Page.getLayoutMetrics");
792
+ return {
793
+ afterDomState,
794
+ beforeDomState,
795
+ consoleEvents,
796
+ layoutMetrics,
797
+ mutationEvaluation,
798
+ title: afterDomState.result?.value?.title ?? mutationEvaluation.result?.value?.title
799
+ };
800
+ });
801
+ }
802
+ async function withBrowserDemoCdpSession(wsUrl, run) {
803
+ const socket = new WebSocket(wsUrl);
804
+ const pending = new Map;
805
+ const consoleEvents = [];
806
+ let nextId = 1;
807
+ const send = (method, params = {}) => new Promise((resolveSend, rejectSend) => {
808
+ const id = nextId;
809
+ nextId += 1;
810
+ pending.set(id, {
811
+ method,
812
+ reject: rejectSend,
813
+ resolve: (value) => resolveSend(value),
814
+ timeout: setTimeout(() => {
815
+ pending.delete(id);
816
+ rejectSend(new Error(`${method} timed out waiting for CDP response`));
817
+ }, 5000)
818
+ });
819
+ socket.send(JSON.stringify({ id, method, params }));
820
+ });
821
+ socket.addEventListener("message", (event) => {
822
+ const message = JSON.parse(String(event.data));
823
+ if (message.id !== undefined) {
824
+ const request = pending.get(message.id);
825
+ if (!request)
826
+ return;
827
+ pending.delete(message.id);
828
+ clearTimeout(request.timeout);
829
+ if (message.error) {
830
+ request.reject(new Error(`${request.method} failed: ${JSON.stringify(message.error)}`));
831
+ return;
832
+ }
833
+ request.resolve(message.result);
834
+ return;
835
+ }
836
+ if (message.method === "Runtime.consoleAPICalled") {
837
+ consoleEvents.push(message.params);
838
+ }
839
+ });
840
+ socket.addEventListener("close", () => {
841
+ for (const request of pending.values()) {
842
+ clearTimeout(request.timeout);
843
+ request.reject(new Error(`CDP WebSocket closed before ${request.method} completed`));
844
+ }
845
+ pending.clear();
846
+ }, { once: true });
847
+ await new Promise((resolveOpen, rejectOpen) => {
848
+ const timeout = setTimeout(() => rejectOpen(new Error("CDP WebSocket failed to open within 5000ms")), 5000);
849
+ socket.addEventListener("open", () => resolveOpen(), { once: true });
850
+ socket.addEventListener("error", () => rejectOpen(new Error("CDP WebSocket failed to open")), { once: true });
851
+ socket.addEventListener("open", () => clearTimeout(timeout), { once: true });
852
+ socket.addEventListener("error", () => clearTimeout(timeout), { once: true });
853
+ });
854
+ try {
855
+ return await run({ consoleEvents, send });
856
+ } finally {
857
+ socket.close();
858
+ await new Promise((resolveClose) => {
859
+ socket.addEventListener("close", resolveClose, { once: true });
860
+ setTimeout(resolveClose, 500);
861
+ });
862
+ }
863
+ }
864
+ async function stopBrowserDemo(child) {
865
+ if (child.exitCode !== null || child.signalCode !== null) {
866
+ return;
867
+ }
868
+ child.kill("SIGTERM");
869
+ await Promise.race([
870
+ new Promise((resolveExit) => child.once("exit", resolveExit)),
871
+ new Promise((resolveDelay) => setTimeout(resolveDelay, 2000))
872
+ ]);
873
+ if (child.exitCode === null && child.signalCode === null) {
874
+ child.kill("SIGKILL");
875
+ }
876
+ }
877
+ export {
878
+ normalizeBrowserDemoCommand,
879
+ formatBrowserDemoCommandOutput,
880
+ formatBrowserDemoCheckOutput,
881
+ formatBrowserDemoCdpResult,
882
+ findBrowserDemoPageTarget,
883
+ executeBrowserDemo,
884
+ executeBrowser,
885
+ buildBrowserDemoRuntime,
886
+ buildBrowserDemoAgentCommands,
887
+ browserHelpText,
888
+ browserDemoCommandMatches,
889
+ browserDemoCommandCompletions
890
+ };