@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.
- package/dist/bin/rig.js +1159 -292
- package/dist/src/app/board.js +462 -48
- package/dist/src/app-opentui/adapters/doctor.js +458 -46
- package/dist/src/app-opentui/adapters/family.js +670 -147
- package/dist/src/app-opentui/adapters/fleet.js +477 -46
- package/dist/src/app-opentui/adapters/inbox.js +477 -46
- package/dist/src/app-opentui/adapters/init.js +497 -74
- package/dist/src/app-opentui/adapters/inspect.js +477 -46
- package/dist/src/app-opentui/adapters/pi-attach.js +484 -53
- package/dist/src/app-opentui/adapters/run-detail.js +477 -46
- package/dist/src/app-opentui/adapters/server.d.ts +26 -0
- package/dist/src/app-opentui/adapters/server.js +676 -59
- package/dist/src/app-opentui/adapters/tasks.js +539 -81
- package/dist/src/app-opentui/autocomplete.js +4 -2
- package/dist/src/app-opentui/bootstrap.js +1155 -288
- package/dist/src/app-opentui/command-palette.js +37 -10
- package/dist/src/app-opentui/index.js +566 -89
- package/dist/src/app-opentui/intent.js +33 -8
- package/dist/src/app-opentui/keymap.js +43 -29
- package/dist/src/app-opentui/pi-host-child.js +465 -53
- package/dist/src/app-opentui/react/App.js +104 -44
- package/dist/src/app-opentui/react/ChromeHost.js +16 -4
- package/dist/src/app-opentui/react/launch.js +555 -88
- package/dist/src/app-opentui/react/nav.js +1 -1
- package/dist/src/app-opentui/registry.js +1062 -237
- package/dist/src/app-opentui/remote-link.d.ts +10 -0
- package/dist/src/app-opentui/remote-link.js +47 -0
- package/dist/src/app-opentui/runtime.js +566 -89
- package/dist/src/app-opentui/scenes/doctor.js +1 -1
- package/dist/src/app-opentui/scenes/error.js +50 -4
- package/dist/src/app-opentui/scenes/family.js +60 -6
- package/dist/src/app-opentui/scenes/fleet.js +65 -13
- package/dist/src/app-opentui/scenes/help.js +4 -2
- package/dist/src/app-opentui/scenes/init.js +12 -12
- package/dist/src/app-opentui/scenes/main.js +7 -7
- package/dist/src/app-opentui/scenes/server.js +83 -11
- package/dist/src/app-opentui/scenes/tasks.js +79 -16
- package/dist/src/app-opentui/state.js +25 -5
- package/dist/src/app-opentui/surface-catalog.js +4 -2
- package/dist/src/app-opentui/types.d.ts +1 -1
- package/dist/src/commands/_cli-format.d.ts +10 -1
- package/dist/src/commands/_cli-format.js +5 -2
- package/dist/src/commands/_connection-state.d.ts +11 -1
- package/dist/src/commands/_connection-state.js +50 -5
- package/dist/src/commands/_doctor-checks.js +458 -46
- package/dist/src/commands/_help-catalog.js +4 -2
- package/dist/src/commands/_json-output.js +4 -0
- package/dist/src/commands/_operator-view.js +465 -53
- package/dist/src/commands/_pi-frontend.js +465 -53
- package/dist/src/commands/_preflight.js +509 -72
- package/dist/src/commands/_server-client.d.ts +33 -0
- package/dist/src/commands/_server-client.js +477 -46
- package/dist/src/commands/_server-events.js +446 -41
- package/dist/src/commands/_snapshot-upload.js +460 -48
- package/dist/src/commands/connect.js +620 -15
- package/dist/src/commands/doctor.js +458 -46
- package/dist/src/commands/github.js +462 -50
- package/dist/src/commands/inbox.js +458 -46
- package/dist/src/commands/init.js +497 -74
- package/dist/src/commands/inspect.js +458 -46
- package/dist/src/commands/run.js +465 -53
- package/dist/src/commands/server.js +647 -163
- package/dist/src/commands/setup.js +463 -51
- package/dist/src/commands/stats.js +462 -48
- package/dist/src/commands/task-run-driver.js +464 -52
- package/dist/src/commands/task.js +520 -81
- package/dist/src/commands.js +670 -147
- package/dist/src/index.js +674 -147
- package/dist/src/launcher.js +4 -0
- 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
|
|
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
|
-
|
|
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
|
|
253
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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 (
|
|
433
|
-
headers.set("x-rig-project-root",
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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
|
-
|
|
1557
|
+
repairRemoteProjectRootLinkFromApp,
|
|
1558
|
+
refreshServerStatus,
|
|
1559
|
+
importGitHubAuthFromGhApp,
|
|
1560
|
+
addRemoteServerFromApp
|
|
944
1561
|
};
|