@h-rig/cli 0.0.6-alpha.5 → 0.0.6-alpha.50

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 (49) hide show
  1. package/dist/bin/rig.js +4072 -1204
  2. package/dist/src/commands/_cli-format.js +369 -0
  3. package/dist/src/commands/_connection-state.js +12 -6
  4. package/dist/src/commands/_doctor-checks.js +65 -34
  5. package/dist/src/commands/_help-catalog.js +445 -0
  6. package/dist/src/commands/_operator-surface.js +220 -0
  7. package/dist/src/commands/_operator-view.js +1109 -63
  8. package/dist/src/commands/_parsers.js +0 -2
  9. package/dist/src/commands/_pi-frontend.js +1066 -0
  10. package/dist/src/commands/_pi-install.js +4 -3
  11. package/dist/src/commands/_pi-remote-session.js +757 -0
  12. package/dist/src/commands/_pi-worker-bridge-extension.js +820 -0
  13. package/dist/src/commands/_policy.js +0 -2
  14. package/dist/src/commands/_preflight.js +84 -116
  15. package/dist/src/commands/_run-driver-helpers.js +2 -2
  16. package/dist/src/commands/_server-client.js +211 -48
  17. package/dist/src/commands/_snapshot-upload.js +60 -30
  18. package/dist/src/commands/_spinner.js +63 -0
  19. package/dist/src/commands/_task-picker.js +44 -16
  20. package/dist/src/commands/agent.js +8 -9
  21. package/dist/src/commands/browser.js +4 -6
  22. package/dist/src/commands/connect.js +134 -26
  23. package/dist/src/commands/dist.js +4 -6
  24. package/dist/src/commands/doctor.js +65 -34
  25. package/dist/src/commands/github.js +62 -32
  26. package/dist/src/commands/inbox.js +396 -31
  27. package/dist/src/commands/init.js +342 -88
  28. package/dist/src/commands/inspect.js +282 -23
  29. package/dist/src/commands/inspector.js +2 -4
  30. package/dist/src/commands/pi.js +168 -0
  31. package/dist/src/commands/plugin.js +81 -22
  32. package/dist/src/commands/profile-and-review.js +8 -10
  33. package/dist/src/commands/queue.js +2 -3
  34. package/dist/src/commands/remote.js +18 -20
  35. package/dist/src/commands/repo-git-harness.js +6 -8
  36. package/dist/src/commands/run.js +1407 -130
  37. package/dist/src/commands/server.js +266 -40
  38. package/dist/src/commands/setup.js +69 -44
  39. package/dist/src/commands/task-report-bug.js +5 -7
  40. package/dist/src/commands/task-run-driver.js +662 -70
  41. package/dist/src/commands/task.js +1846 -259
  42. package/dist/src/commands/test.js +3 -5
  43. package/dist/src/commands/workspace.js +4 -6
  44. package/dist/src/commands.js +4054 -1180
  45. package/dist/src/index.js +4065 -1200
  46. package/dist/src/launcher.js +5 -3
  47. package/dist/src/report-bug.js +3 -3
  48. package/dist/src/runner.js +5 -19
  49. package/package.json +7 -4
@@ -3,15 +3,13 @@ var __require = import.meta.require;
3
3
 
4
4
  // packages/cli/src/commands/init.ts
5
5
  import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
