@h-rig/cli 0.0.6-alpha.88 → 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 (70) hide show
  1. package/dist/bin/rig.js +1159 -292
  2. package/dist/src/app/board.js +462 -48
  3. package/dist/src/app-opentui/adapters/doctor.js +458 -46
  4. package/dist/src/app-opentui/adapters/family.js +670 -147
  5. package/dist/src/app-opentui/adapters/fleet.js +477 -46
  6. package/dist/src/app-opentui/adapters/inbox.js +477 -46
  7. package/dist/src/app-opentui/adapters/init.js +497 -74
  8. package/dist/src/app-opentui/adapters/inspect.js +477 -46
  9. package/dist/src/app-opentui/adapters/pi-attach.js +484 -53
  10. package/dist/src/app-opentui/adapters/run-detail.js +477 -46
  11. package/dist/src/app-opentui/adapters/server.d.ts +26 -0
  12. package/dist/src/app-opentui/adapters/server.js +676 -59
  13. package/dist/src/app-opentui/adapters/tasks.js +539 -81
  14. package/dist/src/app-opentui/autocomplete.js +4 -2
  15. package/dist/src/app-opentui/bootstrap.js +1155 -288
  16. package/dist/src/app-opentui/command-palette.js +37 -10
  17. package/dist/src/app-opentui/index.js +566 -89
  18. package/dist/src/app-opentui/intent.js +33 -8
  19. package/dist/src/app-opentui/keymap.js +43 -29
  20. package/dist/src/app-opentui/pi-host-child.js +465 -53
  21. package/dist/src/app-opentui/react/App.js +104 -44
  22. package/dist/src/app-opentui/react/ChromeHost.js +16 -4
  23. package/dist/src/app-opentui/react/launch.js +555 -88
  24. package/dist/src/app-opentui/react/nav.js +1 -1
  25. package/dist/src/app-opentui/registry.js +1062 -237
  26. package/dist/src/app-opentui/remote-link.d.ts +10 -0
  27. package/dist/src/app-opentui/remote-link.js +47 -0
  28. package/dist/src/app-opentui/runtime.js +566 -89
  29. package/dist/src/app-opentui/scenes/doctor.js +1 -1
  30. package/dist/src/app-opentui/scenes/error.js +50 -4
  31. package/dist/src/app-opentui/scenes/family.js +60 -6
  32. package/dist/src/app-opentui/scenes/fleet.js +65 -13
  33. package/dist/src/app-opentui/scenes/help.js +4 -2
  34. package/dist/src/app-opentui/scenes/init.js +12 -12
  35. package/dist/src/app-opentui/scenes/main.js +7 -7
  36. package/dist/src/app-opentui/scenes/server.js +83 -11
  37. package/dist/src/app-opentui/scenes/tasks.js +79 -16
  38. package/dist/src/app-opentui/state.js +25 -5
  39. package/dist/src/app-opentui/surface-catalog.js +4 -2
  40. package/dist/src/app-opentui/types.d.ts +1 -1
  41. package/dist/src/commands/_cli-format.d.ts +10 -1
  42. package/dist/src/commands/_cli-format.js +5 -2
  43. package/dist/src/commands/_connection-state.d.ts +11 -1
  44. package/dist/src/commands/_connection-state.js +50 -5
  45. package/dist/src/commands/_doctor-checks.js +458 -46
  46. package/dist/src/commands/_help-catalog.js +4 -2
  47. package/dist/src/commands/_json-output.js +4 -0
  48. package/dist/src/commands/_operator-view.js +465 -53
  49. package/dist/src/commands/_pi-frontend.js +465 -53
  50. package/dist/src/commands/_preflight.js +509 -72
  51. package/dist/src/commands/_server-client.d.ts +33 -0
  52. package/dist/src/commands/_server-client.js +477 -46
  53. package/dist/src/commands/_server-events.js +446 -41
  54. package/dist/src/commands/_snapshot-upload.js +460 -48
  55. package/dist/src/commands/connect.js +620 -15
  56. package/dist/src/commands/doctor.js +458 -46
  57. package/dist/src/commands/github.js +462 -50
  58. package/dist/src/commands/inbox.js +458 -46
  59. package/dist/src/commands/init.js +497 -74
  60. package/dist/src/commands/inspect.js +458 -46
  61. package/dist/src/commands/run.js +465 -53
  62. package/dist/src/commands/server.js +647 -163
  63. package/dist/src/commands/setup.js +463 -51
  64. package/dist/src/commands/stats.js +462 -48
  65. package/dist/src/commands/task-run-driver.js +464 -52
  66. package/dist/src/commands/task.js +520 -81
  67. package/dist/src/commands.js +670 -147
  68. package/dist/src/index.js +674 -147
  69. package/dist/src/launcher.js +4 -0
  70. package/package.json +8 -8
