@h-rig/cli 0.0.6-alpha.87 → 0.0.6-alpha.89

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 (78) hide show
  1. package/dist/bin/rig.js +1380 -865
  2. package/dist/src/app/board.js +462 -48
  3. package/dist/src/app-opentui/adapters/common.d.ts +3 -0
  4. package/dist/src/app-opentui/adapters/common.js +4 -0
  5. package/dist/src/app-opentui/adapters/doctor.js +458 -46
  6. package/dist/src/app-opentui/adapters/family.js +701 -151
  7. package/dist/src/app-opentui/adapters/fleet.js +477 -46
  8. package/dist/src/app-opentui/adapters/inbox.js +477 -46
  9. package/dist/src/app-opentui/adapters/init.js +497 -74
  10. package/dist/src/app-opentui/adapters/inspect.js +477 -46
  11. package/dist/src/app-opentui/adapters/pi-attach.d.ts +7 -0
  12. package/dist/src/app-opentui/adapters/pi-attach.js +1004 -519
  13. package/dist/src/app-opentui/adapters/run-detail.js +477 -46
  14. package/dist/src/app-opentui/adapters/server.d.ts +26 -0
  15. package/dist/src/app-opentui/adapters/server.js +676 -59
  16. package/dist/src/app-opentui/adapters/tasks.js +621 -549
  17. package/dist/src/app-opentui/autocomplete.js +4 -2
  18. package/dist/src/app-opentui/bootstrap.js +1376 -861
  19. package/dist/src/app-opentui/command-palette.js +37 -10
  20. package/dist/src/app-opentui/index.js +632 -528
  21. package/dist/src/app-opentui/intent.js +33 -8
  22. package/dist/src/app-opentui/keymap.js +43 -414
  23. package/dist/src/app-opentui/pi-host-child.js +496 -57
  24. package/dist/src/app-opentui/pi-pty-host.d.ts +14 -64
  25. package/dist/src/app-opentui/pi-pty-host.js +3 -397
  26. package/dist/src/app-opentui/react/App.js +144 -469
  27. package/dist/src/app-opentui/react/ChromeHost.js +44 -415
  28. package/dist/src/app-opentui/react/launch.js +659 -552
  29. package/dist/src/app-opentui/react/nav.js +1 -1
  30. package/dist/src/app-opentui/registry.js +1181 -742
  31. package/dist/src/app-opentui/remote-link.d.ts +10 -0
  32. package/dist/src/app-opentui/remote-link.js +47 -0
  33. package/dist/src/app-opentui/render/terminal-handoff.d.ts +16 -0
  34. package/dist/src/app-opentui/render/terminal-handoff.js +14 -0
  35. package/dist/src/app-opentui/runtime.js +632 -528
  36. package/dist/src/app-opentui/scenes/doctor.js +1 -1
  37. package/dist/src/app-opentui/scenes/error.js +50 -4
  38. package/dist/src/app-opentui/scenes/family.js +60 -6
  39. package/dist/src/app-opentui/scenes/fleet.js +65 -13
  40. package/dist/src/app-opentui/scenes/help.js +4 -2
  41. package/dist/src/app-opentui/scenes/init.js +12 -12
  42. package/dist/src/app-opentui/scenes/main.js +7 -7
  43. package/dist/src/app-opentui/scenes/server.js +83 -11
  44. package/dist/src/app-opentui/scenes/tasks.js +79 -16
  45. package/dist/src/app-opentui/state.js +25 -5
  46. package/dist/src/app-opentui/surface-catalog.js +4 -2
  47. package/dist/src/app-opentui/types.d.ts +1 -1
  48. package/dist/src/commands/_cli-format.d.ts +10 -1
  49. package/dist/src/commands/_cli-format.js +5 -2
  50. package/dist/src/commands/_connection-state.d.ts +11 -1
  51. package/dist/src/commands/_connection-state.js +50 -5
  52. package/dist/src/commands/_doctor-checks.js +458 -46
  53. package/dist/src/commands/_help-catalog.js +4 -2
  54. package/dist/src/commands/_json-output.js +4 -0
  55. package/dist/src/commands/_operator-view.js +496 -57
  56. package/dist/src/commands/_pi-frontend.d.ts +25 -0
  57. package/dist/src/commands/_pi-frontend.js +497 -57
  58. package/dist/src/commands/_preflight.js +509 -72
  59. package/dist/src/commands/_server-client.d.ts +33 -0
  60. package/dist/src/commands/_server-client.js +477 -46
  61. package/dist/src/commands/_server-events.js +446 -41
  62. package/dist/src/commands/_snapshot-upload.js +460 -48
  63. package/dist/src/commands/connect.js +620 -15
  64. package/dist/src/commands/doctor.js +458 -46
  65. package/dist/src/commands/github.js +462 -50
  66. package/dist/src/commands/inbox.js +458 -46
  67. package/dist/src/commands/init.js +497 -74
  68. package/dist/src/commands/inspect.js +458 -46
  69. package/dist/src/commands/run.js +496 -57
  70. package/dist/src/commands/server.js +647 -163
  71. package/dist/src/commands/setup.js +463 -51
  72. package/dist/src/commands/stats.js +462 -48
  73. package/dist/src/commands/task-run-driver.js +464 -52
  74. package/dist/src/commands/task.js +551 -85
  75. package/dist/src/commands.js +701 -151
  76. package/dist/src/index.js +705 -151
  77. package/dist/src/launcher.js +4 -0
  78. package/package.json +8 -8
