@h-rig/cli 0.0.6-alpha.82 → 0.0.6-alpha.84

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 (206) hide show
  1. package/dist/bin/build-rig-binaries.js +40 -8
  2. package/dist/bin/rig.js +23402 -14577
  3. package/dist/src/app/board.js +217 -41
  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.d.ts +0 -2
  8. package/dist/src/app-opentui/adapters/doctor.js +64 -39
  9. package/dist/src/app-opentui/adapters/family.d.ts +62 -0
  10. package/dist/src/app-opentui/adapters/family.js +14305 -0
  11. package/dist/src/app-opentui/adapters/fleet.d.ts +0 -2
  12. package/dist/src/app-opentui/adapters/fleet.js +90 -60
  13. package/dist/src/app-opentui/adapters/inbox.d.ts +12 -2
  14. package/dist/src/app-opentui/adapters/inbox.js +137 -78
  15. package/dist/src/app-opentui/adapters/init.d.ts +0 -2
  16. package/dist/src/app-opentui/adapters/init.js +85 -47
  17. package/dist/src/app-opentui/adapters/inspect.d.ts +52 -0
  18. package/dist/src/app-opentui/adapters/inspect.js +1024 -0
  19. package/dist/src/app-opentui/adapters/pi-attach.d.ts +15 -6
  20. package/dist/src/app-opentui/adapters/pi-attach.js +442 -125
  21. package/dist/src/app-opentui/adapters/pi.d.ts +23 -0
  22. package/dist/src/app-opentui/adapters/pi.js +363 -0
  23. package/dist/src/app-opentui/adapters/plugin.d.ts +84 -0
  24. package/dist/src/app-opentui/adapters/plugin.js +544 -0
  25. package/dist/src/app-opentui/adapters/repo.d.ts +37 -0
  26. package/dist/src/app-opentui/adapters/repo.js +186 -0
  27. package/dist/src/app-opentui/adapters/run-detail.d.ts +9 -2
  28. package/dist/src/app-opentui/adapters/run-detail.js +180 -63
  29. package/dist/src/app-opentui/adapters/server.d.ts +10 -2
  30. package/dist/src/app-opentui/adapters/server.js +191 -45
  31. package/dist/src/app-opentui/adapters/tasks.d.ts +11 -2
  32. package/dist/src/app-opentui/adapters/tasks.js +1123 -143
  33. package/dist/src/app-opentui/adapters/workspace.d.ts +49 -0
  34. package/dist/src/app-opentui/adapters/workspace.js +333 -0
  35. package/dist/src/app-opentui/autocomplete.d.ts +20 -0
  36. package/dist/src/app-opentui/autocomplete.js +576 -0
  37. package/dist/src/app-opentui/bootstrap.d.ts +1 -6
  38. package/dist/src/app-opentui/bootstrap.js +25252 -16474
  39. package/dist/src/app-opentui/command-palette.d.ts +3 -0
  40. package/dist/src/app-opentui/command-palette.js +1010 -0
  41. package/dist/src/app-opentui/command-pty-host.d.ts +62 -0
  42. package/dist/src/app-opentui/command-pty-host.js +248 -0
  43. package/dist/src/app-opentui/drone.js +8 -6
  44. package/dist/src/app-opentui/events.js +1 -1
  45. package/dist/src/app-opentui/fleet-stats.d.ts +32 -0
  46. package/dist/src/app-opentui/fleet-stats.js +114 -0
  47. package/dist/src/app-opentui/focus-manager.d.ts +14 -0
  48. package/dist/src/app-opentui/focus-manager.js +24 -0
  49. package/dist/src/app-opentui/index.js +5431 -2797
  50. package/dist/src/app-opentui/intent.js +179 -50
  51. package/dist/src/app-opentui/keymap.d.ts +21 -0
  52. package/dist/src/app-opentui/keymap.js +1748 -0
  53. package/dist/src/app-opentui/launch-routing.d.ts +16 -0
  54. package/dist/src/app-opentui/launch-routing.js +55 -0
  55. package/dist/src/app-opentui/layout.d.ts +7 -0
  56. package/dist/src/app-opentui/layout.js +13 -6
  57. package/dist/src/app-opentui/list-search.d.ts +24 -0
  58. package/dist/src/app-opentui/list-search.js +88 -1
  59. package/dist/src/app-opentui/pi-host-child.js +99 -17
  60. package/dist/src/app-opentui/pi-pty-host.d.ts +14 -0
  61. package/dist/src/app-opentui/pi-pty-host.js +30 -14
  62. package/dist/src/app-opentui/react/App.d.ts +9 -0
  63. package/dist/src/app-opentui/react/App.js +5144 -0
  64. package/dist/src/app-opentui/react/Backdrop.d.ts +5 -0
  65. package/dist/src/app-opentui/react/Backdrop.js +1834 -0
  66. package/dist/src/app-opentui/react/ChromeHost.d.ts +5 -0
  67. package/dist/src/app-opentui/react/ChromeHost.js +2942 -0
  68. package/dist/src/app-opentui/react/SceneFrameView.d.ts +7 -0
  69. package/dist/src/app-opentui/react/SceneFrameView.js +529 -0
  70. package/dist/src/app-opentui/react/context.d.ts +17 -0
  71. package/dist/src/app-opentui/react/context.js +37 -0
  72. package/dist/src/app-opentui/react/launch.d.ts +2 -0
  73. package/dist/src/app-opentui/react/launch.js +5743 -0
  74. package/dist/src/app-opentui/react/nav.d.ts +18 -0
  75. package/dist/src/app-opentui/react/nav.js +54 -0
  76. package/dist/src/app-opentui/react/scroll.d.ts +12 -0
  77. package/dist/src/app-opentui/react/scroll.js +21 -0
  78. package/dist/src/app-opentui/react/syntax.d.ts +2 -0
  79. package/dist/src/app-opentui/react/syntax.js +64 -0
  80. package/dist/src/app-opentui/registry.js +20428 -4828
  81. package/dist/src/app-opentui/render/ascii-fleet.d.ts +15 -0
  82. package/dist/src/app-opentui/render/ascii-fleet.js +82 -0
  83. package/dist/src/app-opentui/render/constants.d.ts +31 -0
  84. package/dist/src/app-opentui/render/constants.js +66 -0
  85. package/dist/src/app-opentui/render/graphics.d.ts +2 -1
  86. package/dist/src/app-opentui/render/graphics.js +228 -46
  87. package/dist/src/app-opentui/render/image-visual-layer-worker.js +469 -930
  88. package/dist/src/app-opentui/render/image-visual-layer.d.ts +25 -10
  89. package/dist/src/app-opentui/render/image-visual-layer.js +448 -329
  90. package/dist/src/app-opentui/render/native-host.d.ts +37 -0
  91. package/dist/src/app-opentui/render/native-host.js +179 -0
  92. package/dist/src/app-opentui/render/panel-layout.d.ts +38 -0
  93. package/dist/src/app-opentui/render/panel-layout.js +48 -0
  94. package/dist/src/app-opentui/render/panels.d.ts +2 -0
  95. package/dist/src/app-opentui/render/panels.js +78 -38
  96. package/dist/src/app-opentui/render/preloader.d.ts +10 -0
  97. package/dist/src/app-opentui/render/preloader.js +165 -0
  98. package/dist/src/app-opentui/render/scene.d.ts +53 -1
  99. package/dist/src/app-opentui/render/scene.js +195 -6
  100. package/dist/src/app-opentui/render/text.d.ts +7 -1
  101. package/dist/src/app-opentui/render/text.js +15 -8
  102. package/dist/src/app-opentui/render/type-bar.d.ts +2 -1
  103. package/dist/src/app-opentui/render/type-bar.js +113 -39
  104. package/dist/src/app-opentui/runtime-resources.d.ts +16 -0
  105. package/dist/src/app-opentui/runtime-resources.js +62 -0
  106. package/dist/src/app-opentui/runtime.d.ts +44 -1
  107. package/dist/src/app-opentui/runtime.js +5415 -2738
  108. package/dist/src/app-opentui/scenes/command.d.ts +3 -0
  109. package/dist/src/app-opentui/scenes/command.js +117 -0
  110. package/dist/src/app-opentui/scenes/doctor.d.ts +2 -1
  111. package/dist/src/app-opentui/scenes/doctor.js +221 -17
  112. package/dist/src/app-opentui/scenes/error.js +60 -20
  113. package/dist/src/app-opentui/scenes/family-domains/agent.d.ts +2 -0
  114. package/dist/src/app-opentui/scenes/family-domains/agent.js +348 -0
  115. package/dist/src/app-opentui/scenes/family-domains/browser.d.ts +2 -0
  116. package/dist/src/app-opentui/scenes/family-domains/browser.js +195 -0
  117. package/dist/src/app-opentui/scenes/family-domains/dist.d.ts +2 -0
  118. package/dist/src/app-opentui/scenes/family-domains/dist.js +243 -0
  119. package/dist/src/app-opentui/scenes/family-domains/git.d.ts +2 -0
  120. package/dist/src/app-opentui/scenes/family-domains/git.js +195 -0
  121. package/dist/src/app-opentui/scenes/family-domains/github.d.ts +2 -0
  122. package/dist/src/app-opentui/scenes/family-domains/github.js +274 -0
  123. package/dist/src/app-opentui/scenes/family-domains/harness.d.ts +2 -0
  124. package/dist/src/app-opentui/scenes/family-domains/harness.js +152 -0
  125. package/dist/src/app-opentui/scenes/family-domains/index.d.ts +4 -0
  126. package/dist/src/app-opentui/scenes/family-domains/index.js +1679 -0
  127. package/dist/src/app-opentui/scenes/family-domains/kit.d.ts +76 -0
  128. package/dist/src/app-opentui/scenes/family-domains/kit.js +305 -0
  129. package/dist/src/app-opentui/scenes/family-domains/profile.d.ts +2 -0
  130. package/dist/src/app-opentui/scenes/family-domains/profile.js +212 -0
  131. package/dist/src/app-opentui/scenes/family-domains/queue.d.ts +2 -0
  132. package/dist/src/app-opentui/scenes/family-domains/queue.js +146 -0
  133. package/dist/src/app-opentui/scenes/family-domains/remote.d.ts +2 -0
  134. package/dist/src/app-opentui/scenes/family-domains/remote.js +518 -0
  135. package/dist/src/app-opentui/scenes/family-domains/review.d.ts +2 -0
  136. package/dist/src/app-opentui/scenes/family-domains/review.js +280 -0
  137. package/dist/src/app-opentui/scenes/family-domains/setup.d.ts +2 -0
  138. package/dist/src/app-opentui/scenes/family-domains/setup.js +267 -0
  139. package/dist/src/app-opentui/scenes/family-domains/stats.d.ts +2 -0
  140. package/dist/src/app-opentui/scenes/family-domains/stats.js +370 -0
  141. package/dist/src/app-opentui/scenes/family.d.ts +3 -0
  142. package/dist/src/app-opentui/scenes/family.js +2144 -0
  143. package/dist/src/app-opentui/scenes/fleet.js +552 -43
  144. package/dist/src/app-opentui/scenes/handoff.js +342 -35
  145. package/dist/src/app-opentui/scenes/help.js +640 -56
  146. package/dist/src/app-opentui/scenes/inbox.d.ts +2 -1
  147. package/dist/src/app-opentui/scenes/inbox.js +329 -21
  148. package/dist/src/app-opentui/scenes/init.d.ts +2 -1
  149. package/dist/src/app-opentui/scenes/init.js +120 -34
  150. package/dist/src/app-opentui/scenes/inspect.d.ts +3 -0
  151. package/dist/src/app-opentui/scenes/inspect.js +793 -0
  152. package/dist/src/app-opentui/scenes/main.d.ts +2 -1
  153. package/dist/src/app-opentui/scenes/main.js +264 -29
  154. package/dist/src/app-opentui/scenes/pi.d.ts +3 -0
  155. package/dist/src/app-opentui/scenes/pi.js +508 -0
  156. package/dist/src/app-opentui/scenes/plugin.d.ts +3 -0
  157. package/dist/src/app-opentui/scenes/plugin.js +486 -0
  158. package/dist/src/app-opentui/scenes/repo.d.ts +3 -0
  159. package/dist/src/app-opentui/scenes/repo.js +424 -0
  160. package/dist/src/app-opentui/scenes/run-detail.d.ts +2 -1
  161. package/dist/src/app-opentui/scenes/run-detail.js +362 -35
  162. package/dist/src/app-opentui/scenes/server.d.ts +2 -1
  163. package/dist/src/app-opentui/scenes/server.js +243 -26
  164. package/dist/src/app-opentui/scenes/tasks.js +518 -41
  165. package/dist/src/app-opentui/scenes/workspace.d.ts +3 -0
  166. package/dist/src/app-opentui/scenes/workspace.js +426 -0
  167. package/dist/src/app-opentui/selectable.d.ts +19 -0
  168. package/dist/src/app-opentui/selectable.js +79 -0
  169. package/dist/src/app-opentui/state.js +129 -36
  170. package/dist/src/app-opentui/surface-catalog.d.ts +20 -0
  171. package/dist/src/app-opentui/surface-catalog.js +540 -0
  172. package/dist/src/app-opentui/terminal-capabilities.d.ts +7 -0
  173. package/dist/src/app-opentui/terminal-capabilities.js +15 -0
  174. package/dist/src/app-opentui/theme.d.ts +28 -6
  175. package/dist/src/app-opentui/theme.js +61 -8
  176. package/dist/src/app-opentui/types.d.ts +130 -5
  177. package/dist/src/commands/_authority-runs.d.ts +1 -1
  178. package/dist/src/commands/_authority-runs.js +2 -12
  179. package/dist/src/commands/_doctor-checks.js +62 -13
  180. package/dist/src/commands/_help-catalog.js +95 -15
  181. package/dist/src/commands/_operator-view.js +97 -15
  182. package/dist/src/commands/_pi-frontend.d.ts +2 -0
  183. package/dist/src/commands/_pi-frontend.js +97 -13
  184. package/dist/src/commands/_preflight.js +64 -14
  185. package/dist/src/commands/_server-client.js +82 -18
  186. package/dist/src/commands/_server-events.d.ts +26 -0
  187. package/dist/src/commands/_server-events.js +310 -0
  188. package/dist/src/commands/_snapshot-upload.js +62 -13
  189. package/dist/src/commands/agent.js +2 -12
  190. package/dist/src/commands/connect.js +7 -1
  191. package/dist/src/commands/doctor.js +62 -13
  192. package/dist/src/commands/github.js +144 -23
  193. package/dist/src/commands/inbox.js +62 -13
  194. package/dist/src/commands/init.js +82 -18
  195. package/dist/src/commands/inspect.js +62 -13
  196. package/dist/src/commands/run.js +100 -15
  197. package/dist/src/commands/server.js +71 -26
  198. package/dist/src/commands/setup.js +62 -13
  199. package/dist/src/commands/stats.js +157 -28
  200. package/dist/src/commands/task-run-driver.js +64 -25
  201. package/dist/src/commands/task.js +196 -43
  202. package/dist/src/commands.js +419 -123
  203. package/dist/src/index.js +426 -130
  204. package/package.json +11 -10
  205. package/dist/src/app-opentui/render/image-visual-layer-native-canvas.d.ts +0 -2
  206. package/dist/src/app-opentui/render/image-visual-layer-native-canvas.js +0 -1484
@@ -392,7 +392,13 @@ async function executeConnectionCommand(context, args, options) {
392
392
  if (!state.connections[alias])
393
393
  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>`." });
394
394
  }
395
- const repoState = { selected: alias, linkedAt: new Date().toISOString() };
395
+ const previousRepo = readRepoConnection(context.projectRoot);
396
+ const repoState = {
397
+ selected: alias,
398
+ ...previousRepo?.project ? { project: previousRepo.project } : {},
399
+ linkedAt: new Date().toISOString(),
400
+ ...previousRepo?.serverProjectRoot && previousRepo.selected === alias ? { serverProjectRoot: previousRepo.serverProjectRoot } : {}
401
+ };
396
402
  writeRepoConnection(context.projectRoot, repoState);
397
403
  printJsonOrText(context, repoState, formatSuccessCard("Rig server selected", [
398
404
  ["selected", alias],
@@ -143,17 +143,21 @@ function cleanToken(value) {
143
143
  const trimmed = value?.trim();
144
144
  return trimmed ? trimmed : null;
145
145
  }
146
- function readPrivateRemoteSessionToken(projectRoot) {
146
+ function readRemoteAuthState(projectRoot) {
147
147
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
148
148
  if (!existsSync2(path))
149
149
  return null;
150
150
  try {
151
151
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
152
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
152
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
153
153
  } catch {
154
154
  return null;
155
155
  }
156
156
  }
157
+ function readPrivateRemoteSessionToken(projectRoot) {
158
+ const parsed = readRemoteAuthState(projectRoot);
159
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
160
+ }
157
161
  function readGitHubBearerTokenForRemote(projectRoot) {
158
162
  const scopedKey = resolve2(projectRoot);
159
163
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -164,15 +168,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
164
168
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
165
169
  }
166
170
  function readStoredGitHubAuthToken(projectRoot) {
167
- const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
168
- if (!existsSync2(path))
171
+ const parsed = readRemoteAuthState(projectRoot);
172
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
173
+ }
174
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
175
+ const repo = readRepoConnection(projectRoot);
176
+ const slug = repo?.project?.trim();
177
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
169
178
  return null;
170
- try {
171
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
172
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
173
- } catch {
179
+ const auth = readRemoteAuthState(projectRoot);
180
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
181
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
182
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
174
183
  return null;
175
- }
184
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
185
+ if (!checkoutBaseDir)
186
+ return null;
187
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
188
+ writeRepoServerProjectRoot(projectRoot, inferred);
189
+ return inferred;
176
190
  }
177
191
  function readLocalConnectionFallbackToken(projectRoot) {
178
192
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -183,7 +197,7 @@ async function ensureServerForCli(projectRoot) {
183
197
  if (selected?.connection.kind === "remote") {
184
198
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
185
199
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
186
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
200
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
187
201
  return {
188
202
  baseUrl: selected.connection.baseUrl,
189
203
  authToken,
@@ -212,7 +226,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
212
226
  if (!slug)
213
227
  return null;
214
228
  try {
215
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
229
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
230
+ if (authToken && queryAuthFallbackEnabled())
231
+ url.searchParams.set("rt", authToken);
232
+ const response = await fetch(url, {
216
233
  headers: mergeHeaders(undefined, authToken)
217
234
  });
218
235
  if (!response.ok)
@@ -229,10 +246,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
229
246
  return null;
230
247
  }
231
248
  }
249
+ function mergeCookie(existing, name, value) {
250
+ const encoded = `${name}=${encodeURIComponent(value)}`;
251
+ if (!existing?.trim())
252
+ return encoded;
253
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
254
+ return [...parts, encoded].join("; ");
255
+ }
256
+ function queryAuthFallbackEnabled(env = process.env) {
257
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
258
+ }
232
259
  function mergeHeaders(headers, authToken) {
233
260
  const merged = new Headers(headers);
234
261
  if (authToken) {
235
- merged.set("authorization", `Bearer ${authToken}`);
262
+ const bearer = `Bearer ${authToken}`;
263
+ merged.set("authorization", bearer);
264
+ merged.set("x-auth", bearer);
265
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
236
266
  }
237
267
  return merged;
238
268
  }
@@ -293,15 +323,34 @@ async function buildServerFailureContext(projectRoot, server) {
293
323
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
294
324
  };
295
325
  }
326
+ function isLoopbackRemoteBaseUrl(baseUrl) {
327
+ try {
328
+ const host = new URL(baseUrl).hostname.toLowerCase();
329
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
330
+ } catch {
331
+ return false;
332
+ }
333
+ }
334
+ function canUseRemoteWithoutProjectRoot(pathname) {
335
+ 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/");
336
+ }
296
337
  async function requestServerJson(context, pathname, init = {}) {
297
338
  const server = await ensureServerForCli(context.projectRoot);
339
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
340
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
341
+ const repo = readRepoConnection(context.projectRoot);
342
+ 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." });
343
+ }
298
344
  const headers = mergeHeaders(init.headers, server.authToken);
299
345
  if (server.serverProjectRoot)
300
346
  headers.set("x-rig-project-root", server.serverProjectRoot);
347
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
348
+ requestUrl.searchParams.set("rt", server.authToken);
349
+ }
301
350
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
302
351
  let response;
303
352
  try {
304
- response = await fetch(`${server.baseUrl}${pathname}`, {
353
+ response = await fetch(requestUrl, {
305
354
  ...init,
306
355
  headers
307
356
  });
@@ -1,6 +1,8 @@
1
1
  // @bun
2
2
  // packages/cli/src/commands/github.ts
3
3
  import { spawnSync } from "child_process";
4
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
5
+ import { dirname as dirname2, resolve as resolve3 } from "path";
4
6
 
5
7
  // packages/cli/src/runner.ts
6
8
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
@@ -155,17 +157,25 @@ function cleanToken(value) {
155
157
  const trimmed = value?.trim();
156
158
  return trimmed ? trimmed : null;
157
159
  }
158
- function readPrivateRemoteSessionToken(projectRoot) {
160
+ function setGitHubBearerTokenForCurrentProcess(token, projectRoot) {
161
+ const scopedKey = resolve2(projectRoot ?? process.cwd());
162
+ scopedGitHubBearerTokens.set(scopedKey, cleanToken(token ?? undefined));
163
+ }
164
+ function readRemoteAuthState(projectRoot) {
159
165
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
160
166
  if (!existsSync2(path))
161
167
  return null;
162
168
  try {
163
169
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
164
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
170
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
165
171
  } catch {
166
172
  return null;
167
173
  }
168
174
  }
175
+ function readPrivateRemoteSessionToken(projectRoot) {
176
+ const parsed = readRemoteAuthState(projectRoot);
177
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
178
+ }
169
179
  function readGitHubBearerTokenForRemote(projectRoot) {
170
180
  const scopedKey = resolve2(projectRoot);
171
181
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -176,15 +186,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
176
186
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
177
187
  }
178
188
  function readStoredGitHubAuthToken(projectRoot) {
179
- const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
180
- if (!existsSync2(path))
189
+ const parsed = readRemoteAuthState(projectRoot);
190
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
191
+ }
192
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
193
+ const repo = readRepoConnection(projectRoot);
194
+ const slug = repo?.project?.trim();
195
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
181
196
  return null;
182
- try {
183
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
184
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
185
- } catch {
197
+ const auth = readRemoteAuthState(projectRoot);
198
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
199
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
200
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
186
201
  return null;
187
- }
202
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
203
+ if (!checkoutBaseDir)
204
+ return null;
205
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
206
+ writeRepoServerProjectRoot(projectRoot, inferred);
207
+ return inferred;
188
208
  }
189
209
  function readLocalConnectionFallbackToken(projectRoot) {
190
210
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -195,7 +215,7 @@ async function ensureServerForCli(projectRoot) {
195
215
  if (selected?.connection.kind === "remote") {
196
216
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
197
217
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
198
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
218
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
199
219
  return {
200
220
  baseUrl: selected.connection.baseUrl,
201
221
  authToken,
@@ -224,7 +244,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
224
244
  if (!slug)
225
245
  return null;
226
246
  try {
227
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
247
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
248
+ if (authToken && queryAuthFallbackEnabled())
249
+ url.searchParams.set("rt", authToken);
250
+ const response = await fetch(url, {
228
251
  headers: mergeHeaders(undefined, authToken)
229
252
  });
230
253
  if (!response.ok)
@@ -241,10 +264,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
241
264
  return null;
242
265
  }
243
266
  }
267
+ function mergeCookie(existing, name, value) {
268
+ const encoded = `${name}=${encodeURIComponent(value)}`;
269
+ if (!existing?.trim())
270
+ return encoded;
271
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
272
+ return [...parts, encoded].join("; ");
273
+ }
274
+ function queryAuthFallbackEnabled(env = process.env) {
275
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
276
+ }
244
277
  function mergeHeaders(headers, authToken) {
245
278
  const merged = new Headers(headers);
246
279
  if (authToken) {
247
- merged.set("authorization", `Bearer ${authToken}`);
280
+ const bearer = `Bearer ${authToken}`;
281
+ merged.set("authorization", bearer);
282
+ merged.set("x-auth", bearer);
283
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
248
284
  }
249
285
  return merged;
250
286
  }
@@ -305,15 +341,34 @@ async function buildServerFailureContext(projectRoot, server) {
305
341
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
306
342
  };
307
343
  }
344
+ function isLoopbackRemoteBaseUrl(baseUrl) {
345
+ try {
346
+ const host = new URL(baseUrl).hostname.toLowerCase();
347
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
348
+ } catch {
349
+ return false;
350
+ }
351
+ }
352
+ function canUseRemoteWithoutProjectRoot(pathname) {
353
+ 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/");
354
+ }
308
355
  async function requestServerJson(context, pathname, init = {}) {
309
356
  const server = await ensureServerForCli(context.projectRoot);
357
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
358
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
359
+ const repo = readRepoConnection(context.projectRoot);
360
+ 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." });
361
+ }
310
362
  const headers = mergeHeaders(init.headers, server.authToken);
311
363
  if (server.serverProjectRoot)
312
364
  headers.set("x-rig-project-root", server.serverProjectRoot);
365
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
366
+ requestUrl.searchParams.set("rt", server.authToken);
367
+ }
313
368
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
314
369
  let response;
315
370
  try {
316
- response = await fetch(`${server.baseUrl}${pathname}`, {
371
+ response = await fetch(requestUrl, {
317
372
  ...init,
318
373
  headers
319
374
  });
@@ -346,11 +401,26 @@ async function getGitHubAuthStatusViaServer(context) {
346
401
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
347
402
  }
348
403
  async function postGitHubTokenViaServer(context, token, options = {}) {
349
- const payload = await requestServerJson(context, "/api/github/auth/token", {
350
- method: "POST",
351
- headers: { "content-type": "application/json" },
352
- body: JSON.stringify({ token, selectedRepo: options.selectedRepo, projectRoot: options.projectRoot })
353
- });
404
+ const server = await ensureServerForCli(context.projectRoot);
405
+ const requestUrl = new URL(`${server.baseUrl}/api/github/auth/token`);
406
+ reportServerPhase("POST /api/github/auth/token\u2026");
407
+ let response;
408
+ try {
409
+ response = await fetch(requestUrl, {
410
+ method: "POST",
411
+ headers: { "content-type": "application/json" },
412
+ body: JSON.stringify({ token, selectedRepo: options.selectedRepo, projectRoot: options.projectRoot ?? server.serverProjectRoot ?? undefined })
413
+ });
414
+ } catch (error) {
415
+ const failure = await buildServerFailureContext(context.projectRoot, server);
416
+ throw new CliError(`Rig server auth bootstrap failed: ${error instanceof Error ? error.message : String(error)}
417
+ ${failure.contextLine}`, 1, { hint: failure.hint });
418
+ }
419
+ const payload = await response.json().catch(() => null);
420
+ if (!response.ok) {
421
+ const detail = payload && typeof payload === "object" && !Array.isArray(payload) ? String(payload.error ?? JSON.stringify(payload)) : `HTTP ${response.status}`;
422
+ throw new CliError(`Rig server auth bootstrap failed (${response.status}): ${detail}`, 1, { hint: "Re-run `rig github auth import-gh`, or check the selected server with `rig server status`." });
423
+ }
354
424
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
355
425
  }
356
426
  var RESUMABLE_RUN_STATUSES = new Set([
@@ -564,6 +634,53 @@ function printPayload(context, payload, fallback) {
564
634
  else
565
635
  console.log(fallback);
566
636
  }
637
+ function apiSessionTokenFrom(payload) {
638
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
639
+ return null;
640
+ const token = payload.apiSessionToken;
641
+ return typeof token === "string" && token.trim() ? token.trim() : null;
642
+ }
643
+ function payloadString(payload, key) {
644
+ const value = payload[key];
645
+ return typeof value === "string" && value.trim() ? value.trim() : null;
646
+ }
647
+ function payloadRecord(payload, key) {
648
+ const value = payload[key];
649
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
650
+ }
651
+ function remoteNamespaceMetadata(result) {
652
+ const namespace = payloadRecord(result, "userNamespace");
653
+ return {
654
+ ...payloadString(result, "login") ? { login: payloadString(result, "login") } : {},
655
+ ...payloadString(result, "userId") ? { userId: payloadString(result, "userId") } : {},
656
+ ...namespace && payloadString(namespace, "key") ? { userNamespaceKey: payloadString(namespace, "key") } : {},
657
+ ...namespace && payloadString(namespace, "root") ? { userNamespaceRoot: payloadString(namespace, "root") } : {},
658
+ ...namespace && payloadString(namespace, "checkoutBaseDir") ? { checkoutBaseDir: payloadString(namespace, "checkoutBaseDir") } : {},
659
+ ...namespace && payloadString(namespace, "snapshotBaseDir") ? { snapshotBaseDir: payloadString(namespace, "snapshotBaseDir") } : {}
660
+ };
661
+ }
662
+ function persistRemoteAuthSession(context, source, result, fallbackToken) {
663
+ const apiSessionToken = apiSessionTokenFrom(result);
664
+ setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? fallbackToken, context.projectRoot);
665
+ if (!apiSessionToken)
666
+ return;
667
+ const repo = readRepoConnection(context.projectRoot);
668
+ const path = resolve3(context.projectRoot, ".rig", "state", "github-auth.json");
669
+ mkdirSync2(dirname2(path), { recursive: true });
670
+ writeFileSync2(path, `${JSON.stringify({
671
+ authenticated: true,
672
+ source,
673
+ storedOnServer: true,
674
+ ...repo?.project ? { selectedRepo: repo.project } : {},
675
+ ...remoteNamespaceMetadata(result),
676
+ apiSessionToken,
677
+ updatedAt: new Date().toISOString()
678
+ }, null, 2)}
679
+ `, "utf8");
680
+ }
681
+ function isSignedIn(status) {
682
+ return status.signedIn === true || status.authenticated === true;
683
+ }
567
684
  function readGhToken() {
568
685
  const result = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
569
686
  if (result.status !== 0) {
@@ -585,7 +702,7 @@ async function executeGithub(context, args) {
585
702
  if (rest.length > 0)
586
703
  throw new CliError("Usage: rig github auth status", 1);
587
704
  const status = await withSpinner("Checking GitHub auth on the server\u2026", () => getGitHubAuthStatusViaServer(context), { outputMode: context.outputMode });
588
- printPayload(context, status, `GitHub auth: ${status.authenticated ? "authenticated" : "unauthenticated"}`);
705
+ printPayload(context, status, `GitHub auth: ${isSignedIn(status) ? "authenticated" : "unauthenticated"}`);
589
706
  return { ok: true, group: "github", command: "auth status", details: status };
590
707
  }
591
708
  case "token": {
@@ -595,16 +712,20 @@ async function executeGithub(context, args) {
595
712
  const token = parsed.value?.trim();
596
713
  if (!token)
597
714
  throw new CliError("Missing --token value.", 1, { hint: "Re-run as `rig github auth token --token <token>`." });
598
- const result = await withSpinner("Storing GitHub token on the server\u2026", () => postGitHubTokenViaServer(context, token), { outputMode: context.outputMode });
599
- printPayload(context, result, "GitHub token stored on the selected Rig server.");
715
+ const repo = readRepoConnection(context.projectRoot);
716
+ const result = await withSpinner("Storing GitHub token on the server\u2026", () => postGitHubTokenViaServer(context, token, repo?.project ? { selectedRepo: repo.project } : {}), { outputMode: context.outputMode });
717
+ persistRemoteAuthSession(context, "token", result, token);
718
+ printPayload(context, result, "GitHub token stored on the selected server.");
600
719
  return { ok: true, group: "github", command: "auth token", details: result };
601
720
  }
602
721
  case "import-gh": {
603
722
  if (rest.length > 0)
604
723
  throw new CliError("Usage: rig github auth import-gh", 1);
605
724
  const importedToken = readGhToken();
606
- const result = await withSpinner("Storing GitHub token on the server\u2026", () => postGitHubTokenViaServer(context, importedToken), { outputMode: context.outputMode });
607
- printPayload(context, result, "GitHub token imported from gh and stored on the selected Rig server.");
725
+ const repo = readRepoConnection(context.projectRoot);
726
+ const result = await withSpinner("Storing GitHub token on the server\u2026", () => postGitHubTokenViaServer(context, importedToken, repo?.project ? { selectedRepo: repo.project } : {}), { outputMode: context.outputMode });
727
+ persistRemoteAuthSession(context, "gh", result, importedToken);
728
+ printPayload(context, result, "GitHub token imported from gh and stored on the selected server.");
608
729
  return { ok: true, group: "github", command: "auth import-gh", details: result };
609
730
  }
610
731
  default:
@@ -384,17 +384,21 @@ function cleanToken(value) {
384
384
  const trimmed = value?.trim();
385
385
  return trimmed ? trimmed : null;
386
386
  }
387
- function readPrivateRemoteSessionToken(projectRoot) {
387
+ function readRemoteAuthState(projectRoot) {
388
388
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
389
389
  if (!existsSync2(path))
390
390
  return null;
391
391
  try {
392
392
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
393
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
393
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
394
394
  } catch {
395
395
  return null;
396
396
  }
397
397
  }
398
+ function readPrivateRemoteSessionToken(projectRoot) {
399
+ const parsed = readRemoteAuthState(projectRoot);
400
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
401
+ }
398
402
  function readGitHubBearerTokenForRemote(projectRoot) {
399
403
  const scopedKey = resolve2(projectRoot);
400
404
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -405,15 +409,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
405
409
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
406
410
  }
407
411
  function readStoredGitHubAuthToken(projectRoot) {
408
- const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
409
- if (!existsSync2(path))
412
+ const parsed = readRemoteAuthState(projectRoot);
413
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
414
+ }
415
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
416
+ const repo = readRepoConnection(projectRoot);
417
+ const slug = repo?.project?.trim();
418
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
410
419
  return null;
411
- try {
412
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
413
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
414
- } catch {
420
+ const auth = readRemoteAuthState(projectRoot);
421
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
422
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
423
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
415
424
  return null;
416
- }
425
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
426
+ if (!checkoutBaseDir)
427
+ return null;
428
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
429
+ writeRepoServerProjectRoot(projectRoot, inferred);
430
+ return inferred;
417
431
  }
418
432
  function readLocalConnectionFallbackToken(projectRoot) {
419
433
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -424,7 +438,7 @@ async function ensureServerForCli(projectRoot) {
424
438
  if (selected?.connection.kind === "remote") {
425
439
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
426
440
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
427
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
441
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
428
442
  return {
429
443
  baseUrl: selected.connection.baseUrl,
430
444
  authToken,
@@ -453,7 +467,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
453
467
  if (!slug)
454
468
  return null;
455
469
  try {
456
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
470
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
471
+ if (authToken && queryAuthFallbackEnabled())
472
+ url.searchParams.set("rt", authToken);
473
+ const response = await fetch(url, {
457
474
  headers: mergeHeaders(undefined, authToken)
458
475
  });
459
476
  if (!response.ok)
@@ -470,10 +487,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
470
487
  return null;
471
488
  }
472
489
  }
490
+ function mergeCookie(existing, name, value) {
491
+ const encoded = `${name}=${encodeURIComponent(value)}`;
492
+ if (!existing?.trim())
493
+ return encoded;
494
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
495
+ return [...parts, encoded].join("; ");
496
+ }
497
+ function queryAuthFallbackEnabled(env = process.env) {
498
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
499
+ }
473
500
  function mergeHeaders(headers, authToken) {
474
501
  const merged = new Headers(headers);
475
502
  if (authToken) {
476
- merged.set("authorization", `Bearer ${authToken}`);
503
+ const bearer = `Bearer ${authToken}`;
504
+ merged.set("authorization", bearer);
505
+ merged.set("x-auth", bearer);
506
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
477
507
  }
478
508
  return merged;
479
509
  }
@@ -534,15 +564,34 @@ async function buildServerFailureContext(projectRoot, server) {
534
564
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
535
565
  };
536
566
  }
567
+ function isLoopbackRemoteBaseUrl(baseUrl) {
568
+ try {
569
+ const host = new URL(baseUrl).hostname.toLowerCase();
570
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
571
+ } catch {
572
+ return false;
573
+ }
574
+ }
575
+ function canUseRemoteWithoutProjectRoot(pathname) {
576
+ 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/");
577
+ }
537
578
  async function requestServerJson(context, pathname, init = {}) {
538
579
  const server = await ensureServerForCli(context.projectRoot);
580
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
581
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
582
+ const repo = readRepoConnection(context.projectRoot);
583
+ 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." });
584
+ }
539
585
  const headers = mergeHeaders(init.headers, server.authToken);
540
586
  if (server.serverProjectRoot)
541
587
  headers.set("x-rig-project-root", server.serverProjectRoot);
588
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
589
+ requestUrl.searchParams.set("rt", server.authToken);
590
+ }
542
591
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
543
592
  let response;
544
593
  try {
545
- response = await fetch(`${server.baseUrl}${pathname}`, {
594
+ response = await fetch(requestUrl, {
546
595
  ...init,
547
596
  headers
548
597
  });