@@ -356,14 +356,56 @@ function sortTasks(tasks, spec) {
356
356
  return applyDir(tasks, compare, dir);
357
357
  }
358
358
 
359
+ // packages/cli/src/app-opentui/remote-link.ts
360
+ var REPAIRABLE_REMOTE_LINK_STATUSES = new Set([
361
+ "auth_required",
362
+ "project_not_registered",
363
+ "no_server_checkout",
364
+ "invalid_root",
365
+ "needs_prepare",
366
+ "error"
367
+ ]);
368
+ function remoteProjectLinkError(message) {
369
+ return /no server-host project root link|remote project(?:-root)? link|x-rig-project-root|serverProjectRoot/i.test(message ?? "");
370
+ }
371
+ function serverRecordForRemoteLink(state) {
372
+ const server = state.data.server;
373
+ return server && typeof server === "object" && !Array.isArray(server) ? server : null;
374
+ }
375
+ function remoteProjectLinkState(state) {
376
+ const serverRecord = serverRecordForRemoteLink(state);
377
+ if (!serverRecord || serverRecord.kind !== "remote")
378
+ return null;
379
+ const link = serverRecord.remoteProjectLink;
380
+ if (link && typeof link === "object" && !Array.isArray(link))
381
+ return link;
382
+ const direct = state.data.remoteProjectLink;
383
+ if (direct && typeof direct === "object" && !Array.isArray(direct))
384
+ return direct;
385
+ return null;
386
+ }
387
+ function shouldOfferRemoteLinkRepair(state, message) {
388
+ const link = remoteProjectLinkState(state);
389
+ if (link) {
390
+ if (link.ok === true)
391
+ return false;
392
+ const status = link.status ?? "";
393
+ return REPAIRABLE_REMOTE_LINK_STATUSES.has(status);
394
+ }
395
+ const serverRecord = serverRecordForRemoteLink(state);
396
+ if (serverRecord && serverRecord.kind !== "remote")
397
+ return false;
398
+ return remoteProjectLinkError(message ?? state.error?.message);
399
+ }
400
+
359
401
  // packages/cli/src/app-opentui/scenes/tasks.ts
