@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
@@ -133,17 +133,21 @@ function cleanToken(value) {
133
133
  const trimmed = value?.trim();
134
134
  return trimmed ? trimmed : null;
135
135
  }
136
- function readPrivateRemoteSessionToken(projectRoot) {
136
+ function readRemoteAuthState(projectRoot) {
137
137
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
138
138
  if (!existsSync2(path))
139
139
  return null;
140
140
  try {
141
141
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
142
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
142
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
143
143
  } catch {
144
144
  return null;
145
145
  }
146
146
  }
147
+ function readPrivateRemoteSessionToken(projectRoot) {
148
+ const parsed = readRemoteAuthState(projectRoot);
149
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
150
+ }
147
151
  function readGitHubBearerTokenForRemote(projectRoot) {
148
152
  const scopedKey = resolve2(projectRoot);
149
153
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -154,15 +158,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
154
158
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
155
159
  }
156
160
  function readStoredGitHubAuthToken(projectRoot) {
157
- const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
158
- if (!existsSync2(path))
161
+ const parsed = readRemoteAuthState(projectRoot);
162
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
163
+ }
164
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
165
+ const repo = readRepoConnection(projectRoot);
166
+ const slug = repo?.project?.trim();
167
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
159
168
  return null;
160
- try {
161
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
162
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
163
- } catch {
169
+ const auth = readRemoteAuthState(projectRoot);
170
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
171
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
172
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
164
173
  return null;
165
- }
174
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
175
+ if (!checkoutBaseDir)
176
+ return null;
177
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
178
+ writeRepoServerProjectRoot(projectRoot, inferred);
179
+ return inferred;
166
180
  }
167
181
  function readLocalConnectionFallbackToken(projectRoot) {
168
182
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -173,7 +187,7 @@ async function ensureServerForCli(projectRoot) {
173
187
  if (selected?.connection.kind === "remote") {
174
188
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
175
189
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
176
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
190
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
177
191
  return {
178
192
  baseUrl: selected.connection.baseUrl,
179
193
  authToken,
@@ -202,7 +216,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
202
216
  if (!slug)
203
217
  return null;
204
218
  try {
205
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
219
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
220
+ if (authToken && queryAuthFallbackEnabled())
221
+ url.searchParams.set("rt", authToken);
222
+ const response = await fetch(url, {
206
223
  headers: mergeHeaders(undefined, authToken)
207
224
  });
208
225
  if (!response.ok)
@@ -219,10 +236,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
219
236
  return null;
220
237
  }
221
238
  }
239
+ function mergeCookie(existing, name, value) {
240
+ const encoded = `${name}=${encodeURIComponent(value)}`;
241
+ if (!existing?.trim())
242
+ return encoded;
243
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
244
+ return [...parts, encoded].join("; ");
245
+ }
246
+ function queryAuthFallbackEnabled(env = process.env) {
247
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
248
+ }
222
249
  function mergeHeaders(headers, authToken) {
223
250
  const merged = new Headers(headers);
224
251
  if (authToken) {
225
- merged.set("authorization", `Bearer ${authToken}`);
252
+ const bearer = `Bearer ${authToken}`;
253
+ merged.set("authorization", bearer);
254
+ merged.set("x-auth", bearer);
255
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
226
256
  }
227
257
  return merged;
228
258
  }
@@ -283,15 +313,34 @@ async function buildServerFailureContext(projectRoot, server) {
283
313
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
284
314
  };
285
315
  }
316
+ function isLoopbackRemoteBaseUrl(baseUrl) {
317
+ try {
318
+ const host = new URL(baseUrl).hostname.toLowerCase();
319
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+ function canUseRemoteWithoutProjectRoot(pathname) {
325
+ 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/");
326
+ }
286
327
  async function requestServerJson(context, pathname, init = {}) {
287
328
  const server = await ensureServerForCli(context.projectRoot);
329
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
330
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
331
+ const repo = readRepoConnection(context.projectRoot);
332
+ 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." });
333
+ }
288
334
  const headers = mergeHeaders(init.headers, server.authToken);
289
335
  if (server.serverProjectRoot)
290
336
  headers.set("x-rig-project-root", server.serverProjectRoot);
337
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
338
+ requestUrl.searchParams.set("rt", server.authToken);
339
+ }
291
340
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
292
341
  let response;
293
342
  try {
294
- response = await fetch(`${server.baseUrl}${pathname}`, {
343
+ response = await fetch(requestUrl, {
295
344
  ...init,
296
345
  headers
297
346
  });
@@ -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: