@h-rig/cli 0.0.6-alpha.2 → 0.0.6-alpha.21
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 +1288 -315
- package/dist/src/commands/_authority-runs.js +1 -0
- package/dist/src/commands/_cli-format.js +106 -0
- package/dist/src/commands/_doctor-checks.js +10 -22
- package/dist/src/commands/_operator-surface.js +204 -0
- package/dist/src/commands/_operator-view.js +207 -51
- package/dist/src/commands/_pi-install.js +4 -3
- package/dist/src/commands/_pi-session.js +253 -0
- package/dist/src/commands/_preflight.js +33 -28
- package/dist/src/commands/_server-client.js +80 -27
- package/dist/src/commands/_snapshot-upload.js +7 -20
- package/dist/src/commands/_task-picker.js +44 -16
- package/dist/src/commands/agent.js +2 -0
- package/dist/src/commands/doctor.js +10 -22
- package/dist/src/commands/github.js +9 -22
- package/dist/src/commands/init.js +295 -66
- package/dist/src/commands/queue.js +1 -0
- package/dist/src/commands/run.js +456 -95
- package/dist/src/commands/server.js +9 -22
- package/dist/src/commands/setup.js +10 -22
- package/dist/src/commands/task-run-driver.js +539 -64
- package/dist/src/commands/task.js +502 -130
- package/dist/src/commands.js +1276 -306
- package/dist/src/index.js +1288 -315
- package/dist/src/launcher.js +5 -3
- package/dist/src/runner.js +3 -2
- package/package.json +5 -4
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// packages/cli/src/commands/_operator-view.ts
|
|
3
|
-
import { createInterface } from "readline";
|
|
4
|
-
|
|
5
2
|
// packages/cli/src/commands/_server-client.ts
|
|
6
|
-
import { spawnSync } from "child_process";
|
|
7
3
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
8
4
|
import { resolve as resolve2 } from "path";
|
|
9
5
|
|
|
@@ -102,7 +98,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
|
|
|
102
98
|
}
|
|
103
99
|
|
|
104
100
|
// packages/cli/src/commands/_server-client.ts
|
|
105
|
-
var
|
|
101
|
+
var scopedGitHubBearerTokens = new Map;
|
|
106
102
|
function cleanToken(value) {
|
|
107
103
|
const trimmed = value?.trim();
|
|
108
104
|
return trimmed ? trimmed : null;
|
|
@@ -119,25 +115,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
|
|
|
119
115
|
}
|
|
120
116
|
}
|
|
121
117
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
const scopedKey = resolve2(projectRoot);
|
|
119
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
120
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
124
121
|
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
125
|
-
if (privateSession)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
|
|
130
|
-
if (envToken) {
|
|
131
|
-
cachedGitHubBearerToken = envToken;
|
|
132
|
-
return cachedGitHubBearerToken;
|
|
133
|
-
}
|
|
134
|
-
const result = spawnSync("gh", ["auth", "token"], {
|
|
135
|
-
encoding: "utf8",
|
|
136
|
-
timeout: 5000,
|
|
137
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
138
|
-
});
|
|
139
|
-
cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
|
|
140
|
-
return cachedGitHubBearerToken;
|
|
122
|
+
if (privateSession)
|
|
123
|
+
return privateSession;
|
|
124
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
141
125
|
}
|
|
142
126
|
async function ensureServerForCli(projectRoot) {
|
|
143
127
|
try {
|
|
@@ -218,6 +202,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
|
|
|
218
202
|
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
219
203
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
220
204
|
}
|
|
205
|
+
async function getRunTimelineViaServer(context, runId, options = {}) {
|
|
206
|
+
const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
|
|
207
|
+
if (options.limit !== undefined)
|
|
208
|
+
url.searchParams.set("limit", String(options.limit));
|
|
209
|
+
if (options.cursor)
|
|
210
|
+
url.searchParams.set("cursor", options.cursor);
|
|
211
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
212
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
213
|
+
}
|
|
221
214
|
async function stopRunViaServer(context, runId) {
|
|
222
215
|
const payload = await requestServerJson(context, "/api/runs/stop", {
|
|
223
216
|
method: "POST",
|
|
@@ -235,8 +228,8 @@ async function steerRunViaServer(context, runId, message) {
|
|
|
235
228
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
236
229
|
}
|
|
237
230
|
|
|
238
|
-
// packages/cli/src/commands/_operator-
|
|
239
|
-
|
|
231
|
+
// packages/cli/src/commands/_operator-surface.ts
|
|
232
|
+
import { createInterface } from "readline";
|
|
240
233
|
var CANONICAL_STAGES = [
|
|
241
234
|
"Connect",
|
|
242
235
|
"GitHub/task sync",
|
|
@@ -251,18 +244,168 @@ var CANONICAL_STAGES = [
|
|
|
251
244
|
"Merge",
|
|
252
245
|
"Complete"
|
|
253
246
|
];
|
|
247
|
+
function logDetail(log) {
|
|
248
|
+
return typeof log.detail === "string" ? log.detail.trim() : "";
|
|
249
|
+
}
|
|
250
|
+
function parseProviderProtocolLog(title, detail) {
|
|
251
|
+
if (title.trim().toLowerCase() !== "agent output")
|
|
252
|
+
return null;
|
|
253
|
+
if (!detail.startsWith("{") || !detail.endsWith("}"))
|
|
254
|
+
return null;
|
|
255
|
+
try {
|
|
256
|
+
const record = JSON.parse(detail);
|
|
257
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
258
|
+
return null;
|
|
259
|
+
const type = record.type;
|
|
260
|
+
return typeof type === "string" && [
|
|
261
|
+
"assistant",
|
|
262
|
+
"message_start",
|
|
263
|
+
"message_update",
|
|
264
|
+
"message_end",
|
|
265
|
+
"stream_event",
|
|
266
|
+
"tool_result",
|
|
267
|
+
"tool_execution_start",
|
|
268
|
+
"tool_execution_update",
|
|
269
|
+
"tool_execution_end",
|
|
270
|
+
"turn_start",
|
|
271
|
+
"turn_end"
|
|
272
|
+
].includes(type) ? record : null;
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function renderProviderProtocolLog(record) {
|
|
278
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
279
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
|
|
280
|
+
const toolName = String(record.toolName ?? record.name ?? "tool");
|
|
281
|
+
const status = type === "tool_execution_start" ? "started" : type === "tool_execution_end" ? record.isError === true || record.result && typeof record.result === "object" && !Array.isArray(record.result) && record.result.isError === true ? "failed" : "completed" : "running";
|
|
282
|
+
return `[Pi tool] ${toolName} ${status}`;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
function entryId(entry, fallback) {
|
|
287
|
+
return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
|
|
288
|
+
}
|
|
254
289
|
function renderOperatorSnapshot(snapshot) {
|
|
255
290
|
const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
|
|
256
291
|
const runId = String(run.runId ?? run.id ?? "run");
|
|
257
292
|
const status = String(run.status ?? "unknown");
|
|
258
293
|
const logs = snapshot.logs ?? [];
|
|
294
|
+
const latestByStage = new Map;
|
|
295
|
+
for (const log of logs) {
|
|
296
|
+
const title = String(log.title ?? "").toLowerCase();
|
|
297
|
+
const stageName = String(log.stage ?? "").toLowerCase();
|
|
298
|
+
const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
|
|
299
|
+
if (stage)
|
|
300
|
+
latestByStage.set(stage, log);
|
|
301
|
+
}
|
|
259
302
|
const stageLines = CANONICAL_STAGES.flatMap((stage) => {
|
|
260
|
-
const match =
|
|
261
|
-
return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
|
|
303
|
+
const match = latestByStage.get(stage);
|
|
304
|
+
return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
|
|
262
305
|
});
|
|
263
306
|
return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
|
|
264
307
|
`);
|
|
265
308
|
}
|
|
309
|
+
function createPiRunStreamRenderer(output = process.stdout) {
|
|
310
|
+
let lastSnapshot = "";
|
|
311
|
+
const assistantTextById = new Map;
|
|
312
|
+
const seenTimeline = new Set;
|
|
313
|
+
const seenLogs = new Set;
|
|
314
|
+
const writeLine = (line) => output.write(`${line}
|
|
315
|
+
`);
|
|
316
|
+
return {
|
|
317
|
+
renderSnapshot(snapshot) {
|
|
318
|
+
const rendered = renderOperatorSnapshot(snapshot);
|
|
319
|
+
if (rendered && rendered !== lastSnapshot) {
|
|
320
|
+
writeLine(rendered);
|
|
321
|
+
lastSnapshot = rendered;
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
renderTimeline(entries) {
|
|
325
|
+
for (const [index, entry] of entries.entries()) {
|
|
326
|
+
const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
|
|
327
|
+
if (entry.type === "assistant_message" && typeof entry.text === "string") {
|
|
328
|
+
const text = entry.text;
|
|
329
|
+
const previousText = assistantTextById.get(id) ?? "";
|
|
330
|
+
if (!previousText && text.trim()) {
|
|
331
|
+
writeLine("[Pi assistant]");
|
|
332
|
+
}
|
|
333
|
+
if (text.startsWith(previousText)) {
|
|
334
|
+
const delta = text.slice(previousText.length);
|
|
335
|
+
if (delta)
|
|
336
|
+
output.write(delta);
|
|
337
|
+
} else if (text.trim() && text !== previousText) {
|
|
338
|
+
if (previousText)
|
|
339
|
+
writeLine(`
|
|
340
|
+
[Pi assistant]`);
|
|
341
|
+
output.write(text);
|
|
342
|
+
}
|
|
343
|
+
assistantTextById.set(id, text);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (seenTimeline.has(id))
|
|
347
|
+
continue;
|
|
348
|
+
seenTimeline.add(id);
|
|
349
|
+
if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
|
|
350
|
+
writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (entry.type === "timeline_warning") {
|
|
354
|
+
writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
renderLogs(entries) {
|
|
359
|
+
for (const [index, entry] of entries.entries()) {
|
|
360
|
+
const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
|
|
361
|
+
if (seenLogs.has(id))
|
|
362
|
+
continue;
|
|
363
|
+
seenLogs.add(id);
|
|
364
|
+
const title = String(entry.title ?? "");
|
|
365
|
+
if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
|
|
366
|
+
continue;
|
|
367
|
+
const detail = logDetail(entry);
|
|
368
|
+
if (!detail)
|
|
369
|
+
continue;
|
|
370
|
+
const protocolRecord = parseProviderProtocolLog(title, detail);
|
|
371
|
+
if (protocolRecord) {
|
|
372
|
+
const protocolLine = renderProviderProtocolLog(protocolRecord);
|
|
373
|
+
if (protocolLine)
|
|
374
|
+
writeLine(protocolLine);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
writeLine(`[${title || "Rig log"}] ${detail}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function createOperatorSurface(options = {}) {
|
|
383
|
+
const input = options.input ?? process.stdin;
|
|
384
|
+
const output = options.output ?? process.stdout;
|
|
385
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
386
|
+
const renderer = createPiRunStreamRenderer(output);
|
|
387
|
+
const writeLine = (line) => output.write(`${line}
|
|
388
|
+
`);
|
|
389
|
+
return {
|
|
390
|
+
mode: "pi-compatible-text",
|
|
391
|
+
...renderer,
|
|
392
|
+
info: writeLine,
|
|
393
|
+
error: (message) => errorOutput.write(`${message}
|
|
394
|
+
`),
|
|
395
|
+
attachCommandInput(handler) {
|
|
396
|
+
if (options.interactive === false || !input.isTTY)
|
|
397
|
+
return null;
|
|
398
|
+
const rl = createInterface({ input, output: process.stdout, terminal: false });
|
|
399
|
+
rl.on("line", (line) => {
|
|
400
|
+
Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
401
|
+
});
|
|
402
|
+
return { close: () => rl.close() };
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// packages/cli/src/commands/_operator-view.ts
|
|
408
|
+
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
266
409
|
function runStatusFromPayload(payload) {
|
|
267
410
|
const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
268
411
|
return String(run.status ?? "unknown").toLowerCase();
|
|
@@ -284,11 +427,22 @@ async function applyOperatorCommand(context, input, deps = {}) {
|
|
|
284
427
|
await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
|
|
285
428
|
return { action: "continue", message: "Steering message queued." };
|
|
286
429
|
}
|
|
287
|
-
async function readOperatorSnapshot(context, runId) {
|
|
430
|
+
async function readOperatorSnapshot(context, runId, options = {}) {
|
|
288
431
|
const run = await getRunDetailsViaServer(context, runId);
|
|
289
432
|
const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
|
|
290
|
-
const
|
|
291
|
-
|
|
433
|
+
const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
|
|
434
|
+
entries: [{
|
|
435
|
+
id: `timeline-unavailable:${runId}`,
|
|
436
|
+
type: "timeline_warning",
|
|
437
|
+
detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
|
|
438
|
+
createdAt: new Date().toISOString()
|
|
439
|
+
}],
|
|
440
|
+
nextCursor: options.timelineCursor ?? null
|
|
441
|
+
}));
|
|
442
|
+
const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
|
|
443
|
+
const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
444
|
+
const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
|
|
445
|
+
return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
|
|
292
446
|
}
|
|
293
447
|
async function attachRunOperatorView(context, input) {
|
|
294
448
|
let steered = false;
|
|
@@ -296,45 +450,47 @@ async function attachRunOperatorView(context, input) {
|
|
|
296
450
|
await steerRunViaServer(context, input.runId, input.message.trim());
|
|
297
451
|
steered = true;
|
|
298
452
|
}
|
|
453
|
+
const surface = createOperatorSurface({ interactive: input.interactive !== false });
|
|
299
454
|
let snapshot = await readOperatorSnapshot(context, input.runId);
|
|
300
455
|
if (context.outputMode === "text") {
|
|
301
|
-
|
|
456
|
+
surface.renderSnapshot(snapshot);
|
|
457
|
+
surface.renderTimeline(snapshot.timeline);
|
|
458
|
+
surface.renderLogs(snapshot.logs);
|
|
302
459
|
if (steered)
|
|
303
|
-
|
|
460
|
+
surface.info("Steering message queued.");
|
|
304
461
|
}
|
|
305
462
|
let detached = false;
|
|
306
|
-
let
|
|
463
|
+
let commandInput = null;
|
|
307
464
|
if (input.follow && !input.once && context.outputMode === "text") {
|
|
308
465
|
if (input.interactive !== false && process.stdin.isTTY) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
}).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
466
|
+
surface.info("Controls: /user <message>, /stop, /detach");
|
|
467
|
+
commandInput = surface.attachCommandInput(async (line) => {
|
|
468
|
+
const result = await applyOperatorCommand(context, { runId: input.runId, line });
|
|
469
|
+
if (result.message)
|
|
470
|
+
surface.info(result.message);
|
|
471
|
+
if (result.action === "detach" || result.action === "stopped") {
|
|
472
|
+
detached = true;
|
|
473
|
+
commandInput?.close();
|
|
474
|
+
}
|
|
320
475
|
});
|
|
321
476
|
}
|
|
322
|
-
let lastRendered = snapshot.rendered;
|
|
323
477
|
const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
|
|
478
|
+
let timelineCursor = snapshot.timelineCursor;
|
|
324
479
|
while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
|
|
325
480
|
await Bun.sleep(pollMs);
|
|
326
|
-
snapshot = await readOperatorSnapshot(context, input.runId);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
481
|
+
snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
|
|
482
|
+
timelineCursor = snapshot.timelineCursor;
|
|
483
|
+
surface.renderSnapshot(snapshot);
|
|
484
|
+
surface.renderTimeline(snapshot.timeline);
|
|
485
|
+
surface.renderLogs(snapshot.logs);
|
|
331
486
|
}
|
|
332
|
-
|
|
487
|
+
commandInput?.close();
|
|
333
488
|
}
|
|
334
489
|
return { ...snapshot, steered, detached };
|
|
335
490
|
}
|
|
336
491
|
export {
|
|
337
492
|
renderOperatorSnapshot,
|
|
493
|
+
createPiRunStreamRenderer,
|
|
338
494
|
attachRunOperatorView,
|
|
339
495
|
applyOperatorCommand
|
|
340
496
|
};
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { existsSync, readFileSync, rmSync } from "fs";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { resolve } from "path";
|
|
6
|
-
var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
|
|
6
|
+
var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
|
|
7
|
+
var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
|
|
7
8
|
var LEGACY_PI_RIG_MARKER = `// Managed by Rig. Source package: @rig/pi-rig.
|
|
8
9
|
export { default } from '@rig/pi-rig';
|
|
9
10
|
`;
|
|
@@ -31,7 +32,7 @@ function resolvePiHomeDir(inputHomeDir) {
|
|
|
31
32
|
function piListContainsPiRig(output) {
|
|
32
33
|
return output.split(/\r?\n/).some((line) => {
|
|
33
34
|
const normalized = line.trim();
|
|
34
|
-
return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
|
|
35
|
+
return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
async function safeRun(runner, command, options) {
|
|
@@ -147,7 +148,7 @@ async function ensureRemotePiRigInstalled(input) {
|
|
|
147
148
|
const payload = await input.requestJson("/api/pi-rig/install", {
|
|
148
149
|
method: "POST",
|
|
149
150
|
headers: { "content-type": "application/json" },
|
|
150
|
-
body: JSON.stringify({ package:
|
|
151
|
+
body: JSON.stringify({ package: PI_RIG_PACKAGE_NAME, scope: "global" })
|
|
151
152
|
});
|
|
152
153
|
const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
153
154
|
const piOk = record.piOk === true || record.ok === true;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli/src/commands/_pi-session.ts
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
|
|
5
|
+
// packages/cli/src/runner.ts
|
|
6
|
+
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
7
|
+
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
8
|
+
import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
|
|
9
|
+
import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
|
|
10
|
+
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
11
|
+
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
12
|
+
import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
|
|
13
|
+
function formatCommand(parts) {
|
|
14
|
+
return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// packages/cli/src/commands/_server-client.ts
|
|
18
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
19
|
+
import { resolve as resolve2 } from "path";
|
|
20
|
+
import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
|
|
21
|
+
|
|
22
|
+
// packages/cli/src/commands/_connection-state.ts
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
24
|
+
import { homedir } from "os";
|
|
25
|
+
import { dirname, resolve } from "path";
|
|
26
|
+
function resolveGlobalConnectionsPath(env = process.env) {
|
|
27
|
+
const explicit = env.RIG_CONNECTIONS_FILE?.trim();
|
|
28
|
+
if (explicit)
|
|
29
|
+
return resolve(explicit);
|
|
30
|
+
const stateDir = env.RIG_GLOBAL_STATE_DIR?.trim();
|
|
31
|
+
if (stateDir)
|
|
32
|
+
return resolve(stateDir, "connections.json");
|
|
33
|
+
return resolve(homedir(), ".rig", "connections.json");
|
|
34
|
+
}
|
|
35
|
+
function resolveRepoConnectionPath(projectRoot) {
|
|
36
|
+
return resolve(projectRoot, ".rig", "state", "connection.json");
|
|
37
|
+
}
|
|
38
|
+
function readJsonFile(path) {
|
|
39
|
+
if (!existsSync(path))
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function normalizeConnection(value) {
|
|
48
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
49
|
+
return null;
|
|
50
|
+
const record = value;
|
|
51
|
+
if (record.kind === "local")
|
|
52
|
+
return { kind: "local", mode: "auto" };
|
|
53
|
+
if (record.kind === "remote" && typeof record.baseUrl === "string" && record.baseUrl.trim()) {
|
|
54
|
+
const baseUrl = record.baseUrl.trim().replace(/\/+$/, "");
|
|
55
|
+
return { kind: "remote", baseUrl };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function readGlobalConnections(options = {}) {
|
|
60
|
+
const path = resolveGlobalConnectionsPath(options.env ?? process.env);
|
|
61
|
+
const payload = readJsonFile(path);
|
|
62
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
63
|
+
return { connections: {} };
|
|
64
|
+
}
|
|
65
|
+
const rawConnections = payload.connections;
|
|
66
|
+
const connections = {};
|
|
67
|
+
if (rawConnections && typeof rawConnections === "object" && !Array.isArray(rawConnections)) {
|
|
68
|
+
for (const [alias, raw] of Object.entries(rawConnections)) {
|
|
69
|
+
const connection = normalizeConnection(raw);
|
|
70
|
+
if (connection)
|
|
71
|
+
connections[alias] = connection;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { connections };
|
|
75
|
+
}
|
|
76
|
+
function readRepoConnection(projectRoot) {
|
|
77
|
+
const payload = readJsonFile(resolveRepoConnectionPath(projectRoot));
|
|
78
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
79
|
+
return null;
|
|
80
|
+
const record = payload;
|
|
81
|
+
const selected = typeof record.selected === "string" ? record.selected.trim() : "";
|
|
82
|
+
if (!selected)
|
|
83
|
+
return null;
|
|
84
|
+
return {
|
|
85
|
+
selected,
|
|
86
|
+
project: typeof record.project === "string" ? record.project : undefined,
|
|
87
|
+
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function resolveSelectedConnection(projectRoot, options = {}) {
|
|
91
|
+
const repo = readRepoConnection(projectRoot);
|
|
92
|
+
if (!repo)
|
|
93
|
+
return null;
|
|
94
|
+
if (repo.selected === "local")
|
|
95
|
+
return { alias: "local", connection: { kind: "local", mode: "auto" } };
|
|
96
|
+
const global = readGlobalConnections(options);
|
|
97
|
+
const connection = global.connections[repo.selected];
|
|
98
|
+
if (!connection) {
|
|
99
|
+
throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
|
|
100
|
+
}
|
|
101
|
+
return { alias: repo.selected, connection };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// packages/cli/src/commands/_server-client.ts
|
|
105
|
+
var scopedGitHubBearerTokens = new Map;
|
|
106
|
+
function cleanToken(value) {
|
|
107
|
+
const trimmed = value?.trim();
|
|
108
|
+
return trimmed ? trimmed : null;
|
|
109
|
+
}
|
|
110
|
+
function readPrivateRemoteSessionToken(projectRoot) {
|
|
111
|
+
const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
|
|
112
|
+
if (!existsSync2(path))
|
|
113
|
+
return null;
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
116
|
+
return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
122
|
+
const scopedKey = resolve2(projectRoot);
|
|
123
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
124
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
125
|
+
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
126
|
+
if (privateSession)
|
|
127
|
+
return privateSession;
|
|
128
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
129
|
+
}
|
|
130
|
+
async function ensureServerForCli(projectRoot) {
|
|
131
|
+
try {
|
|
132
|
+
const selected = resolveSelectedConnection(projectRoot);
|
|
133
|
+
if (selected?.connection.kind === "remote") {
|
|
134
|
+
return {
|
|
135
|
+
baseUrl: selected.connection.baseUrl,
|
|
136
|
+
authToken: readGitHubBearerTokenForRemote(projectRoot),
|
|
137
|
+
connectionKind: "remote"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const connection = await ensureLocalRigServerConnection(projectRoot);
|
|
141
|
+
return {
|
|
142
|
+
baseUrl: connection.baseUrl,
|
|
143
|
+
authToken: connection.authToken,
|
|
144
|
+
connectionKind: "local"
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof Error) {
|
|
148
|
+
throw new CliError2(error.message, 1);
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// packages/cli/src/commands/_pi-install.ts
|
|
155
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
|
|
156
|
+
import { resolve as resolve3 } from "path";
|
|
157
|
+
var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
|
|
158
|
+
function resolvePiRigPackageSource(projectRoot, exists = existsSync3) {
|
|
159
|
+
const localPackage = resolve3(projectRoot, "packages", "pi-rig");
|
|
160
|
+
if (exists(resolve3(localPackage, "package.json")))
|
|
161
|
+
return localPackage;
|
|
162
|
+
return `npm:${PI_RIG_PACKAGE_NAME}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// packages/cli/src/commands/_pi-session.ts
|
|
166
|
+
function buildPiRigSessionEnv(input) {
|
|
167
|
+
return {
|
|
168
|
+
RIG_PROJECT_ROOT: input.projectRoot,
|
|
169
|
+
PROJECT_RIG_ROOT: input.projectRoot,
|
|
170
|
+
RIG_RUN_ID: input.runId,
|
|
171
|
+
RIG_SERVER_RUN_ID: input.runId,
|
|
172
|
+
RIG_RUNTIME_ADAPTER: "pi",
|
|
173
|
+
RIG_SERVER_URL: input.serverUrl,
|
|
174
|
+
RIG_SERVER_BASE_URL: input.serverUrl,
|
|
175
|
+
RIG_STEERING_POLL_MS: process.env.RIG_STEERING_POLL_MS?.trim() || "1000",
|
|
176
|
+
RIG_PI_OPERATOR_SESSION: "1",
|
|
177
|
+
...input.taskId ? { RIG_TASK_ID: input.taskId } : {},
|
|
178
|
+
...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function shellBinary(name) {
|
|
182
|
+
const explicit = process.env.RIG_PI_BINARY?.trim();
|
|
183
|
+
if (explicit)
|
|
184
|
+
return explicit;
|
|
185
|
+
return Bun.which(name) || name;
|
|
186
|
+
}
|
|
187
|
+
function buildPiRigSessionCommand(input) {
|
|
188
|
+
const configuredExtension = input.extensionSource ?? process.env.RIG_PI_RIG_EXTENSION_SOURCE?.trim();
|
|
189
|
+
const extensionSource = configuredExtension && configuredExtension.length > 0 ? configuredExtension : resolvePiRigPackageSource(input.projectRoot);
|
|
190
|
+
const initialCommand = `/rig attach ${input.runId}`;
|
|
191
|
+
return [
|
|
192
|
+
shellBinary("pi"),
|
|
193
|
+
"--no-extensions",
|
|
194
|
+
"--extension",
|
|
195
|
+
extensionSource,
|
|
196
|
+
initialCommand
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
async function launchPiRigSession(context, input) {
|
|
200
|
+
if (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
201
|
+
return { launched: false, exitCode: null, command: [] };
|
|
202
|
+
}
|
|
203
|
+
if (process.env.RIG_DISABLE_PI_LAUNCH === "1") {
|
|
204
|
+
return { launched: false, exitCode: null, command: [] };
|
|
205
|
+
}
|
|
206
|
+
const server = await ensureServerForCli(context.projectRoot);
|
|
207
|
+
const command = buildPiRigSessionCommand({ ...input, projectRoot: context.projectRoot });
|
|
208
|
+
const env = {
|
|
209
|
+
...process.env,
|
|
210
|
+
...buildPiRigSessionEnv({
|
|
211
|
+
projectRoot: context.projectRoot,
|
|
212
|
+
runId: input.runId,
|
|
213
|
+
taskId: input.taskId,
|
|
214
|
+
serverUrl: server.baseUrl,
|
|
215
|
+
authToken: server.authToken
|
|
216
|
+
})
|
|
217
|
+
};
|
|
218
|
+
process.stdout.write(`Launching Pi for Rig run ${input.runId}\u2026
|
|
219
|
+
`);
|
|
220
|
+
process.stdout.write(`Pi command: ${formatCommand(command)}
|
|
221
|
+
`);
|
|
222
|
+
const launchedAt = Date.now();
|
|
223
|
+
const child = spawn(command[0], command.slice(1), {
|
|
224
|
+
cwd: context.projectRoot,
|
|
225
|
+
env,
|
|
226
|
+
stdio: "inherit"
|
|
227
|
+
});
|
|
228
|
+
const launchError = await new Promise((resolve4) => {
|
|
229
|
+
child.once("error", (error) => {
|
|
230
|
+
resolve4({ error: error.message });
|
|
231
|
+
});
|
|
232
|
+
child.once("close", (code) => resolve4({ code }));
|
|
233
|
+
});
|
|
234
|
+
if ("error" in launchError) {
|
|
235
|
+
process.stderr.write(`Failed to launch Pi; falling back to Rig attach view: ${launchError.error}
|
|
236
|
+
`);
|
|
237
|
+
return { launched: false, exitCode: null, command, error: launchError.error };
|
|
238
|
+
}
|
|
239
|
+
const exitCode = launchError.code;
|
|
240
|
+
const elapsedMs = Date.now() - launchedAt;
|
|
241
|
+
if (typeof exitCode === "number" && exitCode !== 0 && elapsedMs < 5000) {
|
|
242
|
+
const error = `Pi exited during startup with code ${exitCode}.`;
|
|
243
|
+
process.stderr.write(`${error} Falling back to Rig attach view.
|
|
244
|
+
`);
|
|
245
|
+
return { launched: false, exitCode, command, error };
|
|
246
|
+
}
|
|
247
|
+
return { launched: true, exitCode, command };
|
|
248
|
+
}
|
|
249
|
+
export {
|
|
250
|
+
launchPiRigSession,
|
|
251
|
+
buildPiRigSessionEnv,
|
|
252
|
+
buildPiRigSessionCommand
|
|
253
|
+
};
|