@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
@@ -7,12 +7,19 @@ import { resolve as resolve6 } from "path";
7
7
 
8
8
  // packages/cli/src/runner.ts
9
9
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
10
- import { CliError } from "@rig/runtime/control-plane/errors";
10
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
11
11
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
12
- import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
13
- import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
14
12
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
15
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
13
+
14
+ class CliError extends RuntimeCliError {
15
+ hint;
16
+ constructor(message, exitCode = 1, options = {}) {
17
+ super(message, exitCode);
18
+ if (options.hint?.trim()) {
19
+ this.hint = options.hint.trim();
20
+ }
21
+ }
22
+ }
16
23
  function requireNoExtraArgs(args, usage) {
17
24
  if (args.length > 0) {
18
25
  throw new CliError(`Unexpected arguments: ${args.join(" ")}
@@ -102,7 +109,8 @@ function resolveControlPlaneDefinitionRoot(projectRoot) {
102
109
  import { existsSync, readFileSync, rmSync } from "fs";
103
110
  import { homedir } from "os";
104
111
  import { resolve as resolve2 } from "path";
105
- var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
112
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
113
+ var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
106
114
  async function defaultCommandRunner(command, options = {}) {
107
115
  const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
108
116
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -121,7 +129,7 @@ function resolvePiHomeDir(inputHomeDir) {
121
129
  function piListContainsPiRig(output) {
122
130
  return output.split(/\r?\n/).some((line) => {
123
131
  const normalized = line.trim();
124
- return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
132
+ return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
125
133
  });
126
134
  }
127
135
  async function safeRun(runner, command, options) {
@@ -197,9 +205,14 @@ function readJsonFile(path) {
197
205
  try {
198
206
  return JSON.parse(readFileSync2(path, "utf8"));
199
207
  } catch (error) {
200
- throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
208
+ 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>`." });
201
209
  }
202
210
  }
211
+ function writeJsonFile(path, value) {
212
+ mkdirSync(dirname(path), { recursive: true });
213
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
214
+ `, "utf8");
215
+ }
203
216
  function normalizeConnection(value) {
204
217
  if (!value || typeof value !== "object" || Array.isArray(value))
205
218
  return null;
@@ -240,29 +253,47 @@ function readRepoConnection(projectRoot) {
240
253
  return {
241
254
  selected,
242
255
  project: typeof record.project === "string" ? record.project : undefined,
243
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
256
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
257
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
244
258
  };
245
259
  }
260
+ function writeRepoConnection(projectRoot, state) {
261
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
262
+ }
246
263
  function resolveSelectedConnection(projectRoot, options = {}) {
247
264
  const repo = readRepoConnection(projectRoot);
248
265
  if (!repo)
249
266
  return null;
250
267
  if (repo.selected === "local")
251
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
268
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
252
269
  const global = readGlobalConnections(options);
253
270
  const connection = global.connections[repo.selected];
254
271
  if (!connection) {
255
- throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
272
+ throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
256
273
  }
257
- return { alias: repo.selected, connection };
274
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
275
+ }
276
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
277
+ const repo = readRepoConnection(projectRoot);
278
+ if (!repo)
279
+ return;
280
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
258
281
  }
259
282
 
260
283
  // packages/cli/src/commands/_server-client.ts
261
- import { spawnSync } from "child_process";
262
284
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
263
285
  import { resolve as resolve4 } from "path";
264
286
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
265
- var cachedGitHubBearerToken;
287
+ var scopedGitHubBearerTokens = new Map;
288
+ var serverPhaseListener = null;
289
+ function setServerPhaseListener(listener) {
290
+ const previous = serverPhaseListener;
291
+ serverPhaseListener = listener;
292
+ return previous;
293
+ }
294
+ function reportServerPhase(label) {
295
+ serverPhaseListener?.(label);
296
+ }
266
297
  function cleanToken(value) {
267
298
  const trimmed = value?.trim();
268
299
  return trimmed ? trimmed : null;
@@ -279,49 +310,80 @@ function readPrivateRemoteSessionToken(projectRoot) {
279
310
  }
280
311
  }
281
312
  function readGitHubBearerTokenForRemote(projectRoot) {
282
- if (cachedGitHubBearerToken !== undefined)
283
- return cachedGitHubBearerToken;
313
+ const scopedKey = resolve4(projectRoot);
314
+ if (scopedGitHubBearerTokens.has(scopedKey))
315
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
284
316
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
285
- if (privateSession) {
286
- cachedGitHubBearerToken = privateSession;
287
- return cachedGitHubBearerToken;
288
- }
289
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
290
- if (envToken) {
291
- cachedGitHubBearerToken = envToken;
292
- return cachedGitHubBearerToken;
293
- }
294
- const result = spawnSync("gh", ["auth", "token"], {
295
- encoding: "utf8",
296
- timeout: 5000,
297
- stdio: ["ignore", "pipe", "ignore"]
298
- });
299
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
300
- return cachedGitHubBearerToken;
317
+ if (privateSession)
318
+ return privateSession;
319
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
320
+ }
321
+ function readStoredGitHubAuthToken(projectRoot) {
322
+ const path = resolve4(projectRoot, ".rig", "state", "github-auth.json");
323
+ if (!existsSync3(path))
324
+ return null;
325
+ try {
326
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
327
+ return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+ function readLocalConnectionFallbackToken(projectRoot) {
333
+ return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
301
334
  }
302
335
  async function ensureServerForCli(projectRoot) {
303
336
  try {
304
337
  const selected = resolveSelectedConnection(projectRoot);
305
338
  if (selected?.connection.kind === "remote") {
339
+ reportServerPhase(`Connecting to ${selected.alias}\u2026`);
340
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
341
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
306
342
  return {
307
343
  baseUrl: selected.connection.baseUrl,
308
- authToken: readGitHubBearerTokenForRemote(projectRoot),
309
- connectionKind: "remote"
344
+ authToken,
345
+ connectionKind: "remote",
346
+ serverProjectRoot
310
347
  };
311
348
  }
349
+ reportServerPhase("Starting local Rig server\u2026");
312
350
  const connection = await ensureLocalRigServerConnection(projectRoot);
313
351
  return {
314
352
  baseUrl: connection.baseUrl,
315
- authToken: connection.authToken,
316
- connectionKind: "local"
353
+ authToken: connection.authToken ?? readLocalConnectionFallbackToken(projectRoot),
354
+ connectionKind: "local",
355
+ serverProjectRoot: resolve4(projectRoot)
317
356
  };
318
357
  } catch (error) {
319
358
  if (error instanceof Error) {
320
- throw new CliError2(error.message, 1);
359
+ throw new CliError(error.message, 1);
321
360
  }
322
361
  throw error;
323
362
  }
324
363
  }
364
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
365
+ const repo = readRepoConnection(projectRoot);
366
+ const slug = repo?.project?.trim();
367
+ if (!slug)
368
+ return null;
369
+ try {
370
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
371
+ headers: mergeHeaders(undefined, authToken)
372
+ });
373
+ if (!response.ok)
374
+ return null;
375
+ const payload = await response.json();
376
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
377
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
378
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
379
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
380
+ if (path)
381
+ writeRepoServerProjectRoot(projectRoot, path);
382
+ return path;
383
+ } catch {
384
+ return null;
385
+ }
386
+ }
325
387
  function mergeHeaders(headers, authToken) {
326
388
  const merged = new Headers(headers);
327
389
  if (authToken) {
@@ -344,12 +406,65 @@ function diagnosticMessage(payload) {
344
406
  });
345
407
  return messages.length > 0 ? messages.join("; ") : null;
346
408
  }
409
+ var serverReachabilityCache = new Map;
410
+ async function probeServerReachability(baseUrl, authToken) {
411
+ try {
412
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
413
+ headers: mergeHeaders(undefined, authToken),
414
+ signal: AbortSignal.timeout(1500)
415
+ });
416
+ return response.ok;
417
+ } catch {
418
+ return false;
419
+ }
420
+ }
421
+ function cachedServerReachability(projectRoot, baseUrl, authToken) {
422
+ const key = resolve4(projectRoot);
423
+ const cached = serverReachabilityCache.get(key);
424
+ if (cached)
425
+ return cached;
426
+ const probe = probeServerReachability(baseUrl, authToken);
427
+ serverReachabilityCache.set(key, probe);
428
+ return probe;
429
+ }
430
+ function describeSelectedServer(projectRoot, server) {
431
+ try {
432
+ const selected = resolveSelectedConnection(projectRoot);
433
+ if (selected) {
434
+ return {
435
+ alias: selected.alias,
436
+ target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
437
+ };
438
+ }
439
+ } catch {}
440
+ return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
441
+ }
442
+ async function buildServerFailureContext(projectRoot, server) {
443
+ const { alias, target } = describeSelectedServer(projectRoot, server);
444
+ const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
445
+ const reachability = reachable ? "server is reachable" : "server is unreachable";
446
+ return {
447
+ contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
448
+ hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
449
+ };
450
+ }
347
451
  async function requestServerJson(context, pathname, init = {}) {
348
452
  const server = await ensureServerForCli(context.projectRoot);
349
- const response = await fetch(`${server.baseUrl}${pathname}`, {
350
- ...init,
351
- headers: mergeHeaders(init.headers, server.authToken)
352
- });
453
+ const headers = mergeHeaders(init.headers, server.authToken);
454
+ if (server.serverProjectRoot)
455
+ headers.set("x-rig-project-root", server.serverProjectRoot);
456
+ reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
457
+ let response;
458
+ try {
459
+ response = await fetch(`${server.baseUrl}${pathname}`, {
460
+ ...init,
461
+ headers
462
+ });
463
+ } catch (error) {
464
+ const failure = await buildServerFailureContext(context.projectRoot, server);
465
+ throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
466
+ ${failure.contextLine}`, 1, { hint: failure.hint });
467
+ }
353
468
  const text = await response.text();
