@h-rig/cli 0.0.6-alpha.7 → 0.0.6-alpha.70

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 (53) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/rig.js +4507 -1506
  3. package/dist/src/commands/_async-ui.js +152 -0
  4. package/dist/src/commands/_authority-runs.js +2 -3
  5. package/dist/src/commands/_cli-format.js +369 -0
  6. package/dist/src/commands/_connection-state.js +30 -11
  7. package/dist/src/commands/_doctor-checks.js +177 -43
  8. package/dist/src/commands/_help-catalog.js +485 -0
  9. package/dist/src/commands/_json-output.js +56 -0
  10. package/dist/src/commands/_operator-surface.js +220 -0
  11. package/dist/src/commands/_operator-view.js +595 -72
  12. package/dist/src/commands/_parsers.js +18 -11
  13. package/dist/src/commands/_pi-frontend.js +411 -0
  14. package/dist/src/commands/_pi-install.js +4 -3
  15. package/dist/src/commands/_policy.js +12 -5
  16. package/dist/src/commands/_preflight.js +187 -127
  17. package/dist/src/commands/_run-driver-helpers.js +75 -22
  18. package/dist/src/commands/_run-replay.js +142 -0
  19. package/dist/src/commands/_server-client.js +343 -60
  20. package/dist/src/commands/_snapshot-upload.js +160 -38
  21. package/dist/src/commands/_spinner.js +65 -0
  22. package/dist/src/commands/_task-picker.js +44 -16
  23. package/dist/src/commands/agent.js +39 -20
  24. package/dist/src/commands/browser.js +28 -21
  25. package/dist/src/commands/connect.js +146 -33
  26. package/dist/src/commands/dist.js +19 -12
  27. package/dist/src/commands/doctor.js +304 -44
  28. package/dist/src/commands/github.js +301 -52
  29. package/dist/src/commands/inbox.js +679 -72
  30. package/dist/src/commands/init.js +622 -118
  31. package/dist/src/commands/inspect.js +515 -32
  32. package/dist/src/commands/inspector.js +20 -13
  33. package/dist/src/commands/pi.js +177 -0
  34. package/dist/src/commands/plugin.js +95 -27
  35. package/dist/src/commands/profile-and-review.js +26 -19
  36. package/dist/src/commands/queue.js +32 -12
  37. package/dist/src/commands/remote.js +43 -36
  38. package/dist/src/commands/repo-git-harness.js +22 -15
  39. package/dist/src/commands/run.js +1162 -158
  40. package/dist/src/commands/server.js +373 -56
  41. package/dist/src/commands/setup.js +316 -62
  42. package/dist/src/commands/stats.js +1030 -0
  43. package/dist/src/commands/task-report-bug.js +29 -22
  44. package/dist/src/commands/task-run-driver.js +862 -129
  45. package/dist/src/commands/task.js +1423 -311
  46. package/dist/src/commands/test.js +15 -8
  47. package/dist/src/commands/workspace.js +18 -11
  48. package/dist/src/commands.js +4446 -1499
  49. package/dist/src/index.js +4502 -1504
  50. package/dist/src/launcher.js +77 -13
  51. package/dist/src/report-bug.js +3 -3
  52. package/dist/src/runner.js +16 -22
  53. package/package.json +10 -5
@@ -10,12 +10,19 @@ import pc2 from "picocolors";
10
10
 
11
11
  // packages/cli/src/runner.ts