6
- import { spawnSync as spawnSync2 } from "child_process";
6
+ import { spawnSync } from "child_process";
7
7
  import { 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
11
  import { CliError } from "@rig/runtime/control-plane/errors";
12
12
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
13
- import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
14
- import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
15
13
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
16
14
  import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
17
15
  function takeFlag(args, flag) {
@@ -49,6 +47,7 @@ function takeOption(args, option) {
49
47
 
50
48
  // packages/cli/src/commands/init.ts
51
49
  import { buildRigInitConfigSource } from "@rig/core";
50
+ import { listGitHubProjects as listGitHubProjectsDirect, resolveProjectStatusField as resolveProjectStatusFieldDirect } from "@rig/server";
52
51
 
53
52
  // packages/cli/src/commands/_connection-state.ts
54
53
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -132,7 +131,8 @@ function readRepoConnection(projectRoot) {
132
131
  return {
133
132
  selected,
134
133
  project: typeof record.project === "string" ? record.project : undefined,
135
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
134
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
135
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
136
136
  };
137
137
  }
138
138
  function writeRepoConnection(projectRoot, state) {
@@ -143,27 +143,33 @@ function resolveSelectedConnection(projectRoot, options = {}) {
143
143
  if (!repo)
144
144
  return null;
145
145
  if (repo.selected === "local")
146
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
146
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
147
147
  const global = readGlobalConnections(options);
148
148
  const connection = global.connections[repo.selected];
149
149
  if (!connection) {
150
- throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
150
+ throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
151
151
  }
152
- return { alias: repo.selected, connection };
152
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
153
+ }
154
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
155
+ const repo = readRepoConnection(projectRoot);
156
+ if (!repo)
157
+ return;
158
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
153
159
  }
154
160
 
155
161
  // packages/cli/src/commands/_server-client.ts
156
- import { spawnSync } from "child_process";
157
162
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
158
163
  import { resolve as resolve2 } from "path";
159
164
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
160
- var cachedGitHubBearerToken;
165
+ var scopedGitHubBearerTokens = new Map;
161
166
  function cleanToken(value) {
162
167
  const trimmed = value?.trim();
163
168
  return trimmed ? trimmed : null;
164
169
  }
165
- function setGitHubBearerTokenForCurrentProcess(token) {
166
- cachedGitHubBearerToken = cleanToken(token ?? undefined);
170
+ function setGitHubBearerTokenForCurrentProcess(token, projectRoot) {
171
+ const scopedKey = resolve2(projectRoot ?? process.cwd());
172
+ scopedGitHubBearerTokens.set(scopedKey, cleanToken(token ?? undefined));
167
173
  }
168
174
  function readPrivateRemoteSessionToken(projectRoot) {
169
175
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
@@ -177,41 +183,33 @@ function readPrivateRemoteSessionToken(projectRoot) {
177
183
  }
178
184
  }
179
185
  function readGitHubBearerTokenForRemote(projectRoot) {
180
- if (cachedGitHubBearerToken !== undefined)
181
- return cachedGitHubBearerToken;
186
+ const scopedKey = resolve2(projectRoot);
187
+ if (scopedGitHubBearerTokens.has(scopedKey))
188
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
182
189
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
183
- if (privateSession) {
184
- cachedGitHubBearerToken = privateSession;
185
- return cachedGitHubBearerToken;
186
- }
187
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
188
- if (envToken) {
189
- cachedGitHubBearerToken = envToken;
190
- return cachedGitHubBearerToken;
191
- }
192
- const result = spawnSync("gh", ["auth", "token"], {
193
- encoding: "utf8",
194
- timeout: 5000,
195
- stdio: ["ignore", "pipe", "ignore"]
196
- });
197
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
198
- return cachedGitHubBearerToken;
190
+ if (privateSession)
191
+ return privateSession;
192
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
199
193
  }
200
194
  async function ensureServerForCli(projectRoot) {
201
195
  try {
202
196
  const selected = resolveSelectedConnection(projectRoot);
203
197
  if (selected?.connection.kind === "remote") {
198
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
199
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
204
200
  return {
205
201
  baseUrl: selected.connection.baseUrl,
206
- authToken: readGitHubBearerTokenForRemote(projectRoot),
207
- connectionKind: "remote"
202
+ authToken,
203
+ connectionKind: "remote",
204
+ serverProjectRoot
208
205
  };
209
206
  }
210
207
  const connection = await ensureLocalRigServerConnection(projectRoot);
211
208
  return {
212
209
  baseUrl: connection.baseUrl,
213
210
  authToken: connection.authToken,
214
- connectionKind: "local"
211
+ connectionKind: "local",
212
+ serverProjectRoot: resolve2(projectRoot)
215
213
  };
216
214
  } catch (error) {
217
215
  if (error instanceof Error) {
@@ -220,6 +218,29 @@ async function ensureServerForCli(projectRoot) {
220
218
  throw error;
221
219
  }
222
220
  }
221
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
222
+ const repo = readRepoConnection(projectRoot);
223
+ const slug = repo?.project?.trim();
224
+ if (!slug)
225
+ return null;
226
+ try {
227
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
228
+ headers: mergeHeaders(undefined, authToken)
229
+ });
230
+ if (!response.ok)
231
+ return null;
232
+ const payload = await response.json();
233
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
234
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
235
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
236
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
237
+ if (path)
238
+ writeRepoServerProjectRoot(projectRoot, path);
239
+ return path;
240
+ } catch {
241
+ return null;
242
+ }
243
+ }
223
244
  function mergeHeaders(headers, authToken) {
224
245
  const merged = new Headers(headers);
225
246
  if (authToken) {
@@ -244,9 +265,12 @@ function diagnosticMessage(payload) {
244
265
  }
245
266
  async function requestServerJson(context, pathname, init = {}) {
246
267
  const server = await ensureServerForCli(context.projectRoot);
268
+ const headers = mergeHeaders(init.headers, server.authToken);
269
+ if (server.serverProjectRoot)
270
+ headers.set("x-rig-project-root", server.serverProjectRoot);
247
271
  const response = await fetch(`${server.baseUrl}${pathname}`, {
248
272
  ...init,
249
- headers: mergeHeaders(init.headers, server.authToken)
273
+ headers
250
274
  });
251
275
  const text = await response.text();
252
276
  const payload = text.trim().length > 0 ? (() => {
@@ -290,39 +314,64 @@ async function registerProjectViaServer(context, input) {
290
314
  function sleep(ms) {
291
315
  return new Promise((resolve3) => setTimeout(resolve3, ms));
292
316
  }
317
+ function isRetryableProjectRootSwitchError(error) {
318
+ if (!(error instanceof Error))
319
+ return false;
320
+ const message = error.message.toLowerCase();
321
+ return message.includes("rig server request failed (401): auth-required") || message.includes("rig server request failed (401): github-token-required") || message.includes("rig server request failed (502)") || message.includes("rig server request failed (503)") || message.includes("bad gateway") || message.includes("fetch failed") || message.includes("econnrefused") || message.includes("connection refused");
322
+ }
293
323
  async function switchServerProjectRootViaServer(context, projectRoot, options = {}) {
294
- const switched = await requestServerJson(context, "/api/server/project-root", {
295
- method: "POST",
296
- headers: { "content-type": "application/json" },
297
- body: JSON.stringify({ projectRoot })
298
- });
299
324
  const timeoutMs = options.timeoutMs ?? 30000;
300
325
  const pollMs = options.pollMs ?? 1000;
301
326
  const deadline = Date.now() + timeoutMs;
302
327
  let lastError;
328
+ let switched = null;
303
329
  while (Date.now() < deadline) {
304
330
  try {
305
- const status = await requestServerJson(context, "/api/server/status");
306
- if (status && typeof status === "object" && !Array.isArray(status)) {
307
- const record = status;
308
- if (record.projectRoot === projectRoot) {
309
- return { ok: true, switched, status: record };
310
- }
311
- lastError = `server projectRoot=${String(record.projectRoot ?? "unknown")}`;
312
- }
331
+ switched = await requestServerJson(context, "/api/server/project-root", {
332
+ method: "POST",
333
+ headers: { "content-type": "application/json" },
334
+ body: JSON.stringify({ projectRoot })
335
+ });
336
+ break;
313
337
  } catch (error) {
314
338
  lastError = error;
339
+ if (!isRetryableProjectRootSwitchError(error))
340
+ throw error;
341
+ await sleep(pollMs);
315
342
  }
316
- await sleep(pollMs);
317
343
  }
318
- throw new CliError2(`Rig server did not switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no status")}).`, 1);
344
+ if (!switched) {
345
+ 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);
346
+ }
347
+ const record = switched && typeof switched === "object" && !Array.isArray(switched) ? switched : {};
348
+ if (record.ok === true) {
349
+ writeRepoServerProjectRoot(context.projectRoot, projectRoot);
350
+ return { ok: true, switched: record };
351
+ }
352
+ throw new CliError2(`Rig server rejected project-root scope ${projectRoot}: ${String(record.message ?? record.error ?? "unknown error")}`, 1);
353
+ }
354
+ async function ensureTaskLabelsViaServer(context) {
355
+ const payload = await requestServerJson(context, "/api/workspace/task-labels", { method: "POST" });
356
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
357
+ }
358
+ async function listGitHubProjectsViaServer(context, owner) {
359
+ const url = new URL("http://rig.local/api/github/projects");
360
+ url.searchParams.set("owner", owner);
361
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
362
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { projects: [] };
363
+ }
364
+ async function getGitHubProjectStatusFieldViaServer(context, projectId) {
365
+ const payload = await requestServerJson(context, `/api/github/projects/${encodeURIComponent(projectId)}/status-field`);
366
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
319
367
  }
320
368
 
321
369
  // packages/cli/src/commands/_pi-install.ts
322
370
  import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
323
371
  import { homedir as homedir2 } from "os";
324
372
  import { resolve as resolve3 } from "path";
325
- var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
373
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
374
+ var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
326
375
  var LEGACY_PI_RIG_MARKER = `// Managed by Rig. Source package: @rig/pi-rig.
327
376
  export { default } from '@rig/pi-rig';
328
377
  `;
@@ -350,7 +399,7 @@ function resolvePiHomeDir(inputHomeDir) {
350
399
  function piListContainsPiRig(output) {
351
400
  return output.split(/\r?\n/).some((line) => {
352
401
  const normalized = line.trim();
353
- return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
402
+ return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
354
403
  });
355
404
  }
356
405
  async function safeRun(runner, command, options) {
@@ -466,7 +515,7 @@ async function ensureRemotePiRigInstalled(input) {
466
515
  const payload = await input.requestJson("/api/pi-rig/install", {
467
516
  method: "POST",
468
517
  headers: { "content-type": "application/json" },
469
- body: JSON.stringify({ package: "@rig/pi-rig", scope: "global" })
518
+ body: JSON.stringify({ package: PI_RIG_PACKAGE_NAME, scope: "global" })
470
519
  });
471
520
  const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
472
521
  const piOk = record.piOk === true || record.ok === true;
@@ -727,7 +776,7 @@ async function runRigDoctorChecks(options) {
727
776
  const taskSourceKind = config?.taskSource?.kind;
728
777
  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."));
729
778
  const repo = readRepoConnection(projectRoot);
730
- 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>`."));
779
+ 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>`."));
731
780
  const selected = (() => {
732
781
  try {
733
782
  return resolveSelectedConnection(projectRoot);
@@ -735,7 +784,7 @@ async function runRigDoctorChecks(options) {
735
784
  return null;
736
785
  }
737
786
  })();
738
- 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));
787
+ 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));
739
788
  let server = null;
740
789
  try {
741
790
  server = await (options.resolveServer ?? ensureServerForCli)(projectRoot);
@@ -809,6 +858,7 @@ function countDoctorFailures(checks) {
809
858
 
810
859
  // packages/cli/src/commands/init.ts
811
860
  var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
861
+ var DEFAULT_REMOTE_RIG_URL = "https://where.rig-does.work";
812
862
  var RIG_CONFIG_DEV_DEPENDENCIES = {
813
863
  "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
814
864
  "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
@@ -819,7 +869,7 @@ function parseRepoSlugFromRemote(remoteUrl) {
819
869
  return gitHubMatch ? `${gitHubMatch[1]}/${gitHubMatch[2]}` : null;
820
870
  }
821
871
  function detectOriginRepoSlug(projectRoot) {
822
- const result = spawnSync2("git", ["-C", projectRoot, "remote", "get-url", "origin"], { encoding: "utf8" });
872
+ const result = spawnSync("git", ["-C", projectRoot, "remote", "get-url", "origin"], { encoding: "utf8" });
823
873
  if (result.status !== 0)
824
874
  return null;
825
875
  return parseRepoSlugFromRemote(result.stdout.trim());
@@ -875,11 +925,14 @@ function applyGitHubProjectConfig(source, options) {
875
925
  return source;
876
926
  const projectId = JSON.stringify(options.githubProject);
877
927
  const statusFieldId = JSON.stringify(options.githubProjectStatusField ?? "Status");
928
+ const statuses = options.githubProjectStatuses && Object.keys(options.githubProjectStatuses).length > 0 ? `
929
+ statuses: ${JSON.stringify(options.githubProjectStatuses, null, 8).replace(/\n/g, `
930
+ `)},` : "";
878
931
  return source.replace(` projects: { enabled: false },`, [
879
932
  ` projects: {`,
880
933
  ` enabled: true,`,
881
934
  ` projectId: ${projectId},`,
882
- ` statusFieldId: ${statusFieldId},`,
935
+ ` statusFieldId: ${statusFieldId},${statuses}`,
883
936
  ` },`
884
937
  ].join(`
885
938
  `));
@@ -900,21 +953,41 @@ function checkoutForInit(projectRoot, serverKind, strategy) {
900
953
  }
901
954
  }
902
955
  function detectGhLogin() {
903
- const result = spawnSync2("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
956
+ const result = spawnSync("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
904
957
  return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : null;
905
958
  }
906
959
  function readGhAuthToken() {
907
- const result = spawnSync2("gh", ["auth", "token"], { encoding: "utf8" });
960
+ const result = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
908
961
  if (result.status !== 0 || !result.stdout.trim()) {
909
962
  throw new CliError2(result.stderr.trim() || "Could not read GitHub token from `gh auth token`.", result.status || 1);
910
963
  }
911
964
  return result.stdout.trim();
912
965
  }
966
+ function refreshGhProjectScopesAndReadToken() {
967
+ const result = spawnSync("gh", ["auth", "refresh", "--scopes", "read:project"], {
968
+ encoding: "utf8",
969
+ stdio: ["inherit", "pipe", "pipe"]
970
+ });
971
+ if (result.status !== 0)
972
+ return null;
973
+ try {
974
+ return readGhAuthToken();
975
+ } catch {
976
+ return null;
977
+ }
978
+ }
913
979
  async function loadClackPrompts() {
914
980
  return await import("@clack/prompts");
915
981
  }
982
+ function clackTextOptions(options) {
983
+ return {
984
+ message: options.message,
985
+ ...options.placeholder ? { placeholder: options.placeholder } : {},
986
+ ...(options.initialValue ?? options.defaultValue)?.trim() ? { initialValue: (options.initialValue ?? options.defaultValue).trim() } : {}
987
+ };
988
+ }
916
989
  async function promptRequiredText(prompts, options) {
917
- const value = await prompts.text(options);
990
+ const value = await prompts.text(clackTextOptions(options));
918
991
  if (prompts.isCancel(value))
919
992
  throw new CliError2("Init cancelled.", 1);
920
993
  const text = String(value ?? "").trim();
@@ -923,7 +996,7 @@ async function promptRequiredText(prompts, options) {
923
996
  return text;
924
997
  }
925
998
  async function promptOptionalText(prompts, options) {
926
- const value = await prompts.text(options);
999
+ const value = await prompts.text(clackTextOptions(options));
927
1000
  if (prompts.isCancel(value))
928
1001
  throw new CliError2("Init cancelled.", 1);
929
1002
  return String(value ?? "").trim();
@@ -934,6 +1007,164 @@ async function promptSelect(prompts, options) {
934
1007
  throw new CliError2("Init cancelled.", 1);
935
1008
  return String(value);
936
1009
  }
1010
+ function repoOwnerFromSlug(repoSlug) {
1011
+ return repoSlug.trim().match(/^([^/]+)\/[^/]+$/)?.[1] ?? null;
1012
+ }
1013
+ function recordArray(value, key) {
1014
+ if (!value || typeof value !== "object" || Array.isArray(value))
1015
+ return [];
1016
+ const raw = value[key];
1017
+ return Array.isArray(raw) ? raw.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1018
+ }
1019
+ async function listGitHubProjectsForInit(context, owner, token) {
1020
+ if (token?.trim()) {
1021
+ try {
1022
+ return { ok: true, projects: await listGitHubProjectsDirect({ owner, token: token.trim() }) };
1023
+ } catch (directError) {
1024
+ const serverPayload = await listGitHubProjectsViaServer(context, owner).catch(() => null);
1025
+ if (recordArray(serverPayload, "projects").length > 0)
1026
+ return serverPayload;
1027
+ return { ok: false, error: directError instanceof Error ? directError.message : String(directError), projects: [] };
1028
+ }
1029
+ }
1030
+ return listGitHubProjectsViaServer(context, owner);
1031
+ }
1032
+ async function getGitHubProjectStatusFieldForInit(context, projectId, token) {
1033
+ if (token?.trim()) {
1034
+ try {
1035
+ return { ok: true, field: await resolveProjectStatusFieldDirect({ projectId, token: token.trim() }) };
1036
+ } catch (directError) {
1037
+ const serverPayload = await getGitHubProjectStatusFieldViaServer(context, projectId).catch(() => null);
1038
+ if (serverPayload && typeof serverPayload === "object" && !Array.isArray(serverPayload) && "field" in serverPayload) {
1039
+ return serverPayload;
1040
+ }
1041
+ return { ok: false, error: directError instanceof Error ? directError.message : String(directError) };
1042
+ }
1043
+ }
1044
+ return getGitHubProjectStatusFieldViaServer(context, projectId);
1045
+ }
1046
+ var PROJECT_STATUS_PROMPTS = {
1047
+ running: "Running/In progress",
1048
+ prOpen: "PR open/review",
1049
+ ciFixing: "CI/review fixing",
1050
+ merging: "Merging",
1051
+ done: "Done",
1052
+ needsAttention: "Needs attention"
1053
+ };
1054
+ var DEFAULT_PROJECT_STATUS_OPTIONS = {
1055
+ running: "In Progress",
1056
+ prOpen: "In Review",
1057
+ ciFixing: "In Review",
1058
+ merging: "Merging",
1059
+ done: "Done",
1060
+ needsAttention: "Needs Attention"
1061
+ };
1062
+ async function promptManualProjectStatusMapping(prompts) {
1063
+ const statuses = {};
1064
+ for (const [key, label] of Object.entries(PROJECT_STATUS_PROMPTS)) {
1065
+ const defaultLabel = DEFAULT_PROJECT_STATUS_OPTIONS[key] ?? label;
1066
+ const value = await promptOptionalText(prompts, {
1067
+ message: `Project status option id/name for ${label} (blank for ${defaultLabel})`,
1068
+ placeholder: defaultLabel
1069
+ });
1070
+ statuses[key] = value || defaultLabel;
1071
+ }
1072
+ return statuses;
1073
+ }
1074
+ function projectScopeError(value) {
1075
+ const text = typeof value === "string" ? value : JSON.stringify(value ?? "");
1076
+ return /INSUFFICIENT_SCOPES|read:project|required scopes/i.test(text);
1077
+ }
1078
+ function optionName(option) {
1079
+ return String(option.name ?? option.label ?? option.id ?? "").trim();
1080
+ }
1081
+ function autoProjectStatusValue(options, key, label) {
1082
+ const candidates = [DEFAULT_PROJECT_STATUS_OPTIONS[key], label].filter((value) => Boolean(value)).map((value) => value.trim().toLowerCase());
1083
+ const match = options.find((option) => candidates.includes(optionName(option).toLowerCase()));
1084
+ if (!match)
1085
+ return null;
1086
+ return String(match.id ?? match.name);
1087
+ }
1088
+ async function promptGitHubProjectConfig(context, prompts, repoSlug, githubToken, refreshProjectToken) {
1089
+ const projectChoice = await promptSelect(prompts, {
1090
+ message: "GitHub Projects status sync",
1091
+ initialValue: "select",
1092
+ options: [
1093
+ { value: "select", label: "Select accessible ProjectV2" },
1094
+ { value: "off", label: "Off" },
1095
+ { value: "manual", label: "Enter ProjectV2 ids manually" }
1096
+ ]
1097
+ });
1098
+ if (projectChoice === "off")
1099
+ return { githubProject: "off" };
1100
+ if (projectChoice === "manual") {
1101
+ return {
1102
+ githubProject: await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }),
1103
+ githubProjectStatusField: await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" }),
1104
+ githubProjectStatuses: await promptManualProjectStatusMapping(prompts)
1105
+ };
1106
+ }
1107
+ const owner = repoOwnerFromSlug(repoSlug);
1108
+ if (!owner)
1109
+ throw new CliError2(`Cannot derive GitHub owner from repo slug ${repoSlug}.`, 1);
1110
+ let activeToken = githubToken?.trim() || null;
1111
+ let projectsPayload = await listGitHubProjectsForInit(context, owner, activeToken).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), projects: [] }));
1112
+ let projects = recordArray(projectsPayload, "projects");
1113
+ if (projects.length === 0 && projectScopeError(projectsPayload.error) && refreshProjectToken) {
1114
+ prompts.outro?.("GitHub token is missing read:project; refreshing gh auth scopes and retrying Projects.");
1115
+ const refreshedToken = refreshProjectToken();
1116
+ if (refreshedToken) {
1117
+ activeToken = refreshedToken;
1118
+ projectsPayload = await listGitHubProjectsForInit(context, owner, activeToken).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), projects: [] }));
1119
+ projects = recordArray(projectsPayload, "projects");
1120
+ }
1121
+ }
1122
+ if (projects.length === 0) {
1123
+ const error = typeof projectsPayload.error === "string" ? ` (${String(projectsPayload.error).replace(/\s+/g, " ").slice(0, 240)})` : "";
1124
+ prompts.outro?.(`No accessible GitHub Projects were returned${error}; continuing with GitHub Projects status sync off.`);
1125
+ return { githubProject: "off", ...activeToken ? { githubToken: activeToken } : {} };
1126
+ }
1127
+ const selectedProjectId = await promptSelect(prompts, {
1128
+ message: "GitHub ProjectV2 project",
1129
+ options: [
1130
+ ...projects.map((project) => ({
1131
+ value: String(project.id),
1132
+ label: `${String(project.title ?? "Untitled project")} (#${String(project.number ?? "?")})`,
1133
+ hint: typeof project.url === "string" ? project.url : undefined
1134
+ })),
1135
+ { value: "manual", label: "Enter ProjectV2 id manually" }
1136
+ ]
1137
+ });
1138
+ const projectId = selectedProjectId === "manual" ? await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }) : selectedProjectId;
1139
+ const fieldPayload = await getGitHubProjectStatusFieldForInit(context, projectId, activeToken).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) }));
1140
+ const fieldPayloadRecord = fieldPayload && typeof fieldPayload === "object" && !Array.isArray(fieldPayload) ? fieldPayload : {};
1141
+ const rawField = fieldPayloadRecord.field;
1142
+ const field = rawField && typeof rawField === "object" && !Array.isArray(rawField) ? rawField : null;
1143
+ const fieldId = typeof field?.id === "string" && field.id.trim() ? field.id : await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" });
1144
+ const options = Array.isArray(field?.options) ? field.options.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1145
+ if (options.length === 0) {
1146
+ return {
1147
+ githubProject: projectId,
1148
+ githubProjectStatusField: fieldId,
1149
+ githubProjectStatuses: await promptManualProjectStatusMapping(prompts),
1150
+ ...activeToken ? { githubToken: activeToken } : {}
1151
+ };
1152
+ }
1153
+ const statuses = {};
1154
+ for (const [key, label] of Object.entries(PROJECT_STATUS_PROMPTS)) {
1155
+ const auto = autoProjectStatusValue(options, key, label);
1156
+ statuses[key] = auto ?? await promptSelect(prompts, {
1157
+ message: `Project status option for ${label}`,
1158
+ options: options.map((option) => ({ value: String(option.id ?? option.name), label: optionName(option) }))
1159
+ });
1160
+ }
1161
+ return {
1162
+ githubProject: projectId,
1163
+ githubProjectStatusField: fieldId,
1164
+ githubProjectStatuses: Object.keys(statuses).length > 0 ? statuses : undefined,
1165
+ ...activeToken ? { githubToken: activeToken } : {}
1166
+ };
1167
+ }
937
1168
  function sleep2(ms) {
938
1169
  return new Promise((resolve7) => setTimeout(resolve7, ms));
939
1170
  }
@@ -947,12 +1178,29 @@ function apiSessionTokenFrom(payload) {
947
1178
  const token = payload.apiSessionToken;
948
1179
  return typeof token === "string" && token.trim() ? token.trim() : null;
949
1180
  }
1181
+ function cleanPayloadString(value) {
1182
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1183
+ }
1184
+ function remoteGitHubAuthMetadata(payload) {
1185
+ if (!payload)
1186
+ return {};
1187
+ const userNamespace = payload.userNamespace && typeof payload.userNamespace === "object" && !Array.isArray(payload.userNamespace) ? payload.userNamespace : null;
1188
+ return {
1189
+ ...cleanPayloadString(payload.login) ? { login: cleanPayloadString(payload.login) } : {},
1190
+ ...cleanPayloadString(payload.userId) ? { userId: cleanPayloadString(payload.userId) } : {},
1191
+ ...cleanPayloadString(userNamespace?.key) ? { userNamespaceKey: cleanPayloadString(userNamespace?.key) } : {},
1192
+ ...cleanPayloadString(userNamespace?.root) ? { userNamespaceRoot: cleanPayloadString(userNamespace?.root) } : {},
1193
+ ...cleanPayloadString(userNamespace?.checkoutBaseDir) ? { checkoutBaseDir: cleanPayloadString(userNamespace?.checkoutBaseDir) } : {},
1194
+ ...cleanPayloadString(userNamespace?.snapshotBaseDir) ? { snapshotBaseDir: cleanPayloadString(userNamespace?.snapshotBaseDir) } : {}
1195
+ };
1196
+ }
950
1197
  function writeRemoteGitHubAuthState(projectRoot, input) {
951
1198
  writeFileSync2(resolve6(projectRoot, ".rig", "state", "github-auth.json"), `${JSON.stringify({
952
1199
  authenticated: true,
953
1200
  source: input.source,
954
1201
  storedOnServer: true,
955
1202
  selectedRepo: input.selectedRepo,
1203
+ ...remoteGitHubAuthMetadata(input.authPayload ?? null),
956
1204
  ...input.apiSessionToken ? { apiSessionToken: input.apiSessionToken } : {},
957
1205
  updatedAt: new Date().toISOString()
958
1206
  }, null, 2)}
@@ -1045,12 +1293,13 @@ async function runControlPlaneInit(context, options) {
1045
1293
  if (token) {
1046
1294
  githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug });
1047
1295
  const apiSessionToken = apiSessionTokenFrom(githubAuth);
1048
- setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token);
1296
+ setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token, projectRoot);
1049
1297
  if (serverKind === "remote") {
1050
1298
  writeRemoteGitHubAuthState(projectRoot, {
1051
1299
  source: authMethod === "gh" ? "gh" : "init-token",
1052
1300
  selectedRepo: repo.slug,
1053
- apiSessionToken
1301
+ apiSessionToken,
1302
+ authPayload: githubAuth
1054
1303
  });
1055
1304
  }
1056
1305
  } else if (authMethod === "device") {
@@ -1069,9 +1318,9 @@ async function runControlPlaneInit(context, options) {
1069
1318
  if (completed) {
1070
1319
  const apiSessionToken = apiSessionTokenFrom(completed);
1071
1320
  if (apiSessionToken) {
1072
- setGitHubBearerTokenForCurrentProcess(apiSessionToken);
1321
+ setGitHubBearerTokenForCurrentProcess(apiSessionToken, projectRoot);
1073
1322
  if (serverKind === "remote") {
1074
- writeRemoteGitHubAuthState(projectRoot, { source: "device", selectedRepo: repo.slug, apiSessionToken });
1323
+ writeRemoteGitHubAuthState(projectRoot, { source: "device", selectedRepo: repo.slug, apiSessionToken, authPayload: completed });
1075
1324
  }
1076
1325
  }
1077
1326
  deviceAuth = { ...deviceAuth, poll: completed, completed: completed.status === "signed-in" };
@@ -1089,19 +1338,25 @@ async function runControlPlaneInit(context, options) {
1089
1338
  Object.assign(checkout, preparedCheckout);
1090
1339
  }
1091
1340
  }
1341
+ const checkoutPath = typeof checkout.path === "string" ? checkout.path : null;
1342
+ if (serverKind === "remote" && checkoutPath && token) {
1343
+ githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug, projectRoot: checkoutPath });
1344
+ const apiSessionToken = apiSessionTokenFrom(githubAuth);
1345
+ setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token, projectRoot);
1346
+ writeRemoteGitHubAuthState(projectRoot, { source: authMethod === "gh" ? "gh" : "init-token", selectedRepo: repo.slug, apiSessionToken, authPayload: githubAuth });
1347
+ }
1092
1348
  const registered = await registerProjectViaServer(context, {
1093
1349
  repoSlug: repo.slug,
1094
1350
  checkout
1095
1351
  });
1096
- const checkoutPath = typeof checkout.path === "string" ? checkout.path : null;
1097
1352
  const serverRootSwitch = serverKind === "remote" && checkoutPath ? await switchServerProjectRootViaServer(context, checkoutPath) : null;
1098
- if (serverRootSwitch && token) {
1099
- githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug, projectRoot: checkoutPath ?? undefined });
1100
- const apiSessionToken = apiSessionTokenFrom(githubAuth);
1101
- setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token);
1102
- writeRemoteGitHubAuthState(projectRoot, { source: authMethod === "gh" ? "gh" : "init-token", selectedRepo: repo.slug, apiSessionToken });
1103
- }
1104
1353
  const activeProjectRegistration = serverRootSwitch ? await registerProjectViaServer(context, { repoSlug: repo.slug, checkout }) : null;
1354
+ const labelSetup = await ensureTaskLabelsViaServer(context).catch((error) => ({
1355
+ ok: false,
1356
+ ready: false,
1357
+ labelsReady: false,
1358
+ error: error instanceof Error ? error.message : String(error)
1359
+ }));
1105
1360
  const pi = serverKind === "remote" ? await ensureRemotePiRigInstalled({ requestJson: (pathname, init) => requestServerJson(context, pathname, init) }).catch((error) => ({
1106
1361
  remote: true,
1107
1362
  pi: { ok: false, label: "pi", hint: error instanceof Error ? error.message : String(error) },
@@ -1132,6 +1387,7 @@ async function runControlPlaneInit(context, options) {
1132
1387
  githubAuth,
1133
1388
  deviceAuth,
1134
1389
  githubAuthWarning: remoteGhTokenWarning,
1390
+ labelSetup,
1135
1391
  pi,
1136
1392
  doctor
1137
1393
  };
@@ -1238,12 +1494,13 @@ async function runInteractiveControlPlaneInit(context, prompts) {
1238
1494
  });
1239
1495
  const serverChoice = await promptSelect(prompts, {
1240
1496
  message: "Rig server",
1497
+ initialValue: "remote",
1241
1498
  options: [
1242
- { value: "local", label: "Local server", hint: "run on this machine" },
1243
- { value: "remote", label: "Remote server", hint: "connect to an HTTPS Rig server" }
1499
+ { value: "remote", label: "Remote server", hint: "connect to an HTTPS Rig server" },
1500
+ { value: "local", label: "Local server", hint: "run on this machine" }
1244
1501
  ]
1245
1502
  });
1246
- const remoteUrl = serverChoice === "remote" ? await promptRequiredText(prompts, { message: "Remote Rig server URL", placeholder: "https://rig.example.com" }) : undefined;
1503
+ const remoteUrl = serverChoice === "remote" ? await promptRequiredText(prompts, { message: "Remote Rig server URL", placeholder: DEFAULT_REMOTE_RIG_URL, initialValue: DEFAULT_REMOTE_RIG_URL }) : undefined;
1247
1504
  let remoteCheckout;
1248
1505
  if (serverChoice === "remote") {
1249
1506
  const checkout = await promptSelect(prompts, {
@@ -1275,38 +1532,35 @@ async function runInteractiveControlPlaneInit(context, prompts) {
1275
1532
  { value: "skip", label: "Skip for now" }
1276
1533
  ]
1277
1534
  });
1535
+ let remoteGhTokenConfirmed = false;
1278
1536
  if (serverChoice === "remote" && authMethod === "gh") {
1279
1537
  if (!prompts.confirm)
1280
1538
  throw new CliError2("Remote gh-token import requires explicit confirmation.", 1);
1281
1539
  const confirmed = await prompts.confirm({
1282
1540
  message: `This sends a GitHub token from this machine to ${remoteUrl}. Continue?`,
1283
- initialValue: false
1541
+ initialValue: true
1284
1542
  });
1285
1543
  if (prompts.isCancel(confirmed) || confirmed !== true) {
1286
1544
  throw new CliError2("Remote gh-token import cancelled.", 1);
1287
1545
  }
1546
+ remoteGhTokenConfirmed = true;
1288
1547
  }
1289
- const githubToken = authMethod === "token" ? await promptRequiredText(prompts, { message: "GitHub token", placeholder: "ghp_..." }) : undefined;
1290
- const projectChoice = await promptSelect(prompts, {
1291
- message: "GitHub Projects status sync",
1292
- options: [
1293
- { value: "off", label: "Off" },
1294
- { value: "configure", label: "Configure ProjectV2 status field" }
1295
- ]
1296
- });
1297
- const githubProject = projectChoice === "configure" ? await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }) : "off";
1298
- const githubProjectStatusField = projectChoice === "configure" ? await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" }) : undefined;
1548
+ const githubToken = authMethod === "token" ? await promptRequiredText(prompts, { message: "GitHub token", placeholder: "ghp_..." }) : authMethod === "gh" ? readGhAuthToken() : undefined;
1549
+ const projectConfig = await promptGitHubProjectConfig(context, prompts, repoSlug, githubToken, authMethod === "gh" ? refreshGhProjectScopesAndReadToken : undefined);
1550
+ const effectiveGithubToken = projectConfig.githubToken ?? githubToken;
1299
1551
  const result = await runControlPlaneInit(context, {
1300
1552
  server: serverChoice,
1301
1553
  remoteUrl,
1302
1554
  repoSlug,
1303
- githubToken,
1555
+ githubToken: effectiveGithubToken,
1304
1556
  githubAuthMethod: authMethod,
1305
- githubProject,
1306
- githubProjectStatusField,
1557
+ githubProject: projectConfig.githubProject,
1558
+ githubProjectStatusField: projectConfig.githubProjectStatusField,
1559
+ githubProjectStatuses: projectConfig.githubProjectStatuses,
1307
1560
  remoteCheckout,
1308
1561
  repair,
1309
- privateStateOnly
1562
+ privateStateOnly,
1563
+ yes: remoteGhTokenConfirmed || undefined
1310
1564
  });
1311
1565
  const details = result.details && typeof result.details === "object" && !Array.isArray(result.details) ? result.details : {};
1312
1566
  const deviceAuth = details.deviceAuth && typeof details.deviceAuth === "object" && !Array.isArray(details.deviceAuth) ? details.deviceAuth : null;