@h-rig/cli 0.0.6-alpha.1 → 0.0.6-alpha.11

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.
@@ -2,9 +2,9 @@
2
2
  var __require = import.meta.require;
3
3
 
4
4
  // packages/cli/src/commands/init.ts
5
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
5
+ import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
6
6
  import { spawnSync as spawnSync2 } from "child_process";
7
- import { resolve as resolve5 } from "path";
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";
@@ -154,6 +154,8 @@ function resolveSelectedConnection(projectRoot, options = {}) {
154
154
 
155
155
  // packages/cli/src/commands/_server-client.ts
156
156
  import { spawnSync } from "child_process";
157
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
158
+ import { resolve as resolve2 } from "path";
157
159
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
158
160
  var cachedGitHubBearerToken;
159
161
  function cleanToken(value) {
@@ -163,9 +165,25 @@ function cleanToken(value) {
163
165
  function setGitHubBearerTokenForCurrentProcess(token) {
164
166
  cachedGitHubBearerToken = cleanToken(token ?? undefined);
165
167
  }
166
- function readGitHubBearerTokenForRemote() {
168
+ function readPrivateRemoteSessionToken(projectRoot) {
169
+ const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
170
+ if (!existsSync2(path))
171
+ return null;
172
+ try {
173
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
174
+ return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+ function readGitHubBearerTokenForRemote(projectRoot) {
167
180
  if (cachedGitHubBearerToken !== undefined)
168
181
  return cachedGitHubBearerToken;
182
+ const privateSession = readPrivateRemoteSessionToken(projectRoot);
183
+ if (privateSession) {
184
+ cachedGitHubBearerToken = privateSession;
185
+ return cachedGitHubBearerToken;
186
+ }
169
187
  const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
170
188
  if (envToken) {
171
189
  cachedGitHubBearerToken = envToken;
@@ -185,7 +203,7 @@ async function ensureServerForCli(projectRoot) {
185
203
  if (selected?.connection.kind === "remote") {
186
204
  return {
187
205
  baseUrl: selected.connection.baseUrl,
188
- authToken: readGitHubBearerTokenForRemote(),
206
+ authToken: readGitHubBearerTokenForRemote(projectRoot),
189
207
  connectionKind: "remote"
190
208
  };
191
209
  }
@@ -249,7 +267,7 @@ async function postGitHubTokenViaServer(context, token, options = {}) {
249
267
  const payload = await requestServerJson(context, "/api/github/auth/token", {
250
268
  method: "POST",
251
269
  headers: { "content-type": "application/json" },
252
- body: JSON.stringify({ token, selectedRepo: options.selectedRepo })
270
+ body: JSON.stringify({ token, selectedRepo: options.selectedRepo, projectRoot: options.projectRoot })
253
271
  });
254
272
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
255
273
  }
@@ -270,18 +288,38 @@ async function registerProjectViaServer(context, input) {
270
288
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
271
289
  }
272
290
  function sleep(ms) {
273
- return new Promise((resolve2) => setTimeout(resolve2, ms));
291
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
292
+ }
293
+ function isRetryableProjectRootSwitchError(error) {
294
+ if (!(error instanceof Error))
295
+ return false;
296
+ const message = error.message.toLowerCase();
297
+ 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");
274
298
  }
275
299
  async function switchServerProjectRootViaServer(context, projectRoot, options = {}) {
276
- const switched = await requestServerJson(context, "/api/server/project-root", {
277
- method: "POST",
278
- headers: { "content-type": "application/json" },
279
- body: JSON.stringify({ projectRoot })
280
- });
281
300
  const timeoutMs = options.timeoutMs ?? 30000;
282
301
  const pollMs = options.pollMs ?? 1000;
283
302
  const deadline = Date.now() + timeoutMs;
284
303
  let lastError;
304
+ let switched = null;
305
+ while (Date.now() < deadline) {
306
+ try {
307
+ switched = await requestServerJson(context, "/api/server/project-root", {
308
+ method: "POST",
309
+ headers: { "content-type": "application/json" },
310
+ body: JSON.stringify({ projectRoot })
311
+ });
312
+ break;
313
+ } catch (error) {
314
+ lastError = error;
315
+ if (!isRetryableProjectRootSwitchError(error))
316
+ throw error;
317
+ await sleep(pollMs);
318
+ }
319
+ }
320
+ if (!switched) {
321
+ 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);
322
+ }
285
323
  while (Date.now() < deadline) {
286
324
  try {
287
325
  const status = await requestServerJson(context, "/api/server/status");
@@ -301,9 +339,9 @@ async function switchServerProjectRootViaServer(context, projectRoot, options =
301
339
  }
302
340
 
303
341
  // packages/cli/src/commands/_pi-install.ts
304
- import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync } from "fs";
342
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
305
343
  import { homedir as homedir2 } from "os";
306
- import { resolve as resolve2 } from "path";
344
+ import { resolve as resolve3 } from "path";
307
345
  var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
308
346
  var LEGACY_PI_RIG_MARKER = `// Managed by Rig. Source package: @rig/pi-rig.
309
347
  export { default } from '@rig/pi-rig';
@@ -318,11 +356,11 @@ async function defaultCommandRunner(command, options = {}) {
318
356
  return { exitCode, stdout, stderr };
319
357
  }
320
358
  function resolvePiRigExtensionPath(homeDir) {
321
- return resolve2(homeDir, ".pi", "agent", "extensions", "pi-rig");
359
+ return resolve3(homeDir, ".pi", "agent", "extensions", "pi-rig");
322
360
  }
323
- function resolvePiRigPackageSource(projectRoot, exists = existsSync2) {
324
- const localPackage = resolve2(projectRoot, "packages", "pi-rig");
325
- if (exists(resolve2(localPackage, "package.json")))
361
+ function resolvePiRigPackageSource(projectRoot, exists = existsSync3) {
362
+ const localPackage = resolve3(projectRoot, "packages", "pi-rig");
363
+ if (exists(resolve3(localPackage, "package.json")))
326
364
  return localPackage;
327
365
  return `npm:${PI_RIG_PACKAGE_NAME}`;
328
366
  }
@@ -373,13 +411,13 @@ async function ensurePiBinaryAvailable(input) {
373
411
  ...next.exitCode === 0 ? {} : { error: (next.stderr || next.stdout).trim() || "pi --version failed after install" }
374
412
  };
375
413
  }
376
- function removeManagedLegacyPiRigBridge(homeDir, exists = existsSync2) {
414
+ function removeManagedLegacyPiRigBridge(homeDir, exists = existsSync3) {
377
415
  const extensionPath = resolvePiRigExtensionPath(homeDir);
378
- const indexPath = resolve2(extensionPath, "index.ts");
416
+ const indexPath = resolve3(extensionPath, "index.ts");
379
417
  if (!exists(indexPath))
380
418
  return;
381
419
  try {
382
- const content = readFileSync2(indexPath, "utf8");
420
+ const content = readFileSync3(indexPath, "utf8");
383
421
  if (content === LEGACY_PI_RIG_MARKER || content.includes("Managed by Rig. Source package: @rig/pi-rig")) {
384
422
  rmSync(extensionPath, { recursive: true, force: true });
385
423
  }
@@ -395,13 +433,13 @@ async function checkPiRigInstall(input = {}) {
395
433
  piRig: { ok: true, label: "pi-rig global extension", detail: extensionPath }
396
434
  };
397
435
  }
398
- const exists = input.exists ?? existsSync2;
436
+ const exists = input.exists ?? existsSync3;
399
437
  const runner = input.commandRunner ?? defaultCommandRunner;
400
438
  const piResult = await safeRun(runner, ["pi", "--version"]);
401
439
  const piListResult = piResult.exitCode === 0 ? await safeRun(runner, ["pi", "list"]) : { exitCode: 1, stdout: "", stderr: "" };
402
440
  const listedPiRig = piListResult.exitCode === 0 && piListContainsPiRig(`${piListResult.stdout}
403
441
  ${piListResult.stderr}`);
404
- const legacyBridge = exists(resolve2(extensionPath, "index.ts"));
442
+ const legacyBridge = exists(resolve3(extensionPath, "index.ts"));
405
443
  const hasPiRig = listedPiRig;
406
444
  return {
407
445
  extensionPath,
@@ -478,7 +516,7 @@ async function buildPiSetupChecks(input = {}) {
478
516
 
479
517
  // packages/cli/src/commands/_snapshot-upload.ts
480
518
  import { mkdir, readdir, readFile, writeFile } from "fs/promises";
481
- import { dirname as dirname2, resolve as resolve3, relative, sep } from "path";
519
+ import { dirname as dirname2, resolve as resolve4, relative, sep } from "path";
482
520
  var SNAPSHOT_ARCHIVE_VERSION = 1;
483
521
  var SNAPSHOT_ARCHIVE_CONTENT_TYPE = "application/vnd.rig.snapshot+json";
484
522
  var DEFAULT_EXCLUDED_DIRECTORIES = new Set([
@@ -500,15 +538,15 @@ function assertManifestPath(root, relativePath) {
500
538
  if (!relativePath || relativePath.startsWith("/") || relativePath.includes("\x00")) {
501
539
  throw new Error(`Invalid snapshot path: ${relativePath}`);
502
540
  }
503
- const resolved = resolve3(root, relativePath);
541
+ const resolved = resolve4(root, relativePath);
504
542
  const relativeToRoot = relative(root, resolved);
505
- if (relativeToRoot.startsWith("..") || relativeToRoot === ".." || resolve3(relativeToRoot) === resolved) {
543
+ if (relativeToRoot.startsWith("..") || relativeToRoot === ".." || resolve4(relativeToRoot) === resolved) {
506
544
  throw new Error(`Snapshot path escapes project root: ${relativePath}`);
507
545
  }
508
546
  return resolved;
509
547
  }
510
548
  async function buildSnapshotUploadManifest(projectRoot, options = {}) {
511
- const root = resolve3(projectRoot);
549
+ const root = resolve4(projectRoot);
512
550
  const excludedDirectories = [...new Set([
513
551
  ...DEFAULT_EXCLUDED_DIRECTORIES,
514
552
  ...options.excludedDirectories ?? []
@@ -520,7 +558,7 @@ async function buildSnapshotUploadManifest(projectRoot, options = {}) {
520
558
  for (const entry of entries) {
521
559
  if (entry.isDirectory() && excludedSet.has(entry.name))
522
560
  continue;
523
- const fullPath = resolve3(dir, entry.name);
561
+ const fullPath = resolve4(dir, entry.name);
524
562
  if (entry.isDirectory()) {
525
563
  await visit(fullPath);
526
564
  continue;
@@ -568,8 +606,8 @@ async function uploadSnapshotArchiveViaServer(context, input) {
568
606
  }
569
607
 
570
608
  // packages/cli/src/commands/_doctor-checks.ts
571
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
572
- import { resolve as resolve4 } from "path";
609
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
610
+ import { resolve as resolve5 } from "path";
573
611
  import { isSupportedBunVersion, MIN_SUPPORTED_BUN_VERSION } from "@rig/runtime/control-plane/setup-version";
574
612
 
575
613
  // packages/cli/src/commands/_parsers.ts
@@ -621,11 +659,11 @@ function repoSlugFromConfig(config) {
621
659
  function loadFallbackConfig(projectRoot) {
622
660
  const candidates = ["rig.config.ts", "rig.config.mts", "rig.config.json"];
623
661
  for (const name of candidates) {
624
- const path = resolve4(projectRoot, name);
625
- if (!existsSync3(path))
662
+ const path = resolve5(projectRoot, name);
663
+ if (!existsSync4(path))
626
664
  continue;
627
665
  try {
628
- const source = readFileSync3(path, "utf8");
666
+ const source = readFileSync4(path, "utf8");
629
667
  if (name.endsWith(".json"))
630
668
  return JSON.parse(source);
631
669
  const owner = source.match(/owner\s*:\s*["']([^"']+)["']/)?.[1];
@@ -704,7 +742,7 @@ async function runRigDoctorChecks(options) {
704
742
  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`)."));
705
743
  const loadedConfig = await loadConfig(projectRoot).catch(() => null);
706
744
  const config = loadedConfig ?? loadFallbackConfig(projectRoot);
707
- const hasConfigFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync3(resolve4(projectRoot, name)));
745
+ const hasConfigFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync4(resolve5(projectRoot, name)));
708
746
  checks.push(config ? check("config", "rig.config loadable", "pass") : check("config", "rig.config loadable", hasConfigFile ? "fail" : "fail", hasConfigFile ? "config file exists but failed to load" : "missing rig.config.ts/json", "Run `rig init` or fix the config error."));
709
747
  const taskSourceKind = config?.taskSource?.kind;
710
748
  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."));
@@ -790,10 +828,10 @@ function countDoctorFailures(checks) {
790
828
  }
791
829
 
792
830
  // packages/cli/src/commands/init.ts
793
- var RIG_CONFIG_PACKAGE_VERSION = "0.0.6-alpha.1";
831
+ var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
794
832
  var RIG_CONFIG_DEV_DEPENDENCIES = {
795
- "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_VERSION}`,
796
- "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_VERSION}`
833
+ "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
834
+ "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
797
835
  };
798
836
  function parseRepoSlugFromRemote(remoteUrl) {
799
837
  const trimmed = remoteUrl.trim();
@@ -813,20 +851,20 @@ function parseRepoSlug(value) {
813
851
  return { owner: match[1], repo: match[2], slug: `${match[1]}/${match[2]}` };
814
852
  }
815
853
  function ensureRigPrivateDirs(projectRoot) {
816
- const rigDir = resolve5(projectRoot, ".rig");
817
- mkdirSync2(resolve5(rigDir, "state"), { recursive: true });
818
- mkdirSync2(resolve5(rigDir, "logs"), { recursive: true });
819
- mkdirSync2(resolve5(rigDir, "runs"), { recursive: true });
820
- mkdirSync2(resolve5(rigDir, "tmp"), { recursive: true });
821
- mkdirSync2(resolve5(projectRoot, "artifacts"), { recursive: true });
822
- const taskConfigPath = resolve5(rigDir, "task-config.json");
823
- if (!existsSync4(taskConfigPath))
854
+ const rigDir = resolve6(projectRoot, ".rig");
855
+ mkdirSync2(resolve6(rigDir, "state"), { recursive: true });
856
+ mkdirSync2(resolve6(rigDir, "logs"), { recursive: true });
857
+ mkdirSync2(resolve6(rigDir, "runs"), { recursive: true });
858
+ mkdirSync2(resolve6(rigDir, "tmp"), { recursive: true });
859
+ mkdirSync2(resolve6(projectRoot, "artifacts"), { recursive: true });
860
+ const taskConfigPath = resolve6(rigDir, "task-config.json");
861
+ if (!existsSync5(taskConfigPath))
824
862
  writeFileSync2(taskConfigPath, `{}
825
863
  `, "utf-8");
826
864
  }
827
865
  function ensureGitignoreEntries(projectRoot) {
828
- const path = resolve5(projectRoot, ".gitignore");
829
- const existing = existsSync4(path) ? readFileSync4(path, "utf8") : "";
866
+ const path = resolve6(projectRoot, ".gitignore");
867
+ const existing = existsSync5(path) ? readFileSync5(path, "utf8") : "";
830
868
  const entries = [".rig/state/", ".rig/logs/", ".rig/runs/", ".rig/tmp/"];
831
869
  const missing = entries.filter((entry) => !existing.split(/\r?\n/).includes(entry));
832
870
  if (missing.length === 0)
@@ -839,14 +877,14 @@ function ensureGitignoreEntries(projectRoot) {
839
877
  `, "utf8");
840
878
  }
841
879
  function ensureRigConfigPackageDependencies(projectRoot) {
842
- const path = resolve5(projectRoot, "package.json");
843
- const existing = existsSync4(path) ? JSON.parse(readFileSync4(path, "utf8")) : {};
880
+ const path = resolve6(projectRoot, "package.json");
881
+ const existing = existsSync5(path) ? JSON.parse(readFileSync5(path, "utf8")) : {};
844
882
  const devDependencies = existing.devDependencies && typeof existing.devDependencies === "object" && !Array.isArray(existing.devDependencies) ? { ...existing.devDependencies } : {};
845
883
  for (const [name, spec] of Object.entries(RIG_CONFIG_DEV_DEPENDENCIES)) {
846
884
  devDependencies[name] = spec;
847
885
  }
848
886
  const next = {
849
- ...existsSync4(path) ? existing : { name: "rig-project", private: true },
887
+ ...existsSync5(path) ? existing : { name: "rig-project", private: true },
850
888
  devDependencies
851
889
  };
852
890
  writeFileSync2(path, `${JSON.stringify(next, null, 2)}
@@ -916,15 +954,71 @@ async function promptSelect(prompts, options) {
916
954
  throw new CliError2("Init cancelled.", 1);
917
955
  return String(value);
918
956
  }
919
- async function pollDeviceAuthOnce(context, pollId) {
957
+ function sleep2(ms) {
958
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
959
+ }
960
+ function positiveIntFromEnv(name, fallback) {
961
+ const value = Number.parseInt(process.env[name] ?? "", 10);
962
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
963
+ }
964
+ function apiSessionTokenFrom(payload) {
965
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
966
+ return null;
967
+ const token = payload.apiSessionToken;
968
+ return typeof token === "string" && token.trim() ? token.trim() : null;
969
+ }
970
+ function cleanPayloadString(value) {
971
+ return typeof value === "string" && value.trim() ? value.trim() : null;
972
+ }
973
+ function remoteGitHubAuthMetadata(payload) {
974
+ if (!payload)
975
+ return {};
976
+ const userNamespace = payload.userNamespace && typeof payload.userNamespace === "object" && !Array.isArray(payload.userNamespace) ? payload.userNamespace : null;
977
+ return {
978
+ ...cleanPayloadString(payload.login) ? { login: cleanPayloadString(payload.login) } : {},
979
+ ...cleanPayloadString(payload.userId) ? { userId: cleanPayloadString(payload.userId) } : {},
980
+ ...cleanPayloadString(userNamespace?.key) ? { userNamespaceKey: cleanPayloadString(userNamespace?.key) } : {},
981
+ ...cleanPayloadString(userNamespace?.root) ? { userNamespaceRoot: cleanPayloadString(userNamespace?.root) } : {},
982
+ ...cleanPayloadString(userNamespace?.checkoutBaseDir) ? { checkoutBaseDir: cleanPayloadString(userNamespace?.checkoutBaseDir) } : {},
983
+ ...cleanPayloadString(userNamespace?.snapshotBaseDir) ? { snapshotBaseDir: cleanPayloadString(userNamespace?.snapshotBaseDir) } : {}
984
+ };
985
+ }
986
+ function writeRemoteGitHubAuthState(projectRoot, input) {
987
+ writeFileSync2(resolve6(projectRoot, ".rig", "state", "github-auth.json"), `${JSON.stringify({
988
+ authenticated: true,
989
+ source: input.source,
990
+ storedOnServer: true,
991
+ selectedRepo: input.selectedRepo,
992
+ ...remoteGitHubAuthMetadata(input.authPayload ?? null),
993
+ ...input.apiSessionToken ? { apiSessionToken: input.apiSessionToken } : {},
994
+ updatedAt: new Date().toISOString()
995
+ }, null, 2)}
996
+ `, "utf8");
997
+ }
998
+ async function pollDeviceAuthUntilComplete(context, pollId, firstPayload) {
920
999
  if (typeof pollId !== "string" || !pollId.trim())
921
1000
  return null;
922
- const payload = await requestServerJson(context, "/api/github/auth/device/poll", {
923
- method: "POST",
924
- headers: { "content-type": "application/json" },
925
- body: JSON.stringify({ pollId })
926
- }).catch(() => null);
927
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : null;
1001
+ const intervalSeconds = typeof firstPayload.interval === "number" && Number.isFinite(firstPayload.interval) && firstPayload.interval > 0 ? firstPayload.interval : 5;
1002
+ const timeoutMs = positiveIntFromEnv("RIG_DEVICE_AUTH_POLL_TIMEOUT_MS", 300000);
1003
+ const intervalMs = positiveIntFromEnv("RIG_DEVICE_AUTH_POLL_INTERVAL_MS", Math.max(1000, intervalSeconds * 1000));
1004
+ const deadline = Date.now() + timeoutMs;
1005
+ let last = null;
1006
+ do {
1007
+ const payload = await requestServerJson(context, "/api/github/auth/device/poll", {
1008
+ method: "POST",
1009
+ headers: { "content-type": "application/json" },
1010
+ body: JSON.stringify({ pollId })
1011
+ }).catch(() => null);
1012
+ last = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : null;
1013
+ const status = typeof last?.status === "string" ? last.status : null;
1014
+ if (status === "signed-in" || status === "expired" || status === "cancelled" || status === "failed") {
1015
+ return last;
1016
+ }
1017
+ if (timeoutMs <= 0)
1018
+ return last;
1019
+ await sleep2(intervalMs);
1020
+ } while (Date.now() < deadline);
1021
+ return last;
928
1022
  }
929
1023
  async function runControlPlaneInit(context, options) {
930
1024
  const projectRoot = context.projectRoot;
@@ -947,9 +1041,9 @@ async function runControlPlaneInit(context, options) {
947
1041
  });
948
1042
  ensureRigPrivateDirs(projectRoot);
949
1043
  ensureGitignoreEntries(projectRoot);
950
- const configTsPath = resolve5(projectRoot, "rig.config.ts");
951
- const configJsonPath = resolve5(projectRoot, "rig.config.json");
952
- const configExists = existsSync4(configTsPath) || existsSync4(configJsonPath);
1044
+ const configTsPath = resolve6(projectRoot, "rig.config.ts");
1045
+ const configJsonPath = resolve6(projectRoot, "rig.config.json");
1046
+ const configExists = existsSync5(configTsPath) || existsSync5(configJsonPath);
953
1047
  if (!options.privateStateOnly) {
954
1048
  if (configExists && !options.repair) {
955
1049
  if (context.outputMode !== "json")
@@ -965,7 +1059,7 @@ async function runControlPlaneInit(context, options) {
965
1059
  }
966
1060
  ensureRigConfigPackageDependencies(projectRoot);
967
1061
  }
968
- writeFileSync2(resolve5(projectRoot, ".rig", "state", "project-link.json"), `${JSON.stringify({ repoSlug: repo.slug, connection: connectionAlias, linkedAt: new Date().toISOString() }, null, 2)}
1062
+ writeFileSync2(resolve6(projectRoot, ".rig", "state", "project-link.json"), `${JSON.stringify({ repoSlug: repo.slug, connection: connectionAlias, linkedAt: new Date().toISOString() }, null, 2)}
969
1063
  `, "utf8");
970
1064
  const checkout = checkoutForInit(projectRoot, serverKind, options.remoteCheckout);
971
1065
  let uploadedSnapshot = null;
@@ -987,10 +1081,15 @@ async function runControlPlaneInit(context, options) {
987
1081
  const token = authMethod === "gh" && !options.githubToken ? readGhAuthToken() : options.githubToken?.trim();
988
1082
  if (token) {
989
1083
  githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug });
990
- setGitHubBearerTokenForCurrentProcess(token);
1084
+ const apiSessionToken = apiSessionTokenFrom(githubAuth);
1085
+ setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token);
991
1086
  if (serverKind === "remote") {
992
- writeFileSync2(resolve5(projectRoot, ".rig", "state", "github-auth.json"), `${JSON.stringify({ authenticated: true, source: authMethod === "gh" ? "gh" : "init-token", storedOnServer: true, selectedRepo: repo.slug, updatedAt: new Date().toISOString() }, null, 2)}
993
- `, "utf8");
1087
+ writeRemoteGitHubAuthState(projectRoot, {
1088
+ source: authMethod === "gh" ? "gh" : "init-token",
1089
+ selectedRepo: repo.slug,
1090
+ apiSessionToken,
1091
+ authPayload: githubAuth
1092
+ });
994
1093
  }
995
1094
  } else if (authMethod === "device") {
996
1095
  const payload = await requestServerJson(context, "/api/github/auth/device/start", {
@@ -999,9 +1098,22 @@ async function runControlPlaneInit(context, options) {
999
1098
  body: JSON.stringify({ repoSlug: repo.slug })
1000
1099
  });
1001
1100
  deviceAuth = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
1002
- const completed = await pollDeviceAuthOnce(context, deviceAuth.pollId);
1003
- if (completed)
1101
+ if (context.outputMode !== "json") {
1102
+ const verificationUri = String(deviceAuth.verificationUri ?? deviceAuth.verification_uri ?? deviceAuth.verification_uri_complete ?? "the verification URL returned by the server");
1103
+ const userCode = String(deviceAuth.userCode ?? deviceAuth.user_code ?? "the returned user code");
1104
+ console.log(`GitHub device flow: open ${verificationUri} and enter ${userCode}. Waiting for authorization...`);
1105
+ }
1106
+ const completed = await pollDeviceAuthUntilComplete(context, deviceAuth.pollId, deviceAuth);
1107
+ if (completed) {
1108
+ const apiSessionToken = apiSessionTokenFrom(completed);
1109
+ if (apiSessionToken) {
1110
+ setGitHubBearerTokenForCurrentProcess(apiSessionToken);
1111
+ if (serverKind === "remote") {
1112
+ writeRemoteGitHubAuthState(projectRoot, { source: "device", selectedRepo: repo.slug, apiSessionToken, authPayload: completed });
1113
+ }
1114
+ }
1004
1115
  deviceAuth = { ...deviceAuth, poll: completed, completed: completed.status === "signed-in" };
1116
+ }
1005
1117
  }
1006
1118
  let remoteCheckoutPreparation = null;
1007
1119
  if (serverKind === "remote" && options.remoteCheckout?.kind !== "uploaded-snapshot") {
@@ -1021,6 +1133,12 @@ async function runControlPlaneInit(context, options) {
1021
1133
  });
1022
1134
  const checkoutPath = typeof checkout.path === "string" ? checkout.path : null;
1023
1135
  const serverRootSwitch = serverKind === "remote" && checkoutPath ? await switchServerProjectRootViaServer(context, checkoutPath) : null;
1136
+ if (serverRootSwitch && token) {
1137
+ githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug, projectRoot: checkoutPath ?? undefined });
1138
+ const apiSessionToken = apiSessionTokenFrom(githubAuth);
1139
+ setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token);
1140
+ writeRemoteGitHubAuthState(projectRoot, { source: authMethod === "gh" ? "gh" : "init-token", selectedRepo: repo.slug, apiSessionToken, authPayload: githubAuth });
1141
+ }
1024
1142
  const activeProjectRegistration = serverRootSwitch ? await registerProjectViaServer(context, { repoSlug: repo.slug, checkout }) : null;
1025
1143
  const pi = serverKind === "remote" ? await ensureRemotePiRigInstalled({ requestJson: (pathname, init) => requestServerJson(context, pathname, init) }).catch((error) => ({
1026
1144
  remote: true,
@@ -1130,7 +1248,7 @@ function parseInitOptions(args) {
1130
1248
  async function runInteractiveControlPlaneInit(context, prompts) {
1131
1249
  prompts.intro?.("Initialize a Rig control-plane project");
1132
1250
  const projectRoot = context.projectRoot;
1133
- const existingConfig = existsSync4(resolve5(projectRoot, "rig.config.ts")) || existsSync4(resolve5(projectRoot, "rig.config.json"));
1251
+ const existingConfig = existsSync5(resolve6(projectRoot, "rig.config.ts")) || existsSync5(resolve6(projectRoot, "rig.config.json"));
1134
1252
  let repair = false;
1135
1253
  let privateStateOnly = false;
1136
1254
  if (existingConfig) {
@@ -1230,7 +1348,7 @@ async function runInteractiveControlPlaneInit(context, prompts) {
1230
1348
  });
1231
1349
  const details = result.details && typeof result.details === "object" && !Array.isArray(result.details) ? result.details : {};
1232
1350
  const deviceAuth = details.deviceAuth && typeof details.deviceAuth === "object" && !Array.isArray(details.deviceAuth) ? details.deviceAuth : null;
1233
- const deviceMessage = deviceAuth ? ` GitHub device flow: open ${String(deviceAuth.verification_uri ?? deviceAuth.verification_uri_complete ?? "the verification URL returned by the server")} and enter ${String(deviceAuth.user_code ?? "the returned user code")}.` : "";
1351
+ const deviceMessage = deviceAuth ? ` GitHub device flow: open ${String(deviceAuth.verificationUri ?? deviceAuth.verification_uri ?? deviceAuth.verification_uri_complete ?? "the verification URL returned by the server")} and enter ${String(deviceAuth.userCode ?? deviceAuth.user_code ?? "the returned user code")}.` : "";
1234
1352
  prompts.outro?.(`Rig project initialized.${deviceMessage} Next: rig doctor && rig task list`);
1235
1353
  return result;
1236
1354
  }
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // packages/cli/src/commands/run.ts
3
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
4
- import { resolve as resolve2 } from "path";
3
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
4
+ import { resolve as resolve3 } from "path";
5
5
  import { createInterface as createInterface2 } from "readline/promises";
6
6
 
7
7
  // packages/cli/src/runner.ts
@@ -64,6 +64,7 @@ import {
64
64
  listOpenEpics,
65
65
  resolveDefaultEpic,
66
66
  runResume,
67
+ runRestart,
67
68
  runStatus,
68
69
  runStop,
69
70
  startRun,
@@ -85,6 +86,8 @@ function parsePositiveInt(value, option, fallback) {
85
86
 
86
87
  // packages/cli/src/commands/_server-client.ts
87
88
  import { spawnSync } from "child_process";
89
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
90
+ import { resolve as resolve2 } from "path";
88
91
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
89
92
 
90
93
  // packages/cli/src/commands/_connection-state.ts
@@ -175,9 +178,25 @@ function cleanToken(value) {
175
178
  const trimmed = value?.trim();
176
179
  return trimmed ? trimmed : null;
177
180
  }
178
- function readGitHubBearerTokenForRemote() {
181
+ function readPrivateRemoteSessionToken(projectRoot) {
182
+ const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
183
+ if (!existsSync2(path))
184
+ return null;
185
+ try {
186
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
187
+ return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+ function readGitHubBearerTokenForRemote(projectRoot) {
179
193
  if (cachedGitHubBearerToken !== undefined)
180
194
  return cachedGitHubBearerToken;
195
+ const privateSession = readPrivateRemoteSessionToken(projectRoot);
196
+ if (privateSession) {
197
+ cachedGitHubBearerToken = privateSession;
198
+ return cachedGitHubBearerToken;
199
+ }
181
200
  const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
182
201
  if (envToken) {
183
202
  cachedGitHubBearerToken = envToken;
@@ -197,7 +216,7 @@ async function ensureServerForCli(projectRoot) {
197
216
  if (selected?.connection.kind === "remote") {
198
217
  return {
199
218
  baseUrl: selected.connection.baseUrl,
200
- authToken: readGitHubBearerTokenForRemote(),
219
+ authToken: readGitHubBearerTokenForRemote(projectRoot),
201
220
  connectionKind: "remote"
202
221
  };
203
222
  }
@@ -388,6 +407,17 @@ async function attachRunOperatorView(context, input) {
388
407
  }
389
408
 
390
409
  // packages/cli/src/commands/run.ts
410
+ function normalizeRemoteRunDetails(payload) {
411
+ const run = payload.run;
412
+ if (!run || typeof run !== "object" || Array.isArray(run))
413
+ return null;
414
+ return {
415
+ ...run,
416
+ ...Array.isArray(payload.timeline) ? { timeline: payload.timeline } : {},
417
+ ...Array.isArray(payload.approvals) ? { approvals: payload.approvals } : {},
418
+ ...Array.isArray(payload.userInputs) ? { userInputs: payload.userInputs } : {}
419
+ };
420
+ }
391
421
  function shouldPromptForEpicSelection(context, command, promptEpic, noEpicPrompt) {
392
422
  if (noEpicPrompt) {
393
423
  return false;
@@ -526,7 +556,7 @@ async function executeRun(context, args) {
526
556
  if (!run.value) {
527
557
  throw new CliError2("run show requires --run <id>.");
528
558
  }
529
- const record = readAuthorityRun(context.projectRoot, run.value);
559
+ const record = readAuthorityRun(context.projectRoot, run.value) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, run.value).catch(() => ({})));
530
560
  if (!record) {
531
561
  throw new CliError2(`Run not found: ${run.value}`, 2);
532
562
  }
@@ -545,7 +575,7 @@ async function executeRun(context, args) {
545
575
  if (!run.value) {
546
576
  throw new CliError2("run timeline requires --run <id>.");
547
577
  }
548
- const timelinePath = resolve2(resolveAuthorityRunDir(context.projectRoot, run.value), "timeline.jsonl");
578
+ const timelinePath = resolve3(resolveAuthorityRunDir(context.projectRoot, run.value), "timeline.jsonl");
549
579
  const printEvents = () => {
550
580
  const events2 = readJsonlFile(timelinePath);
551
581
  if (context.outputMode === "text") {
@@ -557,12 +587,12 @@ async function executeRun(context, args) {
557
587
  };
558
588
  const events = printEvents();
559
589
  if (follow.value && context.outputMode === "text") {
560
- let lastLength = existsSync2(timelinePath) ? readFileSync2(timelinePath, "utf8").length : 0;
590
+ let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
561
591
  while (true) {
562
592
  await Bun.sleep(1000);
563
- if (!existsSync2(timelinePath))
593
+ if (!existsSync3(timelinePath))
564
594
  continue;
565
- const next = readFileSync2(timelinePath, "utf8");
595
+ const next = readFileSync3(timelinePath, "utf8");
566
596
  if (next.length <= lastLength)
567
597
  continue;
568
598
  const delta = next.slice(lastLength);
@@ -707,6 +737,20 @@ async function executeRun(context, args) {
707
737
  }
708
738
  return { ok: true, group: "run", command, details: resumed };
709
739
  }
740
+ case "restart": {
741
+ requireNoExtraArgs(rest, "bun run rig run restart");
742
+ if (context.dryRun) {
743
+ if (context.outputMode === "text") {
744
+ console.log("[dry-run] rig run restart");
745
+ }
746
+ return { ok: true, group: "run", command };
747
+ }
748
+ const restarted = await runRestart(context.projectRoot, runtimeContext);
749
+ if (context.outputMode === "text") {
750
+ console.log(`Restarted run: ${restarted.runId}`);
751
+ }
752
+ return { ok: true, group: "run", command, details: restarted };
753
+ }
710
754
  case "stop": {
711
755
  const runOption = takeOption(rest, "--run");
712
756
  const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;