@h-rig/cli 0.0.6-alpha.63 → 0.0.6-alpha.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rig.js +1349 -1599
- package/dist/src/commands/_connection-state.js +14 -5
- package/dist/src/commands/_doctor-checks.js +71 -11
- package/dist/src/commands/_help-catalog.js +99 -60
- package/dist/src/commands/_json-output.js +56 -0
- package/dist/src/commands/_operator-view.js +97 -784
- package/dist/src/commands/_parsers.js +18 -9
- package/dist/src/commands/_pi-frontend.js +96 -788
- package/dist/src/commands/_policy.js +12 -3
- package/dist/src/commands/_preflight.js +73 -13
- package/dist/src/commands/_run-driver-helpers.js +31 -5
- package/dist/src/commands/_run-replay.js +2 -2
- package/dist/src/commands/_server-client.js +76 -16
- package/dist/src/commands/_snapshot-upload.js +70 -10
- package/dist/src/commands/agent.js +21 -11
- package/dist/src/commands/browser.js +24 -15
- package/dist/src/commands/connect.js +22 -17
- package/dist/src/commands/dist.js +15 -6
- package/dist/src/commands/doctor.js +70 -10
- package/dist/src/commands/github.js +79 -19
- package/dist/src/commands/inbox.js +215 -131
- package/dist/src/commands/init.js +202 -35
- package/dist/src/commands/inspect.js +77 -17
- package/dist/src/commands/inspector.js +18 -9
- package/dist/src/commands/pi.js +18 -9
- package/dist/src/commands/plugin.js +19 -10
- package/dist/src/commands/profile-and-review.js +22 -13
- package/dist/src/commands/queue.js +19 -9
- package/dist/src/commands/remote.js +25 -16
- package/dist/src/commands/repo-git-harness.js +18 -9
- package/dist/src/commands/run.js +158 -805
- package/dist/src/commands/server.js +85 -25
- package/dist/src/commands/setup.js +73 -13
- package/dist/src/commands/stats.js +681 -0
- package/dist/src/commands/task-report-bug.js +24 -15
- package/dist/src/commands/task-run-driver.js +121 -49
- package/dist/src/commands/task.js +254 -893
- package/dist/src/commands/test.js +12 -3
- package/dist/src/commands/workspace.js +14 -5
- package/dist/src/commands.js +1339 -1650
- package/dist/src/index.js +1349 -1599
- package/dist/src/launcher.js +72 -10
- package/dist/src/runner.js +14 -5
- package/package.json +10 -7
- package/dist/src/commands/_pi-remote-session.js +0 -771
- package/dist/src/commands/_pi-worker-bridge-extension.js +0 -834
|
@@ -8,10 +8,19 @@ 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
13
|
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
14
|
-
|
|
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
|
+
}
|
|
15
24
|
function takeFlag(args, flag) {
|
|
16
25
|
const rest = [];
|
|
17
26
|
let value = false;
|
|
@@ -32,7 +41,7 @@ function takeOption(args, option) {
|
|
|
32
41
|
if (current === option) {
|
|
33
42
|
const next = args[index + 1];
|
|
34
43
|
if (!next || next.startsWith("-")) {
|
|
35
|
-
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>\`.` });
|
|
36
45
|
}
|
|
37
46
|
value = next;
|
|
38
47
|
index += 1;
|
|
@@ -71,7 +80,7 @@ function readJsonFile(path) {
|
|
|
71
80
|
try {
|
|
72
81
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
73
82
|
} catch (error) {
|
|
74
|
-
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>`." });
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
86
|
function writeJsonFile(path, value) {
|
|
@@ -114,7 +123,7 @@ function writeGlobalConnections(state, options = {}) {
|
|
|
114
123
|
function upsertGlobalConnection(alias, connection, options = {}) {
|
|
115
124
|
const cleanAlias = alias.trim();
|
|
116
125
|
if (!cleanAlias)
|
|
117
|
-
throw new
|
|
126
|
+
throw new CliError("Connection alias is required.", 1, { hint: "Save a server with `rig server add <alias> <url>`." });
|
|
118
127
|
const state = readGlobalConnections(options);
|
|
119
128
|
state.connections[cleanAlias] = connection;
|
|
120
129
|
writeGlobalConnections(state, options);
|
|
@@ -147,7 +156,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
|
|
|
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
161
|
return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
|
|
153
162
|
}
|
|
@@ -227,7 +236,7 @@ async function ensureServerForCli(projectRoot) {
|
|
|
227
236
|
};
|
|
228
237
|
} catch (error) {
|
|
229
238
|
if (error instanceof Error) {
|
|
230
|
-
throw new
|
|
239
|
+
throw new CliError(error.message, 1);
|
|
231
240
|
}
|
|
232
241
|
throw error;
|
|
233
242
|
}
|
|
@@ -277,15 +286,64 @@ function diagnosticMessage(payload) {
|
|
|
277
286
|
});
|
|
278
287
|
return messages.length > 0 ? messages.join("; ") : null;
|
|
279
288
|
}
|
|
289
|
+
var serverReachabilityCache = new Map;
|
|
290
|
+
async function probeServerReachability(baseUrl, authToken) {
|
|
291
|
+
try {
|
|
292
|
+
const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
|
|
293
|
+
headers: mergeHeaders(undefined, authToken),
|
|
294
|
+
signal: AbortSignal.timeout(1500)
|
|
295
|
+
});
|
|
296
|
+
return response.ok;
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function cachedServerReachability(projectRoot, baseUrl, authToken) {
|
|
302
|
+
const key = resolve2(projectRoot);
|
|
303
|
+
const cached = serverReachabilityCache.get(key);
|
|
304
|
+
if (cached)
|
|
305
|
+
return cached;
|
|
306
|
+
const probe = probeServerReachability(baseUrl, authToken);
|
|
307
|
+
serverReachabilityCache.set(key, probe);
|
|
308
|
+
return probe;
|
|
309
|
+
}
|
|
310
|
+
function describeSelectedServer(projectRoot, server) {
|
|
311
|
+
try {
|
|
312
|
+
const selected = resolveSelectedConnection(projectRoot);
|
|
313
|
+
if (selected) {
|
|
314
|
+
return {
|
|
315
|
+
alias: selected.alias,
|
|
316
|
+
target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
} catch {}
|
|
320
|
+
return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
|
|
321
|
+
}
|
|
322
|
+
async function buildServerFailureContext(projectRoot, server) {
|
|
323
|
+
const { alias, target } = describeSelectedServer(projectRoot, server);
|
|
324
|
+
const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
|
|
325
|
+
const reachability = reachable ? "server is reachable" : "server is unreachable";
|
|
326
|
+
return {
|
|
327
|
+
contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
|
|
328
|
+
hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
|
|
329
|
+
};
|
|
330
|
+
}
|
|
280
331
|
async function requestServerJson(context, pathname, init = {}) {
|
|
281
332
|
const server = await ensureServerForCli(context.projectRoot);
|
|
282
333
|
const headers = mergeHeaders(init.headers, server.authToken);
|
|
283
334
|
if (server.serverProjectRoot)
|
|
284
335
|
headers.set("x-rig-project-root", server.serverProjectRoot);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
336
|
+
let response;
|
|
337
|
+
try {
|
|
338
|
+
response = await fetch(`${server.baseUrl}${pathname}`, {
|
|
339
|
+
...init,
|
|
340
|
+
headers
|
|
341
|
+
});
|
|
342
|
+
} catch (error) {
|
|
343
|
+
const failure = await buildServerFailureContext(context.projectRoot, server);
|
|
344
|
+
throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
|
|
345
|
+
${failure.contextLine}`, 1, { hint: failure.hint });
|
|
346
|
+
}
|
|
289
347
|
const text = await response.text();
|
|
290
348
|
const payload = text.trim().length > 0 ? (() => {
|
|
291
349
|
try {
|
|
@@ -297,7 +355,9 @@ async function requestServerJson(context, pathname, init = {}) {
|
|
|
297
355
|
if (!response.ok) {
|
|
298
356
|
const diagnostics = diagnosticMessage(payload);
|
|
299
357
|
const detail = diagnostics ?? (text || response.statusText);
|
|
300
|
-
|
|
358
|
+
const failure = await buildServerFailureContext(context.projectRoot, server);
|
|
359
|
+
throw new CliError(`Rig server request failed (${response.status}): ${detail}
|
|
360
|
+
${failure.contextLine}`, 1, { hint: failure.hint });
|
|
301
361
|
}
|
|
302
362
|
return payload;
|
|
303
363
|
}
|
|
@@ -356,14 +416,14 @@ async function switchServerProjectRootViaServer(context, projectRoot, options =
|
|
|
356
416
|
}
|
|
357
417
|
}
|
|
358
418
|
if (!switched) {
|
|
359
|
-
throw new
|
|
419
|
+
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);
|
|
360
420
|
}
|
|
361
421
|
const record = switched && typeof switched === "object" && !Array.isArray(switched) ? switched : {};
|
|
362
422
|
if (record.ok === true) {
|
|
363
423
|
writeRepoServerProjectRoot(context.projectRoot, projectRoot);
|
|
364
424
|
return { ok: true, switched: record };
|
|
365
425
|
}
|
|
366
|
-
throw new
|
|
426
|
+
throw new CliError(`Rig server rejected project-root scope ${projectRoot}: ${String(record.message ?? record.error ?? "unknown error")}`, 1);
|
|
367
427
|
}
|
|
368
428
|
async function ensureTaskLabelsViaServer(context) {
|
|
369
429
|
const payload = await requestServerJson(context, "/api/workspace/task-labels", { method: "POST" });
|
|
@@ -891,7 +951,7 @@ function detectOriginRepoSlug(projectRoot) {
|
|
|
891
951
|
function parseRepoSlug(value) {
|
|
892
952
|
const match = value.trim().match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
893
953
|
if (!match)
|
|
894
|
-
throw new
|
|
954
|
+
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`." });
|
|
895
955
|
return { owner: match[1], repo: match[2], slug: `${match[1]}/${match[2]}` };
|
|
896
956
|
}
|
|
897
957
|
function ensureRigPrivateDirs(projectRoot) {
|
|
@@ -973,7 +1033,7 @@ function detectGhLogin() {
|
|
|
973
1033
|
function readGhAuthToken() {
|
|
974
1034
|
const result = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
|
|
975
1035
|
if (result.status !== 0 || !result.stdout.trim()) {
|
|
976
|
-
throw new
|
|
1036
|
+
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>`." });
|
|
977
1037
|
}
|
|
978
1038
|
return result.stdout.trim();
|
|
979
1039
|
}
|
|
@@ -1003,22 +1063,22 @@ function clackTextOptions(options) {
|
|
|
1003
1063
|
async function promptRequiredText(prompts, options) {
|
|
1004
1064
|
const value = await prompts.text(clackTextOptions(options));
|
|
1005
1065
|
if (prompts.isCancel(value))
|
|
1006
|
-
throw new
|
|
1066
|
+
throw new CliError("Init cancelled.", 1);
|
|
1007
1067
|
const text = String(value ?? "").trim();
|
|
1008
1068
|
if (!text)
|
|
1009
|
-
throw new
|
|
1069
|
+
throw new CliError(`${options.message} is required.`, 1);
|
|
1010
1070
|
return text;
|
|
1011
1071
|
}
|
|
1012
1072
|
async function promptOptionalText(prompts, options) {
|
|
1013
1073
|
const value = await prompts.text(clackTextOptions(options));
|
|
1014
1074
|
if (prompts.isCancel(value))
|
|
1015
|
-
throw new
|
|
1075
|
+
throw new CliError("Init cancelled.", 1);
|
|
1016
1076
|
return String(value ?? "").trim();
|
|
1017
1077
|
}
|
|
1018
1078
|
async function promptSelect(prompts, options) {
|
|
1019
1079
|
const value = await prompts.select(options);
|
|
1020
1080
|
if (prompts.isCancel(value))
|
|
1021
|
-
throw new
|
|
1081
|
+
throw new CliError("Init cancelled.", 1);
|
|
1022
1082
|
return String(value);
|
|
1023
1083
|
}
|
|
1024
1084
|
function repoOwnerFromSlug(repoSlug) {
|
|
@@ -1120,7 +1180,7 @@ async function promptGitHubProjectConfig(context, prompts, repoSlug, githubToken
|
|
|
1120
1180
|
}
|
|
1121
1181
|
const owner = repoOwnerFromSlug(repoSlug);
|
|
1122
1182
|
if (!owner)
|
|
1123
|
-
throw new
|
|
1183
|
+
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`." });
|
|
1124
1184
|
let activeToken = githubToken?.trim() || null;
|
|
1125
1185
|
let projectsPayload = await listGitHubProjectsForInit(context, owner, activeToken).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), projects: [] }));
|
|
1126
1186
|
let projects = recordArray(projectsPayload, "projects");
|
|
@@ -1278,6 +1338,100 @@ function runLocalFilesInit(context, options) {
|
|
|
1278
1338
|
}
|
|
1279
1339
|
return { ok: true, group: "init", command: "init", details: { mode: "local-files", projectRoot, taskSourcePath: "tasks" } };
|
|
1280
1340
|
}
|
|
1341
|
+
var DEMO_TASKS_RELATIVE_DIR = ".rig/demo-tasks";
|
|
1342
|
+
var DEMO_TASKS = [
|
|
1343
|
+
{
|
|
1344
|
+
id: "demo-1",
|
|
1345
|
+
title: "Add a hello CLI script",
|
|
1346
|
+
body: [
|
|
1347
|
+
"Create `scripts/hello.ts` that prints `Hello from Rig!` plus the current date,",
|
|
1348
|
+
"and add a `hello` script entry to package.json that runs it with bun.",
|
|
1349
|
+
"Keep it dependency-free."
|
|
1350
|
+
].join(`
|
|
1351
|
+
`),
|
|
1352
|
+
status: "ready",
|
|
1353
|
+
labels: ["demo"]
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
id: "demo-2",
|
|
1357
|
+
title: "Write a README section about this project",
|
|
1358
|
+
body: [
|
|
1359
|
+
"Add (or extend) README.md with a short `## What this is` section:",
|
|
1360
|
+
"two or three sentences describing the repository and how to run it.",
|
|
1361
|
+
"Plain prose, no badges."
|
|
1362
|
+
].join(`
|
|
1363
|
+
`),
|
|
1364
|
+
status: "ready",
|
|
1365
|
+
labels: ["demo"]
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
id: "demo-3",
|
|
1369
|
+
title: "Add a unit test for the hello script",
|
|
1370
|
+
body: [
|
|
1371
|
+
"Add `scripts/hello.test.ts` with a bun test that imports the greeting",
|
|
1372
|
+
"helper from the hello script and asserts it contains `Hello from Rig!`.",
|
|
1373
|
+
"Refactor the script to export that helper if needed."
|
|
1374
|
+
].join(`
|
|
1375
|
+
`),
|
|
1376
|
+
status: "ready",
|
|
1377
|
+
labels: ["demo"]
|
|
1378
|
+
}
|
|
1379
|
+
];
|
|
1380
|
+
function runDemoInit(context, options) {
|
|
1381
|
+
const projectRoot = context.projectRoot;
|
|
1382
|
+
ensureRigPrivateDirs(projectRoot);
|
|
1383
|
+
ensureGitignoreEntries(projectRoot);
|
|
1384
|
+
writeRepoConnection(projectRoot, { selected: "local", linkedAt: new Date().toISOString() });
|
|
1385
|
+
const configTsPath = resolve6(projectRoot, "rig.config.ts");
|
|
1386
|
+
const configExists = existsSync5(configTsPath) || existsSync5(resolve6(projectRoot, "rig.config.json"));
|
|
1387
|
+
let configWritten = false;
|
|
1388
|
+
if (configExists && !options.repair) {
|
|
1389
|
+
if (context.outputMode !== "json") {
|
|
1390
|
+
console.log("rig.config already exists; leaving it unchanged. Pass --repair to rewrite it for the demo.");
|
|
1391
|
+
}
|
|
1392
|
+
} else {
|
|
1393
|
+
writeFileSync2(configTsPath, buildRigInitConfigSource({
|
|
1394
|
+
projectName: basename(projectRoot) || "rig-demo",
|
|
1395
|
+
taskSource: { kind: "files", path: DEMO_TASKS_RELATIVE_DIR },
|
|
1396
|
+
useStandardPlugin: true
|
|
1397
|
+
}), "utf-8");
|
|
1398
|
+
configWritten = true;
|
|
1399
|
+
}
|
|
1400
|
+
ensureRigConfigPackageDependencies(projectRoot);
|
|
1401
|
+
const demoTasksDir = resolve6(projectRoot, DEMO_TASKS_RELATIVE_DIR);
|
|
1402
|
+
mkdirSync2(demoTasksDir, { recursive: true });
|
|
1403
|
+
const taskIds = [];
|
|
1404
|
+
for (const task of DEMO_TASKS) {
|
|
1405
|
+
const id = String(task.id);
|
|
1406
|
+
taskIds.push(id);
|
|
1407
|
+
const taskPath = resolve6(demoTasksDir, `${id}.json`);
|
|
1408
|
+
if (!existsSync5(taskPath)) {
|
|
1409
|
+
writeFileSync2(taskPath, `${JSON.stringify(task, null, 2)}
|
|
1410
|
+
`, "utf-8");
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
if (context.outputMode !== "json") {
|
|
1414
|
+
console.log(`Demo Rig project ready (offline, no GitHub).`);
|
|
1415
|
+
console.log(` config: rig.config.ts (files task source -> ${DEMO_TASKS_RELATIVE_DIR}/)`);
|
|
1416
|
+
console.log(` tasks: ${taskIds.join(", ")}`);
|
|
1417
|
+
console.log("Next steps:");
|
|
1418
|
+
console.log(" 1. rig task list");
|
|
1419
|
+
console.log(" 2. rig task run --next");
|
|
1420
|
+
}
|
|
1421
|
+
return {
|
|
1422
|
+
ok: true,
|
|
1423
|
+
group: "init",
|
|
1424
|
+
command: "init",
|
|
1425
|
+
details: {
|
|
1426
|
+
mode: "demo",
|
|
1427
|
+
projectRoot,
|
|
1428
|
+
taskSourcePath: DEMO_TASKS_RELATIVE_DIR,
|
|
1429
|
+
demoTasksDir,
|
|
1430
|
+
tasks: taskIds,
|
|
1431
|
+
configWritten
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1281
1435
|
async function runControlPlaneInit(context, options) {
|
|
1282
1436
|
const projectRoot = context.projectRoot;
|
|
1283
1437
|
const detectedSlug = options.repoSlug ?? detectOriginRepoSlug(projectRoot);
|
|
@@ -1286,14 +1440,14 @@ async function runControlPlaneInit(context, options) {
|
|
|
1286
1440
|
if ((options.server ?? "local") === "local" && authMethod2 === "skip") {
|
|
1287
1441
|
return runLocalFilesInit(context, options);
|
|
1288
1442
|
}
|
|
1289
|
-
throw new
|
|
1443
|
+
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);
|
|
1290
1444
|
}
|
|
1291
1445
|
const repo = parseRepoSlug(detectedSlug);
|
|
1292
1446
|
const serverKind = options.server ?? "local";
|
|
1293
1447
|
const connectionAlias = options.connectionAlias ?? (serverKind === "local" ? "local" : "remote");
|
|
1294
1448
|
if (serverKind === "remote") {
|
|
1295
1449
|
if (!options.remoteUrl)
|
|
1296
|
-
throw new
|
|
1450
|
+
throw new CliError("Missing --remote-url for --server remote.", 1, { hint: "Re-run as `rig init --server remote --remote-url https://your-rig-server`." });
|
|
1297
1451
|
upsertGlobalConnection(connectionAlias, { kind: "remote", baseUrl: options.remoteUrl });
|
|
1298
1452
|
}
|
|
1299
1453
|
writeRepoConnection(projectRoot, {
|
|
@@ -1338,7 +1492,7 @@ async function runControlPlaneInit(context, options) {
|
|
|
1338
1492
|
const authMethod = options.githubAuthMethod ?? (options.githubToken ? "token" : "skip");
|
|
1339
1493
|
const remoteGhTokenWarning = serverKind === "remote" && authMethod === "gh" ? `This sends a GitHub token from this machine to ${options.remoteUrl ?? "the remote Rig server"}.` : null;
|
|
1340
1494
|
if (remoteGhTokenWarning && !options.yes) {
|
|
1341
|
-
throw new
|
|
1495
|
+
throw new CliError(`${remoteGhTokenWarning} Re-run with --yes to confirm this explicit token transfer.`, 1);
|
|
1342
1496
|
}
|
|
1343
1497
|
const token = authMethod === "gh" && !options.githubToken ? readGhAuthToken() : options.githubToken?.trim();
|
|
1344
1498
|
if (token) {
|
|
@@ -1450,6 +1604,8 @@ async function runControlPlaneInit(context, options) {
|
|
|
1450
1604
|
}
|
|
1451
1605
|
function parseInitOptions(args) {
|
|
1452
1606
|
let rest = [...args];
|
|
1607
|
+
const demo = takeFlag(rest, "--demo");
|
|
1608
|
+
rest = demo.rest;
|
|
1453
1609
|
const yes = takeFlag(rest, "--yes");
|
|
1454
1610
|
rest = yes.rest;
|
|
1455
1611
|
const repair = takeFlag(rest, "--repair");
|
|
@@ -1479,6 +1635,7 @@ function parseInitOptions(args) {
|
|
|
1479
1635
|
const ref = takeOption(rest, "--ref");
|
|
1480
1636
|
rest = ref.rest;
|
|
1481
1637
|
const options = {
|
|
1638
|
+
demo: demo.value,
|
|
1482
1639
|
yes: yes.value,
|
|
1483
1640
|
repair: repair.value,
|
|
1484
1641
|
privateStateOnly: privateStateOnly.value,
|
|
@@ -1492,10 +1649,10 @@ function parseInitOptions(args) {
|
|
|
1492
1649
|
githubProjectStatusField: githubProjectStatusField.value
|
|
1493
1650
|
};
|
|
1494
1651
|
if (server.value && options.server === undefined) {
|
|
1495
|
-
throw new
|
|
1652
|
+
throw new CliError("--server must be local or remote.", 1);
|
|
1496
1653
|
}
|
|
1497
1654
|
if (githubAuth.value && !["gh", "token", "device", "skip"].includes(githubAuth.value)) {
|
|
1498
|
-
throw new
|
|
1655
|
+
throw new CliError("--github-auth must be gh, token, device, or skip.", 1);
|
|
1499
1656
|
}
|
|
1500
1657
|
if (remoteCheckout.value) {
|
|
1501
1658
|
if (remoteCheckout.value === "managed-clone")
|
|
@@ -1506,10 +1663,10 @@ function parseInitOptions(args) {
|
|
|
1506
1663
|
options.remoteCheckout = { kind: "uploaded-snapshot" };
|
|
1507
1664
|
else if (remoteCheckout.value === "existing-path") {
|
|
1508
1665
|
if (!existingPath.value)
|
|
1509
|
-
throw new
|
|
1666
|
+
throw new CliError("--remote-checkout existing-path requires --existing-path <path>.", 1);
|
|
1510
1667
|
options.remoteCheckout = { kind: "existing-path", path: existingPath.value };
|
|
1511
1668
|
} else {
|
|
1512
|
-
throw new
|
|
1669
|
+
throw new CliError("--remote-checkout must be managed-clone, current-ref, uploaded-snapshot, or existing-path.", 1);
|
|
1513
1670
|
}
|
|
1514
1671
|
}
|
|
1515
1672
|
return { options, rest };
|
|
@@ -1586,13 +1743,13 @@ async function runInteractiveControlPlaneInit(context, prompts) {
|
|
|
1586
1743
|
let remoteGhTokenConfirmed = false;
|
|
1587
1744
|
if (serverChoice === "remote" && authMethod === "gh") {
|
|
1588
1745
|
if (!prompts.confirm)
|
|
1589
|
-
throw new
|
|
1746
|
+
throw new CliError("Remote gh-token import requires explicit confirmation.", 1);
|
|
1590
1747
|
const confirmed = await prompts.confirm({
|
|
1591
1748
|
message: `This sends a GitHub token from this machine to ${remoteUrl}. Continue?`,
|
|
1592
1749
|
initialValue: true
|
|
1593
1750
|
});
|
|
1594
1751
|
if (prompts.isCancel(confirmed) || confirmed !== true) {
|
|
1595
|
-
throw new
|
|
1752
|
+
throw new CliError("Remote gh-token import cancelled.", 1);
|
|
1596
1753
|
}
|
|
1597
1754
|
remoteGhTokenConfirmed = true;
|
|
1598
1755
|
}
|
|
@@ -1621,22 +1778,32 @@ async function runInteractiveControlPlaneInit(context, prompts) {
|
|
|
1621
1778
|
}
|
|
1622
1779
|
async function executeInit(context, args) {
|
|
1623
1780
|
const parsed = parseInitOptions(args);
|
|
1781
|
+
if (parsed.options.demo) {
|
|
1782
|
+
if (parsed.rest.length > 0) {
|
|
1783
|
+
throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
|
|
1784
|
+
Usage: rig init --demo [--yes] [--repair]`, 1, { hint: "Run `rig init --demo` (optionally with --repair to rewrite an existing rig.config)." });
|
|
1785
|
+
}
|
|
1786
|
+
return runDemoInit(context, parsed.options);
|
|
1787
|
+
}
|
|
1624
1788
|
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) {
|
|
1625
1789
|
if (parsed.rest.length > 0)
|
|
1626
|
-
throw new
|
|
1627
|
-
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);
|
|
1790
|
+
throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
|
|
1791
|
+
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);
|
|
1628
1792
|
return runControlPlaneInit(context, parsed.options);
|
|
1629
1793
|
}
|
|
1630
1794
|
if (parsed.rest.length > 0)
|
|
1631
|
-
throw new
|
|
1795
|
+
throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
|
|
1632
1796
|
Usage: rig init`, 1);
|
|
1633
1797
|
if (!process.stdin.isTTY) {
|
|
1634
|
-
throw new
|
|
1798
|
+
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);
|
|
1635
1799
|
}
|
|
1636
1800
|
return runInteractiveControlPlaneInit(context, await loadClackPrompts());
|
|
1637
1801
|
}
|
|
1638
1802
|
export {
|
|
1639
1803
|
runInteractiveControlPlaneInit,
|
|
1804
|
+
runDemoInit,
|
|
1640
1805
|
executeInit,
|
|
1641
|
-
buildRigInitConfigSource
|
|
1806
|
+
buildRigInitConfigSource,
|
|
1807
|
+
DEMO_TASKS_RELATIVE_DIR,
|
|
1808
|
+
DEMO_TASKS
|
|
1642
1809
|
};
|
|
@@ -5,10 +5,19 @@ import { resolve as resolve3 } from "path";
|
|
|
5
5
|
|
|
6
6
|
// packages/cli/src/runner.ts
|
|
7
7
|
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
8
|
-
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
8
|
+
import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
|
|
9
9
|
import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
|
|
10
10
|
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
class CliError extends RuntimeCliError {
|
|
13
|
+
hint;
|
|
14
|
+
constructor(message, exitCode = 1, options = {}) {
|
|
15
|
+
super(message, exitCode);
|
|
16
|
+
if (options.hint?.trim()) {
|
|
17
|
+
this.hint = options.hint.trim();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
12
21
|
function takeOption(args, option) {
|
|
13
22
|
const rest = [];
|
|
14
23
|
let value;
|
|
@@ -17,7 +26,7 @@ function takeOption(args, option) {
|
|
|
17
26
|
if (current === option) {
|
|
18
27
|
const next = args[index + 1];
|
|
19
28
|
if (!next || next.startsWith("-")) {
|
|
20
|
-
throw new CliError(`Missing value for ${option}`);
|
|
29
|
+
throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
|
|
21
30
|
}
|
|
22
31
|
value = next;
|
|
23
32
|
index += 1;
|
|
@@ -81,7 +90,7 @@ function readJsonFile(path) {
|
|
|
81
90
|
try {
|
|
82
91
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
83
92
|
} catch (error) {
|
|
84
|
-
throw new
|
|
93
|
+
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>`." });
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
96
|
function writeJsonFile(path, value) {
|
|
@@ -145,7 +154,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
|
|
|
145
154
|
const global = readGlobalConnections(options);
|
|
146
155
|
const connection = global.connections[repo.selected];
|
|
147
156
|
if (!connection) {
|
|
148
|
-
throw new
|
|
157
|
+
throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
|
|
149
158
|
}
|
|
150
159
|
return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
|
|
151
160
|
}
|
|
@@ -218,7 +227,7 @@ async function ensureServerForCli(projectRoot) {
|
|
|
218
227
|
};
|
|
219
228
|
} catch (error) {
|
|
220
229
|
if (error instanceof Error) {
|
|
221
|
-
throw new
|
|
230
|
+
throw new CliError(error.message, 1);
|
|
222
231
|
}
|
|
223
232
|
throw error;
|
|
224
233
|
}
|
|
@@ -268,15 +277,64 @@ function diagnosticMessage(payload) {
|
|
|
268
277
|
});
|
|
269
278
|
return messages.length > 0 ? messages.join("; ") : null;
|
|
270
279
|
}
|
|
280
|
+
var serverReachabilityCache = new Map;
|
|
281
|
+
async function probeServerReachability(baseUrl, authToken) {
|
|
282
|
+
try {
|
|
283
|
+
const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
|
|
284
|
+
headers: mergeHeaders(undefined, authToken),
|
|
285
|
+
signal: AbortSignal.timeout(1500)
|
|
286
|
+
});
|
|
287
|
+
return response.ok;
|
|
288
|
+
} catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function cachedServerReachability(projectRoot, baseUrl, authToken) {
|
|
293
|
+
const key = resolve2(projectRoot);
|
|
294
|
+
const cached = serverReachabilityCache.get(key);
|
|
295
|
+
if (cached)
|
|
296
|
+
return cached;
|
|
297
|
+
const probe = probeServerReachability(baseUrl, authToken);
|
|
298
|
+
serverReachabilityCache.set(key, probe);
|
|
299
|
+
return probe;
|
|
300
|
+
}
|
|
301
|
+
function describeSelectedServer(projectRoot, server) {
|
|
302
|
+
try {
|
|
303
|
+
const selected = resolveSelectedConnection(projectRoot);
|
|
304
|
+
if (selected) {
|
|
305
|
+
return {
|
|
306
|
+
alias: selected.alias,
|
|
307
|
+
target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
} catch {}
|
|
311
|
+
return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
|
|
312
|
+
}
|
|
313
|
+
async function buildServerFailureContext(projectRoot, server) {
|
|
314
|
+
const { alias, target } = describeSelectedServer(projectRoot, server);
|
|
315
|
+
const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
|
|
316
|
+
const reachability = reachable ? "server is reachable" : "server is unreachable";
|
|
317
|
+
return {
|
|
318
|
+
contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
|
|
319
|
+
hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
|
|
320
|
+
};
|
|
321
|
+
}
|
|
271
322
|
async function requestServerJson(context, pathname, init = {}) {
|
|
272
323
|
const server = await ensureServerForCli(context.projectRoot);
|
|
273
324
|
const headers = mergeHeaders(init.headers, server.authToken);
|
|
274
325
|
if (server.serverProjectRoot)
|
|
275
326
|
headers.set("x-rig-project-root", server.serverProjectRoot);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
327
|
+
let response;
|
|
328
|
+
try {
|
|
329
|
+
response = await fetch(`${server.baseUrl}${pathname}`, {
|
|
330
|
+
...init,
|
|
331
|
+
headers
|
|
332
|
+
});
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const failure = await buildServerFailureContext(context.projectRoot, server);
|
|
335
|
+
throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
|
|
336
|
+
${failure.contextLine}`, 1, { hint: failure.hint });
|
|
337
|
+
}
|
|
280
338
|
const text = await response.text();
|
|
281
339
|
const payload = text.trim().length > 0 ? (() => {
|
|
282
340
|
try {
|
|
@@ -288,7 +346,9 @@ async function requestServerJson(context, pathname, init = {}) {
|
|
|
288
346
|
if (!response.ok) {
|
|
289
347
|
const diagnostics = diagnosticMessage(payload);
|
|
290
348
|
const detail = diagnostics ?? (text || response.statusText);
|
|
291
|
-
|
|
349
|
+
const failure = await buildServerFailureContext(context.projectRoot, server);
|
|
350
|
+
throw new CliError(`Rig server request failed (${response.status}): ${detail}
|
|
351
|
+
${failure.contextLine}`, 1, { hint: failure.hint });
|
|
292
352
|
}
|
|
293
353
|
return payload;
|
|
294
354
|
}
|
|
@@ -323,7 +383,7 @@ async function executeInspect(context, args) {
|
|
|
323
383
|
const serverRuns = await listRunsViaServer(context, { limit: 200 }).catch(() => []);
|
|
324
384
|
const serverRun = serverRuns.filter((run) => String(run.taskId ?? "") === requiredTask).sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")))[0];
|
|
325
385
|
if (!serverRun || typeof serverRun.runId !== "string") {
|
|
326
|
-
throw new
|
|
386
|
+
throw new CliError(`No runs found for ${requiredTask} (local or on the selected server).`, 1, { hint: "Start one with `rig task run --task <id>`, or check `rig run list`." });
|
|
327
387
|
}
|
|
328
388
|
const page = await getRunLogsViaServer(context, serverRun.runId, { limit: 500 });
|
|
329
389
|
const entries = Array.isArray(page.entries) ? page.entries : [];
|
|
@@ -341,7 +401,7 @@ async function executeInspect(context, args) {
|
|
|
341
401
|
}
|
|
342
402
|
const logsPath = resolve3(resolveAuthorityRunDir(context.projectRoot, latestRun.runId), "logs.jsonl");
|
|
343
403
|
if (!existsSync3(logsPath)) {
|
|
344
|
-
throw new
|
|
404
|
+
throw new CliError(`No logs found for run ${latestRun.runId}.`, 1, { hint: `Try \`rig run show ${latestRun.runId}\` or \`rig run replay ${latestRun.runId}\`.` });
|
|
345
405
|
}
|
|
346
406
|
await context.runCommand(["cat", logsPath]);
|
|
347
407
|
return { ok: true, group: "inspect", command, details: { task: requiredTask, runId: latestRun.runId } };
|
|
@@ -352,7 +412,7 @@ async function executeInspect(context, args) {
|
|
|
352
412
|
const requiredTask = requireTask(task, "rig inspect artifacts --task <task-id>");
|
|
353
413
|
const artifactRoot = resolveTaskArtifactDirs(context.projectRoot, requiredTask).find((path) => existsSync3(path));
|
|
354
414
|
if (!artifactRoot) {
|
|
355
|
-
throw new
|
|
415
|
+
throw new CliError(`No artifacts found for ${requiredTask}.`, 1, { hint: "Artifacts appear after a run completes; check `rig run list` for run state." });
|
|
356
416
|
}
|
|
357
417
|
await context.runCommand(["ls", "-la", artifactRoot]);
|
|
358
418
|
return { ok: true, group: "inspect", command, details: { task: requiredTask } };
|
|
@@ -366,7 +426,7 @@ async function executeInspect(context, args) {
|
|
|
366
426
|
requireNoExtraArgs(previewPending, "rig inspect artifact --task <task-id> --file <name>");
|
|
367
427
|
const requiredTask = requireTask(task.value, "rig inspect artifact --task <task-id> --file <name>");
|
|
368
428
|
if (!file.value) {
|
|
369
|
-
throw new
|
|
429
|
+
throw new CliError("Missing --file for rig inspect artifact.", 1, { hint: "List artifact files first: `rig inspect artifacts --task <id>`, then pass one with --file." });
|
|
370
430
|
}
|
|
371
431
|
const preview = readTaskArtifactPreview(context.projectRoot, requiredTask, file.value);
|
|
372
432
|
if (context.outputMode === "text") {
|
|
@@ -420,7 +480,7 @@ async function executeInspect(context, args) {
|
|
|
420
480
|
const monorepoRoot = resolveMonorepoRoot(context.projectRoot);
|
|
421
481
|
const result = runCapture(["br", "--no-db", "list", "--pretty"], monorepoRoot);
|
|
422
482
|
if (result.exitCode !== 0) {
|
|
423
|
-
throw new
|
|
483
|
+
throw new CliError(result.stderr || result.stdout || "Failed to inspect graph");
|
|
424
484
|
}
|
|
425
485
|
process.stdout.write(result.stdout);
|
|
426
486
|
}
|
|
@@ -439,7 +499,7 @@ async function executeInspect(context, args) {
|
|
|
439
499
|
return { ok: true, group: "inspect", command };
|
|
440
500
|
}
|
|
441
501
|
default:
|
|
442
|
-
throw new
|
|
502
|
+
throw new CliError(`Unknown inspect command: ${command}`, 1, { hint: "Run `rig inspect --help` to list inspect commands." });
|
|
443
503
|
}
|
|
444
504
|
}
|
|
445
505
|
export {
|