@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
@@ -46,7 +46,8 @@ __export(exports__connection_state, {
46
46
  resolveGlobalConnectionsPath: () => resolveGlobalConnectionsPath,
47
47
  readRepoConnection: () => readRepoConnection,
48
48
  readGlobalConnections: () => readGlobalConnections,
49
- isRemoteConnectionSelected: () => isRemoteConnectionSelected
49
+ isRemoteConnectionSelected: () => isRemoteConnectionSelected,
50
+ clearRepoServerProjectRoot: () => clearRepoServerProjectRoot
50
51
  });
51
52
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
52
53
  import { homedir } from "os";
@@ -130,12 +131,28 @@ function readRepoConnection(projectRoot) {
130
131
  selected,
131
132
  project: typeof record.project === "string" ? record.project : undefined,
132
133
  linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
133
- serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
134
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined,
135
+ serverProjectRootAlias: typeof record.serverProjectRootAlias === "string" && record.serverProjectRootAlias.trim() ? record.serverProjectRootAlias.trim() : undefined,
136
+ serverProjectRootBaseUrl: typeof record.serverProjectRootBaseUrl === "string" && record.serverProjectRootBaseUrl.trim() ? record.serverProjectRootBaseUrl.trim().replace(/\/+$/, "") : undefined
134
137
  };
135
138
  }
136
139
  function writeRepoConnection(projectRoot, state) {
137
140
  writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
138
141
  }
142
+ function rootAllowedForSelection(repo, connection) {
143
+ const root = repo.serverProjectRoot?.trim();
144
+ if (!root)
145
+ return;
146
+ if (connection.kind === "remote") {
147
+ if (repo.serverProjectRootAlias !== repo.selected)
148
+ return;
149
+ if (repo.serverProjectRootBaseUrl !== connection.baseUrl)
150
+ return;
151
+ } else if (repo.serverProjectRootAlias && repo.serverProjectRootAlias !== repo.selected) {
152
+ return;
153
+ }
154
+ return root;
155
+ }
139
156
  function resolveSelectedConnection(projectRoot, options = {}) {
140
157
  const repo = readRepoConnection(projectRoot);
141
158
  if (!repo)
@@ -147,13 +164,41 @@ function resolveSelectedConnection(projectRoot, options = {}) {
147
164
  if (!connection) {
148
165
  throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
149
166
  }
150
- return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
167
+ return { alias: repo.selected, connection, serverProjectRoot: rootAllowedForSelection(repo, connection) };
151
168
  }
152
- function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
169
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot, metadata = {}) {
153
170
  const repo = readRepoConnection(projectRoot);
154
171
  if (!repo)
155
172
  return;
156
- writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
173
+ let inferred = metadata;
174
+ if (!inferred.alias || !inferred.baseUrl) {
175
+ try {
176
+ const selected = resolveSelectedConnection(projectRoot);
177
+ if (selected?.connection.kind === "remote") {
178
+ inferred = {
179
+ alias: inferred.alias ?? selected.alias,
180
+ baseUrl: inferred.baseUrl ?? selected.connection.baseUrl
181
+ };
182
+ }
183
+ } catch {}
184
+ }
185
+ writeRepoConnection(projectRoot, {
186
+ ...repo,
187
+ ...metadata.project ? { project: metadata.project } : {},
188
+ serverProjectRoot,
189
+ ...inferred.alias ? { serverProjectRootAlias: inferred.alias } : {},
190
+ ...inferred.baseUrl ? { serverProjectRootBaseUrl: inferred.baseUrl.replace(/\/+$/, "") } : {}
191
+ });
192
+ }
193
+ function clearRepoServerProjectRoot(projectRoot) {
194
+ const repo = readRepoConnection(projectRoot);
195
+ if (!repo)
196
+ return;
197
+ writeRepoConnection(projectRoot, {
198
+ selected: repo.selected,
199
+ ...repo.project ? { project: repo.project } : {},
200
+ ...repo.linkedAt ? { linkedAt: repo.linkedAt } : {}
201
+ });
157
202
  }
158
203
  function isRemoteConnectionSelected(projectRoot) {
159
204
  return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
@@ -180,12 +225,16 @@ __export(exports__server_client, {
180
225
  respondRunPiExtensionUiViaServer: () => respondRunPiExtensionUiViaServer,
181
226
  resolveServerConnectionLabel: () => resolveServerConnectionLabel,
182
227
  requestServerJson: () => requestServerJson,
228
+ repairRemoteProjectRootLink: () => repairRemoteProjectRootLink,
183
229
  registerProjectViaServer: () => registerProjectViaServer,
184
230
  prepareRemoteCheckoutViaServer: () => prepareRemoteCheckoutViaServer,
185
231
  postGitHubTokenViaServer: () => postGitHubTokenViaServer,
232
+ normalizeRepoSlug: () => normalizeRepoSlug,
186
233
  listWorkspaceTasksViaServer: () => listWorkspaceTasksViaServer,
187
234
  listRunsViaServer: () => listRunsViaServer,
188
235
  listGitHubProjectsViaServer: () => listGitHubProjectsViaServer,
236
+ isRemoteProjectRootLinkError: () => isRemoteProjectRootLinkError,
237
+ inspectRemoteProjectLink: () => inspectRemoteProjectLink,
189
238
  getWorkspaceTaskViaServer: () => getWorkspaceTaskViaServer,
190
239
  getRunTimelineViaServer: () => getRunTimelineViaServer,
191
240
  getRunPiStatusViaServer: () => getRunPiStatusViaServer,
@@ -197,13 +246,15 @@ __export(exports__server_client, {
197
246
  getRunDetailsViaServer: () => getRunDetailsViaServer,
198
247
  getGitHubProjectStatusFieldViaServer: () => getGitHubProjectStatusFieldViaServer,
199
248
  getGitHubAuthStatusViaServer: () => getGitHubAuthStatusViaServer,
249
+ formatRemoteProjectLinkHint: () => formatRemoteProjectLinkHint,
200
250
  ensureTaskLabelsViaServer: () => ensureTaskLabelsViaServer,
201
251
  ensureServerForCli: () => ensureServerForCli,
252
+ ensureRemoteProjectRootLink: () => ensureRemoteProjectRootLink,
202
253
  buildRunPiEventsWebSocketUrl: () => buildRunPiEventsWebSocketUrl,
203
254
  abortRunPiViaServer: () => abortRunPiViaServer
204
255
  });
205
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
206
- import { resolve as resolve2 } from "path";
256
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
257
+ import { dirname as dirname2, isAbsolute, resolve as resolve2 } from "path";
207
258
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
208
259
  function setServerPhaseListener(listener) {
209
260
  const previous = serverPhaseListener;
@@ -249,11 +300,10 @@ function readStoredGitHubAuthToken(projectRoot) {
249
300
  const parsed = readRemoteAuthState(projectRoot);
250
301
  return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
251
302
  }
252
- function inferRemoteServerProjectRootFromAuthState(projectRoot) {
253
- const repo = readRepoConnection(projectRoot);
254
- const slug = repo?.project?.trim();
255
- if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
303
+ function inferRemoteServerProjectRootCandidateFromAuthState(projectRoot, repoSlug) {
304
+ if (!/^[^/]+\/[^/]+$/.test(repoSlug))
256
305
  return null;
306
+ const repo = readRepoConnection(projectRoot);
257
307
  const auth = readRemoteAuthState(projectRoot);
258
308
  const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
259
309
  const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
@@ -262,25 +312,426 @@ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
262
312
  const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
263
313
  if (!checkoutBaseDir)
264
314
  return null;
265
- const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
266
- writeRepoServerProjectRoot(projectRoot, inferred);
267
- return inferred;
315
+ return `${checkoutBaseDir.replace(/\/+$/, "")}/${repoSlug}`;
268
316
  }
269
317
  function readLocalConnectionFallbackToken(projectRoot) {
270
318
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
271
319
  }
320
+ function normalizeRepoSlug(value) {
321
+ const slug = value?.trim();
322
+ return slug && /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
323
+ }
324
+ function readProjectLinkSlug(projectRoot) {
325
+ const path = resolve2(projectRoot, ".rig", "state", "project-link.json");
326
+ if (!existsSync2(path))
327
+ return null;
328
+ try {
329
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
330
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
331
+ return null;
332
+ return normalizeRepoSlug(typeof parsed.repoSlug === "string" ? String(parsed.repoSlug) : null);
333
+ } catch {
334
+ return null;
335
+ }
336
+ }
337
+ function writeProjectLinkConnection(projectRoot, repoSlug, alias) {
338
+ const path = resolve2(projectRoot, ".rig", "state", "project-link.json");
339
+ mkdirSync2(dirname2(path), { recursive: true });
340
+ writeFileSync2(path, `${JSON.stringify({ repoSlug, connection: alias, linkedAt: new Date().toISOString() }, null, 2)}
341
+ `, "utf8");
342
+ }
343
+ function remoteLinkRepairCommand(repoSlug) {
344
+ return repoSlug ? `rig server repair-link --repo ${repoSlug}` : "rig server repair-link --repo owner/repo";
345
+ }
346
+ function isRemoteProjectRootLinkError(error) {
347
+ const text = error instanceof Error ? error.message : String(error ?? "");
348
+ return /no server-host project root link|remote project(?:-root)? link|x-rig-project-root|serverProjectRoot/i.test(text);
349
+ }
350
+ function formatRemoteProjectLinkHint(resolution) {
351
+ const repair = remoteLinkRepairCommand(resolution.repoSlug);
352
+ if (resolution.status === "auth_required") {
353
+ 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}` : ""}\`.`;
354
+ }
355
+ if (resolution.status === "missing_project") {
356
+ return `Record the repo slug, then run \`${repair}\` (or \`rig init --repo owner/repo --server remote --github-auth device\`).`;
357
+ }
358
+ if (resolution.status === "not_remote")
359
+ return "Select a remote server with `rig server use <alias>` before repairing a remote project link.";
360
+ return `Run \`${repair}\` to backfill or prepare a server-host checkout for the selected remote${resolution.alias ? ` (${resolution.alias})` : ""}.`;
361
+ }
362
+ function remoteProjectLinkFailure(input) {
363
+ const partial = {
364
+ status: input.status,
365
+ alias: input.alias,
366
+ baseUrl: input.baseUrl,
367
+ repoSlug: input.repoSlug
368
+ };
369
+ return {
370
+ ok: false,
371
+ ...input,
372
+ hint: formatRemoteProjectLinkHint(partial),
373
+ next: remoteLinkRepairCommand(input.repoSlug)
374
+ };
375
+ }
376
+ function remoteProjectLinkSuccess(input) {
377
+ return {
378
+ ok: true,
379
+ status: input.status,
380
+ alias: input.alias,
381
+ baseUrl: input.baseUrl,
382
+ repoSlug: input.repoSlug,
383
+ serverProjectRoot: input.serverProjectRoot,
384
+ source: input.source,
385
+ prepared: input.prepared ?? input.status === "prepared",
386
+ validated: input.validated ?? false,
387
+ message: input.message ?? `Remote project link ready for ${input.repoSlug}.`,
388
+ hint: "Remote-scoped task/run/status requests will send x-rig-project-root for this server-host checkout.",
389
+ next: "rig task list"
390
+ };
391
+ }
392
+ function localCheckoutPathCandidate(projectRoot, candidate) {
393
+ try {
394
+ const local = resolve2(projectRoot);
395
+ const resolved = resolve2(candidate);
396
+ return resolved === local || resolved.startsWith(`${local}/`);
397
+ } catch {
398
+ return false;
399
+ }
400
+ }
401
+ async function requestRemoteProjectLinkJson(baseUrl, pathname, authToken, init = {}) {
402
+ const requestUrl = new URL(`${baseUrl}${pathname}`);
403
+ if (authToken && queryAuthFallbackEnabled())
404
+ requestUrl.searchParams.set("rt", authToken);
405
+ const response = await fetch(requestUrl, {
406
+ ...init,
407
+ headers: mergeHeaders(init.headers, authToken)
408
+ });
409
+ const text = await response.text();
410
+ const payload = text.trim().length > 0 ? (() => {
411
+ try {
412
+ return JSON.parse(text);
413
+ } catch {
414
+ return null;
415
+ }
416
+ })() : null;
417
+ return { ok: response.ok, status: response.status, payload, text };
418
+ }
419
+ function payloadError(payload, fallback) {
420
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
421
+ const record = payload;
422
+ const value = record.error ?? record.message ?? record.reason;
423
+ if (typeof value === "string" && value.trim())
424
+ return value.trim();
425
+ }
426
+ return fallback;
427
+ }
428
+ function checkoutPathsFromProjectPayload(payload) {
429
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
430
+ return [];
431
+ const project = payload.project;
432
+ if (!project || typeof project !== "object" || Array.isArray(project))
433
+ return [];
434
+ const checkouts = project.checkouts;
435
+ if (!Array.isArray(checkouts))
436
+ return [];
437
+ return [...checkouts].reverse().flatMap((entry) => {
438
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
439
+ return [];
440
+ const path = entry.path;
441
+ return typeof path === "string" && path.trim() ? [path.trim()] : [];
442
+ });
443
+ }
444
+ function checkoutPathFromPreparePayload(payload) {
445
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
446
+ return null;
447
+ const checkout = payload.checkout;
448
+ if (!checkout || typeof checkout !== "object" || Array.isArray(checkout))
449
+ return null;
450
+ const path = checkout.path;
451
+ return typeof path === "string" && path.trim() ? path.trim() : null;
452
+ }
453
+ async function validateAndPersistRemoteRoot(input) {
454
+ const candidate = input.candidate.trim();
455
+ if (!candidate || !isAbsolute(candidate)) {
456
+ return remoteProjectLinkFailure({
457
+ status: "invalid_root",
458
+ alias: input.alias,
459
+ baseUrl: input.baseUrl,
460
+ repoSlug: input.repoSlug,
461
+ message: `Remote project root candidate is not an absolute server-host path: ${candidate || "(empty)"}.`
462
+ });
463
+ }
464
+ if (localCheckoutPathCandidate(input.projectRoot, candidate)) {
465
+ return remoteProjectLinkFailure({
466
+ status: "invalid_root",
467
+ alias: input.alias,
468
+ baseUrl: input.baseUrl,
469
+ repoSlug: input.repoSlug,
470
+ message: `Refusing to use local checkout path ${candidate} as a remote server project root.`
471
+ });
472
+ }
473
+ let response;
474
+ try {
475
+ response = await requestRemoteProjectLinkJson(input.baseUrl, "/api/server/project-root", input.authToken, {
476
+ method: "POST",
477
+ headers: { "content-type": "application/json" },
478
+ body: JSON.stringify({ projectRoot: candidate })
479
+ });
480
+ } catch (error) {
481
+ return remoteProjectLinkFailure({
482
+ status: "error",
483
+ alias: input.alias,
484
+ baseUrl: input.baseUrl,
485
+ repoSlug: input.repoSlug,
486
+ message: `Could not validate remote project root ${candidate}: ${error instanceof Error ? error.message : String(error)}`
487
+ });
488
+ }
489
+ if (response.status === 401 || response.status === 403) {
490
+ return remoteProjectLinkFailure({
491
+ status: "auth_required",
492
+ alias: input.alias,
493
+ baseUrl: input.baseUrl,
494
+ repoSlug: input.repoSlug,
495
+ statusCode: response.status,
496
+ message: `Remote server ${input.alias} requires authentication before validating project root ${candidate}.`
497
+ });
498
+ }
499
+ if (!response.ok) {
500
+ return remoteProjectLinkFailure({
501
+ status: "invalid_root",
502
+ alias: input.alias,
503
+ baseUrl: input.baseUrl,
504
+ repoSlug: input.repoSlug,
505
+ statusCode: response.status,
506
+ message: `Remote server rejected project root ${candidate} (${response.status}): ${payloadError(response.payload, response.text || "project-root validation failed")}`
507
+ });
508
+ }
509
+ const record = response.payload && typeof response.payload === "object" && !Array.isArray(response.payload) ? response.payload : {};
510
+ if (record.ok !== true) {
511
+ return remoteProjectLinkFailure({
512
+ status: "invalid_root",
513
+ alias: input.alias,
514
+ baseUrl: input.baseUrl,
515
+ repoSlug: input.repoSlug,
516
+ message: `Remote server did not accept project root ${candidate}: ${payloadError(response.payload, "project-root validation failed")}`
517
+ });
518
+ }
519
+ const accepted = typeof record.projectRoot === "string" && record.projectRoot.trim() ? record.projectRoot.trim() : typeof record.requestedProjectRoot === "string" && record.requestedProjectRoot.trim() ? record.requestedProjectRoot.trim() : candidate;
520
+ writeRepoServerProjectRoot(input.projectRoot, accepted, { alias: input.alias, baseUrl: input.baseUrl, project: input.repoSlug });
521
+ writeProjectLinkConnection(input.projectRoot, input.repoSlug, input.alias);
522
+ const status = input.status ?? (input.prepared ? "prepared" : "backfilled");
523
+ return remoteProjectLinkSuccess({
524
+ status,
525
+ alias: input.alias,
526
+ baseUrl: input.baseUrl,
527
+ repoSlug: input.repoSlug,
528
+ serverProjectRoot: accepted,
529
+ source: input.source,
530
+ prepared: input.prepared,
531
+ validated: true,
532
+ 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}.`
533
+ });
534
+ }
535
+ async function ensureRemoteProjectRootLink(projectRoot, options = {}) {
536
+ let selected;
537
+ try {
538
+ selected = resolveSelectedConnection(projectRoot);
539
+ } catch (error) {
540
+ return remoteProjectLinkFailure({ status: "error", message: error instanceof Error ? error.message : String(error) });
541
+ }
542
+ if (!selected || selected.connection.kind !== "remote") {
543
+ return remoteProjectLinkFailure({ status: "not_remote", message: "No remote Rig server is selected for this repo." });
544
+ }
545
+ const repo = readRepoConnection(projectRoot);
546
+ const explicitRepoRequested = options.repoSlug !== undefined;
547
+ const explicitRepoSlug = explicitRepoRequested ? normalizeRepoSlug(options.repoSlug) : null;
548
+ const repoProjectSlug = normalizeRepoSlug(repo?.project);
549
+ const repoSlug = explicitRepoSlug ?? repoProjectSlug ?? readProjectLinkSlug(projectRoot);
550
+ const alias = selected.alias;
551
+ const baseUrl = selected.connection.baseUrl;
552
+ const authToken = options.authToken === undefined ? readGitHubBearerTokenForRemote(projectRoot) : options.authToken;
553
+ const mode = options.mode ?? "backfill-only";
554
+ if (explicitRepoRequested && !explicitRepoSlug) {
555
+ return remoteProjectLinkFailure({
556
+ status: "missing_project",
557
+ alias,
558
+ baseUrl,
559
+ message: `Invalid --repo value ${JSON.stringify(options.repoSlug)}. Expected owner/repo.`
560
+ });
561
+ }
562
+ if (!repoSlug) {
563
+ return remoteProjectLinkFailure({
564
+ status: "missing_project",
565
+ alias,
566
+ baseUrl,
567
+ message: `Remote server ${alias} is selected, but this checkout has no GitHub repo slug recorded.`
568
+ });
569
+ }
570
+ const skippedCandidates = [];
571
+ const storedRoot = selected.serverProjectRoot?.trim() || null;
572
+ const storedRootMatchesResolvedRepo = repoProjectSlug === repoSlug;
573
+ if (storedRoot && storedRootMatchesResolvedRepo) {
574
+ if (localCheckoutPathCandidate(projectRoot, storedRoot)) {
575
+ clearRepoServerProjectRoot(projectRoot);
576
+ skippedCandidates.push(storedRoot);
577
+ } else {
578
+ const storedResult = await validateAndPersistRemoteRoot({
579
+ projectRoot,
580
+ alias,
581
+ baseUrl,
582
+ authToken,
583
+ repoSlug,
584
+ candidate: storedRoot,
585
+ source: "stored",
586
+ status: "ready"
587
+ });
588
+ if (storedResult.ok || storedResult.status === "auth_required" || storedResult.status === "error")
589
+ return storedResult;
590
+ clearRepoServerProjectRoot(projectRoot);
591
+ skippedCandidates.push(storedRoot);
592
+ }
593
+ } else if (storedRoot) {
594
+ skippedCandidates.push(storedRoot);
595
+ }
596
+ const authCandidate = inferRemoteServerProjectRootCandidateFromAuthState(projectRoot, repoSlug);
597
+ if (authCandidate) {
598
+ const authResult = await validateAndPersistRemoteRoot({ projectRoot, alias, baseUrl, authToken, repoSlug, candidate: authCandidate, source: "auth-state" });
599
+ if (authResult.ok || authResult.status === "auth_required")
600
+ return authResult;
601
+ }
602
+ let registryResponse;
603
+ try {
604
+ registryResponse = await requestRemoteProjectLinkJson(baseUrl, `/api/projects/${encodeURIComponent(repoSlug)}`, authToken);
605
+ } catch (error) {
606
+ return remoteProjectLinkFailure({
607
+ status: "error",
608
+ alias,
609
+ baseUrl,
610
+ repoSlug,
611
+ message: `Could not read remote project registry for ${repoSlug} on ${alias}: ${error instanceof Error ? error.message : String(error)}`
612
+ });
613
+ }
614
+ if (registryResponse.status === 401 || registryResponse.status === 403) {
615
+ return remoteProjectLinkFailure({
616
+ status: "auth_required",
617
+ alias,
618
+ baseUrl,
619
+ repoSlug,
620
+ statusCode: registryResponse.status,
621
+ message: `Remote server ${alias} requires authentication before reading the ${repoSlug} project registry.`
622
+ });
623
+ }
624
+ if (registryResponse.ok) {
625
+ const candidates = checkoutPathsFromProjectPayload(registryResponse.payload);
626
+ for (const candidate of candidates) {
627
+ const result = await validateAndPersistRemoteRoot({ projectRoot, alias, baseUrl, authToken, repoSlug, candidate, source: "registry" });
628
+ if (result.ok || result.status === "auth_required")
629
+ return result;
630
+ skippedCandidates.push(candidate);
631
+ }
632
+ if (mode === "backfill-only") {
633
+ return remoteProjectLinkFailure({
634
+ status: candidates.length > 0 ? "invalid_root" : "no_server_checkout",
635
+ alias,
636
+ baseUrl,
637
+ repoSlug,
638
+ 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.`,
639
+ skippedCandidates
640
+ });
641
+ }
642
+ } else if (registryResponse.status === 404) {
643
+ if (mode === "backfill-only") {
644
+ return remoteProjectLinkFailure({
645
+ status: "project_not_registered",
646
+ alias,
647
+ baseUrl,
648
+ repoSlug,
649
+ statusCode: registryResponse.status,
650
+ message: `Remote server ${alias} has no registered project record for ${repoSlug}.`
651
+ });
652
+ }
653
+ } else {
654
+ return remoteProjectLinkFailure({
655
+ status: "error",
656
+ alias,
657
+ baseUrl,
658
+ repoSlug,
659
+ statusCode: registryResponse.status,
660
+ message: `Could not backfill ${repoSlug} from remote registry (${registryResponse.status}): ${payloadError(registryResponse.payload, registryResponse.text || "registry lookup failed")}`,
661
+ skippedCandidates
662
+ });
663
+ }
664
+ let prepareResponse;
665
+ try {
666
+ prepareResponse = await requestRemoteProjectLinkJson(baseUrl, `/api/projects/${encodeURIComponent(repoSlug)}/prepare-checkout`, authToken, {
667
+ method: "POST",
668
+ headers: { "content-type": "application/json" },
669
+ body: JSON.stringify({ checkout: { kind: "managed-clone" }, repoUrl: `https://github.com/${repoSlug}.git` })
670
+ });
671
+ } catch (error) {
672
+ return remoteProjectLinkFailure({
673
+ status: "error",
674
+ alias,
675
+ baseUrl,
676
+ repoSlug,
677
+ message: `Could not prepare remote checkout for ${repoSlug} on ${alias}: ${error instanceof Error ? error.message : String(error)}`
678
+ });
679
+ }
680
+ if (prepareResponse.status === 401 || prepareResponse.status === 403) {
681
+ return remoteProjectLinkFailure({
682
+ status: "auth_required",
683
+ alias,
684
+ baseUrl,
685
+ repoSlug,
686
+ statusCode: prepareResponse.status,
687
+ message: `Remote server ${alias} requires authentication before preparing ${repoSlug}.`
688
+ });
689
+ }
690
+ if (!prepareResponse.ok) {
691
+ return remoteProjectLinkFailure({
692
+ status: "error",
693
+ alias,
694
+ baseUrl,
695
+ repoSlug,
696
+ statusCode: prepareResponse.status,
697
+ message: `Remote checkout prepare failed for ${repoSlug} (${prepareResponse.status}): ${payloadError(prepareResponse.payload, prepareResponse.text || "prepare-checkout failed")}`,
698
+ skippedCandidates
699
+ });
700
+ }
701
+ const preparedPath = checkoutPathFromPreparePayload(prepareResponse.payload);
702
+ if (!preparedPath) {
703
+ return remoteProjectLinkFailure({
704
+ status: "invalid_root",
705
+ alias,
706
+ baseUrl,
707
+ repoSlug,
708
+ message: `Remote checkout prepare for ${repoSlug} did not return a checkout.path.`,
709
+ skippedCandidates
710
+ });
711
+ }
712
+ return validateAndPersistRemoteRoot({ projectRoot, alias, baseUrl, authToken, repoSlug, candidate: preparedPath, source: "prepare", prepared: true });
713
+ }
714
+ async function inspectRemoteProjectLink(projectRoot) {
715
+ return ensureRemoteProjectRootLink(projectRoot, { mode: "backfill-only" });
716
+ }
717
+ async function repairRemoteProjectRootLink(context, options = {}) {
718
+ const resolution = await ensureRemoteProjectRootLink(context.projectRoot, { mode: options.mode ?? "prepare-if-missing", repoSlug: options.repoSlug });
719
+ if (!resolution.ok)
720
+ throw new CliError(resolution.message, 1, { hint: resolution.hint });
721
+ return resolution;
722
+ }
272
723
  async function ensureServerForCli(projectRoot) {
273
724
  try {
274
725
  const selected = resolveSelectedConnection(projectRoot);
275
726
  if (selected?.connection.kind === "remote") {
276
727
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
277
728
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
278
- const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
729
+ const link = await ensureRemoteProjectRootLink(projectRoot, { mode: "backfill-only", authToken });
279
730
  return {
280
731
  baseUrl: selected.connection.baseUrl,
281
732
  authToken,
282
733
  connectionKind: "remote",
283
- serverProjectRoot
734
+ serverProjectRoot: link.ok ? link.serverProjectRoot ?? null : null
284
735
  };
285
736
  }
286
737
  reportServerPhase("Starting local Rig server\u2026");
@@ -298,32 +749,6 @@ async function ensureServerForCli(projectRoot) {
298
749
  throw error;
299
750
  }
300
751
  }
301
- async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
302
- const repo = readRepoConnection(projectRoot);
303
- const slug = repo?.project?.trim();
304
- if (!slug)
305
- return null;
306
- try {
307
- const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
308
- if (authToken && queryAuthFallbackEnabled())
309
- url.searchParams.set("rt", authToken);
310
- const response = await fetch(url, {
311
- headers: mergeHeaders(undefined, authToken)
312
- });
313
- if (!response.ok)
314
- return null;
315
- const payload = await response.json();
316
- const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
317
- const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
318
- const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
319
- const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
320
- if (path)
321
- writeRepoServerProjectRoot(projectRoot, path);
322
- return path;
323
- } catch {
324
- return null;
325
- }
326
- }
327
752
  function appendTaskFilterParams(url, filters) {
328
753
  if (filters.assignee)
329
754
  url.searchParams.set("assignee", filters.assignee);
@@ -424,13 +849,20 @@ function canUseRemoteWithoutProjectRoot(pathname) {
424
849
  async function requestServerJson(context, pathname, init = {}) {
425
850
  const server = await ensureServerForCli(context.projectRoot);
426
851
  const requestUrl = new URL(`${server.baseUrl}${pathname}`);
427
- if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
428
- const repo = readRepoConnection(context.projectRoot);
429
- 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." });
852
+ let scopedServerProjectRoot = server.serverProjectRoot;
853
+ if (server.connectionKind === "remote" && !scopedServerProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
854
+ const link = await ensureRemoteProjectRootLink(context.projectRoot, { mode: "backfill-only", authToken: server.authToken });
855
+ if (link.ok && link.serverProjectRoot) {
856
+ scopedServerProjectRoot = link.serverProjectRoot;
857
+ } else {
858
+ const repo = readRepoConnection(context.projectRoot);
859
+ const target = link.alias && link.baseUrl ? `${link.alias} (${link.baseUrl})` : server.baseUrl;
860
+ 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 });
861
+ }
430
862
  }
431
863
  const headers = mergeHeaders(init.headers, server.authToken);
432
- if (server.serverProjectRoot)
433
- headers.set("x-rig-project-root", server.serverProjectRoot);
864
+ if (scopedServerProjectRoot)
865
+ headers.set("x-rig-project-root", scopedServerProjectRoot);
434
866
  if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
435
867
  requestUrl.searchParams.set("rt", server.authToken);
436
868
  }
@@ -795,6 +1227,9 @@ var init__server_client = __esm(() => {
795
1227
  ]);
796
1228
  });
797
1229
 
1230
+ // packages/cli/src/app-opentui/adapters/server.ts
1231
+ import { spawnSync } from "child_process";
1232
+
798
1233
  // packages/cli/src/app-opentui/adapters/common.ts
799
1234
  function projectRootOf(ctx) {
800
1235
  const root = ctx.rig?.projectRoot ?? ctx.projectRoot ?? ctx.getState().projectRoot;
@@ -802,6 +1237,22 @@ function projectRootOf(ctx) {
802
1237
  return root;
803
1238
  throw new Error("App adapter requires a projectRoot.");
804
1239
  }
1240
+ async function runtimeOf(ctx) {
1241
+ if (ctx.rig && "runId" in ctx.rig && ctx.ensureRuntime === undefined) {
1242
+ return ctx.rig;
1243
+ }
1244
+ if (ctx.ensureRuntime)
1245
+ return ctx.ensureRuntime();
1246
+ throw new Error("App adapter requires ensureRuntime() before this action can run.");
1247
+ }
1248
+ function normalizeAppError(error) {
1249
+ const message = error instanceof Error ? error.message : String(error);
1250
+ const rawHint = error instanceof Error ? error.hint : undefined;
1251
+ return {
1252
+ message,
1253
+ ...typeof rawHint === "string" && rawHint.trim() ? { hint: rawHint.trim() } : {}
1254
+ };
1255
+ }
805
1256
  function emitStarted(ctx, label, optimistic) {
806
1257
  ctx.emit({ type: "action.started", label, ...optimistic ? { optimistic } : {} });
807
1258
  }
@@ -811,6 +1262,18 @@ function emitProgress(ctx, label, detail, data) {
811
1262
  function emitCompleted(ctx, label, data) {
812
1263
  ctx.emit({ type: "action.completed", label, ...data ? { data } : {} });
813
1264
  }
1265
+ function emitFailed(ctx, label, error, data) {
1266
+ if (data)
1267
+ patchData(ctx, data);
1268
+ const normalized = normalizeAppError(error);
1269
+ ctx.emit({
1270
+ type: "action.failed",
1271
+ label,
1272
+ message: normalized.message,
1273
+ ...normalized.hint ? { hint: normalized.hint } : {},
1274
+ cause: error
1275
+ });
1276
+ }
814
1277
  function patchData(ctx, data) {
815
1278
  ctx.emit({ type: "data.patch", data });
816
1279
  }
@@ -856,11 +1319,128 @@ function startLocalRequested(ctx) {
856
1319
  const action = intent.action;
857
1320
  return action?.payload?.startLocal === true;
858
1321
  }
1322
+ async function serverAliases(projectRoot) {
1323
+ const { readGlobalConnections: readGlobalConnections2, readRepoConnection: readRepoConnection2 } = await Promise.resolve().then(() => (init__connection_state(), exports__connection_state));
1324
+ const selected = readRepoConnection2(projectRoot)?.selected ?? "local";
1325
+ const global = readGlobalConnections2();
1326
+ const aliases = [{ alias: "local", kind: "local", selected: selected === "local" }];
1327
+ for (const [alias, connection] of Object.entries(global.connections)) {
1328
+ aliases.push({
1329
+ alias,
1330
+ kind: connection.kind,
1331
+ ...connection.kind === "remote" ? { baseUrl: connection.baseUrl } : {},
1332
+ selected: alias === selected
1333
+ });
1334
+ }
1335
+ return aliases;
1336
+ }
1337
+ function firstString(parts, offset = 0) {
1338
+ return parts.slice(offset).find((part) => part.trim() && !part.startsWith("-"))?.trim();
1339
+ }
1340
+ function normalizeUrl(value) {
1341
+ const trimmed = value?.trim().replace(/\/+$/, "");
1342
+ if (!trimmed)
1343
+ return;
1344
+ return /^https?:\/\//i.test(trimmed) ? trimmed : undefined;
1345
+ }
1346
+ async function useLocalServerFromApp(ctx) {
1347
+ const label = "Selecting local server";
1348
+ emitStarted(ctx, label);
1349
+ try {
1350
+ const projectRoot = projectRootOf(ctx);
1351
+ const { readRepoConnection: readRepoConnection2, writeRepoConnection: writeRepoConnection2 } = await Promise.resolve().then(() => (init__connection_state(), exports__connection_state));
1352
+ const previous = readRepoConnection2(projectRoot);
1353
+ writeRepoConnection2(projectRoot, {
1354
+ selected: "local",
1355
+ ...previous?.project ? { project: previous.project } : {},
1356
+ linkedAt: new Date().toISOString()
1357
+ });
1358
+ emitCompleted(ctx, label, { selected: "local" });
1359
+ return refreshServerStatus(ctx);
1360
+ } catch (error) {
1361
+ emitFailed(ctx, label, error);
1362
+ throw error;
1363
+ }
1364
+ }
1365
+ async function useRemoteServerFromApp(ctx, alias) {
1366
+ const label = alias ? `Selecting remote ${alias}` : "Selecting remote server";
1367
+ emitStarted(ctx, label);
1368
+ try {
1369
+ const cleanAlias = alias?.trim();
1370
+ if (!cleanAlias)
1371
+ throw new Error("Enter a saved remote alias, or add one with: add remote \u2192 <alias> <https-url>.");
1372
+ const projectRoot = projectRootOf(ctx);
1373
+ const { readGlobalConnections: readGlobalConnections2, readRepoConnection: readRepoConnection2, writeRepoConnection: writeRepoConnection2 } = await Promise.resolve().then(() => (init__connection_state(), exports__connection_state));
1374
+ const connection = readGlobalConnections2().connections[cleanAlias];
1375
+ if (!connection)
1376
+ throw new Error(`Remote alias ${cleanAlias} is not saved yet. Use add remote first.`);
1377
+ if (connection.kind !== "remote")
1378
+ throw new Error(`${cleanAlias} is not a remote server alias.`);
1379
+ const previous = readRepoConnection2(projectRoot);
1380
+ writeRepoConnection2(projectRoot, {
1381
+ selected: cleanAlias,
1382
+ ...previous?.project ? { project: previous.project } : {},
1383
+ linkedAt: new Date().toISOString()
1384
+ });
1385
+ emitCompleted(ctx, label, { selected: cleanAlias, baseUrl: connection.baseUrl });
1386
+ return refreshServerStatus(ctx);
1387
+ } catch (error) {
1388
+ emitFailed(ctx, label, error);
1389
+ throw error;
1390
+ }
1391
+ }
1392
+ async function addRemoteServerFromApp(ctx, args) {
1393
+ const label = "Adding remote server";
1394
+ emitStarted(ctx, label);
1395
+ try {
1396
+ const alias = firstString(args, 2);
1397
+ const url = normalizeUrl(firstString(args, alias ? args.indexOf(alias) + 1 : 3));
1398
+ if (!alias || !url)
1399
+ throw new Error("Add remote expects: <alias> <https-url>. Example: prod https://rig.example.com");
1400
+ const projectRoot = projectRootOf(ctx);
1401
+ const { readRepoConnection: readRepoConnection2, upsertGlobalConnection: upsertGlobalConnection2, writeRepoConnection: writeRepoConnection2 } = await Promise.resolve().then(() => (init__connection_state(), exports__connection_state));
1402
+ upsertGlobalConnection2(alias, { kind: "remote", baseUrl: url });
1403
+ const previous = readRepoConnection2(projectRoot);
1404
+ writeRepoConnection2(projectRoot, {
1405
+ selected: alias,
1406
+ ...previous?.project ? { project: previous.project } : {},
1407
+ linkedAt: new Date().toISOString()
1408
+ });
1409
+ emitCompleted(ctx, label, { alias, baseUrl: url });
1410
+ return refreshServerStatus(ctx);
1411
+ } catch (error) {
1412
+ emitFailed(ctx, label, error);
1413
+ throw error;
1414
+ }
1415
+ }
1416
+ async function importGitHubAuthFromGhApp(ctx) {
1417
+ const label = "Importing GitHub auth";
1418
+ emitStarted(ctx, label);
1419
+ try {
1420
+ const projectRoot = projectRootOf(ctx);
1421
+ const token = spawnSync("gh", ["auth", "token"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 1e4 }).stdout.trim();
1422
+ if (!token)
1423
+ throw new Error("Could not read GitHub token from gh. Sign in with gh auth login, then retry import auth.");
1424
+ const runtime = await runtimeOf(ctx);
1425
+ const { postGitHubTokenViaServer: postGitHubTokenViaServer2, setGitHubBearerTokenForCurrentProcess: setGitHubBearerTokenForCurrentProcess2 } = await Promise.resolve().then(() => (init__server_client(), exports__server_client));
1426
+ const { readRepoConnection: readRepoConnection2 } = await Promise.resolve().then(() => (init__connection_state(), exports__connection_state));
1427
+ const selectedRepo = readRepoConnection2(projectRoot)?.project;
1428
+ const payload = await postGitHubTokenViaServer2(runtime, token, selectedRepo ? { selectedRepo } : {});
1429
+ const apiSessionToken = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.apiSessionToken : undefined;
1430
+ setGitHubBearerTokenForCurrentProcess2(typeof apiSessionToken === "string" ? apiSessionToken : token, projectRoot);
1431
+ emitCompleted(ctx, label, { selectedRepo: selectedRepo ?? null });
1432
+ return refreshServerStatus(ctx);
1433
+ } catch (error) {
1434
+ emitFailed(ctx, label, error);
1435
+ throw error;
1436
+ }
1437
+ }
859
1438
  async function startLocalServerFromApp(ctx) {
860
1439
  const label = "Starting local server";
861
1440
  emitStarted(ctx, label);
862
1441
  patchData(ctx, {
863
1442
  lastIntent: undefined,
1443
+ remoteProjectLink: undefined,
864
1444
  server: { label: "starting local server", reachable: false, error: "spawning local rig-server (detached)\u2026" }
865
1445
  });
866
1446
  try {
@@ -885,32 +1465,61 @@ async function startLocalServerFromApp(ctx) {
885
1465
  reachable: false,
886
1466
  error: error instanceof Error ? error.message : String(error)
887
1467
  };
888
- patchData(ctx, { server: state, lastRefreshError: state.error });
1468
+ patchData(ctx, { server: state, remoteProjectLink: undefined, lastRefreshError: state.error });
889
1469
  patchFooter(ctx, { server: "unavailable" });
890
- emitCompleted(ctx, label, { server: state.label, reachable: false, error: state.error });
1470
+ emitCompleted(ctx, label, { serverLabel: state.label, reachable: false, error: state.error });
891
1471
  return state;
892
1472
  }
893
1473
  }
1474
+ async function repairRemoteProjectRootLinkFromApp(ctx) {
1475
+ const label = "Repairing remote project link";
1476
+ emitStarted(ctx, label);
1477
+ try {
1478
+ const projectRoot = projectRootOf(ctx);
1479
+ const { ensureRemoteProjectRootLink: ensureRemoteProjectRootLink2 } = await Promise.resolve().then(() => (init__server_client(), exports__server_client));
1480
+ emitProgress(ctx, label, "backfilling or preparing server-host checkout");
1481
+ const result = await ensureRemoteProjectRootLink2(projectRoot, { mode: "prepare-if-missing" });
1482
+ patchData(ctx, { remoteProjectLink: result, server: { label: result.alias ?? "remote", baseUrl: result.baseUrl, kind: "remote", reachable: result.status !== "not_remote", remoteProjectLink: result } });
1483
+ patchFooter(ctx, { server: result.alias ?? "remote", message: `remote link ${result.status}` });
1484
+ if (!result.ok) {
1485
+ const error = new Error(result.message);
1486
+ error.hint = result.hint;
1487
+ patchData(ctx, { lastRefreshError: result.message });
1488
+ throw error;
1489
+ }
1490
+ emitCompleted(ctx, label, { repoSlug: result.repoSlug, serverProjectRoot: result.serverProjectRoot, status: result.status });
1491
+ return result;
1492
+ } catch (error) {
1493
+ const message = error instanceof Error ? error.message : String(error);
1494
+ const existing = ctx.getState().data?.remoteProjectLink;
1495
+ const remoteProjectLink = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : { ok: false, status: "error", message };
1496
+ patchData(ctx, { lastRefreshError: message, remoteProjectLink });
1497
+ emitCompleted(ctx, label, { ok: false, error: message, remoteProjectLink });
1498
+ throw error;
1499
+ }
1500
+ }
894
1501
  async function refreshServerStatus(ctx) {
895
1502
  if (startLocalRequested(ctx)) {
896
1503
  return startLocalServerFromApp(ctx);
897
1504
  }
898
1505
  const label = "Checking server";
899
1506
  emitStarted(ctx, label);
900
- patchData(ctx, { server: { label: "checking selected server", reachable: false, error: "checking selected server\u2026" } });
1507
+ patchData(ctx, { server: { label: "checking selected server", reachable: false, error: "checking selected server\u2026" }, remoteProjectLink: undefined });
901
1508
  const started = Date.now();
902
1509
  try {
903
- const { ensureServerForCli: ensureServerForCli2, requestServerJson: requestServerJson2, resolveServerConnectionLabel: resolveServerConnectionLabel2 } = await Promise.resolve().then(() => (init__server_client(), exports__server_client));
1510
+ const { ensureRemoteProjectRootLink: ensureRemoteProjectRootLink2, ensureServerForCli: ensureServerForCli2, requestServerJson: requestServerJson2, resolveServerConnectionLabel: resolveServerConnectionLabel2 } = await Promise.resolve().then(() => (init__server_client(), exports__server_client));
904
1511
  const projectRoot = projectRootOf(ctx);
1512
+ const aliases = await serverAliases(projectRoot);
905
1513
  emitProgress(ctx, label, "resolving selected server");
906
1514
  const [connectionLabel, server] = await Promise.all([
907
1515
  resolveServerConnectionLabel2(projectRoot),
908
1516
  withTimeout(ensureServerForCli2(projectRoot), 5000, "server connection")
909
1517
  ]);
910
1518
  emitProgress(ctx, label, "requesting server status", { baseUrl: server.baseUrl, kind: server.connectionKind });
911
- const [status, auth] = await Promise.all([
1519
+ const [status, auth, remoteProjectLink] = await Promise.all([
912
1520
  withRetry("server status", () => withTimeout(requestServerJson2({ projectRoot }, "/api/server/status"), SERVER_PROBE_TIMEOUT_MS, "server status")).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) })),
913
- withRetry("github auth status", () => withTimeout(requestServerJson2({ projectRoot }, "/api/github/auth/status"), SERVER_PROBE_TIMEOUT_MS, "github auth status")).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) }))
1521
+ withRetry("github auth status", () => withTimeout(requestServerJson2({ projectRoot }, "/api/github/auth/status"), SERVER_PROBE_TIMEOUT_MS, "github auth status")).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) })),
1522
+ server.connectionKind === "remote" ? withTimeout(ensureRemoteProjectRootLink2(projectRoot, { mode: "backfill-only", authToken: server.authToken }), SERVER_PROBE_TIMEOUT_MS, "remote project link").catch((error) => ({ ok: false, status: "error", message: error instanceof Error ? error.message : String(error) })) : Promise.resolve(null)
914
1523
  ]);
915
1524
  const state = {
916
1525
  label: connectionLabel,
@@ -919,26 +1528,34 @@ async function refreshServerStatus(ctx) {
919
1528
  reachable: true,
920
1529
  latencyMs: Date.now() - started,
921
1530
  status: status && typeof status === "object" && !Array.isArray(status) ? status : {},
922
- auth: auth && typeof auth === "object" && !Array.isArray(auth) ? auth : {}
1531
+ auth: auth && typeof auth === "object" && !Array.isArray(auth) ? auth : {},
1532
+ aliases,
1533
+ ...remoteProjectLink && typeof remoteProjectLink === "object" && !Array.isArray(remoteProjectLink) ? { remoteProjectLink } : {}
923
1534
  };
924
- patchData(ctx, { server: state });
1535
+ patchData(ctx, { server: state, remoteProjectLink: state.remoteProjectLink });
925
1536
  patchFooter(ctx, { server: connectionLabel, latency: `${state.latencyMs}ms` });
926
- emitCompleted(ctx, label, { server: connectionLabel, latencyMs: state.latencyMs });
1537
+ emitCompleted(ctx, label, { serverLabel: connectionLabel, latencyMs: state.latencyMs });
927
1538
  return state;
928
1539
  } catch (error) {
929
1540
  const state = {
930
1541
  label: "server unavailable",
931
1542
  reachable: false,
932
1543
  latencyMs: Date.now() - started,
1544
+ aliases: await serverAliases(projectRootOf(ctx)).catch(() => []),
933
1545
  error: error instanceof Error ? error.message : String(error)
934
1546
  };
935
- patchData(ctx, { server: state });
1547
+ patchData(ctx, { server: state, remoteProjectLink: undefined });
936
1548
  patchFooter(ctx, { server: "unavailable" });
937
- emitCompleted(ctx, label, { server: state.label, reachable: false, error: state.error });
1549
+ emitCompleted(ctx, label, { serverLabel: state.label, reachable: false, error: state.error });
938
1550
  return state;
939
1551
  }
940
1552
  }
941
1553
  export {
1554
+ useRemoteServerFromApp,
1555
+ useLocalServerFromApp,
942
1556
  startLocalServerFromApp,
943
- refreshServerStatus
1557
+ repairRemoteProjectRootLinkFromApp,
1558
+ refreshServerStatus,
1559
+ importGitHubAuthFromGhApp,
1560
+ addRemoteServerFromApp
944
1561
  };