@h-rig/cli 0.0.6-alpha.64 → 0.0.6-alpha.66

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 (46) hide show
  1. package/dist/bin/rig.js +1349 -1599
  2. package/dist/src/commands/_connection-state.js +14 -5
  3. package/dist/src/commands/_doctor-checks.js +71 -11
  4. package/dist/src/commands/_help-catalog.js +99 -60
  5. package/dist/src/commands/_json-output.js +56 -0
  6. package/dist/src/commands/_operator-view.js +97 -784
  7. package/dist/src/commands/_parsers.js +18 -9
  8. package/dist/src/commands/_pi-frontend.js +96 -788
  9. package/dist/src/commands/_policy.js +12 -3
  10. package/dist/src/commands/_preflight.js +73 -13
  11. package/dist/src/commands/_run-driver-helpers.js +31 -5
  12. package/dist/src/commands/_run-replay.js +2 -2
  13. package/dist/src/commands/_server-client.js +76 -16
  14. package/dist/src/commands/_snapshot-upload.js +70 -10
  15. package/dist/src/commands/agent.js +21 -11
  16. package/dist/src/commands/browser.js +24 -15
  17. package/dist/src/commands/connect.js +22 -17
  18. package/dist/src/commands/dist.js +15 -6
  19. package/dist/src/commands/doctor.js +70 -10
  20. package/dist/src/commands/github.js +79 -19
  21. package/dist/src/commands/inbox.js +215 -131
  22. package/dist/src/commands/init.js +202 -35
  23. package/dist/src/commands/inspect.js +77 -17
  24. package/dist/src/commands/inspector.js +18 -9
  25. package/dist/src/commands/pi.js +18 -9
  26. package/dist/src/commands/plugin.js +19 -10
  27. package/dist/src/commands/profile-and-review.js +22 -13
  28. package/dist/src/commands/queue.js +19 -9
  29. package/dist/src/commands/remote.js +25 -16
  30. package/dist/src/commands/repo-git-harness.js +18 -9
  31. package/dist/src/commands/run.js +158 -805
  32. package/dist/src/commands/server.js +85 -25
  33. package/dist/src/commands/setup.js +73 -13
  34. package/dist/src/commands/stats.js +681 -0
  35. package/dist/src/commands/task-report-bug.js +24 -15
  36. package/dist/src/commands/task-run-driver.js +121 -49
  37. package/dist/src/commands/task.js +254 -893
  38. package/dist/src/commands/test.js +12 -3
  39. package/dist/src/commands/workspace.js +14 -5
  40. package/dist/src/commands.js +1339 -1650
  41. package/dist/src/index.js +1349 -1599
  42. package/dist/src/launcher.js +72 -10
  43. package/dist/src/runner.js +14 -5
  44. package/package.json +10 -7
  45. package/dist/src/commands/_pi-remote-session.js +0 -771
  46. package/dist/src/commands/_pi-worker-bridge-extension.js +0 -834
@@ -8,10 +8,19 @@ import { basename, resolve as resolve6 } from "path";
8
8
 
9
9
  // packages/cli/src/runner.ts
