@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
@@ -3,12 +3,19 @@ var __require = import.meta.require;
3
3
 
4
4
  // packages/cli/src/runner.ts
5
5
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
6
- import { CliError } from "@rig/runtime/control-plane/errors";
6
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
7
7
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
8
- import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
9
- import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
10
8
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
11
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
9
+
10
+ class CliError extends RuntimeCliError {
11
+ hint;
12
+ constructor(message, exitCode = 1, options = {}) {
13
+ super(message, exitCode);
14
+ if (options.hint?.trim()) {
15
+ this.hint = options.hint.trim();
16
+ }
17
+ }
18
+ }
12
19
  function requireNoExtraArgs(args, usage) {
13
20
  if (args.length > 0) {
14
21
  throw new CliError(`Unexpected arguments: ${args.join(" ")}
@@ -43,9 +50,14 @@ function readJsonFile(path) {
43
50
  try {
44
51
  return JSON.parse(readFileSync(path, "utf8"));
45
52
  } catch (error) {
46
- throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
53
+ 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>`." });
47
54
  }
48
55
  }
56
+ function writeJsonFile(path, value) {
57
+ mkdirSync(dirname(path), { recursive: true });
58
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
59
+ `, "utf8");
60
+ }
49
61
  function normalizeConnection(value) {
50
62
  if (!value || typeof value !== "object" || Array.isArray(value))
51
63
  return null;
@@ -86,29 +98,47 @@ function readRepoConnection(projectRoot) {
86
98
  return {
87
99
  selected,
88
100
  project: typeof record.project === "string" ? record.project : undefined,
89
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
101
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
102
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
90
103
  };
91
104
  }
105
+ function writeRepoConnection(projectRoot, state) {
106
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
107
+ }
92
108
  function resolveSelectedConnection(projectRoot, options = {}) {
93
109
  const repo = readRepoConnection(projectRoot);
94
110
  if (!repo)
95
111
  return null;
96
112
  if (repo.selected === "local")
97
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
113
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
98
114
  const global = readGlobalConnections(options);
99
115
  const connection = global.connections[repo.selected];
100
116
  if (!connection) {
101
- throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
117
+ throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
102
118
  }
103
- return { alias: repo.selected, connection };
119
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
120
+ }
121
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
122
+ const repo = readRepoConnection(projectRoot);
123
+ if (!repo)
124
+ return;
125
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
104
126
  }
105
127
 
106
128
  // packages/cli/src/commands/_server-client.ts
107
- import { spawnSync } from "child_process";
108
129
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
109
130
  import { resolve as resolve2 } from "path";
110
131
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
111
- var cachedGitHubBearerToken;
132
+ var scopedGitHubBearerTokens = new Map;
133
+ var serverPhaseListener = null;
134
+ function setServerPhaseListener(listener) {
135
+ const previous = serverPhaseListener;
136
+ serverPhaseListener = listener;
137
+ return previous;
138
+ }
139
+ function reportServerPhase(label) {
140
+ serverPhaseListener?.(label);
141
+ }
112
142
  function cleanToken(value) {
113
143
  const trimmed = value?.trim();
114
144
  return trimmed ? trimmed : null;
@@ -125,49 +155,80 @@ function readPrivateRemoteSessionToken(projectRoot) {
125
155
  }
126
156
  }
127
157
  function readGitHubBearerTokenForRemote(projectRoot) {
128
- if (cachedGitHubBearerToken !== undefined)
129
- return cachedGitHubBearerToken;
158
+ const scopedKey = resolve2(projectRoot);
159
+ if (scopedGitHubBearerTokens.has(scopedKey))
160
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
130
161
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
131
- if (privateSession) {
132
- cachedGitHubBearerToken = privateSession;
133
- return cachedGitHubBearerToken;
134
- }
135
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
136
- if (envToken) {
137
- cachedGitHubBearerToken = envToken;
138
- return cachedGitHubBearerToken;
139
- }
140
- const result = spawnSync("gh", ["auth", "token"], {
141
- encoding: "utf8",
142
- timeout: 5000,
143
- stdio: ["ignore", "pipe", "ignore"]
144
- });
145
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
146
- return cachedGitHubBearerToken;
162
+ if (privateSession)
163
+ return privateSession;
164
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
165
+ }
166
+ function readStoredGitHubAuthToken(projectRoot) {
167
+ const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
168
+ if (!existsSync2(path))
169
+ return null;
170
+ try {
171
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
172
+ return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+ function readLocalConnectionFallbackToken(projectRoot) {
178
+ return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
147
179
  }
148
180
  async function ensureServerForCli(projectRoot) {
149
181
  try {
150
182
  const selected = resolveSelectedConnection(projectRoot);
151
183
  if (selected?.connection.kind === "remote") {
184
+ reportServerPhase(`Connecting to ${selected.alias}\u2026`);
185
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
186
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
152
187
  return {
153
188
  baseUrl: selected.connection.baseUrl,
154
- authToken: readGitHubBearerTokenForRemote(projectRoot),
155
- connectionKind: "remote"
189
+ authToken,
190
+ connectionKind: "remote",
191
+ serverProjectRoot
156
192
  };
157
193
  }
194
+ reportServerPhase("Starting local Rig server\u2026");
158
195
  const connection = await ensureLocalRigServerConnection(projectRoot);
159
196
  return {
160
197
  baseUrl: connection.baseUrl,
161
- authToken: connection.authToken,
162
- connectionKind: "local"
198
+ authToken: connection.authToken ?? readLocalConnectionFallbackToken(projectRoot),
199
+ connectionKind: "local",
200
+ serverProjectRoot: resolve2(projectRoot)
163
201
  };
164
202
  } catch (error) {
165
203
  if (error instanceof Error) {
166
- throw new CliError2(error.message, 1);
204
+ throw new CliError(error.message, 1);
167
205
  }
168
206
  throw error;
169
207
  }
170
208
  }
209
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
210
+ const repo = readRepoConnection(projectRoot);
211
+ const slug = repo?.project?.trim();
212
+ if (!slug)
213
+ return null;
214
+ try {
215
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
216
+ headers: mergeHeaders(undefined, authToken)
217
+ });
218
+ if (!response.ok)
219
+ return null;
220
+ const payload = await response.json();
221
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
222
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
223
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
224
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
225
+ if (path)
226
+ writeRepoServerProjectRoot(projectRoot, path);
227
+ return path;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
171
232
  function mergeHeaders(headers, authToken) {
172
233
  const merged = new Headers(headers);
173
234
  if (authToken) {
@@ -190,12 +251,65 @@ function diagnosticMessage(payload) {
190
251
  });
191
252
  return messages.length > 0 ? messages.join("; ") : null;
192
253
  }
254
+ var serverReachabilityCache = new Map;
255
+ async function probeServerReachability(baseUrl, authToken) {
256
+ try {
257
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
258
+ headers: mergeHeaders(undefined, authToken),
259
+ signal: AbortSignal.timeout(1500)
260
+ });
261
+ return response.ok;
262
+ } catch {
263
+ return false;
264
+ }
265
+ }
266
+ function cachedServerReachability(projectRoot, baseUrl, authToken) {
267
+ const key = resolve2(projectRoot);
268
+ const cached = serverReachabilityCache.get(key);
269
+ if (cached)
270
+ return cached;
271
+ const probe = probeServerReachability(baseUrl, authToken);
272
+ serverReachabilityCache.set(key, probe);
273
+ return probe;
274
+ }
275
+ function describeSelectedServer(projectRoot, server) {
276
+ try {
277
+ const selected = resolveSelectedConnection(projectRoot);
278
+ if (selected) {
279
+ return {
280
+ alias: selected.alias,
281
+ target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
282
+ };
283
+ }
284
+ } catch {}
285
+ return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
286
+ }
287
+ async function buildServerFailureContext(projectRoot, server) {
288
+ const { alias, target } = describeSelectedServer(projectRoot, server);
289
+ const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
290
+ const reachability = reachable ? "server is reachable" : "server is unreachable";
291
+ return {
292
+ contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
293
+ hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
294
+ };
295
+ }
193
296
  async function requestServerJson(context, pathname, init = {}) {
194
297
  const server = await ensureServerForCli(context.projectRoot);
195
- const response = await fetch(`${server.baseUrl}${pathname}`, {
196
- ...init,
197
- headers: mergeHeaders(init.headers, server.authToken)
198
- });
298
+ const headers = mergeHeaders(init.headers, server.authToken);
299
+ if (server.serverProjectRoot)
300
+ headers.set("x-rig-project-root", server.serverProjectRoot);
301
+ reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
302
+ let response;
303
+ try {
304
+ response = await fetch(`${server.baseUrl}${pathname}`, {
305
+ ...init,
306
+ headers
307
+ });
308
+ } catch (error) {
309
+ const failure = await buildServerFailureContext(context.projectRoot, server);
310
+ throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
311
+ ${failure.contextLine}`, 1, { hint: failure.hint });
312
+ }
199
313
  const text = await response.text();
200
314
  const payload = text.trim().length > 0 ? (() => {
201
315
  try {
@@ -207,10 +321,23 @@ async function requestServerJson(context, pathname, init = {}) {
207
321
  if (!response.ok) {
208
322
  const diagnostics = diagnosticMessage(payload);
209
323
  const detail = diagnostics ?? (text || response.statusText);
210
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
324
+ const failure = await buildServerFailureContext(context.projectRoot, server);
325
+ throw new CliError(`Rig server request failed (${response.status}): ${detail}
326
+ ${failure.contextLine}`, 1, { hint: failure.hint });
211
327
  }
212
328
  return payload;
213
329
  }
330
+ var RESUMABLE_RUN_STATUSES = new Set([
331
+ "created",
332
+ "preparing",
333
+ "running",
334
+ "validating",
335
+ "reviewing",
336
+ "stopped",
337
+ "failed",
338
+ "needs-attention",
339
+ "needs_attention"
340
+ ]);
214
341
 
215
342
  // packages/cli/src/commands/_parsers.ts
216
343
  async function loadRigConfigOrNull(projectRoot) {
@@ -226,7 +353,8 @@ async function loadRigConfigOrNull(projectRoot) {
226
353
  import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
227
354
  import { homedir as homedir2 } from "os";
228
355
  import { resolve as resolve3 } from "path";
229
- var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
356
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
357
+ var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
230
358
  async function defaultCommandRunner(command, options = {}) {
231
359
  const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
232
360
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -245,7 +373,7 @@ function resolvePiHomeDir(inputHomeDir) {
245
373
  function piListContainsPiRig(output) {
246
374
  return output.split(/\r?\n/).some((line) => {
247
375
  const normalized = line.trim();
248
- return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
376
+ return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
249
377
  });
250
378
  }
251
379
  async function safeRun(runner, command, options) {
@@ -413,7 +541,10 @@ async function runRigDoctorChecks(options) {
413
541
  const bunVersion = options.bunVersion ?? Bun.version;
414
542
  const request = options.requestJson ?? ((pathname, init) => requestServerJson({ projectRoot }, pathname, init));
415
543
  const loadConfig = options.loadConfig ?? loadRigConfigOrNull;
544
+ const progress = options.onProgress ?? (() => {});
545
+ progress("Checking local toolchain\u2026");
416
546
  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`)."));
547
+ progress("Loading rig.config\u2026");
417
548
  const loadedConfig = await loadConfig(projectRoot).catch(() => null);
418
549
  const config = loadedConfig ?? loadFallbackConfig(projectRoot);
419
550
  const hasConfigFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync4(resolve4(projectRoot, name)));
@@ -421,7 +552,7 @@ async function runRigDoctorChecks(options) {
421
552
  const taskSourceKind = config?.taskSource?.kind;
422
553
  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."));
423
554
  const repo = readRepoConnection(projectRoot);
424
- 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>`."));
555
+ 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>`."));
425
556
  const selected = (() => {
426
557
  try {
427
558
  return resolveSelectedConnection(projectRoot);
@@ -429,9 +560,10 @@ async function runRigDoctorChecks(options) {
429
560
  return null;
430
561
  }
431
562
  })();
432
- 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));
563
+ 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));
433
564
  let server = null;
434
565
  try {
566
+ progress("Connecting to the selected Rig server\u2026");
435
567
  server = await (options.resolveServer ?? ensureServerForCli)(projectRoot);
436
568
  checks.push(check("server", "Rig server reachable", "pass", `${server.connectionKind} ${server.baseUrl}`));
437
569
  } catch (error) {
@@ -439,18 +571,21 @@ async function runRigDoctorChecks(options) {
439
571
  }
440
572
  if (server || options.requestJson) {
441
573
  try {
574
+ progress("Checking server status\u2026");
442
575
  const status = await request("/api/server/status");
443
576
  checks.push(check("server-status", "server project status", "pass", JSON.stringify(status).slice(0, 180)));
444
577
  } catch (error) {
445
578
  checks.push(check("server-status", "server project status", "fail", errorMessage(error), "Run `rig doctor` after the selected server is reachable."));
446
579
  }
447
580
  try {
581
+ progress("Checking GitHub auth\u2026");
448
582
  const auth = await request("/api/github/auth/status");
449
583
  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>`."));
450
584
  } catch (error) {
451
585
  checks.push(check("github-auth", "GitHub auth", "fail", errorMessage(error), "Authenticate GitHub through Rig and ensure the server exposes auth status."));
452
586
  }
453
587
  try {
588
+ progress("Checking GitHub repo permissions\u2026");
454
589
  const permissions = await request("/api/github/repo/permissions");
455
590
  const allowed = permissionAllowsPr(permissions);
456
591
  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."));
@@ -458,6 +593,7 @@ async function runRigDoctorChecks(options) {
458
593
  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."));
459
594
  }
460
595
  try {
596
+ progress("Checking GitHub issue labels\u2026");
461
597
  const labels = await request("/api/workspace/task-labels");
462
598
  const ready = labelsReady(labels);
463
599
  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."));
@@ -465,6 +601,7 @@ async function runRigDoctorChecks(options) {
465
601
  checks.push(check("task-labels", "GitHub issue labels", "warn", errorMessage(error), "Run `rig init`/`rig doctor` after label setup is wired on the server."));
466
602
  }
467
603
  try {
604
+ progress("Checking task projection\u2026");
468
605
  const projection = await request("/api/workspace/task-projection");
469
606
  checks.push(check("task-projection", "task projection", "pass", JSON.stringify(projection).slice(0, 180)));
470
607
  } catch (error) {
@@ -473,6 +610,7 @@ async function runRigDoctorChecks(options) {
473
610
  const slug = projectStatusSlug(projectRoot, config);
474
611
  if (slug) {
475
612
  try {
613
+ progress("Checking server project checkout\u2026");
476
614
  const project = await request(`/api/projects/${encodeURIComponent(slug)}`);
477
615
  checks.push(check("remote-checkout", "server project checkout", "pass", JSON.stringify(project).slice(0, 180)));
478
616
  } catch (error) {
@@ -487,6 +625,7 @@ async function runRigDoctorChecks(options) {
487
625
  }
488
626
  checks.push(githubProjectsCheck(config));
489
627
  checks.push(prMergeCheck(config));
628
+ progress("Checking Pi installation\u2026");
490
629
  const piChecks = await (options.piChecks ?? (() => buildPiSetupChecks()))().catch((error) => [{
491
630
  ok: false,
492
631
  label: "pi/pi-rig checks",
@@ -510,10 +649,131 @@ function countDoctorFailures(checks) {
510
649
  return checks.filter((entry) => entry.status === "fail").length;
511
650
  }
512
651
 
652
+ // packages/cli/src/commands/_async-ui.ts
653
+ import pc from "picocolors";
654
+
655
+ // packages/cli/src/commands/_spinner.ts
656
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
657
+ function createTtySpinner(input) {
658
+ const output = input.output ?? process.stdout;
659
+ const isTty = output.isTTY === true;
660
+ const frames = input.frames && input.frames.length > 0 ? input.frames : SPINNER_FRAMES;
661
+ let label = input.label;
662
+ let frame = 0;
663
+ let paused = false;
664
+ let stopped = false;
665
+ let lastPrintedLabel = "";
666
+ const render = () => {
667
+ if (stopped || paused)
668
+ return;
669
+ if (!isTty) {
670
+ if (label !== lastPrintedLabel) {
671
+ output.write(`${label}
672
+ `);
673
+ lastPrintedLabel = label;
674
+ }
675
+ return;
676
+ }
677
+ frame = (frame + 1) % frames.length;
678
+ const glyph = frames[frame] ?? frames[0] ?? "";
679
+ output.write(`\r\x1B[2K${input.styleFrame ? input.styleFrame(glyph) : glyph} ${label}`);
680
+ };
681
+ const clearLine = () => {
682
+ if (isTty)
683
+ output.write("\r\x1B[2K");
684
+ };
685
+ render();
686
+ const timer = isTty ? setInterval(render, input.intervalMs ?? 120) : null;
687
+ return {
688
+ setLabel(next) {
689
+ label = next;
690
+ render();
691
+ },
692
+ pause() {
693
+ paused = true;
694
+ clearLine();
695
+ },
696
+ resume() {
697
+ if (stopped)
698
+ return;
699
+ paused = false;
700
+ render();
701
+ },
702
+ stop(finalLine) {
703
+ if (stopped)
704
+ return;
705
+ stopped = true;
706
+ if (timer)
707
+ clearInterval(timer);
708
+ clearLine();
709
+ if (finalLine)
710
+ output.write(`${finalLine}
711
+ `);
712
+ }
713
+ };
714
+ }
715
+
716
+ // packages/cli/src/commands/_async-ui.ts
717
+ var CLACK_SPINNER_FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
718
+ var DONE_SYMBOL = pc.green("\u25C7");
719
+ var FAIL_SYMBOL = pc.red("\u25A0");
720
+ var activeUpdate = null;
721
+ async function withSpinner(label, work, options = {}) {
722
+ if (options.outputMode === "json") {
723
+ return work(() => {});
724
+ }
725
+ if (activeUpdate) {
726
+ const outer = activeUpdate;
727
+ outer(label);
728
+ return work(outer);
729
+ }
730
+ const output = options.output ?? process.stderr;
731
+ const isTty = output.isTTY === true;
732
+ let lastLabel = label;
733
+ if (!isTty) {
734
+ output.write(`${label}
735
+ `);
736
+ const update2 = (next) => {
737
+ lastLabel = next;
738
+ };
739
+ activeUpdate = update2;
740
+ const previousListener2 = setServerPhaseListener(update2);
741
+ try {
742
+ return await work(update2);
743
+ } finally {
744
+ activeUpdate = null;
745
+ setServerPhaseListener(previousListener2);
746
+ }
747
+ }
748
+ const spinner = createTtySpinner({
749
+ label,
750
+ output,
751
+ frames: CLACK_SPINNER_FRAMES,
752
+ styleFrame: (frame) => pc.magenta(frame)
753
+ });
754
+ const update = (next) => {
755
+ lastLabel = next;
756
+ spinner.setLabel(next);
757
+ };
758
+ activeUpdate = update;
759
+ const previousListener = setServerPhaseListener(update);
760
+ try {
761
+ const result = await work(update);
762
+ spinner.stop(options.doneLabel ? `${DONE_SYMBOL} ${options.doneLabel}` : undefined);
763
+ return result;
764
+ } catch (error) {
765
+ spinner.stop(`${FAIL_SYMBOL} ${lastLabel}`);
766
+ throw error;
767
+ } finally {
768
+ activeUpdate = null;
769
+ setServerPhaseListener(previousListener);
770
+ }
771
+ }
772
+
513
773
  // packages/cli/src/commands/doctor.ts
514
774
  async function executeDoctor(context, args) {
515
775
  requireNoExtraArgs(args, "rig doctor");
516
- const checks = await runRigDoctorChecks({ projectRoot: context.projectRoot });
776
+ const checks = await withSpinner("Running doctor checks\u2026", (update) => runRigDoctorChecks({ projectRoot: context.projectRoot, onProgress: update }), { outputMode: context.outputMode });
517
777
  if (context.outputMode === "text") {
518
778
  console.log(formatDoctorChecks(checks));
519
779
  }