@h-rig/cli 0.0.6-alpha.81 → 0.0.6-alpha.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/build-rig-binaries.js +40 -8
- package/dist/bin/rig.js +2807 -1443
- package/dist/src/app/board.js +62 -13
- package/dist/src/app-opentui/adapters/command.d.ts +2 -0
- package/dist/src/app-opentui/adapters/command.js +329 -0
- package/dist/src/app-opentui/adapters/common.js +2 -2
- package/dist/src/app-opentui/adapters/doctor.js +63 -14
- package/dist/src/app-opentui/adapters/fleet.js +84 -20
- package/dist/src/app-opentui/adapters/inbox.js +83 -19
- package/dist/src/app-opentui/adapters/init.js +87 -23
- package/dist/src/app-opentui/adapters/pi-attach.js +102 -36
- package/dist/src/app-opentui/adapters/run-detail.js +83 -19
- package/dist/src/app-opentui/adapters/server.js +100 -23
- package/dist/src/app-opentui/adapters/tasks.d.ts +11 -0
- package/dist/src/app-opentui/adapters/tasks.js +742 -94
- package/dist/src/app-opentui/bootstrap.d.ts +1 -6
- package/dist/src/app-opentui/bootstrap.js +13776 -12369
- package/dist/src/app-opentui/command-pty-host.d.ts +62 -0
- package/dist/src/app-opentui/command-pty-host.js +248 -0
- package/dist/src/app-opentui/events.js +1 -1
- package/dist/src/app-opentui/focus-manager.d.ts +14 -0
- package/dist/src/app-opentui/focus-manager.js +24 -0
- package/dist/src/app-opentui/index.js +3806 -2878
- package/dist/src/app-opentui/intent.js +154 -48
- package/dist/src/app-opentui/keymap.d.ts +20 -0
- package/dist/src/app-opentui/keymap.js +707 -0
- package/dist/src/app-opentui/launch-routing.d.ts +16 -0
- package/dist/src/app-opentui/launch-routing.js +55 -0
- package/dist/src/app-opentui/layout.js +2 -2
- package/dist/src/app-opentui/pi-host-child.js +66 -16
- package/dist/src/app-opentui/pi-pty-host.d.ts +9 -0
- package/dist/src/app-opentui/pi-pty-host.js +15 -13
- package/dist/src/app-opentui/registry.js +3228 -2396
- package/dist/src/app-opentui/render/ascii-fleet.d.ts +15 -0
- package/dist/src/app-opentui/render/ascii-fleet.js +82 -0
- package/dist/src/app-opentui/render/graphics.d.ts +1 -1
- package/dist/src/app-opentui/render/graphics.js +131 -16
- package/dist/src/app-opentui/render/image-visual-layer-worker.js +413 -958
- package/dist/src/app-opentui/render/image-visual-layer.d.ts +19 -10
- package/dist/src/app-opentui/render/image-visual-layer.js +391 -357
- package/dist/src/app-opentui/render/panel-layout.d.ts +38 -0
- package/dist/src/app-opentui/render/panel-layout.js +48 -0
- package/dist/src/app-opentui/render/panels.d.ts +2 -0
- package/dist/src/app-opentui/render/panels.js +62 -29
- package/dist/src/app-opentui/render/preloader.d.ts +10 -0
- package/dist/src/app-opentui/render/preloader.js +163 -0
- package/dist/src/app-opentui/render/scene.d.ts +3 -0
- package/dist/src/app-opentui/render/scene.js +12 -0
- package/dist/src/app-opentui/render/text.js +5 -1
- package/dist/src/app-opentui/render/type-bar.d.ts +2 -1
- package/dist/src/app-opentui/render/type-bar.js +51 -16
- package/dist/src/app-opentui/runtime-resources.d.ts +16 -0
- package/dist/src/app-opentui/runtime-resources.js +62 -0
- package/dist/src/app-opentui/runtime.js +2329 -1512
- package/dist/src/app-opentui/scenes/command.d.ts +3 -0
- package/dist/src/app-opentui/scenes/command.js +103 -0
- package/dist/src/app-opentui/scenes/doctor.d.ts +2 -1
- package/dist/src/app-opentui/scenes/doctor.js +53 -14
- package/dist/src/app-opentui/scenes/error.js +44 -14
- package/dist/src/app-opentui/scenes/fleet.js +40 -21
- package/dist/src/app-opentui/scenes/handoff.js +142 -27
- package/dist/src/app-opentui/scenes/help.js +63 -48
- package/dist/src/app-opentui/scenes/inbox.d.ts +2 -1
- package/dist/src/app-opentui/scenes/inbox.js +64 -17
- package/dist/src/app-opentui/scenes/init.d.ts +2 -1
- package/dist/src/app-opentui/scenes/init.js +62 -21
- package/dist/src/app-opentui/scenes/main.d.ts +2 -1
- package/dist/src/app-opentui/scenes/main.js +80 -24
- package/dist/src/app-opentui/scenes/run-detail.d.ts +2 -1
- package/dist/src/app-opentui/scenes/run-detail.js +39 -25
- package/dist/src/app-opentui/scenes/server.d.ts +2 -1
- package/dist/src/app-opentui/scenes/server.js +71 -20
- package/dist/src/app-opentui/scenes/tasks.js +44 -22
- package/dist/src/app-opentui/state.js +70 -30
- package/dist/src/app-opentui/terminal-capabilities.d.ts +7 -0
- package/dist/src/app-opentui/terminal-capabilities.js +15 -0
- package/dist/src/app-opentui/types.d.ts +10 -2
- package/dist/src/commands/_doctor-checks.js +62 -13
- package/dist/src/commands/_operator-view.js +63 -13
- package/dist/src/commands/_pi-frontend.js +63 -13
- package/dist/src/commands/_preflight.js +64 -14
- package/dist/src/commands/_server-client.js +82 -18
- package/dist/src/commands/_snapshot-upload.js +62 -13
- package/dist/src/commands/connect.js +7 -1
- package/dist/src/commands/doctor.js +62 -13
- package/dist/src/commands/github.js +144 -23
- package/dist/src/commands/inbox.js +62 -13
- package/dist/src/commands/init.js +82 -18
- package/dist/src/commands/inspect.js +62 -13
- package/dist/src/commands/run.js +66 -13
- package/dist/src/commands/server.js +69 -14
- package/dist/src/commands/setup.js +62 -13
- package/dist/src/commands/stats.js +62 -13
- package/dist/src/commands/task-run-driver.js +62 -13
- package/dist/src/commands/task.js +65 -14
- package/dist/src/commands.js +227 -92
- package/dist/src/index.js +234 -99
- package/package.json +8 -9
- package/dist/src/app-opentui/render/image-visual-layer-native-canvas.d.ts +0 -2
- package/dist/src/app-opentui/render/image-visual-layer-native-canvas.js +0 -1480
|
@@ -133,17 +133,21 @@ function cleanToken(value) {
|
|
|
133
133
|
const trimmed = value?.trim();
|
|
134
134
|
return trimmed ? trimmed : null;
|
|
135
135
|
}
|
|
136
|
-
function
|
|
136
|
+
function readRemoteAuthState(projectRoot) {
|
|
137
137
|
const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
|
|
138
138
|
if (!existsSync2(path))
|
|
139
139
|
return null;
|
|
140
140
|
try {
|
|
141
141
|
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
142
|
-
return
|
|
142
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
143
143
|
} catch {
|
|
144
144
|
return null;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
function readPrivateRemoteSessionToken(projectRoot) {
|
|
148
|
+
const parsed = readRemoteAuthState(projectRoot);
|
|
149
|
+
return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
|
|
150
|
+
}
|
|
147
151
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
148
152
|
const scopedKey = resolve2(projectRoot);
|
|
149
153
|
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
@@ -154,15 +158,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
|
|
|
154
158
|
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
155
159
|
}
|
|
156
160
|
function readStoredGitHubAuthToken(projectRoot) {
|
|
157
|
-
const
|
|
158
|
-
|
|
161
|
+
const parsed = readRemoteAuthState(projectRoot);
|
|
162
|
+
return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
|
|
163
|
+
}
|
|
164
|
+
function inferRemoteServerProjectRootFromAuthState(projectRoot) {
|
|
165
|
+
const repo = readRepoConnection(projectRoot);
|
|
166
|
+
const slug = repo?.project?.trim();
|
|
167
|
+
if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
|
|
159
168
|
return null;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
const auth = readRemoteAuthState(projectRoot);
|
|
170
|
+
const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
|
|
171
|
+
const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
|
|
172
|
+
if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
|
|
164
173
|
return null;
|
|
165
|
-
|
|
174
|
+
const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
|
|
175
|
+
if (!checkoutBaseDir)
|
|
176
|
+
return null;
|
|
177
|
+
const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
|
|
178
|
+
writeRepoServerProjectRoot(projectRoot, inferred);
|
|
179
|
+
return inferred;
|
|
166
180
|
}
|
|
167
181
|
function readLocalConnectionFallbackToken(projectRoot) {
|
|
168
182
|
return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
|
|
@@ -173,7 +187,7 @@ async function ensureServerForCli(projectRoot) {
|
|
|
173
187
|
if (selected?.connection.kind === "remote") {
|
|
174
188
|
reportServerPhase(`Connecting to ${selected.alias}\u2026`);
|
|
175
189
|
const authToken = readGitHubBearerTokenForRemote(projectRoot);
|
|
176
|
-
const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
|
|
190
|
+
const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
|
|
177
191
|
return {
|
|
178
192
|
baseUrl: selected.connection.baseUrl,
|
|
179
193
|
authToken,
|
|
@@ -202,7 +216,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
|
|
|
202
216
|
if (!slug)
|
|
203
217
|
return null;
|
|
204
218
|
try {
|
|
205
|
-
const
|
|
219
|
+
const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
|
|
220
|
+
if (authToken && queryAuthFallbackEnabled())
|
|
221
|
+
url.searchParams.set("rt", authToken);
|
|
222
|
+
const response = await fetch(url, {
|
|
206
223
|
headers: mergeHeaders(undefined, authToken)
|
|
207
224
|
});
|
|
208
225
|
if (!response.ok)
|
|
@@ -219,10 +236,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
|
|
|
219
236
|
return null;
|
|
220
237
|
}
|
|
221
238
|
}
|
|
239
|
+
function mergeCookie(existing, name, value) {
|
|
240
|
+
const encoded = `${name}=${encodeURIComponent(value)}`;
|
|
241
|
+
if (!existing?.trim())
|
|
242
|
+
return encoded;
|
|
243
|
+
const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
|
|
244
|
+
return [...parts, encoded].join("; ");
|
|
245
|
+
}
|
|
246
|
+
function queryAuthFallbackEnabled(env = process.env) {
|
|
247
|
+
return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
|
|
248
|
+
}
|
|
222
249
|
function mergeHeaders(headers, authToken) {
|
|
223
250
|
const merged = new Headers(headers);
|
|
224
251
|
if (authToken) {
|
|
225
|
-
|
|
252
|
+
const bearer = `Bearer ${authToken}`;
|
|
253
|
+
merged.set("authorization", bearer);
|
|
254
|
+
merged.set("x-auth", bearer);
|
|
255
|
+
merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
|
|
226
256
|
}
|
|
227
257
|
return merged;
|
|
228
258
|
}
|
|
@@ -283,15 +313,34 @@ async function buildServerFailureContext(projectRoot, server) {
|
|
|
283
313
|
hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
|
|
284
314
|
};
|
|
285
315
|
}
|
|
316
|
+
function isLoopbackRemoteBaseUrl(baseUrl) {
|
|
317
|
+
try {
|
|
318
|
+
const host = new URL(baseUrl).hostname.toLowerCase();
|
|
319
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function canUseRemoteWithoutProjectRoot(pathname) {
|
|
325
|
+
return pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/server/project-root" || pathname.startsWith("/api/github/auth/") || pathname === "/api/projects" || pathname.startsWith("/api/projects/");
|
|
326
|
+
}
|
|
286
327
|
async function requestServerJson(context, pathname, init = {}) {
|
|
287
328
|
const server = await ensureServerForCli(context.projectRoot);
|
|
329
|
+
const requestUrl = new URL(`${server.baseUrl}${pathname}`);
|
|
330
|
+
if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
|
|
331
|
+
const repo = readRepoConnection(context.projectRoot);
|
|
332
|
+
throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
|
|
333
|
+
}
|
|
288
334
|
const headers = mergeHeaders(init.headers, server.authToken);
|
|
289
335
|
if (server.serverProjectRoot)
|
|
290
336
|
headers.set("x-rig-project-root", server.serverProjectRoot);
|
|
337
|
+
if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
|
|
338
|
+
requestUrl.searchParams.set("rt", server.authToken);
|
|
339
|
+
}
|
|
291
340
|
reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
|
|
292
341
|
let response;
|
|
293
342
|
try {
|
|
294
|
-
response = await fetch(
|
|
343
|
+
response = await fetch(requestUrl, {
|
|
295
344
|
...init,
|
|
296
345
|
headers
|
|
297
346
|
});
|
|
@@ -392,7 +392,13 @@ async function executeConnectionCommand(context, args, options) {
|
|
|
392
392
|
if (!state.connections[alias])
|
|
393
393
|
throw new CliError(`Unknown Rig server: ${alias}`, 1, { hint: "Run `rig server list` to see saved aliases, or save one with `rig server add <alias> <url>`." });
|
|
394
394
|
}
|
|
395
|
-
const
|
|
395
|
+
const previousRepo = readRepoConnection(context.projectRoot);
|
|
396
|
+
const repoState = {
|
|
397
|
+
selected: alias,
|
|
398
|
+
...previousRepo?.project ? { project: previousRepo.project } : {},
|
|
399
|
+
linkedAt: new Date().toISOString(),
|
|
400
|
+
...previousRepo?.serverProjectRoot && previousRepo.selected === alias ? { serverProjectRoot: previousRepo.serverProjectRoot } : {}
|
|
401
|
+
};
|
|
396
402
|
writeRepoConnection(context.projectRoot, repoState);
|
|
397
403
|
printJsonOrText(context, repoState, formatSuccessCard("Rig server selected", [
|
|
398
404
|
["selected", alias],
|
|
@@ -143,17 +143,21 @@ function cleanToken(value) {
|
|
|
143
143
|
const trimmed = value?.trim();
|
|
144
144
|
return trimmed ? trimmed : null;
|
|
145
145
|
}
|
|
146
|
-
function
|
|
146
|
+
function readRemoteAuthState(projectRoot) {
|
|
147
147
|
const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
|
|
148
148
|
if (!existsSync2(path))
|
|
149
149
|
return null;
|
|
150
150
|
try {
|
|
151
151
|
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
152
|
-
return
|
|
152
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
153
153
|
} catch {
|
|
154
154
|
return null;
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
function readPrivateRemoteSessionToken(projectRoot) {
|
|
158
|
+
const parsed = readRemoteAuthState(projectRoot);
|
|
159
|
+
return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
|
|
160
|
+
}
|
|
157
161
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
158
162
|
const scopedKey = resolve2(projectRoot);
|
|
159
163
|
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
@@ -164,15 +168,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
|
|
|
164
168
|
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
165
169
|
}
|
|
166
170
|
function readStoredGitHubAuthToken(projectRoot) {
|
|
167
|
-
const
|
|
168
|
-
|
|
171
|
+
const parsed = readRemoteAuthState(projectRoot);
|
|
172
|
+
return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
|
|
173
|
+
}
|
|
174
|
+
function inferRemoteServerProjectRootFromAuthState(projectRoot) {
|
|
175
|
+
const repo = readRepoConnection(projectRoot);
|
|
176
|
+
const slug = repo?.project?.trim();
|
|
177
|
+
if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
|
|
169
178
|
return null;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
const auth = readRemoteAuthState(projectRoot);
|
|
180
|
+
const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
|
|
181
|
+
const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
|
|
182
|
+
if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
|
|
174
183
|
return null;
|
|
175
|
-
|
|
184
|
+
const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
|
|
185
|
+
if (!checkoutBaseDir)
|
|
186
|
+
return null;
|
|
187
|
+
const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
|
|
188
|
+
writeRepoServerProjectRoot(projectRoot, inferred);
|
|
189
|
+
return inferred;
|
|
176
190
|
}
|
|
177
191
|
function readLocalConnectionFallbackToken(projectRoot) {
|
|
178
192
|
return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
|
|
@@ -183,7 +197,7 @@ async function ensureServerForCli(projectRoot) {
|
|
|
183
197
|
if (selected?.connection.kind === "remote") {
|
|
184
198
|
reportServerPhase(`Connecting to ${selected.alias}\u2026`);
|
|
185
199
|
const authToken = readGitHubBearerTokenForRemote(projectRoot);
|
|
186
|
-
const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
|
|
200
|
+
const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
|
|
187
201
|
return {
|
|
188
202
|
baseUrl: selected.connection.baseUrl,
|
|
189
203
|
authToken,
|
|
@@ -212,7 +226,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
|
|
|
212
226
|
if (!slug)
|
|
213
227
|
return null;
|
|
214
228
|
try {
|
|
215
|
-
const
|
|
229
|
+
const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
|
|
230
|
+
if (authToken && queryAuthFallbackEnabled())
|
|
231
|
+
url.searchParams.set("rt", authToken);
|
|
232
|
+
const response = await fetch(url, {
|
|
216
233
|
headers: mergeHeaders(undefined, authToken)
|
|
217
234
|
});
|
|
218
235
|
if (!response.ok)
|
|
@@ -229,10 +246,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
|
|
|
229
246
|
return null;
|
|
230
247
|
}
|
|
231
248
|
}
|
|
249
|
+
function mergeCookie(existing, name, value) {
|
|
250
|
+
const encoded = `${name}=${encodeURIComponent(value)}`;
|
|
251
|
+
if (!existing?.trim())
|
|
252
|
+
return encoded;
|
|
253
|
+
const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
|
|
254
|
+
return [...parts, encoded].join("; ");
|
|
255
|
+
}
|
|
256
|
+
function queryAuthFallbackEnabled(env = process.env) {
|
|
257
|
+
return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
|
|
258
|
+
}
|
|
232
259
|
function mergeHeaders(headers, authToken) {
|
|
233
260
|
const merged = new Headers(headers);
|
|
234
261
|
if (authToken) {
|
|
235
|
-
|
|
262
|
+
const bearer = `Bearer ${authToken}`;
|
|
263
|
+
merged.set("authorization", bearer);
|
|
264
|
+
merged.set("x-auth", bearer);
|
|
265
|
+
merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
|
|
236
266
|
}
|
|
237
267
|
return merged;
|
|
238
268
|
}
|
|
@@ -293,15 +323,34 @@ async function buildServerFailureContext(projectRoot, server) {
|
|
|
293
323
|
hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
|
|
294
324
|
};
|
|
295
325
|
}
|
|
326
|
+
function isLoopbackRemoteBaseUrl(baseUrl) {
|
|
327
|
+
try {
|
|
328
|
+
const host = new URL(baseUrl).hostname.toLowerCase();
|
|
329
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
330
|
+
} catch {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function canUseRemoteWithoutProjectRoot(pathname) {
|
|
335
|
+
return pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/server/project-root" || pathname.startsWith("/api/github/auth/") || pathname === "/api/projects" || pathname.startsWith("/api/projects/");
|
|
336
|
+
}
|
|
296
337
|
async function requestServerJson(context, pathname, init = {}) {
|
|
297
338
|
const server = await ensureServerForCli(context.projectRoot);
|
|
339
|
+
const requestUrl = new URL(`${server.baseUrl}${pathname}`);
|
|
340
|
+
if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
|
|
341
|
+
const repo = readRepoConnection(context.projectRoot);
|
|
342
|
+
throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
|
|
343
|
+
}
|
|
298
344
|
const headers = mergeHeaders(init.headers, server.authToken);
|
|
299
345
|
if (server.serverProjectRoot)
|
|
300
346
|
headers.set("x-rig-project-root", server.serverProjectRoot);
|
|
347
|
+
if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
|
|
348
|
+
requestUrl.searchParams.set("rt", server.authToken);
|
|
349
|
+
}
|
|
301
350
|
reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
|
|
302
351
|
let response;
|
|
303
352
|
try {
|
|
304
|
-
response = await fetch(
|
|
353
|
+
response = await fetch(requestUrl, {
|
|
305
354
|
...init,
|
|
306
355
|
headers
|
|
307
356
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/cli/src/commands/github.ts
|
|
3
3
|
import { spawnSync } from "child_process";
|
|
4
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
4
6
|
|
|
5
7
|
// packages/cli/src/runner.ts
|
|
6
8
|
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
@@ -155,17 +157,25 @@ function cleanToken(value) {
|
|
|
155
157
|
const trimmed = value?.trim();
|
|
156
158
|
return trimmed ? trimmed : null;
|
|
157
159
|
}
|
|
158
|
-
function
|
|
160
|
+
function setGitHubBearerTokenForCurrentProcess(token, projectRoot) {
|
|
161
|
+
const scopedKey = resolve2(projectRoot ?? process.cwd());
|
|
162
|
+
scopedGitHubBearerTokens.set(scopedKey, cleanToken(token ?? undefined));
|
|
163
|
+
}
|
|
164
|
+
function readRemoteAuthState(projectRoot) {
|
|
159
165
|
const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
|
|
160
166
|
if (!existsSync2(path))
|
|
161
167
|
return null;
|
|
162
168
|
try {
|
|
163
169
|
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
164
|
-
return
|
|
170
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
165
171
|
} catch {
|
|
166
172
|
return null;
|
|
167
173
|
}
|
|
168
174
|
}
|
|
175
|
+
function readPrivateRemoteSessionToken(projectRoot) {
|
|
176
|
+
const parsed = readRemoteAuthState(projectRoot);
|
|
177
|
+
return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
|
|
178
|
+
}
|
|
169
179
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
170
180
|
const scopedKey = resolve2(projectRoot);
|
|
171
181
|
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
@@ -176,15 +186,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
|
|
|
176
186
|
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
177
187
|
}
|
|
178
188
|
function readStoredGitHubAuthToken(projectRoot) {
|
|
179
|
-
const
|
|
180
|
-
|
|
189
|
+
const parsed = readRemoteAuthState(projectRoot);
|
|
190
|
+
return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
|
|
191
|
+
}
|
|
192
|
+
function inferRemoteServerProjectRootFromAuthState(projectRoot) {
|
|
193
|
+
const repo = readRepoConnection(projectRoot);
|
|
194
|
+
const slug = repo?.project?.trim();
|
|
195
|
+
if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
|
|
181
196
|
return null;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
197
|
+
const auth = readRemoteAuthState(projectRoot);
|
|
198
|
+
const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
|
|
199
|
+
const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
|
|
200
|
+
if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
|
|
186
201
|
return null;
|
|
187
|
-
|
|
202
|
+
const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
|
|
203
|
+
if (!checkoutBaseDir)
|
|
204
|
+
return null;
|
|
205
|
+
const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
|
|
206
|
+
writeRepoServerProjectRoot(projectRoot, inferred);
|
|
207
|
+
return inferred;
|
|
188
208
|
}
|
|
189
209
|
function readLocalConnectionFallbackToken(projectRoot) {
|
|
190
210
|
return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
|
|
@@ -195,7 +215,7 @@ async function ensureServerForCli(projectRoot) {
|
|
|
195
215
|
if (selected?.connection.kind === "remote") {
|
|
196
216
|
reportServerPhase(`Connecting to ${selected.alias}\u2026`);
|
|
197
217
|
const authToken = readGitHubBearerTokenForRemote(projectRoot);
|
|
198
|
-
const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
|
|
218
|
+
const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
|
|
199
219
|
return {
|
|
200
220
|
baseUrl: selected.connection.baseUrl,
|
|
201
221
|
authToken,
|
|
@@ -224,7 +244,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
|
|
|
224
244
|
if (!slug)
|
|
225
245
|
return null;
|
|
226
246
|
try {
|
|
227
|
-
const
|
|
247
|
+
const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
|
|
248
|
+
if (authToken && queryAuthFallbackEnabled())
|
|
249
|
+
url.searchParams.set("rt", authToken);
|
|
250
|
+
const response = await fetch(url, {
|
|
228
251
|
headers: mergeHeaders(undefined, authToken)
|
|
229
252
|
});
|
|
230
253
|
if (!response.ok)
|
|
@@ -241,10 +264,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
|
|
|
241
264
|
return null;
|
|
242
265
|
}
|
|
243
266
|
}
|
|
267
|
+
function mergeCookie(existing, name, value) {
|
|
268
|
+
const encoded = `${name}=${encodeURIComponent(value)}`;
|
|
269
|
+
if (!existing?.trim())
|
|
270
|
+
return encoded;
|
|
271
|
+
const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
|
|
272
|
+
return [...parts, encoded].join("; ");
|
|
273
|
+
}
|
|
274
|
+
function queryAuthFallbackEnabled(env = process.env) {
|
|
275
|
+
return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
|
|
276
|
+
}
|
|
244
277
|
function mergeHeaders(headers, authToken) {
|
|
245
278
|
const merged = new Headers(headers);
|
|
246
279
|
if (authToken) {
|
|
247
|
-
|
|
280
|
+
const bearer = `Bearer ${authToken}`;
|
|
281
|
+
merged.set("authorization", bearer);
|
|
282
|
+
merged.set("x-auth", bearer);
|
|
283
|
+
merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
|
|
248
284
|
}
|
|
249
285
|
return merged;
|
|
250
286
|
}
|
|
@@ -305,15 +341,34 @@ async function buildServerFailureContext(projectRoot, server) {
|
|
|
305
341
|
hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
|
|
306
342
|
};
|
|
307
343
|
}
|
|
344
|
+
function isLoopbackRemoteBaseUrl(baseUrl) {
|
|
345
|
+
try {
|
|
346
|
+
const host = new URL(baseUrl).hostname.toLowerCase();
|
|
347
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
348
|
+
} catch {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function canUseRemoteWithoutProjectRoot(pathname) {
|
|
353
|
+
return pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/server/project-root" || pathname.startsWith("/api/github/auth/") || pathname === "/api/projects" || pathname.startsWith("/api/projects/");
|
|
354
|
+
}
|
|
308
355
|
async function requestServerJson(context, pathname, init = {}) {
|
|
309
356
|
const server = await ensureServerForCli(context.projectRoot);
|
|
357
|
+
const requestUrl = new URL(`${server.baseUrl}${pathname}`);
|
|
358
|
+
if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
|
|
359
|
+
const repo = readRepoConnection(context.projectRoot);
|
|
360
|
+
throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
|
|
361
|
+
}
|
|
310
362
|
const headers = mergeHeaders(init.headers, server.authToken);
|
|
311
363
|
if (server.serverProjectRoot)
|
|
312
364
|
headers.set("x-rig-project-root", server.serverProjectRoot);
|
|
365
|
+
if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
|
|
366
|
+
requestUrl.searchParams.set("rt", server.authToken);
|
|
367
|
+
}
|
|
313
368
|
reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
|
|
314
369
|
let response;
|
|
315
370
|
try {
|
|
316
|
-
response = await fetch(
|
|
371
|
+
response = await fetch(requestUrl, {
|
|
317
372
|
...init,
|
|
318
373
|
headers
|
|
319
374
|
});
|
|
@@ -346,11 +401,26 @@ async function getGitHubAuthStatusViaServer(context) {
|
|
|
346
401
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
347
402
|
}
|
|
348
403
|
async function postGitHubTokenViaServer(context, token, options = {}) {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
404
|
+
const server = await ensureServerForCli(context.projectRoot);
|
|
405
|
+
const requestUrl = new URL(`${server.baseUrl}/api/github/auth/token`);
|
|
406
|
+
reportServerPhase("POST /api/github/auth/token\u2026");
|
|
407
|
+
let response;
|
|
408
|
+
try {
|
|
409
|
+
response = await fetch(requestUrl, {
|
|
410
|
+
method: "POST",
|
|
411
|
+
headers: { "content-type": "application/json" },
|
|
412
|
+
body: JSON.stringify({ token, selectedRepo: options.selectedRepo, projectRoot: options.projectRoot ?? server.serverProjectRoot ?? undefined })
|
|
413
|
+
});
|
|
414
|
+
} catch (error) {
|
|
415
|
+
const failure = await buildServerFailureContext(context.projectRoot, server);
|
|
416
|
+
throw new CliError(`Rig server auth bootstrap failed: ${error instanceof Error ? error.message : String(error)}
|
|
417
|
+
${failure.contextLine}`, 1, { hint: failure.hint });
|
|
418
|
+
}
|
|
419
|
+
const payload = await response.json().catch(() => null);
|
|
420
|
+
if (!response.ok) {
|
|
421
|
+
const detail = payload && typeof payload === "object" && !Array.isArray(payload) ? String(payload.error ?? JSON.stringify(payload)) : `HTTP ${response.status}`;
|
|
422
|
+
throw new CliError(`Rig server auth bootstrap failed (${response.status}): ${detail}`, 1, { hint: "Re-run `rig github auth import-gh`, or check the selected server with `rig server status`." });
|
|
423
|
+
}
|
|
354
424
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
355
425
|
}
|
|
356
426
|
var RESUMABLE_RUN_STATUSES = new Set([
|
|
@@ -564,6 +634,53 @@ function printPayload(context, payload, fallback) {
|
|
|
564
634
|
else
|
|
565
635
|
console.log(fallback);
|
|
566
636
|
}
|
|
637
|
+
function apiSessionTokenFrom(payload) {
|
|
638
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
639
|
+
return null;
|
|
640
|
+
const token = payload.apiSessionToken;
|
|
641
|
+
return typeof token === "string" && token.trim() ? token.trim() : null;
|
|
642
|
+
}
|
|
643
|
+
function payloadString(payload, key) {
|
|
644
|
+
const value = payload[key];
|
|
645
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
646
|
+
}
|
|
647
|
+
function payloadRecord(payload, key) {
|
|
648
|
+
const value = payload[key];
|
|
649
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
650
|
+
}
|
|
651
|
+
function remoteNamespaceMetadata(result) {
|
|
652
|
+
const namespace = payloadRecord(result, "userNamespace");
|
|
653
|
+
return {
|
|
654
|
+
...payloadString(result, "login") ? { login: payloadString(result, "login") } : {},
|
|
655
|
+
...payloadString(result, "userId") ? { userId: payloadString(result, "userId") } : {},
|
|
656
|
+
...namespace && payloadString(namespace, "key") ? { userNamespaceKey: payloadString(namespace, "key") } : {},
|
|
657
|
+
...namespace && payloadString(namespace, "root") ? { userNamespaceRoot: payloadString(namespace, "root") } : {},
|
|
658
|
+
...namespace && payloadString(namespace, "checkoutBaseDir") ? { checkoutBaseDir: payloadString(namespace, "checkoutBaseDir") } : {},
|
|
659
|
+
...namespace && payloadString(namespace, "snapshotBaseDir") ? { snapshotBaseDir: payloadString(namespace, "snapshotBaseDir") } : {}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function persistRemoteAuthSession(context, source, result, fallbackToken) {
|
|
663
|
+
const apiSessionToken = apiSessionTokenFrom(result);
|
|
664
|
+
setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? fallbackToken, context.projectRoot);
|
|
665
|
+
if (!apiSessionToken)
|
|
666
|
+
return;
|
|
667
|
+
const repo = readRepoConnection(context.projectRoot);
|
|
668
|
+
const path = resolve3(context.projectRoot, ".rig", "state", "github-auth.json");
|
|
669
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
670
|
+
writeFileSync2(path, `${JSON.stringify({
|
|
671
|
+
authenticated: true,
|
|
672
|
+
source,
|
|
673
|
+
storedOnServer: true,
|
|
674
|
+
...repo?.project ? { selectedRepo: repo.project } : {},
|
|
675
|
+
...remoteNamespaceMetadata(result),
|
|
676
|
+
apiSessionToken,
|
|
677
|
+
updatedAt: new Date().toISOString()
|
|
678
|
+
}, null, 2)}
|
|
679
|
+
`, "utf8");
|
|
680
|
+
}
|
|
681
|
+
function isSignedIn(status) {
|
|
682
|
+
return status.signedIn === true || status.authenticated === true;
|
|
683
|
+
}
|
|
567
684
|
function readGhToken() {
|
|
568
685
|
const result = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
|
|
569
686
|
if (result.status !== 0) {
|
|
@@ -585,7 +702,7 @@ async function executeGithub(context, args) {
|
|
|
585
702
|
if (rest.length > 0)
|
|
586
703
|
throw new CliError("Usage: rig github auth status", 1);
|
|
587
704
|
const status = await withSpinner("Checking GitHub auth on the server\u2026", () => getGitHubAuthStatusViaServer(context), { outputMode: context.outputMode });
|
|
588
|
-
printPayload(context, status, `GitHub auth: ${status
|
|
705
|
+
printPayload(context, status, `GitHub auth: ${isSignedIn(status) ? "authenticated" : "unauthenticated"}`);
|
|
589
706
|
return { ok: true, group: "github", command: "auth status", details: status };
|
|
590
707
|
}
|
|
591
708
|
case "token": {
|
|
@@ -595,16 +712,20 @@ async function executeGithub(context, args) {
|
|
|
595
712
|
const token = parsed.value?.trim();
|
|
596
713
|
if (!token)
|
|
597
714
|
throw new CliError("Missing --token value.", 1, { hint: "Re-run as `rig github auth token --token <token>`." });
|
|
598
|
-
const
|
|
599
|
-
|
|
715
|
+
const repo = readRepoConnection(context.projectRoot);
|
|
716
|
+
const result = await withSpinner("Storing GitHub token on the server\u2026", () => postGitHubTokenViaServer(context, token, repo?.project ? { selectedRepo: repo.project } : {}), { outputMode: context.outputMode });
|
|
717
|
+
persistRemoteAuthSession(context, "token", result, token);
|
|
718
|
+
printPayload(context, result, "GitHub token stored on the selected server.");
|
|
600
719
|
return { ok: true, group: "github", command: "auth token", details: result };
|
|
601
720
|
}
|
|
602
721
|
case "import-gh": {
|
|
603
722
|
if (rest.length > 0)
|
|
604
723
|
throw new CliError("Usage: rig github auth import-gh", 1);
|
|
605
724
|
const importedToken = readGhToken();
|
|
606
|
-
const
|
|
607
|
-
|
|
725
|
+
const repo = readRepoConnection(context.projectRoot);
|
|
726
|
+
const result = await withSpinner("Storing GitHub token on the server\u2026", () => postGitHubTokenViaServer(context, importedToken, repo?.project ? { selectedRepo: repo.project } : {}), { outputMode: context.outputMode });
|
|
727
|
+
persistRemoteAuthSession(context, "gh", result, importedToken);
|
|
728
|
+
printPayload(context, result, "GitHub token imported from gh and stored on the selected server.");
|
|
608
729
|
return { ok: true, group: "github", command: "auth import-gh", details: result };
|
|
609
730
|
}
|
|
610
731
|
default:
|