@h-rig/cli 0.0.6-alpha.7 → 0.0.6-alpha.71
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/README.md +1 -1
- package/dist/bin/rig.js +4507 -1506
- package/dist/src/commands/_async-ui.js +152 -0
- package/dist/src/commands/_authority-runs.js +2 -3
- package/dist/src/commands/_cli-format.js +369 -0
- package/dist/src/commands/_connection-state.js +30 -11
- package/dist/src/commands/_doctor-checks.js +177 -43
- package/dist/src/commands/_help-catalog.js +485 -0
- package/dist/src/commands/_json-output.js +56 -0
- package/dist/src/commands/_operator-surface.js +220 -0
- package/dist/src/commands/_operator-view.js +595 -72
- package/dist/src/commands/_parsers.js +18 -11
- package/dist/src/commands/_pi-frontend.js +411 -0
- package/dist/src/commands/_pi-install.js +4 -3
- package/dist/src/commands/_policy.js +12 -5
- package/dist/src/commands/_preflight.js +187 -127
- package/dist/src/commands/_run-driver-helpers.js +75 -22
- package/dist/src/commands/_run-replay.js +142 -0
- package/dist/src/commands/_server-client.js +343 -60
- package/dist/src/commands/_snapshot-upload.js +160 -38
- package/dist/src/commands/_spinner.js +65 -0
- package/dist/src/commands/_task-picker.js +44 -16
- package/dist/src/commands/agent.js +39 -20
- package/dist/src/commands/browser.js +28 -21
- package/dist/src/commands/connect.js +146 -33
- package/dist/src/commands/dist.js +19 -12
- package/dist/src/commands/doctor.js +304 -44
- package/dist/src/commands/github.js +301 -52
- package/dist/src/commands/inbox.js +679 -72
- package/dist/src/commands/init.js +622 -118
- package/dist/src/commands/inspect.js +515 -32
- package/dist/src/commands/inspector.js +20 -13
- package/dist/src/commands/pi.js +177 -0
- package/dist/src/commands/plugin.js +95 -27
- package/dist/src/commands/profile-and-review.js +26 -19
- package/dist/src/commands/queue.js +32 -12
- package/dist/src/commands/remote.js +43 -36
- package/dist/src/commands/repo-git-harness.js +22 -15
- package/dist/src/commands/run.js +1162 -158
- package/dist/src/commands/server.js +373 -56
- package/dist/src/commands/setup.js +316 -62
- package/dist/src/commands/stats.js +1030 -0
- package/dist/src/commands/task-report-bug.js +29 -22
- package/dist/src/commands/task-run-driver.js +862 -129
- package/dist/src/commands/task.js +1423 -311
- package/dist/src/commands/test.js +15 -8
- package/dist/src/commands/workspace.js +18 -11
- package/dist/src/commands.js +4446 -1499
- package/dist/src/index.js +4502 -1504
- package/dist/src/launcher.js +77 -13
- package/dist/src/report-bug.js +3 -3
- package/dist/src/runner.js +16 -22
- package/package.json +10 -5
|
@@ -3,17 +3,24 @@ var __require = import.meta.require;
|
|
|
3
3
|
|
|
4
4
|
// packages/cli/src/commands/init.ts
|
|
5
5
|
import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
6
|
-
import { spawnSync
|
|
7
|
-
import { resolve as resolve6 } from "path";
|
|
6
|
+
import { spawnSync } from "child_process";
|
|
7
|
+
import { basename, resolve as resolve6 } from "path";
|
|
8
8
|
|
|
9
9
|
// packages/cli/src/runner.ts
|
|
10
10
|
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
11
|
-
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
11
|
+
import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
|
|
12
12
|
import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
|
|
13
|
-
import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
|
|
14
|
-
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
15
13
|
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
16
|
-
|
|
14
|
+
|
|
15
|
+
class CliError extends RuntimeCliError {
|
|
16
|
+
hint;
|
|
17
|
+
constructor(message, exitCode = 1, options = {}) {
|
|
18
|
+
super(message, exitCode);
|
|
19
|
+
if (options.hint?.trim()) {
|
|
20
|
+
this.hint = options.hint.trim();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
17
24
|
function takeFlag(args, flag) {
|
|
18
25
|
const rest = [];
|
|
19
26
|
let value = false;
|
|
@@ -34,7 +41,7 @@ function takeOption(args, option) {
|
|
|
34
41
|
if (current === option) {
|
|
35
42
|
const next = args[index + 1];
|
|
36
43
|
if (!next || next.startsWith("-")) {
|
|
37
|
-
throw new CliError(`Missing value for ${option}`);
|
|
44
|
+
throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
|
|
38
45
|
}
|
|
39
46
|
value = next;
|
|
40
47
|
index += 1;
|
|
@@ -49,6 +56,7 @@ function takeOption(args, option) {
|
|
|
49
56
|
|
|
50
57
|
// packages/cli/src/commands/init.ts
|
|
51
58
|
import { buildRigInitConfigSource } from "@rig/core";
|
|
59
|
+
import { listGitHubProjects as listGitHubProjectsDirect, resolveProjectStatusField as resolveProjectStatusFieldDirect } from "@rig/server";
|
|
52
60
|
|
|
53
61
|
// packages/cli/src/commands/_connection-state.ts
|
|
54
62
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -72,7 +80,7 @@ function readJsonFile(path) {
|
|
|
72
80
|
try {
|
|
73
81
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
74
82
|
} catch (error) {
|
|
75
|
-
throw new
|
|
83
|
+
throw new CliError(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1, { hint: "Fix or delete that file, then re-select a server with `rig server use <alias|local>`." });
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
86
|
function writeJsonFile(path, value) {
|
|
@@ -115,7 +123,7 @@ function writeGlobalConnections(state, options = {}) {
|
|
|
115
123
|
function upsertGlobalConnection(alias, connection, options = {}) {
|
|
116
124
|
const cleanAlias = alias.trim();
|
|
117
125
|
if (!cleanAlias)
|
|
118
|
-
throw new
|
|
126
|
+
throw new CliError("Connection alias is required.", 1, { hint: "Save a server with `rig server add <alias> <url>`." });
|
|
119
127
|
const state = readGlobalConnections(options);
|
|
120
128
|
state.connections[cleanAlias] = connection;
|
|
121
129
|
writeGlobalConnections(state, options);
|
|
@@ -132,7 +140,8 @@ function readRepoConnection(projectRoot) {
|
|
|
132
140
|
return {
|
|
133
141
|
selected,
|
|
134
142
|
project: typeof record.project === "string" ? record.project : undefined,
|
|
135
|
-
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
|
|
143
|
+
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
|
|
144
|
+
serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
|
|
136
145
|
};
|
|
137
146
|
}
|
|
138
147
|
function writeRepoConnection(projectRoot, state) {
|
|
@@ -143,27 +152,37 @@ function resolveSelectedConnection(projectRoot, options = {}) {
|
|
|
143
152
|
if (!repo)
|
|
144
153
|
return null;
|
|
145
154
|
if (repo.selected === "local")
|
|
146
|
-
return { alias: "local", connection: { kind: "local", mode: "auto" } };
|
|
155
|
+
return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
|
|
147
156
|
const global = readGlobalConnections(options);
|
|
148
157
|
const connection = global.connections[repo.selected];
|
|
149
158
|
if (!connection) {
|
|
150
|
-
throw new
|
|
159
|
+
throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
|
|
151
160
|
}
|
|
152
|
-
return { alias: repo.selected, connection };
|
|
161
|
+
return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
|
|
162
|
+
}
|
|
163
|
+
function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
|
|
164
|
+
const repo = readRepoConnection(projectRoot);
|
|
165
|
+
if (!repo)
|
|
166
|
+
return;
|
|
167
|
+
writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
|
|
153
168
|
}
|
|
154
169
|
|
|
155
170
|
// packages/cli/src/commands/_server-client.ts
|
|
156
|
-
import { spawnSync } from "child_process";
|
|
157
171
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
158
172
|
import { resolve as resolve2 } from "path";
|
|
159
173
|
import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
|
|
160
|
-
var
|
|
174
|
+
var scopedGitHubBearerTokens = new Map;
|
|
175
|
+
var serverPhaseListener = null;
|
|
176
|
+
function reportServerPhase(label) {
|
|
177
|
+
serverPhaseListener?.(label);
|
|
178
|
+
}
|
|
161
179
|
function cleanToken(value) {
|
|
162
180
|
const trimmed = value?.trim();
|
|
163
181
|
return trimmed ? trimmed : null;
|
|
164
182
|
}
|
|
165
|
-
function setGitHubBearerTokenForCurrentProcess(token) {
|
|
166
|
-
|
|
183
|
+
function setGitHubBearerTokenForCurrentProcess(token, projectRoot) {
|
|
184
|
+
const scopedKey = resolve2(projectRoot ?? process.cwd());
|
|
185
|
+
scopedGitHubBearerTokens.set(scopedKey, cleanToken(token ?? undefined));
|
|
167
186
|
}
|
|
168
187
|
function readPrivateRemoteSessionToken(projectRoot) {
|
|
169
188
|
const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
|
|
@@ -177,49 +196,80 @@ function readPrivateRemoteSessionToken(projectRoot) {
|
|
|
177
196
|
}
|
|
178
197
|
}
|
|
179
198
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
180
|
-
|
|
181
|
-
|
|
199
|
+
const scopedKey = resolve2(projectRoot);
|
|
200
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
201
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
182
202
|
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
183
|
-
if (privateSession)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return
|
|
203
|
+
if (privateSession)
|
|
204
|
+
return privateSession;
|
|
205
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
206
|
+
}
|
|
207
|
+
function readStoredGitHubAuthToken(projectRoot) {
|
|
208
|
+
const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
|
|
209
|
+
if (!existsSync2(path))
|
|
210
|
+
return null;
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
213
|
+
return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
191
216
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
196
|
-
});
|
|
197
|
-
cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
|
|
198
|
-
return cachedGitHubBearerToken;
|
|
217
|
+
}
|
|
218
|
+
function readLocalConnectionFallbackToken(projectRoot) {
|
|
219
|
+
return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
|
|
199
220
|
}
|
|
200
221
|
async function ensureServerForCli(projectRoot) {
|
|
201
222
|
try {
|
|
202
223
|
const selected = resolveSelectedConnection(projectRoot);
|
|
203
224
|
if (selected?.connection.kind === "remote") {
|
|
225
|
+
reportServerPhase(`Connecting to ${selected.alias}\u2026`);
|
|
226
|
+
const authToken = readGitHubBearerTokenForRemote(projectRoot);
|
|
227
|
+
const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
|
|
204
228
|
return {
|
|
205
229
|
baseUrl: selected.connection.baseUrl,
|
|
206
|
-
authToken
|
|
207
|
-
connectionKind: "remote"
|
|
230
|
+
authToken,
|
|
231
|
+
connectionKind: "remote",
|
|
232
|
+
serverProjectRoot
|
|
208
233
|
};
|
|
209
234
|
}
|
|
235
|
+
reportServerPhase("Starting local Rig server\u2026");
|
|
210
236
|
const connection = await ensureLocalRigServerConnection(projectRoot);
|
|
211
237
|
return {
|
|
212
238
|
baseUrl: connection.baseUrl,
|
|
213
|
-
authToken: connection.authToken,
|
|
214
|
-
connectionKind: "local"
|
|
239
|
+
authToken: connection.authToken ?? readLocalConnectionFallbackToken(projectRoot),
|
|
240
|
+
connectionKind: "local",
|
|
241
|
+
serverProjectRoot: resolve2(projectRoot)
|
|
215
242
|
};
|
|
216
243
|
} catch (error) {
|
|
217
244
|
if (error instanceof Error) {
|
|
218
|
-
throw new
|
|
245
|
+
throw new CliError(error.message, 1);
|
|
219
246
|
}
|
|
220
247
|
throw error;
|
|
221
248
|
}
|
|
222
249
|
}
|
|
250
|
+
async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
|
|
251
|
+
const repo = readRepoConnection(projectRoot);
|
|
252
|
+
const slug = repo?.project?.trim();
|
|
253
|
+
if (!slug)
|
|
254
|
+
return null;
|
|
255
|
+
try {
|
|
256
|
+
const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
|
|
257
|
+
headers: mergeHeaders(undefined, authToken)
|
|
258
|
+
});
|
|
259
|
+
if (!response.ok)
|
|
260
|
+
return null;
|
|
261
|
+
const payload = await response.json();
|
|
262
|
+
const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
|
|
263
|
+
const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
|
|
264
|
+
const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
|
|
265
|
+
const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
|
|
266
|
+
if (path)
|
|
267
|
+
writeRepoServerProjectRoot(projectRoot, path);
|
|
268
|
+
return path;
|
|
269
|
+
} catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
223
273
|
function mergeHeaders(headers, authToken) {
|
|
224
274
|
const merged = new Headers(headers);
|
|
225
275
|
if (authToken) {
|
|
@@ -242,12 +292,65 @@ function diagnosticMessage(payload) {
|
|
|
242
292
|
});
|
|
243
293
|
return messages.length > 0 ? messages.join("; ") : null;
|
|
244
294
|
}
|
|
295
|
+
var serverReachabilityCache = new Map;
|
|
296
|
+
async function probeServerReachability(baseUrl, authToken) {
|
|
297
|
+
try {
|
|
298
|
+
const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
|
|
299
|
+
headers: mergeHeaders(undefined, authToken),
|
|
300
|
+
signal: AbortSignal.timeout(1500)
|
|
301
|
+
});
|
|
302
|
+
return response.ok;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function cachedServerReachability(projectRoot, baseUrl, authToken) {
|
|
308
|
+
const key = resolve2(projectRoot);
|
|
309
|
+
const cached = serverReachabilityCache.get(key);
|
|
310
|
+
if (cached)
|
|
311
|
+
return cached;
|
|
312
|
+
const probe = probeServerReachability(baseUrl, authToken);
|
|
313
|
+
serverReachabilityCache.set(key, probe);
|
|
314
|
+
return probe;
|
|
315
|
+
}
|
|
316
|
+
function describeSelectedServer(projectRoot, server) {
|
|
317
|
+
try {
|
|
318
|
+
const selected = resolveSelectedConnection(projectRoot);
|
|
319
|
+
if (selected) {
|
|
320
|
+
return {
|
|
321
|
+
alias: selected.alias,
|
|
322
|
+
target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
} catch {}
|
|
326
|
+
return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
|
|
327
|
+
}
|
|
328
|
+
async function buildServerFailureContext(projectRoot, server) {
|
|
329
|
+
const { alias, target } = describeSelectedServer(projectRoot, server);
|
|
330
|
+
const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
|
|
331
|
+
const reachability = reachable ? "server is reachable" : "server is unreachable";
|
|
332
|
+
return {
|
|
333
|
+
contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
|
|
334
|
+
hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
|
|
335
|
+
};
|
|
336
|
+
}
|
|
245
337
|
async function requestServerJson(context, pathname, init = {}) {
|
|
246
338
|
const server = await ensureServerForCli(context.projectRoot);
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
headers
|
|
250
|
-
});
|
|
339
|
+
const headers = mergeHeaders(init.headers, server.authToken);
|
|
340
|
+
if (server.serverProjectRoot)
|
|
341
|
+
headers.set("x-rig-project-root", server.serverProjectRoot);
|
|
342
|
+
reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
|
|
343
|
+
let response;
|
|
344
|
+
try {
|
|
345
|
+
response = await fetch(`${server.baseUrl}${pathname}`, {
|
|
346
|
+
...init,
|
|
347
|
+
headers
|
|
348
|
+
});
|
|
349
|
+
} catch (error) {
|
|
350
|
+
const failure = await buildServerFailureContext(context.projectRoot, server);
|
|
351
|
+
throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
|
|
352
|
+
${failure.contextLine}`, 1, { hint: failure.hint });
|
|
353
|
+
}
|
|
251
354
|
const text = await response.text();
|
|
252
355
|
const payload = text.trim().length > 0 ? (() => {
|
|
253
356
|
try {
|
|
@@ -259,7 +362,9 @@ async function requestServerJson(context, pathname, init = {}) {
|
|
|
259
362
|
if (!response.ok) {
|
|
260
363
|
const diagnostics = diagnosticMessage(payload);
|
|
261
364
|
const detail = diagnostics ?? (text || response.statusText);
|
|
262
|
-
|
|
365
|
+
const failure = await buildServerFailureContext(context.projectRoot, server);
|
|
366
|
+
throw new CliError(`Rig server request failed (${response.status}): ${detail}
|
|
367
|
+
${failure.contextLine}`, 1, { hint: failure.hint });
|
|
263
368
|
}
|
|
264
369
|
return payload;
|
|
265
370
|
}
|
|
@@ -290,39 +395,75 @@ async function registerProjectViaServer(context, input) {
|
|
|
290
395
|
function sleep(ms) {
|
|
291
396
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
292
397
|
}
|
|
398
|
+
function isRetryableProjectRootSwitchError(error) {
|
|
399
|
+
if (!(error instanceof Error))
|
|
400
|
+
return false;
|
|
401
|
+
const message = error.message.toLowerCase();
|
|
402
|
+
return message.includes("rig server request failed (401): auth-required") || message.includes("rig server request failed (401): github-token-required") || message.includes("rig server request failed (502)") || message.includes("rig server request failed (503)") || message.includes("bad gateway") || message.includes("fetch failed") || message.includes("econnrefused") || message.includes("connection refused");
|
|
403
|
+
}
|
|
293
404
|
async function switchServerProjectRootViaServer(context, projectRoot, options = {}) {
|
|
294
|
-
const switched = await requestServerJson(context, "/api/server/project-root", {
|
|
295
|
-
method: "POST",
|
|
296
|
-
headers: { "content-type": "application/json" },
|
|
297
|
-
body: JSON.stringify({ projectRoot })
|
|
298
|
-
});
|
|
299
405
|
const timeoutMs = options.timeoutMs ?? 30000;
|
|
300
406
|
const pollMs = options.pollMs ?? 1000;
|
|
301
407
|
const deadline = Date.now() + timeoutMs;
|
|
302
408
|
let lastError;
|
|
409
|
+
let switched = null;
|
|
303
410
|
while (Date.now() < deadline) {
|
|
304
411
|
try {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
lastError = `server projectRoot=${String(record.projectRoot ?? "unknown")}`;
|
|
312
|
-
}
|
|
412
|
+
switched = await requestServerJson(context, "/api/server/project-root", {
|
|
413
|
+
method: "POST",
|
|
414
|
+
headers: { "content-type": "application/json" },
|
|
415
|
+
body: JSON.stringify({ projectRoot })
|
|
416
|
+
});
|
|
417
|
+
break;
|
|
313
418
|
} catch (error) {
|
|
314
419
|
lastError = error;
|
|
420
|
+
if (!isRetryableProjectRootSwitchError(error))
|
|
421
|
+
throw error;
|
|
422
|
+
await sleep(pollMs);
|
|
315
423
|
}
|
|
316
|
-
await sleep(pollMs);
|
|
317
424
|
}
|
|
318
|
-
|
|
425
|
+
if (!switched) {
|
|
426
|
+
throw new CliError(`Rig server did not accept project-root switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no response")}).`, 1);
|
|
427
|
+
}
|
|
428
|
+
const record = switched && typeof switched === "object" && !Array.isArray(switched) ? switched : {};
|
|
429
|
+
if (record.ok === true) {
|
|
430
|
+
writeRepoServerProjectRoot(context.projectRoot, projectRoot);
|
|
431
|
+
return { ok: true, switched: record };
|
|
432
|
+
}
|
|
433
|
+
throw new CliError(`Rig server rejected project-root scope ${projectRoot}: ${String(record.message ?? record.error ?? "unknown error")}`, 1);
|
|
319
434
|
}
|
|
435
|
+
async function ensureTaskLabelsViaServer(context) {
|
|
436
|
+
const payload = await requestServerJson(context, "/api/workspace/task-labels", { method: "POST" });
|
|
437
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
438
|
+
}
|
|
439
|
+
async function listGitHubProjectsViaServer(context, owner) {
|
|
440
|
+
const url = new URL("http://rig.local/api/github/projects");
|
|
441
|
+
url.searchParams.set("owner", owner);
|
|
442
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
443
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { projects: [] };
|
|
444
|
+
}
|
|
445
|
+
async function getGitHubProjectStatusFieldViaServer(context, projectId) {
|
|
446
|
+
const payload = await requestServerJson(context, `/api/github/projects/${encodeURIComponent(projectId)}/status-field`);
|
|
447
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
448
|
+
}
|
|
449
|
+
var RESUMABLE_RUN_STATUSES = new Set([
|
|
450
|
+
"created",
|
|
451
|
+
"preparing",
|
|
452
|
+
"running",
|
|
453
|
+
"validating",
|
|
454
|
+
"reviewing",
|
|
455
|
+
"stopped",
|
|
456
|
+
"failed",
|
|
457
|
+
"needs-attention",
|
|
458
|
+
"needs_attention"
|
|
459
|
+
]);
|
|
320
460
|
|
|
321
461
|
// packages/cli/src/commands/_pi-install.ts
|
|
322
462
|
import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
|
|
323
463
|
import { homedir as homedir2 } from "os";
|
|
324
464
|
import { resolve as resolve3 } from "path";
|
|
325
|
-
var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
|
|
465
|
+
var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
|
|
466
|
+
var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
|
|
326
467
|
var LEGACY_PI_RIG_MARKER = `// Managed by Rig. Source package: @rig/pi-rig.
|
|
327
468
|
export { default } from '@rig/pi-rig';
|
|
328
469
|
`;
|
|
@@ -350,7 +491,7 @@ function resolvePiHomeDir(inputHomeDir) {
|
|
|
350
491
|
function piListContainsPiRig(output) {
|
|
351
492
|
return output.split(/\r?\n/).some((line) => {
|
|
352
493
|
const normalized = line.trim();
|
|
353
|
-
return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
|
|
494
|
+
return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
|
|
354
495
|
});
|
|
355
496
|
}
|
|
356
497
|
async function safeRun(runner, command, options) {
|
|
@@ -466,7 +607,7 @@ async function ensureRemotePiRigInstalled(input) {
|
|
|
466
607
|
const payload = await input.requestJson("/api/pi-rig/install", {
|
|
467
608
|
method: "POST",
|
|
468
609
|
headers: { "content-type": "application/json" },
|
|
469
|
-
body: JSON.stringify({ package:
|
|
610
|
+
body: JSON.stringify({ package: PI_RIG_PACKAGE_NAME, scope: "global" })
|
|
470
611
|
});
|
|
471
612
|
const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
472
613
|
const piOk = record.piOk === true || record.ok === true;
|
|
@@ -719,7 +860,10 @@ async function runRigDoctorChecks(options) {
|
|
|
719
860
|
const bunVersion = options.bunVersion ?? Bun.version;
|
|
720
861
|
const request = options.requestJson ?? ((pathname, init) => requestServerJson({ projectRoot }, pathname, init));
|
|
721
862
|
const loadConfig = options.loadConfig ?? loadRigConfigOrNull;
|
|
863
|
+
const progress = options.onProgress ?? (() => {});
|
|
864
|
+
progress("Checking local toolchain\u2026");
|
|
722
865
|
checks.push(check("bun", `bun >= ${MIN_SUPPORTED_BUN_VERSION}`, isSupportedBunVersion(bunVersion) ? "pass" : "fail", `found ${bunVersion}`, `Install Bun ${MIN_SUPPORTED_BUN_VERSION} or newer.`), check("git", "git", which("git") ? "pass" : "fail", which("git") ?? undefined, "Install git and ensure it is on PATH."), check("jq", "jq", which("jq") ? "pass" : "warn", which("jq") ?? undefined, "Install jq (for example `brew install jq`)."));
|
|
866
|
+
progress("Loading rig.config\u2026");
|
|
723
867
|
const loadedConfig = await loadConfig(projectRoot).catch(() => null);
|
|
724
868
|
const config = loadedConfig ?? loadFallbackConfig(projectRoot);
|
|
725
869
|
const hasConfigFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync4(resolve5(projectRoot, name)));
|
|
@@ -727,7 +871,7 @@ async function runRigDoctorChecks(options) {
|
|
|
727
871
|
const taskSourceKind = config?.taskSource?.kind;
|
|
728
872
|
checks.push(taskSourceKind ? check("task-source", "task source configured", "pass", taskSourceKind) : check("task-source", "task source configured", "fail", "missing taskSource", "Configure taskSource in rig.config.ts."));
|
|
729
873
|
const repo = readRepoConnection(projectRoot);
|
|
730
|
-
checks.push(repo ? check("project-link", "repo selected Rig
|
|
874
|
+
checks.push(repo ? check("project-link", "repo selected Rig server", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to link this checkout to a GitHub repo slug.") : check("project-link", "repo selected Rig server", "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig server use <alias|local>`."));
|
|
731
875
|
const selected = (() => {
|
|
732
876
|
try {
|
|
733
877
|
return resolveSelectedConnection(projectRoot);
|
|
@@ -735,9 +879,10 @@ async function runRigDoctorChecks(options) {
|
|
|
735
879
|
return null;
|
|
736
880
|
}
|
|
737
881
|
})();
|
|
738
|
-
checks.push(selected ? check("connection", "selected server connection", "pass", selected.connection.kind === "remote" ? selected.connection.baseUrl : "local auto") : check("connection", "selected server
|
|
882
|
+
checks.push(selected ? check("connection", "selected server connection", "pass", selected.connection.kind === "remote" ? selected.connection.baseUrl : "local auto") : check("connection", "selected server", repo ? "fail" : "warn", repo ? "selected alias is missing" : "will auto-start local server", repo ? "Run `rig server list` and `rig server use <alias|local>`." : undefined));
|
|
739
883
|
let server = null;
|
|
740
884
|
try {
|
|
885
|
+
progress("Connecting to the selected Rig server\u2026");
|
|
741
886
|
server = await (options.resolveServer ?? ensureServerForCli)(projectRoot);
|
|
742
887
|
checks.push(check("server", "Rig server reachable", "pass", `${server.connectionKind} ${server.baseUrl}`));
|
|
743
888
|
} catch (error) {
|
|
@@ -745,18 +890,21 @@ async function runRigDoctorChecks(options) {
|
|
|
745
890
|
}
|
|
746
891
|
if (server || options.requestJson) {
|
|
747
892
|
try {
|
|
893
|
+
progress("Checking server status\u2026");
|
|
748
894
|
const status = await request("/api/server/status");
|
|
749
895
|
checks.push(check("server-status", "server project status", "pass", JSON.stringify(status).slice(0, 180)));
|
|
750
896
|
} catch (error) {
|
|
751
897
|
checks.push(check("server-status", "server project status", "fail", errorMessage(error), "Run `rig doctor` after the selected server is reachable."));
|
|
752
898
|
}
|
|
753
899
|
try {
|
|
900
|
+
progress("Checking GitHub auth\u2026");
|
|
754
901
|
const auth = await request("/api/github/auth/status");
|
|
755
902
|
checks.push(isAuthenticated(auth) ? check("github-auth", "GitHub auth", "pass") : check("github-auth", "GitHub auth", "fail", "not authenticated", "Run `rig github auth import-gh` or `rig github auth token --token <token>`."));
|
|
756
903
|
} catch (error) {
|
|
757
904
|
checks.push(check("github-auth", "GitHub auth", "fail", errorMessage(error), "Authenticate GitHub through Rig and ensure the server exposes auth status."));
|
|
758
905
|
}
|
|
759
906
|
try {
|
|
907
|
+
progress("Checking GitHub repo permissions\u2026");
|
|
760
908
|
const permissions = await request("/api/github/repo/permissions");
|
|
761
909
|
const allowed = permissionAllowsPr(permissions);
|
|
762
910
|
checks.push(allowed === true ? check("github-repo-permissions", "GitHub repo PR permissions", "pass", JSON.stringify(permissions).slice(0, 180)) : allowed === false ? check("github-repo-permissions", "GitHub repo PR permissions", "fail", JSON.stringify(permissions).slice(0, 180), "Grant the selected GitHub token permission to push branches, open PRs, and merge according to repo rules.") : check("github-repo-permissions", "GitHub repo PR permissions", "warn", JSON.stringify(permissions).slice(0, 180), "Confirm the selected token can push branches and open PRs."));
|
|
@@ -764,6 +912,7 @@ async function runRigDoctorChecks(options) {
|
|
|
764
912
|
checks.push(check("github-repo-permissions", "GitHub repo PR permissions", "warn", errorMessage(error), "Ensure the server exposes repo permission checks and the token can open PRs."));
|
|
765
913
|
}
|
|
766
914
|
try {
|
|
915
|
+
progress("Checking GitHub issue labels\u2026");
|
|
767
916
|
const labels = await request("/api/workspace/task-labels");
|
|
768
917
|
const ready = labelsReady(labels);
|
|
769
918
|
checks.push(ready === false ? check("task-labels", "GitHub issue labels", "fail", JSON.stringify(labels).slice(0, 180), "Let Rig create required labels or create the configured lifecycle labels manually.") : check("task-labels", "GitHub issue labels", ready === true ? "pass" : "warn", JSON.stringify(labels).slice(0, 180), "Confirm required Rig lifecycle labels exist."));
|
|
@@ -771,6 +920,7 @@ async function runRigDoctorChecks(options) {
|
|
|
771
920
|
checks.push(check("task-labels", "GitHub issue labels", "warn", errorMessage(error), "Run `rig init`/`rig doctor` after label setup is wired on the server."));
|
|
772
921
|
}
|
|
773
922
|
try {
|
|
923
|
+
progress("Checking task projection\u2026");
|
|
774
924
|
const projection = await request("/api/workspace/task-projection");
|
|
775
925
|
checks.push(check("task-projection", "task projection", "pass", JSON.stringify(projection).slice(0, 180)));
|
|
776
926
|
} catch (error) {
|
|
@@ -779,6 +929,7 @@ async function runRigDoctorChecks(options) {
|
|
|
779
929
|
const slug = projectStatusSlug(projectRoot, config);
|
|
780
930
|
if (slug) {
|
|
781
931
|
try {
|
|
932
|
+
progress("Checking server project checkout\u2026");
|
|
782
933
|
const project = await request(`/api/projects/${encodeURIComponent(slug)}`);
|
|
783
934
|
checks.push(check("remote-checkout", "server project checkout", "pass", JSON.stringify(project).slice(0, 180)));
|
|
784
935
|
} catch (error) {
|
|
@@ -793,6 +944,7 @@ async function runRigDoctorChecks(options) {
|
|
|
793
944
|
}
|
|
794
945
|
checks.push(githubProjectsCheck(config));
|
|
795
946
|
checks.push(prMergeCheck(config));
|
|
947
|
+
progress("Checking Pi installation\u2026");
|
|
796
948
|
const piChecks = await (options.piChecks ?? (() => buildPiSetupChecks()))().catch((error) => [{
|
|
797
949
|
ok: false,
|
|
798
950
|
label: "pi/pi-rig checks",
|
|
@@ -809,6 +961,7 @@ function countDoctorFailures(checks) {
|
|
|
809
961
|
|
|
810
962
|
// packages/cli/src/commands/init.ts
|
|
811
963
|
var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
|
|
964
|
+
var DEFAULT_REMOTE_RIG_URL = "https://where.rig-does.work";
|
|
812
965
|
var RIG_CONFIG_DEV_DEPENDENCIES = {
|
|
813
966
|
"@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
|
|
814
967
|
"@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
|
|
@@ -819,7 +972,7 @@ function parseRepoSlugFromRemote(remoteUrl) {
|
|
|
819
972
|
return gitHubMatch ? `${gitHubMatch[1]}/${gitHubMatch[2]}` : null;
|
|
820
973
|
}
|
|
821
974
|
function detectOriginRepoSlug(projectRoot) {
|
|
822
|
-
const result =
|
|
975
|
+
const result = spawnSync("git", ["-C", projectRoot, "remote", "get-url", "origin"], { encoding: "utf8" });
|
|
823
976
|
if (result.status !== 0)
|
|
824
977
|
return null;
|
|
825
978
|
return parseRepoSlugFromRemote(result.stdout.trim());
|
|
@@ -827,7 +980,7 @@ function detectOriginRepoSlug(projectRoot) {
|
|
|
827
980
|
function parseRepoSlug(value) {
|
|
828
981
|
const match = value.trim().match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
829
982
|
if (!match)
|
|
830
|
-
throw new
|
|
983
|
+
throw new CliError(`Invalid GitHub repo slug: ${value}. Expected owner/repo.`, 1, { hint: "Pass the slug as owner/repo, e.g. `rig init --repo acme/widgets`." });
|
|
831
984
|
return { owner: match[1], repo: match[2], slug: `${match[1]}/${match[2]}` };
|
|
832
985
|
}
|
|
833
986
|
function ensureRigPrivateDirs(projectRoot) {
|
|
@@ -875,11 +1028,14 @@ function applyGitHubProjectConfig(source, options) {
|
|
|
875
1028
|
return source;
|
|
876
1029
|
const projectId = JSON.stringify(options.githubProject);
|
|
877
1030
|
const statusFieldId = JSON.stringify(options.githubProjectStatusField ?? "Status");
|
|
1031
|
+
const statuses = options.githubProjectStatuses && Object.keys(options.githubProjectStatuses).length > 0 ? `
|
|
1032
|
+
statuses: ${JSON.stringify(options.githubProjectStatuses, null, 8).replace(/\n/g, `
|
|
1033
|
+
`)},` : "";
|
|
878
1034
|
return source.replace(` projects: { enabled: false },`, [
|
|
879
1035
|
` projects: {`,
|
|
880
1036
|
` enabled: true,`,
|
|
881
1037
|
` projectId: ${projectId},`,
|
|
882
|
-
` statusFieldId: ${statusFieldId}
|
|
1038
|
+
` statusFieldId: ${statusFieldId},${statuses}`,
|
|
883
1039
|
` },`
|
|
884
1040
|
].join(`
|
|
885
1041
|
`));
|
|
@@ -900,40 +1056,218 @@ function checkoutForInit(projectRoot, serverKind, strategy) {
|
|
|
900
1056
|
}
|
|
901
1057
|
}
|
|
902
1058
|
function detectGhLogin() {
|
|
903
|
-
const result =
|
|
1059
|
+
const result = spawnSync("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
|
|
904
1060
|
return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : null;
|
|
905
1061
|
}
|
|
906
1062
|
function readGhAuthToken() {
|
|
907
|
-
const result =
|
|
1063
|
+
const result = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
|
|
908
1064
|
if (result.status !== 0 || !result.stdout.trim()) {
|
|
909
|
-
throw new
|
|
1065
|
+
throw new CliError(result.stderr.trim() || "Could not read GitHub token from `gh auth token`.", result.status || 1, { hint: "Sign in with `gh auth login`, or pass a token directly: `rig init --github-auth token --github-token <token>`." });
|
|
910
1066
|
}
|
|
911
1067
|
return result.stdout.trim();
|
|
912
1068
|
}
|
|
1069
|
+
function refreshGhProjectScopesAndReadToken() {
|
|
1070
|
+
const result = spawnSync("gh", ["auth", "refresh", "--scopes", "read:project"], {
|
|
1071
|
+
encoding: "utf8",
|
|
1072
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
1073
|
+
});
|
|
1074
|
+
if (result.status !== 0)
|
|
1075
|
+
return null;
|
|
1076
|
+
try {
|
|
1077
|
+
return readGhAuthToken();
|
|
1078
|
+
} catch {
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
913
1082
|
async function loadClackPrompts() {
|
|
914
1083
|
return await import("@clack/prompts");
|
|
915
1084
|
}
|
|
1085
|
+
function clackTextOptions(options) {
|
|
1086
|
+
return {
|
|
1087
|
+
message: options.message,
|
|
1088
|
+
...options.placeholder ? { placeholder: options.placeholder } : {},
|
|
1089
|
+
...(options.initialValue ?? options.defaultValue)?.trim() ? { initialValue: (options.initialValue ?? options.defaultValue).trim() } : {}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
916
1092
|
async function promptRequiredText(prompts, options) {
|
|
917
|
-
const value = await prompts.text(options);
|
|
1093
|
+
const value = await prompts.text(clackTextOptions(options));
|
|
918
1094
|
if (prompts.isCancel(value))
|
|
919
|
-
throw new
|
|
1095
|
+
throw new CliError("Init cancelled.", 1);
|
|
920
1096
|
const text = String(value ?? "").trim();
|
|
921
1097
|
if (!text)
|
|
922
|
-
throw new
|
|
1098
|
+
throw new CliError(`${options.message} is required.`, 1);
|
|
923
1099
|
return text;
|
|
924
1100
|
}
|
|
925
1101
|
async function promptOptionalText(prompts, options) {
|
|
926
|
-
const value = await prompts.text(options);
|
|
1102
|
+
const value = await prompts.text(clackTextOptions(options));
|
|
927
1103
|
if (prompts.isCancel(value))
|
|
928
|
-
throw new
|
|
1104
|
+
throw new CliError("Init cancelled.", 1);
|
|
929
1105
|
return String(value ?? "").trim();
|
|
930
1106
|
}
|
|
931
1107
|
async function promptSelect(prompts, options) {
|
|
932
1108
|
const value = await prompts.select(options);
|
|
933
1109
|
if (prompts.isCancel(value))
|
|
934
|
-
throw new
|
|
1110
|
+
throw new CliError("Init cancelled.", 1);
|
|
935
1111
|
return String(value);
|
|
936
1112
|
}
|
|
1113
|
+
function repoOwnerFromSlug(repoSlug) {
|
|
1114
|
+
return repoSlug.trim().match(/^([^/]+)\/[^/]+$/)?.[1] ?? null;
|
|
1115
|
+
}
|
|
1116
|
+
function recordArray(value, key) {
|
|
1117
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1118
|
+
return [];
|
|
1119
|
+
const raw = value[key];
|
|
1120
|
+
return Array.isArray(raw) ? raw.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1121
|
+
}
|
|
1122
|
+
async function listGitHubProjectsForInit(context, owner, token) {
|
|
1123
|
+
if (token?.trim()) {
|
|
1124
|
+
try {
|
|
1125
|
+
return { ok: true, projects: await listGitHubProjectsDirect({ owner, token: token.trim() }) };
|
|
1126
|
+
} catch (directError) {
|
|
1127
|
+
const serverPayload = await listGitHubProjectsViaServer(context, owner).catch(() => null);
|
|
1128
|
+
if (recordArray(serverPayload, "projects").length > 0)
|
|
1129
|
+
return serverPayload;
|
|
1130
|
+
return { ok: false, error: directError instanceof Error ? directError.message : String(directError), projects: [] };
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return listGitHubProjectsViaServer(context, owner);
|
|
1134
|
+
}
|
|
1135
|
+
async function getGitHubProjectStatusFieldForInit(context, projectId, token) {
|
|
1136
|
+
if (token?.trim()) {
|
|
1137
|
+
try {
|
|
1138
|
+
return { ok: true, field: await resolveProjectStatusFieldDirect({ projectId, token: token.trim() }) };
|
|
1139
|
+
} catch (directError) {
|
|
1140
|
+
const serverPayload = await getGitHubProjectStatusFieldViaServer(context, projectId).catch(() => null);
|
|
1141
|
+
if (serverPayload && typeof serverPayload === "object" && !Array.isArray(serverPayload) && "field" in serverPayload) {
|
|
1142
|
+
return serverPayload;
|
|
1143
|
+
}
|
|
1144
|
+
return { ok: false, error: directError instanceof Error ? directError.message : String(directError) };
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return getGitHubProjectStatusFieldViaServer(context, projectId);
|
|
1148
|
+
}
|
|
1149
|
+
var PROJECT_STATUS_PROMPTS = {
|
|
1150
|
+
running: "Running/In progress",
|
|
1151
|
+
prOpen: "PR open/review",
|
|
1152
|
+
ciFixing: "CI/review fixing",
|
|
1153
|
+
merging: "Merging",
|
|
1154
|
+
done: "Done",
|
|
1155
|
+
needsAttention: "Needs attention"
|
|
1156
|
+
};
|
|
1157
|
+
var DEFAULT_PROJECT_STATUS_OPTIONS = {
|
|
1158
|
+
running: "In Progress",
|
|
1159
|
+
prOpen: "In Review",
|
|
1160
|
+
ciFixing: "In Review",
|
|
1161
|
+
merging: "Merging",
|
|
1162
|
+
done: "Done",
|
|
1163
|
+
needsAttention: "Needs Attention"
|
|
1164
|
+
};
|
|
1165
|
+
async function promptManualProjectStatusMapping(prompts) {
|
|
1166
|
+
const statuses = {};
|
|
1167
|
+
for (const [key, label] of Object.entries(PROJECT_STATUS_PROMPTS)) {
|
|
1168
|
+
const defaultLabel = DEFAULT_PROJECT_STATUS_OPTIONS[key] ?? label;
|
|
1169
|
+
const value = await promptOptionalText(prompts, {
|
|
1170
|
+
message: `Project status option id/name for ${label} (blank for ${defaultLabel})`,
|
|
1171
|
+
placeholder: defaultLabel
|
|
1172
|
+
});
|
|
1173
|
+
statuses[key] = value || defaultLabel;
|
|
1174
|
+
}
|
|
1175
|
+
return statuses;
|
|
1176
|
+
}
|
|
1177
|
+
function projectScopeError(value) {
|
|
1178
|
+
const text = typeof value === "string" ? value : JSON.stringify(value ?? "");
|
|
1179
|
+
return /INSUFFICIENT_SCOPES|read:project|required scopes/i.test(text);
|
|
1180
|
+
}
|
|
1181
|
+
function optionName(option) {
|
|
1182
|
+
return String(option.name ?? option.label ?? option.id ?? "").trim();
|
|
1183
|
+
}
|
|
1184
|
+
function autoProjectStatusValue(options, key, label) {
|
|
1185
|
+
const candidates = [DEFAULT_PROJECT_STATUS_OPTIONS[key], label].filter((value) => Boolean(value)).map((value) => value.trim().toLowerCase());
|
|
1186
|
+
const match = options.find((option) => candidates.includes(optionName(option).toLowerCase()));
|
|
1187
|
+
if (!match)
|
|
1188
|
+
return null;
|
|
1189
|
+
return String(match.id ?? match.name);
|
|
1190
|
+
}
|
|
1191
|
+
async function promptGitHubProjectConfig(context, prompts, repoSlug, githubToken, refreshProjectToken) {
|
|
1192
|
+
const projectChoice = await promptSelect(prompts, {
|
|
1193
|
+
message: "GitHub Projects status sync",
|
|
1194
|
+
initialValue: "select",
|
|
1195
|
+
options: [
|
|
1196
|
+
{ value: "select", label: "Select accessible ProjectV2" },
|
|
1197
|
+
{ value: "off", label: "Off" },
|
|
1198
|
+
{ value: "manual", label: "Enter ProjectV2 ids manually" }
|
|
1199
|
+
]
|
|
1200
|
+
});
|
|
1201
|
+
if (projectChoice === "off")
|
|
1202
|
+
return { githubProject: "off" };
|
|
1203
|
+
if (projectChoice === "manual") {
|
|
1204
|
+
return {
|
|
1205
|
+
githubProject: await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }),
|
|
1206
|
+
githubProjectStatusField: await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" }),
|
|
1207
|
+
githubProjectStatuses: await promptManualProjectStatusMapping(prompts)
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
const owner = repoOwnerFromSlug(repoSlug);
|
|
1211
|
+
if (!owner)
|
|
1212
|
+
throw new CliError(`Cannot derive GitHub owner from repo slug ${repoSlug}.`, 1, { hint: "Pass the slug as owner/repo, e.g. `rig init --repo acme/widgets`." });
|
|
1213
|
+
let activeToken = githubToken?.trim() || null;
|
|
1214
|
+
let projectsPayload = await listGitHubProjectsForInit(context, owner, activeToken).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), projects: [] }));
|
|
1215
|
+
let projects = recordArray(projectsPayload, "projects");
|
|
1216
|
+
if (projects.length === 0 && projectScopeError(projectsPayload.error) && refreshProjectToken) {
|
|
1217
|
+
prompts.outro?.("GitHub token is missing read:project; refreshing gh auth scopes and retrying Projects.");
|
|
1218
|
+
const refreshedToken = refreshProjectToken();
|
|
1219
|
+
if (refreshedToken) {
|
|
1220
|
+
activeToken = refreshedToken;
|
|
1221
|
+
projectsPayload = await listGitHubProjectsForInit(context, owner, activeToken).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), projects: [] }));
|
|
1222
|
+
projects = recordArray(projectsPayload, "projects");
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (projects.length === 0) {
|
|
1226
|
+
const error = typeof projectsPayload.error === "string" ? ` (${String(projectsPayload.error).replace(/\s+/g, " ").slice(0, 240)})` : "";
|
|
1227
|
+
prompts.outro?.(`No accessible GitHub Projects were returned${error}; continuing with GitHub Projects status sync off.`);
|
|
1228
|
+
return { githubProject: "off", ...activeToken ? { githubToken: activeToken } : {} };
|
|
1229
|
+
}
|
|
1230
|
+
const selectedProjectId = await promptSelect(prompts, {
|
|
1231
|
+
message: "GitHub ProjectV2 project",
|
|
1232
|
+
options: [
|
|
1233
|
+
...projects.map((project) => ({
|
|
1234
|
+
value: String(project.id),
|
|
1235
|
+
label: `${String(project.title ?? "Untitled project")} (#${String(project.number ?? "?")})`,
|
|
1236
|
+
hint: typeof project.url === "string" ? project.url : undefined
|
|
1237
|
+
})),
|
|
1238
|
+
{ value: "manual", label: "Enter ProjectV2 id manually" }
|
|
1239
|
+
]
|
|
1240
|
+
});
|
|
1241
|
+
const projectId = selectedProjectId === "manual" ? await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }) : selectedProjectId;
|
|
1242
|
+
const fieldPayload = await getGitHubProjectStatusFieldForInit(context, projectId, activeToken).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
1243
|
+
const fieldPayloadRecord = fieldPayload && typeof fieldPayload === "object" && !Array.isArray(fieldPayload) ? fieldPayload : {};
|
|
1244
|
+
const rawField = fieldPayloadRecord.field;
|
|
1245
|
+
const field = rawField && typeof rawField === "object" && !Array.isArray(rawField) ? rawField : null;
|
|
1246
|
+
const fieldId = typeof field?.id === "string" && field.id.trim() ? field.id : await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" });
|
|
1247
|
+
const options = Array.isArray(field?.options) ? field.options.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1248
|
+
if (options.length === 0) {
|
|
1249
|
+
return {
|
|
1250
|
+
githubProject: projectId,
|
|
1251
|
+
githubProjectStatusField: fieldId,
|
|
1252
|
+
githubProjectStatuses: await promptManualProjectStatusMapping(prompts),
|
|
1253
|
+
...activeToken ? { githubToken: activeToken } : {}
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
const statuses = {};
|
|
1257
|
+
for (const [key, label] of Object.entries(PROJECT_STATUS_PROMPTS)) {
|
|
1258
|
+
const auto = autoProjectStatusValue(options, key, label);
|
|
1259
|
+
statuses[key] = auto ?? await promptSelect(prompts, {
|
|
1260
|
+
message: `Project status option for ${label}`,
|
|
1261
|
+
options: options.map((option) => ({ value: String(option.id ?? option.name), label: optionName(option) }))
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
return {
|
|
1265
|
+
githubProject: projectId,
|
|
1266
|
+
githubProjectStatusField: fieldId,
|
|
1267
|
+
githubProjectStatuses: Object.keys(statuses).length > 0 ? statuses : undefined,
|
|
1268
|
+
...activeToken ? { githubToken: activeToken } : {}
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
937
1271
|
function sleep2(ms) {
|
|
938
1272
|
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
939
1273
|
}
|
|
@@ -947,12 +1281,29 @@ function apiSessionTokenFrom(payload) {
|
|
|
947
1281
|
const token = payload.apiSessionToken;
|
|
948
1282
|
return typeof token === "string" && token.trim() ? token.trim() : null;
|
|
949
1283
|
}
|
|
1284
|
+
function cleanPayloadString(value) {
|
|
1285
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1286
|
+
}
|
|
1287
|
+
function remoteGitHubAuthMetadata(payload) {
|
|
1288
|
+
if (!payload)
|
|
1289
|
+
return {};
|
|
1290
|
+
const userNamespace = payload.userNamespace && typeof payload.userNamespace === "object" && !Array.isArray(payload.userNamespace) ? payload.userNamespace : null;
|
|
1291
|
+
return {
|
|
1292
|
+
...cleanPayloadString(payload.login) ? { login: cleanPayloadString(payload.login) } : {},
|
|
1293
|
+
...cleanPayloadString(payload.userId) ? { userId: cleanPayloadString(payload.userId) } : {},
|
|
1294
|
+
...cleanPayloadString(userNamespace?.key) ? { userNamespaceKey: cleanPayloadString(userNamespace?.key) } : {},
|
|
1295
|
+
...cleanPayloadString(userNamespace?.root) ? { userNamespaceRoot: cleanPayloadString(userNamespace?.root) } : {},
|
|
1296
|
+
...cleanPayloadString(userNamespace?.checkoutBaseDir) ? { checkoutBaseDir: cleanPayloadString(userNamespace?.checkoutBaseDir) } : {},
|
|
1297
|
+
...cleanPayloadString(userNamespace?.snapshotBaseDir) ? { snapshotBaseDir: cleanPayloadString(userNamespace?.snapshotBaseDir) } : {}
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
950
1300
|
function writeRemoteGitHubAuthState(projectRoot, input) {
|
|
951
1301
|
writeFileSync2(resolve6(projectRoot, ".rig", "state", "github-auth.json"), `${JSON.stringify({
|
|
952
1302
|
authenticated: true,
|
|
953
1303
|
source: input.source,
|
|
954
1304
|
storedOnServer: true,
|
|
955
1305
|
selectedRepo: input.selectedRepo,
|
|
1306
|
+
...remoteGitHubAuthMetadata(input.authPayload ?? null),
|
|
956
1307
|
...input.apiSessionToken ? { apiSessionToken: input.apiSessionToken } : {},
|
|
957
1308
|
updatedAt: new Date().toISOString()
|
|
958
1309
|
}, null, 2)}
|
|
@@ -983,18 +1334,149 @@ async function pollDeviceAuthUntilComplete(context, pollId, firstPayload) {
|
|
|
983
1334
|
} while (Date.now() < deadline);
|
|
984
1335
|
return last;
|
|
985
1336
|
}
|
|
1337
|
+
function runLocalFilesInit(context, options) {
|
|
1338
|
+
const projectRoot = context.projectRoot;
|
|
1339
|
+
ensureRigPrivateDirs(projectRoot);
|
|
1340
|
+
ensureGitignoreEntries(projectRoot);
|
|
1341
|
+
writeRepoConnection(projectRoot, { selected: "local", linkedAt: new Date().toISOString() });
|
|
1342
|
+
const configTsPath = resolve6(projectRoot, "rig.config.ts");
|
|
1343
|
+
const configExists = existsSync5(configTsPath) || existsSync5(resolve6(projectRoot, "rig.config.json"));
|
|
1344
|
+
if (configExists && !options.repair) {
|
|
1345
|
+
if (context.outputMode !== "json")
|
|
1346
|
+
console.log("rig.config already exists; leaving it unchanged. Pass --repair to rewrite it.");
|
|
1347
|
+
} else {
|
|
1348
|
+
const projectName = basename(projectRoot) || "rig-project";
|
|
1349
|
+
writeFileSync2(configTsPath, buildRigInitConfigSource({
|
|
1350
|
+
projectName,
|
|
1351
|
+
taskSource: { kind: "files", path: "tasks" },
|
|
1352
|
+
useStandardPlugin: true
|
|
1353
|
+
}), "utf-8");
|
|
1354
|
+
}
|
|
1355
|
+
ensureRigConfigPackageDependencies(projectRoot);
|
|
1356
|
+
const tasksDir = resolve6(projectRoot, "tasks");
|
|
1357
|
+
if (!existsSync5(tasksDir)) {
|
|
1358
|
+
mkdirSync2(tasksDir, { recursive: true });
|
|
1359
|
+
writeFileSync2(resolve6(tasksDir, "T-1.json"), `${JSON.stringify({ id: "T-1", title: "My first Rig task", body: "Describe the change you want an agent to make." }, null, 2)}
|
|
1360
|
+
`, "utf-8");
|
|
1361
|
+
}
|
|
1362
|
+
if (context.outputMode !== "json") {
|
|
1363
|
+
console.log("Initialized a local files-source Rig project (no GitHub).");
|
|
1364
|
+
console.log(" tasks live in tasks/*.json \xB7 server: local");
|
|
1365
|
+
console.log("Next: `rig task list`, then `rig task run T-1`.");
|
|
1366
|
+
console.log("To wire GitHub later: `rig init --repair --repo owner/repo --github-auth gh`.");
|
|
1367
|
+
}
|
|
1368
|
+
return { ok: true, group: "init", command: "init", details: { mode: "local-files", projectRoot, taskSourcePath: "tasks" } };
|
|
1369
|
+
}
|
|
1370
|
+
var DEMO_TASKS_RELATIVE_DIR = ".rig/demo-tasks";
|
|
1371
|
+
var DEMO_TASKS = [
|
|
1372
|
+
{
|
|
1373
|
+
id: "demo-1",
|
|
1374
|
+
title: "Add a hello CLI script",
|
|
1375
|
+
body: [
|
|
1376
|
+
"Create `scripts/hello.ts` that prints `Hello from Rig!` plus the current date,",
|
|
1377
|
+
"and add a `hello` script entry to package.json that runs it with bun.",
|
|
1378
|
+
"Keep it dependency-free."
|
|
1379
|
+
].join(`
|
|
1380
|
+
`),
|
|
1381
|
+
status: "ready",
|
|
1382
|
+
labels: ["demo"]
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
id: "demo-2",
|
|
1386
|
+
title: "Write a README section about this project",
|
|
1387
|
+
body: [
|
|
1388
|
+
"Add (or extend) README.md with a short `## What this is` section:",
|
|
1389
|
+
"two or three sentences describing the repository and how to run it.",
|
|
1390
|
+
"Plain prose, no badges."
|
|
1391
|
+
].join(`
|
|
1392
|
+
`),
|
|
1393
|
+
status: "ready",
|
|
1394
|
+
labels: ["demo"]
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
id: "demo-3",
|
|
1398
|
+
title: "Add a unit test for the hello script",
|
|
1399
|
+
body: [
|
|
1400
|
+
"Add `scripts/hello.test.ts` with a bun test that imports the greeting",
|
|
1401
|
+
"helper from the hello script and asserts it contains `Hello from Rig!`.",
|
|
1402
|
+
"Refactor the script to export that helper if needed."
|
|
1403
|
+
].join(`
|
|
1404
|
+
`),
|
|
1405
|
+
status: "ready",
|
|
1406
|
+
labels: ["demo"]
|
|
1407
|
+
}
|
|
1408
|
+
];
|
|
1409
|
+
function runDemoInit(context, options) {
|
|
1410
|
+
const projectRoot = context.projectRoot;
|
|
1411
|
+
ensureRigPrivateDirs(projectRoot);
|
|
1412
|
+
ensureGitignoreEntries(projectRoot);
|
|
1413
|
+
writeRepoConnection(projectRoot, { selected: "local", linkedAt: new Date().toISOString() });
|
|
1414
|
+
const configTsPath = resolve6(projectRoot, "rig.config.ts");
|
|
1415
|
+
const configExists = existsSync5(configTsPath) || existsSync5(resolve6(projectRoot, "rig.config.json"));
|
|
1416
|
+
let configWritten = false;
|
|
1417
|
+
if (configExists && !options.repair) {
|
|
1418
|
+
if (context.outputMode !== "json") {
|
|
1419
|
+
console.log("rig.config already exists; leaving it unchanged. Pass --repair to rewrite it for the demo.");
|
|
1420
|
+
}
|
|
1421
|
+
} else {
|
|
1422
|
+
writeFileSync2(configTsPath, buildRigInitConfigSource({
|
|
1423
|
+
projectName: basename(projectRoot) || "rig-demo",
|
|
1424
|
+
taskSource: { kind: "files", path: DEMO_TASKS_RELATIVE_DIR },
|
|
1425
|
+
useStandardPlugin: true
|
|
1426
|
+
}), "utf-8");
|
|
1427
|
+
configWritten = true;
|
|
1428
|
+
}
|
|
1429
|
+
ensureRigConfigPackageDependencies(projectRoot);
|
|
1430
|
+
const demoTasksDir = resolve6(projectRoot, DEMO_TASKS_RELATIVE_DIR);
|
|
1431
|
+
mkdirSync2(demoTasksDir, { recursive: true });
|
|
1432
|
+
const taskIds = [];
|
|
1433
|
+
for (const task of DEMO_TASKS) {
|
|
1434
|
+
const id = String(task.id);
|
|
1435
|
+
taskIds.push(id);
|
|
1436
|
+
const taskPath = resolve6(demoTasksDir, `${id}.json`);
|
|
1437
|
+
if (!existsSync5(taskPath)) {
|
|
1438
|
+
writeFileSync2(taskPath, `${JSON.stringify(task, null, 2)}
|
|
1439
|
+
`, "utf-8");
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (context.outputMode !== "json") {
|
|
1443
|
+
console.log(`Demo Rig project ready (offline, no GitHub).`);
|
|
1444
|
+
console.log(` config: rig.config.ts (files task source -> ${DEMO_TASKS_RELATIVE_DIR}/)`);
|
|
1445
|
+
console.log(` tasks: ${taskIds.join(", ")}`);
|
|
1446
|
+
console.log("Next steps:");
|
|
1447
|
+
console.log(" 1. rig task list");
|
|
1448
|
+
console.log(" 2. rig task run --next");
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
ok: true,
|
|
1452
|
+
group: "init",
|
|
1453
|
+
command: "init",
|
|
1454
|
+
details: {
|
|
1455
|
+
mode: "demo",
|
|
1456
|
+
projectRoot,
|
|
1457
|
+
taskSourcePath: DEMO_TASKS_RELATIVE_DIR,
|
|
1458
|
+
demoTasksDir,
|
|
1459
|
+
tasks: taskIds,
|
|
1460
|
+
configWritten
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
986
1464
|
async function runControlPlaneInit(context, options) {
|
|
987
1465
|
const projectRoot = context.projectRoot;
|
|
988
1466
|
const detectedSlug = options.repoSlug ?? detectOriginRepoSlug(projectRoot);
|
|
989
1467
|
if (!detectedSlug) {
|
|
990
|
-
|
|
1468
|
+
const authMethod2 = options.githubAuthMethod ?? (options.githubToken ? "token" : "skip");
|
|
1469
|
+
if ((options.server ?? "local") === "local" && authMethod2 === "skip") {
|
|
1470
|
+
return runLocalFilesInit(context, options);
|
|
1471
|
+
}
|
|
1472
|
+
throw new CliError("Could not detect GitHub repo slug from origin. Pass --repo owner/repo \u2014 or run `rig init --yes --server local --github-auth skip` for a local files-source project without GitHub.", 1);
|
|
991
1473
|
}
|
|
992
1474
|
const repo = parseRepoSlug(detectedSlug);
|
|
993
1475
|
const serverKind = options.server ?? "local";
|
|
994
1476
|
const connectionAlias = options.connectionAlias ?? (serverKind === "local" ? "local" : "remote");
|
|
995
1477
|
if (serverKind === "remote") {
|
|
996
1478
|
if (!options.remoteUrl)
|
|
997
|
-
throw new
|
|
1479
|
+
throw new CliError("Missing --remote-url for --server remote.", 1, { hint: "Re-run as `rig init --server remote --remote-url https://your-rig-server`." });
|
|
998
1480
|
upsertGlobalConnection(connectionAlias, { kind: "remote", baseUrl: options.remoteUrl });
|
|
999
1481
|
}
|
|
1000
1482
|
writeRepoConnection(projectRoot, {
|
|
@@ -1039,18 +1521,19 @@ async function runControlPlaneInit(context, options) {
|
|
|
1039
1521
|
const authMethod = options.githubAuthMethod ?? (options.githubToken ? "token" : "skip");
|
|
1040
1522
|
const remoteGhTokenWarning = serverKind === "remote" && authMethod === "gh" ? `This sends a GitHub token from this machine to ${options.remoteUrl ?? "the remote Rig server"}.` : null;
|
|
1041
1523
|
if (remoteGhTokenWarning && !options.yes) {
|
|
1042
|
-
throw new
|
|
1524
|
+
throw new CliError(`${remoteGhTokenWarning} Re-run with --yes to confirm this explicit token transfer.`, 1);
|
|
1043
1525
|
}
|
|
1044
1526
|
const token = authMethod === "gh" && !options.githubToken ? readGhAuthToken() : options.githubToken?.trim();
|
|
1045
1527
|
if (token) {
|
|
1046
1528
|
githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug });
|
|
1047
1529
|
const apiSessionToken = apiSessionTokenFrom(githubAuth);
|
|
1048
|
-
setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token);
|
|
1530
|
+
setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token, projectRoot);
|
|
1049
1531
|
if (serverKind === "remote") {
|
|
1050
1532
|
writeRemoteGitHubAuthState(projectRoot, {
|
|
1051
1533
|
source: authMethod === "gh" ? "gh" : "init-token",
|
|
1052
1534
|
selectedRepo: repo.slug,
|
|
1053
|
-
apiSessionToken
|
|
1535
|
+
apiSessionToken,
|
|
1536
|
+
authPayload: githubAuth
|
|
1054
1537
|
});
|
|
1055
1538
|
}
|
|
1056
1539
|
} else if (authMethod === "device") {
|
|
@@ -1069,9 +1552,9 @@ async function runControlPlaneInit(context, options) {
|
|
|
1069
1552
|
if (completed) {
|
|
1070
1553
|
const apiSessionToken = apiSessionTokenFrom(completed);
|
|
1071
1554
|
if (apiSessionToken) {
|
|
1072
|
-
setGitHubBearerTokenForCurrentProcess(apiSessionToken);
|
|
1555
|
+
setGitHubBearerTokenForCurrentProcess(apiSessionToken, projectRoot);
|
|
1073
1556
|
if (serverKind === "remote") {
|
|
1074
|
-
writeRemoteGitHubAuthState(projectRoot, { source: "device", selectedRepo: repo.slug, apiSessionToken });
|
|
1557
|
+
writeRemoteGitHubAuthState(projectRoot, { source: "device", selectedRepo: repo.slug, apiSessionToken, authPayload: completed });
|
|
1075
1558
|
}
|
|
1076
1559
|
}
|
|
1077
1560
|
deviceAuth = { ...deviceAuth, poll: completed, completed: completed.status === "signed-in" };
|
|
@@ -1089,19 +1572,25 @@ async function runControlPlaneInit(context, options) {
|
|
|
1089
1572
|
Object.assign(checkout, preparedCheckout);
|
|
1090
1573
|
}
|
|
1091
1574
|
}
|
|
1575
|
+
const checkoutPath = typeof checkout.path === "string" ? checkout.path : null;
|
|
1576
|
+
if (serverKind === "remote" && checkoutPath && token) {
|
|
1577
|
+
githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug, projectRoot: checkoutPath });
|
|
1578
|
+
const apiSessionToken = apiSessionTokenFrom(githubAuth);
|
|
1579
|
+
setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token, projectRoot);
|
|
1580
|
+
writeRemoteGitHubAuthState(projectRoot, { source: authMethod === "gh" ? "gh" : "init-token", selectedRepo: repo.slug, apiSessionToken, authPayload: githubAuth });
|
|
1581
|
+
}
|
|
1092
1582
|
const registered = await registerProjectViaServer(context, {
|
|
1093
1583
|
repoSlug: repo.slug,
|
|
1094
1584
|
checkout
|
|
1095
1585
|
});
|
|
1096
|
-
const checkoutPath = typeof checkout.path === "string" ? checkout.path : null;
|
|
1097
1586
|
const serverRootSwitch = serverKind === "remote" && checkoutPath ? await switchServerProjectRootViaServer(context, checkoutPath) : null;
|
|
1098
|
-
if (serverRootSwitch && token) {
|
|
1099
|
-
githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug, projectRoot: checkoutPath ?? undefined });
|
|
1100
|
-
const apiSessionToken = apiSessionTokenFrom(githubAuth);
|
|
1101
|
-
setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token);
|
|
1102
|
-
writeRemoteGitHubAuthState(projectRoot, { source: authMethod === "gh" ? "gh" : "init-token", selectedRepo: repo.slug, apiSessionToken });
|
|
1103
|
-
}
|
|
1104
1587
|
const activeProjectRegistration = serverRootSwitch ? await registerProjectViaServer(context, { repoSlug: repo.slug, checkout }) : null;
|
|
1588
|
+
const labelSetup = await ensureTaskLabelsViaServer(context).catch((error) => ({
|
|
1589
|
+
ok: false,
|
|
1590
|
+
ready: false,
|
|
1591
|
+
labelsReady: false,
|
|
1592
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1593
|
+
}));
|
|
1105
1594
|
const pi = serverKind === "remote" ? await ensureRemotePiRigInstalled({ requestJson: (pathname, init) => requestServerJson(context, pathname, init) }).catch((error) => ({
|
|
1106
1595
|
remote: true,
|
|
1107
1596
|
pi: { ok: false, label: "pi", hint: error instanceof Error ? error.message : String(error) },
|
|
@@ -1132,6 +1621,7 @@ async function runControlPlaneInit(context, options) {
|
|
|
1132
1621
|
githubAuth,
|
|
1133
1622
|
deviceAuth,
|
|
1134
1623
|
githubAuthWarning: remoteGhTokenWarning,
|
|
1624
|
+
labelSetup,
|
|
1135
1625
|
pi,
|
|
1136
1626
|
doctor
|
|
1137
1627
|
};
|
|
@@ -1143,6 +1633,8 @@ async function runControlPlaneInit(context, options) {
|
|
|
1143
1633
|
}
|
|
1144
1634
|
function parseInitOptions(args) {
|
|
1145
1635
|
let rest = [...args];
|
|
1636
|
+
const demo = takeFlag(rest, "--demo");
|
|
1637
|
+
rest = demo.rest;
|
|
1146
1638
|
const yes = takeFlag(rest, "--yes");
|
|
1147
1639
|
rest = yes.rest;
|
|
1148
1640
|
const repair = takeFlag(rest, "--repair");
|
|
@@ -1172,6 +1664,7 @@ function parseInitOptions(args) {
|
|
|
1172
1664
|
const ref = takeOption(rest, "--ref");
|
|
1173
1665
|
rest = ref.rest;
|
|
1174
1666
|
const options = {
|
|
1667
|
+
demo: demo.value,
|
|
1175
1668
|
yes: yes.value,
|
|
1176
1669
|
repair: repair.value,
|
|
1177
1670
|
privateStateOnly: privateStateOnly.value,
|
|
@@ -1185,10 +1678,10 @@ function parseInitOptions(args) {
|
|
|
1185
1678
|
githubProjectStatusField: githubProjectStatusField.value
|
|
1186
1679
|
};
|
|
1187
1680
|
if (server.value && options.server === undefined) {
|
|
1188
|
-
throw new
|
|
1681
|
+
throw new CliError("--server must be local or remote.", 1);
|
|
1189
1682
|
}
|
|
1190
1683
|
if (githubAuth.value && !["gh", "token", "device", "skip"].includes(githubAuth.value)) {
|
|
1191
|
-
throw new
|
|
1684
|
+
throw new CliError("--github-auth must be gh, token, device, or skip.", 1);
|
|
1192
1685
|
}
|
|
1193
1686
|
if (remoteCheckout.value) {
|
|
1194
1687
|
if (remoteCheckout.value === "managed-clone")
|
|
@@ -1199,10 +1692,10 @@ function parseInitOptions(args) {
|
|
|
1199
1692
|
options.remoteCheckout = { kind: "uploaded-snapshot" };
|
|
1200
1693
|
else if (remoteCheckout.value === "existing-path") {
|
|
1201
1694
|
if (!existingPath.value)
|
|
1202
|
-
throw new
|
|
1695
|
+
throw new CliError("--remote-checkout existing-path requires --existing-path <path>.", 1);
|
|
1203
1696
|
options.remoteCheckout = { kind: "existing-path", path: existingPath.value };
|
|
1204
1697
|
} else {
|
|
1205
|
-
throw new
|
|
1698
|
+
throw new CliError("--remote-checkout must be managed-clone, current-ref, uploaded-snapshot, or existing-path.", 1);
|
|
1206
1699
|
}
|
|
1207
1700
|
}
|
|
1208
1701
|
return { options, rest };
|
|
@@ -1238,12 +1731,13 @@ async function runInteractiveControlPlaneInit(context, prompts) {
|
|
|
1238
1731
|
});
|
|
1239
1732
|
const serverChoice = await promptSelect(prompts, {
|
|
1240
1733
|
message: "Rig server",
|
|
1734
|
+
initialValue: "remote",
|
|
1241
1735
|
options: [
|
|
1242
|
-
{ value: "
|
|
1243
|
-
{ value: "
|
|
1736
|
+
{ value: "remote", label: "Remote server", hint: "connect to an HTTPS Rig server" },
|
|
1737
|
+
{ value: "local", label: "Local server", hint: "run on this machine" }
|
|
1244
1738
|
]
|
|
1245
1739
|
});
|
|
1246
|
-
const remoteUrl = serverChoice === "remote" ? await promptRequiredText(prompts, { message: "Remote Rig server URL", placeholder:
|
|
1740
|
+
const remoteUrl = serverChoice === "remote" ? await promptRequiredText(prompts, { message: "Remote Rig server URL", placeholder: DEFAULT_REMOTE_RIG_URL, initialValue: DEFAULT_REMOTE_RIG_URL }) : undefined;
|
|
1247
1741
|
let remoteCheckout;
|
|
1248
1742
|
if (serverChoice === "remote") {
|
|
1249
1743
|
const checkout = await promptSelect(prompts, {
|
|
@@ -1275,38 +1769,35 @@ async function runInteractiveControlPlaneInit(context, prompts) {
|
|
|
1275
1769
|
{ value: "skip", label: "Skip for now" }
|
|
1276
1770
|
]
|
|
1277
1771
|
});
|
|
1772
|
+
let remoteGhTokenConfirmed = false;
|
|
1278
1773
|
if (serverChoice === "remote" && authMethod === "gh") {
|
|
1279
1774
|
if (!prompts.confirm)
|
|
1280
|
-
throw new
|
|
1775
|
+
throw new CliError("Remote gh-token import requires explicit confirmation.", 1);
|
|
1281
1776
|
const confirmed = await prompts.confirm({
|
|
1282
1777
|
message: `This sends a GitHub token from this machine to ${remoteUrl}. Continue?`,
|
|
1283
|
-
initialValue:
|
|
1778
|
+
initialValue: true
|
|
1284
1779
|
});
|
|
1285
1780
|
if (prompts.isCancel(confirmed) || confirmed !== true) {
|
|
1286
|
-
throw new
|
|
1781
|
+
throw new CliError("Remote gh-token import cancelled.", 1);
|
|
1287
1782
|
}
|
|
1783
|
+
remoteGhTokenConfirmed = true;
|
|
1288
1784
|
}
|
|
1289
|
-
const githubToken = authMethod === "token" ? await promptRequiredText(prompts, { message: "GitHub token", placeholder: "ghp_..." }) : undefined;
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
options: [
|
|
1293
|
-
{ value: "off", label: "Off" },
|
|
1294
|
-
{ value: "configure", label: "Configure ProjectV2 status field" }
|
|
1295
|
-
]
|
|
1296
|
-
});
|
|
1297
|
-
const githubProject = projectChoice === "configure" ? await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }) : "off";
|
|
1298
|
-
const githubProjectStatusField = projectChoice === "configure" ? await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" }) : undefined;
|
|
1785
|
+
const githubToken = authMethod === "token" ? await promptRequiredText(prompts, { message: "GitHub token", placeholder: "ghp_..." }) : authMethod === "gh" ? readGhAuthToken() : undefined;
|
|
1786
|
+
const projectConfig = await promptGitHubProjectConfig(context, prompts, repoSlug, githubToken, authMethod === "gh" ? refreshGhProjectScopesAndReadToken : undefined);
|
|
1787
|
+
const effectiveGithubToken = projectConfig.githubToken ?? githubToken;
|
|
1299
1788
|
const result = await runControlPlaneInit(context, {
|
|
1300
1789
|
server: serverChoice,
|
|
1301
1790
|
remoteUrl,
|
|
1302
1791
|
repoSlug,
|
|
1303
|
-
githubToken,
|
|
1792
|
+
githubToken: effectiveGithubToken,
|
|
1304
1793
|
githubAuthMethod: authMethod,
|
|
1305
|
-
githubProject,
|
|
1306
|
-
githubProjectStatusField,
|
|
1794
|
+
githubProject: projectConfig.githubProject,
|
|
1795
|
+
githubProjectStatusField: projectConfig.githubProjectStatusField,
|
|
1796
|
+
githubProjectStatuses: projectConfig.githubProjectStatuses,
|
|
1307
1797
|
remoteCheckout,
|
|
1308
1798
|
repair,
|
|
1309
|
-
privateStateOnly
|
|
1799
|
+
privateStateOnly,
|
|
1800
|
+
yes: remoteGhTokenConfirmed || undefined
|
|
1310
1801
|
});
|
|
1311
1802
|
const details = result.details && typeof result.details === "object" && !Array.isArray(result.details) ? result.details : {};
|
|
1312
1803
|
const deviceAuth = details.deviceAuth && typeof details.deviceAuth === "object" && !Array.isArray(details.deviceAuth) ? details.deviceAuth : null;
|
|
@@ -1316,19 +1807,32 @@ async function runInteractiveControlPlaneInit(context, prompts) {
|
|
|
1316
1807
|
}
|
|
1317
1808
|
async function executeInit(context, args) {
|
|
1318
1809
|
const parsed = parseInitOptions(args);
|
|
1810
|
+
if (parsed.options.demo) {
|
|
1811
|
+
if (parsed.rest.length > 0) {
|
|
1812
|
+
throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
|
|
1813
|
+
Usage: rig init --demo [--yes] [--repair]`, 1, { hint: "Run `rig init --demo` (optionally with --repair to rewrite an existing rig.config)." });
|
|
1814
|
+
}
|
|
1815
|
+
return runDemoInit(context, parsed.options);
|
|
1816
|
+
}
|
|
1319
1817
|
if (parsed.options.yes || parsed.options.server || parsed.options.repoSlug || parsed.options.githubToken || parsed.options.privateStateOnly || parsed.options.repair || parsed.options.githubAuthMethod || parsed.options.remoteCheckout) {
|
|
1320
1818
|
if (parsed.rest.length > 0)
|
|
1321
|
-
throw new
|
|
1322
|
-
Usage: rig init [--server local|remote] [--remote-url <url>] [--repo owner/repo] [--github-auth gh|token|device|skip] [--github-token <token>] [--github-project off|<project-id>] [--remote-checkout managed-clone|current-ref|uploaded-snapshot|existing-path] [--yes]`, 1);
|
|
1819
|
+
throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
|
|
1820
|
+
Usage: rig init [--demo] [--server local|remote] [--remote-url <url>] [--repo owner/repo] [--github-auth gh|token|device|skip] [--github-token <token>] [--github-project off|<project-id>] [--remote-checkout managed-clone|current-ref|uploaded-snapshot|existing-path] [--yes]`, 1);
|
|
1323
1821
|
return runControlPlaneInit(context, parsed.options);
|
|
1324
1822
|
}
|
|
1325
1823
|
if (parsed.rest.length > 0)
|
|
1326
|
-
throw new
|
|
1824
|
+
throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
|
|
1327
1825
|
Usage: rig init`, 1);
|
|
1826
|
+
if (!process.stdin.isTTY) {
|
|
1827
|
+
throw new CliError("rig init is interactive and needs a terminal. For scripts, pass flags: rig init --yes --server local --github-auth skip [--repo owner/repo].", 1);
|
|
1828
|
+
}
|
|
1328
1829
|
return runInteractiveControlPlaneInit(context, await loadClackPrompts());
|
|
1329
1830
|
}
|
|
1330
1831
|
export {
|
|
1331
1832
|
runInteractiveControlPlaneInit,
|
|
1833
|
+
runDemoInit,
|
|
1332
1834
|
executeInit,
|
|
1333
|
-
buildRigInitConfigSource
|
|
1835
|
+
buildRigInitConfigSource,
|
|
1836
|
+
DEMO_TASKS_RELATIVE_DIR,
|
|
1837
|
+
DEMO_TASKS
|
|
1334
1838
|
};
|