360
- var TASK_RECOVERY_ITEMS = [
402
+ var BASE_TASK_RECOVERY_ITEMS = [
361
403
  { id: "refresh", label: "refresh", intent: { scene: "tasks", argv: ["tasks"], action: { kind: "refresh", label: "Refresh tasks" } }, message: "refresh task source" },
362
- { id: "next", label: "next", intent: { scene: "command", argv: ["task", "run", "--next"], action: { kind: "command-run", label: "Run next task" } }, message: "dispatch next runnable task" },
363
- { id: "repair", label: "repair", intent: { scene: "command", argv: ["init", "--repair"], action: { kind: "command-run", label: "Repair task source" } }, message: "repair project/task source" },
404
+ { id: "next", label: "next", intent: { scene: "tasks", argv: ["task", "run", "--next"], action: { kind: "task-run-next", label: "Dispatch next task" } }, message: "dispatch next runnable task" },
364
405
  { id: "server", label: "server", intent: { scene: "server", argv: ["server", "status"], action: { kind: "refresh", label: "Open server controls" } }, message: "server controls" },
365
406
  { id: "doctor", label: "doctor", intent: { scene: "doctor", argv: ["doctor"], action: { kind: "doctor-run", label: "Run doctor" } }, message: "diagnose task source" }
366
407
  ];
408
+ var TASK_REPAIR_LINK_ITEM = { id: "repair-link", label: "repair link", intent: { scene: "server", argv: ["server", "repair-link"], action: { kind: "remote-link-repair", label: "Repair remote link", payload: { returnScene: "tasks" } } }, message: "backfill or prepare remote project link" };
367
409
  var COL = {
368
410
  glyph: 1,
369
411
  id: 11,
@@ -523,34 +565,44 @@ function taskRow(width, task, active, optimistic) {
523
565
  const item = taskSelectableItem(task);
524
566
  return item ? withSelectable(row, item) : row;
525
567
  }
526
- var RECOVERY_DECKS = [
568
+ var BASE_RECOVERY_DECKS = [
527
569
  { label: "refresh", detail: "reload task source" },
528
570
  { label: "next", detail: "dispatch next runnable task" },
529
- { label: "repair", detail: "repair project/task source link" },
530
571
  { label: "server", detail: "open server controls" },
531
572
  { label: "doctor", detail: "diagnose task source" }
532
573
  ];
533
- function recoveryRows(selected, reason) {
574
+ function taskRecoveryItems(state) {
575
+ if (!shouldOfferRemoteLinkRepair(state))
576
+ return BASE_TASK_RECOVERY_ITEMS;
577
+ return [BASE_TASK_RECOVERY_ITEMS[0], BASE_TASK_RECOVERY_ITEMS[1], TASK_REPAIR_LINK_ITEM, ...BASE_TASK_RECOVERY_ITEMS.slice(2)];
578
+ }
579
+ function taskRecoveryDecks(state) {
580
+ if (!shouldOfferRemoteLinkRepair(state))
581
+ return BASE_RECOVERY_DECKS;
582
+ return [BASE_RECOVERY_DECKS[0], BASE_RECOVERY_DECKS[1], { label: "repair link", detail: "backfill/prepare remote project-root link" }, ...BASE_RECOVERY_DECKS.slice(2)];
583
+ }
584
+ function recoveryRows(state, selected, reason) {
585
+ const items = taskRecoveryItems(state);
534
586
  return [
535
587
  ...reason ? [line(reason, { fg: RIG_UI.yellow }), line("", { fg: RIG_UI.ink3 })] : [],
536
588
  line("ACTIONS", { fg: RIG_UI.ink3, bold: true }),
537
- ...RECOVERY_DECKS.map((deck, index) => selectableDeckRow({ ...deck, index, active: selected === index }, TASK_RECOVERY_ITEMS[index]))
589
+ ...taskRecoveryDecks(state).map((deck, index) => selectableDeckRow({ ...deck, index, active: selected === index }, items[index]))
538
590
  ];
539
591
  }
540
- function emptyRows(query, total, selected) {
592
+ function emptyRows(state, query, total, selected) {
541
593
  if (!query.trim() && total === 0)
542
- return recoveryRows(selected, "No tasks loaded.");
594
+ return recoveryRows(state, selected, "No tasks loaded.");
543
595
  if (query.trim() && total > 0) {
544
596
  return [
545
- ...recoveryRows(selected, `No matching tasks for ${JSON.stringify(query)} (${total} total).`)
597
+ ...recoveryRows(state, selected, `No matching tasks for ${JSON.stringify(query)} (${total} total).`)
546
598
  ];
547
599
  }
548
- return recoveryRows(selected, "No tasks are visible with this filter.");
600
+ return recoveryRows(state, selected, "No tasks are visible with this filter.");
549
601
  }
550
602
  function degradedRows(state, selected) {
551
603
  if (!state.error)
552
604
  return [];
553
- return recoveryRows(selected, state.error.message);
605
+ return recoveryRows(state, selected, state.error.message);
554
606
  }
555
607
  function rawString(raw, key) {
556
608
  const value = raw?.[key];
@@ -574,7 +626,10 @@ function renderTaskDetail(state, taskId, layout) {
574
626
  const activeRunId = task?.activeRun?.runId?.trim();
575
627
  const width = panelWidth(layout);
576
628
  const selected = Math.max(0, state.selection.index);
629
+ const errorText = state.error?.message ?? lastRefreshError(state);
630
+ const offerRepairLink = shouldOfferRemoteLinkRepair(state, errorText);
577
631
  const headerLines = [
632
+ ...errorText ? errorBanner(errorText, offerRepairLink ? "select repair link below to prepare the remote checkout" : "retry dispatch after resolving the issue") : [],
578
633
  line(`${glyph} ${title}`, { fg: color, bold: true }),
579
634
  line(`task ${taskId} \xB7 ${status}`, { fg: RIG_UI.ink3 }),
580
635
  ...task && task.labels.length > 0 ? [line(`labels: ${task.labels.join(", ")}`, { fg: RIG_UI.ink4 })] : [],
@@ -586,8 +641,16 @@ function renderTaskDetail(state, taskId, layout) {
586
641
  const actionRows = [];
587
642
  actionRows.push(selectableDeckRow({ label: "back", detail: "return to the task list", index, active: selected === index }, { id: "task-detail-back", label: "back", data: { taskDetailId: undefined }, message: "back to task list" }));
588
643
  index += 1;
589
- actionRows.push(selectableDeckRow({ label: "dispatch", detail: "submit a Pi run for this task", index, active: selected === index }, { id: `task-detail-run:${taskId}`, label: `dispatch ${taskId}`, intent: { scene: "tasks", argv: ["run", taskId], action: { kind: "task-run-id", payload: { task: taskId }, label: `Dispatching ${taskId}` } }, message: `dispatch task ${taskId}` }));
590
- index += 1;
644
+ if (offerRepairLink) {
645
+ actionRows.push(selectableDeckRow({ label: "repair link", detail: "prepare/backfill the selected remote project-root link", index, active: selected === index }, { id: `task-detail-repair-link:${taskId}`, label: "repair link", intent: { scene: "server", argv: ["server", "repair-link"], action: { kind: "remote-link-repair", label: "Repair remote link", payload: { returnScene: "tasks", taskDetailId: taskId } } }, message: "prepare remote checkout link" }));
646
+ index += 1;
647
+ }
648
+ if (offerRepairLink) {
649
+ actionRows.push(line(" dispatch blocked until the remote project link is repaired", { fg: RIG_UI.ink4 }));
650
+ } else {
651
+ actionRows.push(selectableDeckRow({ label: "dispatch", detail: "submit a Pi run for this task", index, active: selected === index }, { id: `task-detail-run:${taskId}`, label: `dispatch ${taskId}`, intent: { scene: "tasks", argv: ["run", taskId], action: { kind: "task-run-id", payload: { task: taskId }, label: `Dispatching ${taskId}` } }, message: `dispatch task ${taskId}` }));
652
+ index += 1;
653
+ }
591
654
  if (activeRunId) {
592
655
  actionRows.push(selectableDeckRow({ label: "attach", detail: `attach the Pi console to run ${activeRunId.slice(0, 8)}`, index, active: selected === index }, { id: `task-detail-attach:${taskId}`, label: `attach ${activeRunId.slice(0, 8)}`, intent: { scene: "handoff", argv: ["attach", activeRunId], action: { kind: "run-attach", payload: { runId: activeRunId }, label: `Attach Pi ${activeRunId.slice(0, 8)}` } }, message: `attach Pi to ${activeRunId.slice(0, 8)}` }));
593
656
  index += 1;
@@ -646,7 +709,7 @@ function renderTasksScene(state, layout) {
646
709
  const tasks = sortTasks(filterTasksForSearch(allTasks, query), spec);
647
710
  const selected = typeof state.data.selectedTaskId === "string" && tasks.some((task) => task.id === state.data.selectedTaskId) ? state.data.selectedTaskId : tasks[0]?.id;
648
711
  const selectedIndex = Math.max(0, tasks.findIndex((task) => task.id === selected));
649
- const recoveryIndex = Math.max(0, Math.min(4, state.selection.index));
712
+ const recoveryIndex = Math.max(0, Math.min(taskRecoveryItems(state).length - 1, state.selection.index));
650
713
  const dispatching = dispatchingFor(state);
651
714
  const dispatchLine = dispatching ? ` \xB7 ${state.data.dispatchingRun?.runId ?? "new"} ${dispatching.status}` : "";
652
715
  const updatedAgo = updatedAgoLabel(state);
@@ -666,7 +729,7 @@ function renderTasksScene(state, layout) {
666
729
  ...tasks.map((task, index) => taskRow(contentWidth, task, index === selectedIndex, dispatching && dispatching.taskId === task.id ? dispatching.status : undefined))
667
730
  ] : state.error ? degradedRows(state, recoveryIndex) : loading ? loadingRows("tasks", state.tick) : [
668
731
  ...query.trim() ? [line(searchSummary("tasks", query, tasks.length, allTasks.length), { fg: RIG_UI.ink4 }), line("", { fg: RIG_UI.ink3 })] : [],
669
- ...emptyRows(query, allTasks.length, recoveryIndex)
732
+ ...emptyRows(state, query, allTasks.length, recoveryIndex)
670
733
  ]
671
734
  ];
672
735
  return makeSceneFrame({
@@ -152,16 +152,31 @@ function taskViewIntent(argv, command = "", rest = [], raw) {
152
152
  const { payload, label } = taskViewPayload(command, rest);
153
153
  return intent("tasks", argv, "refresh", payload, label, raw);
154
154
  }
155
- function routeServer(argv, command, raw, checkingLabel) {
156
- if (!command || command === "status") {
155
+ function routeServer(argv, command, rest, raw, checkingLabel) {
156
+ if (!command || command === "status" || command === "list") {
157
157
  return intent("server", argv, "refresh", undefined, command ? checkingLabel : "Loading server", raw);
158
158
  }
159
+ if (command === "use") {
160
+ const alias = firstNonOption(rest);
161
+ if (alias === "local")
162
+ return intent("server", argv, "server-use-local", undefined, "Use local server", raw);
163
+ if (alias)
164
+ return intent("server", argv, "server-use-remote", { alias }, `Use ${alias}`, raw);
165
+ return intent("server", argv, "refresh", undefined, "Select a remote alias in Server", raw);
166
+ }
167
+ if (command === "add")
168
+ return intent("server", argv, "server-add-remote", { argv: [...argv] }, "Add remote server", raw);
169
+ if (command === "repair-link")
170
+ return intent("server", argv, "remote-link-repair", undefined, "Repair remote link", raw);
159
171
  return commandRunIntent(argv, raw);
160
172
  }
161
173
  function routeGithub(argv, command, rest, raw) {
162
174
  if (command === "auth" && (rest[0]?.toLowerCase() ?? "status") === "status" && rest.length <= 1) {
163
175
  return intent("server", argv, "refresh", undefined, "Checking GitHub auth", raw);
164
176
  }
177
+ if (command === "auth" && rest[0]?.toLowerCase() === "import-gh" && rest.length <= 1) {
178
+ return intent("server", argv, "github-auth-import", undefined, "Import GitHub auth", raw);
179
+ }
165
180
  return commandRunIntent(argv, raw);
166
181
  }
167
182
  function routeDoctor(argv, command, raw) {
@@ -210,11 +225,16 @@ function intentFromArgv(argv) {
210
225
  return commandRunIntent(argv);
211
226
  }
212
227
  if (normalizedGroup === "server")
213
- return routeServer(argv, normalizedCommand, undefined, "Checking server");
228
+ return routeServer(argv, normalizedCommand, rest, undefined, "Checking server");
214
229
  if (normalizedGroup === "github")
215
230
  return routeGithub(argv, normalizedCommand, rest);
216
- if (normalizedGroup === "init")
217
- return commandRunIntent(argv);
231
+ if (normalizedGroup === "init") {
232
+ if (!normalizedCommand)
233
+ return intent("init", argv, "refresh", undefined, "Open init");
234
+ if (rest.includes("--yes") || normalizedCommand === "--yes")
235
+ return intent("init", argv, "init-start", undefined, "Run init");
236
+ return intent("init", argv, "refresh", undefined, "Open init");
237
+ }
218
238
  if (normalizedGroup === "doctor")
219
239
  return routeDoctor(argv, normalizedCommand);
220
240
  if (normalizedGroup === "inbox")
@@ -67,12 +67,13 @@ var PRIMARY_GROUPS = [
67
67
  {
68
68
  name: "server",
69
69
  summary: "Choose, inspect, and start the Rig server that owns tasks and runs.",
70
- usage: ["rig server <status|list|add|use|start> [options]"],
70
+ usage: ["rig server <status|list|add|use|repair-link|start> [options]"],
71
71
  commands: [
72
- { command: "status", description: "Show the selected server for this repo.", primary: true },
72
+ { command: "status", description: "Show the selected server and remote project-root link for this repo.", primary: true },
73
73
  { command: "use local", description: "Switch this repo to the local Rig server.", primary: true },
74
74
  { command: "add <alias> <url>", description: "Save a remote Rig server URL.", primary: true },
75
75
  { command: "use <alias>", description: "Select a saved remote server alias.", primary: true },
76
+ { command: "repair-link [--prepare|--backfill-only] [--repo owner/repo]", description: "Backfill or prepare the selected remote's server-host checkout link.", primary: true },
76
77
  { command: "list", description: "List saved local/remote server aliases.", primary: true },
77
78
  { command: "start [--host <host>] [--port <n>]", description: "Start a local rig-server process." }
78
79
  ],
@@ -80,6 +81,7 @@ var PRIMARY_GROUPS = [
80
81
  "rig server status",
81
82
  "rig server add prod https://where.rig-does.work",
82
83
  "rig server use prod",
84
+ "rig server repair-link --repo owner/repo",
83
85
  "rig server use local",
84
86
  "rig server start --port 3773"
85
87
  ],
@@ -2,7 +2,7 @@ import type { StyledText } from "@opentui/core";
2
2
  import type { RunnerContext } from "../runner";
3
3
  import type { StageLayout } from "./layout";
4
4
  export type AppSceneId = "main" | "fleet" | "tasks" | "inbox" | "server" | "init" | "doctor" | "command" | "run-detail" | "handoff" | "pi" | "plugin" | "repo" | "workspace" | "inspect" | "family" | "help" | "error";
5
- export type AppActionKind = "none" | "refresh" | "navigate" | "task-run-next" | "task-run-id" | "run-stop" | "run-attach" | "run-steer" | "inbox-approve" | "inbox-reject" | "inbox-answer" | "init-start" | "command-run" | "doctor-run";
5
+ export type AppActionKind = "none" | "refresh" | "navigate" | "task-run-next" | "task-run-id" | "run-stop" | "run-attach" | "run-steer" | "inbox-approve" | "inbox-reject" | "inbox-answer" | "init-start" | "server-use-local" | "server-use-remote" | "server-add-remote" | "github-auth-import" | "remote-link-repair" | "command-run" | "doctor-run";
6
6
  export type AppIntent = {
7
7
  readonly scene: AppSceneId;
8
8
  readonly argv: readonly string[];
@@ -46,4 +46,13 @@ export declare function formatConnectionStatus(selected: string, connections: Re
46
46
  kind: string;
47
47
  baseUrl?: string;
48
48
  mode?: string;
49
- }>): string;
49
+ }>, repo?: {
50
+ project?: string;
51
+ serverProjectRoot?: string;
52
+ } | null, remoteProjectLink?: {
53
+ ok?: boolean;
54
+ status?: string;
55
+ serverProjectRoot?: string;
56
+ message?: string;
57
+ hint?: string;
58
+ } | null): string;
@@ -415,16 +415,19 @@ function formatConnectionList(connections) {
415
415
  return [formatSection("Rig servers", `${rows.length} available`), `${pc.bold(pad("ALIAS", aliasWidth))} ${pc.bold("KIND")} ${pc.bold("TARGET")}`, ...lines, "", ...formatNextSteps(["Select one: `rig server use <alias|local>`"])].join(`
416
416
  `);
417
417
  }
418
- function formatConnectionStatus(selected, connections) {
418
+ function formatConnectionStatus(selected, connections, repo, remoteProjectLink) {
419
419
  const connection = selected === "local" ? { kind: "local", mode: "auto" } : connections[selected];
420
420
  const target = !connection ? "not configured" : connection.kind === "remote" ? connection.baseUrl : "local";
421
+ const rootLabel = remoteProjectLink ? remoteProjectLink.ok ? `ready \xB7 ${remoteProjectLink.serverProjectRoot ?? repo?.serverProjectRoot ?? "linked"}` : `${remoteProjectLink.status ?? "needs repair"} \xB7 ${remoteProjectLink.hint ?? remoteProjectLink.message ?? "run rig server repair-link"}` : repo?.serverProjectRoot ? repo.serverProjectRoot : "not required";
421
422
  return [
422
423
  formatSection("Rig server", "selected for this repo"),
423
424
  `${themeFaint("\u2502")} ${themeDim("selected ")} ${pc.bold(selected)}`,
424
425
  `${themeFaint("\u2502")} ${themeDim("kind ")} ${formatStatusPill(connection?.kind ?? "unknown")}`,
425
426
  `${themeFaint("\u2502")} ${themeDim("target ")} ${target ?? "not configured"}`,
427
+ ...repo?.project ? [`${themeFaint("\u2502")} ${themeDim("project ")} ${repo.project}`] : [],
428
+ ...connection?.kind === "remote" ? [`${themeFaint("\u2502")} ${themeDim("root ")} ${rootLabel}`] : [],
426
429
  "",
427
- ...formatNextSteps(["Change: `rig server use <alias|local>`", "List saved servers: `rig server list`"])
430
+ ...formatNextSteps(remoteProjectLink && !remoteProjectLink.ok ? ["Repair remote link: `rig server repair-link`", "Authenticate first if needed: `rig github auth import-gh`"] : ["Change: `rig server use <alias|local>`", "List saved servers: `rig server list`"])
428
431
  ].join(`
429
432
  `);
430
433
  }
@@ -17,6 +17,10 @@ export type RepoConnectionState = {
17
17
  * Sent as the x-rig-project-root header on every remote request.
18
18
  */
19
19
  serverProjectRoot?: string;
20
+ /** The selected alias that produced serverProjectRoot. Prevents cross-alias reuse. */
21
+ serverProjectRootAlias?: string;
22
+ /** The remote base URL that produced serverProjectRoot. Prevents same-alias retarget leaks. */
23
+ serverProjectRootBaseUrl?: string;
20
24
  };
21
25
  export declare function resolveGlobalConnectionsPath(env?: NodeJS.ProcessEnv): string;
22
26
  export declare function resolveRepoConnectionPath(projectRoot: string): string;
@@ -39,6 +43,12 @@ export declare function resolveSelectedConnection(projectRoot: string, options?:
39
43
  serverProjectRoot?: string;
40
44
  } | null;
41
45
  /** Persist the server-host project root this repo scopes to (multi-root serving). */
42
- export declare function writeRepoServerProjectRoot(projectRoot: string, serverProjectRoot: string): void;
46
+ export declare function writeRepoServerProjectRoot(projectRoot: string, serverProjectRoot: string, metadata?: {
47
+ alias?: string;
48
+ baseUrl?: string;
49
+ project?: string;
50
+ }): void;
51
+ /** Remove a stale server-host root while preserving selected server/repo state. */
52
+ export declare function clearRepoServerProjectRoot(projectRoot: string): void;
43
53
  /** Whether this repo's selected connection targets a remote Rig server. */
44
54
  export declare function isRemoteConnectionSelected(projectRoot: string): boolean;
@@ -100,12 +100,28 @@ function readRepoConnection(projectRoot) {
100
100
  selected,
101
101
  project: typeof record.project === "string" ? record.project : undefined,
102
102
  linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
103
- serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
103
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined,
104
+ serverProjectRootAlias: typeof record.serverProjectRootAlias === "string" && record.serverProjectRootAlias.trim() ? record.serverProjectRootAlias.trim() : undefined,
105
+ serverProjectRootBaseUrl: typeof record.serverProjectRootBaseUrl === "string" && record.serverProjectRootBaseUrl.trim() ? record.serverProjectRootBaseUrl.trim().replace(/\/+$/, "") : undefined
104
106
  };
105
107
  }
106
108
  function writeRepoConnection(projectRoot, state) {
107
109
  writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
108
110
  }
111
+ function rootAllowedForSelection(repo, connection) {
112
+ const root = repo.serverProjectRoot?.trim();
113
+ if (!root)
114
+ return;
115
+ if (connection.kind === "remote") {
116
+ if (repo.serverProjectRootAlias !== repo.selected)
117
+ return;
118
+ if (repo.serverProjectRootBaseUrl !== connection.baseUrl)
119
+ return;
120
+ } else if (repo.serverProjectRootAlias && repo.serverProjectRootAlias !== repo.selected) {
121
+ return;
122
+ }
123
+ return root;
124
+ }
109
125
  function resolveSelectedConnection(projectRoot, options = {}) {
110
126
  const repo = readRepoConnection(projectRoot);
111
127
  if (!repo)
@@ -117,13 +133,41 @@ function resolveSelectedConnection(projectRoot, options = {}) {
117
133
  if (!connection) {
118
134
  throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
119
135
  }
120
- return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
136
+ return { alias: repo.selected, connection, serverProjectRoot: rootAllowedForSelection(repo, connection) };
137
+ }
138
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot, metadata = {}) {
139
+ const repo = readRepoConnection(projectRoot);
140
+ if (!repo)
141
+ return;
142
+ let inferred = metadata;
143
+ if (!inferred.alias || !inferred.baseUrl) {
144
+ try {
145
+ const selected = resolveSelectedConnection(projectRoot);
146
+ if (selected?.connection.kind === "remote") {
147
+ inferred = {
148
+ alias: inferred.alias ?? selected.alias,
149
+ baseUrl: inferred.baseUrl ?? selected.connection.baseUrl
150
+ };
151
+ }
152
+ } catch {}
153
+ }
154
+ writeRepoConnection(projectRoot, {
155
+ ...repo,
156
+ ...metadata.project ? { project: metadata.project } : {},
157
+ serverProjectRoot,
158
+ ...inferred.alias ? { serverProjectRootAlias: inferred.alias } : {},
159
+ ...inferred.baseUrl ? { serverProjectRootBaseUrl: inferred.baseUrl.replace(/\/+$/, "") } : {}
160
+ });
121
161
  }
122
- function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
162
+ function clearRepoServerProjectRoot(projectRoot) {
123
163
  const repo = readRepoConnection(projectRoot);
124
164
  if (!repo)
125
165
  return;
126
- writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
166
+ writeRepoConnection(projectRoot, {
167
+ selected: repo.selected,
168
+ ...repo.project ? { project: repo.project } : {},
169
+ ...repo.linkedAt ? { linkedAt: repo.linkedAt } : {}
170
+ });
127
171
  }
128
172
  function isRemoteConnectionSelected(projectRoot) {
129
173
  return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
@@ -138,5 +182,6 @@ export {
138
182
  resolveGlobalConnectionsPath,
139
183
  readRepoConnection,
140
184
  readGlobalConnections,
141
- isRemoteConnectionSelected
185
+ isRemoteConnectionSelected,
186
+ clearRepoServerProjectRoot
142
187
  };