@@ -86,8 +86,8 @@ function parsePositiveInt(value, option, fallback) {
86
86
  }
87
87
 
88
88
  // packages/cli/src/commands/_server-client.ts
89
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
90
- import { resolve as resolve2 } from "path";
89
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
90
+ import { dirname as dirname2, isAbsolute, resolve as resolve2 } from "path";
91
91
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
92
92
 
93
93
  // packages/cli/src/commands/_connection-state.ts
@@ -161,12 +161,28 @@ function readRepoConnection(projectRoot) {
161
161
  selected,
162
162
  project: typeof record.project === "string" ? record.project : undefined,
163
163
  linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
164
- serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
164
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined,
165
+ serverProjectRootAlias: typeof record.serverProjectRootAlias === "string" && record.serverProjectRootAlias.trim() ? record.serverProjectRootAlias.trim() : undefined,
166
+ serverProjectRootBaseUrl: typeof record.serverProjectRootBaseUrl === "string" && record.serverProjectRootBaseUrl.trim() ? record.serverProjectRootBaseUrl.trim().replace(/\/+$/, "") : undefined
165
167
  };
166
168
  }
167
169
  function writeRepoConnection(projectRoot, state) {
168
170
  writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
169
171
  }
172
+ function rootAllowedForSelection(repo, connection) {
173
+ const root = repo.serverProjectRoot?.trim();
174
+ if (!root)
175
+ return;
176
+ if (connection.kind === "remote") {
177
+ if (repo.serverProjectRootAlias !== repo.selected)
178
+ return;
179
+ if (repo.serverProjectRootBaseUrl !== connection.baseUrl)
180
+ return;
181
+ } else if (repo.serverProjectRootAlias && repo.serverProjectRootAlias !== repo.selected) {
182
+ return;
183
+ }
184
+ return root;
185
+ }
170
186
  function resolveSelectedConnection(projectRoot, options = {}) {
171
187
  const repo = readRepoConnection(projectRoot);
172
188
  if (!repo)
@@ -178,13 +194,41 @@ function resolveSelectedConnection(projectRoot, options = {}) {
178
194
  if (!connection) {
179
195
  throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
180
196
  }
181
- return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
197
+ return { alias: repo.selected, connection, serverProjectRoot: rootAllowedForSelection(repo, connection) };
182
198
  }
183
- function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
199
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot, metadata = {}) {
184
200
  const repo = readRepoConnection(projectRoot);
185
201
  if (!repo)
186
202
  return;
187
- writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
203
+ let inferred = metadata;
204
+ if (!inferred.alias || !inferred.baseUrl) {
205
+ try {
206
+ const selected = resolveSelectedConnection(projectRoot);
207
+ if (selected?.connection.kind === "remote") {
208
+ inferred = {
209
+ alias: inferred.alias ?? selected.alias,
210
+ baseUrl: inferred.baseUrl ?? selected.connection.baseUrl
211
+ };
212
+ }
213
+ } catch {}
214
+ }
215
+ writeRepoConnection(projectRoot, {
216
+ ...repo,
217
+ ...metadata.project ? { project: metadata.project } : {},
218
+ serverProjectRoot,
219
+ ...inferred.alias ? { serverProjectRootAlias: inferred.alias } : {},
220
+ ...inferred.baseUrl ? { serverProjectRootBaseUrl: inferred.baseUrl.replace(/\/+$/, "") } : {}
221
+ });
222
+ }
223
+ function clearRepoServerProjectRoot(projectRoot) {
224
+ const repo = readRepoConnection(projectRoot);
225
+ if (!repo)
226
+ return;
227
+ writeRepoConnection(projectRoot, {
228
+ selected: repo.selected,
229
+ ...repo.project ? { project: repo.project } : {},
230
+ ...repo.linkedAt ? { linkedAt: repo.linkedAt } : {}
231
+ });
188
232
  }
