@h-rig/cli 0.0.6-alpha.0
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 +30 -0
- package/dist/bin/build-rig-binaries.js +107 -0
- package/dist/bin/rig.js +9330 -0
- package/dist/src/commands/_authority-runs.js +110 -0
- package/dist/src/commands/_connection-state.js +123 -0
- package/dist/src/commands/_doctor-checks.js +501 -0
- package/dist/src/commands/_operator-view.js +322 -0
- package/dist/src/commands/_parsers.js +107 -0
- package/dist/src/commands/_paths.js +50 -0
- package/dist/src/commands/_pi-install.js +184 -0
- package/dist/src/commands/_policy.js +79 -0
- package/dist/src/commands/_preflight.js +460 -0
- package/dist/src/commands/_probes.js +13 -0
- package/dist/src/commands/_run-driver-helpers.js +289 -0
- package/dist/src/commands/_server-client.js +364 -0
- package/dist/src/commands/_snapshot-upload.js +313 -0
- package/dist/src/commands/_task-picker.js +48 -0
- package/dist/src/commands/agent.js +497 -0
- package/dist/src/commands/browser.js +890 -0
- package/dist/src/commands/connect.js +180 -0
- package/dist/src/commands/dist.js +402 -0
- package/dist/src/commands/doctor.js +511 -0
- package/dist/src/commands/github.js +276 -0
- package/dist/src/commands/inbox.js +160 -0
- package/dist/src/commands/init.js +1254 -0
- package/dist/src/commands/inspect.js +174 -0
- package/dist/src/commands/inspector.js +256 -0
- package/dist/src/commands/plugin.js +167 -0
- package/dist/src/commands/profile-and-review.js +178 -0
- package/dist/src/commands/queue.js +197 -0
- package/dist/src/commands/remote.js +507 -0
- package/dist/src/commands/repo-git-harness.js +221 -0
- package/dist/src/commands/run.js +753 -0
- package/dist/src/commands/server.js +368 -0
- package/dist/src/commands/setup.js +681 -0
- package/dist/src/commands/task-report-bug.js +1083 -0
- package/dist/src/commands/task-run-driver.js +1933 -0
- package/dist/src/commands/task.js +1325 -0
- package/dist/src/commands/test.js +39 -0
- package/dist/src/commands/workspace.js +123 -0
- package/dist/src/commands.js +9012 -0
- package/dist/src/index.js +9348 -0
- package/dist/src/launcher.js +131 -0
- package/dist/src/report-bug.js +260 -0
- package/dist/src/runner.js +272 -0
- package/dist/src/withMutedConsole.js +42 -0
- package/package.json +31 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli/src/commands/_operator-view.ts
|
|
3
|
+
import { createInterface } from "readline";
|
|
4
|
+
|
|
5
|
+
// packages/cli/src/commands/_server-client.ts
|
|
6
|
+
import { spawnSync } from "child_process";
|
|
7
|
+
|
|
8
|
+
// packages/cli/src/runner.ts
|
|
9
|
+
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
10
|
+
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
11
|
+
import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
|
|
12
|
+
import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
|
|
13
|
+
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
14
|
+
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
15
|
+
import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
|
|
16
|
+
|
|
17
|
+
// packages/cli/src/commands/_server-client.ts
|
|
18
|
+
import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
|
|
19
|
+
|
|
20
|
+
// packages/cli/src/commands/_connection-state.ts
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
22
|
+
import { homedir } from "os";
|
|
23
|
+
import { dirname, resolve } from "path";
|
|
24
|
+
function resolveGlobalConnectionsPath(env = process.env) {
|
|
25
|
+
const explicit = env.RIG_CONNECTIONS_FILE?.trim();
|
|
26
|
+
if (explicit)
|
|
27
|
+
return resolve(explicit);
|
|
28
|
+
const stateDir = env.RIG_GLOBAL_STATE_DIR?.trim();
|
|
29
|
+
if (stateDir)
|
|
30
|
+
return resolve(stateDir, "connections.json");
|
|
31
|
+
return resolve(homedir(), ".rig", "connections.json");
|
|
32
|
+
}
|
|
33
|
+
function resolveRepoConnectionPath(projectRoot) {
|
|
34
|
+
return resolve(projectRoot, ".rig", "state", "connection.json");
|
|
35
|
+
}
|
|
36
|
+
function readJsonFile(path) {
|
|
37
|
+
if (!existsSync(path))
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function normalizeConnection(value) {
|
|
46
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
47
|
+
return null;
|
|
48
|
+
const record = value;
|
|
49
|
+
if (record.kind === "local")
|
|
50
|
+
return { kind: "local", mode: "auto" };
|
|
51
|
+
if (record.kind === "remote" && typeof record.baseUrl === "string" && record.baseUrl.trim()) {
|
|
52
|
+
const baseUrl = record.baseUrl.trim().replace(/\/+$/, "");
|
|
53
|
+
return { kind: "remote", baseUrl };
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function readGlobalConnections(options = {}) {
|
|
58
|
+
const path = resolveGlobalConnectionsPath(options.env ?? process.env);
|
|
59
|
+
const payload = readJsonFile(path);
|
|
60
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
61
|
+
return { connections: {} };
|
|
62
|
+
}
|
|
63
|
+
const rawConnections = payload.connections;
|
|
64
|
+
const connections = {};
|
|
65
|
+
if (rawConnections && typeof rawConnections === "object" && !Array.isArray(rawConnections)) {
|
|
66
|
+
for (const [alias, raw] of Object.entries(rawConnections)) {
|
|
67
|
+
const connection = normalizeConnection(raw);
|
|
68
|
+
if (connection)
|
|
69
|
+
connections[alias] = connection;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { connections };
|
|
73
|
+
}
|
|
74
|
+
function readRepoConnection(projectRoot) {
|
|
75
|
+
const payload = readJsonFile(resolveRepoConnectionPath(projectRoot));
|
|
76
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
77
|
+
return null;
|
|
78
|
+
const record = payload;
|
|
79
|
+
const selected = typeof record.selected === "string" ? record.selected.trim() : "";
|
|
80
|
+
if (!selected)
|
|
81
|
+
return null;
|
|
82
|
+
return {
|
|
83
|
+
selected,
|
|
84
|
+
project: typeof record.project === "string" ? record.project : undefined,
|
|
85
|
+
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function resolveSelectedConnection(projectRoot, options = {}) {
|
|
89
|
+
const repo = readRepoConnection(projectRoot);
|
|
90
|
+
if (!repo)
|
|
91
|
+
return null;
|
|
92
|
+
if (repo.selected === "local")
|
|
93
|
+
return { alias: "local", connection: { kind: "local", mode: "auto" } };
|
|
94
|
+
const global = readGlobalConnections(options);
|
|
95
|
+
const connection = global.connections[repo.selected];
|
|
96
|
+
if (!connection) {
|
|
97
|
+
throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
|
|
98
|
+
}
|
|
99
|
+
return { alias: repo.selected, connection };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// packages/cli/src/commands/_server-client.ts
|
|
103
|
+
var cachedGitHubBearerToken;
|
|
104
|
+
function cleanToken(value) {
|
|
105
|
+
const trimmed = value?.trim();
|
|
106
|
+
return trimmed ? trimmed : null;
|
|
107
|
+
}
|
|
108
|
+
function readGitHubBearerTokenForRemote() {
|
|
109
|
+
if (cachedGitHubBearerToken !== undefined)
|
|
110
|
+
return cachedGitHubBearerToken;
|
|
111
|
+
const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
|
|
112
|
+
if (envToken) {
|
|
113
|
+
cachedGitHubBearerToken = envToken;
|
|
114
|
+
return cachedGitHubBearerToken;
|
|
115
|
+
}
|
|
116
|
+
const result = spawnSync("gh", ["auth", "token"], {
|
|
117
|
+
encoding: "utf8",
|
|
118
|
+
timeout: 5000,
|
|
119
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
120
|
+
});
|
|
121
|
+
cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
|
|
122
|
+
return cachedGitHubBearerToken;
|
|
123
|
+
}
|
|
124
|
+
async function ensureServerForCli(projectRoot) {
|
|
125
|
+
try {
|
|
126
|
+
const selected = resolveSelectedConnection(projectRoot);
|
|
127
|
+
if (selected?.connection.kind === "remote") {
|
|
128
|
+
return {
|
|
129
|
+
baseUrl: selected.connection.baseUrl,
|
|
130
|
+
authToken: readGitHubBearerTokenForRemote(),
|
|
131
|
+
connectionKind: "remote"
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const connection = await ensureLocalRigServerConnection(projectRoot);
|
|
135
|
+
return {
|
|
136
|
+
baseUrl: connection.baseUrl,
|
|
137
|
+
authToken: connection.authToken,
|
|
138
|
+
connectionKind: "local"
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error instanceof Error) {
|
|
142
|
+
throw new CliError2(error.message, 1);
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function mergeHeaders(headers, authToken) {
|
|
148
|
+
const merged = new Headers(headers);
|
|
149
|
+
if (authToken) {
|
|
150
|
+
merged.set("authorization", `Bearer ${authToken}`);
|
|
151
|
+
}
|
|
152
|
+
return merged;
|
|
153
|
+
}
|
|
154
|
+
function diagnosticMessage(payload) {
|
|
155
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
156
|
+
return null;
|
|
157
|
+
const record = payload;
|
|
158
|
+
const diagnostics = Array.isArray(record.diagnostics) ? record.diagnostics : [];
|
|
159
|
+
const messages = diagnostics.flatMap((entry) => {
|
|
160
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
161
|
+
return [];
|
|
162
|
+
const diagnostic = entry;
|
|
163
|
+
const kind = typeof diagnostic.kind === "string" ? diagnostic.kind : "task-source";
|
|
164
|
+
const message = typeof diagnostic.message === "string" ? diagnostic.message : null;
|
|
165
|
+
return message ? [`${kind}: ${message}`] : [];
|
|
166
|
+
});
|
|
167
|
+
return messages.length > 0 ? messages.join("; ") : null;
|
|
168
|
+
}
|
|
169
|
+
async function requestServerJson(context, pathname, init = {}) {
|
|
170
|
+
const server = await ensureServerForCli(context.projectRoot);
|
|
171
|
+
const response = await fetch(`${server.baseUrl}${pathname}`, {
|
|
172
|
+
...init,
|
|
173
|
+
headers: mergeHeaders(init.headers, server.authToken)
|
|
174
|
+
});
|
|
175
|
+
const text = await response.text();
|
|
176
|
+
const payload = text.trim().length > 0 ? (() => {
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(text);
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
})() : null;
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
const diagnostics = diagnosticMessage(payload);
|
|
185
|
+
const detail = diagnostics ?? (text || response.statusText);
|
|
186
|
+
throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
|
|
187
|
+
}
|
|
188
|
+
return payload;
|
|
189
|
+
}
|
|
190
|
+
async function getRunDetailsViaServer(context, runId) {
|
|
191
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}`);
|
|
192
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
193
|
+
}
|
|
194
|
+
async function getRunLogsViaServer(context, runId, options = {}) {
|
|
195
|
+
const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/logs`);
|
|
196
|
+
if (options.limit !== undefined)
|
|
197
|
+
url.searchParams.set("limit", String(options.limit));
|
|
198
|
+
if (options.cursor)
|
|
199
|
+
url.searchParams.set("cursor", options.cursor);
|
|
200
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
201
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
202
|
+
}
|
|
203
|
+
async function stopRunViaServer(context, runId) {
|
|
204
|
+
const payload = await requestServerJson(context, "/api/runs/stop", {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: { "content-type": "application/json" },
|
|
207
|
+
body: JSON.stringify({ runId })
|
|
208
|
+
});
|
|
209
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true, runId };
|
|
210
|
+
}
|
|
211
|
+
async function steerRunViaServer(context, runId, message) {
|
|
212
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/steer`, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: { "content-type": "application/json" },
|
|
215
|
+
body: JSON.stringify({ message })
|
|
216
|
+
});
|
|
217
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// packages/cli/src/commands/_operator-view.ts
|
|
221
|
+
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
222
|
+
var CANONICAL_STAGES = [
|
|
223
|
+
"Connect",
|
|
224
|
+
"GitHub/task sync",
|
|
225
|
+
"Prepare workspace",
|
|
226
|
+
"Launch Pi",
|
|
227
|
+
"Plan",
|
|
228
|
+
"Implement",
|
|
229
|
+
"Validate",
|
|
230
|
+
"Commit",
|
|
231
|
+
"Open PR",
|
|
232
|
+
"Review/CI",
|
|
233
|
+
"Merge",
|
|
234
|
+
"Complete"
|
|
235
|
+
];
|
|
236
|
+
function renderOperatorSnapshot(snapshot) {
|
|
237
|
+
const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
|
|
238
|
+
const runId = String(run.runId ?? run.id ?? "run");
|
|
239
|
+
const status = String(run.status ?? "unknown");
|
|
240
|
+
const logs = snapshot.logs ?? [];
|
|
241
|
+
const stageLines = CANONICAL_STAGES.flatMap((stage) => {
|
|
242
|
+
const match = logs.find((log) => String(log.title ?? "").toLowerCase() === stage.toLowerCase() || String(log.stage ?? "").toLowerCase() === stage.toLowerCase());
|
|
243
|
+
return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
|
|
244
|
+
});
|
|
245
|
+
return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
|
|
246
|
+
`);
|
|
247
|
+
}
|
|
248
|
+
function runStatusFromPayload(payload) {
|
|
249
|
+
const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
250
|
+
return String(run.status ?? "unknown").toLowerCase();
|
|
251
|
+
}
|
|
252
|
+
async function applyOperatorCommand(context, input, deps = {}) {
|
|
253
|
+
const line = input.line.trim();
|
|
254
|
+
if (!line)
|
|
255
|
+
return { action: "ignored" };
|
|
256
|
+
if (line === "/detach" || line === "/quit" || line === "/q") {
|
|
257
|
+
return { action: "detach", message: "Detached from run." };
|
|
258
|
+
}
|
|
259
|
+
if (line === "/stop") {
|
|
260
|
+
await (deps.stop ?? stopRunViaServer)(context, input.runId);
|
|
261
|
+
return { action: "stopped", message: "Stop requested." };
|
|
262
|
+
}
|
|
263
|
+
const userMessage = line.startsWith("/user ") ? line.slice("/user ".length).trim() : line;
|
|
264
|
+
if (!userMessage)
|
|
265
|
+
return { action: "ignored" };
|
|
266
|
+
await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
|
|
267
|
+
return { action: "continue", message: "Steering message queued." };
|
|
268
|
+
}
|
|
269
|
+
async function readOperatorSnapshot(context, runId) {
|
|
270
|
+
const run = await getRunDetailsViaServer(context, runId);
|
|
271
|
+
const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
|
|
272
|
+
const entries = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
273
|
+
return { run, logs: entries, rendered: renderOperatorSnapshot({ run, logs: entries }) };
|
|
274
|
+
}
|
|
275
|
+
async function attachRunOperatorView(context, input) {
|
|
276
|
+
let steered = false;
|
|
277
|
+
if (input.message?.trim()) {
|
|
278
|
+
await steerRunViaServer(context, input.runId, input.message.trim());
|
|
279
|
+
steered = true;
|
|
280
|
+
}
|
|
281
|
+
let snapshot = await readOperatorSnapshot(context, input.runId);
|
|
282
|
+
if (context.outputMode === "text") {
|
|
283
|
+
console.log(snapshot.rendered);
|
|
284
|
+
if (steered)
|
|
285
|
+
console.log("Steering message queued.");
|
|
286
|
+
}
|
|
287
|
+
let detached = false;
|
|
288
|
+
let rl = null;
|
|
289
|
+
if (input.follow && !input.once && context.outputMode === "text") {
|
|
290
|
+
if (input.interactive !== false && process.stdin.isTTY) {
|
|
291
|
+
console.log("Controls: /user <message>, /stop, /detach");
|
|
292
|
+
rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
293
|
+
rl.on("line", (line) => {
|
|
294
|
+
applyOperatorCommand(context, { runId: input.runId, line }).then((result) => {
|
|
295
|
+
if (result.message)
|
|
296
|
+
console.log(result.message);
|
|
297
|
+
if (result.action === "detach" || result.action === "stopped") {
|
|
298
|
+
detached = true;
|
|
299
|
+
rl?.close();
|
|
300
|
+
}
|
|
301
|
+
}).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
let lastRendered = snapshot.rendered;
|
|
305
|
+
const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
|
|
306
|
+
while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
|
|
307
|
+
await Bun.sleep(pollMs);
|
|
308
|
+
snapshot = await readOperatorSnapshot(context, input.runId);
|
|
309
|
+
if (snapshot.rendered !== lastRendered) {
|
|
310
|
+
console.log(snapshot.rendered);
|
|
311
|
+
lastRendered = snapshot.rendered;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
rl?.close();
|
|
315
|
+
}
|
|
316
|
+
return { ...snapshot, steered, detached };
|
|
317
|
+
}
|
|
318
|
+
export {
|
|
319
|
+
renderOperatorSnapshot,
|
|
320
|
+
attachRunOperatorView,
|
|
321
|
+
applyOperatorCommand
|
|
322
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// packages/cli/src/commands/_parsers.ts
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
|
|
8
|
+
// packages/cli/src/runner.ts
|
|
9
|
+
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
10
|
+
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
11
|
+
import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
|
|
12
|
+
import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
|
|
13
|
+
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
14
|
+
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
15
|
+
import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
|
|
16
|
+
|
|
17
|
+
// packages/cli/src/commands/_parsers.ts
|
|
18
|
+
function parsePositiveInt(value, option, fallback) {
|
|
19
|
+
if (!value) {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
const parsed = Number.parseInt(value, 10);
|
|
23
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
24
|
+
throw new CliError2(`Invalid ${option} value: ${value}`);
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
function parseOptionalPositiveInt(value, option) {
|
|
29
|
+
if (!value) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const parsed = Number.parseInt(value, 10);
|
|
33
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
34
|
+
throw new CliError2(`Invalid ${option} value: ${value}`);
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
function parseRequiredPositiveInt(value, option) {
|
|
39
|
+
if (!value) {
|
|
40
|
+
throw new CliError2(`Missing value for ${option}.`);
|
|
41
|
+
}
|
|
42
|
+
const parsed = Number.parseInt(value, 10);
|
|
43
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
44
|
+
throw new CliError2(`Invalid ${option} value: ${value}`);
|
|
45
|
+
}
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
function parseAction(value) {
|
|
49
|
+
if (!value || value === "validate") {
|
|
50
|
+
return "validate";
|
|
51
|
+
}
|
|
52
|
+
if (value === "verify") {
|
|
53
|
+
return "verify";
|
|
54
|
+
}
|
|
55
|
+
if (value === "pipeline") {
|
|
56
|
+
return "pipeline";
|
|
57
|
+
}
|
|
58
|
+
throw new CliError2(`Invalid --action value: ${value}. Use validate, verify, or pipeline.`);
|
|
59
|
+
}
|
|
60
|
+
function parseIsolationMode(value, allowOff) {
|
|
61
|
+
if (!value) {
|
|
62
|
+
return "worktree";
|
|
63
|
+
}
|
|
64
|
+
if (value === "worktree") {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
if (allowOff && value === "off") {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
throw new CliError2(`Invalid isolation mode: ${value}. Use ${allowOff ? "off|" : ""}worktree.`);
|
|
71
|
+
}
|
|
72
|
+
function parseInstallScope(value) {
|
|
73
|
+
if (!value || value === "user") {
|
|
74
|
+
return "user";
|
|
75
|
+
}
|
|
76
|
+
if (value === "system") {
|
|
77
|
+
return "system";
|
|
78
|
+
}
|
|
79
|
+
throw new CliError2(`Invalid --scope value: ${value}. Use user|system.`);
|
|
80
|
+
}
|
|
81
|
+
function resolveInstallDir(scope, explicitPath) {
|
|
82
|
+
if (explicitPath) {
|
|
83
|
+
return resolve(explicitPath);
|
|
84
|
+
}
|
|
85
|
+
if (scope === "system") {
|
|
86
|
+
return "/usr/local/bin";
|
|
87
|
+
}
|
|
88
|
+
return resolve(homedir(), ".local/bin");
|
|
89
|
+
}
|
|
90
|
+
async function loadRigConfigOrNull(projectRoot) {
|
|
91
|
+
try {
|
|
92
|
+
const { loadConfig } = await import("@rig/core/load-config");
|
|
93
|
+
return await loadConfig(projectRoot);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
resolveInstallDir,
|
|
100
|
+
parseRequiredPositiveInt,
|
|
101
|
+
parsePositiveInt,
|
|
102
|
+
parseOptionalPositiveInt,
|
|
103
|
+
parseIsolationMode,
|
|
104
|
+
parseInstallScope,
|
|
105
|
+
parseAction,
|
|
106
|
+
loadRigConfigOrNull
|
|
107
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli/src/commands/_paths.ts
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { resolveMonorepoRoot } from "@rig/runtime/control-plane/native/utils";
|
|
5
|
+
function resolveControlPlaneMonorepoRoot(projectRoot) {
|
|
6
|
+
return resolveMonorepoRoot(projectRoot);
|
|
7
|
+
}
|
|
8
|
+
function resolveControlPlaneTaskConfigPath(projectRoot) {
|
|
9
|
+
return resolve(resolveControlPlaneMonorepoRoot(projectRoot), ".rig", "task-config.json");
|
|
10
|
+
}
|
|
11
|
+
function resolveControlPlaneHostStateRoot(projectRoot) {
|
|
12
|
+
return resolve(projectRoot, ".rig");
|
|
13
|
+
}
|
|
14
|
+
function resolveControlPlaneHostStateDir(projectRoot) {
|
|
15
|
+
return resolve(resolveControlPlaneHostStateRoot(projectRoot), "state");
|
|
16
|
+
}
|
|
17
|
+
function resolveControlPlaneHostLogsDir(projectRoot) {
|
|
18
|
+
return resolve(resolveControlPlaneHostStateRoot(projectRoot), "logs");
|
|
19
|
+
}
|
|
20
|
+
function resolveControlPlaneHostBinDir(projectRoot) {
|
|
21
|
+
return resolve(resolveControlPlaneHostStateRoot(projectRoot), "bin");
|
|
22
|
+
}
|
|
23
|
+
function resolveControlPlaneHostDistDir(projectRoot) {
|
|
24
|
+
return resolve(resolveControlPlaneHostStateRoot(projectRoot), "dist");
|
|
25
|
+
}
|
|
26
|
+
function resolveControlPlaneMonorepoStateRoot(projectRoot) {
|
|
27
|
+
return resolve(resolveControlPlaneMonorepoRoot(projectRoot), ".rig");
|
|
28
|
+
}
|
|
29
|
+
function resolveControlPlaneMonorepoRuntimeDir(projectRoot) {
|
|
30
|
+
return resolve(resolveControlPlaneMonorepoStateRoot(projectRoot), "runtime");
|
|
31
|
+
}
|
|
32
|
+
function resolveControlPlaneArtifactsDir(projectRoot) {
|
|
33
|
+
return resolve(resolveControlPlaneMonorepoRoot(projectRoot), "artifacts");
|
|
34
|
+
}
|
|
35
|
+
function resolveControlPlaneDefinitionRoot(projectRoot) {
|
|
36
|
+
return resolve(projectRoot, "rig");
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
resolveControlPlaneTaskConfigPath,
|
|
40
|
+
resolveControlPlaneMonorepoStateRoot,
|
|
41
|
+
resolveControlPlaneMonorepoRuntimeDir,
|
|
42
|
+
resolveControlPlaneMonorepoRoot,
|
|
43
|
+
resolveControlPlaneHostStateRoot,
|
|
44
|
+
resolveControlPlaneHostStateDir,
|
|
45
|
+
resolveControlPlaneHostLogsDir,
|
|
46
|
+
resolveControlPlaneHostDistDir,
|
|
47
|
+
resolveControlPlaneHostBinDir,
|
|
48
|
+
resolveControlPlaneDefinitionRoot,
|
|
49
|
+
resolveControlPlaneArtifactsDir
|
|
50
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli/src/commands/_pi-install.ts
|
|
3
|
+
import { existsSync, readFileSync, rmSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
|
|
7
|
+
var LEGACY_PI_RIG_MARKER = `// Managed by Rig. Source package: @rig/pi-rig.
|
|
8
|
+
export { default } from '@rig/pi-rig';
|
|
9
|
+
`;
|
|
10
|
+
async function defaultCommandRunner(command, options = {}) {
|
|
11
|
+
const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
|
|
12
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
13
|
+
new Response(proc.stdout).text(),
|
|
14
|
+
new Response(proc.stderr).text(),
|
|
15
|
+
proc.exited
|
|
16
|
+
]);
|
|
17
|
+
return { exitCode, stdout, stderr };
|
|
18
|
+
}
|
|
19
|
+
function resolvePiRigExtensionPath(homeDir) {
|
|
20
|
+
return resolve(homeDir, ".pi", "agent", "extensions", "pi-rig");
|
|
21
|
+
}
|
|
22
|
+
function resolvePiRigPackageSource(projectRoot, exists = existsSync) {
|
|
23
|
+
const localPackage = resolve(projectRoot, "packages", "pi-rig");
|
|
24
|
+
if (exists(resolve(localPackage, "package.json")))
|
|
25
|
+
return localPackage;
|
|
26
|
+
return `npm:${PI_RIG_PACKAGE_NAME}`;
|
|
27
|
+
}
|
|
28
|
+
function resolvePiHomeDir(inputHomeDir) {
|
|
29
|
+
return inputHomeDir ?? process.env.RIG_PI_HOME_DIR?.trim() ?? homedir();
|
|
30
|
+
}
|
|
31
|
+
function piListContainsPiRig(output) {
|
|
32
|
+
return output.split(/\r?\n/).some((line) => {
|
|
33
|
+
const normalized = line.trim();
|
|
34
|
+
return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function safeRun(runner, command, options) {
|
|
38
|
+
try {
|
|
39
|
+
return await runner(command, options);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return { exitCode: 1, stdout: "", stderr: error instanceof Error ? error.message : String(error) };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function splitInstallCommand(value) {
|
|
45
|
+
return value.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => part.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
46
|
+
}
|
|
47
|
+
async function ensurePiBinaryAvailable(input) {
|
|
48
|
+
const current = await safeRun(input.runner, ["pi", "--version"]);
|
|
49
|
+
if (current.exitCode === 0) {
|
|
50
|
+
const updateCommand = process.env.RIG_PI_UPDATE_COMMAND?.trim();
|
|
51
|
+
if (updateCommand) {
|
|
52
|
+
const parts2 = splitInstallCommand(updateCommand);
|
|
53
|
+
if (parts2.length > 0)
|
|
54
|
+
await safeRun(input.runner, parts2, input.projectRoot ? { cwd: input.projectRoot } : undefined);
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, detail: (current.stdout || current.stderr).trim() || undefined };
|
|
57
|
+
}
|
|
58
|
+
const installCommand = process.env.RIG_PI_INSTALL_COMMAND?.trim() || "bunx @earendil-works/pi@latest install";
|
|
59
|
+
const parts = splitInstallCommand(installCommand);
|
|
60
|
+
if (parts.length === 0) {
|
|
61
|
+
return { ok: false, error: (current.stderr || current.stdout).trim() || "pi --version failed" };
|
|
62
|
+
}
|
|
63
|
+
const install = await safeRun(input.runner, parts, input.projectRoot ? { cwd: input.projectRoot } : undefined);
|
|
64
|
+
if (install.exitCode !== 0) {
|
|
65
|
+
return { ok: false, installedOrUpdated: true, error: (install.stderr || install.stdout).trim() || `Pi install command failed (${install.exitCode})` };
|
|
66
|
+
}
|
|
67
|
+
const next = await safeRun(input.runner, ["pi", "--version"]);
|
|
68
|
+
return {
|
|
69
|
+
ok: next.exitCode === 0,
|
|
70
|
+
installedOrUpdated: true,
|
|
71
|
+
detail: (next.stdout || next.stderr).trim() || undefined,
|
|
72
|
+
...next.exitCode === 0 ? {} : { error: (next.stderr || next.stdout).trim() || "pi --version failed after install" }
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function removeManagedLegacyPiRigBridge(homeDir, exists = existsSync) {
|
|
76
|
+
const extensionPath = resolvePiRigExtensionPath(homeDir);
|
|
77
|
+
const indexPath = resolve(extensionPath, "index.ts");
|
|
78
|
+
if (!exists(indexPath))
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
const content = readFileSync(indexPath, "utf8");
|
|
82
|
+
if (content === LEGACY_PI_RIG_MARKER || content.includes("Managed by Rig. Source package: @rig/pi-rig")) {
|
|
83
|
+
rmSync(extensionPath, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
async function checkPiRigInstall(input = {}) {
|
|
88
|
+
const home = resolvePiHomeDir(input.homeDir);
|
|
89
|
+
const extensionPath = resolvePiRigExtensionPath(home);
|
|
90
|
+
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
91
|
+
return {
|
|
92
|
+
extensionPath,
|
|
93
|
+
pi: { ok: true, label: "pi", detail: "fake-pi" },
|
|
94
|
+
piRig: { ok: true, label: "pi-rig global extension", detail: extensionPath }
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const exists = input.exists ?? existsSync;
|
|
98
|
+
const runner = input.commandRunner ?? defaultCommandRunner;
|
|
99
|
+
const piResult = await safeRun(runner, ["pi", "--version"]);
|
|
100
|
+
const piListResult = piResult.exitCode === 0 ? await safeRun(runner, ["pi", "list"]) : { exitCode: 1, stdout: "", stderr: "" };
|
|
101
|
+
const listedPiRig = piListResult.exitCode === 0 && piListContainsPiRig(`${piListResult.stdout}
|
|
102
|
+
${piListResult.stderr}`);
|
|
103
|
+
const legacyBridge = exists(resolve(extensionPath, "index.ts"));
|
|
104
|
+
const hasPiRig = listedPiRig;
|
|
105
|
+
return {
|
|
106
|
+
extensionPath,
|
|
107
|
+
pi: {
|
|
108
|
+
ok: piResult.exitCode === 0,
|
|
109
|
+
label: "pi",
|
|
110
|
+
detail: (piResult.stdout || piResult.stderr).trim() || undefined,
|
|
111
|
+
hint: piResult.exitCode === 0 ? undefined : "Install Pi or run `rig init --yes` to install/update the Pi runtime."
|
|
112
|
+
},
|
|
113
|
+
piRig: {
|
|
114
|
+
ok: hasPiRig,
|
|
115
|
+
label: "pi-rig global extension",
|
|
116
|
+
detail: hasPiRig ? piListResult.stdout.trim() || PI_RIG_PACKAGE_NAME : legacyBridge ? `${extensionPath} (legacy bridge; reinstall required)` : undefined,
|
|
117
|
+
hint: hasPiRig ? undefined : "Run `rig init --yes` to install/enable the global pi-rig package with `pi install`."
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function ensurePiRigInstalled(input) {
|
|
122
|
+
const home = resolvePiHomeDir(input.homeDir);
|
|
123
|
+
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
124
|
+
const status2 = await checkPiRigInstall({ homeDir: home, commandRunner: input.commandRunner });
|
|
125
|
+
return { ...status2, installedPath: status2.extensionPath };
|
|
126
|
+
}
|
|
127
|
+
const runner = input.commandRunner ?? defaultCommandRunner;
|
|
128
|
+
const piAvailable = await ensurePiBinaryAvailable({ runner, projectRoot: input.projectRoot });
|
|
129
|
+
const status = piAvailable.ok ? await checkPiRigInstall({ homeDir: home, commandRunner: runner }) : {
|
|
130
|
+
extensionPath: resolvePiRigExtensionPath(home),
|
|
131
|
+
pi: { ok: false, label: "pi", detail: piAvailable.error, hint: "Install/update Pi with RIG_PI_INSTALL_COMMAND or install Pi manually." },
|
|
132
|
+
piRig: { ok: false, label: "pi-rig global extension", hint: "Pi is required before pi-rig can be installed." }
|
|
133
|
+
};
|
|
134
|
+
if (!piAvailable.ok) {
|
|
135
|
+
throw new Error(`Pi install/update failed: ${piAvailable.error ?? "pi unavailable"}`);
|
|
136
|
+
}
|
|
137
|
+
const packageSource = resolvePiRigPackageSource(input.projectRoot);
|
|
138
|
+
removeManagedLegacyPiRigBridge(home);
|
|
139
|
+
const install = await runner(["pi", "install", packageSource], { cwd: input.projectRoot });
|
|
140
|
+
if (install.exitCode !== 0) {
|
|
141
|
+
throw new Error(`pi-rig install failed: ${(install.stderr || install.stdout).trim() || `exit ${install.exitCode}`}`);
|
|
142
|
+
}
|
|
143
|
+
const next = await checkPiRigInstall({ homeDir: home, commandRunner: runner });
|
|
144
|
+
return { ...next, installedPath: packageSource };
|
|
145
|
+
}
|
|
146
|
+
async function ensureRemotePiRigInstalled(input) {
|
|
147
|
+
const payload = await input.requestJson("/api/pi-rig/install", {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: { "content-type": "application/json" },
|
|
150
|
+
body: JSON.stringify({ package: "@rig/pi-rig", scope: "global" })
|
|
151
|
+
});
|
|
152
|
+
const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
153
|
+
const piOk = record.piOk === true || record.ok === true;
|
|
154
|
+
const piRigOk = record.piRigOk === true || record.installed === true || record.ok === true;
|
|
155
|
+
const extensionPath = typeof record.extensionPath === "string" ? record.extensionPath : "remote:~/.pi/agent/extensions/pi-rig";
|
|
156
|
+
return {
|
|
157
|
+
remote: true,
|
|
158
|
+
extensionPath,
|
|
159
|
+
pi: {
|
|
160
|
+
ok: piOk,
|
|
161
|
+
label: "pi",
|
|
162
|
+
detail: typeof record.piVersion === "string" ? record.piVersion : undefined,
|
|
163
|
+
hint: piOk ? undefined : "Install/update Pi on the selected remote Rig server."
|
|
164
|
+
},
|
|
165
|
+
piRig: {
|
|
166
|
+
ok: piRigOk,
|
|
167
|
+
label: "pi-rig global extension",
|
|
168
|
+
detail: extensionPath,
|
|
169
|
+
hint: piRigOk ? undefined : "Install/enable pi-rig on the selected remote Rig server."
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
async function buildPiSetupChecks(input = {}) {
|
|
174
|
+
const status = await checkPiRigInstall(input);
|
|
175
|
+
return [status.pi, status.piRig];
|
|
176
|
+
}
|
|
177
|
+
export {
|
|
178
|
+
resolvePiRigPackageSource,
|
|
179
|
+
resolvePiRigExtensionPath,
|
|
180
|
+
ensureRemotePiRigInstalled,
|
|
181
|
+
ensurePiRigInstalled,
|
|
182
|
+
checkPiRigInstall,
|
|
183
|
+
buildPiSetupChecks
|
|
184
|
+
};
|