10
10
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
11
- import { CliError } from "@rig/runtime/control-plane/errors";
11
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
12
12
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
13
13
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
14
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
14
+
15
+ class CliError extends RuntimeCliError {
16
+ hint;
17
+ constructor(message, exitCode = 1, options = {}) {
18
+ super(message, exitCode);
19
+ if (options.hint?.trim()) {
20
+ this.hint = options.hint.trim();
21
+ }
22
+ }
23
+ }
15
24
  function takeFlag(args, flag) {
16
25
  const rest = [];
17
26
  let value = false;
@@ -32,7 +41,7 @@ function takeOption(args, option) {
32
41
  if (current === option) {
33
42
  const next = args[index + 1];
34
43
  if (!next || next.startsWith("-")) {
35
- throw new CliError(`Missing value for ${option}`);
44
+ throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
36
45
  }
37
46
  value = next;
38
47
  index += 1;
@@ -71,7 +80,7 @@ function readJsonFile(path) {
71
80
  try {
72
81
  return JSON.parse(readFileSync(path, "utf8"));
73
82
  } catch (error) {
74
- throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
83
+ 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>`." });
75
84
  }
76
85
  }
77
86
  function writeJsonFile(path, value) {
@@ -114,7 +123,7 @@ function writeGlobalConnections(state, options = {}) {
114
123
  function upsertGlobalConnection(alias, connection, options = {}) {
115
124
  const cleanAlias = alias.trim();
116
125
  if (!cleanAlias)
117
- throw new CliError2("Connection alias is required.", 1);
126
+ throw new CliError("Connection alias is required.", 1, { hint: "Save a server with `rig server add <alias> <url>`." });
118
127
  const state = readGlobalConnections(options);
119
128
  state.connections[cleanAlias] = connection;
120
129
  writeGlobalConnections(state, options);
@@ -147,7 +156,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
147
156
  const global = readGlobalConnections(options);
148
157
  const connection = global.connections[repo.selected];
149
158
  if (!connection) {
150
- throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
159
+ throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
151
160
  }
152
161
  return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
153
162
  }
@@ -227,7 +236,7 @@ async function ensureServerForCli(projectRoot) {
227
236
  };
228
237
  } catch (error) {
229
238
  if (error instanceof Error) {
230
- throw new CliError2(error.message, 1);
239
+ throw new CliError(error.message, 1);
231
240
  }
232
241
  throw error;
233
242
  }
@@ -277,15 +286,64 @@ function diagnosticMessage(payload) {
277
286
  });
278
287
  return messages.length > 0 ? messages.join("; ") : null;
279
288
  }
289
+ var serverReachabilityCache = new Map;
290
+ async function probeServerReachability(baseUrl, authToken) {
291
+ try {
292
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
293
+ headers: mergeHeaders(undefined, authToken),
294
+ signal: AbortSignal.timeout(1500)
295
+ });
296
+ return response.ok;
297
+ } catch {
298
+ return false;
299
+ }
300
+ }
301
+ function cachedServerReachability(projectRoot, baseUrl, authToken) {
302
+ const key = resolve2(projectRoot);
303
+ const cached = serverReachabilityCache.get(key);
304
+ if (cached)
305
+ return cached;
306
+ const probe = probeServerReachability(baseUrl, authToken);
307
+ serverReachabilityCache.set(key, probe);
308
+ return probe;
309
+ }
310
+ function describeSelectedServer(projectRoot, server) {
311
+ try {
312
+ const selected = resolveSelectedConnection(projectRoot);
313
+ if (selected) {
314
+ return {
315
+ alias: selected.alias,
316
+ target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
317
+ };
318
+ }
319
+ } catch {}
320
+ return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
321
+ }
322
+ async function buildServerFailureContext(projectRoot, server) {
323
+ const { alias, target } = describeSelectedServer(projectRoot, server);
324
+ const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
325
+ const reachability = reachable ? "server is reachable" : "server is unreachable";
326
+ return {
327
+ contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
328
+ hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
329
+ };
330
+ }
280
331
  async function requestServerJson(context, pathname, init = {}) {
281
332
  const server = await ensureServerForCli(context.projectRoot);
282
333
  const headers = mergeHeaders(init.headers, server.authToken);
283
334
  if (server.serverProjectRoot)
284
335
  headers.set("x-rig-project-root", server.serverProjectRoot);
285
- const response = await fetch(`${server.baseUrl}${pathname}`, {
286
- ...init,
287
- headers
288
- });
336
+ let response;
337
+ try {
338
+ response = await fetch(`${server.baseUrl}${pathname}`, {
339
+ ...init,
340
+ headers
341
+ });
342
+ } catch (error) {
343
+ const failure = await buildServerFailureContext(context.projectRoot, server);
344
+ throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
345
+ ${failure.contextLine}`, 1, { hint: failure.hint });
346
+ }
289
347
  const text = await response.text();
290
348
  const payload = text.trim().length > 0 ? (() => {
291
349
  try {
@@ -297,7 +355,9 @@ async function requestServerJson(context, pathname, init = {}) {
297
355
  if (!response.ok) {
298
356
  const diagnostics = diagnosticMessage(payload);
299
357
  const detail = diagnostics ?? (text || response.statusText);
300
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
358
+ const failure = await buildServerFailureContext(context.projectRoot, server);
359
+ throw new CliError(`Rig server request failed (${response.status}): ${detail}
360
+ ${failure.contextLine}`, 1, { hint: failure.hint });
301
361
  }
302
362
  return payload;
303
363
  }
@@ -356,14 +416,14 @@ async function switchServerProjectRootViaServer(context, projectRoot, options =
356
416
  }
357
417
  }
358
418
  if (!switched) {
359
- throw new CliError2(`Rig server did not accept project-root switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no response")}).`, 1);
419
+ throw new CliError(`Rig server did not accept project-root switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no response")}).`, 1);
360
420
  }
361
421
  const record = switched && typeof switched === "object" && !Array.isArray(switched) ? switched : {};
362
422
  if (record.ok === true) {
363
423
  writeRepoServerProjectRoot(context.projectRoot, projectRoot);
364
424
  return { ok: true, switched: record };
365
425
  }
366
- throw new CliError2(`Rig server rejected project-root scope ${projectRoot}: ${String(record.message ?? record.error ?? "unknown error")}`, 1);
426
+ throw new CliError(`Rig server rejected project-root scope ${projectRoot}: ${String(record.message ?? record.error ?? "unknown error")}`, 1);
367
427
  }
368
428
  async function ensureTaskLabelsViaServer(context) {
369
429
  const payload = await requestServerJson(context, "/api/workspace/task-labels", { method: "POST" });
@@ -891,7 +951,7 @@ function detectOriginRepoSlug(projectRoot) {
891
951
  function parseRepoSlug(value) {
892
952
  const match = value.trim().match(/^([^/\s]+)\/([^/\s]+)$/);
893
953
  if (!match)
894
- throw new CliError2(`Invalid GitHub repo slug: ${value}. Expected owner/repo.`, 1);
954
+ throw new CliError(`Invalid GitHub repo slug: ${value}. Expected owner/repo.`, 1, { hint: "Pass the slug as owner/repo, e.g. `rig init --repo acme/widgets`." });
895
955
  return { owner: match[1], repo: match[2], slug: `${match[1]}/${match[2]}` };
896
956
  }
897
957
  function ensureRigPrivateDirs(projectRoot) {
@@ -973,7 +1033,7 @@ function detectGhLogin() {
973
1033
  function readGhAuthToken() {
974
1034
  const result = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
975
1035
  if (result.status !== 0 || !result.stdout.trim()) {
976
- throw new CliError2(result.stderr.trim() || "Could not read GitHub token from `gh auth token`.", result.status || 1);
1036
+ throw new CliError(result.stderr.trim() || "Could not read GitHub token from `gh auth token`.", result.status || 1, { hint: "Sign in with `gh auth login`, or pass a token directly: `rig init --github-auth token --github-token <token>`." });
977
1037
  }
978
1038
  return result.stdout.trim();
979
1039
  }
@@ -1003,22 +1063,22 @@ function clackTextOptions(options) {
1003
1063
  async function promptRequiredText(prompts, options) {
1004
1064
  const value = await prompts.text(clackTextOptions(options));
1005
1065
  if (prompts.isCancel(value))
1006
- throw new CliError2("Init cancelled.", 1);
1066
+ throw new CliError("Init cancelled.", 1);
1007
1067
  const text = String(value ?? "").trim();
1008
1068
  if (!text)
1009
- throw new CliError2(`${options.message} is required.`, 1);
1069
+ throw new CliError(`${options.message} is required.`, 1);
1010
1070
  return text;
1011
1071
  }
1012
1072
  async function promptOptionalText(prompts, options) {
1013
1073
  const value = await prompts.text(clackTextOptions(options));
1014
1074
  if (prompts.isCancel(value))
1015
- throw new CliError2("Init cancelled.", 1);
1075
+ throw new CliError("Init cancelled.", 1);
1016
1076
  return String(value ?? "").trim();
1017
1077
  }
1018
1078
  async function promptSelect(prompts, options) {
1019
1079
  const value = await prompts.select(options);
1020
1080
  if (prompts.isCancel(value))
1021
- throw new CliError2("Init cancelled.", 1);
1081
+ throw new CliError("Init cancelled.", 1);
1022
1082
  return String(value);
1023
1083
  }
1024
1084
  function repoOwnerFromSlug(repoSlug) {
@@ -1120,7 +1180,7 @@ async function promptGitHubProjectConfig(context, prompts, repoSlug, githubToken
1120
1180
  }
1121
1181
  const owner = repoOwnerFromSlug(repoSlug);
1122
1182
  if (!owner)
1123
- throw new CliError2(`Cannot derive GitHub owner from repo slug ${repoSlug}.`, 1);
1183
+ throw new CliError(`Cannot derive GitHub owner from repo slug ${repoSlug}.`, 1, { hint: "Pass the slug as owner/repo, e.g. `rig init --repo acme/widgets`." });
1124
1184
  let activeToken = githubToken?.trim() || null;
1125
1185
  let projectsPayload = await listGitHubProjectsForInit(context, owner, activeToken).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), projects: [] }));
1126
1186
  let projects = recordArray(projectsPayload, "projects");
@@ -1278,6 +1338,100 @@ function runLocalFilesInit(context, options) {
1278
1338
  }
1279
1339
  return { ok: true, group: "init", command: "init", details: { mode: "local-files", projectRoot, taskSourcePath: "tasks" } };
1280
1340
  }
1341
+ var DEMO_TASKS_RELATIVE_DIR = ".rig/demo-tasks";
1342
+ var DEMO_TASKS = [
1343
+ {
1344
+ id: "demo-1",
1345
+ title: "Add a hello CLI script",
1346
+ body: [
1347
+ "Create `scripts/hello.ts` that prints `Hello from Rig!` plus the current date,",
1348
+ "and add a `hello` script entry to package.json that runs it with bun.",
1349
+ "Keep it dependency-free."
1350
+ ].join(`
1351
+ `),
1352
+ status: "ready",
1353
+ labels: ["demo"]
1354
+ },
1355
+ {
1356
+ id: "demo-2",
1357
+ title: "Write a README section about this project",
1358
+ body: [
1359
+ "Add (or extend) README.md with a short `## What this is` section:",
1360
+ "two or three sentences describing the repository and how to run it.",
1361
+ "Plain prose, no badges."
1362
+ ].join(`
1363
+ `),
1364
+ status: "ready",
1365
+ labels: ["demo"]
1366
+ },
1367
+ {
1368
+ id: "demo-3",
1369
+ title: "Add a unit test for the hello script",
1370
+ body: [
1371
+ "Add `scripts/hello.test.ts` with a bun test that imports the greeting",
1372
+ "helper from the hello script and asserts it contains `Hello from Rig!`.",
1373
+ "Refactor the script to export that helper if needed."
1374
+ ].join(`
1375
+ `),
1376
+ status: "ready",
1377
+ labels: ["demo"]
1378
+ }
1379
+ ];
1380
+ function runDemoInit(context, options) {
1381
+ const projectRoot = context.projectRoot;
1382
+ ensureRigPrivateDirs(projectRoot);
1383
+ ensureGitignoreEntries(projectRoot);
1384
+ writeRepoConnection(projectRoot, { selected: "local", linkedAt: new Date().toISOString() });
1385
+ const configTsPath = resolve6(projectRoot, "rig.config.ts");
1386
+ const configExists = existsSync5(configTsPath) || existsSync5(resolve6(projectRoot, "rig.config.json"));
1387
+ let configWritten = false;
1388
+ if (configExists && !options.repair) {
1389
+ if (context.outputMode !== "json") {
1390
+ console.log("rig.config already exists; leaving it unchanged. Pass --repair to rewrite it for the demo.");
1391
+ }
1392
+ } else {
1393
+ writeFileSync2(configTsPath, buildRigInitConfigSource({
1394
+ projectName: basename(projectRoot) || "rig-demo",
1395
+ taskSource: { kind: "files", path: DEMO_TASKS_RELATIVE_DIR },
1396
+ useStandardPlugin: true
1397
+ }), "utf-8");
1398
+ configWritten = true;
1399
+ }
1400
+ ensureRigConfigPackageDependencies(projectRoot);
1401
+ const demoTasksDir = resolve6(projectRoot, DEMO_TASKS_RELATIVE_DIR);
1402
+ mkdirSync2(demoTasksDir, { recursive: true });
1403
+ const taskIds = [];
1404
+ for (const task of DEMO_TASKS) {
1405
+ const id = String(task.id);
1406
+ taskIds.push(id);
1407
+ const taskPath = resolve6(demoTasksDir, `${id}.json`);
1408
+ if (!existsSync5(taskPath)) {
1409
+ writeFileSync2(taskPath, `${JSON.stringify(task, null, 2)}
1410
+ `, "utf-8");
1411
+ }
1412
+ }
1413
+ if (context.outputMode !== "json") {
1414
+ console.log(`Demo Rig project ready (offline, no GitHub).`);
1415
+ console.log(` config: rig.config.ts (files task source -> ${DEMO_TASKS_RELATIVE_DIR}/)`);
1416
+ console.log(` tasks: ${taskIds.join(", ")}`);
1417
+ console.log("Next steps:");
1418
+ console.log(" 1. rig task list");
1419
+ console.log(" 2. rig task run --next");
1420
+ }
1421
+ return {
1422
+ ok: true,
1423
+ group: "init",
1424
+ command: "init",
1425
+ details: {
1426
+ mode: "demo",
1427
+ projectRoot,
1428
+ taskSourcePath: DEMO_TASKS_RELATIVE_DIR,
1429
+ demoTasksDir,
1430
+ tasks: taskIds,
1431
+ configWritten
1432
+ }
1433
+ };
1434
+ }
1281
1435
  async function runControlPlaneInit(context, options) {
1282
1436
  const projectRoot = context.projectRoot;
1283
1437
  const detectedSlug = options.repoSlug ?? detectOriginRepoSlug(projectRoot);
@@ -1286,14 +1440,14 @@ async function runControlPlaneInit(context, options) {
1286
1440
  if ((options.server ?? "local") === "local" && authMethod2 === "skip") {
1287
1441
  return runLocalFilesInit(context, options);
1288
1442
  }
1289
- throw new CliError2("Could not detect GitHub repo slug from origin. Pass --repo owner/repo \u2014 or run `rig init --yes --server local --github-auth skip` for a local files-source project without GitHub.", 1);
1443
+ throw new CliError("Could not detect GitHub repo slug from origin. Pass --repo owner/repo \u2014 or run `rig init --yes --server local --github-auth skip` for a local files-source project without GitHub.", 1);
1290
1444
  }
1291
1445
  const repo = parseRepoSlug(detectedSlug);
1292
1446
  const serverKind = options.server ?? "local";
1293
1447
  const connectionAlias = options.connectionAlias ?? (serverKind === "local" ? "local" : "remote");
1294
1448
  if (serverKind === "remote") {
1295
1449
  if (!options.remoteUrl)
1296
- throw new CliError2("Missing --remote-url for --server remote.", 1);
1450
+ throw new CliError("Missing --remote-url for --server remote.", 1, { hint: "Re-run as `rig init --server remote --remote-url https://your-rig-server`." });
1297
1451
  upsertGlobalConnection(connectionAlias, { kind: "remote", baseUrl: options.remoteUrl });
1298
1452
  }
1299
1453
  writeRepoConnection(projectRoot, {
@@ -1338,7 +1492,7 @@ async function runControlPlaneInit(context, options) {
1338
1492
  const authMethod = options.githubAuthMethod ?? (options.githubToken ? "token" : "skip");
1339
1493
  const remoteGhTokenWarning = serverKind === "remote" && authMethod === "gh" ? `This sends a GitHub token from this machine to ${options.remoteUrl ?? "the remote Rig server"}.` : null;
1340
1494
  if (remoteGhTokenWarning && !options.yes) {
1341
- throw new CliError2(`${remoteGhTokenWarning} Re-run with --yes to confirm this explicit token transfer.`, 1);
1495
+ throw new CliError(`${remoteGhTokenWarning} Re-run with --yes to confirm this explicit token transfer.`, 1);
1342
1496
  }
1343
1497
  const token = authMethod === "gh" && !options.githubToken ? readGhAuthToken() : options.githubToken?.trim();
1344
1498
  if (token) {
@@ -1450,6 +1604,8 @@ async function runControlPlaneInit(context, options) {
1450
1604
  }
1451
1605
  function parseInitOptions(args) {
1452
1606
  let rest = [...args];
1607
+ const demo = takeFlag(rest, "--demo");
1608
+ rest = demo.rest;
1453
1609
  const yes = takeFlag(rest, "--yes");
1454
1610
  rest = yes.rest;
1455
1611
  const repair = takeFlag(rest, "--repair");
@@ -1479,6 +1635,7 @@ function parseInitOptions(args) {
1479
1635
  const ref = takeOption(rest, "--ref");
1480
1636
  rest = ref.rest;
1481
1637
  const options = {
1638
+ demo: demo.value,
1482
1639
  yes: yes.value,
1483
1640
  repair: repair.value,
1484
1641
  privateStateOnly: privateStateOnly.value,
@@ -1492,10 +1649,10 @@ function parseInitOptions(args) {
1492
1649
  githubProjectStatusField: githubProjectStatusField.value
1493
1650
  };
1494
1651
  if (server.value && options.server === undefined) {
1495
- throw new CliError2("--server must be local or remote.", 1);
1652
+ throw new CliError("--server must be local or remote.", 1);
1496
1653
  }
1497
1654
  if (githubAuth.value && !["gh", "token", "device", "skip"].includes(githubAuth.value)) {
1498
- throw new CliError2("--github-auth must be gh, token, device, or skip.", 1);
1655
+ throw new CliError("--github-auth must be gh, token, device, or skip.", 1);
1499
1656
  }
1500
1657
  if (remoteCheckout.value) {
1501
1658
  if (remoteCheckout.value === "managed-clone")
@@ -1506,10 +1663,10 @@ function parseInitOptions(args) {
1506
1663
  options.remoteCheckout = { kind: "uploaded-snapshot" };
1507
1664
  else if (remoteCheckout.value === "existing-path") {
1508
1665
  if (!existingPath.value)
1509
- throw new CliError2("--remote-checkout existing-path requires --existing-path <path>.", 1);
1666
+ throw new CliError("--remote-checkout existing-path requires --existing-path <path>.", 1);
1510
1667
  options.remoteCheckout = { kind: "existing-path", path: existingPath.value };
1511
1668
  } else {
1512
- throw new CliError2("--remote-checkout must be managed-clone, current-ref, uploaded-snapshot, or existing-path.", 1);
1669
+ throw new CliError("--remote-checkout must be managed-clone, current-ref, uploaded-snapshot, or existing-path.", 1);
1513
1670
  }
1514
1671
  }
1515
1672
  return { options, rest };
@@ -1586,13 +1743,13 @@ async function runInteractiveControlPlaneInit(context, prompts) {
1586
1743
  let remoteGhTokenConfirmed = false;
1587
1744
  if (serverChoice === "remote" && authMethod === "gh") {
1588
1745
  if (!prompts.confirm)
1589
- throw new CliError2("Remote gh-token import requires explicit confirmation.", 1);
1746
+ throw new CliError("Remote gh-token import requires explicit confirmation.", 1);
1590
1747
  const confirmed = await prompts.confirm({
1591
1748
  message: `This sends a GitHub token from this machine to ${remoteUrl}. Continue?`,
1592
1749
  initialValue: true
1593
1750
  });
1594
1751
  if (prompts.isCancel(confirmed) || confirmed !== true) {
1595
- throw new CliError2("Remote gh-token import cancelled.", 1);
1752
+ throw new CliError("Remote gh-token import cancelled.", 1);
1596
1753
  }
1597
1754
  remoteGhTokenConfirmed = true;
1598
1755
  }
@@ -1621,22 +1778,32 @@ async function runInteractiveControlPlaneInit(context, prompts) {
1621
1778
  }
1622
1779
  async function executeInit(context, args) {
1623
1780
  const parsed = parseInitOptions(args);
1781
+ if (parsed.options.demo) {
1782
+ if (parsed.rest.length > 0) {
1783
+ throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
1784
+ Usage: rig init --demo [--yes] [--repair]`, 1, { hint: "Run `rig init --demo` (optionally with --repair to rewrite an existing rig.config)." });
1785
+ }
1786
+ return runDemoInit(context, parsed.options);
1787
+ }
1624
1788
  if (parsed.options.yes || parsed.options.server || parsed.options.repoSlug || parsed.options.githubToken || parsed.options.privateStateOnly || parsed.options.repair || parsed.options.githubAuthMethod || parsed.options.remoteCheckout) {
1625
1789
  if (parsed.rest.length > 0)
1626
- throw new CliError2(`Unexpected arguments: ${parsed.rest.join(" ")}
1627
- Usage: rig init [--server local|remote] [--remote-url <url>] [--repo owner/repo] [--github-auth gh|token|device|skip] [--github-token <token>] [--github-project off|<project-id>] [--remote-checkout managed-clone|current-ref|uploaded-snapshot|existing-path] [--yes]`, 1);
1790
+ throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
1791
+ Usage: rig init [--demo] [--server local|remote] [--remote-url <url>] [--repo owner/repo] [--github-auth gh|token|device|skip] [--github-token <token>] [--github-project off|<project-id>] [--remote-checkout managed-clone|current-ref|uploaded-snapshot|existing-path] [--yes]`, 1);
1628
1792
  return runControlPlaneInit(context, parsed.options);
1629
1793
  }
1630
1794
  if (parsed.rest.length > 0)
1631
- throw new CliError2(`Unexpected arguments: ${parsed.rest.join(" ")}
1795
+ throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
1632
1796
  Usage: rig init`, 1);
1633
1797
  if (!process.stdin.isTTY) {
1634
- throw new CliError2("rig init is interactive and needs a terminal. For scripts, pass flags: rig init --yes --server local --github-auth skip [--repo owner/repo].", 1);
1798
+ throw new CliError("rig init is interactive and needs a terminal. For scripts, pass flags: rig init --yes --server local --github-auth skip [--repo owner/repo].", 1);
1635
1799
  }
1636
1800
  return runInteractiveControlPlaneInit(context, await loadClackPrompts());
1637
1801
  }
1638
1802
  export {
1639
1803
  runInteractiveControlPlaneInit,
1804
+ runDemoInit,
1640
1805
  executeInit,
1641
- buildRigInitConfigSource
1806
+ buildRigInitConfigSource,
1807
+ DEMO_TASKS_RELATIVE_DIR,
1808
+ DEMO_TASKS
1642
1809
  };
@@ -5,10 +5,19 @@ import { resolve as resolve3 } from "path";
5
5
 
6
6
  // packages/cli/src/runner.ts
7
7
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
8
- import { CliError } from "@rig/runtime/control-plane/errors";
8
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
9
9
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
10
10
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
11
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
11
+
12
+ class CliError extends RuntimeCliError {
13
+ hint;
14
+ constructor(message, exitCode = 1, options = {}) {
15
+ super(message, exitCode);
16
+ if (options.hint?.trim()) {
17
+ this.hint = options.hint.trim();
18
+ }
19
+ }
20
+ }
12
21
  function takeOption(args, option) {
13
22
  const rest = [];
14
23
  let value;
@@ -17,7 +26,7 @@ function takeOption(args, option) {
17
26
  if (current === option) {
18
27
  const next = args[index + 1];
19
28
  if (!next || next.startsWith("-")) {
20
- throw new CliError(`Missing value for ${option}`);
29
+ throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
21
30
  }
22
31
  value = next;
23
32
  index += 1;
@@ -81,7 +90,7 @@ function readJsonFile(path) {
81
90
  try {
82
91
  return JSON.parse(readFileSync(path, "utf8"));
83
92
  } catch (error) {
84
- throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
93
+ 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>`." });
85
94
  }
86
95
  }
87
96
  function writeJsonFile(path, value) {
@@ -145,7 +154,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
145
154
  const global = readGlobalConnections(options);
146
155
  const connection = global.connections[repo.selected];
147
156
  if (!connection) {
148
- throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
157
+ throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
149
158
  }
150
159
  return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
151
160
  }
@@ -218,7 +227,7 @@ async function ensureServerForCli(projectRoot) {
218
227
  };
219
228
  } catch (error) {
220
229
  if (error instanceof Error) {
221
- throw new CliError2(error.message, 1);
230
+ throw new CliError(error.message, 1);
222
231
  }
223
232
  throw error;
224
233
  }
@@ -268,15 +277,64 @@ function diagnosticMessage(payload) {
268
277
  });
269
278
  return messages.length > 0 ? messages.join("; ") : null;
270
279
  }
280
+ var serverReachabilityCache = new Map;
281
+ async function probeServerReachability(baseUrl, authToken) {
282
+ try {
283
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
284
+ headers: mergeHeaders(undefined, authToken),
285
+ signal: AbortSignal.timeout(1500)
286
+ });
287
+ return response.ok;
288
+ } catch {
289
+ return false;
290
+ }
291
+ }
292
+ function cachedServerReachability(projectRoot, baseUrl, authToken) {
293
+ const key = resolve2(projectRoot);
294
+ const cached = serverReachabilityCache.get(key);
295
+ if (cached)
296
+ return cached;
297
+ const probe = probeServerReachability(baseUrl, authToken);
298
+ serverReachabilityCache.set(key, probe);
299
+ return probe;
300
+ }
301
+ function describeSelectedServer(projectRoot, server) {
302
+ try {
303
+ const selected = resolveSelectedConnection(projectRoot);
304
+ if (selected) {
305
+ return {
306
+ alias: selected.alias,
307
+ target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
308
+ };
309
+ }
310
+ } catch {}
311
+ return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
312
+ }
313
+ async function buildServerFailureContext(projectRoot, server) {
314
+ const { alias, target } = describeSelectedServer(projectRoot, server);
315
+ const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
316
+ const reachability = reachable ? "server is reachable" : "server is unreachable";
317
+ return {
318
+ contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
319
+ hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
320
+ };
321
+ }
271
322
  async function requestServerJson(context, pathname, init = {}) {
272
323
  const server = await ensureServerForCli(context.projectRoot);
273
324
  const headers = mergeHeaders(init.headers, server.authToken);
274
325
  if (server.serverProjectRoot)
275
326
  headers.set("x-rig-project-root", server.serverProjectRoot);
276
- const response = await fetch(`${server.baseUrl}${pathname}`, {
277
- ...init,
278
- headers
279
- });
327
+ let response;
328
+ try {
329
+ response = await fetch(`${server.baseUrl}${pathname}`, {
330
+ ...init,
331
+ headers
332
+ });
333
+ } catch (error) {
334
+ const failure = await buildServerFailureContext(context.projectRoot, server);
335
+ throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
336
+ ${failure.contextLine}`, 1, { hint: failure.hint });
337
+ }
280
338
  const text = await response.text();
281
339
  const payload = text.trim().length > 0 ? (() => {
282
340
  try {
@@ -288,7 +346,9 @@ async function requestServerJson(context, pathname, init = {}) {
288
346
  if (!response.ok) {
289
347
  const diagnostics = diagnosticMessage(payload);
290
348
  const detail = diagnostics ?? (text || response.statusText);
291
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
349
+ const failure = await buildServerFailureContext(context.projectRoot, server);
350
+ throw new CliError(`Rig server request failed (${response.status}): ${detail}
351
+ ${failure.contextLine}`, 1, { hint: failure.hint });
292
352
  }
293
353
  return payload;
294
354
  }
@@ -323,7 +383,7 @@ async function executeInspect(context, args) {
323
383
  const serverRuns = await listRunsViaServer(context, { limit: 200 }).catch(() => []);
324
384
  const serverRun = serverRuns.filter((run) => String(run.taskId ?? "") === requiredTask).sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")))[0];
325
385
  if (!serverRun || typeof serverRun.runId !== "string") {
326
- throw new CliError2(`No runs found for ${requiredTask} (local or on the selected server).`);
386
+ throw new CliError(`No runs found for ${requiredTask} (local or on the selected server).`, 1, { hint: "Start one with `rig task run --task <id>`, or check `rig run list`." });
327
387
  }
328
388
  const page = await getRunLogsViaServer(context, serverRun.runId, { limit: 500 });
329
389
  const entries = Array.isArray(page.entries) ? page.entries : [];
@@ -341,7 +401,7 @@ async function executeInspect(context, args) {
341
401
  }
342
402
  const logsPath = resolve3(resolveAuthorityRunDir(context.projectRoot, latestRun.runId), "logs.jsonl");
343
403
  if (!existsSync3(logsPath)) {
344
- throw new CliError2(`No logs found for run ${latestRun.runId}.`);
404
+ throw new CliError(`No logs found for run ${latestRun.runId}.`, 1, { hint: `Try \`rig run show ${latestRun.runId}\` or \`rig run replay ${latestRun.runId}\`.` });
345
405
  }
346
406
  await context.runCommand(["cat", logsPath]);
347
407
  return { ok: true, group: "inspect", command, details: { task: requiredTask, runId: latestRun.runId } };
@@ -352,7 +412,7 @@ async function executeInspect(context, args) {
352
412
  const requiredTask = requireTask(task, "rig inspect artifacts --task <task-id>");
353
413
  const artifactRoot = resolveTaskArtifactDirs(context.projectRoot, requiredTask).find((path) => existsSync3(path));
354
414
  if (!artifactRoot) {
355
- throw new CliError2(`No artifacts found for ${requiredTask}.`);
415
+ throw new CliError(`No artifacts found for ${requiredTask}.`, 1, { hint: "Artifacts appear after a run completes; check `rig run list` for run state." });
356
416
  }
357
417
  await context.runCommand(["ls", "-la", artifactRoot]);
358
418
  return { ok: true, group: "inspect", command, details: { task: requiredTask } };
@@ -366,7 +426,7 @@ async function executeInspect(context, args) {
366
426
  requireNoExtraArgs(previewPending, "rig inspect artifact --task <task-id> --file <name>");
367
427
  const requiredTask = requireTask(task.value, "rig inspect artifact --task <task-id> --file <name>");
368
428
  if (!file.value) {
369
- throw new CliError2("Missing --file for rig inspect artifact.");
429
+ throw new CliError("Missing --file for rig inspect artifact.", 1, { hint: "List artifact files first: `rig inspect artifacts --task <id>`, then pass one with --file." });
370
430
  }
371
431
  const preview = readTaskArtifactPreview(context.projectRoot, requiredTask, file.value);
372
432
  if (context.outputMode === "text") {
@@ -420,7 +480,7 @@ async function executeInspect(context, args) {
420
480
  const monorepoRoot = resolveMonorepoRoot(context.projectRoot);
421
481
  const result = runCapture(["br", "--no-db", "list", "--pretty"], monorepoRoot);
422
482
  if (result.exitCode !== 0) {
423
- throw new CliError2(result.stderr || result.stdout || "Failed to inspect graph");
483
+ throw new CliError(result.stderr || result.stdout || "Failed to inspect graph");
424
484
  }
425
485
  process.stdout.write(result.stdout);
426
486
  }
@@ -439,7 +499,7 @@ async function executeInspect(context, args) {
439
499
  return { ok: true, group: "inspect", command };
440
500
  }
441
501
  default:
442
- throw new CliError2(`Unknown inspect command: ${command}`);
502
+ throw new CliError(`Unknown inspect command: ${command}`, 1, { hint: "Run `rig inspect --help` to list inspect commands." });
443
503
  }
444
504
  }
445
505
  export {