@h-rig/cli 0.0.6-alpha.3 → 0.0.6-alpha.30
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 +3606 -1172
- package/dist/src/commands/_authority-runs.js +1 -0
- package/dist/src/commands/_cli-format.js +369 -0
- package/dist/src/commands/_connection-state.js +1 -3
- package/dist/src/commands/_doctor-checks.js +13 -27
- package/dist/src/commands/_help-catalog.js +388 -0
- package/dist/src/commands/_operator-surface.js +204 -0
- package/dist/src/commands/_operator-view.js +861 -56
- package/dist/src/commands/_parsers.js +0 -2
- package/dist/src/commands/_pi-frontend.js +841 -0
- package/dist/src/commands/_pi-install.js +4 -3
- package/dist/src/commands/_pi-worker-bridge-extension.js +759 -0
- package/dist/src/commands/_policy.js +0 -2
- package/dist/src/commands/_preflight.js +32 -109
- package/dist/src/commands/_run-driver-helpers.js +0 -2
- package/dist/src/commands/_server-client.js +161 -31
- package/dist/src/commands/_snapshot-upload.js +8 -23
- package/dist/src/commands/_task-picker.js +44 -16
- package/dist/src/commands/agent.js +9 -9
- package/dist/src/commands/browser.js +4 -6
- package/dist/src/commands/connect.js +132 -25
- package/dist/src/commands/dist.js +4 -6
- package/dist/src/commands/doctor.js +13 -27
- package/dist/src/commands/github.js +10 -25
- package/dist/src/commands/inbox.js +351 -31
- package/dist/src/commands/init.js +298 -71
- package/dist/src/commands/inspect.js +10 -12
- package/dist/src/commands/inspector.js +2 -4
- package/dist/src/commands/plugin.js +76 -22
- package/dist/src/commands/profile-and-review.js +8 -10
- package/dist/src/commands/queue.js +2 -3
- package/dist/src/commands/remote.js +18 -20
- package/dist/src/commands/repo-git-harness.js +6 -8
- package/dist/src/commands/run.js +1157 -122
- package/dist/src/commands/server.js +217 -33
- package/dist/src/commands/setup.js +17 -37
- package/dist/src/commands/task-report-bug.js +5 -7
- package/dist/src/commands/task-run-driver.js +660 -73
- package/dist/src/commands/task.js +1542 -252
- package/dist/src/commands/test.js +3 -5
- package/dist/src/commands/workspace.js +4 -6
- package/dist/src/commands.js +3599 -1159
- package/dist/src/index.js +3646 -1215
- package/dist/src/launcher.js +5 -3
- package/dist/src/report-bug.js +3 -3
- package/dist/src/runner.js +5 -19
- package/package.json +6 -4
package/dist/src/commands/run.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/cli/src/commands/run.ts
|
|
3
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
4
|
-
import { resolve as resolve3 } from "path";
|
|
5
3
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
6
4
|
|
|
7
5
|
// packages/cli/src/runner.ts
|
|
8
6
|
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
9
7
|
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
10
8
|
import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
|
|
11
|
-
import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
|
|
12
|
-
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
13
9
|
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
14
10
|
import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
|
|
15
11
|
function takeFlag(args, flag) {
|
|
@@ -54,9 +50,7 @@ Usage: ${usage}`);
|
|
|
54
50
|
// packages/cli/src/commands/run.ts
|
|
55
51
|
import {
|
|
56
52
|
listAuthorityRuns,
|
|
57
|
-
readAuthorityRun
|
|
58
|
-
readJsonlFile,
|
|
59
|
-
resolveAuthorityRunDir
|
|
53
|
+
readAuthorityRun
|
|
60
54
|
} from "@rig/runtime/control-plane/authority-files";
|
|
61
55
|
import {
|
|
62
56
|
cleanupRunState,
|
|
@@ -64,12 +58,13 @@ import {
|
|
|
64
58
|
listOpenEpics,
|
|
65
59
|
resolveDefaultEpic,
|
|
66
60
|
runResume,
|
|
61
|
+
runRestart,
|
|
67
62
|
runStatus,
|
|
68
63
|
runStop,
|
|
69
64
|
startRun,
|
|
70
65
|
defaultStartRunOptions
|
|
71
66
|
} from "@rig/runtime/control-plane/native/run-ops";
|
|
72
|
-
import { loadRuntimeContextFromEnv
|
|
67
|
+
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
73
68
|
|
|
74
69
|
// packages/cli/src/commands/_parsers.ts
|
|
75
70
|
function parsePositiveInt(value, option, fallback) {
|
|
@@ -84,7 +79,6 @@ function parsePositiveInt(value, option, fallback) {
|
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
// packages/cli/src/commands/_server-client.ts
|
|
87
|
-
import { spawnSync } from "child_process";
|
|
88
82
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
89
83
|
import { resolve as resolve2 } from "path";
|
|
90
84
|
import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
|
|
@@ -166,13 +160,13 @@ function resolveSelectedConnection(projectRoot, options = {}) {
|
|
|
166
160
|
const global = readGlobalConnections(options);
|
|
167
161
|
const connection = global.connections[repo.selected];
|
|
168
162
|
if (!connection) {
|
|
169
|
-
throw new CliError2(`Selected Rig
|
|
163
|
+
throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
|
|
170
164
|
}
|
|
171
165
|
return { alias: repo.selected, connection };
|
|
172
166
|
}
|
|
173
167
|
|
|
174
168
|
// packages/cli/src/commands/_server-client.ts
|
|
175
|
-
var
|
|
169
|
+
var scopedGitHubBearerTokens = new Map;
|
|
176
170
|
function cleanToken(value) {
|
|
177
171
|
const trimmed = value?.trim();
|
|
178
172
|
return trimmed ? trimmed : null;
|
|
@@ -189,25 +183,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
|
|
|
189
183
|
}
|
|
190
184
|
}
|
|
191
185
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
192
|
-
|
|
193
|
-
|
|
186
|
+
const scopedKey = resolve2(projectRoot);
|
|
187
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
188
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
194
189
|
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
195
|
-
if (privateSession)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
|
|
200
|
-
if (envToken) {
|
|
201
|
-
cachedGitHubBearerToken = envToken;
|
|
202
|
-
return cachedGitHubBearerToken;
|
|
203
|
-
}
|
|
204
|
-
const result = spawnSync("gh", ["auth", "token"], {
|
|
205
|
-
encoding: "utf8",
|
|
206
|
-
timeout: 5000,
|
|
207
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
208
|
-
});
|
|
209
|
-
cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
|
|
210
|
-
return cachedGitHubBearerToken;
|
|
190
|
+
if (privateSession)
|
|
191
|
+
return privateSession;
|
|
192
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
211
193
|
}
|
|
212
194
|
async function ensureServerForCli(projectRoot) {
|
|
213
195
|
try {
|
|
@@ -275,6 +257,14 @@ async function requestServerJson(context, pathname, init = {}) {
|
|
|
275
257
|
}
|
|
276
258
|
return payload;
|
|
277
259
|
}
|
|
260
|
+
async function listRunsViaServer(context, options = {}) {
|
|
261
|
+
const url = new URL("http://rig.local/api/runs");
|
|
262
|
+
if (options.limit !== undefined)
|
|
263
|
+
url.searchParams.set("limit", String(options.limit));
|
|
264
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
265
|
+
const runs = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.runs) ? payload.runs : [];
|
|
266
|
+
return runs.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
267
|
+
}
|
|
278
268
|
async function getRunDetailsViaServer(context, runId) {
|
|
279
269
|
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}`);
|
|
280
270
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
@@ -288,6 +278,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
|
|
|
288
278
|
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
289
279
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
290
280
|
}
|
|
281
|
+
async function getRunTimelineViaServer(context, runId, options = {}) {
|
|
282
|
+
const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
|
|
283
|
+
if (options.limit !== undefined)
|
|
284
|
+
url.searchParams.set("limit", String(options.limit));
|
|
285
|
+
if (options.cursor)
|
|
286
|
+
url.searchParams.set("cursor", options.cursor);
|
|
287
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
288
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
289
|
+
}
|
|
291
290
|
async function stopRunViaServer(context, runId) {
|
|
292
291
|
const payload = await requestServerJson(context, "/api/runs/stop", {
|
|
293
292
|
method: "POST",
|
|
@@ -304,10 +303,69 @@ async function steerRunViaServer(context, runId, message) {
|
|
|
304
303
|
});
|
|
305
304
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
306
305
|
}
|
|
306
|
+
async function getRunPiSessionViaServer(context, runId) {
|
|
307
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi`);
|
|
308
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
309
|
+
}
|
|
310
|
+
async function getRunPiMessagesViaServer(context, runId) {
|
|
311
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/messages`);
|
|
312
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { messages: [] };
|
|
313
|
+
}
|
|
314
|
+
async function getRunPiStatusViaServer(context, runId) {
|
|
315
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/status`);
|
|
316
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
317
|
+
}
|
|
318
|
+
async function getRunPiCommandsViaServer(context, runId) {
|
|
319
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands`);
|
|
320
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { commands: [] };
|
|
321
|
+
}
|
|
322
|
+
async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
|
|
323
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
|
|
324
|
+
method: "POST",
|
|
325
|
+
headers: { "content-type": "application/json" },
|
|
326
|
+
body: JSON.stringify({ text, streamingBehavior })
|
|
327
|
+
});
|
|
328
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
329
|
+
}
|
|
330
|
+
async function sendRunPiShellViaServer(context, runId, text) {
|
|
331
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/shell`, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: { "content-type": "application/json" },
|
|
334
|
+
body: JSON.stringify({ text })
|
|
335
|
+
});
|
|
336
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
337
|
+
}
|
|
338
|
+
async function runRunPiCommandViaServer(context, runId, text) {
|
|
339
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/commands/run`, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
headers: { "content-type": "application/json" },
|
|
342
|
+
body: JSON.stringify({ text })
|
|
343
|
+
});
|
|
344
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { type: "done" };
|
|
345
|
+
}
|
|
346
|
+
async function respondRunPiExtensionUiViaServer(context, runId, requestId, valueOrCancel) {
|
|
347
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/extension-ui/respond`, {
|
|
348
|
+
method: "POST",
|
|
349
|
+
headers: { "content-type": "application/json" },
|
|
350
|
+
body: JSON.stringify({ requestId, ...valueOrCancel })
|
|
351
|
+
});
|
|
352
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
353
|
+
}
|
|
354
|
+
async function abortRunPiViaServer(context, runId) {
|
|
355
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/abort`, { method: "POST" });
|
|
356
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { aborted: true };
|
|
357
|
+
}
|
|
358
|
+
async function buildRunPiEventsWebSocketUrl(context, runId) {
|
|
359
|
+
const server = await ensureServerForCli(context.projectRoot);
|
|
360
|
+
const url = new URL(`${server.baseUrl.replace(/\/+$/, "")}/api/runs/${encodeURIComponent(runId)}/pi/events`);
|
|
361
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
362
|
+
if (server.authToken)
|
|
363
|
+
url.searchParams.set("token", server.authToken);
|
|
364
|
+
return url.toString();
|
|
365
|
+
}
|
|
307
366
|
|
|
308
|
-
// packages/cli/src/commands/_operator-
|
|
367
|
+
// packages/cli/src/commands/_operator-surface.ts
|
|
309
368
|
import { createInterface } from "readline";
|
|
310
|
-
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
311
369
|
var CANONICAL_STAGES = [
|
|
312
370
|
"Connect",
|
|
313
371
|
"GitHub/task sync",
|
|
@@ -322,18 +380,753 @@ var CANONICAL_STAGES = [
|
|
|
322
380
|
"Merge",
|
|
323
381
|
"Complete"
|
|
324
382
|
];
|
|
383
|
+
function logDetail(log) {
|
|
384
|
+
return typeof log.detail === "string" ? log.detail.trim() : "";
|
|
385
|
+
}
|
|
386
|
+
function parseProviderProtocolLog(title, detail) {
|
|
387
|
+
if (title.trim().toLowerCase() !== "agent output")
|
|
388
|
+
return null;
|
|
389
|
+
if (!detail.startsWith("{") || !detail.endsWith("}"))
|
|
390
|
+
return null;
|
|
391
|
+
try {
|
|
392
|
+
const record = JSON.parse(detail);
|
|
393
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
394
|
+
return null;
|
|
395
|
+
const type = record.type;
|
|
396
|
+
return typeof type === "string" && [
|
|
397
|
+
"assistant",
|
|
398
|
+
"message_start",
|
|
399
|
+
"message_update",
|
|
400
|
+
"message_end",
|
|
401
|
+
"stream_event",
|
|
402
|
+
"tool_result",
|
|
403
|
+
"tool_execution_start",
|
|
404
|
+
"tool_execution_update",
|
|
405
|
+
"tool_execution_end",
|
|
406
|
+
"turn_start",
|
|
407
|
+
"turn_end"
|
|
408
|
+
].includes(type) ? record : null;
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function renderProviderProtocolLog(record) {
|
|
414
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
415
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
|
|
416
|
+
const toolName = String(record.toolName ?? record.name ?? "tool");
|
|
417
|
+
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";
|
|
418
|
+
return `[Pi tool] ${toolName} ${status}`;
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
function entryId(entry, fallback) {
|
|
423
|
+
return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
|
|
424
|
+
}
|
|
325
425
|
function renderOperatorSnapshot(snapshot) {
|
|
326
426
|
const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
|
|
327
427
|
const runId = String(run.runId ?? run.id ?? "run");
|
|
328
428
|
const status = String(run.status ?? "unknown");
|
|
329
429
|
const logs = snapshot.logs ?? [];
|
|
430
|
+
const latestByStage = new Map;
|
|
431
|
+
for (const log of logs) {
|
|
432
|
+
const title = String(log.title ?? "").toLowerCase();
|
|
433
|
+
const stageName = String(log.stage ?? "").toLowerCase();
|
|
434
|
+
const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
|
|
435
|
+
if (stage)
|
|
436
|
+
latestByStage.set(stage, log);
|
|
437
|
+
}
|
|
330
438
|
const stageLines = CANONICAL_STAGES.flatMap((stage) => {
|
|
331
|
-
const match =
|
|
332
|
-
return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
|
|
439
|
+
const match = latestByStage.get(stage);
|
|
440
|
+
return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
|
|
333
441
|
});
|
|
334
442
|
return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
|
|
335
443
|
`);
|
|
336
444
|
}
|
|
445
|
+
function createPiRunStreamRenderer(output = process.stdout) {
|
|
446
|
+
let lastSnapshot = "";
|
|
447
|
+
const assistantTextById = new Map;
|
|
448
|
+
const seenTimeline = new Set;
|
|
449
|
+
const seenLogs = new Set;
|
|
450
|
+
const writeLine = (line) => output.write(`${line}
|
|
451
|
+
`);
|
|
452
|
+
return {
|
|
453
|
+
renderSnapshot(snapshot) {
|
|
454
|
+
const rendered = renderOperatorSnapshot(snapshot);
|
|
455
|
+
if (rendered && rendered !== lastSnapshot) {
|
|
456
|
+
writeLine(rendered);
|
|
457
|
+
lastSnapshot = rendered;
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
renderTimeline(entries) {
|
|
461
|
+
for (const [index, entry] of entries.entries()) {
|
|
462
|
+
const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
|
|
463
|
+
if (entry.type === "assistant_message" && typeof entry.text === "string") {
|
|
464
|
+
const text = entry.text;
|
|
465
|
+
const previousText = assistantTextById.get(id) ?? "";
|
|
466
|
+
if (!previousText && text.trim()) {
|
|
467
|
+
writeLine("[Pi assistant]");
|
|
468
|
+
}
|
|
469
|
+
if (text.startsWith(previousText)) {
|
|
470
|
+
const delta = text.slice(previousText.length);
|
|
471
|
+
if (delta)
|
|
472
|
+
output.write(delta);
|
|
473
|
+
} else if (text.trim() && text !== previousText) {
|
|
474
|
+
if (previousText)
|
|
475
|
+
writeLine(`
|
|
476
|
+
[Pi assistant]`);
|
|
477
|
+
output.write(text);
|
|
478
|
+
}
|
|
479
|
+
assistantTextById.set(id, text);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (seenTimeline.has(id))
|
|
483
|
+
continue;
|
|
484
|
+
seenTimeline.add(id);
|
|
485
|
+
if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
|
|
486
|
+
writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (entry.type === "timeline_warning") {
|
|
490
|
+
writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
renderLogs(entries) {
|
|
495
|
+
for (const [index, entry] of entries.entries()) {
|
|
496
|
+
const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
|
|
497
|
+
if (seenLogs.has(id))
|
|
498
|
+
continue;
|
|
499
|
+
seenLogs.add(id);
|
|
500
|
+
const title = String(entry.title ?? "");
|
|
501
|
+
if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
|
|
502
|
+
continue;
|
|
503
|
+
const detail = logDetail(entry);
|
|
504
|
+
if (!detail)
|
|
505
|
+
continue;
|
|
506
|
+
const protocolRecord = parseProviderProtocolLog(title, detail);
|
|
507
|
+
if (protocolRecord) {
|
|
508
|
+
const protocolLine = renderProviderProtocolLog(protocolRecord);
|
|
509
|
+
if (protocolLine)
|
|
510
|
+
writeLine(protocolLine);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
writeLine(`[${title || "Rig log"}] ${detail}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function createOperatorSurface(options = {}) {
|
|
519
|
+
const input = options.input ?? process.stdin;
|
|
520
|
+
const output = options.output ?? process.stdout;
|
|
521
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
522
|
+
const renderer = createPiRunStreamRenderer(output);
|
|
523
|
+
const writeLine = (line) => output.write(`${line}
|
|
524
|
+
`);
|
|
525
|
+
return {
|
|
526
|
+
mode: "pi-compatible-text",
|
|
527
|
+
...renderer,
|
|
528
|
+
info: writeLine,
|
|
529
|
+
error: (message) => errorOutput.write(`${message}
|
|
530
|
+
`),
|
|
531
|
+
attachCommandInput(handler) {
|
|
532
|
+
if (options.interactive === false || !input.isTTY)
|
|
533
|
+
return null;
|
|
534
|
+
const rl = createInterface({ input, output: process.stdout, terminal: false });
|
|
535
|
+
rl.on("line", (line) => {
|
|
536
|
+
Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
537
|
+
});
|
|
538
|
+
return { close: () => rl.close() };
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// packages/cli/src/commands/_pi-frontend.ts
|
|
544
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
545
|
+
import { tmpdir } from "os";
|
|
546
|
+
import { join } from "path";
|
|
547
|
+
import { main as runPiMain } from "@earendil-works/pi-coding-agent";
|
|
548
|
+
|
|
549
|
+
// packages/cli/src/commands/_pi-worker-bridge-extension.ts
|
|
550
|
+
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
551
|
+
var MAX_TRANSCRIPT_LINES = 120;
|
|
552
|
+
function recordOf(value) {
|
|
553
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
554
|
+
}
|
|
555
|
+
function asText(value) {
|
|
556
|
+
if (typeof value === "string")
|
|
557
|
+
return value;
|
|
558
|
+
if (value === null || value === undefined)
|
|
559
|
+
return "";
|
|
560
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
561
|
+
return String(value);
|
|
562
|
+
try {
|
|
563
|
+
return JSON.stringify(value);
|
|
564
|
+
} catch {
|
|
565
|
+
return String(value);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function textFromContent(content) {
|
|
569
|
+
if (typeof content === "string")
|
|
570
|
+
return content;
|
|
571
|
+
if (!Array.isArray(content))
|
|
572
|
+
return asText(content);
|
|
573
|
+
return content.flatMap((part) => {
|
|
574
|
+
const item = recordOf(part);
|
|
575
|
+
if (!item)
|
|
576
|
+
return [];
|
|
577
|
+
if (typeof item.text === "string")
|
|
578
|
+
return [item.text];
|
|
579
|
+
if (typeof item.content === "string")
|
|
580
|
+
return [item.content];
|
|
581
|
+
if (item.type === "toolCall")
|
|
582
|
+
return [`\u23FA ${String(item.name ?? "tool")} ${asText(item.arguments ?? "")}`.trim()];
|
|
583
|
+
if (item.type === "toolResult")
|
|
584
|
+
return [`\u21B3 ${asText(item.content ?? item.result ?? "")}`.trim()];
|
|
585
|
+
return [];
|
|
586
|
+
}).join(`
|
|
587
|
+
`);
|
|
588
|
+
}
|
|
589
|
+
function appendTranscript(state, label, text) {
|
|
590
|
+
const trimmed = text.trimEnd();
|
|
591
|
+
if (!trimmed)
|
|
592
|
+
return;
|
|
593
|
+
const lines = trimmed.split(/\r?\n/);
|
|
594
|
+
state.transcript.push(`${label}: ${lines[0] ?? ""}`);
|
|
595
|
+
for (const line of lines.slice(1))
|
|
596
|
+
state.transcript.push(` ${line}`);
|
|
597
|
+
if (state.transcript.length > MAX_TRANSCRIPT_LINES) {
|
|
598
|
+
state.transcript.splice(0, state.transcript.length - MAX_TRANSCRIPT_LINES);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function nativePiUi(ctx) {
|
|
602
|
+
const ui = ctx.ui;
|
|
603
|
+
return typeof ui.emitSessionEvent === "function" && typeof ui.appendSessionMessages === "function" ? ui : null;
|
|
604
|
+
}
|
|
605
|
+
function syncNativeDisplayCwd(ctx, state) {
|
|
606
|
+
const ui = nativePiUi(ctx);
|
|
607
|
+
if (ui?.setDisplayCwd && state.cwd)
|
|
608
|
+
ui.setDisplayCwd(state.cwd);
|
|
609
|
+
}
|
|
610
|
+
function parseExtensionUiRequest(value) {
|
|
611
|
+
const request = recordOf(value) ?? {};
|
|
612
|
+
const requestId = String(request.requestId ?? request.id ?? `ui-${Date.now()}`);
|
|
613
|
+
const method = String(request.method ?? request.type ?? "input");
|
|
614
|
+
const prompt = asText(request.prompt ?? request.message ?? request.title ?? method);
|
|
615
|
+
const rawOptions = Array.isArray(request.options) ? request.options : Array.isArray(request.items) ? request.items : [];
|
|
616
|
+
const options = rawOptions.map((option) => {
|
|
617
|
+
const record = recordOf(option);
|
|
618
|
+
return record ? asText(record.label ?? record.value ?? record.name ?? option) : asText(option);
|
|
619
|
+
}).filter(Boolean);
|
|
620
|
+
return { requestId, method, prompt, options };
|
|
621
|
+
}
|
|
622
|
+
function renderBridgeWidget(state) {
|
|
623
|
+
const statusParts = [
|
|
624
|
+
state.wsConnected ? "live WS" : "WS pending",
|
|
625
|
+
state.status,
|
|
626
|
+
state.model,
|
|
627
|
+
state.cwd
|
|
628
|
+
].filter(Boolean);
|
|
629
|
+
const lines = [`Worker Pi daemon bridge \xB7 ${statusParts.join(" \xB7 ")}`];
|
|
630
|
+
if (state.activity)
|
|
631
|
+
lines.push(state.activity);
|
|
632
|
+
if (state.commands.length > 0) {
|
|
633
|
+
lines.push(`Worker commands: ${state.commands.slice(0, 10).join(", ")}${state.commands.length > 10 ? ", \u2026" : ""}`);
|
|
634
|
+
}
|
|
635
|
+
lines.push("");
|
|
636
|
+
if (state.transcript.length > 0) {
|
|
637
|
+
lines.push(...state.transcript.slice(-MAX_TRANSCRIPT_LINES));
|
|
638
|
+
} else {
|
|
639
|
+
lines.push("Waiting for worker Pi daemon transcript\u2026");
|
|
640
|
+
}
|
|
641
|
+
if (state.pendingUi) {
|
|
642
|
+
lines.push("");
|
|
643
|
+
lines.push(`Extension UI request \xB7 ${state.pendingUi.method}`);
|
|
644
|
+
lines.push(state.pendingUi.prompt);
|
|
645
|
+
state.pendingUi.options.forEach((option, index) => lines.push(`${index + 1}. ${option}`));
|
|
646
|
+
lines.push("Reply in the Pi editor. /cancel cancels this request.");
|
|
647
|
+
}
|
|
648
|
+
return lines;
|
|
649
|
+
}
|
|
650
|
+
function updatePiUi(ctx, state) {
|
|
651
|
+
ctx.ui.setTitle("Pi \xB7 Rig worker daemon");
|
|
652
|
+
ctx.ui.setStatus("rig-worker-pi", state.wsConnected ? "worker Pi WS live" : state.status);
|
|
653
|
+
syncNativeDisplayCwd(ctx, state);
|
|
654
|
+
if (state.nativeStream && nativePiUi(ctx)) {
|
|
655
|
+
ctx.ui.setWidget("rig-worker-pi-transcript", undefined);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
ctx.ui.setWorkingVisible(false);
|
|
659
|
+
ctx.ui.setWidget("rig-worker-pi-transcript", [`Worker Pi daemon bridge \xB7 degraded widget transcript`, ...renderBridgeWidget(state)], { placement: "aboveEditor" });
|
|
660
|
+
}
|
|
661
|
+
function applyStatus(state, payload) {
|
|
662
|
+
const status = recordOf(payload.status) ?? payload;
|
|
663
|
+
state.streaming = status.isStreaming === true || status.isCompacting === true || status.isBashRunning === true;
|
|
664
|
+
state.cwd = typeof status.cwd === "string" ? status.cwd : state.cwd;
|
|
665
|
+
state.model = typeof status.model === "string" ? status.model : state.model;
|
|
666
|
+
const pending = typeof status.pendingMessageCount === "number" ? status.pendingMessageCount : 0;
|
|
667
|
+
state.status = `${state.streaming ? "streaming" : "idle"}${pending ? ` \xB7 ${pending} queued` : ""}`;
|
|
668
|
+
}
|
|
669
|
+
function applyMessage(state, message) {
|
|
670
|
+
const record = recordOf(message);
|
|
671
|
+
if (!record)
|
|
672
|
+
return;
|
|
673
|
+
const role = String(record.role ?? "system");
|
|
674
|
+
const label = role === "assistant" ? "Pi" : role === "user" ? "You" : role === "tool" || role === "toolResult" ? "Tool" : "System";
|
|
675
|
+
appendTranscript(state, label, textFromContent(record.content ?? record.message ?? record.text ?? ""));
|
|
676
|
+
}
|
|
677
|
+
function applyPiEvent(ctx, state, eventValue) {
|
|
678
|
+
const event = recordOf(eventValue);
|
|
679
|
+
if (!event)
|
|
680
|
+
return;
|
|
681
|
+
const type = String(event.type ?? "event");
|
|
682
|
+
if (type === "agent_start") {
|
|
683
|
+
state.streaming = true;
|
|
684
|
+
state.status = "streaming";
|
|
685
|
+
} else if (type === "agent_end") {
|
|
686
|
+
state.streaming = false;
|
|
687
|
+
state.status = "idle";
|
|
688
|
+
} else if (type === "queue_update") {
|
|
689
|
+
const steering = Array.isArray(event.steering) ? event.steering.length : 0;
|
|
690
|
+
const followUp = Array.isArray(event.followUp) ? event.followUp.length : 0;
|
|
691
|
+
state.status = `queued \xB7 steer ${steering} \xB7 follow-up ${followUp}`;
|
|
692
|
+
}
|
|
693
|
+
const native = nativePiUi(ctx);
|
|
694
|
+
if (state.nativeStream && native?.emitSessionEvent) {
|
|
695
|
+
native.emitSessionEvent(eventValue);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (type === "agent_end") {
|
|
699
|
+
appendTranscript(state, "System", "Agent turn complete.");
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (type === "message_start" || type === "message_end" || type === "turn_end") {
|
|
703
|
+
applyMessage(state, event.message);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (type === "message_update") {
|
|
707
|
+
const assistantEvent = recordOf(event.assistantMessageEvent);
|
|
708
|
+
const delta = typeof assistantEvent?.delta === "string" ? assistantEvent.delta : typeof assistantEvent?.text === "string" ? assistantEvent.text : "";
|
|
709
|
+
if (delta)
|
|
710
|
+
appendTranscript(state, assistantEvent?.type === "thinking_delta" ? "Thinking" : "Pi", delta);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (type === "tool_execution_start") {
|
|
714
|
+
appendTranscript(state, "Tool", `${String(event.toolName ?? "tool")} ${asText(event.args ?? "")}`.trim());
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (type === "tool_execution_update") {
|
|
718
|
+
appendTranscript(state, "Tool", asText(event.partialResult ?? ""));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (type === "tool_execution_end") {
|
|
722
|
+
appendTranscript(state, event.isError === true ? "Error" : "Tool", asText(event.result ?? `${String(event.toolName ?? "tool")} complete`));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function firstPendingShell(state) {
|
|
726
|
+
return state.pendingShells[0];
|
|
727
|
+
}
|
|
728
|
+
function finishPendingShell(state, shell, result) {
|
|
729
|
+
const index = state.pendingShells.indexOf(shell);
|
|
730
|
+
if (index !== -1)
|
|
731
|
+
state.pendingShells.splice(index, 1);
|
|
732
|
+
shell.resolve(result);
|
|
733
|
+
}
|
|
734
|
+
function failPendingShell(state, shell, error) {
|
|
735
|
+
const index = state.pendingShells.indexOf(shell);
|
|
736
|
+
if (index !== -1)
|
|
737
|
+
state.pendingShells.splice(index, 1);
|
|
738
|
+
shell.reject(error);
|
|
739
|
+
}
|
|
740
|
+
function applyUiEvent(state, value) {
|
|
741
|
+
const event = recordOf(value);
|
|
742
|
+
if (!event)
|
|
743
|
+
return;
|
|
744
|
+
const type = String(event.type ?? "ui");
|
|
745
|
+
if (type === "shell.chunk") {
|
|
746
|
+
const pending = firstPendingShell(state);
|
|
747
|
+
const chunk = asText(event.chunk);
|
|
748
|
+
if (pending) {
|
|
749
|
+
pending.sawChunk = true;
|
|
750
|
+
pending.onData(Buffer.from(chunk));
|
|
751
|
+
} else {
|
|
752
|
+
appendTranscript(state, "Tool", chunk);
|
|
753
|
+
}
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (type === "shell.end") {
|
|
757
|
+
const pending = firstPendingShell(state);
|
|
758
|
+
const output = asText(event.output ?? "");
|
|
759
|
+
const exitCode = typeof event.exitCode === "number" ? event.exitCode : event.isError === true ? 1 : 0;
|
|
760
|
+
if (pending) {
|
|
761
|
+
if (output && !pending.sawChunk)
|
|
762
|
+
pending.onData(Buffer.from(output));
|
|
763
|
+
finishPendingShell(state, pending, { exitCode });
|
|
764
|
+
} else {
|
|
765
|
+
appendTranscript(state, event.isError === true ? "Error" : "Tool", output || `exit ${String(exitCode)}`);
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (type === "shell.start") {
|
|
770
|
+
if (!firstPendingShell(state))
|
|
771
|
+
appendTranscript(state, "Tool", `$ ${asText(event.command)}`);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
appendTranscript(state, "System", `${type}: ${asText(event)}`);
|
|
775
|
+
}
|
|
776
|
+
function applyEnvelope(ctx, state, envelopeValue) {
|
|
777
|
+
const envelope = recordOf(envelopeValue);
|
|
778
|
+
if (!envelope)
|
|
779
|
+
return;
|
|
780
|
+
const type = String(envelope.type ?? "");
|
|
781
|
+
if (type === "ready") {
|
|
782
|
+
const metadata = recordOf(envelope.metadata);
|
|
783
|
+
state.cwd = typeof metadata?.cwd === "string" ? metadata.cwd : state.cwd;
|
|
784
|
+
state.status = "worker Pi daemon ready";
|
|
785
|
+
if (!state.nativeStream)
|
|
786
|
+
appendTranscript(state, "System", "Connected to worker Pi daemon.");
|
|
787
|
+
} else if (type === "status.update") {
|
|
788
|
+
applyStatus(state, envelope);
|
|
789
|
+
} else if (type === "activity.update") {
|
|
790
|
+
const activity = recordOf(envelope.activity);
|
|
791
|
+
state.activity = [activity?.label, activity?.detail].map(asText).filter(Boolean).join(" \u2014 ");
|
|
792
|
+
} else if (type === "extension_ui_request") {
|
|
793
|
+
state.pendingUi = parseExtensionUiRequest(envelope.request);
|
|
794
|
+
appendTranscript(state, "System", `Extension UI request: ${state.pendingUi.prompt}`);
|
|
795
|
+
} else if (type === "pi.ui_event") {
|
|
796
|
+
applyUiEvent(state, envelope.event);
|
|
797
|
+
} else if (type === "pi.event") {
|
|
798
|
+
applyPiEvent(ctx, state, envelope.event);
|
|
799
|
+
} else if (type === "error") {
|
|
800
|
+
appendTranscript(state, "Error", asText(envelope.message ?? envelope.detail ?? "unknown error"));
|
|
801
|
+
}
|
|
802
|
+
syncNativeDisplayCwd(ctx, state);
|
|
803
|
+
}
|
|
804
|
+
async function waitForWorkerReady(options, ctx, state) {
|
|
805
|
+
while (true) {
|
|
806
|
+
const session = await getRunPiSessionViaServer(options.context, options.runId).catch((error) => ({
|
|
807
|
+
ready: false,
|
|
808
|
+
status: error instanceof Error ? error.message : String(error),
|
|
809
|
+
retryAfterMs: 1000
|
|
810
|
+
}));
|
|
811
|
+
if (session.ready === false) {
|
|
812
|
+
const status = String(session.status ?? "starting");
|
|
813
|
+
state.status = `waiting for worker Pi daemon \xB7 ${status}`;
|
|
814
|
+
updatePiUi(ctx, state);
|
|
815
|
+
if (TERMINAL_RUN_STATUSES.has(status.toLowerCase())) {
|
|
816
|
+
appendTranscript(state, "Error", `Run ended before worker Pi daemon became ready: ${status}`);
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
await Bun.sleep(typeof session.retryAfterMs === "number" ? session.retryAfterMs : 750);
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
const sessionRecord = recordOf(session) ?? {};
|
|
823
|
+
applyEnvelope(ctx, state, { type: "ready", metadata: sessionRecord.metadata ?? sessionRecord });
|
|
824
|
+
updatePiUi(ctx, state);
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function parseWsPayload(message) {
|
|
829
|
+
if (typeof message.data === "string")
|
|
830
|
+
return JSON.parse(message.data);
|
|
831
|
+
return JSON.parse(Buffer.from(message.data).toString("utf8"));
|
|
832
|
+
}
|
|
833
|
+
async function connectWorkerStream(options, ctx, state) {
|
|
834
|
+
const ready = await waitForWorkerReady(options, ctx, state);
|
|
835
|
+
if (!ready)
|
|
836
|
+
return;
|
|
837
|
+
let catchupDone = false;
|
|
838
|
+
const buffered = [];
|
|
839
|
+
const wsUrl = await buildRunPiEventsWebSocketUrl(options.context, options.runId);
|
|
840
|
+
const socket = new WebSocket(wsUrl);
|
|
841
|
+
const closePromise = new Promise((resolve3) => {
|
|
842
|
+
socket.onopen = () => {
|
|
843
|
+
state.wsConnected = true;
|
|
844
|
+
state.status = "live worker Pi WebSocket connected";
|
|
845
|
+
updatePiUi(ctx, state);
|
|
846
|
+
};
|
|
847
|
+
socket.onmessage = (message) => {
|
|
848
|
+
try {
|
|
849
|
+
const payload = parseWsPayload(message);
|
|
850
|
+
if (!catchupDone)
|
|
851
|
+
buffered.push(payload);
|
|
852
|
+
else {
|
|
853
|
+
applyEnvelope(ctx, state, payload);
|
|
854
|
+
updatePiUi(ctx, state);
|
|
855
|
+
}
|
|
856
|
+
} catch (error) {
|
|
857
|
+
appendTranscript(state, "Error", `Unparseable worker Pi event: ${error instanceof Error ? error.message : String(error)}`);
|
|
858
|
+
updatePiUi(ctx, state);
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
socket.onerror = () => socket.close();
|
|
862
|
+
socket.onclose = () => {
|
|
863
|
+
state.wsConnected = false;
|
|
864
|
+
state.status = "worker Pi WebSocket disconnected";
|
|
865
|
+
updatePiUi(ctx, state);
|
|
866
|
+
resolve3();
|
|
867
|
+
};
|
|
868
|
+
});
|
|
869
|
+
try {
|
|
870
|
+
const [messagesPayload, statusPayload, commandsPayload] = await Promise.all([
|
|
871
|
+
getRunPiMessagesViaServer(options.context, options.runId),
|
|
872
|
+
getRunPiStatusViaServer(options.context, options.runId),
|
|
873
|
+
getRunPiCommandsViaServer(options.context, options.runId)
|
|
874
|
+
]);
|
|
875
|
+
const messages = Array.isArray(messagesPayload.messages) ? messagesPayload.messages : [];
|
|
876
|
+
const native = nativePiUi(ctx);
|
|
877
|
+
if (state.nativeStream && native?.appendSessionMessages)
|
|
878
|
+
native.appendSessionMessages(messages);
|
|
879
|
+
else
|
|
880
|
+
for (const message of messages)
|
|
881
|
+
applyMessage(state, message);
|
|
882
|
+
applyStatus(state, statusPayload);
|
|
883
|
+
const commands = Array.isArray(commandsPayload.commands) ? commandsPayload.commands : [];
|
|
884
|
+
state.commands = commands.flatMap((command) => {
|
|
885
|
+
const record = recordOf(command);
|
|
886
|
+
return typeof record?.name === "string" ? [`/${record.name}`] : [];
|
|
887
|
+
});
|
|
888
|
+
catchupDone = true;
|
|
889
|
+
for (const payload of buffered.splice(0))
|
|
890
|
+
applyEnvelope(ctx, state, payload);
|
|
891
|
+
updatePiUi(ctx, state);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
appendTranscript(state, "Error", `Worker Pi catch-up failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
894
|
+
catchupDone = true;
|
|
895
|
+
updatePiUi(ctx, state);
|
|
896
|
+
}
|
|
897
|
+
await closePromise;
|
|
898
|
+
}
|
|
899
|
+
function createRemoteBashOperations(options, state, excludeFromContext) {
|
|
900
|
+
return {
|
|
901
|
+
exec(command, _cwd, execOptions) {
|
|
902
|
+
return new Promise((resolve3, reject) => {
|
|
903
|
+
const pending = {
|
|
904
|
+
command,
|
|
905
|
+
onData: execOptions.onData,
|
|
906
|
+
resolve: resolve3,
|
|
907
|
+
reject,
|
|
908
|
+
sawChunk: false
|
|
909
|
+
};
|
|
910
|
+
const cleanup = () => {
|
|
911
|
+
execOptions.signal?.removeEventListener("abort", onAbort);
|
|
912
|
+
if (timer)
|
|
913
|
+
clearTimeout(timer);
|
|
914
|
+
};
|
|
915
|
+
const onAbort = () => {
|
|
916
|
+
cleanup();
|
|
917
|
+
failPendingShell(state, pending, new Error("Remote worker shell command aborted locally."));
|
|
918
|
+
};
|
|
919
|
+
const timeoutMs = typeof execOptions.timeout === "number" && execOptions.timeout > 0 ? execOptions.timeout : 0;
|
|
920
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
921
|
+
cleanup();
|
|
922
|
+
failPendingShell(state, pending, new Error(`Remote worker shell command timed out after ${timeoutMs}ms.`));
|
|
923
|
+
}, timeoutMs) : null;
|
|
924
|
+
const wrappedResolve = pending.resolve;
|
|
925
|
+
const wrappedReject = pending.reject;
|
|
926
|
+
pending.resolve = (result) => {
|
|
927
|
+
cleanup();
|
|
928
|
+
wrappedResolve(result);
|
|
929
|
+
};
|
|
930
|
+
pending.reject = (error) => {
|
|
931
|
+
cleanup();
|
|
932
|
+
wrappedReject(error);
|
|
933
|
+
};
|
|
934
|
+
execOptions.signal?.addEventListener("abort", onAbort, { once: true });
|
|
935
|
+
state.pendingShells.push(pending);
|
|
936
|
+
sendRunPiShellViaServer(options.context, options.runId, `${excludeFromContext ? "!!" : "!"}${command}`).catch((error) => {
|
|
937
|
+
cleanup();
|
|
938
|
+
failPendingShell(state, pending, error instanceof Error ? error : new Error(String(error)));
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
async function answerPendingUi(options, state, line) {
|
|
945
|
+
const pending = state.pendingUi;
|
|
946
|
+
if (!pending)
|
|
947
|
+
return false;
|
|
948
|
+
if (line === "/cancel") {
|
|
949
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, pending.requestId, { cancelled: true });
|
|
950
|
+
} else if (pending.method === "confirm") {
|
|
951
|
+
const confirmed = /^(y|yes|true|1)$/i.test(line);
|
|
952
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, pending.requestId, { value: confirmed, confirmed });
|
|
953
|
+
} else if (pending.options.length > 0 && /^\d+$/.test(line)) {
|
|
954
|
+
const selected = pending.options[Math.max(0, Number(line) - 1)] ?? line;
|
|
955
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, pending.requestId, { value: selected });
|
|
956
|
+
} else {
|
|
957
|
+
await respondRunPiExtensionUiViaServer(options.context, options.runId, pending.requestId, { value: line });
|
|
958
|
+
}
|
|
959
|
+
appendTranscript(state, "System", `Responded to extension UI request ${pending.requestId}.`);
|
|
960
|
+
state.pendingUi = null;
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
async function routeInput(options, ctx, state, line) {
|
|
964
|
+
const text = line.trim();
|
|
965
|
+
if (!text)
|
|
966
|
+
return;
|
|
967
|
+
if (await answerPendingUi(options, state, text)) {
|
|
968
|
+
updatePiUi(ctx, state);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (text === "/detach" || text === "/quit" || text === "/q") {
|
|
972
|
+
appendTranscript(state, "System", "Detached locally; worker Pi daemon continues.");
|
|
973
|
+
updatePiUi(ctx, state);
|
|
974
|
+
ctx.shutdown();
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (text === "/stop") {
|
|
978
|
+
await abortRunPiViaServer(options.context, options.runId);
|
|
979
|
+
appendTranscript(state, "System", "Stop requested for worker Pi daemon.");
|
|
980
|
+
updatePiUi(ctx, state);
|
|
981
|
+
ctx.shutdown();
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (text.startsWith("!")) {
|
|
985
|
+
appendTranscript(state, "You", text);
|
|
986
|
+
await sendRunPiShellViaServer(options.context, options.runId, text);
|
|
987
|
+
} else if (text.startsWith("/")) {
|
|
988
|
+
appendTranscript(state, "You", text);
|
|
989
|
+
const result = await runRunPiCommandViaServer(options.context, options.runId, text);
|
|
990
|
+
const message = typeof result.message === "string" ? result.message : "worker command accepted";
|
|
991
|
+
appendTranscript(state, "System", message);
|
|
992
|
+
} else {
|
|
993
|
+
appendTranscript(state, "You", text);
|
|
994
|
+
await sendRunPiPromptViaServer(options.context, options.runId, text, state.streaming ? "steer" : undefined);
|
|
995
|
+
}
|
|
996
|
+
updatePiUi(ctx, state);
|
|
997
|
+
}
|
|
998
|
+
function createRigWorkerPiBridgeExtension(options) {
|
|
999
|
+
return (pi) => {
|
|
1000
|
+
const state = {
|
|
1001
|
+
transcript: [],
|
|
1002
|
+
status: "starting worker Pi daemon bridge",
|
|
1003
|
+
activity: "",
|
|
1004
|
+
cwd: "",
|
|
1005
|
+
model: "",
|
|
1006
|
+
commands: [],
|
|
1007
|
+
streaming: false,
|
|
1008
|
+
pendingUi: null,
|
|
1009
|
+
pendingShells: [],
|
|
1010
|
+
wsConnected: false,
|
|
1011
|
+
nativeStream: false
|
|
1012
|
+
};
|
|
1013
|
+
if (options.initialMessageSent)
|
|
1014
|
+
appendTranscript(state, "System", "Initial message sent to worker Pi daemon.");
|
|
1015
|
+
let nativePiUiContextAvailable = false;
|
|
1016
|
+
pi.on("user_bash", (event) => {
|
|
1017
|
+
state.nativeStream = Boolean(state.nativeStream || nativePiUiContextAvailable);
|
|
1018
|
+
return { operations: createRemoteBashOperations(options, state, event.excludeFromContext === true) };
|
|
1019
|
+
});
|
|
1020
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1021
|
+
nativePiUiContextAvailable = Boolean(nativePiUi(ctx));
|
|
1022
|
+
state.nativeStream = nativePiUiContextAvailable;
|
|
1023
|
+
updatePiUi(ctx, state);
|
|
1024
|
+
ctx.ui.notify(nativePiUiContextAvailable ? "Rig worker Pi native stream bridge loaded" : "Rig worker Pi bridge extension loaded (degraded widget fallback)", "info");
|
|
1025
|
+
ctx.ui.onTerminalInput((data) => {
|
|
1026
|
+
if (data.includes("\x04")) {
|
|
1027
|
+
ctx.shutdown();
|
|
1028
|
+
return { consume: true };
|
|
1029
|
+
}
|
|
1030
|
+
if (!data.includes("\r") && !data.includes(`
|
|
1031
|
+
`))
|
|
1032
|
+
return;
|
|
1033
|
+
const inlineText = data.replace(/[\r\n]+/g, "").trim();
|
|
1034
|
+
const editorText = ctx.ui.getEditorText().trim();
|
|
1035
|
+
const text = [editorText, inlineText].filter(Boolean).join(" ").trim();
|
|
1036
|
+
if (!text)
|
|
1037
|
+
return;
|
|
1038
|
+
if (text.startsWith("!"))
|
|
1039
|
+
return;
|
|
1040
|
+
ctx.ui.setEditorText("");
|
|
1041
|
+
routeInput(options, ctx, state, text).catch((error) => {
|
|
1042
|
+
appendTranscript(state, "Error", error instanceof Error ? error.message : String(error));
|
|
1043
|
+
updatePiUi(ctx, state);
|
|
1044
|
+
});
|
|
1045
|
+
return { consume: true };
|
|
1046
|
+
});
|
|
1047
|
+
connectWorkerStream(options, ctx, state).catch((error) => {
|
|
1048
|
+
appendTranscript(state, "Error", error instanceof Error ? error.message : String(error));
|
|
1049
|
+
updatePiUi(ctx, state);
|
|
1050
|
+
});
|
|
1051
|
+
});
|
|
1052
|
+
pi.on("session_shutdown", () => {});
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// packages/cli/src/commands/_pi-frontend.ts
|
|
1057
|
+
function setTemporaryEnv(updates) {
|
|
1058
|
+
const previous = new Map;
|
|
1059
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
1060
|
+
previous.set(key, process.env[key]);
|
|
1061
|
+
process.env[key] = value;
|
|
1062
|
+
}
|
|
1063
|
+
return () => {
|
|
1064
|
+
for (const [key, value] of previous) {
|
|
1065
|
+
if (value === undefined)
|
|
1066
|
+
delete process.env[key];
|
|
1067
|
+
else
|
|
1068
|
+
process.env[key] = value;
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
async function attachRunBundledPiFrontend(context, input) {
|
|
1073
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "rig-pi-frontend-"));
|
|
1074
|
+
const cwd = join(tempRoot, "workspace");
|
|
1075
|
+
const agentDir = join(tempRoot, "agent");
|
|
1076
|
+
const sessionDir = join(tempRoot, "sessions");
|
|
1077
|
+
const previousCwd = process.cwd();
|
|
1078
|
+
const restoreEnv = setTemporaryEnv({
|
|
1079
|
+
PI_CODING_AGENT_DIR: agentDir,
|
|
1080
|
+
PI_CODING_AGENT_SESSION_DIR: sessionDir,
|
|
1081
|
+
PI_OFFLINE: "1",
|
|
1082
|
+
PI_SKIP_VERSION_CHECK: "1"
|
|
1083
|
+
});
|
|
1084
|
+
let detached = false;
|
|
1085
|
+
try {
|
|
1086
|
+
await Bun.$`mkdir -p ${cwd} ${agentDir} ${sessionDir}`.quiet();
|
|
1087
|
+
process.chdir(cwd);
|
|
1088
|
+
await runPiMain([
|
|
1089
|
+
"--offline",
|
|
1090
|
+
"--no-session",
|
|
1091
|
+
"--no-tools",
|
|
1092
|
+
"--no-builtin-tools",
|
|
1093
|
+
"--no-skills",
|
|
1094
|
+
"--no-prompt-templates",
|
|
1095
|
+
"--no-themes",
|
|
1096
|
+
"--no-context-files",
|
|
1097
|
+
"--no-approve"
|
|
1098
|
+
], {
|
|
1099
|
+
extensionFactories: [
|
|
1100
|
+
createRigWorkerPiBridgeExtension({
|
|
1101
|
+
context,
|
|
1102
|
+
runId: input.runId,
|
|
1103
|
+
initialMessageSent: input.steered === true
|
|
1104
|
+
})
|
|
1105
|
+
]
|
|
1106
|
+
});
|
|
1107
|
+
detached = true;
|
|
1108
|
+
} finally {
|
|
1109
|
+
process.chdir(previousCwd);
|
|
1110
|
+
restoreEnv();
|
|
1111
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
1112
|
+
}
|
|
1113
|
+
let run = { runId: input.runId, status: "unknown" };
|
|
1114
|
+
try {
|
|
1115
|
+
run = await getRunDetailsViaServer(context, input.runId);
|
|
1116
|
+
} catch {}
|
|
1117
|
+
return {
|
|
1118
|
+
run,
|
|
1119
|
+
logs: [],
|
|
1120
|
+
timeline: [],
|
|
1121
|
+
timelineCursor: null,
|
|
1122
|
+
steered: input.steered === true,
|
|
1123
|
+
detached,
|
|
1124
|
+
rendered: "actual bundled Pi frontend hosted Rig worker Pi bridge extension"
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// packages/cli/src/commands/_operator-view.ts
|
|
1129
|
+
var TERMINAL_RUN_STATUSES2 = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
337
1130
|
function runStatusFromPayload(payload) {
|
|
338
1131
|
const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
339
1132
|
return String(run.status ?? "unknown").toLowerCase();
|
|
@@ -355,57 +1148,299 @@ async function applyOperatorCommand(context, input, deps = {}) {
|
|
|
355
1148
|
await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
|
|
356
1149
|
return { action: "continue", message: "Steering message queued." };
|
|
357
1150
|
}
|
|
358
|
-
async function readOperatorSnapshot(context, runId) {
|
|
1151
|
+
async function readOperatorSnapshot(context, runId, options = {}) {
|
|
359
1152
|
const run = await getRunDetailsViaServer(context, runId);
|
|
360
1153
|
const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
|
|
361
|
-
const
|
|
362
|
-
|
|
1154
|
+
const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
|
|
1155
|
+
entries: [{
|
|
1156
|
+
id: `timeline-unavailable:${runId}`,
|
|
1157
|
+
type: "timeline_warning",
|
|
1158
|
+
detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
|
|
1159
|
+
createdAt: new Date().toISOString()
|
|
1160
|
+
}],
|
|
1161
|
+
nextCursor: options.timelineCursor ?? null
|
|
1162
|
+
}));
|
|
1163
|
+
const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
|
|
1164
|
+
const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1165
|
+
const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
|
|
1166
|
+
return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
|
|
363
1167
|
}
|
|
364
1168
|
async function attachRunOperatorView(context, input) {
|
|
365
1169
|
let steered = false;
|
|
366
1170
|
if (input.message?.trim()) {
|
|
367
|
-
await steerRunViaServer(context, input.runId, input.message.trim());
|
|
1171
|
+
await sendRunPiPromptViaServer(context, input.runId, input.message.trim(), "steer").catch(() => steerRunViaServer(context, input.runId, input.message.trim()));
|
|
368
1172
|
steered = true;
|
|
369
1173
|
}
|
|
1174
|
+
if (input.follow && !input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
1175
|
+
return attachRunBundledPiFrontend(context, {
|
|
1176
|
+
runId: input.runId,
|
|
1177
|
+
steered
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
const surface = createOperatorSurface({ interactive: input.interactive !== false });
|
|
370
1181
|
let snapshot = await readOperatorSnapshot(context, input.runId);
|
|
371
1182
|
if (context.outputMode === "text") {
|
|
372
|
-
|
|
1183
|
+
surface.renderSnapshot(snapshot);
|
|
1184
|
+
surface.renderTimeline(snapshot.timeline);
|
|
1185
|
+
surface.renderLogs(snapshot.logs);
|
|
373
1186
|
if (steered)
|
|
374
|
-
|
|
1187
|
+
surface.info("Message submitted to worker Pi.");
|
|
375
1188
|
}
|
|
376
1189
|
let detached = false;
|
|
377
|
-
let
|
|
1190
|
+
let commandInput = null;
|
|
378
1191
|
if (input.follow && !input.once && context.outputMode === "text") {
|
|
379
1192
|
if (input.interactive !== false && process.stdin.isTTY) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
}).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
1193
|
+
surface.info("Controls: /user <message>, /stop, /detach");
|
|
1194
|
+
commandInput = surface.attachCommandInput(async (line) => {
|
|
1195
|
+
const result = await applyOperatorCommand(context, { runId: input.runId, line });
|
|
1196
|
+
if (result.message)
|
|
1197
|
+
surface.info(result.message);
|
|
1198
|
+
if (result.action === "detach" || result.action === "stopped") {
|
|
1199
|
+
detached = true;
|
|
1200
|
+
commandInput?.close();
|
|
1201
|
+
}
|
|
391
1202
|
});
|
|
392
1203
|
}
|
|
393
|
-
let lastRendered = snapshot.rendered;
|
|
394
1204
|
const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
|
|
395
|
-
|
|
1205
|
+
let timelineCursor = snapshot.timelineCursor;
|
|
1206
|
+
while (!detached && !TERMINAL_RUN_STATUSES2.has(runStatusFromPayload(snapshot.run))) {
|
|
396
1207
|
await Bun.sleep(pollMs);
|
|
397
|
-
snapshot = await readOperatorSnapshot(context, input.runId);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
1208
|
+
snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
|
|
1209
|
+
timelineCursor = snapshot.timelineCursor;
|
|
1210
|
+
surface.renderSnapshot(snapshot);
|
|
1211
|
+
surface.renderTimeline(snapshot.timeline);
|
|
1212
|
+
surface.renderLogs(snapshot.logs);
|
|
402
1213
|
}
|
|
403
|
-
|
|
1214
|
+
commandInput?.close();
|
|
404
1215
|
}
|
|
405
1216
|
return { ...snapshot, steered, detached };
|
|
406
1217
|
}
|
|
407
1218
|
|
|
1219
|
+
// packages/cli/src/commands/_cli-format.ts
|
|
1220
|
+
import { log, note } from "@clack/prompts";
|
|
1221
|
+
import pc from "picocolors";
|
|
1222
|
+
function stringField(record, key, fallback = "") {
|
|
1223
|
+
const value = record[key];
|
|
1224
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
1225
|
+
}
|
|
1226
|
+
function rawObject(record) {
|
|
1227
|
+
const raw = record.raw;
|
|
1228
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
1229
|
+
}
|
|
1230
|
+
function truncate(value, width) {
|
|
1231
|
+
if (value.length <= width)
|
|
1232
|
+
return value;
|
|
1233
|
+
if (width <= 1)
|
|
1234
|
+
return "\u2026";
|
|
1235
|
+
return `${value.slice(0, width - 1)}\u2026`;
|
|
1236
|
+
}
|
|
1237
|
+
function pad(value, width) {
|
|
1238
|
+
return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
|
|
1239
|
+
}
|
|
1240
|
+
function statusColor(status) {
|
|
1241
|
+
const normalized = status.toLowerCase();
|
|
1242
|
+
if (["completed", "merged", "closed", "done", "accepted", "pass", "selected", "approved"].includes(normalized))
|
|
1243
|
+
return pc.green;
|
|
1244
|
+
if (["failed", "needs_attention", "needs-attention", "blocked", "error", "rejected"].includes(normalized))
|
|
1245
|
+
return pc.red;
|
|
1246
|
+
if (["running", "reviewing", "validating", "in_progress", "in-progress", "remote"].includes(normalized))
|
|
1247
|
+
return pc.cyan;
|
|
1248
|
+
if (["ready", "open", "queued", "created", "preparing", "local", "pending"].includes(normalized))
|
|
1249
|
+
return pc.yellow;
|
|
1250
|
+
return pc.dim;
|
|
1251
|
+
}
|
|
1252
|
+
function compactDate(value) {
|
|
1253
|
+
if (!value.trim())
|
|
1254
|
+
return "";
|
|
1255
|
+
const parsed = Date.parse(value);
|
|
1256
|
+
if (!Number.isFinite(parsed))
|
|
1257
|
+
return value;
|
|
1258
|
+
return new Date(parsed).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
|
|
1259
|
+
}
|
|
1260
|
+
function firstString(record, keys, fallback = "") {
|
|
1261
|
+
for (const key of keys) {
|
|
1262
|
+
const value = stringField(record, key);
|
|
1263
|
+
if (value)
|
|
1264
|
+
return value;
|
|
1265
|
+
}
|
|
1266
|
+
return fallback;
|
|
1267
|
+
}
|
|
1268
|
+
function runIdOf(run) {
|
|
1269
|
+
return firstString(run, ["runId", "id"], "(unknown-run)");
|
|
1270
|
+
}
|
|
1271
|
+
function taskIdOf(run) {
|
|
1272
|
+
return firstString(run, ["taskId", "task", "task_id"]);
|
|
1273
|
+
}
|
|
1274
|
+
function runTitleOf(run) {
|
|
1275
|
+
return firstString(run, ["title", "summary", "name"], taskIdOf(run) || "(untitled)");
|
|
1276
|
+
}
|
|
1277
|
+
function shouldUseClackOutput() {
|
|
1278
|
+
return Boolean(process.stdout.isTTY) && process.env.RIG_CLI_PLAIN_HELP !== "1";
|
|
1279
|
+
}
|
|
1280
|
+
function printFormattedOutput(message, options = {}) {
|
|
1281
|
+
if (!shouldUseClackOutput()) {
|
|
1282
|
+
console.log(message);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (options.title)
|
|
1286
|
+
note(message, options.title);
|
|
1287
|
+
else
|
|
1288
|
+
log.message(message);
|
|
1289
|
+
}
|
|
1290
|
+
function formatStatusPill(status) {
|
|
1291
|
+
const label = status || "unknown";
|
|
1292
|
+
return statusColor(label)(`\u25CF ${label}`);
|
|
1293
|
+
}
|
|
1294
|
+
function formatSection(title, subtitle) {
|
|
1295
|
+
return `${pc.bold(pc.cyan("\u25C6"))} ${pc.bold(title)}${subtitle ? pc.dim(` \u2014 ${subtitle}`) : ""}`;
|
|
1296
|
+
}
|
|
1297
|
+
function formatSuccessCard(title, rows = []) {
|
|
1298
|
+
const body = rows.filter(([, value]) => value !== undefined && value !== null && String(value).length > 0).map(([key, value]) => `${pc.dim("\u2502")} ${pc.dim(key.padEnd(12))} ${value}`);
|
|
1299
|
+
return [formatSection(title), ...body].join(`
|
|
1300
|
+
`);
|
|
1301
|
+
}
|
|
1302
|
+
function formatNextSteps(steps) {
|
|
1303
|
+
if (steps.length === 0)
|
|
1304
|
+
return [];
|
|
1305
|
+
return [pc.bold("Next"), ...steps.map((step) => `${pc.dim("\u203A")} ${step}`)];
|
|
1306
|
+
}
|
|
1307
|
+
function formatRunList(runs, options = {}) {
|
|
1308
|
+
if (runs.length === 0) {
|
|
1309
|
+
return [
|
|
1310
|
+
formatSection("Runs", "none recorded"),
|
|
1311
|
+
options.source === "server" ? pc.dim("No runs recorded on the selected Rig server.") : pc.dim("No runs recorded in .rig/runs."),
|
|
1312
|
+
"",
|
|
1313
|
+
...formatNextSteps(["Start one: `rig task run --next`", "Check server: `rig server status`"])
|
|
1314
|
+
].join(`
|
|
1315
|
+
`);
|
|
1316
|
+
}
|
|
1317
|
+
const rows = runs.map((run) => {
|
|
1318
|
+
const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
|
|
1319
|
+
const status = stringField(run, "status", "unknown");
|
|
1320
|
+
const taskId = stringField(run, "taskId", "");
|
|
1321
|
+
const title = stringField(run, "title", taskId || "(untitled)");
|
|
1322
|
+
const runtime = stringField(run, "runtimeAdapter", "");
|
|
1323
|
+
return { runId, status, title, runtime };
|
|
1324
|
+
});
|
|
1325
|
+
const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
|
|
1326
|
+
const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
|
|
1327
|
+
const header = `${pc.bold(pad("RUN", idWidth))} ${pc.bold(pad("STATUS", statusWidth))} ${pc.bold("TITLE")}`;
|
|
1328
|
+
const body = rows.map((row) => [
|
|
1329
|
+
pc.bold(pad(truncate(row.runId, idWidth), idWidth)),
|
|
1330
|
+
statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
|
|
1331
|
+
`${row.title}${row.runtime ? pc.dim(` ${row.runtime}`) : ""}`
|
|
1332
|
+
].join(" "));
|
|
1333
|
+
return [formatSection("Runs", options.source === "server" ? "selected server" : "local state"), header, ...body, "", ...formatNextSteps(["Follow live: `rig run attach <run-id> --follow`", "Details: `rig run show <run-id>`"])].join(`
|
|
1334
|
+
`);
|
|
1335
|
+
}
|
|
1336
|
+
function formatRunCard(run, options = {}) {
|
|
1337
|
+
const raw = rawObject(run);
|
|
1338
|
+
const merged = { ...raw, ...run };
|
|
1339
|
+
const runId = runIdOf(merged);
|
|
1340
|
+
const status = firstString(merged, ["status"], "unknown");
|
|
1341
|
+
const taskId = taskIdOf(merged);
|
|
1342
|
+
const title = runTitleOf(merged);
|
|
1343
|
+
const runtime = firstString(merged, ["runtimeAdapter", "runtime", "adapter"]);
|
|
1344
|
+
const mode = firstString(merged, ["runtimeMode", "mode"]);
|
|
1345
|
+
const interaction = firstString(merged, ["interactionMode"]);
|
|
1346
|
+
const created = compactDate(firstString(merged, ["createdAt"]));
|
|
1347
|
+
const started = compactDate(firstString(merged, ["startedAt"]));
|
|
1348
|
+
const updated = compactDate(firstString(merged, ["updatedAt"]));
|
|
1349
|
+
const completed = compactDate(firstString(merged, ["completedAt", "finishedAt"]));
|
|
1350
|
+
const worktree = firstString(merged, ["worktreePath", "cwd", "projectRoot"]);
|
|
1351
|
+
const piSession = merged.piSession && typeof merged.piSession === "object" && !Array.isArray(merged.piSession) ? firstString(merged.piSession, ["sessionId", "id"]) : "";
|
|
1352
|
+
const timeline = Array.isArray(merged.timeline) ? merged.timeline.length : null;
|
|
1353
|
+
const approvals = Array.isArray(merged.approvals) ? merged.approvals.length : null;
|
|
1354
|
+
const inputs = Array.isArray(merged.userInputs) ? merged.userInputs.length : null;
|
|
1355
|
+
const rows = [
|
|
1356
|
+
["run", pc.bold(runId)],
|
|
1357
|
+
["status", formatStatusPill(status)],
|
|
1358
|
+
["task", taskId],
|
|
1359
|
+
["title", title],
|
|
1360
|
+
["runtime", [runtime, mode, interaction].filter(Boolean).join(" \xB7 ")],
|
|
1361
|
+
["created", created],
|
|
1362
|
+
["started", started],
|
|
1363
|
+
["updated", updated],
|
|
1364
|
+
["completed", completed],
|
|
1365
|
+
["worktree", worktree],
|
|
1366
|
+
["pi", piSession],
|
|
1367
|
+
["timeline", timeline],
|
|
1368
|
+
["approvals", approvals],
|
|
1369
|
+
["inputs", inputs]
|
|
1370
|
+
];
|
|
1371
|
+
return [
|
|
1372
|
+
formatSuccessCard(options.title ?? "Run details", rows),
|
|
1373
|
+
"",
|
|
1374
|
+
...formatNextSteps([`Follow live: \`rig run attach ${runId} --follow\``, `Raw payload: \`rig run show ${runId} --raw\``])
|
|
1375
|
+
].join(`
|
|
1376
|
+
`);
|
|
1377
|
+
}
|
|
1378
|
+
function formatRunStatus(summary, options = {}) {
|
|
1379
|
+
const activeRuns = summary.activeRuns ?? [];
|
|
1380
|
+
const recentRuns = summary.recentRuns ?? [];
|
|
1381
|
+
const lines = [formatSection("Run status", options.source === "server" ? "selected server" : "local state")];
|
|
1382
|
+
lines.push("", pc.bold(`Active runs (${activeRuns.length})`));
|
|
1383
|
+
if (activeRuns.length === 0) {
|
|
1384
|
+
lines.push(pc.dim("No active runs."));
|
|
1385
|
+
} else {
|
|
1386
|
+
for (const run of activeRuns) {
|
|
1387
|
+
lines.push(formatRunSummaryLine(run));
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
lines.push("", pc.bold(`Recent runs (${recentRuns.length})`));
|
|
1391
|
+
if (recentRuns.length === 0) {
|
|
1392
|
+
lines.push(pc.dim("No recent terminal runs."));
|
|
1393
|
+
} else {
|
|
1394
|
+
for (const run of recentRuns.slice(0, 10)) {
|
|
1395
|
+
lines.push(formatRunSummaryLine(run));
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
lines.push("", ...formatNextSteps(["Start work: `rig task run --next`", "Attach: `rig run attach <run-id> --follow`", "Details: `rig run show <run-id>`"]));
|
|
1399
|
+
return lines.join(`
|
|
1400
|
+
`);
|
|
1401
|
+
}
|
|
1402
|
+
function formatRunSummaryLine(run) {
|
|
1403
|
+
const record = run;
|
|
1404
|
+
const runId = runIdOf(record);
|
|
1405
|
+
const status = firstString(record, ["status"], "unknown");
|
|
1406
|
+
const taskId = taskIdOf(record);
|
|
1407
|
+
const title = runTitleOf(record);
|
|
1408
|
+
const runtime = firstString(record, ["runtimeAdapter", "runtime", "adapter"]);
|
|
1409
|
+
const descriptor = [taskId, title].filter(Boolean).join(" \xB7 ");
|
|
1410
|
+
return `${pc.dim("\u2502")} ${pc.bold(runId)} ${formatStatusPill(status)} ${descriptor}${runtime ? pc.dim(` ${runtime}`) : ""}`;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
408
1413
|
// packages/cli/src/commands/run.ts
|
|
1414
|
+
function normalizeRemoteRunDetails(payload) {
|
|
1415
|
+
const run = payload.run;
|
|
1416
|
+
if (!run || typeof run !== "object" || Array.isArray(run))
|
|
1417
|
+
return null;
|
|
1418
|
+
return {
|
|
1419
|
+
...run,
|
|
1420
|
+
...Array.isArray(payload.timeline) ? { timeline: payload.timeline } : {},
|
|
1421
|
+
...Array.isArray(payload.approvals) ? { approvals: payload.approvals } : {},
|
|
1422
|
+
...Array.isArray(payload.userInputs) ? { userInputs: payload.userInputs } : {}
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
var REMOTE_TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged"]);
|
|
1426
|
+
function isRemoteConnectionSelected(projectRoot) {
|
|
1427
|
+
return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
|
|
1428
|
+
}
|
|
1429
|
+
async function listRunsForSelectedConnection(context, options = {}) {
|
|
1430
|
+
if (isRemoteConnectionSelected(context.projectRoot)) {
|
|
1431
|
+
return { runs: await listRunsViaServer(context, options), source: "server" };
|
|
1432
|
+
}
|
|
1433
|
+
return { runs: listAuthorityRuns(context.projectRoot), source: "local" };
|
|
1434
|
+
}
|
|
1435
|
+
function runStringField(run, key, fallback = "") {
|
|
1436
|
+
const value = run[key];
|
|
1437
|
+
return typeof value === "string" && value.trim() ? value : fallback;
|
|
1438
|
+
}
|
|
1439
|
+
function buildServerRunStatus(runs) {
|
|
1440
|
+
const activeRuns = runs.filter((run) => !REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
|
|
1441
|
+
const recentRuns = runs.filter((run) => REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
|
|
1442
|
+
return { activeRuns, recentRuns, runs };
|
|
1443
|
+
}
|
|
409
1444
|
function shouldPromptForEpicSelection(context, command, promptEpic, noEpicPrompt) {
|
|
410
1445
|
if (noEpicPrompt) {
|
|
411
1446
|
return false;
|
|
@@ -467,21 +1502,15 @@ async function promptForEpicSelection(projectRoot, command) {
|
|
|
467
1502
|
}
|
|
468
1503
|
async function executeRun(context, args) {
|
|
469
1504
|
const [command = "status", ...rest] = args;
|
|
470
|
-
const runtimeContext =
|
|
1505
|
+
const runtimeContext = loadRuntimeContextFromEnv() ?? undefined;
|
|
471
1506
|
switch (command) {
|
|
472
1507
|
case "list": {
|
|
473
|
-
requireNoExtraArgs(rest, "
|
|
474
|
-
const runs =
|
|
1508
|
+
requireNoExtraArgs(rest, "rig run list");
|
|
1509
|
+
const { runs, source } = await listRunsForSelectedConnection(context, { limit: 100 });
|
|
475
1510
|
if (context.outputMode === "text") {
|
|
476
|
-
|
|
477
|
-
console.log("No runs recorded in .rig/runs.");
|
|
478
|
-
} else {
|
|
479
|
-
for (const run of runs) {
|
|
480
|
-
console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.title}`);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
1511
|
+
printFormattedOutput(formatRunList(runs, { source }));
|
|
483
1512
|
}
|
|
484
|
-
return { ok: true, group: "run", command, details: { runs } };
|
|
1513
|
+
return { ok: true, group: "run", command, details: { runs, source } };
|
|
485
1514
|
}
|
|
486
1515
|
case "delete": {
|
|
487
1516
|
let pending = rest;
|
|
@@ -489,7 +1518,7 @@ async function executeRun(context, args) {
|
|
|
489
1518
|
pending = run.rest;
|
|
490
1519
|
const purgeArtifacts = takeFlag(pending, "--purge-artifacts");
|
|
491
1520
|
pending = purgeArtifacts.rest;
|
|
492
|
-
requireNoExtraArgs(pending, "
|
|
1521
|
+
requireNoExtraArgs(pending, "rig run delete --run <id> [--purge-artifacts]");
|
|
493
1522
|
if (!run.value) {
|
|
494
1523
|
throw new CliError2("run delete requires --run <id>.");
|
|
495
1524
|
}
|
|
@@ -519,7 +1548,7 @@ async function executeRun(context, args) {
|
|
|
519
1548
|
pending = keepRuntimes.rest;
|
|
520
1549
|
const keepQueue = takeFlag(pending, "--keep-queue");
|
|
521
1550
|
pending = keepQueue.rest;
|
|
522
|
-
requireNoExtraArgs(pending, "
|
|
1551
|
+
requireNoExtraArgs(pending, "rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
|
|
523
1552
|
if (!all.value) {
|
|
524
1553
|
throw new CliError2("run cleanup currently requires --all.");
|
|
525
1554
|
}
|
|
@@ -538,20 +1567,25 @@ async function executeRun(context, args) {
|
|
|
538
1567
|
}
|
|
539
1568
|
case "show": {
|
|
540
1569
|
let pending = rest;
|
|
1570
|
+
const rawResult = takeFlag(pending, "--raw");
|
|
1571
|
+
pending = rawResult.rest;
|
|
541
1572
|
const run = takeOption(pending, "--run");
|
|
542
1573
|
pending = run.rest;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
1574
|
+
const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
|
|
1575
|
+
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
1576
|
+
requireNoExtraArgs(extra, "rig run show <id>|--run <id> [--raw]");
|
|
1577
|
+
const runId = run.value ?? positionalRunId;
|
|
1578
|
+
if (!runId) {
|
|
1579
|
+
throw new CliError2("run show requires a run id.");
|
|
546
1580
|
}
|
|
547
|
-
const record = readAuthorityRun(context.projectRoot,
|
|
1581
|
+
const record = readAuthorityRun(context.projectRoot, runId) ?? normalizeRemoteRunDetails(await getRunDetailsViaServer(context, runId).catch(() => ({})));
|
|
548
1582
|
if (!record) {
|
|
549
|
-
throw new CliError2(`Run not found: ${
|
|
1583
|
+
throw new CliError2(`Run not found: ${runId}`, 2);
|
|
550
1584
|
}
|
|
551
1585
|
if (context.outputMode === "text") {
|
|
552
|
-
|
|
1586
|
+
printFormattedOutput(rawResult.value ? JSON.stringify(record, null, 2) : formatRunCard(record));
|
|
553
1587
|
}
|
|
554
|
-
return { ok: true, group: "run", command, details: record };
|
|
1588
|
+
return { ok: true, group: "run", command, details: { ...record, rawOutput: rawResult.value } };
|
|
555
1589
|
}
|
|
556
1590
|
case "timeline": {
|
|
557
1591
|
let pending = rest;
|
|
@@ -559,38 +1593,28 @@ async function executeRun(context, args) {
|
|
|
559
1593
|
pending = run.rest;
|
|
560
1594
|
const follow = takeFlag(pending, "--follow");
|
|
561
1595
|
pending = follow.rest;
|
|
562
|
-
requireNoExtraArgs(pending, "
|
|
1596
|
+
requireNoExtraArgs(pending, "rig run timeline --run <id> [--follow]");
|
|
563
1597
|
if (!run.value) {
|
|
564
1598
|
throw new CliError2("run timeline requires --run <id>.");
|
|
565
1599
|
}
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
return events2;
|
|
575
|
-
};
|
|
576
|
-
const events = printEvents();
|
|
1600
|
+
const renderer = createPiRunStreamRenderer();
|
|
1601
|
+
let cursor = null;
|
|
1602
|
+
const page = await getRunTimelineViaServer(context, run.value, { limit: 500 });
|
|
1603
|
+
const events = Array.isArray(page.entries) ? page.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1604
|
+
cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
|
|
1605
|
+
if (context.outputMode === "text") {
|
|
1606
|
+
renderer.renderTimeline(events);
|
|
1607
|
+
}
|
|
577
1608
|
if (follow.value && context.outputMode === "text") {
|
|
578
|
-
let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
|
|
579
1609
|
while (true) {
|
|
580
1610
|
await Bun.sleep(1000);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
continue;
|
|
586
|
-
const delta = next.slice(lastLength);
|
|
587
|
-
lastLength = next.length;
|
|
588
|
-
for (const line of delta.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) {
|
|
589
|
-
console.log(line);
|
|
590
|
-
}
|
|
1611
|
+
const nextPage = await getRunTimelineViaServer(context, run.value, { limit: 500, ...cursor ? { cursor } : {} });
|
|
1612
|
+
const nextEvents = Array.isArray(nextPage.entries) ? nextPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1613
|
+
cursor = typeof nextPage.nextCursor === "string" ? nextPage.nextCursor : cursor;
|
|
1614
|
+
renderer.renderTimeline(nextEvents);
|
|
591
1615
|
}
|
|
592
1616
|
}
|
|
593
|
-
return { ok: true, group: "run", command, details: { runId: run.value, events } };
|
|
1617
|
+
return { ok: true, group: "run", command, details: { runId: run.value, events, cursor } };
|
|
594
1618
|
}
|
|
595
1619
|
case "attach": {
|
|
596
1620
|
let pending = rest;
|
|
@@ -606,41 +1630,38 @@ async function executeRun(context, args) {
|
|
|
606
1630
|
pending = pollMs.rest;
|
|
607
1631
|
const positionalRunId = pending.length > 0 ? pending[0] : undefined;
|
|
608
1632
|
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
609
|
-
requireNoExtraArgs(extra, "
|
|
1633
|
+
requireNoExtraArgs(extra, "rig run attach <run-id>|--run <run-id> [--message <text>] [--once|--follow] [--poll-ms <ms>]");
|
|
610
1634
|
const runId = runOption.value ?? positionalRunId;
|
|
611
1635
|
if (!runId) {
|
|
612
1636
|
throw new CliError2("run attach requires a run id.", 2);
|
|
613
1637
|
}
|
|
1638
|
+
let steered = false;
|
|
1639
|
+
if (messageOption.value?.trim()) {
|
|
1640
|
+
await steerRunViaServer(context, runId, messageOption.value.trim());
|
|
1641
|
+
steered = true;
|
|
1642
|
+
}
|
|
614
1643
|
const attached = await attachRunOperatorView(context, {
|
|
615
1644
|
runId,
|
|
616
|
-
message:
|
|
1645
|
+
message: null,
|
|
617
1646
|
once: once.value,
|
|
618
1647
|
follow: follow.value,
|
|
619
1648
|
pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
|
|
620
1649
|
});
|
|
621
|
-
return { ok: true, group: "run", command, details: attached };
|
|
1650
|
+
return { ok: true, group: "run", command, details: { ...attached, steered: attached.steered || steered } };
|
|
622
1651
|
}
|
|
623
1652
|
case "status": {
|
|
624
|
-
requireNoExtraArgs(rest, "
|
|
1653
|
+
requireNoExtraArgs(rest, "rig run status");
|
|
625
1654
|
if (context.dryRun) {
|
|
626
1655
|
if (context.outputMode === "text") {
|
|
627
1656
|
console.log("[dry-run] rig run status");
|
|
628
1657
|
}
|
|
629
1658
|
return { ok: true, group: "run", command };
|
|
630
1659
|
}
|
|
631
|
-
const summary = runStatus(context.projectRoot, runtimeContext);
|
|
1660
|
+
const summary = isRemoteConnectionSelected(context.projectRoot) ? buildServerRunStatus(await listRunsViaServer(context, { limit: 100 })) : runStatus(context.projectRoot, runtimeContext);
|
|
1661
|
+
const activeRuns = Array.isArray(summary.activeRuns) ? summary.activeRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
|
|
1662
|
+
const recentRuns = Array.isArray(summary.recentRuns) ? summary.recentRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
|
|
632
1663
|
if (context.outputMode === "text") {
|
|
633
|
-
|
|
634
|
-
for (const run of summary.activeRuns) {
|
|
635
|
-
console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.taskId ?? run.title}`);
|
|
636
|
-
}
|
|
637
|
-
if (summary.recentRuns.length > 0) {
|
|
638
|
-
console.log("");
|
|
639
|
-
console.log("Recent runs:");
|
|
640
|
-
for (const run of summary.recentRuns) {
|
|
641
|
-
console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.taskId ?? run.title}`);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
1664
|
+
printFormattedOutput(formatRunStatus({ activeRuns, recentRuns, runs: Array.isArray(summary.runs) ? summary.runs : [...activeRuns, ...recentRuns] }, { source: isRemoteConnectionSelected(context.projectRoot) ? "server" : "local" }));
|
|
644
1665
|
}
|
|
645
1666
|
return { ok: true, group: "run", command, details: summary };
|
|
646
1667
|
}
|
|
@@ -664,7 +1685,7 @@ async function executeRun(context, args) {
|
|
|
664
1685
|
pending = pollResult.rest;
|
|
665
1686
|
const noServerResult = takeFlag(pending, "--no-server");
|
|
666
1687
|
pending = noServerResult.rest;
|
|
667
|
-
requireNoExtraArgs(pending, "
|
|
1688
|
+
requireNoExtraArgs(pending, "rig run start [--epic <id>] [--prompt-epic|--no-epic-prompt] [--ws-port <n>] [--server-host <host>] [--server-port <n>] [--poll-ms <n>] [--no-server]");
|
|
668
1689
|
if (promptEpicResult.value && noEpicPromptResult.value) {
|
|
669
1690
|
throw new CliError2("Cannot use --prompt-epic and --no-epic-prompt together.");
|
|
670
1691
|
}
|
|
@@ -712,7 +1733,7 @@ async function executeRun(context, args) {
|
|
|
712
1733
|
};
|
|
713
1734
|
}
|
|
714
1735
|
case "resume": {
|
|
715
|
-
requireNoExtraArgs(rest, "
|
|
1736
|
+
requireNoExtraArgs(rest, "rig run resume");
|
|
716
1737
|
if (context.dryRun) {
|
|
717
1738
|
if (context.outputMode === "text") {
|
|
718
1739
|
console.log("[dry-run] rig run resume");
|
|
@@ -725,11 +1746,25 @@ async function executeRun(context, args) {
|
|
|
725
1746
|
}
|
|
726
1747
|
return { ok: true, group: "run", command, details: resumed };
|
|
727
1748
|
}
|
|
1749
|
+
case "restart": {
|
|
1750
|
+
requireNoExtraArgs(rest, "rig run restart");
|
|
1751
|
+
if (context.dryRun) {
|
|
1752
|
+
if (context.outputMode === "text") {
|
|
1753
|
+
console.log("[dry-run] rig run restart");
|
|
1754
|
+
}
|
|
1755
|
+
return { ok: true, group: "run", command };
|
|
1756
|
+
}
|
|
1757
|
+
const restarted = await runRestart(context.projectRoot, runtimeContext);
|
|
1758
|
+
if (context.outputMode === "text") {
|
|
1759
|
+
console.log(`Restarted run: ${restarted.runId}`);
|
|
1760
|
+
}
|
|
1761
|
+
return { ok: true, group: "run", command, details: restarted };
|
|
1762
|
+
}
|
|
728
1763
|
case "stop": {
|
|
729
1764
|
const runOption = takeOption(rest, "--run");
|
|
730
1765
|
const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;
|
|
731
1766
|
const extra = positionalRunId ? runOption.rest.slice(1) : runOption.rest;
|
|
732
|
-
requireNoExtraArgs(extra, "
|
|
1767
|
+
requireNoExtraArgs(extra, "rig run stop [<run-id>|--run <id>]");
|
|
733
1768
|
const runId = runOption.value ?? positionalRunId;
|
|
734
1769
|
if (context.dryRun) {
|
|
735
1770
|
return {
|