@h-rig/cli 0.0.6-alpha.13 → 0.0.6-alpha.14

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.
@@ -3,7 +3,7 @@ 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
@@ -49,6 +49,7 @@ function takeOption(args, option) {
49
49
 
50
50
  // packages/cli/src/commands/init.ts
51
51
  import { buildRigInitConfigSource } from "@rig/core";
52
+ import { listGitHubProjects as listGitHubProjectsDirect, resolveProjectStatusField as resolveProjectStatusFieldDirect } from "@rig/server";
52
53
 
53
54
  // packages/cli/src/commands/_connection-state.ts
54
55
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -153,17 +154,17 @@ function resolveSelectedConnection(projectRoot, options = {}) {
153
154
  }
154
155
 
155
156
  // packages/cli/src/commands/_server-client.ts
156
- import { spawnSync } from "child_process";
157
157
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
158
158
  import { resolve as resolve2 } from "path";
159
159
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
160
- var cachedGitHubBearerToken;
160
+ var scopedGitHubBearerTokens = new Map;
161
161
  function cleanToken(value) {
162
162
  const trimmed = value?.trim();
163
163
  return trimmed ? trimmed : null;
164
164
  }
165
- function setGitHubBearerTokenForCurrentProcess(token) {
166
- cachedGitHubBearerToken = cleanToken(token ?? undefined);
165
+ function setGitHubBearerTokenForCurrentProcess(token, projectRoot) {
166
+ const scopedKey = resolve2(projectRoot ?? process.cwd());
167
+ scopedGitHubBearerTokens.set(scopedKey, cleanToken(token ?? undefined));
167
168
  }
168
169
  function readPrivateRemoteSessionToken(projectRoot) {
169
170
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
@@ -177,25 +178,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
177
178
  }
178
179
  }
179
180
  function readGitHubBearerTokenForRemote(projectRoot) {
180
- if (cachedGitHubBearerToken !== undefined)
181
- return cachedGitHubBearerToken;
181
+ const scopedKey = resolve2(projectRoot);
182
+ if (scopedGitHubBearerTokens.has(scopedKey))
183
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
182
184
  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;
185
+ if (privateSession)
186
+ return privateSession;
187
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
199
188
  }
200
189
  async function ensureServerForCli(projectRoot) {
201
190
  try {
@@ -337,6 +326,20 @@ async function switchServerProjectRootViaServer(context, projectRoot, options =
337
326
  }
338
327
  throw new CliError2(`Rig server did not switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no status")}).`, 1);
339
328
  }
329
+ async function ensureTaskLabelsViaServer(context) {
330
+ const payload = await requestServerJson(context, "/api/workspace/task-labels", { method: "POST" });
331
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
332
+ }
333
+ async function listGitHubProjectsViaServer(context, owner) {
334
+ const url = new URL("http://rig.local/api/github/projects");
335
+ url.searchParams.set("owner", owner);
336
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
337
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { projects: [] };
338
+ }
339
+ async function getGitHubProjectStatusFieldViaServer(context, projectId) {
340
+ const payload = await requestServerJson(context, `/api/github/projects/${encodeURIComponent(projectId)}/status-field`);
341
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
342
+ }
340
343
 
341
344
  // packages/cli/src/commands/_pi-install.ts
342
345
  import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
@@ -839,7 +842,7 @@ function parseRepoSlugFromRemote(remoteUrl) {
839
842
  return gitHubMatch ? `${gitHubMatch[1]}/${gitHubMatch[2]}` : null;
840
843
  }
841
844
  function detectOriginRepoSlug(projectRoot) {
842
- const result = spawnSync2("git", ["-C", projectRoot, "remote", "get-url", "origin"], { encoding: "utf8" });
845
+ const result = spawnSync("git", ["-C", projectRoot, "remote", "get-url", "origin"], { encoding: "utf8" });
843
846
  if (result.status !== 0)
844
847
  return null;
845
848
  return parseRepoSlugFromRemote(result.stdout.trim());
@@ -895,11 +898,14 @@ function applyGitHubProjectConfig(source, options) {
895
898
  return source;
896
899
  const projectId = JSON.stringify(options.githubProject);
897
900
  const statusFieldId = JSON.stringify(options.githubProjectStatusField ?? "Status");
901
+ const statuses = options.githubProjectStatuses && Object.keys(options.githubProjectStatuses).length > 0 ? `
902
+ statuses: ${JSON.stringify(options.githubProjectStatuses, null, 8).replace(/\n/g, `
903
+ `)},` : "";
898
904
  return source.replace(` projects: { enabled: false },`, [
899
905
  ` projects: {`,
900
906
  ` enabled: true,`,
901
907
  ` projectId: ${projectId},`,
902
- ` statusFieldId: ${statusFieldId},`,
908
+ ` statusFieldId: ${statusFieldId},${statuses}`,
903
909
  ` },`
904
910
  ].join(`
905
911
  `));
@@ -920,11 +926,11 @@ function checkoutForInit(projectRoot, serverKind, strategy) {
920
926
  }
921
927
  }
922
928
  function detectGhLogin() {
923
- const result = spawnSync2("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
929
+ const result = spawnSync("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
924
930
  return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : null;
925
931
  }
926
932
  function readGhAuthToken() {
927
- const result = spawnSync2("gh", ["auth", "token"], { encoding: "utf8" });
933
+ const result = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
928
934
  if (result.status !== 0 || !result.stdout.trim()) {
929
935
  throw new CliError2(result.stderr.trim() || "Could not read GitHub token from `gh auth token`.", result.status || 1);
930
936
  }
@@ -933,8 +939,15 @@ function readGhAuthToken() {
933
939
  async function loadClackPrompts() {
934
940
  return await import("@clack/prompts");
935
941
  }
942
+ function clackTextOptions(options) {
943
+ return {
944
+ message: options.message,
945
+ ...options.placeholder ? { placeholder: options.placeholder } : {},
946
+ ...(options.initialValue ?? options.defaultValue)?.trim() ? { initialValue: (options.initialValue ?? options.defaultValue).trim() } : {}
947
+ };
948
+ }
936
949
  async function promptRequiredText(prompts, options) {
937
- const value = await prompts.text(options);
950
+ const value = await prompts.text(clackTextOptions(options));
938
951
  if (prompts.isCancel(value))
939
952
  throw new CliError2("Init cancelled.", 1);
940
953
  const text = String(value ?? "").trim();
@@ -943,7 +956,7 @@ async function promptRequiredText(prompts, options) {
943
956
  return text;
944
957
  }
945
958
  async function promptOptionalText(prompts, options) {
946
- const value = await prompts.text(options);
959
+ const value = await prompts.text(clackTextOptions(options));
947
960
  if (prompts.isCancel(value))
948
961
  throw new CliError2("Init cancelled.", 1);
949
962
  return String(value ?? "").trim();
@@ -954,6 +967,132 @@ async function promptSelect(prompts, options) {
954
967
  throw new CliError2("Init cancelled.", 1);
955
968
  return String(value);
956
969
  }
970
+ function repoOwnerFromSlug(repoSlug) {
971
+ return repoSlug.trim().match(/^([^/]+)\/[^/]+$/)?.[1] ?? null;
972
+ }
973
+ function recordArray(value, key) {
974
+ if (!value || typeof value !== "object" || Array.isArray(value))
975
+ return [];
976
+ const raw = value[key];
977
+ return Array.isArray(raw) ? raw.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
978
+ }
979
+ async function listGitHubProjectsForInit(context, owner, token) {
980
+ if (token?.trim()) {
981
+ try {
982
+ return { ok: true, projects: await listGitHubProjectsDirect({ owner, token: token.trim() }) };
983
+ } catch (directError) {
984
+ const serverPayload = await listGitHubProjectsViaServer(context, owner).catch(() => null);
985
+ if (recordArray(serverPayload, "projects").length > 0)
986
+ return serverPayload;
987
+ return { ok: false, error: directError instanceof Error ? directError.message : String(directError), projects: [] };
988
+ }
989
+ }
990
+ return listGitHubProjectsViaServer(context, owner);
991
+ }
992
+ async function getGitHubProjectStatusFieldForInit(context, projectId, token) {
993
+ if (token?.trim()) {
994
+ try {
995
+ return { ok: true, field: await resolveProjectStatusFieldDirect({ projectId, token: token.trim() }) };
996
+ } catch (directError) {
997
+ const serverPayload = await getGitHubProjectStatusFieldViaServer(context, projectId).catch(() => null);
998
+ if (serverPayload && typeof serverPayload === "object" && !Array.isArray(serverPayload) && "field" in serverPayload) {
999
+ return serverPayload;
1000
+ }
1001
+ return { ok: false, error: directError instanceof Error ? directError.message : String(directError) };
1002
+ }
1003
+ }
1004
+ return getGitHubProjectStatusFieldViaServer(context, projectId);
1005
+ }
1006
+ var PROJECT_STATUS_PROMPTS = {
1007
+ running: "Running/In progress",
1008
+ prOpen: "PR open/review",
1009
+ ciFixing: "CI/review fixing",
1010
+ merging: "Merging",
1011
+ done: "Done",
1012
+ needsAttention: "Needs attention"
1013
+ };
1014
+ var DEFAULT_PROJECT_STATUS_OPTIONS = {
1015
+ running: "In Progress",
1016
+ prOpen: "In Review",
1017
+ ciFixing: "In Review",
1018
+ merging: "Merging",
1019
+ done: "Done",
1020
+ needsAttention: "Needs Attention"
1021
+ };
1022
+ async function promptManualProjectStatusMapping(prompts) {
1023
+ const statuses = {};
1024
+ for (const [key, label] of Object.entries(PROJECT_STATUS_PROMPTS)) {
1025
+ const defaultLabel = DEFAULT_PROJECT_STATUS_OPTIONS[key] ?? label;
1026
+ const value = await promptOptionalText(prompts, {
1027
+ message: `Project status option id/name for ${label} (blank for ${defaultLabel})`,
1028
+ placeholder: defaultLabel
1029
+ });
1030
+ statuses[key] = value || defaultLabel;
1031
+ }
1032
+ return statuses;
1033
+ }
1034
+ async function promptGitHubProjectConfig(context, prompts, repoSlug, githubToken) {
1035
+ const projectChoice = await promptSelect(prompts, {
1036
+ message: "GitHub Projects status sync",
1037
+ options: [
1038
+ { value: "off", label: "Off" },
1039
+ { value: "select", label: "Select accessible ProjectV2" },
1040
+ { value: "manual", label: "Enter ProjectV2 ids manually" }
1041
+ ]
1042
+ });
1043
+ if (projectChoice === "off")
1044
+ return { githubProject: "off" };
1045
+ if (projectChoice === "manual") {
1046
+ return {
1047
+ githubProject: await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }),
1048
+ githubProjectStatusField: await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" }),
1049
+ githubProjectStatuses: await promptManualProjectStatusMapping(prompts)
1050
+ };
1051
+ }
1052
+ const owner = repoOwnerFromSlug(repoSlug);
1053
+ if (!owner)
1054
+ throw new CliError2(`Cannot derive GitHub owner from repo slug ${repoSlug}.`, 1);
1055
+ const projectsPayload = await listGitHubProjectsForInit(context, owner, githubToken).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), projects: [] }));
1056
+ const projects = recordArray(projectsPayload, "projects");
1057
+ if (projects.length === 0) {
1058
+ const error = typeof projectsPayload.error === "string" ? ` (${projectsPayload.error})` : "";
1059
+ prompts.outro?.(`No accessible GitHub Projects were returned${error}; falling back to manual ProjectV2 ids.`);
1060
+ return {
1061
+ githubProject: await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }),
1062
+ githubProjectStatusField: await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" }),
1063
+ githubProjectStatuses: await promptManualProjectStatusMapping(prompts)
1064
+ };
1065
+ }
1066
+ const selectedProjectId = await promptSelect(prompts, {
1067
+ message: "GitHub ProjectV2 project",
1068
+ options: [
1069
+ ...projects.map((project) => ({
1070
+ value: String(project.id),
1071
+ label: `${String(project.title ?? "Untitled project")} (#${String(project.number ?? "?")})`,
1072
+ hint: typeof project.url === "string" ? project.url : undefined
1073
+ })),
1074
+ { value: "manual", label: "Enter ProjectV2 id manually" }
1075
+ ]
1076
+ });
1077
+ const projectId = selectedProjectId === "manual" ? await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }) : selectedProjectId;
1078
+ const fieldPayload = await getGitHubProjectStatusFieldForInit(context, projectId, githubToken).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) }));
1079
+ const fieldPayloadRecord = fieldPayload && typeof fieldPayload === "object" && !Array.isArray(fieldPayload) ? fieldPayload : {};
1080
+ const rawField = fieldPayloadRecord.field;
1081
+ const field = rawField && typeof rawField === "object" && !Array.isArray(rawField) ? rawField : null;
1082
+ const fieldId = typeof field?.id === "string" && field.id.trim() ? field.id : await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" });
1083
+ const options = Array.isArray(field?.options) ? field.options.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
1084
+ if (options.length === 0) {
1085
+ return { githubProject: projectId, githubProjectStatusField: fieldId, githubProjectStatuses: await promptManualProjectStatusMapping(prompts) };
1086
+ }
1087
+ const statuses = {};
1088
+ for (const [key, label] of Object.entries(PROJECT_STATUS_PROMPTS)) {
1089
+ statuses[key] = await promptSelect(prompts, {
1090
+ message: `Project status option for ${label}`,
1091
+ options: options.map((option) => ({ value: String(option.id ?? option.name), label: String(option.name ?? option.id) }))
1092
+ });
1093
+ }
1094
+ return { githubProject: projectId, githubProjectStatusField: fieldId, githubProjectStatuses: Object.keys(statuses).length > 0 ? statuses : undefined };
1095
+ }
957
1096
  function sleep2(ms) {
958
1097
  return new Promise((resolve7) => setTimeout(resolve7, ms));
959
1098
  }
@@ -1082,7 +1221,7 @@ async function runControlPlaneInit(context, options) {
1082
1221
  if (token) {
1083
1222
  githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug });
1084
1223
  const apiSessionToken = apiSessionTokenFrom(githubAuth);
1085
- setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token);
1224
+ setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token, projectRoot);
1086
1225
  if (serverKind === "remote") {
1087
1226
  writeRemoteGitHubAuthState(projectRoot, {
1088
1227
  source: authMethod === "gh" ? "gh" : "init-token",
@@ -1107,7 +1246,7 @@ async function runControlPlaneInit(context, options) {
1107
1246
  if (completed) {
1108
1247
  const apiSessionToken = apiSessionTokenFrom(completed);
1109
1248
  if (apiSessionToken) {
1110
- setGitHubBearerTokenForCurrentProcess(apiSessionToken);
1249
+ setGitHubBearerTokenForCurrentProcess(apiSessionToken, projectRoot);
1111
1250
  if (serverKind === "remote") {
1112
1251
  writeRemoteGitHubAuthState(projectRoot, { source: "device", selectedRepo: repo.slug, apiSessionToken, authPayload: completed });
1113
1252
  }
@@ -1131,7 +1270,7 @@ async function runControlPlaneInit(context, options) {
1131
1270
  if (serverKind === "remote" && checkoutPath && token) {
1132
1271
  githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug, projectRoot: checkoutPath });
1133
1272
  const apiSessionToken = apiSessionTokenFrom(githubAuth);
1134
- setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token);
1273
+ setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token, projectRoot);
1135
1274
  writeRemoteGitHubAuthState(projectRoot, { source: authMethod === "gh" ? "gh" : "init-token", selectedRepo: repo.slug, apiSessionToken, authPayload: githubAuth });
1136
1275
  }
1137
1276
  const registered = await registerProjectViaServer(context, {
@@ -1140,6 +1279,12 @@ async function runControlPlaneInit(context, options) {
1140
1279
  });
1141
1280
  const serverRootSwitch = serverKind === "remote" && checkoutPath ? await switchServerProjectRootViaServer(context, checkoutPath) : null;
1142
1281
  const activeProjectRegistration = serverRootSwitch ? await registerProjectViaServer(context, { repoSlug: repo.slug, checkout }) : null;
1282
+ const labelSetup = await ensureTaskLabelsViaServer(context).catch((error) => ({
1283
+ ok: false,
1284
+ ready: false,
1285
+ labelsReady: false,
1286
+ error: error instanceof Error ? error.message : String(error)
1287
+ }));
1143
1288
  const pi = serverKind === "remote" ? await ensureRemotePiRigInstalled({ requestJson: (pathname, init) => requestServerJson(context, pathname, init) }).catch((error) => ({
1144
1289
  remote: true,
1145
1290
  pi: { ok: false, label: "pi", hint: error instanceof Error ? error.message : String(error) },
@@ -1170,6 +1315,7 @@ async function runControlPlaneInit(context, options) {
1170
1315
  githubAuth,
1171
1316
  deviceAuth,
1172
1317
  githubAuthWarning: remoteGhTokenWarning,
1318
+ labelSetup,
1173
1319
  pi,
1174
1320
  doctor
1175
1321
  };
@@ -1324,24 +1470,17 @@ async function runInteractiveControlPlaneInit(context, prompts) {
1324
1470
  throw new CliError2("Remote gh-token import cancelled.", 1);
1325
1471
  }
1326
1472
  }
1327
- const githubToken = authMethod === "token" ? await promptRequiredText(prompts, { message: "GitHub token", placeholder: "ghp_..." }) : undefined;
1328
- const projectChoice = await promptSelect(prompts, {
1329
- message: "GitHub Projects status sync",
1330
- options: [
1331
- { value: "off", label: "Off" },
1332
- { value: "configure", label: "Configure ProjectV2 status field" }
1333
- ]
1334
- });
1335
- const githubProject = projectChoice === "configure" ? await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }) : "off";
1336
- const githubProjectStatusField = projectChoice === "configure" ? await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" }) : undefined;
1473
+ const githubToken = authMethod === "token" ? await promptRequiredText(prompts, { message: "GitHub token", placeholder: "ghp_..." }) : authMethod === "gh" ? readGhAuthToken() : undefined;
1474
+ const projectConfig = await promptGitHubProjectConfig(context, prompts, repoSlug, githubToken);
1337
1475
  const result = await runControlPlaneInit(context, {
1338
1476
  server: serverChoice,
1339
1477
  remoteUrl,
1340
1478
  repoSlug,
1341
1479
  githubToken,
1342
1480
  githubAuthMethod: authMethod,
1343
- githubProject,
1344
- githubProjectStatusField,
1481
+ githubProject: projectConfig.githubProject,
1482
+ githubProjectStatusField: projectConfig.githubProjectStatusField,
1483
+ githubProjectStatuses: projectConfig.githubProjectStatuses,
1345
1484
  remoteCheckout,
1346
1485
  repair,
1347
1486
  privateStateOnly
@@ -90,6 +90,7 @@ import { ensureProjectMainFreshBeforeRun } from "@rig/runtime/control-plane/proj
90
90
 
91
91
  // packages/cli/src/commands/_server-client.ts
92
92
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
93
+ var scopedGitHubBearerTokens = new Map;
93
94
 
94
95
  // packages/cli/src/commands/_preflight.ts
95
96
  async function runProjectMainSyncPreflight(context, options) {