12
12
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
13
- import { CliError } from "@rig/runtime/control-plane/errors";
13
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
14
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
15
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
18
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
16
+
17
+ class CliError extends RuntimeCliError {
18
+ hint;
19
+ constructor(message, exitCode = 1, options = {}) {
20
+ super(message, exitCode);
21
+ if (options.hint?.trim()) {
22
+ this.hint = options.hint.trim();
23
+ }
24
+ }
25
+ }
19
26
  function takeFlag(args, flag) {
20
27
  const rest = [];
21
28
  let value = false;
@@ -36,7 +43,7 @@ function takeOption(args, option) {
36
43
  if (current === option) {
37
44
  const next = args[index + 1];
38
45
  if (!next || next.startsWith("-")) {
39
- throw new CliError(`Missing value for ${option}`);
46
+ throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
40
47
  }
41
48
  value = next;
42
49
  index += 1;
@@ -95,7 +102,7 @@ async function promptBugConfirm(message, initialValue) {
95
102
  function unwrapClackPrompt(result) {
96
103
  if (clack.isCancel(result)) {
97
104
  clack.cancel("Bug report cancelled.");
98
- throw new CliError2("Bug report cancelled by user.");
105
+ throw new CliError("Bug report cancelled by user.");
99
106
  }
100
107
  return result;
101
108
  }
@@ -351,7 +358,7 @@ async function executeBrowser(context, args) {
351
358
  return { ok: true, group: "browser", command: "help" };
352
359
  }
353
360
  if (command === "explain") {
354
- requireNoExtraArgs(rest, "bun run rig browser explain");
361
+ requireNoExtraArgs(rest, "rig browser explain");
355
362
  console.log(browserAgentUsageText());
356
363
  return { ok: true, group: "browser", command: "explain" };
357
364
  }
@@ -361,7 +368,7 @@ async function executeBrowser(context, args) {
361
368
  if (command === "app" || command === "hp-next") {
362
369
  const appSlug = command === "hp-next" ? "hp-next" : process.env.RIG_BROWSER_APP?.trim() || "";
363
370
  if (!appSlug) {
364
- throw new CliError2(`rig browser app: set RIG_BROWSER_APP=<app-slug> to select which project app scripts to invoke.
371
+ throw new CliError(`rig browser app: set RIG_BROWSER_APP=<app-slug> to select which project app scripts to invoke.
365
372
  ` + `Scripts are invoked as 'bun run app:<mode>:browser:<app-slug>'.
366
373
 
367
374
  ${browserHelpText()}`);
@@ -373,11 +380,11 @@ ${browserHelpText()}`);
373
380
  }
374
381
  const modes = ["dev", "start", "check", "e2e", "reset"];
375
382
  if (!modes.includes(subcommand)) {
376
- throw new CliError2(`Unknown browser ${command} command: ${subcommand}. Valid modes: ${modes.join(", ")}.
383
+ throw new CliError(`Unknown browser ${command} command: ${subcommand}. Valid modes: ${modes.join(", ")}.
377
384
 
378
385
  ${browserHelpText()}`);
379
386
  }
380
- requireNoExtraArgs(appRest, `bun run rig browser ${command} ${subcommand}`);
387
+ requireNoExtraArgs(appRest, `rig browser ${command} ${subcommand}`);
381
388
  await context.runCommand(["bun", "run", `app:${subcommand}:browser:${appSlug}`]);
382
389
  return { ok: true, group: "browser", command: `${command}-${subcommand}` };
383
390
  }
@@ -389,13 +396,13 @@ ${browserHelpText()}`);
389
396
  };
390
397
  const packageScript = packageScripts[command];
391
398
  if (packageScript) {
392
- requireNoExtraArgs(rest, `bun run rig browser ${command}`);
399
+ requireNoExtraArgs(rest, `rig browser ${command}`);
393
400
  await context.runCommand(["bun", "run", "--filter=@rig/browser", packageScript]);
394
401
  return { ok: true, group: "browser", command };
395
402
  }
396
- throw new CliError2(`Unknown browser command: ${command}
403
+ throw new CliError(`Unknown browser command: ${command}
397
404
 
398
- ${browserHelpText()}`);
405
+ ${browserHelpText()}`, 1, { hint: "Run `rig browser help` for the full browser command reference." });
399
406
  }
400
407
  async function executeBrowserDemo(context, args) {
401
408
  const [maybeHelp] = args;
@@ -416,13 +423,13 @@ async function executeBrowserDemo(context, args) {
416
423
  pending = keepOpenFlag.rest;
417
424
  const noBuildFlag = takeFlag(pending, "--no-build");
418
425
  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]");
426
+ requireNoExtraArgs(pending, "rig browser demo [--port <n>] [--profile <name>] [--state-dir <path>] [--target-url <url>] [--keep-open] [--no-build]");
420
427
  if (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY) {
421
- throw new CliError2("rig browser demo requires an interactive TTY in text mode.");
428
+ throw new CliError("rig browser demo requires an interactive TTY in text mode.", 1, { hint: "Run `rig browser demo` from an interactive terminal (no --json, no piped stdin)." });
422
429
  }
423
430
  const port = portResult.value ? Number.parseInt(portResult.value, 10) : undefined;
424
431
  if (port !== undefined && (!Number.isFinite(port) || port <= 0)) {
425
- throw new CliError2(`Invalid --port value: ${portResult.value}`);
432
+ throw new CliError(`Invalid --port value: ${portResult.value}`, 1, { hint: "Pass a positive port number, e.g. `--port 9222`." });
426
433
  }
427
434
  const runtime = buildBrowserDemoRuntime(context.projectRoot, {
428
435
  ...port !== undefined ? { port } : {},
@@ -577,7 +584,7 @@ function runBrowserDemoBuild(projectRoot) {
577
584
  if (result.exitCode !== 0) {
578
585
  const details = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join(`
579
586
  `);
580
- throw new CliError2(`Browser demo build failed.${details ? `
587
+ throw new CliError(`Browser demo build failed.${details ? `
581
588
  ${details}` : ""}`, result.exitCode);
582
589
  }
583
590
  }
@@ -632,7 +639,7 @@ function readBrowserDemoCommandLine(command) {
632
639
  cleanup();
633
640
  stdout.write(`
634
641
  `);
635
- rejectInput(new CliError2("Browser demo cancelled."));
642
+ rejectInput(new CliError("Browser demo cancelled."));
636
643
  return;
637
644
  }
638
645
  if (key.name === "return" || key.name === "enter") {
@@ -698,7 +705,7 @@ async function waitForBrowserDemoReady(runtime, child) {
698
705
  return { targets: nextTargets, pageTarget: nextPageTarget };
699
706
  }, child, "CDP demo page target");
700
707
  if (!version.Browser) {
701
- throw new CliError2("Browser demo reached CDP but /json/version did not include Browser metadata.");
708
+ throw new CliError("Browser demo reached CDP but /json/version did not include Browser metadata.");
702
709
  }
703
710
  return { version, targets, pageTarget };
704
711
  }
@@ -711,14 +718,14 @@ async function waitForBrowserDemo(check, child, label, timeoutMs = 18000) {
711
718
  let lastError = null;
712
719
  for (;; ) {
713
720
  if (child.exitCode !== null || child.signalCode !== null) {
714
- throw new CliError2(`Rig Browser exited before ${label} became ready.`);
721
+ throw new CliError(`Rig Browser exited before ${label} became ready.`);
715
722
  }
716
723
  try {
717
724
  return await check();
718
725
  } catch (error) {
719
726
  lastError = error;
720
727
  if (Date.now() - startedAt > timeoutMs) {
721
- throw new CliError2(`${label} did not become ready within ${timeoutMs}ms: ${String(lastError)}`);
728
+ throw new CliError(`${label} did not become ready within ${timeoutMs}ms: ${String(lastError)}`);
722
729
  }
723
730
  await new Promise((resolveDelay) => setTimeout(resolveDelay, 150));
724
731
  }
@@ -1,12 +1,22 @@
1
1
  // @bun
2
+ // packages/cli/src/commands/connect.ts
3
+ import { cancel, isCancel, select } from "@clack/prompts";
4
+
2
5
  // packages/cli/src/runner.ts
3
6
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
4
- import { CliError } from "@rig/runtime/control-plane/errors";
7
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
5
8
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
6
- import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
7
- import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
8
9
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
9
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
10
+
11
+ class CliError extends RuntimeCliError {
12
+ hint;
13
+ constructor(message, exitCode = 1, options = {}) {
14
+ super(message, exitCode);
15
+ if (options.hint?.trim()) {
16
+ this.hint = options.hint.trim();
17
+ }
18
+ }
19
+ }
10
20
  function requireNoExtraArgs(args, usage) {
11
21
  if (args.length > 0) {
12
22
  throw new CliError(`Unexpected arguments: ${args.join(" ")}
@@ -36,7 +46,7 @@ function readJsonFile(path) {
36
46
  try {
37
47
  return JSON.parse(readFileSync(path, "utf8"));
38
48
  } catch (error) {
39
- throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
49
+ throw new CliError(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1, { hint: "Fix or delete that file, then re-select a server with `rig server use <alias|local>`." });
40
50
  }
41
51
  }
42
52
  function writeJsonFile(path, value) {
@@ -79,7 +89,7 @@ function writeGlobalConnections(state, options = {}) {
79
89
  function upsertGlobalConnection(alias, connection, options = {}) {
80
90
  const cleanAlias = alias.trim();
81
91
  if (!cleanAlias)
82
- throw new CliError2("Connection alias is required.", 1);
92
+ throw new CliError("Connection alias is required.", 1, { hint: "Save a server with `rig server add <alias> <url>`." });
83
93
  const state = readGlobalConnections(options);
84
94
  state.connections[cleanAlias] = connection;
85
95
  writeGlobalConnections(state, options);
@@ -96,27 +106,98 @@ function readRepoConnection(projectRoot) {
96
106
  return {
97
107
  selected,
98
108
  project: typeof record.project === "string" ? record.project : undefined,
99
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
109
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
110
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
100
111
  };
101
112
  }
102
113
  function writeRepoConnection(projectRoot, state) {
103
114
  writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
104
115
  }
105
116
 
117
+ // packages/cli/src/commands/_cli-format.ts
118
+ import { log, note } from "@clack/prompts";
119
+ import pc from "picocolors";
120
+ function truncate(value, width) {
121
+ if (value.length <= width)
122
+ return value;
123
+ if (width <= 1)
124
+ return "\u2026";
125
+ return `${value.slice(0, width - 1)}\u2026`;
126
+ }
127
+ function pad(value, width) {
128
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
129
+ }
130
+ function statusColor(status) {
131
+ const normalized = status.toLowerCase();
132
+ if (["completed", "merged", "closed", "done", "accepted", "pass", "selected", "approved"].includes(normalized))
133
+ return pc.green;
134
+ if (["failed", "needs_attention", "needs-attention", "blocked", "error", "rejected"].includes(normalized))
135
+ return pc.red;
136
+ if (["running", "reviewing", "validating", "in_progress", "in-progress", "remote"].includes(normalized))
137
+ return pc.cyan;
138
+ if (["ready", "open", "queued", "created", "preparing", "local", "pending"].includes(normalized))
139
+ return pc.yellow;
140
+ return pc.dim;
141
+ }
142
+ function formatStatusPill(status) {
143
+ const label = status || "unknown";
144
+ return statusColor(label)(`\u25CF ${label}`);
145
+ }
146
+ function formatSection(title, subtitle) {
147
+ return `${pc.bold(pc.cyan("\u25C6"))} ${pc.bold(title)}${subtitle ? pc.dim(` \u2014 ${subtitle}`) : ""}`;
148
+ }
149
+ function formatSuccessCard(title, rows = []) {
150
+ const body = rows.filter(([, value]) => value !== undefined && value !== null && String(value).length > 0).map(([key, value]) => `${pc.dim("\u2502")} ${pc.dim(key.padEnd(12))} ${value}`);
151
+ return [formatSection(title), ...body].join(`
152
+ `);
153
+ }
154
+ function formatNextSteps(steps) {
155
+ if (steps.length === 0)
156
+ return [];
157
+ return [pc.bold("Next"), ...steps.map((step) => `${pc.dim("\u203A")} ${step}`)];
158
+ }
159
+ function formatConnectionList(connections) {
160
+ const rows = [["local", { kind: "local", mode: "auto" }], ...Object.entries(connections)];
161
+ const aliasWidth = Math.min(24, Math.max(5, ...rows.map(([alias]) => alias.length)));
162
+ const lines = rows.map(([alias, connection]) => [
163
+ pc.bold(pad(truncate(alias, aliasWidth), aliasWidth)),
164
+ formatStatusPill(connection.kind),
165
+ connection.kind === "remote" ? connection.baseUrl ?? "" : connection.mode ?? "local"
166
+ ].join(" "));
167
+ return [formatSection("Rig servers", `${rows.length} available`), `${pc.bold(pad("ALIAS", aliasWidth))} ${pc.bold("KIND")} ${pc.bold("TARGET")}`, ...lines, "", ...formatNextSteps(["Select one: `rig server use <alias|local>`"])].join(`
168
+ `);
169
+ }
170
+ function formatConnectionStatus(selected, connections) {
171
+ const connection = selected === "local" ? { kind: "local", mode: "auto" } : connections[selected];
172
+ const target = !connection ? "not configured" : connection.kind === "remote" ? connection.baseUrl : "local";
173
+ return [
174
+ formatSection("Rig server", "selected for this repo"),
175
+ `${pc.dim("\u2502")} ${pc.dim("selected ")} ${pc.bold(selected)}`,
176
+ `${pc.dim("\u2502")} ${pc.dim("kind ")} ${formatStatusPill(connection?.kind ?? "unknown")}`,
177
+ `${pc.dim("\u2502")} ${pc.dim("target ")} ${target ?? "not configured"}`,
178
+ "",
179
+ ...formatNextSteps(["Change: `rig server use <alias|local>`", "List saved servers: `rig server list`"])
180
+ ].join(`
181
+ `);
182
+ }
183
+
106
184
  // packages/cli/src/commands/connect.ts
107
- function parseConnection(alias, value) {
185
+ function usageName(options) {
186
+ return `rig ${options.group}`;
187
+ }
188
+ function parseConnection(alias, value, options) {
108
189
  if (alias === "local" && !value)
109
190
  return { kind: "local", mode: "auto" };
110
191
  if (!value)
111
- throw new CliError2("Missing remote server URL. Usage: rig connect add <alias> <url>", 1);
192
+ throw new CliError(`Missing remote server URL. Usage: ${usageName(options)} add <alias> <url>`, 1);
112
193
  let parsed;
113
194
  try {
114
195
  parsed = new URL(value);
115
196
  } catch {
116
- throw new CliError2(`Invalid Rig server URL: ${value}`, 1);
197
+ throw new CliError(`Invalid Rig server URL: ${value}`, 1, { hint: "Pass a full http(s) URL, e.g. `rig server add prod https://rig.example.com`." });
117
198
  }
118
199
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
119
- throw new CliError2("Rig remote server URL must be http(s).", 1);
200
+ throw new CliError("Rig remote server URL must be http(s).", 1, { hint: "Use an http:// or https:// URL, e.g. `rig server add prod https://rig.example.com`." });
120
201
  }
121
202
  return { kind: "remote", baseUrl: parsed.toString().replace(/\/+$/, "") };
122
203
  }
@@ -127,54 +208,86 @@ function printJsonOrText(context, payload, text) {
127
208
  console.log(text);
128
209
  }
129
210
  }
130
- async function executeConnect(context, args) {
211
+ async function promptForConnectionAlias(context) {
212
+ const state = readGlobalConnections();
213
+ const repo = readRepoConnection(context.projectRoot);
214
+ const options = [
215
+ { value: "local", label: "local", hint: "Use/start a local Rig server" },
216
+ ...Object.entries(state.connections).map(([alias, connection]) => ({
217
+ value: alias,
218
+ label: alias,
219
+ hint: connection.kind === "remote" ? connection.baseUrl : "local"
220
+ }))
221
+ ].filter((option, index, all) => all.findIndex((candidate) => candidate.value === option.value) === index);
222
+ const answer = await select({
223
+ message: "Select Rig server for this repo",
224
+ initialValue: repo?.selected ?? "local",
225
+ options
226
+ });
227
+ if (isCancel(answer)) {
228
+ cancel("No server selected.");
229
+ throw new CliError("No server selected.", 3);
230
+ }
231
+ return String(answer);
232
+ }
233
+ async function executeConnectionCommand(context, args, options) {
131
234
  const [command, ...rest] = args;
132
235
  switch (command ?? "status") {
133
236
  case "list": {
134
- requireNoExtraArgs(rest, "rig connect list");
237
+ requireNoExtraArgs(rest, `${usageName(options)} list`);
135
238
  const state = readGlobalConnections();
136
- printJsonOrText(context, state, Object.entries(state.connections).map(([alias, connection]) => `${alias} ${connection.kind === "remote" ? connection.baseUrl : "local"}`).join(`
137
- `) || "No Rig connections configured.");
138
- return { ok: true, group: "connect", command: "list", details: state };
239
+ printJsonOrText(context, state, formatConnectionList(state.connections));
240
+ return { ok: true, group: options.group, command: "list", details: state };
139
241
  }
140
242
  case "add": {
141
243
  const [alias, url, ...extra] = rest;
142
244
  if (!alias)
143
- throw new CliError2("Missing alias. Usage: rig connect add <alias> <url>", 1);
144
- requireNoExtraArgs(extra, "rig connect add <alias> <url>");
145
- const connection = parseConnection(alias, url);
245
+ throw new CliError(`Missing alias. Usage: ${usageName(options)} add <alias> <url>`, 1);
246
+ requireNoExtraArgs(extra, `${usageName(options)} add <alias> <url>`);
247
+ const connection = parseConnection(alias, url, options);
146
248
  const state = upsertGlobalConnection(alias, connection);
147
- printJsonOrText(context, { alias, connection }, `Added Rig connection ${alias}.`);
148
- return { ok: true, group: "connect", command: "add", details: { alias, connection, count: Object.keys(state.connections).length } };
249
+ printJsonOrText(context, { alias, connection }, formatSuccessCard("Rig server saved", [
250
+ ["alias", alias],
251
+ ["target", connection.kind === "remote" ? connection.baseUrl : "local"],
252
+ ["next", `${usageName(options)} use ${alias}`]
253
+ ]));
254
+ return { ok: true, group: options.group, command: "add", details: { alias, connection, count: Object.keys(state.connections).length } };
149
255
  }
150
256
  case "use": {
151
- const [alias, ...extra] = rest;
257
+ let [alias, ...extra] = rest;
258
+ requireNoExtraArgs(extra, `${usageName(options)} use <alias|local>`);
259
+ if (!alias && options.interactiveUse && context.outputMode === "text" && process.stdin.isTTY) {
260
+ alias = await promptForConnectionAlias(context);
261
+ }
152
262
  if (!alias)
153
- throw new CliError2("Missing alias. Usage: rig connect use <alias|local>", 1);
154
- requireNoExtraArgs(extra, "rig connect use <alias|local>");
263
+ throw new CliError(`Missing alias. Usage: ${usageName(options)} use <alias|local>`, 1);
155
264
  if (alias !== "local") {
156
265
  const state = readGlobalConnections();
157
266
  if (!state.connections[alias])
158
- throw new CliError2(`Unknown Rig connection: ${alias}`, 1);
267
+ throw new CliError(`Unknown Rig server: ${alias}`, 1, { hint: "Run `rig server list` to see saved aliases, or save one with `rig server add <alias> <url>`." });
159
268
  }
160
269
  const repoState = { selected: alias, linkedAt: new Date().toISOString() };
161
270
  writeRepoConnection(context.projectRoot, repoState);
162
- printJsonOrText(context, repoState, `Selected Rig connection ${alias} for this repo.`);
163
- return { ok: true, group: "connect", command: "use", details: repoState };
271
+ printJsonOrText(context, repoState, formatSuccessCard("Rig server selected", [
272
+ ["selected", alias],
273
+ ["scope", "this repo"],
274
+ ["next", "rig task list"]
275
+ ]));
276
+ return { ok: true, group: options.group, command: "use", details: repoState };
164
277
  }
165
278
  case "status": {
166
- requireNoExtraArgs(rest, "rig connect status");
279
+ requireNoExtraArgs(rest, `${usageName(options)} status`);
167
280
  const repo = readRepoConnection(context.projectRoot);
168
281
  const global = readGlobalConnections();
169
282
  const details = { selected: repo?.selected ?? "local", repo, connections: global.connections };
170
- printJsonOrText(context, details, `Selected Rig connection: ${details.selected}`);
171
- return { ok: true, group: "connect", command: "status", details };
283
+ printJsonOrText(context, details, formatConnectionStatus(details.selected, global.connections));
284
+ return { ok: true, group: options.group, command: "status", details };
172
285
  }
173
286
  default:
174
- throw new CliError2(`Unknown connect command: ${String(command)}
175
- Usage: rig connect <list|add|use|status>`, 1);
287
+ throw new CliError(`Unknown ${options.group} command: ${String(command)}
288
+ Usage: ${usageName(options)} <list|add|use|status>`, 1);
176
289
  }
177
290
  }
178
291
  export {
179
- executeConnect
292
+ executeConnectionCommand
180
293
  };
@@ -17,12 +17,19 @@ import { resolve as resolve3 } from "path";
17
17
 
18
18
  // packages/cli/src/runner.ts
19
19
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
20
- import { CliError } from "@rig/runtime/control-plane/errors";
20
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
21
21
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
22
- import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
23
- import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
24
22
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
25
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
23
+
24
+ class CliError extends RuntimeCliError {
25
+ hint;
26
+ constructor(message, exitCode = 1, options = {}) {
27
+ super(message, exitCode);
28
+ if (options.hint?.trim()) {
29
+ this.hint = options.hint.trim();
30
+ }
31
+ }
32
+ }
26
33
  function takeOption(args, option) {
27
34
  const rest = [];
28
35
  let value;
@@ -31,7 +38,7 @@ function takeOption(args, option) {
31
38
  if (current === option) {
32
39
  const next = args[index + 1];
33
40
  if (!next || next.startsWith("-")) {
34
- throw new CliError(`Missing value for ${option}`);
41
+ throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
35
42
  }
36
43
  value = next;
37
44
  index += 1;
@@ -67,7 +74,7 @@ function parseInstallScope(value) {
67
74
  if (value === "system") {
68
75
  return "system";
69
76
  }
70
- throw new CliError2(`Invalid --scope value: ${value}. Use user|system.`);
77
+ throw new CliError(`Invalid --scope value: ${value}. Use user|system.`);
71
78
  }
72
79
  function resolveInstallDir(scope, explicitPath) {
73
80
  if (explicitPath) {
@@ -185,7 +192,7 @@ async function executeDist(context, args) {
185
192
  switch (command) {
186
193
  case "build": {
187
194
  const { value: outputDir, rest: pending } = takeOption(rest, "--output-dir");
188
- requireNoExtraArgs(pending, "bun run rig dist build [--output-dir <dir>]");
195
+ requireNoExtraArgs(pending, "rig dist build [--output-dir <dir>]");
189
196
  const commandParts = ["bun", "run", "packages/cli/bin/build-rig-binaries.ts"];
190
197
  if (outputDir) {
191
198
  commandParts.push("--output-dir", outputDir);
@@ -199,7 +206,7 @@ async function executeDist(context, args) {
199
206
  pending = scopeResult.rest;
200
207
  const pathResult = takeOption(pending, "--path");
201
208
  pending = pathResult.rest;
202
- requireNoExtraArgs(pending, "bun run rig dist install [--scope user|system] [--path <dir>]");
209
+ requireNoExtraArgs(pending, "rig dist install [--scope user|system] [--path <dir>]");
203
210
  const scope = parseInstallScope(scopeResult.value);
204
211
  const installDir = resolveInstallDir(scope, pathResult.value);
205
212
  mkdirSync(installDir, { recursive: true });
@@ -211,7 +218,7 @@ async function executeDist(context, args) {
211
218
  source = resolve3(buildDir, "bin", "rig");
212
219
  }
213
220
  if (!existsSync(source)) {
214
- throw new CliError2(`Unable to locate rig binary at ${source}.`, 2);
221
+ throw new CliError(`Unable to locate rig binary at ${source}.`, 2, { hint: "Build it first with `rig dist build`, then re-run `rig dist install`." });
215
222
  }
216
223
  const installedPath = resolve3(installDir, "rig");
217
224
  if (existsSync(installedPath)) {
@@ -242,7 +249,7 @@ async function executeDist(context, args) {
242
249
  };
243
250
  }
244
251
  case "doctor": {
245
- requireNoExtraArgs(rest, "bun run rig dist doctor");
252
+ requireNoExtraArgs(rest, "rig dist doctor");
246
253
  const details = await runDistDoctor(context.projectRoot);
247
254
  if (context.outputMode === "text") {
248
255
  console.log(`bun: ${details.bun.available ? `ok (${details.bun.version})` : "missing"}`);
@@ -253,7 +260,7 @@ async function executeDist(context, args) {
253
260
  return { ok: true, group: "dist", command, details };
254
261
  }
255
262
  case "rebuild-agent": {
256
- requireNoExtraArgs(rest, "bun run rig dist rebuild-agent");
263
+ requireNoExtraArgs(rest, "rig dist rebuild-agent");
257
264
  const fp = await computeRuntimeImageFingerprint(context.projectRoot);
258
265
  const currentId = computeRuntimeImageId(fp);
259
266
  const imagesDir = resolve3(resolveControlPlaneMonorepoRuntimeDir(context.projectRoot), "images");
@@ -393,7 +400,7 @@ async function executeDist(context, args) {
393
400
  return { ok: true, group: "dist", command, details: { imageId: currentId, count: targets.length } };
394
401
  }
395
402
  default:
396
- throw new CliError2(`Unknown dist command: ${command}`);
403
+ throw new CliError(`Unknown dist command: ${command}`, 1, { hint: "Run `rig dist --help` \u2014 commands are build|install|doctor." });
397
404
  }
398
405
  }
399
406
  export {