189
233
  function isRemoteConnectionSelected(projectRoot) {
190
234
  return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
@@ -233,11 +277,10 @@ function readStoredGitHubAuthToken(projectRoot) {
233
277
  const parsed = readRemoteAuthState(projectRoot);
234
278
  return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
235
279
  }
236
- function inferRemoteServerProjectRootFromAuthState(projectRoot) {
237
- const repo = readRepoConnection(projectRoot);
238
- const slug = repo?.project?.trim();
239
- if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
280
+ function inferRemoteServerProjectRootCandidateFromAuthState(projectRoot, repoSlug) {
281
+ if (!/^[^/]+\/[^/]+$/.test(repoSlug))
240
282
  return null;
283
+ const repo = readRepoConnection(projectRoot);
241
284
  const auth = readRemoteAuthState(projectRoot);
242
285
  const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
243
286
  const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
@@ -246,25 +289,413 @@ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
246
289
  const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
247
290
  if (!checkoutBaseDir)
248
291
  return null;
249
- const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
250
- writeRepoServerProjectRoot(projectRoot, inferred);
251
- return inferred;
292
+ return `${checkoutBaseDir.replace(/\/+$/, "")}/${repoSlug}`;
252
293
  }
253
294
  function readLocalConnectionFallbackToken(projectRoot) {
254
295
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
255
296
  }
297
+ function normalizeRepoSlug(value) {
298
+ const slug = value?.trim();
299
+ return slug && /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
300
+ }
301
+ function readProjectLinkSlug(projectRoot) {
302
+ const path = resolve2(projectRoot, ".rig", "state", "project-link.json");
303
+ if (!existsSync2(path))
304
+ return null;
305
+ try {
306
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
307
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
308
+ return null;
309
+ return normalizeRepoSlug(typeof parsed.repoSlug === "string" ? String(parsed.repoSlug) : null);
310
+ } catch {
311
+ return null;
312
+ }
313
+ }
314
+ function writeProjectLinkConnection(projectRoot, repoSlug, alias) {
315
+ const path = resolve2(projectRoot, ".rig", "state", "project-link.json");
316
+ mkdirSync2(dirname2(path), { recursive: true });
317
+ writeFileSync2(path, `${JSON.stringify({ repoSlug, connection: alias, linkedAt: new Date().toISOString() }, null, 2)}
318
+ `, "utf8");
319
+ }
320
+ function remoteLinkRepairCommand(repoSlug) {
321
+ return repoSlug ? `rig server repair-link --repo ${repoSlug}` : "rig server repair-link --repo owner/repo";
322
+ }
323
+ function formatRemoteProjectLinkHint(resolution) {
324
+ const repair = remoteLinkRepairCommand(resolution.repoSlug);
325
+ if (resolution.status === "auth_required") {
326
+ return `Authenticate the selected remote${resolution.alias ? ` (${resolution.alias})` : ""}, then run \`${repair}\`. Use \`rig github auth import-gh\` or \`rig init --repair --server remote --github-auth device${resolution.repoSlug ? ` --repo ${resolution.repoSlug}` : ""}\`.`;
327
+ }
328
+ if (resolution.status === "missing_project") {
329
+ return `Record the repo slug, then run \`${repair}\` (or \`rig init --repo owner/repo --server remote --github-auth device\`).`;
330
+ }
331
+ if (resolution.status === "not_remote")
332
+ return "Select a remote server with `rig server use <alias>` before repairing a remote project link.";
333
+ return `Run \`${repair}\` to backfill or prepare a server-host checkout for the selected remote${resolution.alias ? ` (${resolution.alias})` : ""}.`;
334
+ }
335
+ function remoteProjectLinkFailure(input) {
336
+ const partial = {
337
+ status: input.status,
338
+ alias: input.alias,
339
+ baseUrl: input.baseUrl,
340
+ repoSlug: input.repoSlug
341
+ };
342
+ return {
343
+ ok: false,
344
+ ...input,
345
+ hint: formatRemoteProjectLinkHint(partial),
346
+ next: remoteLinkRepairCommand(input.repoSlug)
347
+ };
348
+ }
349
+ function remoteProjectLinkSuccess(input) {
350
+ return {
351
+ ok: true,
352
+ status: input.status,
353
+ alias: input.alias,
354
+ baseUrl: input.baseUrl,
355
+ repoSlug: input.repoSlug,
356
+ serverProjectRoot: input.serverProjectRoot,
357
+ source: input.source,
358
+ prepared: input.prepared ?? input.status === "prepared",
359
+ validated: input.validated ?? false,
360
+ message: input.message ?? `Remote project link ready for ${input.repoSlug}.`,
361
+ hint: "Remote-scoped task/run/status requests will send x-rig-project-root for this server-host checkout.",
362
+ next: "rig task list"
363
+ };
364
+ }
365
+ function localCheckoutPathCandidate(projectRoot, candidate) {
366
+ try {
367
+ const local = resolve2(projectRoot);
368
+ const resolved = resolve2(candidate);
369
+ return resolved === local || resolved.startsWith(`${local}/`);
370
+ } catch {
371
+ return false;
372
+ }
373
+ }
374
+ async function requestRemoteProjectLinkJson(baseUrl, pathname, authToken, init = {}) {
375
+ const requestUrl = new URL(`${baseUrl}${pathname}`);
376
+ if (authToken && queryAuthFallbackEnabled())
377
+ requestUrl.searchParams.set("rt", authToken);
378
+ const response = await fetch(requestUrl, {
379
+ ...init,
380
+ headers: mergeHeaders(init.headers, authToken)
381
+ });
382
+ const text = await response.text();
383
+ const payload = text.trim().length > 0 ? (() => {
384
+ try {
385
+ return JSON.parse(text);
386
+ } catch {
387
+ return null;
388
+ }
389
+ })() : null;
390
+ return { ok: response.ok, status: response.status, payload, text };
391
+ }
392
+ function payloadError(payload, fallback) {
393
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
394
+ const record = payload;
395
+ const value = record.error ?? record.message ?? record.reason;
396
+ if (typeof value === "string" && value.trim())
397
+ return value.trim();
398
+ }
399
+ return fallback;
400
+ }
401
+ function checkoutPathsFromProjectPayload(payload) {
402
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
403
+ return [];
404
+ const project = payload.project;
405
+ if (!project || typeof project !== "object" || Array.isArray(project))
406
+ return [];
407
+ const checkouts = project.checkouts;
408
+ if (!Array.isArray(checkouts))
409
+ return [];
410
+ return [...checkouts].reverse().flatMap((entry) => {
411
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
412
+ return [];
413
+ const path = entry.path;
414
+ return typeof path === "string" && path.trim() ? [path.trim()] : [];
415
+ });
416
+ }
417
+ function checkoutPathFromPreparePayload(payload) {
418
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
419
+ return null;
420
+ const checkout = payload.checkout;
421
+ if (!checkout || typeof checkout !== "object" || Array.isArray(checkout))
422
+ return null;
423
+ const path = checkout.path;
424
+ return typeof path === "string" && path.trim() ? path.trim() : null;
425
+ }
426
+ async function validateAndPersistRemoteRoot(input) {
427
+ const candidate = input.candidate.trim();
428
+ if (!candidate || !isAbsolute(candidate)) {
429
+ return remoteProjectLinkFailure({
430
+ status: "invalid_root",
431
+ alias: input.alias,
432
+ baseUrl: input.baseUrl,
433
+ repoSlug: input.repoSlug,
434
+ message: `Remote project root candidate is not an absolute server-host path: ${candidate || "(empty)"}.`
435
+ });
436
+ }
437
+ if (localCheckoutPathCandidate(input.projectRoot, candidate)) {
438
+ return remoteProjectLinkFailure({
439
+ status: "invalid_root",
440
+ alias: input.alias,
441
+ baseUrl: input.baseUrl,
442
+ repoSlug: input.repoSlug,
443
+ message: `Refusing to use local checkout path ${candidate} as a remote server project root.`
444
+ });
445
+ }
446
+ let response;
447
+ try {
448
+ response = await requestRemoteProjectLinkJson(input.baseUrl, "/api/server/project-root", input.authToken, {
449
+ method: "POST",
450
+ headers: { "content-type": "application/json" },
451
+ body: JSON.stringify({ projectRoot: candidate })
452
+ });
453
+ } catch (error) {
454
+ return remoteProjectLinkFailure({
455
+ status: "error",
456
+ alias: input.alias,
457
+ baseUrl: input.baseUrl,
458
+ repoSlug: input.repoSlug,
459
+ message: `Could not validate remote project root ${candidate}: ${error instanceof Error ? error.message : String(error)}`
460
+ });
461
+ }
462
+ if (response.status === 401 || response.status === 403) {
463
+ return remoteProjectLinkFailure({
464
+ status: "auth_required",
465
+ alias: input.alias,
466
+ baseUrl: input.baseUrl,
467
+ repoSlug: input.repoSlug,
468
+ statusCode: response.status,
469
+ message: `Remote server ${input.alias} requires authentication before validating project root ${candidate}.`
470
+ });
471
+ }
472
+ if (!response.ok) {
473
+ return remoteProjectLinkFailure({
474
+ status: "invalid_root",
475
+ alias: input.alias,
476
+ baseUrl: input.baseUrl,
477
+ repoSlug: input.repoSlug,
478
+ statusCode: response.status,
479
+ message: `Remote server rejected project root ${candidate} (${response.status}): ${payloadError(response.payload, response.text || "project-root validation failed")}`
480
+ });
481
+ }
482
+ const record = response.payload && typeof response.payload === "object" && !Array.isArray(response.payload) ? response.payload : {};
483
+ if (record.ok !== true) {
484
+ return remoteProjectLinkFailure({
485
+ status: "invalid_root",
486
+ alias: input.alias,
487
+ baseUrl: input.baseUrl,
488
+ repoSlug: input.repoSlug,
489
+ message: `Remote server did not accept project root ${candidate}: ${payloadError(response.payload, "project-root validation failed")}`
490
+ });
491
+ }
492
+ const accepted = typeof record.projectRoot === "string" && record.projectRoot.trim() ? record.projectRoot.trim() : typeof record.requestedProjectRoot === "string" && record.requestedProjectRoot.trim() ? record.requestedProjectRoot.trim() : candidate;
493
+ writeRepoServerProjectRoot(input.projectRoot, accepted, { alias: input.alias, baseUrl: input.baseUrl, project: input.repoSlug });
494
+ writeProjectLinkConnection(input.projectRoot, input.repoSlug, input.alias);
495
+ const status = input.status ?? (input.prepared ? "prepared" : "backfilled");
496
+ return remoteProjectLinkSuccess({
497
+ status,
498
+ alias: input.alias,
499
+ baseUrl: input.baseUrl,
500
+ repoSlug: input.repoSlug,
501
+ serverProjectRoot: accepted,
502
+ source: input.source,
503
+ prepared: input.prepared,
504
+ validated: true,
505
+ message: status === "ready" ? `Remote project link already points to ${accepted} and was validated on ${input.alias}.` : input.prepared ? `Prepared and linked remote checkout ${accepted} for ${input.repoSlug}.` : `Backfilled remote checkout ${accepted} for ${input.repoSlug}.`
506
+ });
507
+ }
508
+ async function ensureRemoteProjectRootLink(projectRoot, options = {}) {
509
+ let selected;
510
+ try {
511
+ selected = resolveSelectedConnection(projectRoot);
512
+ } catch (error) {
513
+ return remoteProjectLinkFailure({ status: "error", message: error instanceof Error ? error.message : String(error) });
514
+ }
515
+ if (!selected || selected.connection.kind !== "remote") {
516
+ return remoteProjectLinkFailure({ status: "not_remote", message: "No remote Rig server is selected for this repo." });
517
+ }
518
+ const repo = readRepoConnection(projectRoot);
519
+ const explicitRepoRequested = options.repoSlug !== undefined;
520
+ const explicitRepoSlug = explicitRepoRequested ? normalizeRepoSlug(options.repoSlug) : null;
521
+ const repoProjectSlug = normalizeRepoSlug(repo?.project);
522
+ const repoSlug = explicitRepoSlug ?? repoProjectSlug ?? readProjectLinkSlug(projectRoot);
523
+ const alias = selected.alias;
524
+ const baseUrl = selected.connection.baseUrl;
525
+ const authToken = options.authToken === undefined ? readGitHubBearerTokenForRemote(projectRoot) : options.authToken;
526
+ const mode = options.mode ?? "backfill-only";
527
+ if (explicitRepoRequested && !explicitRepoSlug) {
528
+ return remoteProjectLinkFailure({
529
+ status: "missing_project",
530
+ alias,
531
+ baseUrl,
532
+ message: `Invalid --repo value ${JSON.stringify(options.repoSlug)}. Expected owner/repo.`
533
+ });
534
+ }
535
+ if (!repoSlug) {
536
+ return remoteProjectLinkFailure({
537
+ status: "missing_project",
538
+ alias,
539
+ baseUrl,
540
+ message: `Remote server ${alias} is selected, but this checkout has no GitHub repo slug recorded.`
541
+ });
542
+ }
543
+ const skippedCandidates = [];
544
+ const storedRoot = selected.serverProjectRoot?.trim() || null;
545
+ const storedRootMatchesResolvedRepo = repoProjectSlug === repoSlug;
546
+ if (storedRoot && storedRootMatchesResolvedRepo) {
547
+ if (localCheckoutPathCandidate(projectRoot, storedRoot)) {
548
+ clearRepoServerProjectRoot(projectRoot);
549
+ skippedCandidates.push(storedRoot);
550
+ } else {
551
+ const storedResult = await validateAndPersistRemoteRoot({
552
+ projectRoot,
553
+ alias,
554
+ baseUrl,
555
+ authToken,
556
+ repoSlug,
557
+ candidate: storedRoot,
558
+ source: "stored",
559
+ status: "ready"
560
+ });
561
+ if (storedResult.ok || storedResult.status === "auth_required" || storedResult.status === "error")
562
+ return storedResult;
563
+ clearRepoServerProjectRoot(projectRoot);
564
+ skippedCandidates.push(storedRoot);
565
+ }
566
+ } else if (storedRoot) {
567
+ skippedCandidates.push(storedRoot);
568
+ }
569
+ const authCandidate = inferRemoteServerProjectRootCandidateFromAuthState(projectRoot, repoSlug);
570
+ if (authCandidate) {
571
+ const authResult = await validateAndPersistRemoteRoot({ projectRoot, alias, baseUrl, authToken, repoSlug, candidate: authCandidate, source: "auth-state" });
572
+ if (authResult.ok || authResult.status === "auth_required")
573
+ return authResult;
574
+ }
575
+ let registryResponse;
576
+ try {
577
+ registryResponse = await requestRemoteProjectLinkJson(baseUrl, `/api/projects/${encodeURIComponent(repoSlug)}`, authToken);
578
+ } catch (error) {
579
+ return remoteProjectLinkFailure({
580
+ status: "error",
581
+ alias,
582
+ baseUrl,
583
+ repoSlug,
584
+ message: `Could not read remote project registry for ${repoSlug} on ${alias}: ${error instanceof Error ? error.message : String(error)}`
585
+ });
586
+ }
587
+ if (registryResponse.status === 401 || registryResponse.status === 403) {
588
+ return remoteProjectLinkFailure({
589
+ status: "auth_required",
590
+ alias,
591
+ baseUrl,
592
+ repoSlug,
593
+ statusCode: registryResponse.status,
594
+ message: `Remote server ${alias} requires authentication before reading the ${repoSlug} project registry.`
595
+ });
596
+ }
597
+ if (registryResponse.ok) {
598
+ const candidates = checkoutPathsFromProjectPayload(registryResponse.payload);
599
+ for (const candidate of candidates) {
600
+ const result = await validateAndPersistRemoteRoot({ projectRoot, alias, baseUrl, authToken, repoSlug, candidate, source: "registry" });
601
+ if (result.ok || result.status === "auth_required")
602
+ return result;
603
+ skippedCandidates.push(candidate);
604
+ }
605
+ if (mode === "backfill-only") {
606
+ return remoteProjectLinkFailure({
607
+ status: candidates.length > 0 ? "invalid_root" : "no_server_checkout",
608
+ alias,
609
+ baseUrl,
610
+ repoSlug,
611
+ message: candidates.length > 0 ? `Remote registry has checkout candidates for ${repoSlug}, but none validated for ${alias}.` : `Remote registry has ${repoSlug}, but no server checkout path is linked yet.`,
612
+ skippedCandidates
613
+ });
614
+ }
615
+ } else if (registryResponse.status === 404) {
616
+ if (mode === "backfill-only") {
617
+ return remoteProjectLinkFailure({
618
+ status: "project_not_registered",
619
+ alias,
620
+ baseUrl,
621
+ repoSlug,
622
+ statusCode: registryResponse.status,
623
+ message: `Remote server ${alias} has no registered project record for ${repoSlug}.`
624
+ });
625
+ }
626
+ } else {
627
+ return remoteProjectLinkFailure({
628
+ status: "error",
629
+ alias,
630
+ baseUrl,
631
+ repoSlug,
632
+ statusCode: registryResponse.status,
633
+ message: `Could not backfill ${repoSlug} from remote registry (${registryResponse.status}): ${payloadError(registryResponse.payload, registryResponse.text || "registry lookup failed")}`,
634
+ skippedCandidates
635
+ });
636
+ }
637
+ let prepareResponse;
638
+ try {
639
+ prepareResponse = await requestRemoteProjectLinkJson(baseUrl, `/api/projects/${encodeURIComponent(repoSlug)}/prepare-checkout`, authToken, {
640
+ method: "POST",
641
+ headers: { "content-type": "application/json" },
642
+ body: JSON.stringify({ checkout: { kind: "managed-clone" }, repoUrl: `https://github.com/${repoSlug}.git` })
643
+ });
644
+ } catch (error) {
645
+ return remoteProjectLinkFailure({
646
+ status: "error",
647
+ alias,
648
+ baseUrl,
649
+ repoSlug,
650
+ message: `Could not prepare remote checkout for ${repoSlug} on ${alias}: ${error instanceof Error ? error.message : String(error)}`
651
+ });
652
+ }
653
+ if (prepareResponse.status === 401 || prepareResponse.status === 403) {
654
+ return remoteProjectLinkFailure({
655
+ status: "auth_required",
656
+ alias,
657
+ baseUrl,
658
+ repoSlug,
659
+ statusCode: prepareResponse.status,
660
+ message: `Remote server ${alias} requires authentication before preparing ${repoSlug}.`
661
+ });
662
+ }
663
+ if (!prepareResponse.ok) {
664
+ return remoteProjectLinkFailure({
665
+ status: "error",
666
+ alias,
667
+ baseUrl,
668
+ repoSlug,
669
+ statusCode: prepareResponse.status,
670
+ message: `Remote checkout prepare failed for ${repoSlug} (${prepareResponse.status}): ${payloadError(prepareResponse.payload, prepareResponse.text || "prepare-checkout failed")}`,
671
+ skippedCandidates
672
+ });
673
+ }
674
+ const preparedPath = checkoutPathFromPreparePayload(prepareResponse.payload);
675
+ if (!preparedPath) {
676
+ return remoteProjectLinkFailure({
677
+ status: "invalid_root",
678
+ alias,
679
+ baseUrl,
680
+ repoSlug,
681
+ message: `Remote checkout prepare for ${repoSlug} did not return a checkout.path.`,
682
+ skippedCandidates
683
+ });
684
+ }
685
+ return validateAndPersistRemoteRoot({ projectRoot, alias, baseUrl, authToken, repoSlug, candidate: preparedPath, source: "prepare", prepared: true });
686
+ }
256
687
  async function ensureServerForCli(projectRoot) {
257
688
  try {
258
689
  const selected = resolveSelectedConnection(projectRoot);
259
690
  if (selected?.connection.kind === "remote") {
260
691
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
261
692
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
262
- const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
693
+ const link = await ensureRemoteProjectRootLink(projectRoot, { mode: "backfill-only", authToken });
263
694
  return {
264
695
  baseUrl: selected.connection.baseUrl,
265
696
  authToken,
266
697
  connectionKind: "remote",
267
- serverProjectRoot
698
+ serverProjectRoot: link.ok ? link.serverProjectRoot ?? null : null
268
699
  };
269
700
  }
270
701
  reportServerPhase("Starting local Rig server\u2026");
@@ -282,32 +713,6 @@ async function ensureServerForCli(projectRoot) {
282
713
  throw error;
283
714
  }
284
715
  }
285
- async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
286
- const repo = readRepoConnection(projectRoot);
287
- const slug = repo?.project?.trim();
288
- if (!slug)
289
- return null;
290
- try {
291
- const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
292
- if (authToken && queryAuthFallbackEnabled())
293
- url.searchParams.set("rt", authToken);
294
- const response = await fetch(url, {
295
- headers: mergeHeaders(undefined, authToken)
296
- });
297
- if (!response.ok)
298
- return null;
299
- const payload = await response.json();
300
- const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
301
- const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
302
- const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
303
- const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
304
- if (path)
305
- writeRepoServerProjectRoot(projectRoot, path);
306
- return path;
307
- } catch {
308
- return null;
309
- }
310
- }
311
716
  function mergeCookie(existing, name, value) {
312
717
  const encoded = `${name}=${encodeURIComponent(value)}`;
313
718
  if (!existing?.trim())
@@ -399,13 +804,20 @@ function canUseRemoteWithoutProjectRoot(pathname) {
399
804
  async function requestServerJson(context, pathname, init = {}) {
400
805
  const server = await ensureServerForCli(context.projectRoot);
401
806
  const requestUrl = new URL(`${server.baseUrl}${pathname}`);
402
- if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
403
- const repo = readRepoConnection(context.projectRoot);
404
- 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." });
807
+ let scopedServerProjectRoot = server.serverProjectRoot;
808
+ if (server.connectionKind === "remote" && !scopedServerProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
809
+ const link = await ensureRemoteProjectRootLink(context.projectRoot, { mode: "backfill-only", authToken: server.authToken });
810
+ if (link.ok && link.serverProjectRoot) {
811
+ scopedServerProjectRoot = link.serverProjectRoot;
812
+ } else {
813
+ const repo = readRepoConnection(context.projectRoot);
814
+ const target = link.alias && link.baseUrl ? `${link.alias} (${link.baseUrl})` : server.baseUrl;
815
+ throw new CliError(`Remote server ${target} is selected for ${link.repoSlug ?? repo?.project ?? "this repo"}, but this checkout has no server-host project root link. ${link.message}`, 1, { hint: link.hint });
816
+ }
405
817
  }
406
818
  const headers = mergeHeaders(init.headers, server.authToken);
407
- if (server.serverProjectRoot)
408
- headers.set("x-rig-project-root", server.serverProjectRoot);
819
+ if (scopedServerProjectRoot)
820
+ headers.set("x-rig-project-root", scopedServerProjectRoot);
409
821
  if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
410
822
  requestUrl.searchParams.set("rt", server.authToken);
411
823
  }
@@ -858,7 +1270,7 @@ function createOperatorSurface(options = {}) {
858
1270
  }
859
1271
 
860
1272
  // packages/cli/src/commands/_pi-frontend.ts
861
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
1273
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "fs";
862
1274
  import { homedir as homedir2, tmpdir } from "os";
863
1275
  import { join as join2 } from "path";
864
1276
  import { main as runPiMain } from "@earendil-works/pi-coding-agent";
@@ -1112,7 +1524,7 @@ function statusFromRunDetails(run) {
1112
1524
  async function prepareOperatorConsole(context, runId, tempSessionDir) {
1113
1525
  const server = await ensureServerForCli(context.projectRoot);
1114
1526
  const localCwd = join2(homedir2(), ".rig", "drones", runId.slice(0, 8));
1115
- mkdirSync2(localCwd, { recursive: true });
1527
+ mkdirSync3(localCwd, { recursive: true });
1116
1528
  trustDroneCwd(localCwd);
1117
1529
  installRigPiTheme();
1118
1530
  let sessionFileArg = [];
@@ -1134,7 +1546,7 @@ async function prepareOperatorConsole(context, runId, tempSessionDir) {
1134
1546
  return line;
1135
1547
  }).join(`
1136
1548
  `);
1137
- writeFileSync2(localSessionPath, content);
1549
+ writeFileSync3(localSessionPath, content);
1138
1550
  sessionFileArg = ["--session", localSessionPath];
1139
1551
  }
1140
1552
  } catch {}
@@ -1224,12 +1636,12 @@ var RIG_PI_THEME = {
1224
1636
  function trustDroneCwd(localCwd) {
1225
1637
  try {
1226
1638
  const agentDir = join2(homedir2(), ".pi", "agent");
1227
- mkdirSync2(agentDir, { recursive: true });
1639
+ mkdirSync3(agentDir, { recursive: true });
1228
1640
  const trustPath = join2(agentDir, "trust.json");
1229
1641
  const store = existsSync4(trustPath) ? JSON.parse(readFileSync3(trustPath, "utf8")) : {};
1230
1642
  if (store[localCwd] !== true) {
1231
1643
  store[localCwd] = true;
1232
- writeFileSync2(trustPath, `${JSON.stringify(store, null, "\t")}
1644
+ writeFileSync3(trustPath, `${JSON.stringify(store, null, "\t")}
1233
1645
  `);
1234
1646
  }
1235
1647
  } catch {}
@@ -1237,15 +1649,38 @@ function trustDroneCwd(localCwd) {
1237
1649
  function installRigPiTheme() {
1238
1650
  try {
1239
1651
  const themesDir = join2(homedir2(), ".pi", "agent", "themes");
1240
- mkdirSync2(themesDir, { recursive: true });
1652
+ mkdirSync3(themesDir, { recursive: true });
1241
1653
  const themePath = join2(themesDir, "rig.json");
1242
1654
  const next = `${JSON.stringify(RIG_PI_THEME, null, "\t")}
1243
1655
  `;
1244
1656
  if (!existsSync4(themePath) || readFileSync3(themePath, "utf8") !== next) {
1245
- writeFileSync2(themePath, next);
1657
+ writeFileSync3(themePath, next);
1246
1658
  }
1247
1659
  } catch {}
1248
1660
  }
1661
+ async function runWithProcessExitGuard(body) {
1662
+ const realExit = process.exit;
1663
+ let exitCode = 0;
1664
+ let signalQuit = () => {};
1665
+ const quit = new Promise((resolve4) => {
1666
+ signalQuit = resolve4;
1667
+ });
1668
+ const guardedExit = (code) => {
1669
+ exitCode = typeof code === "number" ? code : 0;
1670
+ signalQuit();
1671
+ return;
1672
+ };
1673
+ process.exit = guardedExit;
1674
+ try {
1675
+ await Promise.race([Promise.resolve().then(body), quit]);
1676
+ } finally {
1677
+ process.exit = realExit;
1678
+ }
1679
+ return exitCode;
1680
+ }
1681
+ function runPiMainReturningOnQuit(args, options) {
1682
+ return runWithProcessExitGuard(() => runPiMain(args, options));
1683
+ }
1249
1684
  async function attachRunBundledPiFrontend(context, input) {
1250
1685
  const tempSessionDir = mkdtempSync(join2(tmpdir(), "rig-pi-frontend-sessions-"));
1251
1686
  const { server, sessionFileArg } = await withSpinner(`Opening Pi console for run ${input.runId}\u2026`, () => prepareOperatorConsole(context, input.runId, tempSessionDir), { outputMode: context.outputMode });
@@ -1261,16 +1696,20 @@ async function attachRunBundledPiFrontend(context, input) {
1261
1696
  };
1262
1697
  let detached = false;
1263
1698
  try {
1264
- await runPiMain([
1699
+ const piArgs = [
1265
1700
  "--offline",
1266
1701
  "--no-extensions",
1267
1702
  "--no-skills",
1268
1703
  "--no-prompt-templates",
1269
1704
  "--no-context-files",
1270
1705
  ...sessionFileArg
1271
- ], {
1272
- extensionFactories: [piRigExtensionFactory]
1273
- });
1706
+ ];
1707
+ const piOptions = { extensionFactories: [piRigExtensionFactory] };
1708
+ if (input.returnOnQuit) {
1709
+ await runPiMainReturningOnQuit(piArgs, piOptions);
1710
+ } else {
1711
+ await runPiMain(piArgs, piOptions);
1712
+ }
1274
1713
  detached = true;
1275
1714
  } finally {
1276
1715
  restoreEnv();