354
469
  const payload = text.trim().length > 0 ? (() => {
355
470
  try {
@@ -361,10 +476,23 @@ async function requestServerJson(context, pathname, init = {}) {
361
476
  if (!response.ok) {
362
477
  const diagnostics = diagnosticMessage(payload);
363
478
  const detail = diagnostics ?? (text || response.statusText);
364
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
479
+ const failure = await buildServerFailureContext(context.projectRoot, server);
480
+ throw new CliError(`Rig server request failed (${response.status}): ${detail}
481
+ ${failure.contextLine}`, 1, { hint: failure.hint });
365
482
  }
366
483
  return payload;
367
484
  }
485
+ var RESUMABLE_RUN_STATUSES = new Set([
486
+ "created",
487
+ "preparing",
488
+ "running",
489
+ "validating",
490
+ "reviewing",
491
+ "stopped",
492
+ "failed",
493
+ "needs-attention",
494
+ "needs_attention"
495
+ ]);
368
496
 
369
497
  // packages/cli/src/commands/_doctor-checks.ts
370
498
  function check(id, label, status, detail, remediation) {
@@ -485,7 +613,10 @@ async function runRigDoctorChecks(options) {
485
613
  const bunVersion = options.bunVersion ?? Bun.version;
486
614
  const request = options.requestJson ?? ((pathname, init) => requestServerJson({ projectRoot }, pathname, init));
487
615
  const loadConfig = options.loadConfig ?? loadRigConfigOrNull;
616
+ const progress = options.onProgress ?? (() => {});
617
+ progress("Checking local toolchain\u2026");
488
618
  checks.push(check("bun", `bun >= ${MIN_SUPPORTED_BUN_VERSION}`, isSupportedBunVersion(bunVersion) ? "pass" : "fail", `found ${bunVersion}`, `Install Bun ${MIN_SUPPORTED_BUN_VERSION} or newer.`), check("git", "git", which("git") ? "pass" : "fail", which("git") ?? undefined, "Install git and ensure it is on PATH."), check("jq", "jq", which("jq") ? "pass" : "warn", which("jq") ?? undefined, "Install jq (for example `brew install jq`)."));
619
+ progress("Loading rig.config\u2026");
489
620
  const loadedConfig = await loadConfig(projectRoot).catch(() => null);
490
621
  const config = loadedConfig ?? loadFallbackConfig(projectRoot);
491
622
  const hasConfigFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync4(resolve5(projectRoot, name)));
@@ -493,7 +624,7 @@ async function runRigDoctorChecks(options) {
493
624
  const taskSourceKind = config?.taskSource?.kind;
494
625
  checks.push(taskSourceKind ? check("task-source", "task source configured", "pass", taskSourceKind) : check("task-source", "task source configured", "fail", "missing taskSource", "Configure taskSource in rig.config.ts."));
495
626
  const repo = readRepoConnection(projectRoot);
496
- checks.push(repo ? check("project-link", "repo selected Rig connection", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to link this checkout to a GitHub repo slug.") : check("project-link", "repo selected Rig connection", "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig connect use <alias|local>`."));
627
+ checks.push(repo ? check("project-link", "repo selected Rig server", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to link this checkout to a GitHub repo slug.") : check("project-link", "repo selected Rig server", "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig server use <alias|local>`."));
497
628
  const selected = (() => {
498
629
  try {
499
630
  return resolveSelectedConnection(projectRoot);
@@ -501,9 +632,10 @@ async function runRigDoctorChecks(options) {
501
632
  return null;
502
633
  }
503
634
  })();
504
- checks.push(selected ? check("connection", "selected server connection", "pass", selected.connection.kind === "remote" ? selected.connection.baseUrl : "local auto") : check("connection", "selected server connection", repo ? "fail" : "warn", repo ? "selected alias is missing" : "will auto-start local server", repo ? "Run `rig connect list` and `rig connect use <alias|local>`." : undefined));
635
+ checks.push(selected ? check("connection", "selected server connection", "pass", selected.connection.kind === "remote" ? selected.connection.baseUrl : "local auto") : check("connection", "selected server", repo ? "fail" : "warn", repo ? "selected alias is missing" : "will auto-start local server", repo ? "Run `rig server list` and `rig server use <alias|local>`." : undefined));
505
636
  let server = null;
506
637
  try {
638
+ progress("Connecting to the selected Rig server\u2026");
507
639
  server = await (options.resolveServer ?? ensureServerForCli)(projectRoot);
508
640
  checks.push(check("server", "Rig server reachable", "pass", `${server.connectionKind} ${server.baseUrl}`));
509
641
  } catch (error) {
@@ -511,18 +643,21 @@ async function runRigDoctorChecks(options) {
511
643
  }
512
644
  if (server || options.requestJson) {
513
645
  try {
646
+ progress("Checking server status\u2026");
514
647
  const status = await request("/api/server/status");
515
648
  checks.push(check("server-status", "server project status", "pass", JSON.stringify(status).slice(0, 180)));
516
649
  } catch (error) {
517
650
  checks.push(check("server-status", "server project status", "fail", errorMessage(error), "Run `rig doctor` after the selected server is reachable."));
518
651
  }
519
652
  try {
653
+ progress("Checking GitHub auth\u2026");
520
654
  const auth = await request("/api/github/auth/status");
521
655
  checks.push(isAuthenticated(auth) ? check("github-auth", "GitHub auth", "pass") : check("github-auth", "GitHub auth", "fail", "not authenticated", "Run `rig github auth import-gh` or `rig github auth token --token <token>`."));
522
656
  } catch (error) {
523
657
  checks.push(check("github-auth", "GitHub auth", "fail", errorMessage(error), "Authenticate GitHub through Rig and ensure the server exposes auth status."));
524
658
  }
525
659
  try {
660
+ progress("Checking GitHub repo permissions\u2026");
526
661
  const permissions = await request("/api/github/repo/permissions");
527
662
  const allowed = permissionAllowsPr(permissions);
528
663
  checks.push(allowed === true ? check("github-repo-permissions", "GitHub repo PR permissions", "pass", JSON.stringify(permissions).slice(0, 180)) : allowed === false ? check("github-repo-permissions", "GitHub repo PR permissions", "fail", JSON.stringify(permissions).slice(0, 180), "Grant the selected GitHub token permission to push branches, open PRs, and merge according to repo rules.") : check("github-repo-permissions", "GitHub repo PR permissions", "warn", JSON.stringify(permissions).slice(0, 180), "Confirm the selected token can push branches and open PRs."));
@@ -530,6 +665,7 @@ async function runRigDoctorChecks(options) {
530
665
  checks.push(check("github-repo-permissions", "GitHub repo PR permissions", "warn", errorMessage(error), "Ensure the server exposes repo permission checks and the token can open PRs."));
531
666
  }
532
667
  try {
668
+ progress("Checking GitHub issue labels\u2026");
533
669
  const labels = await request("/api/workspace/task-labels");
534
670
  const ready = labelsReady(labels);
535
671
  checks.push(ready === false ? check("task-labels", "GitHub issue labels", "fail", JSON.stringify(labels).slice(0, 180), "Let Rig create required labels or create the configured lifecycle labels manually.") : check("task-labels", "GitHub issue labels", ready === true ? "pass" : "warn", JSON.stringify(labels).slice(0, 180), "Confirm required Rig lifecycle labels exist."));
@@ -537,6 +673,7 @@ async function runRigDoctorChecks(options) {
537
673
  checks.push(check("task-labels", "GitHub issue labels", "warn", errorMessage(error), "Run `rig init`/`rig doctor` after label setup is wired on the server."));
538
674
  }
539
675
  try {
676
+ progress("Checking task projection\u2026");
540
677
  const projection = await request("/api/workspace/task-projection");
541
678
  checks.push(check("task-projection", "task projection", "pass", JSON.stringify(projection).slice(0, 180)));
542
679
  } catch (error) {
@@ -545,6 +682,7 @@ async function runRigDoctorChecks(options) {
545
682
  const slug = projectStatusSlug(projectRoot, config);
546
683
  if (slug) {
547
684
  try {
685
+ progress("Checking server project checkout\u2026");
548
686
  const project = await request(`/api/projects/${encodeURIComponent(slug)}`);
549
687
  checks.push(check("remote-checkout", "server project checkout", "pass", JSON.stringify(project).slice(0, 180)));
550
688
  } catch (error) {
@@ -559,6 +697,7 @@ async function runRigDoctorChecks(options) {
559
697
  }
560
698
  checks.push(githubProjectsCheck(config));
561
699
  checks.push(prMergeCheck(config));
700
+ progress("Checking Pi installation\u2026");
562
701
  const piChecks = await (options.piChecks ?? (() => buildPiSetupChecks()))().catch((error) => [{
563
702
  ok: false,
564
703
  label: "pi/pi-rig checks",
@@ -582,12 +721,133 @@ function countDoctorFailures(checks) {
582
721
  return checks.filter((entry) => entry.status === "fail").length;
583
722
  }
584
723
 
724
+ // packages/cli/src/commands/_async-ui.ts
725
+ import pc from "picocolors";
726
+
727
+ // packages/cli/src/commands/_spinner.ts
728
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
729
+ function createTtySpinner(input) {
730
+ const output = input.output ?? process.stdout;
731
+ const isTty = output.isTTY === true;
732
+ const frames = input.frames && input.frames.length > 0 ? input.frames : SPINNER_FRAMES;
733
+ let label = input.label;
734
+ let frame = 0;
735
+ let paused = false;
736
+ let stopped = false;
737
+ let lastPrintedLabel = "";
738
+ const render = () => {
739
+ if (stopped || paused)
740
+ return;
741
+ if (!isTty) {
742
+ if (label !== lastPrintedLabel) {
743
+ output.write(`${label}
744
+ `);
745
+ lastPrintedLabel = label;
746
+ }
747
+ return;
748
+ }
749
+ frame = (frame + 1) % frames.length;
750
+ const glyph = frames[frame] ?? frames[0] ?? "";
751
+ output.write(`\r\x1B[2K${input.styleFrame ? input.styleFrame(glyph) : glyph} ${label}`);
752
+ };
753
+ const clearLine = () => {
754
+ if (isTty)
755
+ output.write("\r\x1B[2K");
756
+ };
757
+ render();
758
+ const timer = isTty ? setInterval(render, input.intervalMs ?? 120) : null;
759
+ return {
760
+ setLabel(next) {
761
+ label = next;
762
+ render();
763
+ },
764
+ pause() {
765
+ paused = true;
766
+ clearLine();
767
+ },
768
+ resume() {
769
+ if (stopped)
770
+ return;
771
+ paused = false;
772
+ render();
773
+ },
774
+ stop(finalLine) {
775
+ if (stopped)
776
+ return;
777
+ stopped = true;
778
+ if (timer)
779
+ clearInterval(timer);
780
+ clearLine();
781
+ if (finalLine)
782
+ output.write(`${finalLine}
783
+ `);
784
+ }
785
+ };
786
+ }
787
+
788
+ // packages/cli/src/commands/_async-ui.ts
789
+ var CLACK_SPINNER_FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
790
+ var DONE_SYMBOL = pc.green("\u25C7");
791
+ var FAIL_SYMBOL = pc.red("\u25A0");
792
+ var activeUpdate = null;
793
+ async function withSpinner(label, work, options = {}) {
794
+ if (options.outputMode === "json") {
795
+ return work(() => {});
796
+ }
797
+ if (activeUpdate) {
798
+ const outer = activeUpdate;
799
+ outer(label);
800
+ return work(outer);
801
+ }
802
+ const output = options.output ?? process.stderr;
803
+ const isTty = output.isTTY === true;
804
+ let lastLabel = label;
805
+ if (!isTty) {
806
+ output.write(`${label}
807
+ `);
808
+ const update2 = (next) => {
809
+ lastLabel = next;
810
+ };
811
+ activeUpdate = update2;
812
+ const previousListener2 = setServerPhaseListener(update2);
813
+ try {
814
+ return await work(update2);
815
+ } finally {
816
+ activeUpdate = null;
817
+ setServerPhaseListener(previousListener2);
818
+ }
819
+ }
820
+ const spinner = createTtySpinner({
821
+ label,
822
+ output,
823
+ frames: CLACK_SPINNER_FRAMES,
824
+ styleFrame: (frame) => pc.magenta(frame)
825
+ });
826
+ const update = (next) => {
827
+ lastLabel = next;
828
+ spinner.setLabel(next);
829
+ };
830
+ activeUpdate = update;
831
+ const previousListener = setServerPhaseListener(update);
832
+ try {
833
+ const result = await work(update);
834
+ spinner.stop(options.doneLabel ? `${DONE_SYMBOL} ${options.doneLabel}` : undefined);
835
+ return result;
836
+ } catch (error) {
837
+ spinner.stop(`${FAIL_SYMBOL} ${lastLabel}`);
838
+ throw error;
839
+ } finally {
840
+ activeUpdate = null;
841
+ setServerPhaseListener(previousListener);
842
+ }
843
+ }
844
+
585
845
  // packages/cli/src/commands/setup.ts
586
846
  async function executeSetup(context, args) {
587
847
  const [command = "check", ...rest] = args;
588
848
  switch (command) {
589
849
  case "bootstrap":
590
- requireNoExtraArgs(rest, "bun run rig setup bootstrap");
850
+ requireNoExtraArgs(rest, "rig setup bootstrap");
591
851
  {
592
852
  const hostBash = Bun.which("bash") || "/bin/bash";
593
853
  const env = { ...process.env };
@@ -605,32 +865,26 @@ async function executeSetup(context, args) {
605
865
  });
606
866
  const exitCode = await proc.exited;
607
867
  if (exitCode !== 0) {
608
- throw new CliError2(`Command failed (${exitCode}): ${hostBash} ./bootstrap.sh`, exitCode);
868
+ throw new CliError(`Command failed (${exitCode}): ${hostBash} ./bootstrap.sh`, exitCode);
609
869
  }
610
870
  }
611
871
  return { ok: true, group: "setup", command };
612
872
  case "check":
613
- requireNoExtraArgs(rest, `bun run rig setup ${command}`);
873
+ requireNoExtraArgs(rest, `rig setup ${command}`);
614
874
  {
615
- const checks = await withMutedConsole(context.outputMode === "json", () => runSetupCheck(context.projectRoot));
875
+ const checks = await withMutedConsole(context.outputMode === "json", () => runSetupCheck(context.projectRoot, context.outputMode));
616
876
  return { ok: true, group: "setup", command, details: { checks, failures: countDoctorFailures(checks) } };
617
877
  }
618
878
  case "setup":
619
- requireNoExtraArgs(rest, "bun run rig setup setup");
879
+ requireNoExtraArgs(rest, "rig setup setup");
620
880
  withMutedConsole(context.outputMode === "json", () => runSetupInit(context.projectRoot));
621
881
  return { ok: true, group: "setup", command };
622
882
  case "preflight":
623
- requireNoExtraArgs(rest, "bun run rig setup preflight");
624
- await withMutedConsole(context.outputMode === "json", () => runSetupPreflight(context.projectRoot));
625
- return { ok: true, group: "setup", command };
626
- case "install-agent-shell":
627
- requireNoExtraArgs(rest, "bun run rig setup install-agent-shell");
628
- if (context.outputMode === "text") {
629
- console.log("install-agent-shell is deprecated. Runtime shells now use compiled rig-agent directly.");
630
- }
883
+ requireNoExtraArgs(rest, "rig setup preflight");
884
+ await withMutedConsole(context.outputMode === "json", () => runSetupPreflight(context.projectRoot, context.outputMode));
631
885
  return { ok: true, group: "setup", command };
632
886
  default:
633
- throw new CliError2(`Unknown setup command: ${command}`);
887
+ throw new CliError(`Unknown setup command: ${command}`, 1, { hint: "Run `rig setup --help` \u2014 commands are bootstrap|check|preflight." });
634
888
  }
635
889
  }
636
890
  function formatTaskSourceKinds(kinds) {
@@ -664,17 +918,17 @@ function runSetupInit(projectRoot) {
664
918
  }
665
919
  console.log("Harness directories ready.");
666
920
  }
667
- async function runSetupCheck(projectRoot) {
668
- const doctorChecks = await runRigDoctorChecks({ projectRoot });
921
+ async function runSetupCheck(projectRoot, outputMode = "text") {
922
+ const doctorChecks = await withSpinner("Running setup checks\u2026", (update) => runRigDoctorChecks({ projectRoot, onProgress: update }), { outputMode });
669
923
  console.log(formatDoctorChecks(doctorChecks));
670
924
  const failures = countDoctorFailures(doctorChecks);
671
925
  if (failures > 0) {
672
- throw new CliError2(`Setup check failed (${failures} failing doctor check${failures === 1 ? "" : "s"}).`, 1);
926
+ throw new CliError(`Setup check failed (${failures} failing doctor check${failures === 1 ? "" : "s"}).`, 1, { hint: "Run `rig doctor` for the full check list with remediations." });
673
927
  }
674
928
  return doctorChecks;
675
929
  }
676
- async function runSetupPreflight(projectRoot) {
677
- await runSetupCheck(projectRoot);
930
+ async function runSetupPreflight(projectRoot, outputMode = "text") {
931
+ await runSetupCheck(projectRoot, outputMode);
678
932
  const validationRoot = resolve6(resolveControlPlaneDefinitionRoot(projectRoot), "validation");
679
933
  if (existsSync5(validationRoot)) {
680
934
  const validators = readdirSync(validationRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory());