@h-rig/cli 0.0.6-alpha.81 → 0.0.6-alpha.83

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 (100) hide show
  1. package/dist/bin/build-rig-binaries.js +40 -8
  2. package/dist/bin/rig.js +2807 -1443
  3. package/dist/src/app/board.js +62 -13
  4. package/dist/src/app-opentui/adapters/command.d.ts +2 -0
  5. package/dist/src/app-opentui/adapters/command.js +329 -0
  6. package/dist/src/app-opentui/adapters/common.js +2 -2
  7. package/dist/src/app-opentui/adapters/doctor.js +63 -14
  8. package/dist/src/app-opentui/adapters/fleet.js +84 -20
  9. package/dist/src/app-opentui/adapters/inbox.js +83 -19
  10. package/dist/src/app-opentui/adapters/init.js +87 -23
  11. package/dist/src/app-opentui/adapters/pi-attach.js +102 -36
  12. package/dist/src/app-opentui/adapters/run-detail.js +83 -19
  13. package/dist/src/app-opentui/adapters/server.js +100 -23
  14. package/dist/src/app-opentui/adapters/tasks.d.ts +11 -0
  15. package/dist/src/app-opentui/adapters/tasks.js +742 -94
  16. package/dist/src/app-opentui/bootstrap.d.ts +1 -6
  17. package/dist/src/app-opentui/bootstrap.js +13776 -12369
  18. package/dist/src/app-opentui/command-pty-host.d.ts +62 -0
  19. package/dist/src/app-opentui/command-pty-host.js +248 -0
  20. package/dist/src/app-opentui/events.js +1 -1
  21. package/dist/src/app-opentui/focus-manager.d.ts +14 -0
  22. package/dist/src/app-opentui/focus-manager.js +24 -0
  23. package/dist/src/app-opentui/index.js +3806 -2878
  24. package/dist/src/app-opentui/intent.js +154 -48
  25. package/dist/src/app-opentui/keymap.d.ts +20 -0
  26. package/dist/src/app-opentui/keymap.js +707 -0
  27. package/dist/src/app-opentui/launch-routing.d.ts +16 -0
  28. package/dist/src/app-opentui/launch-routing.js +55 -0
  29. package/dist/src/app-opentui/layout.js +2 -2
  30. package/dist/src/app-opentui/pi-host-child.js +66 -16
  31. package/dist/src/app-opentui/pi-pty-host.d.ts +9 -0
  32. package/dist/src/app-opentui/pi-pty-host.js +15 -13
  33. package/dist/src/app-opentui/registry.js +3228 -2396
  34. package/dist/src/app-opentui/render/ascii-fleet.d.ts +15 -0
  35. package/dist/src/app-opentui/render/ascii-fleet.js +82 -0
  36. package/dist/src/app-opentui/render/graphics.d.ts +1 -1
  37. package/dist/src/app-opentui/render/graphics.js +131 -16
  38. package/dist/src/app-opentui/render/image-visual-layer-worker.js +413 -958
  39. package/dist/src/app-opentui/render/image-visual-layer.d.ts +19 -10
  40. package/dist/src/app-opentui/render/image-visual-layer.js +391 -357
  41. package/dist/src/app-opentui/render/panel-layout.d.ts +38 -0
  42. package/dist/src/app-opentui/render/panel-layout.js +48 -0
  43. package/dist/src/app-opentui/render/panels.d.ts +2 -0
  44. package/dist/src/app-opentui/render/panels.js +62 -29
  45. package/dist/src/app-opentui/render/preloader.d.ts +10 -0
  46. package/dist/src/app-opentui/render/preloader.js +163 -0
  47. package/dist/src/app-opentui/render/scene.d.ts +3 -0
  48. package/dist/src/app-opentui/render/scene.js +12 -0
  49. package/dist/src/app-opentui/render/text.js +5 -1
  50. package/dist/src/app-opentui/render/type-bar.d.ts +2 -1
  51. package/dist/src/app-opentui/render/type-bar.js +51 -16
  52. package/dist/src/app-opentui/runtime-resources.d.ts +16 -0
  53. package/dist/src/app-opentui/runtime-resources.js +62 -0
  54. package/dist/src/app-opentui/runtime.js +2329 -1512
  55. package/dist/src/app-opentui/scenes/command.d.ts +3 -0
  56. package/dist/src/app-opentui/scenes/command.js +103 -0
  57. package/dist/src/app-opentui/scenes/doctor.d.ts +2 -1
  58. package/dist/src/app-opentui/scenes/doctor.js +53 -14
  59. package/dist/src/app-opentui/scenes/error.js +44 -14
  60. package/dist/src/app-opentui/scenes/fleet.js +40 -21
  61. package/dist/src/app-opentui/scenes/handoff.js +142 -27
  62. package/dist/src/app-opentui/scenes/help.js +63 -48
  63. package/dist/src/app-opentui/scenes/inbox.d.ts +2 -1
  64. package/dist/src/app-opentui/scenes/inbox.js +64 -17
  65. package/dist/src/app-opentui/scenes/init.d.ts +2 -1
  66. package/dist/src/app-opentui/scenes/init.js +62 -21
  67. package/dist/src/app-opentui/scenes/main.d.ts +2 -1
  68. package/dist/src/app-opentui/scenes/main.js +80 -24
  69. package/dist/src/app-opentui/scenes/run-detail.d.ts +2 -1
  70. package/dist/src/app-opentui/scenes/run-detail.js +39 -25
  71. package/dist/src/app-opentui/scenes/server.d.ts +2 -1
  72. package/dist/src/app-opentui/scenes/server.js +71 -20
  73. package/dist/src/app-opentui/scenes/tasks.js +44 -22
  74. package/dist/src/app-opentui/state.js +70 -30
  75. package/dist/src/app-opentui/terminal-capabilities.d.ts +7 -0
  76. package/dist/src/app-opentui/terminal-capabilities.js +15 -0
  77. package/dist/src/app-opentui/types.d.ts +10 -2
  78. package/dist/src/commands/_doctor-checks.js +62 -13
  79. package/dist/src/commands/_operator-view.js +63 -13
  80. package/dist/src/commands/_pi-frontend.js +63 -13
  81. package/dist/src/commands/_preflight.js +64 -14
  82. package/dist/src/commands/_server-client.js +82 -18
  83. package/dist/src/commands/_snapshot-upload.js +62 -13
  84. package/dist/src/commands/connect.js +7 -1
  85. package/dist/src/commands/doctor.js +62 -13
  86. package/dist/src/commands/github.js +144 -23
  87. package/dist/src/commands/inbox.js +62 -13
  88. package/dist/src/commands/init.js +82 -18
  89. package/dist/src/commands/inspect.js +62 -13
  90. package/dist/src/commands/run.js +66 -13
  91. package/dist/src/commands/server.js +69 -14
  92. package/dist/src/commands/setup.js +62 -13
  93. package/dist/src/commands/stats.js +62 -13
  94. package/dist/src/commands/task-run-driver.js +62 -13
  95. package/dist/src/commands/task.js +65 -14
  96. package/dist/src/commands.js +227 -92
  97. package/dist/src/index.js +234 -99
  98. package/package.json +8 -9
  99. package/dist/src/app-opentui/render/image-visual-layer-native-canvas.d.ts +0 -2
  100. package/dist/src/app-opentui/render/image-visual-layer-native-canvas.js +0 -1480
@@ -459,7 +459,13 @@ async function executeConnectionCommand(context, args, options) {
459
459
  if (!state.connections[alias])
460
460
  throw new CliError(`Unknown Rig server: ${alias}`, 1, { hint: "Run `rig server list` to see saved aliases, or save one with `rig server add <alias> <url>`." });
461
461
  }
462
- const repoState = { selected: alias, linkedAt: new Date().toISOString() };
462
+ const previousRepo = readRepoConnection(context.projectRoot);
463
+ const repoState = {
464
+ selected: alias,
465
+ ...previousRepo?.project ? { project: previousRepo.project } : {},
466
+ linkedAt: new Date().toISOString(),
467
+ ...previousRepo?.serverProjectRoot && previousRepo.selected === alias ? { serverProjectRoot: previousRepo.serverProjectRoot } : {}
468
+ };
463
469
  writeRepoConnection(context.projectRoot, repoState);
464
470
  printJsonOrText(context, repoState, formatSuccessCard("Rig server selected", [
465
471
  ["selected", alias],
@@ -495,17 +501,21 @@ function cleanToken(value) {
495
501
  const trimmed = value?.trim();
496
502
  return trimmed ? trimmed : null;
497
503
  }
498
- function readPrivateRemoteSessionToken(projectRoot) {
504
+ function readRemoteAuthState(projectRoot) {
499
505
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
500
506
  if (!existsSync2(path))
501
507
  return null;
502
508
  try {
503
509
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
504
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
510
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
505
511
  } catch {
506
512
  return null;
507
513
  }
508
514
  }
515
+ function readPrivateRemoteSessionToken(projectRoot) {
516
+ const parsed = readRemoteAuthState(projectRoot);
517
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
518
+ }
509
519
  function readGitHubBearerTokenForRemote(projectRoot) {
510
520
  const scopedKey = resolve2(projectRoot);
511
521
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -516,15 +526,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
516
526
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
517
527
  }
518
528
  function readStoredGitHubAuthToken(projectRoot) {
519
- const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
520
- if (!existsSync2(path))
529
+ const parsed = readRemoteAuthState(projectRoot);
530
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
531
+ }
532
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
533
+ const repo = readRepoConnection(projectRoot);
534
+ const slug = repo?.project?.trim();
535
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
521
536
  return null;
522
- try {
523
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
524
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
525
- } catch {
537
+ const auth = readRemoteAuthState(projectRoot);
538
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
539
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
540
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
526
541
  return null;
527
- }
542
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
543
+ if (!checkoutBaseDir)
544
+ return null;
545
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
546
+ writeRepoServerProjectRoot(projectRoot, inferred);
547
+ return inferred;
528
548
  }
529
549
  function readLocalConnectionFallbackToken(projectRoot) {
530
550
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -535,7 +555,7 @@ async function ensureServerForCli(projectRoot) {
535
555
  if (selected?.connection.kind === "remote") {
536
556
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
537
557
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
538
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
558
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
539
559
  return {
540
560
  baseUrl: selected.connection.baseUrl,
541
561
  authToken,
@@ -564,7 +584,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
564
584
  if (!slug)
565
585
  return null;
566
586
  try {
567
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
587
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
588
+ if (authToken && queryAuthFallbackEnabled())
589
+ url.searchParams.set("rt", authToken);
590
+ const response = await fetch(url, {
568
591
  headers: mergeHeaders(undefined, authToken)
569
592
  });
570
593
  if (!response.ok)
@@ -581,10 +604,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
581
604
  return null;
582
605
  }
583
606
  }
607
+ function mergeCookie(existing, name, value) {
608
+ const encoded = `${name}=${encodeURIComponent(value)}`;
609
+ if (!existing?.trim())
610
+ return encoded;
611
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
612
+ return [...parts, encoded].join("; ");
613
+ }
614
+ function queryAuthFallbackEnabled(env = process.env) {
615
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
616
+ }
584
617
  function mergeHeaders(headers, authToken) {
585
618
  const merged = new Headers(headers);
586
619
  if (authToken) {
587
- merged.set("authorization", `Bearer ${authToken}`);
620
+ const bearer = `Bearer ${authToken}`;
621
+ merged.set("authorization", bearer);
622
+ merged.set("x-auth", bearer);
623
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
588
624
  }
589
625
  return merged;
590
626
  }
@@ -645,15 +681,34 @@ async function buildServerFailureContext(projectRoot, server) {
645
681
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
646
682
  };
647
683
  }
684
+ function isLoopbackRemoteBaseUrl(baseUrl) {
685
+ try {
686
+ const host = new URL(baseUrl).hostname.toLowerCase();
687
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
688
+ } catch {
689
+ return false;
690
+ }
691
+ }
692
+ function canUseRemoteWithoutProjectRoot(pathname) {
693
+ return pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/server/project-root" || pathname.startsWith("/api/github/auth/") || pathname === "/api/projects" || pathname.startsWith("/api/projects/");
694
+ }
648
695
  async function requestServerJson(context, pathname, init = {}) {
649
696
  const server = await ensureServerForCli(context.projectRoot);
697
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
698
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
699
+ const repo = readRepoConnection(context.projectRoot);
700
+ throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
701
+ }
650
702
  const headers = mergeHeaders(init.headers, server.authToken);
651
703
  if (server.serverProjectRoot)
652
704
  headers.set("x-rig-project-root", server.serverProjectRoot);
705
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
706
+ requestUrl.searchParams.set("rt", server.authToken);
707
+ }
653
708
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
654
709
  let response;
655
710
  try {
656
- response = await fetch(`${server.baseUrl}${pathname}`, {
711
+ response = await fetch(requestUrl, {
657
712
  ...init,
658
713
  headers
659
714
  });
@@ -298,17 +298,21 @@ function cleanToken(value) {
298
298
  const trimmed = value?.trim();
299
299
  return trimmed ? trimmed : null;
300
300
  }
301
- function readPrivateRemoteSessionToken(projectRoot) {
301
+ function readRemoteAuthState(projectRoot) {
302
302
  const path = resolve4(projectRoot, ".rig", "state", "github-auth.json");
303
303
  if (!existsSync3(path))
304
304
  return null;
305
305
  try {
306
306
  const parsed = JSON.parse(readFileSync3(path, "utf8"));
307
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
307
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
308
308
  } catch {
309
309
  return null;
310
310
  }
311
311
  }
312
+ function readPrivateRemoteSessionToken(projectRoot) {
313
+ const parsed = readRemoteAuthState(projectRoot);
314
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
315
+ }
312
316
  function readGitHubBearerTokenForRemote(projectRoot) {
313
317
  const scopedKey = resolve4(projectRoot);
314
318
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -319,15 +323,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
319
323
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
320
324
  }
321
325
  function readStoredGitHubAuthToken(projectRoot) {
322
- const path = resolve4(projectRoot, ".rig", "state", "github-auth.json");
323
- if (!existsSync3(path))
326
+ const parsed = readRemoteAuthState(projectRoot);
327
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
328
+ }
329
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
330
+ const repo = readRepoConnection(projectRoot);
331
+ const slug = repo?.project?.trim();
332
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
324
333
  return null;
325
- try {
326
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
327
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
328
- } catch {
334
+ const auth = readRemoteAuthState(projectRoot);
335
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
336
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
337
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
329
338
  return null;
330
- }
339
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
340
+ if (!checkoutBaseDir)
341
+ return null;
342
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
343
+ writeRepoServerProjectRoot(projectRoot, inferred);
344
+ return inferred;
331
345
  }
332
346
  function readLocalConnectionFallbackToken(projectRoot) {
333
347
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -338,7 +352,7 @@ async function ensureServerForCli(projectRoot) {
338
352
  if (selected?.connection.kind === "remote") {
339
353
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
340
354
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
341
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
355
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
342
356
  return {
343
357
  baseUrl: selected.connection.baseUrl,
344
358
  authToken,
@@ -367,7 +381,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
367
381
  if (!slug)
368
382
  return null;
369
383
  try {
370
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
384
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
385
+ if (authToken && queryAuthFallbackEnabled())
386
+ url.searchParams.set("rt", authToken);
387
+ const response = await fetch(url, {
371
388
  headers: mergeHeaders(undefined, authToken)
372
389
  });
373
390
  if (!response.ok)
@@ -384,10 +401,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
384
401
  return null;
385
402
  }
386
403
  }
404
+ function mergeCookie(existing, name, value) {
405
+ const encoded = `${name}=${encodeURIComponent(value)}`;
406
+ if (!existing?.trim())
407
+ return encoded;
408
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
409
+ return [...parts, encoded].join("; ");
410
+ }
411
+ function queryAuthFallbackEnabled(env = process.env) {
412
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
413
+ }
387
414
  function mergeHeaders(headers, authToken) {
388
415
  const merged = new Headers(headers);
389
416
  if (authToken) {
390
- merged.set("authorization", `Bearer ${authToken}`);
417
+ const bearer = `Bearer ${authToken}`;
418
+ merged.set("authorization", bearer);
419
+ merged.set("x-auth", bearer);
420
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
391
421
  }
392
422
  return merged;
393
423
  }
@@ -448,15 +478,34 @@ async function buildServerFailureContext(projectRoot, server) {
448
478
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
449
479
  };
450
480
  }
481
+ function isLoopbackRemoteBaseUrl(baseUrl) {
482
+ try {
483
+ const host = new URL(baseUrl).hostname.toLowerCase();
484
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
485
+ } catch {
486
+ return false;
487
+ }
488
+ }
489
+ function canUseRemoteWithoutProjectRoot(pathname) {
490
+ return pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/server/project-root" || pathname.startsWith("/api/github/auth/") || pathname === "/api/projects" || pathname.startsWith("/api/projects/");
491
+ }
451
492
  async function requestServerJson(context, pathname, init = {}) {
452
493
  const server = await ensureServerForCli(context.projectRoot);
494
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
495
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
496
+ const repo = readRepoConnection(context.projectRoot);
497
+ throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
498
+ }
453
499
  const headers = mergeHeaders(init.headers, server.authToken);
454
500
  if (server.serverProjectRoot)
455
501
  headers.set("x-rig-project-root", server.serverProjectRoot);
502
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
503
+ requestUrl.searchParams.set("rt", server.authToken);
504
+ }
456
505
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
457
506
  let response;
458
507
  try {
459
- response = await fetch(`${server.baseUrl}${pathname}`, {
508
+ response = await fetch(requestUrl, {
460
509
  ...init,
461
510
  headers
462
511
  });
@@ -165,17 +165,21 @@ function cleanToken(value) {
165
165
  const trimmed = value?.trim();
166
166
  return trimmed ? trimmed : null;
167
167
  }
168
- function readPrivateRemoteSessionToken(projectRoot) {
168
+ function readRemoteAuthState(projectRoot) {
169
169
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
170
170
  if (!existsSync2(path))
171
171
  return null;
172
172
  try {
173
173
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
174
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
174
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
175
175
  } catch {
176
176
  return null;
177
177
  }
178
178
  }
179
+ function readPrivateRemoteSessionToken(projectRoot) {
180
+ const parsed = readRemoteAuthState(projectRoot);
181
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
182
+ }
179
183
  function readGitHubBearerTokenForRemote(projectRoot) {
180
184
  const scopedKey = resolve2(projectRoot);
181
185
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -186,15 +190,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
186
190
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
187
191
  }
188
192
  function readStoredGitHubAuthToken(projectRoot) {
189
- const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
190
- if (!existsSync2(path))
193
+ const parsed = readRemoteAuthState(projectRoot);
194
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
195
+ }
196
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
197
+ const repo = readRepoConnection(projectRoot);
198
+ const slug = repo?.project?.trim();
199
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
191
200
  return null;
192
- try {
193
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
194
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
195
- } catch {
201
+ const auth = readRemoteAuthState(projectRoot);
202
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
203
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
204
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
196
205
  return null;
197
- }
206
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
207
+ if (!checkoutBaseDir)
208
+ return null;
209
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
210
+ writeRepoServerProjectRoot(projectRoot, inferred);
211
+ return inferred;
198
212
  }
199
213
  function readLocalConnectionFallbackToken(projectRoot) {
200
214
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -205,7 +219,7 @@ async function ensureServerForCli(projectRoot) {
205
219
  if (selected?.connection.kind === "remote") {
206
220
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
207
221
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
208
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
222
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
209
223
  return {
210
224
  baseUrl: selected.connection.baseUrl,
211
225
  authToken,
@@ -234,7 +248,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
234
248
  if (!slug)
235
249
  return null;
236
250
  try {
237
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
251
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
252
+ if (authToken && queryAuthFallbackEnabled())
253
+ url.searchParams.set("rt", authToken);
254
+ const response = await fetch(url, {
238
255
  headers: mergeHeaders(undefined, authToken)
239
256
  });
240
257
  if (!response.ok)
@@ -251,10 +268,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
251
268
  return null;
252
269
  }
253
270
  }
271
+ function mergeCookie(existing, name, value) {
272
+ const encoded = `${name}=${encodeURIComponent(value)}`;
273
+ if (!existing?.trim())
274
+ return encoded;
275
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
276
+ return [...parts, encoded].join("; ");
277
+ }
278
+ function queryAuthFallbackEnabled(env = process.env) {
279
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
280
+ }
254
281
  function mergeHeaders(headers, authToken) {
255
282
  const merged = new Headers(headers);
256
283
  if (authToken) {
257
- merged.set("authorization", `Bearer ${authToken}`);
284
+ const bearer = `Bearer ${authToken}`;
285
+ merged.set("authorization", bearer);
286
+ merged.set("x-auth", bearer);
287
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
258
288
  }
259
289
  return merged;
260
290
  }
@@ -315,15 +345,34 @@ async function buildServerFailureContext(projectRoot, server) {
315
345
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
316
346
  };
317
347
  }
348
+ function isLoopbackRemoteBaseUrl(baseUrl) {
349
+ try {
350
+ const host = new URL(baseUrl).hostname.toLowerCase();
351
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
352
+ } catch {
353
+ return false;
354
+ }
355
+ }
356
+ function canUseRemoteWithoutProjectRoot(pathname) {
357
+ return pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/server/project-root" || pathname.startsWith("/api/github/auth/") || pathname === "/api/projects" || pathname.startsWith("/api/projects/");
358
+ }
318
359
  async function requestServerJson(context, pathname, init = {}) {
319
360
  const server = await ensureServerForCli(context.projectRoot);
361
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
362
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
363
+ const repo = readRepoConnection(context.projectRoot);
364
+ throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
365
+ }
320
366
  const headers = mergeHeaders(init.headers, server.authToken);
321
367
  if (server.serverProjectRoot)
322
368
  headers.set("x-rig-project-root", server.serverProjectRoot);
369
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
370
+ requestUrl.searchParams.set("rt", server.authToken);
371
+ }
323
372
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
324
373
  let response;
325
374
  try {
326
- response = await fetch(`${server.baseUrl}${pathname}`, {
375
+ response = await fetch(requestUrl, {
327
376
  ...init,
328
377
  headers
329
378
  });
@@ -551,17 +551,21 @@ function cleanToken(value) {
551
551
  const trimmed = value?.trim();
552
552
  return trimmed ? trimmed : null;
553
553
  }
554
- function readPrivateRemoteSessionToken(projectRoot) {
554
+ function readRemoteAuthState(projectRoot) {
555
555
  const path = resolve5(projectRoot, ".rig", "state", "github-auth.json");
556
556
  if (!existsSync3(path))
557
557
  return null;
558
558
  try {
559
559
  const parsed = JSON.parse(readFileSync3(path, "utf8"));
560
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
560
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
561
561
  } catch {
562
562
  return null;
563
563
  }
564
564
  }
565
+ function readPrivateRemoteSessionToken(projectRoot) {
566
+ const parsed = readRemoteAuthState(projectRoot);
567
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
568
+ }
565
569
  function readGitHubBearerTokenForRemote(projectRoot) {
566
570
  const scopedKey = resolve5(projectRoot);
567
571
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -572,15 +576,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
572
576
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
573
577
  }
574
578
  function readStoredGitHubAuthToken(projectRoot) {
575
- const path = resolve5(projectRoot, ".rig", "state", "github-auth.json");
576
- if (!existsSync3(path))
579
+ const parsed = readRemoteAuthState(projectRoot);
580
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
581
+ }
582
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
583
+ const repo = readRepoConnection(projectRoot);
584
+ const slug = repo?.project?.trim();
585
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
577
586
  return null;
578
- try {
579
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
580
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
581
- } catch {
587
+ const auth = readRemoteAuthState(projectRoot);
588
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
589
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
590
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
582
591
  return null;
583
- }
592
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
593
+ if (!checkoutBaseDir)
594
+ return null;
595
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
596
+ writeRepoServerProjectRoot(projectRoot, inferred);
597
+ return inferred;
584
598
  }
585
599
  function readLocalConnectionFallbackToken(projectRoot) {
586
600
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -591,7 +605,7 @@ async function ensureServerForCli(projectRoot) {
591
605
  if (selected?.connection.kind === "remote") {
592
606
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
593
607
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
594
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
608
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
595
609
  return {
596
610
  baseUrl: selected.connection.baseUrl,
597
611
  authToken,
@@ -620,7 +634,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
620
634
  if (!slug)
621
635
  return null;
622
636
  try {
623
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
637
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
638
+ if (authToken && queryAuthFallbackEnabled())
639
+ url.searchParams.set("rt", authToken);
640
+ const response = await fetch(url, {
624
641
  headers: mergeHeaders(undefined, authToken)
625
642
  });
626
643
  if (!response.ok)
@@ -637,10 +654,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
637
654
  return null;
638
655
  }
639
656
  }
657
+ function mergeCookie(existing, name, value) {
658
+ const encoded = `${name}=${encodeURIComponent(value)}`;
659
+ if (!existing?.trim())
660
+ return encoded;
661
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
662
+ return [...parts, encoded].join("; ");
663
+ }
664
+ function queryAuthFallbackEnabled(env = process.env) {
665
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
666
+ }
640
667
  function mergeHeaders(headers, authToken) {
641
668
  const merged = new Headers(headers);
642
669
  if (authToken) {
643
- merged.set("authorization", `Bearer ${authToken}`);
670
+ const bearer = `Bearer ${authToken}`;
671
+ merged.set("authorization", bearer);
672
+ merged.set("x-auth", bearer);
673
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
644
674
  }
645
675
  return merged;
646
676
  }
@@ -701,15 +731,34 @@ async function buildServerFailureContext(projectRoot, server) {
701
731
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
702
732
  };
703
733
  }
734
+ function isLoopbackRemoteBaseUrl(baseUrl) {
735
+ try {
736
+ const host = new URL(baseUrl).hostname.toLowerCase();
737
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
738
+ } catch {
739
+ return false;
740
+ }
741
+ }
742
+ function canUseRemoteWithoutProjectRoot(pathname) {
743
+ return pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/server/project-root" || pathname.startsWith("/api/github/auth/") || pathname === "/api/projects" || pathname.startsWith("/api/projects/");
744
+ }
704
745
  async function requestServerJson(context, pathname, init = {}) {
705
746
  const server = await ensureServerForCli(context.projectRoot);
747
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
748
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
749
+ const repo = readRepoConnection(context.projectRoot);
750
+ throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
751
+ }
706
752
  const headers = mergeHeaders(init.headers, server.authToken);
707
753
  if (server.serverProjectRoot)
708
754
  headers.set("x-rig-project-root", server.serverProjectRoot);
755
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
756
+ requestUrl.searchParams.set("rt", server.authToken);
757
+ }
709
758
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
710
759
  let response;
711
760
  try {
712
- response = await fetch(`${server.baseUrl}${pathname}`, {
761
+ response = await fetch(requestUrl, {
713
762
  ...init,
714
763
  headers
715
764
  });