@h-rig/cli 0.0.6-alpha.9 → 0.0.6-alpha.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin/build-rig-binaries.d.ts +2 -0
- package/dist/bin/rig.d.ts +2 -0
- package/dist/bin/rig.js +11052 -6668
- package/dist/src/app/board.d.ts +23 -0
- package/dist/src/app/board.js +1786 -0
- package/dist/src/app/drone-ui.d.ts +37 -0
- package/dist/src/app/drone-ui.js +294 -0
- package/dist/src/app/theme.d.ts +47 -0
- package/dist/src/app/theme.js +150 -0
- package/dist/src/commands/_async-ui.d.ts +13 -0
- package/dist/src/commands/_async-ui.js +223 -0
- package/dist/src/commands/_authority-runs.d.ts +22 -0
- package/dist/src/commands/_authority-runs.js +2 -3
- package/dist/src/commands/_cli-format.d.ts +49 -0
- package/dist/src/commands/_cli-format.js +447 -0
- package/dist/src/commands/_connection-state.d.ts +44 -0
- package/dist/src/commands/_connection-state.js +30 -11
- package/dist/src/commands/_doctor-checks.d.ts +52 -0
- package/dist/src/commands/_doctor-checks.js +180 -44
- package/dist/src/commands/_help-catalog.d.ts +51 -0
- package/dist/src/commands/_help-catalog.js +442 -0
- package/dist/src/commands/_json-output.d.ts +11 -0
- package/dist/src/commands/_json-output.js +56 -0
- package/dist/src/commands/_operator-surface.d.ts +34 -0
- package/dist/src/commands/_operator-surface.js +220 -0
- package/dist/src/commands/_operator-view.d.ts +30 -0
- package/dist/src/commands/_operator-view.js +803 -73
- package/dist/src/commands/_parsers.d.ts +15 -0
- package/dist/src/commands/_parsers.js +18 -11
- package/dist/src/commands/_paths.d.ts +11 -0
- package/dist/src/commands/_pi-frontend.d.ts +27 -0
- package/dist/src/commands/_pi-frontend.js +742 -0
- package/dist/src/commands/_pi-install.d.ts +42 -0
- package/dist/src/commands/_pi-install.js +5 -4
- package/dist/src/commands/_policy.d.ts +8 -0
- package/dist/src/commands/_policy.js +12 -5
- package/dist/src/commands/_preflight.d.ts +22 -0
- package/dist/src/commands/_preflight.js +190 -128
- package/dist/src/commands/_probes.d.ts +1 -0
- package/dist/src/commands/_run-driver-helpers.d.ts +99 -0
- package/dist/src/commands/_run-driver-helpers.js +75 -22
- package/dist/src/commands/_run-replay.d.ts +24 -0
- package/dist/src/commands/_run-replay.js +142 -0
- package/dist/src/commands/_server-client.d.ts +186 -0
- package/dist/src/commands/_server-client.js +360 -61
- package/dist/src/commands/_snapshot-upload.d.ts +39 -0
- package/dist/src/commands/_snapshot-upload.js +163 -39
- package/dist/src/commands/_spinner.d.ts +25 -0
- package/dist/src/commands/_spinner.js +65 -0
- package/dist/src/commands/_task-picker.d.ts +9 -0
- package/dist/src/commands/_task-picker.js +172 -19
- package/dist/src/commands/agent.d.ts +3 -0
- package/dist/src/commands/agent.js +39 -20
- package/dist/src/commands/browser.d.ts +65 -0
- package/dist/src/commands/browser.js +334 -51
- package/dist/src/commands/connect.d.ts +7 -0
- package/dist/src/commands/connect.js +272 -33
- package/dist/src/commands/dist.d.ts +28 -0
- package/dist/src/commands/dist.js +19 -12
- package/dist/src/commands/doctor.d.ts +3 -0
- package/dist/src/commands/doctor.js +378 -45
- package/dist/src/commands/github.d.ts +3 -0
- package/dist/src/commands/github.js +375 -53
- package/dist/src/commands/inbox.d.ts +30 -0
- package/dist/src/commands/inbox.js +760 -70
- package/dist/src/commands/init.d.ts +74 -0
- package/dist/src/commands/init.js +963 -119
- package/dist/src/commands/inspect.d.ts +3 -0
- package/dist/src/commands/inspect.js +588 -32
- package/dist/src/commands/inspector.d.ts +3 -0
- package/dist/src/commands/inspector.js +20 -13
- package/dist/src/commands/pi.d.ts +3 -0
- package/dist/src/commands/pi.js +177 -0
- package/dist/src/commands/plugin.d.ts +16 -0
- package/dist/src/commands/plugin.js +95 -27
- package/dist/src/commands/profile-and-review.d.ts +4 -0
- package/dist/src/commands/profile-and-review.js +26 -19
- package/dist/src/commands/queue.d.ts +3 -0
- package/dist/src/commands/queue.js +32 -12
- package/dist/src/commands/remote.d.ts +3 -0
- package/dist/src/commands/remote.js +43 -36
- package/dist/src/commands/repo-git-harness.d.ts +5 -0
- package/dist/src/commands/repo-git-harness.js +22 -15
- package/dist/src/commands/run.d.ts +3 -0
- package/dist/src/commands/run.js +1379 -159
- package/dist/src/commands/server.d.ts +7 -0
- package/dist/src/commands/server.js +502 -57
- package/dist/src/commands/setup.d.ts +16 -0
- package/dist/src/commands/setup.js +390 -63
- package/dist/src/commands/stats.d.ts +12 -0
- package/dist/src/commands/stats.js +1051 -0
- package/dist/src/commands/task-report-bug.d.ts +19 -0
- package/dist/src/commands/task-report-bug.js +260 -62
- package/dist/src/commands/task-run-driver.d.ts +132 -0
- package/dist/src/commands/task-run-driver.js +886 -130
- package/dist/src/commands/task.d.ts +14 -0
- package/dist/src/commands/task.js +1644 -314
- package/dist/src/commands/test.d.ts +3 -0
- package/dist/src/commands/test.js +15 -8
- package/dist/src/commands/workspace.d.ts +3 -0
- package/dist/src/commands/workspace.js +18 -11
- package/dist/src/commands.d.ts +29 -0
- package/dist/src/commands.js +10967 -6637
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +10168 -5787
- package/dist/src/launcher.d.ts +61 -0
- package/dist/src/launcher.js +77 -13
- package/dist/src/report-bug.d.ts +44 -0
- package/dist/src/report-bug.js +3 -3
- package/dist/src/runner.d.ts +47 -0
- package/dist/src/runner.js +16 -22
- package/dist/src/withMutedConsole.d.ts +2 -0
- package/package.json +13 -6
package/dist/src/commands/run.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
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
|
-
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
7
|
+
import { CliError as RuntimeCliError } 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
|
+
|
|
11
|
+
class CliError extends RuntimeCliError {
|
|
12
|
+
hint;
|
|
13
|
+
constructor(message, exitCode = 1, options = {}) {
|
|
14
|
+
super(message, exitCode);
|
|
15
|
+
if (options.hint?.trim()) {
|
|
16
|
+
this.hint = options.hint.trim();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
15
20
|
function takeFlag(args, flag) {
|
|
16
21
|
const rest = [];
|
|
17
22
|
let value = false;
|
|
@@ -32,7 +37,7 @@ function takeOption(args, option) {
|
|
|
32
37
|
if (current === option) {
|
|
33
38
|
const next = args[index + 1];
|
|
34
39
|
if (!next || next.startsWith("-")) {
|
|
35
|
-
throw new CliError(`Missing value for ${option}`);
|
|
40
|
+
throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
|
|
36
41
|
}
|
|
37
42
|
value = next;
|
|
38
43
|
index += 1;
|
|
@@ -54,23 +59,19 @@ Usage: ${usage}`);
|
|
|
54
59
|
// packages/cli/src/commands/run.ts
|
|
55
60
|
import {
|
|
56
61
|
listAuthorityRuns,
|
|
57
|
-
readAuthorityRun
|
|
58
|
-
readJsonlFile,
|
|
59
|
-
resolveAuthorityRunDir
|
|
62
|
+
readAuthorityRun as readAuthorityRun2
|
|
60
63
|
} from "@rig/runtime/control-plane/authority-files";
|
|
61
64
|
import {
|
|
62
65
|
cleanupRunState,
|
|
63
66
|
deleteRunState,
|
|
64
67
|
listOpenEpics,
|
|
65
68
|
resolveDefaultEpic,
|
|
66
|
-
runResume,
|
|
67
|
-
runRestart,
|
|
68
69
|
runStatus,
|
|
69
70
|
runStop,
|
|
70
71
|
startRun,
|
|
71
72
|
defaultStartRunOptions
|
|
72
73
|
} from "@rig/runtime/control-plane/native/run-ops";
|
|
73
|
-
import { loadRuntimeContextFromEnv
|
|
74
|
+
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
74
75
|
|
|
75
76
|
// packages/cli/src/commands/_parsers.ts
|
|
76
77
|
function parsePositiveInt(value, option, fallback) {
|
|
@@ -79,13 +80,12 @@ function parsePositiveInt(value, option, fallback) {
|
|
|
79
80
|
}
|
|
80
81
|
const parsed = Number.parseInt(value, 10);
|
|
81
82
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
82
|
-
throw new
|
|
83
|
+
throw new CliError(`Invalid ${option} value: ${value}`, 1, { hint: `Pass a positive integer, e.g. \`${option} 10\`.` });
|
|
83
84
|
}
|
|
84
85
|
return parsed;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
// packages/cli/src/commands/_server-client.ts
|
|
88
|
-
import { spawnSync } from "child_process";
|
|
89
89
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
90
90
|
import { resolve as resolve2 } from "path";
|
|
91
91
|
import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
|
|
@@ -112,9 +112,14 @@ function readJsonFile(path) {
|
|
|
112
112
|
try {
|
|
113
113
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
114
114
|
} catch (error) {
|
|
115
|
-
throw new
|
|
115
|
+
throw new CliError(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1, { hint: "Fix or delete that file, then re-select a server with `rig server use <alias|local>`." });
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
function writeJsonFile(path, value) {
|
|
119
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
120
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
121
|
+
`, "utf8");
|
|
122
|
+
}
|
|
118
123
|
function normalizeConnection(value) {
|
|
119
124
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
120
125
|
return null;
|
|
@@ -155,25 +160,47 @@ function readRepoConnection(projectRoot) {
|
|
|
155
160
|
return {
|
|
156
161
|
selected,
|
|
157
162
|
project: typeof record.project === "string" ? record.project : undefined,
|
|
158
|
-
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
|
|
163
|
+
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
|
|
164
|
+
serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
|
|
159
165
|
};
|
|
160
166
|
}
|
|
167
|
+
function writeRepoConnection(projectRoot, state) {
|
|
168
|
+
writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
|
|
169
|
+
}
|
|
161
170
|
function resolveSelectedConnection(projectRoot, options = {}) {
|
|
162
171
|
const repo = readRepoConnection(projectRoot);
|
|
163
172
|
if (!repo)
|
|
164
173
|
return null;
|
|
165
174
|
if (repo.selected === "local")
|
|
166
|
-
return { alias: "local", connection: { kind: "local", mode: "auto" } };
|
|
175
|
+
return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
|
|
167
176
|
const global = readGlobalConnections(options);
|
|
168
177
|
const connection = global.connections[repo.selected];
|
|
169
178
|
if (!connection) {
|
|
170
|
-
throw new
|
|
179
|
+
throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
|
|
171
180
|
}
|
|
172
|
-
return { alias: repo.selected, connection };
|
|
181
|
+
return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
|
|
182
|
+
}
|
|
183
|
+
function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
|
|
184
|
+
const repo = readRepoConnection(projectRoot);
|
|
185
|
+
if (!repo)
|
|
186
|
+
return;
|
|
187
|
+
writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
|
|
188
|
+
}
|
|
189
|
+
function isRemoteConnectionSelected(projectRoot) {
|
|
190
|
+
return resolveSelectedConnection(projectRoot)?.connection.kind === "remote";
|
|
173
191
|
}
|
|
174
192
|
|
|
175
193
|
// packages/cli/src/commands/_server-client.ts
|
|
176
|
-
var
|
|
194
|
+
var scopedGitHubBearerTokens = new Map;
|
|
195
|
+
var serverPhaseListener = null;
|
|
196
|
+
function setServerPhaseListener(listener) {
|
|
197
|
+
const previous = serverPhaseListener;
|
|
198
|
+
serverPhaseListener = listener;
|
|
199
|
+
return previous;
|
|
200
|
+
}
|
|
201
|
+
function reportServerPhase(label) {
|
|
202
|
+
serverPhaseListener?.(label);
|
|
203
|
+
}
|
|
177
204
|
function cleanToken(value) {
|
|
178
205
|
const trimmed = value?.trim();
|
|
179
206
|
return trimmed ? trimmed : null;
|
|
@@ -190,49 +217,80 @@ function readPrivateRemoteSessionToken(projectRoot) {
|
|
|
190
217
|
}
|
|
191
218
|
}
|
|
192
219
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
193
|
-
|
|
194
|
-
|
|
220
|
+
const scopedKey = resolve2(projectRoot);
|
|
221
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
222
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
195
223
|
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
196
|
-
if (privateSession)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
if (privateSession)
|
|
225
|
+
return privateSession;
|
|
226
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
227
|
+
}
|
|
228
|
+
function readStoredGitHubAuthToken(projectRoot) {
|
|
229
|
+
const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
|
|
230
|
+
if (!existsSync2(path))
|
|
231
|
+
return null;
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
234
|
+
return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function readLocalConnectionFallbackToken(projectRoot) {
|
|
240
|
+
return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
|
|
212
241
|
}
|
|
213
242
|
async function ensureServerForCli(projectRoot) {
|
|
214
243
|
try {
|
|
215
244
|
const selected = resolveSelectedConnection(projectRoot);
|
|
216
245
|
if (selected?.connection.kind === "remote") {
|
|
246
|
+
reportServerPhase(`Connecting to ${selected.alias}\u2026`);
|
|
247
|
+
const authToken = readGitHubBearerTokenForRemote(projectRoot);
|
|
248
|
+
const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
|
|
217
249
|
return {
|
|
218
250
|
baseUrl: selected.connection.baseUrl,
|
|
219
|
-
authToken
|
|
220
|
-
connectionKind: "remote"
|
|
251
|
+
authToken,
|
|
252
|
+
connectionKind: "remote",
|
|
253
|
+
serverProjectRoot
|
|
221
254
|
};
|
|
222
255
|
}
|
|
256
|
+
reportServerPhase("Starting local Rig server\u2026");
|
|
223
257
|
const connection = await ensureLocalRigServerConnection(projectRoot);
|
|
224
258
|
return {
|
|
225
259
|
baseUrl: connection.baseUrl,
|
|
226
|
-
authToken: connection.authToken,
|
|
227
|
-
connectionKind: "local"
|
|
260
|
+
authToken: connection.authToken ?? readLocalConnectionFallbackToken(projectRoot),
|
|
261
|
+
connectionKind: "local",
|
|
262
|
+
serverProjectRoot: resolve2(projectRoot)
|
|
228
263
|
};
|
|
229
264
|
} catch (error) {
|
|
230
265
|
if (error instanceof Error) {
|
|
231
|
-
throw new
|
|
266
|
+
throw new CliError(error.message, 1);
|
|
232
267
|
}
|
|
233
268
|
throw error;
|
|
234
269
|
}
|
|
235
270
|
}
|
|
271
|
+
async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
|
|
272
|
+
const repo = readRepoConnection(projectRoot);
|
|
273
|
+
const slug = repo?.project?.trim();
|
|
274
|
+
if (!slug)
|
|
275
|
+
return null;
|
|
276
|
+
try {
|
|
277
|
+
const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
|
|
278
|
+
headers: mergeHeaders(undefined, authToken)
|
|
279
|
+
});
|
|
280
|
+
if (!response.ok)
|
|
281
|
+
return null;
|
|
282
|
+
const payload = await response.json();
|
|
283
|
+
const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
|
|
284
|
+
const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
|
|
285
|
+
const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
|
|
286
|
+
const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
|
|
287
|
+
if (path)
|
|
288
|
+
writeRepoServerProjectRoot(projectRoot, path);
|
|
289
|
+
return path;
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
236
294
|
function mergeHeaders(headers, authToken) {
|
|
237
295
|
const merged = new Headers(headers);
|
|
238
296
|
if (authToken) {
|
|
@@ -255,12 +313,65 @@ function diagnosticMessage(payload) {
|
|
|
255
313
|
});
|
|
256
314
|
return messages.length > 0 ? messages.join("; ") : null;
|
|
257
315
|
}
|
|
316
|
+
var serverReachabilityCache = new Map;
|
|
317
|
+
async function probeServerReachability(baseUrl, authToken) {
|
|
318
|
+
try {
|
|
319
|
+
const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
|
|
320
|
+
headers: mergeHeaders(undefined, authToken),
|
|
321
|
+
signal: AbortSignal.timeout(1500)
|
|
322
|
+
});
|
|
323
|
+
return response.ok;
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function cachedServerReachability(projectRoot, baseUrl, authToken) {
|
|
329
|
+
const key = resolve2(projectRoot);
|
|
330
|
+
const cached = serverReachabilityCache.get(key);
|
|
331
|
+
if (cached)
|
|
332
|
+
return cached;
|
|
333
|
+
const probe = probeServerReachability(baseUrl, authToken);
|
|
334
|
+
serverReachabilityCache.set(key, probe);
|
|
335
|
+
return probe;
|
|
336
|
+
}
|
|
337
|
+
function describeSelectedServer(projectRoot, server) {
|
|
338
|
+
try {
|
|
339
|
+
const selected = resolveSelectedConnection(projectRoot);
|
|
340
|
+
if (selected) {
|
|
341
|
+
return {
|
|
342
|
+
alias: selected.alias,
|
|
343
|
+
target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
} catch {}
|
|
347
|
+
return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
|
|
348
|
+
}
|
|
349
|
+
async function buildServerFailureContext(projectRoot, server) {
|
|
350
|
+
const { alias, target } = describeSelectedServer(projectRoot, server);
|
|
351
|
+
const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
|
|
352
|
+
const reachability = reachable ? "server is reachable" : "server is unreachable";
|
|
353
|
+
return {
|
|
354
|
+
contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
|
|
355
|
+
hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
|
|
356
|
+
};
|
|
357
|
+
}
|
|
258
358
|
async function requestServerJson(context, pathname, init = {}) {
|
|
259
359
|
const server = await ensureServerForCli(context.projectRoot);
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
headers
|
|
263
|
-
});
|
|
360
|
+
const headers = mergeHeaders(init.headers, server.authToken);
|
|
361
|
+
if (server.serverProjectRoot)
|
|
362
|
+
headers.set("x-rig-project-root", server.serverProjectRoot);
|
|
363
|
+
reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
|
|
364
|
+
let response;
|
|
365
|
+
try {
|
|
366
|
+
response = await fetch(`${server.baseUrl}${pathname}`, {
|
|
367
|
+
...init,
|
|
368
|
+
headers
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
const failure = await buildServerFailureContext(context.projectRoot, server);
|
|
372
|
+
throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
|
|
373
|
+
${failure.contextLine}`, 1, { hint: failure.hint });
|
|
374
|
+
}
|
|
264
375
|
const text = await response.text();
|
|
265
376
|
const payload = text.trim().length > 0 ? (() => {
|
|
266
377
|
try {
|
|
@@ -271,11 +382,23 @@ async function requestServerJson(context, pathname, init = {}) {
|
|
|
271
382
|
})() : null;
|
|
272
383
|
if (!response.ok) {
|
|
273
384
|
const diagnostics = diagnosticMessage(payload);
|
|
274
|
-
const
|
|
275
|
-
|
|
385
|
+
const rawDetail = diagnostics ?? (text || response.statusText);
|
|
386
|
+
const detail = diagnostics ? rawDetail : rawDetail.split(`
|
|
387
|
+
`).map((line) => line.trim()).find((line) => line.length > 0)?.slice(0, 200) ?? response.statusText;
|
|
388
|
+
const failure = await buildServerFailureContext(context.projectRoot, server);
|
|
389
|
+
throw new CliError(`Rig server request failed (${response.status}): ${detail}
|
|
390
|
+
${failure.contextLine}`, 1, { hint: failure.hint });
|
|
276
391
|
}
|
|
277
392
|
return payload;
|
|
278
393
|
}
|
|
394
|
+
async function listRunsViaServer(context, options = {}) {
|
|
395
|
+
const url = new URL("http://rig.local/api/runs");
|
|
396
|
+
if (options.limit !== undefined)
|
|
397
|
+
url.searchParams.set("limit", String(options.limit));
|
|
398
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
399
|
+
const runs = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.runs) ? payload.runs : [];
|
|
400
|
+
return runs.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
401
|
+
}
|
|
279
402
|
async function getRunDetailsViaServer(context, runId) {
|
|
280
403
|
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}`);
|
|
281
404
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
@@ -289,6 +412,47 @@ async function getRunLogsViaServer(context, runId, options = {}) {
|
|
|
289
412
|
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
290
413
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
291
414
|
}
|
|
415
|
+
async function getRunTimelineViaServer(context, runId, options = {}) {
|
|
416
|
+
const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
|
|
417
|
+
if (options.limit !== undefined)
|
|
418
|
+
url.searchParams.set("limit", String(options.limit));
|
|
419
|
+
if (options.cursor)
|
|
420
|
+
url.searchParams.set("cursor", options.cursor);
|
|
421
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
422
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
423
|
+
}
|
|
424
|
+
var RESUMABLE_RUN_STATUSES = new Set([
|
|
425
|
+
"created",
|
|
426
|
+
"preparing",
|
|
427
|
+
"running",
|
|
428
|
+
"validating",
|
|
429
|
+
"reviewing",
|
|
430
|
+
"stopped",
|
|
431
|
+
"failed",
|
|
432
|
+
"needs-attention",
|
|
433
|
+
"needs_attention"
|
|
434
|
+
]);
|
|
435
|
+
async function resumeRunViaServer(context, runId, options) {
|
|
436
|
+
let targetRunId = runId?.trim() || null;
|
|
437
|
+
if (!targetRunId) {
|
|
438
|
+
const candidates = (await listRunsViaServer(context)).filter((run) => RESUMABLE_RUN_STATUSES.has(String(run.status ?? ""))).sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")));
|
|
439
|
+
targetRunId = typeof candidates[0]?.runId === "string" ? candidates[0].runId : null;
|
|
440
|
+
}
|
|
441
|
+
if (!targetRunId) {
|
|
442
|
+
throw new CliError(options.restart ? "No run is available to restart." : "No resumable run is available.", 2, { hint: "List runs with `rig run list`, then pass an explicit id: `rig run resume <run-id>`." });
|
|
443
|
+
}
|
|
444
|
+
const payload = await requestServerJson(context, "/api/runs/resume", {
|
|
445
|
+
method: "POST",
|
|
446
|
+
headers: { "content-type": "application/json" },
|
|
447
|
+
body: JSON.stringify({ runId: targetRunId, createdAt: new Date().toISOString(), restart: options.restart })
|
|
448
|
+
});
|
|
449
|
+
const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
450
|
+
if (record.ok === false) {
|
|
451
|
+
const message = typeof record.error === "string" && record.error.trim() ? record.error : "run resume failed";
|
|
452
|
+
throw new CliError(`${options.restart ? "restart" : "resume"} failed for ${targetRunId}: ${message}`, 1);
|
|
453
|
+
}
|
|
454
|
+
return { ok: true, runId: targetRunId, ...record };
|
|
455
|
+
}
|
|
292
456
|
async function stopRunViaServer(context, runId) {
|
|
293
457
|
const payload = await requestServerJson(context, "/api/runs/stop", {
|
|
294
458
|
method: "POST",
|
|
@@ -305,10 +469,155 @@ async function steerRunViaServer(context, runId, message) {
|
|
|
305
469
|
});
|
|
306
470
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
307
471
|
}
|
|
472
|
+
async function sendRunPiPromptViaServer(context, runId, text, streamingBehavior) {
|
|
473
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/pi/prompt`, {
|
|
474
|
+
method: "POST",
|
|
475
|
+
headers: { "content-type": "application/json" },
|
|
476
|
+
body: JSON.stringify({ text, streamingBehavior })
|
|
477
|
+
});
|
|
478
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { accepted: true };
|
|
479
|
+
}
|
|
308
480
|
|
|
309
|
-
// packages/cli/src/commands/
|
|
481
|
+
// packages/cli/src/commands/_run-replay.ts
|
|
482
|
+
import { existsSync as existsSync3, readdirSync } from "fs";
|
|
483
|
+
import { join, resolve as resolve3 } from "path";
|
|
484
|
+
import {
|
|
485
|
+
readAuthorityRun,
|
|
486
|
+
readJsonlFile,
|
|
487
|
+
runJournalPath
|
|
488
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
489
|
+
function asRecord(value) {
|
|
490
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
491
|
+
}
|
|
492
|
+
function text(value) {
|
|
493
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
494
|
+
}
|
|
495
|
+
function snippet(value, max = 120) {
|
|
496
|
+
const raw = text(value);
|
|
497
|
+
if (!raw)
|
|
498
|
+
return null;
|
|
499
|
+
const flat = raw.replace(/\s+/g, " ");
|
|
500
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
501
|
+
}
|
|
502
|
+
function summarizeLifecycleEntry(entry) {
|
|
503
|
+
const type = text(entry.type) ?? "event";
|
|
504
|
+
switch (type) {
|
|
505
|
+
case "status": {
|
|
506
|
+
const anchor = text(entry.sessionId);
|
|
507
|
+
return [
|
|
508
|
+
`status \u2192 ${text(entry.status) ?? "(unknown)"}`,
|
|
509
|
+
snippet(entry.detail) ? `\u2014 ${snippet(entry.detail)}` : null,
|
|
510
|
+
anchor ? `[session ${anchor}]` : null
|
|
511
|
+
].filter(Boolean).join(" ");
|
|
512
|
+
}
|
|
513
|
+
case "timeline-entry": {
|
|
514
|
+
const payload = asRecord(entry.payload) ?? {};
|
|
515
|
+
const kind = text(payload.type) ?? "entry";
|
|
516
|
+
const body = snippet(payload.title) ?? snippet(payload.text) ?? snippet(payload.detail);
|
|
517
|
+
const state = text(payload.state);
|
|
518
|
+
return [`timeline ${kind}`, body ? `\u2014 ${body}` : null, state ? `(${state})` : null].filter(Boolean).join(" ");
|
|
519
|
+
}
|
|
520
|
+
case "log-entry": {
|
|
521
|
+
const payload = asRecord(entry.payload) ?? {};
|
|
522
|
+
const title = snippet(payload.title) ?? "log";
|
|
523
|
+
const detail = snippet(payload.detail);
|
|
524
|
+
return [`log ${title}`, detail ? `\u2014 ${detail}` : null].filter(Boolean).join(" ");
|
|
525
|
+
}
|
|
526
|
+
case "record-patch": {
|
|
527
|
+
const patch = asRecord(entry.patch) ?? {};
|
|
528
|
+
const keys = Object.keys(patch);
|
|
529
|
+
const shown = keys.slice(0, 8).join(", ");
|
|
530
|
+
return `record-patch {${shown}${keys.length > 8 ? `, +${keys.length - 8} more` : ""}}`;
|
|
531
|
+
}
|
|
532
|
+
case "timeline":
|
|
533
|
+
return "timeline updated (signal)";
|
|
534
|
+
case "log":
|
|
535
|
+
return `log signal${snippet(entry.title) ? ` \u2014 ${snippet(entry.title)}` : ""}`;
|
|
536
|
+
case "completed":
|
|
537
|
+
return "run completed";
|
|
538
|
+
case "failed":
|
|
539
|
+
return `run failed${snippet(entry.error) ? ` \u2014 ${snippet(entry.error)}` : ""}`;
|
|
540
|
+
default:
|
|
541
|
+
return type;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function summarizeSessionEntry(entry) {
|
|
545
|
+
const type = text(entry.type) ?? "entry";
|
|
546
|
+
const message = asRecord(entry.message);
|
|
547
|
+
const role = text(message?.role);
|
|
548
|
+
const content = message?.content;
|
|
549
|
+
let body = null;
|
|
550
|
+
if (typeof content === "string") {
|
|
551
|
+
body = snippet(content);
|
|
552
|
+
} else if (Array.isArray(content)) {
|
|
553
|
+
body = snippet(content.map((part) => text(asRecord(part)?.text) ?? "").filter(Boolean).join(" "));
|
|
554
|
+
}
|
|
555
|
+
return [
|
|
556
|
+
`pi ${type}`,
|
|
557
|
+
role ? `(${role})` : null,
|
|
558
|
+
body ? `\u2014 ${body}` : null
|
|
559
|
+
].filter(Boolean).join(" ");
|
|
560
|
+
}
|
|
561
|
+
function sessionEntryTimestamp(entry) {
|
|
562
|
+
return text(entry.timestamp) ?? text(entry.at) ?? text(entry.createdAt) ?? null;
|
|
563
|
+
}
|
|
564
|
+
function resolveRunSessionFile(record) {
|
|
565
|
+
const piSession = record?.piSession ?? null;
|
|
566
|
+
if (!piSession)
|
|
567
|
+
return null;
|
|
568
|
+
const recorded = text(piSession.sessionFile);
|
|
569
|
+
if (recorded && existsSync3(recorded)) {
|
|
570
|
+
return recorded;
|
|
571
|
+
}
|
|
572
|
+
const cwd = text(piSession.cwd);
|
|
573
|
+
const sessionId = text(piSession.sessionId);
|
|
574
|
+
if (!cwd || !sessionId)
|
|
575
|
+
return null;
|
|
576
|
+
const sessionDir = resolve3(cwd, ".rig", "session");
|
|
577
|
+
try {
|
|
578
|
+
const match = readdirSync(sessionDir).find((name) => name.endsWith(`_${sessionId}.jsonl`));
|
|
579
|
+
return match ? join(sessionDir, match) : null;
|
|
580
|
+
} catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function buildRunReplay(projectRoot, runId, options = {}) {
|
|
585
|
+
const logPath = runJournalPath(projectRoot, runId);
|
|
586
|
+
const record = readAuthorityRun(projectRoot, runId);
|
|
587
|
+
const lifecycleEntries = readJsonlFile(logPath).map((entry) => asRecord(entry)).filter((entry) => entry !== null);
|
|
588
|
+
const merged = lifecycleEntries.map((entry) => ({
|
|
589
|
+
at: text(entry.at),
|
|
590
|
+
source: "run",
|
|
591
|
+
summary: summarizeLifecycleEntry(entry)
|
|
592
|
+
}));
|
|
593
|
+
let sessionFile = null;
|
|
594
|
+
let sessionEntryCount = 0;
|
|
595
|
+
if (options.withSession) {
|
|
596
|
+
sessionFile = resolveRunSessionFile(record);
|
|
597
|
+
if (sessionFile) {
|
|
598
|
+
let lastSeenAt = null;
|
|
599
|
+
const sessionLines = readJsonlFile(sessionFile).map((entry) => asRecord(entry)).filter((entry) => entry !== null).map((entry) => {
|
|
600
|
+
lastSeenAt = sessionEntryTimestamp(entry) ?? lastSeenAt;
|
|
601
|
+
return { at: lastSeenAt, source: "session", summary: summarizeSessionEntry(entry) };
|
|
602
|
+
});
|
|
603
|
+
sessionEntryCount = sessionLines.length;
|
|
604
|
+
merged.push(...sessionLines);
|
|
605
|
+
merged.sort((left, right) => (left.at ?? "").localeCompare(right.at ?? ""));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const lines = merged.map((line) => `${line.at ?? "(no timestamp) "} ${line.source === "run" ? "run " : "session"} ${line.summary}`);
|
|
609
|
+
return {
|
|
610
|
+
runId,
|
|
611
|
+
logPath,
|
|
612
|
+
sessionFile,
|
|
613
|
+
entryCount: lifecycleEntries.length,
|
|
614
|
+
sessionEntryCount,
|
|
615
|
+
lines
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// packages/cli/src/commands/_operator-surface.ts
|
|
310
620
|
import { createInterface } from "readline";
|
|
311
|
-
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
312
621
|
var CANONICAL_STAGES = [
|
|
313
622
|
"Connect",
|
|
314
623
|
"GitHub/task sync",
|
|
@@ -323,18 +632,585 @@ var CANONICAL_STAGES = [
|
|
|
323
632
|
"Merge",
|
|
324
633
|
"Complete"
|
|
325
634
|
];
|
|
635
|
+
function logDetail(log) {
|
|
636
|
+
return typeof log.detail === "string" ? log.detail.trim() : "";
|
|
637
|
+
}
|
|
638
|
+
function parseProviderProtocolLog(title, detail) {
|
|
639
|
+
if (title.trim().toLowerCase() !== "agent output")
|
|
640
|
+
return null;
|
|
641
|
+
if (!detail.startsWith("{") || !detail.endsWith("}"))
|
|
642
|
+
return null;
|
|
643
|
+
try {
|
|
644
|
+
const record = JSON.parse(detail);
|
|
645
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
646
|
+
return null;
|
|
647
|
+
const type = record.type;
|
|
648
|
+
return typeof type === "string" && [
|
|
649
|
+
"assistant",
|
|
650
|
+
"message_start",
|
|
651
|
+
"message_update",
|
|
652
|
+
"message_end",
|
|
653
|
+
"stream_event",
|
|
654
|
+
"tool_result",
|
|
655
|
+
"tool_execution_start",
|
|
656
|
+
"tool_execution_update",
|
|
657
|
+
"tool_execution_end",
|
|
658
|
+
"turn_start",
|
|
659
|
+
"turn_end"
|
|
660
|
+
].includes(type) ? record : null;
|
|
661
|
+
} catch {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function renderProviderProtocolLog(record) {
|
|
666
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
667
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
|
|
668
|
+
const toolName = String(record.toolName ?? record.name ?? "tool");
|
|
669
|
+
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";
|
|
670
|
+
return `[Pi tool] ${toolName} ${status}`;
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
function entryId(entry, fallback) {
|
|
675
|
+
return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
|
|
676
|
+
}
|
|
326
677
|
function renderOperatorSnapshot(snapshot) {
|
|
327
678
|
const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
|
|
328
679
|
const runId = String(run.runId ?? run.id ?? "run");
|
|
329
680
|
const status = String(run.status ?? "unknown");
|
|
330
681
|
const logs = snapshot.logs ?? [];
|
|
682
|
+
const latestByStage = new Map;
|
|
683
|
+
for (const log of logs) {
|
|
684
|
+
const title = String(log.title ?? "").toLowerCase();
|
|
685
|
+
const stageName = String(log.stage ?? "").toLowerCase();
|
|
686
|
+
const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
|
|
687
|
+
if (stage)
|
|
688
|
+
latestByStage.set(stage, log);
|
|
689
|
+
}
|
|
331
690
|
const stageLines = CANONICAL_STAGES.flatMap((stage) => {
|
|
332
|
-
const match =
|
|
333
|
-
return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
|
|
691
|
+
const match = latestByStage.get(stage);
|
|
692
|
+
return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
|
|
334
693
|
});
|
|
335
694
|
return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
|
|
336
695
|
`);
|
|
337
696
|
}
|
|
697
|
+
function createPiRunStreamRenderer(output = process.stdout) {
|
|
698
|
+
let lastSnapshot = "";
|
|
699
|
+
const assistantTextById = new Map;
|
|
700
|
+
const seenTimeline = new Set;
|
|
701
|
+
const seenLogs = new Set;
|
|
702
|
+
const writeLine = (line) => output.write(`${line}
|
|
703
|
+
`);
|
|
704
|
+
return {
|
|
705
|
+
renderSnapshot(snapshot) {
|
|
706
|
+
const rendered = renderOperatorSnapshot(snapshot);
|
|
707
|
+
if (rendered && rendered !== lastSnapshot) {
|
|
708
|
+
writeLine(rendered);
|
|
709
|
+
lastSnapshot = rendered;
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
renderTimeline(entries) {
|
|
713
|
+
for (const [index, entry] of entries.entries()) {
|
|
714
|
+
const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
|
|
715
|
+
if (entry.type === "assistant_message" && typeof entry.text === "string") {
|
|
716
|
+
const text2 = entry.text;
|
|
717
|
+
const previousText = assistantTextById.get(id) ?? "";
|
|
718
|
+
if (!previousText && text2.trim()) {
|
|
719
|
+
writeLine("[Pi assistant]");
|
|
720
|
+
}
|
|
721
|
+
if (text2.startsWith(previousText)) {
|
|
722
|
+
const delta = text2.slice(previousText.length);
|
|
723
|
+
if (delta)
|
|
724
|
+
output.write(delta);
|
|
725
|
+
} else if (text2.trim() && text2 !== previousText) {
|
|
726
|
+
if (previousText)
|
|
727
|
+
writeLine(`
|
|
728
|
+
[Pi assistant]`);
|
|
729
|
+
output.write(text2);
|
|
730
|
+
}
|
|
731
|
+
assistantTextById.set(id, text2);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (seenTimeline.has(id))
|
|
735
|
+
continue;
|
|
736
|
+
seenTimeline.add(id);
|
|
737
|
+
if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
|
|
738
|
+
writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (entry.type === "timeline_warning") {
|
|
742
|
+
writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (entry.type === "action") {
|
|
746
|
+
const text2 = String(entry.detail ?? entry.message ?? entry.title ?? "").trim();
|
|
747
|
+
if (text2)
|
|
748
|
+
writeLine(`[Rig action] ${text2}`);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (entry.type === "user_message") {
|
|
752
|
+
const text2 = String(entry.text ?? entry.message ?? entry.detail ?? "").trim();
|
|
753
|
+
if (text2)
|
|
754
|
+
writeLine(`[Operator] ${text2}`);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const fallback = String(entry.detail ?? entry.message ?? entry.text ?? entry.title ?? "").trim();
|
|
758
|
+
if (fallback)
|
|
759
|
+
writeLine(`[${String(entry.type ?? "timeline")}] ${fallback}`);
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
renderLogs(entries) {
|
|
763
|
+
for (const [index, entry] of entries.entries()) {
|
|
764
|
+
const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
|
|
765
|
+
if (seenLogs.has(id))
|
|
766
|
+
continue;
|
|
767
|
+
seenLogs.add(id);
|
|
768
|
+
const title = String(entry.title ?? "");
|
|
769
|
+
if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
|
|
770
|
+
continue;
|
|
771
|
+
const detail = logDetail(entry);
|
|
772
|
+
if (!detail)
|
|
773
|
+
continue;
|
|
774
|
+
const protocolRecord = parseProviderProtocolLog(title, detail);
|
|
775
|
+
if (protocolRecord) {
|
|
776
|
+
const protocolLine = renderProviderProtocolLog(protocolRecord);
|
|
777
|
+
if (protocolLine)
|
|
778
|
+
writeLine(protocolLine);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
writeLine(`[${title || "Rig log"}] ${detail}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function createOperatorSurface(options = {}) {
|
|
787
|
+
const input = options.input ?? process.stdin;
|
|
788
|
+
const output = options.output ?? process.stdout;
|
|
789
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
790
|
+
const renderer = createPiRunStreamRenderer(output);
|
|
791
|
+
const writeLine = (line) => output.write(`${line}
|
|
792
|
+
`);
|
|
793
|
+
return {
|
|
794
|
+
mode: "pi-compatible-text",
|
|
795
|
+
...renderer,
|
|
796
|
+
info: writeLine,
|
|
797
|
+
error: (message) => errorOutput.write(`${message}
|
|
798
|
+
`),
|
|
799
|
+
attachCommandInput(handler) {
|
|
800
|
+
if (options.interactive === false || !input.isTTY)
|
|
801
|
+
return null;
|
|
802
|
+
const rl = createInterface({ input, output: process.stdout, terminal: false });
|
|
803
|
+
rl.on("line", (line) => {
|
|
804
|
+
Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
805
|
+
});
|
|
806
|
+
return { close: () => rl.close() };
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// packages/cli/src/commands/_pi-frontend.ts
|
|
812
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
813
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
814
|
+
import { join as join2 } from "path";
|
|
815
|
+
import { main as runPiMain } from "@earendil-works/pi-coding-agent";
|
|
816
|
+
import createPiRigExtension from "@rig/pi-rig";
|
|
817
|
+
|
|
818
|
+
// packages/cli/src/commands/_async-ui.ts
|
|
819
|
+
import pc from "picocolors";
|
|
820
|
+
|
|
821
|
+
// packages/cli/src/commands/_spinner.ts
|
|
822
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
823
|
+
function createTtySpinner(input) {
|
|
824
|
+
const output = input.output ?? process.stdout;
|
|
825
|
+
const isTty = output.isTTY === true;
|
|
826
|
+
const frames = input.frames && input.frames.length > 0 ? input.frames : SPINNER_FRAMES;
|
|
827
|
+
let label = input.label;
|
|
828
|
+
let frame = 0;
|
|
829
|
+
let paused = false;
|
|
830
|
+
let stopped = false;
|
|
831
|
+
let lastPrintedLabel = "";
|
|
832
|
+
const render = () => {
|
|
833
|
+
if (stopped || paused)
|
|
834
|
+
return;
|
|
835
|
+
if (!isTty) {
|
|
836
|
+
if (label !== lastPrintedLabel) {
|
|
837
|
+
output.write(`${label}
|
|
838
|
+
`);
|
|
839
|
+
lastPrintedLabel = label;
|
|
840
|
+
}
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
frame = (frame + 1) % frames.length;
|
|
844
|
+
const glyph = frames[frame] ?? frames[0] ?? "";
|
|
845
|
+
output.write(`\r\x1B[2K${input.styleFrame ? input.styleFrame(glyph) : glyph} ${label}`);
|
|
846
|
+
};
|
|
847
|
+
const clearLine = () => {
|
|
848
|
+
if (isTty)
|
|
849
|
+
output.write("\r\x1B[2K");
|
|
850
|
+
};
|
|
851
|
+
render();
|
|
852
|
+
const timer = isTty ? setInterval(render, input.intervalMs ?? 120) : null;
|
|
853
|
+
return {
|
|
854
|
+
setLabel(next) {
|
|
855
|
+
label = next;
|
|
856
|
+
render();
|
|
857
|
+
},
|
|
858
|
+
pause() {
|
|
859
|
+
paused = true;
|
|
860
|
+
clearLine();
|
|
861
|
+
},
|
|
862
|
+
resume() {
|
|
863
|
+
if (stopped)
|
|
864
|
+
return;
|
|
865
|
+
paused = false;
|
|
866
|
+
render();
|
|
867
|
+
},
|
|
868
|
+
stop(finalLine) {
|
|
869
|
+
if (stopped)
|
|
870
|
+
return;
|
|
871
|
+
stopped = true;
|
|
872
|
+
if (timer)
|
|
873
|
+
clearInterval(timer);
|
|
874
|
+
clearLine();
|
|
875
|
+
if (finalLine)
|
|
876
|
+
output.write(`${finalLine}
|
|
877
|
+
`);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// packages/cli/src/app/theme.ts
|
|
883
|
+
var RIG_PALETTE = {
|
|
884
|
+
ink: "#f2f3f6",
|
|
885
|
+
ink2: "#aeb0ba",
|
|
886
|
+
ink3: "#6c6e79",
|
|
887
|
+
ink4: "#44464f",
|
|
888
|
+
accent: "#ccff4d",
|
|
889
|
+
accentDim: "#a9d63f",
|
|
890
|
+
cyan: "#56d8ff",
|
|
891
|
+
red: "#ff5d5d",
|
|
892
|
+
yellow: "#ffd24d"
|
|
893
|
+
};
|
|
894
|
+
function hexToRgb(hex) {
|
|
895
|
+
const value = hex.replace("#", "");
|
|
896
|
+
return [
|
|
897
|
+
Number.parseInt(value.slice(0, 2), 16),
|
|
898
|
+
Number.parseInt(value.slice(2, 4), 16),
|
|
899
|
+
Number.parseInt(value.slice(4, 6), 16)
|
|
900
|
+
];
|
|
901
|
+
}
|
|
902
|
+
function fg(hex) {
|
|
903
|
+
const [r, g, b] = hexToRgb(hex);
|
|
904
|
+
return (text2) => `\x1B[38;2;${r};${g};${b}m${text2}\x1B[39m`;
|
|
905
|
+
}
|
|
906
|
+
var ink = fg(RIG_PALETTE.ink);
|
|
907
|
+
var ink2 = fg(RIG_PALETTE.ink2);
|
|
908
|
+
var ink3 = fg(RIG_PALETTE.ink3);
|
|
909
|
+
var ink4 = fg(RIG_PALETTE.ink4);
|
|
910
|
+
var accent = fg(RIG_PALETTE.accent);
|
|
911
|
+
var accentDim = fg(RIG_PALETTE.accentDim);
|
|
912
|
+
var cyan = fg(RIG_PALETTE.cyan);
|
|
913
|
+
var red = fg(RIG_PALETTE.red);
|
|
914
|
+
var yellow = fg(RIG_PALETTE.yellow);
|
|
915
|
+
function bold(text2) {
|
|
916
|
+
return `\x1B[1m${text2}\x1B[22m`;
|
|
917
|
+
}
|
|
918
|
+
var DRONE_ART = [
|
|
919
|
+
" .-=-. .-=-. ",
|
|
920
|
+
" ( !!! ) ( !!! ) ",
|
|
921
|
+
" '-=-'._ _.'-=-' ",
|
|
922
|
+
" '._ _.' ",
|
|
923
|
+
" '=$$$$$$$=.' ",
|
|
924
|
+
" =$$$$$$$$$$$= ",
|
|
925
|
+
" $$$@@@@@@@@@@$$$ ",
|
|
926
|
+
" $$$@@ @@$$$ ",
|
|
927
|
+
" $$@ ? @$$$ ",
|
|
928
|
+
" $$$@ '-' @$$$ ",
|
|
929
|
+
" $$$@@ @@$$$ ",
|
|
930
|
+
" $$$@@@@@@@@@@$$$ ",
|
|
931
|
+
" =$$$$$$$$$$$= ",
|
|
932
|
+
" '=$$$$$$$=.' ",
|
|
933
|
+
" _.' '._ ",
|
|
934
|
+
" .-=-.' '.-=-. ",
|
|
935
|
+
" ( !!! ) ( !!! ) ",
|
|
936
|
+
" '-=-' '-=-' "
|
|
937
|
+
];
|
|
938
|
+
var EYE_FRAMES = ["@", "o", "."];
|
|
939
|
+
var DRONE_WIDTH = DRONE_ART[0].length;
|
|
940
|
+
var DRONE_HEIGHT = DRONE_ART.length;
|
|
941
|
+
var MICRO_BLADES = ["---", "\\\\\\", "|||", "///"];
|
|
942
|
+
function microDroneFrame(tick) {
|
|
943
|
+
const blade = MICRO_BLADES[tick % MICRO_BLADES.length];
|
|
944
|
+
const eye = EYE_FRAMES[Math.floor(tick / 2) % EYE_FRAMES.length];
|
|
945
|
+
return `(${blade})${eye}(${blade})`;
|
|
946
|
+
}
|
|
947
|
+
var MICRO_DRONE_FRAMES = Array.from({ length: 12 }, (_, index) => microDroneFrame(index));
|
|
948
|
+
function renderMicroDroneFrame(tick) {
|
|
949
|
+
const blade = MICRO_BLADES[tick % MICRO_BLADES.length];
|
|
950
|
+
const eye = EYE_FRAMES[Math.floor(tick / 2) % EYE_FRAMES.length];
|
|
951
|
+
return `${ink4("(")}${cyan(blade)}${ink4(")")}${bold(accent(eye))}${ink4("(")}${cyan(blade)}${ink4(")")}`;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// packages/cli/src/commands/_async-ui.ts
|
|
955
|
+
var DONE_SYMBOL = pc.green("\u25C7");
|
|
956
|
+
var FAIL_SYMBOL = pc.red("\u25A0");
|
|
957
|
+
var activeUpdate = null;
|
|
958
|
+
async function withSpinner(label, work, options = {}) {
|
|
959
|
+
if (options.outputMode === "json") {
|
|
960
|
+
return work(() => {});
|
|
961
|
+
}
|
|
962
|
+
if (activeUpdate) {
|
|
963
|
+
const outer = activeUpdate;
|
|
964
|
+
outer(label);
|
|
965
|
+
return work(outer);
|
|
966
|
+
}
|
|
967
|
+
const output = options.output ?? process.stderr;
|
|
968
|
+
const isTty = output.isTTY === true;
|
|
969
|
+
let lastLabel = label;
|
|
970
|
+
if (!isTty) {
|
|
971
|
+
output.write(`${label}
|
|
972
|
+
`);
|
|
973
|
+
const update2 = (next) => {
|
|
974
|
+
lastLabel = next;
|
|
975
|
+
};
|
|
976
|
+
activeUpdate = update2;
|
|
977
|
+
const previousListener2 = setServerPhaseListener(update2);
|
|
978
|
+
try {
|
|
979
|
+
return await work(update2);
|
|
980
|
+
} finally {
|
|
981
|
+
activeUpdate = null;
|
|
982
|
+
setServerPhaseListener(previousListener2);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const spinner = createTtySpinner({
|
|
986
|
+
label,
|
|
987
|
+
output,
|
|
988
|
+
frames: MICRO_DRONE_FRAMES,
|
|
989
|
+
styleFrame: (frame) => renderMicroDroneFrame(Math.max(0, MICRO_DRONE_FRAMES.indexOf(frame)))
|
|
990
|
+
});
|
|
991
|
+
const update = (next) => {
|
|
992
|
+
lastLabel = next;
|
|
993
|
+
spinner.setLabel(next);
|
|
994
|
+
};
|
|
995
|
+
activeUpdate = update;
|
|
996
|
+
const previousListener = setServerPhaseListener(update);
|
|
997
|
+
try {
|
|
998
|
+
const result = await work(update);
|
|
999
|
+
spinner.stop(options.doneLabel ? `${DONE_SYMBOL} ${options.doneLabel}` : undefined);
|
|
1000
|
+
return result;
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
spinner.stop(`${FAIL_SYMBOL} ${lastLabel}`);
|
|
1003
|
+
throw error;
|
|
1004
|
+
} finally {
|
|
1005
|
+
activeUpdate = null;
|
|
1006
|
+
setServerPhaseListener(previousListener);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// packages/cli/src/commands/_pi-frontend.ts
|
|
1011
|
+
function setTemporaryEnv(updates) {
|
|
1012
|
+
const previous = new Map;
|
|
1013
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
1014
|
+
previous.set(key, process.env[key]);
|
|
1015
|
+
process.env[key] = value;
|
|
1016
|
+
}
|
|
1017
|
+
return () => {
|
|
1018
|
+
for (const [key, value] of previous) {
|
|
1019
|
+
if (value === undefined)
|
|
1020
|
+
delete process.env[key];
|
|
1021
|
+
else
|
|
1022
|
+
process.env[key] = value;
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
function buildOperatorPiEnv(input) {
|
|
1027
|
+
return {
|
|
1028
|
+
PI_CODING_AGENT_SESSION_DIR: input.sessionDir,
|
|
1029
|
+
PI_SKIP_VERSION_CHECK: "1",
|
|
1030
|
+
PI_HIDDEN_COMMANDS: "import,fork,clone,tree,new,resume,trust",
|
|
1031
|
+
RIG_PI_OPERATOR_SESSION: "1",
|
|
1032
|
+
RIG_RUN_ID: input.runId,
|
|
1033
|
+
RIG_SERVER_URL: input.serverUrl,
|
|
1034
|
+
...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {},
|
|
1035
|
+
...input.serverProjectRoot ? { RIG_PROJECT_ROOT: input.serverProjectRoot } : {}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
async function prepareOperatorConsole(context, runId, tempSessionDir) {
|
|
1039
|
+
const server = await ensureServerForCli(context.projectRoot);
|
|
1040
|
+
const localCwd = join2(homedir2(), ".rig", "drones", runId.slice(0, 8));
|
|
1041
|
+
mkdirSync2(localCwd, { recursive: true });
|
|
1042
|
+
trustDroneCwd(localCwd);
|
|
1043
|
+
installRigPiTheme();
|
|
1044
|
+
let sessionFileArg = [];
|
|
1045
|
+
try {
|
|
1046
|
+
const payload = await requestServerJson(context, `/api/runs/${encodeURIComponent(runId)}/session-file`);
|
|
1047
|
+
if (payload?.ok && typeof payload.content === "string" && payload.content.trim()) {
|
|
1048
|
+
const fileName = typeof payload.fileName === "string" && payload.fileName.trim() ? payload.fileName.replace(/[^A-Za-z0-9._-]/g, "_") : `rig-run-${runId}.jsonl`;
|
|
1049
|
+
const localSessionPath = join2(tempSessionDir, fileName);
|
|
1050
|
+
const content = payload.content.split(`
|
|
1051
|
+
`).map((line, index) => {
|
|
1052
|
+
if (index > 0 || !line.trim())
|
|
1053
|
+
return line;
|
|
1054
|
+
try {
|
|
1055
|
+
const header = JSON.parse(line);
|
|
1056
|
+
if (header.type === "session" && typeof header.cwd === "string") {
|
|
1057
|
+
return JSON.stringify({ ...header, cwd: localCwd });
|
|
1058
|
+
}
|
|
1059
|
+
} catch {}
|
|
1060
|
+
return line;
|
|
1061
|
+
}).join(`
|
|
1062
|
+
`);
|
|
1063
|
+
writeFileSync2(localSessionPath, content);
|
|
1064
|
+
sessionFileArg = ["--session", localSessionPath];
|
|
1065
|
+
}
|
|
1066
|
+
} catch {}
|
|
1067
|
+
return { server, sessionFileArg };
|
|
1068
|
+
}
|
|
1069
|
+
var RIG_PI_THEME = {
|
|
1070
|
+
$schema: "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
|
1071
|
+
name: "rig",
|
|
1072
|
+
vars: {
|
|
1073
|
+
acid: "#ccff4d",
|
|
1074
|
+
acidDim: "#a9d63f",
|
|
1075
|
+
cyan: "#56d8ff",
|
|
1076
|
+
red: "#ff5d5d",
|
|
1077
|
+
yellow: "#ffd24d",
|
|
1078
|
+
ink: "#f2f3f6",
|
|
1079
|
+
ink2: "#aeb0ba",
|
|
1080
|
+
ink3: "#6c6e79",
|
|
1081
|
+
ink4: "#44464f",
|
|
1082
|
+
panel: "#101115",
|
|
1083
|
+
panelUser: "#14161b",
|
|
1084
|
+
toolPending: "#0e1013",
|
|
1085
|
+
toolSuccess: "#10150c",
|
|
1086
|
+
toolError: "#1a0f0f",
|
|
1087
|
+
customMsg: "#0f1410"
|
|
1088
|
+
},
|
|
1089
|
+
colors: {
|
|
1090
|
+
accent: "acid",
|
|
1091
|
+
border: "ink4",
|
|
1092
|
+
borderAccent: "acid",
|
|
1093
|
+
borderMuted: "ink4",
|
|
1094
|
+
success: "acid",
|
|
1095
|
+
error: "red",
|
|
1096
|
+
warning: "yellow",
|
|
1097
|
+
muted: "ink3",
|
|
1098
|
+
dim: "ink4",
|
|
1099
|
+
text: "ink",
|
|
1100
|
+
thinkingText: "ink3",
|
|
1101
|
+
selectedBg: "panel",
|
|
1102
|
+
userMessageBg: "panelUser",
|
|
1103
|
+
userMessageText: "ink",
|
|
1104
|
+
customMessageBg: "customMsg",
|
|
1105
|
+
customMessageText: "ink2",
|
|
1106
|
+
customMessageLabel: "acidDim",
|
|
1107
|
+
toolPendingBg: "toolPending",
|
|
1108
|
+
toolSuccessBg: "toolSuccess",
|
|
1109
|
+
toolErrorBg: "toolError",
|
|
1110
|
+
toolTitle: "ink",
|
|
1111
|
+
toolOutput: "ink3",
|
|
1112
|
+
mdHeading: "acid",
|
|
1113
|
+
mdLink: "cyan",
|
|
1114
|
+
mdLinkUrl: "ink4",
|
|
1115
|
+
mdCode: "acidDim",
|
|
1116
|
+
mdCodeBlock: "ink2",
|
|
1117
|
+
mdCodeBlockBorder: "ink4",
|
|
1118
|
+
mdQuote: "ink3",
|
|
1119
|
+
mdQuoteBorder: "ink4",
|
|
1120
|
+
mdHr: "ink4",
|
|
1121
|
+
mdListBullet: "acid",
|
|
1122
|
+
toolDiffAdded: "acid",
|
|
1123
|
+
toolDiffRemoved: "red",
|
|
1124
|
+
toolDiffContext: "ink3",
|
|
1125
|
+
syntaxComment: "ink3",
|
|
1126
|
+
syntaxKeyword: "cyan",
|
|
1127
|
+
syntaxFunction: "acid",
|
|
1128
|
+
syntaxVariable: "ink",
|
|
1129
|
+
syntaxString: "acidDim",
|
|
1130
|
+
syntaxNumber: "yellow",
|
|
1131
|
+
syntaxType: "cyan",
|
|
1132
|
+
syntaxOperator: "ink2",
|
|
1133
|
+
syntaxPunctuation: "ink3",
|
|
1134
|
+
thinkingOff: "ink4",
|
|
1135
|
+
thinkingMinimal: "ink3",
|
|
1136
|
+
thinkingLow: "ink2",
|
|
1137
|
+
thinkingMedium: "cyan",
|
|
1138
|
+
thinkingHigh: "acidDim",
|
|
1139
|
+
thinkingXhigh: "acid",
|
|
1140
|
+
bashMode: "cyan"
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
function trustDroneCwd(localCwd) {
|
|
1144
|
+
try {
|
|
1145
|
+
const agentDir = join2(homedir2(), ".pi", "agent");
|
|
1146
|
+
mkdirSync2(agentDir, { recursive: true });
|
|
1147
|
+
const trustPath = join2(agentDir, "trust.json");
|
|
1148
|
+
const store = existsSync4(trustPath) ? JSON.parse(readFileSync3(trustPath, "utf8")) : {};
|
|
1149
|
+
if (store[localCwd] !== true) {
|
|
1150
|
+
store[localCwd] = true;
|
|
1151
|
+
writeFileSync2(trustPath, `${JSON.stringify(store, null, "\t")}
|
|
1152
|
+
`);
|
|
1153
|
+
}
|
|
1154
|
+
} catch {}
|
|
1155
|
+
}
|
|
1156
|
+
function installRigPiTheme() {
|
|
1157
|
+
try {
|
|
1158
|
+
const themesDir = join2(homedir2(), ".pi", "agent", "themes");
|
|
1159
|
+
mkdirSync2(themesDir, { recursive: true });
|
|
1160
|
+
const themePath = join2(themesDir, "rig.json");
|
|
1161
|
+
const next = `${JSON.stringify(RIG_PI_THEME, null, "\t")}
|
|
1162
|
+
`;
|
|
1163
|
+
if (!existsSync4(themePath) || readFileSync3(themePath, "utf8") !== next) {
|
|
1164
|
+
writeFileSync2(themePath, next);
|
|
1165
|
+
}
|
|
1166
|
+
} catch {}
|
|
1167
|
+
}
|
|
1168
|
+
async function attachRunBundledPiFrontend(context, input) {
|
|
1169
|
+
const tempSessionDir = mkdtempSync(join2(tmpdir(), "rig-pi-frontend-sessions-"));
|
|
1170
|
+
const { server, sessionFileArg } = await withSpinner(`Opening Pi console for run ${input.runId}\u2026`, () => prepareOperatorConsole(context, input.runId, tempSessionDir), { outputMode: context.outputMode });
|
|
1171
|
+
const restoreEnv = setTemporaryEnv(buildOperatorPiEnv({
|
|
1172
|
+
runId: input.runId,
|
|
1173
|
+
serverUrl: server.baseUrl,
|
|
1174
|
+
authToken: server.authToken,
|
|
1175
|
+
serverProjectRoot: server.serverProjectRoot,
|
|
1176
|
+
sessionDir: tempSessionDir
|
|
1177
|
+
}));
|
|
1178
|
+
const piRigExtensionFactory = (pi) => {
|
|
1179
|
+
createPiRigExtension(pi);
|
|
1180
|
+
};
|
|
1181
|
+
let detached = false;
|
|
1182
|
+
try {
|
|
1183
|
+
await runPiMain([
|
|
1184
|
+
"--no-extensions",
|
|
1185
|
+
"--no-skills",
|
|
1186
|
+
"--no-prompt-templates",
|
|
1187
|
+
"--no-context-files",
|
|
1188
|
+
...sessionFileArg
|
|
1189
|
+
], {
|
|
1190
|
+
extensionFactories: [piRigExtensionFactory]
|
|
1191
|
+
});
|
|
1192
|
+
detached = true;
|
|
1193
|
+
} finally {
|
|
1194
|
+
restoreEnv();
|
|
1195
|
+
rmSync(tempSessionDir, { recursive: true, force: true });
|
|
1196
|
+
}
|
|
1197
|
+
let run = { runId: input.runId, status: "unknown" };
|
|
1198
|
+
try {
|
|
1199
|
+
run = await getRunDetailsViaServer(context, input.runId);
|
|
1200
|
+
} catch {}
|
|
1201
|
+
return {
|
|
1202
|
+
run,
|
|
1203
|
+
logs: [],
|
|
1204
|
+
timeline: [],
|
|
1205
|
+
timelineCursor: null,
|
|
1206
|
+
steered: input.steered === true,
|
|
1207
|
+
detached,
|
|
1208
|
+
rendered: "stock Pi operator console with the pi-rig extension"
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// packages/cli/src/commands/_operator-view.ts
|
|
1213
|
+
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
338
1214
|
function runStatusFromPayload(payload) {
|
|
339
1215
|
const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
340
1216
|
return String(run.status ?? "unknown").toLowerCase();
|
|
@@ -356,56 +1232,316 @@ async function applyOperatorCommand(context, input, deps = {}) {
|
|
|
356
1232
|
await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
|
|
357
1233
|
return { action: "continue", message: "Steering message queued." };
|
|
358
1234
|
}
|
|
359
|
-
async function readOperatorSnapshot(context, runId) {
|
|
1235
|
+
async function readOperatorSnapshot(context, runId, options = {}) {
|
|
360
1236
|
const run = await getRunDetailsViaServer(context, runId);
|
|
361
1237
|
const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
|
|
362
|
-
const
|
|
363
|
-
|
|
1238
|
+
const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
|
|
1239
|
+
entries: [{
|
|
1240
|
+
id: `timeline-unavailable:${runId}`,
|
|
1241
|
+
type: "timeline_warning",
|
|
1242
|
+
detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
|
|
1243
|
+
createdAt: new Date().toISOString()
|
|
1244
|
+
}],
|
|
1245
|
+
nextCursor: options.timelineCursor ?? null
|
|
1246
|
+
}));
|
|
1247
|
+
const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
|
|
1248
|
+
const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1249
|
+
const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
|
|
1250
|
+
return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
|
|
364
1251
|
}
|
|
365
1252
|
async function attachRunOperatorView(context, input) {
|
|
366
1253
|
let steered = false;
|
|
367
|
-
|
|
368
|
-
|
|
1254
|
+
const attachMessage = input.message?.trim();
|
|
1255
|
+
if (attachMessage) {
|
|
1256
|
+
await withSpinner("Queueing steering message\u2026", () => sendRunPiPromptViaServer(context, input.runId, attachMessage, "steer").catch(() => steerRunViaServer(context, input.runId, attachMessage)), { outputMode: context.outputMode });
|
|
369
1257
|
steered = true;
|
|
370
1258
|
}
|
|
371
|
-
|
|
1259
|
+
if (!input.once && input.interactive !== false && context.outputMode === "text" && Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
1260
|
+
return attachRunBundledPiFrontend(context, {
|
|
1261
|
+
runId: input.runId,
|
|
1262
|
+
steered
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
const surface = createOperatorSurface({ interactive: input.interactive !== false });
|
|
1266
|
+
let snapshot = await withSpinner(`Connecting to run ${input.runId}\u2026`, () => readOperatorSnapshot(context, input.runId), { outputMode: context.outputMode });
|
|
372
1267
|
if (context.outputMode === "text") {
|
|
373
|
-
|
|
1268
|
+
surface.renderSnapshot(snapshot);
|
|
1269
|
+
surface.renderTimeline(snapshot.timeline);
|
|
1270
|
+
surface.renderLogs(snapshot.logs);
|
|
374
1271
|
if (steered)
|
|
375
|
-
|
|
1272
|
+
surface.info("Message submitted to worker Pi.");
|
|
376
1273
|
}
|
|
377
1274
|
let detached = false;
|
|
378
|
-
let
|
|
1275
|
+
let commandInput = null;
|
|
379
1276
|
if (input.follow && !input.once && context.outputMode === "text") {
|
|
380
1277
|
if (input.interactive !== false && process.stdin.isTTY) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
}).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
1278
|
+
surface.info("Controls: /user <message>, /stop, /detach");
|
|
1279
|
+
commandInput = surface.attachCommandInput(async (line) => {
|
|
1280
|
+
const result = await applyOperatorCommand(context, { runId: input.runId, line });
|
|
1281
|
+
if (result.message)
|
|
1282
|
+
surface.info(result.message);
|
|
1283
|
+
if (result.action === "detach" || result.action === "stopped") {
|
|
1284
|
+
detached = true;
|
|
1285
|
+
commandInput?.close();
|
|
1286
|
+
}
|
|
392
1287
|
});
|
|
393
1288
|
}
|
|
394
|
-
let lastRendered = snapshot.rendered;
|
|
395
1289
|
const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
|
|
1290
|
+
let timelineCursor = snapshot.timelineCursor;
|
|
396
1291
|
while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
|
|
397
1292
|
await Bun.sleep(pollMs);
|
|
398
|
-
snapshot = await readOperatorSnapshot(context, input.runId);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
1293
|
+
snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
|
|
1294
|
+
timelineCursor = snapshot.timelineCursor;
|
|
1295
|
+
surface.renderSnapshot(snapshot);
|
|
1296
|
+
surface.renderTimeline(snapshot.timeline);
|
|
1297
|
+
surface.renderLogs(snapshot.logs);
|
|
403
1298
|
}
|
|
404
|
-
|
|
1299
|
+
commandInput?.close();
|
|
405
1300
|
}
|
|
406
1301
|
return { ...snapshot, steered, detached };
|
|
407
1302
|
}
|
|
408
1303
|
|
|
1304
|
+
// packages/cli/src/app/drone-ui.ts
|
|
1305
|
+
import { ProcessTerminal, TUI, Text, Input, SelectList, matchesKey } from "@earendil-works/pi-tui";
|
|
1306
|
+
function droneNote(message, title) {
|
|
1307
|
+
if (title)
|
|
1308
|
+
console.log(` ${accentDim("\u25C7")} ${bold(ink2(title))}`);
|
|
1309
|
+
for (const line of message.split(`
|
|
1310
|
+
`)) {
|
|
1311
|
+
console.log(` ${ink4("\u2502")} ${line}`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// packages/cli/src/commands/_cli-format.ts
|
|
1316
|
+
import pc2 from "picocolors";
|
|
1317
|
+
var themeDim = (value) => ink3(value);
|
|
1318
|
+
var themeFaint = (value) => ink4(value);
|
|
1319
|
+
function stringField(record, key, fallback = "") {
|
|
1320
|
+
const value = record[key];
|
|
1321
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
1322
|
+
}
|
|
1323
|
+
function rawObject(record) {
|
|
1324
|
+
const raw = record.raw;
|
|
1325
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
1326
|
+
}
|
|
1327
|
+
function truncate(value, width) {
|
|
1328
|
+
if (value.length <= width)
|
|
1329
|
+
return value;
|
|
1330
|
+
if (width <= 1)
|
|
1331
|
+
return "\u2026";
|
|
1332
|
+
return `${value.slice(0, width - 1)}\u2026`;
|
|
1333
|
+
}
|
|
1334
|
+
function pad(value, width) {
|
|
1335
|
+
return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
|
|
1336
|
+
}
|
|
1337
|
+
function statusColor(status) {
|
|
1338
|
+
const normalized = status.toLowerCase();
|
|
1339
|
+
if (["completed", "merged", "closed", "done", "accepted", "pass", "selected", "approved", "running"].includes(normalized))
|
|
1340
|
+
return accent;
|
|
1341
|
+
if (["failed", "needs_attention", "needs-attention", "blocked", "error", "rejected"].includes(normalized))
|
|
1342
|
+
return red;
|
|
1343
|
+
if (["reviewing", "validating", "in_progress", "in-progress", "remote", "preparing", "closing-out"].includes(normalized))
|
|
1344
|
+
return cyan;
|
|
1345
|
+
if (["ready", "open", "queued", "created", "local", "pending"].includes(normalized))
|
|
1346
|
+
return yellow;
|
|
1347
|
+
return themeDim;
|
|
1348
|
+
}
|
|
1349
|
+
function compactDate(value) {
|
|
1350
|
+
if (!value.trim())
|
|
1351
|
+
return "";
|
|
1352
|
+
const parsed = Date.parse(value);
|
|
1353
|
+
if (!Number.isFinite(parsed))
|
|
1354
|
+
return value;
|
|
1355
|
+
return new Date(parsed).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
|
|
1356
|
+
}
|
|
1357
|
+
function firstString(record, keys, fallback = "") {
|
|
1358
|
+
for (const key of keys) {
|
|
1359
|
+
const value = stringField(record, key);
|
|
1360
|
+
if (value)
|
|
1361
|
+
return value;
|
|
1362
|
+
}
|
|
1363
|
+
return fallback;
|
|
1364
|
+
}
|
|
1365
|
+
function runIdOf(run) {
|
|
1366
|
+
return firstString(run, ["runId", "id"], "(unknown-run)");
|
|
1367
|
+
}
|
|
1368
|
+
function taskIdOf(run) {
|
|
1369
|
+
return firstString(run, ["taskId", "task", "task_id"]);
|
|
1370
|
+
}
|
|
1371
|
+
function runTitleOf(run) {
|
|
1372
|
+
return firstString(run, ["title", "summary", "name"], taskIdOf(run) || "(untitled)");
|
|
1373
|
+
}
|
|
1374
|
+
function shouldUseClackOutput() {
|
|
1375
|
+
return Boolean(process.stdout.isTTY) && process.env.RIG_CLI_PLAIN_HELP !== "1";
|
|
1376
|
+
}
|
|
1377
|
+
function printFormattedOutput(message, options = {}) {
|
|
1378
|
+
if (!shouldUseClackOutput()) {
|
|
1379
|
+
console.log(message);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
droneNote(message, options.title);
|
|
1383
|
+
}
|
|
1384
|
+
function formatStatusPill(status) {
|
|
1385
|
+
const label = status || "unknown";
|
|
1386
|
+
return statusColor(label)(`\u25CF ${label}`);
|
|
1387
|
+
}
|
|
1388
|
+
function formatSection(title, subtitle) {
|
|
1389
|
+
return `${pc2.bold(accent("\u25C6"))} ${pc2.bold(title)}${subtitle ? themeDim(` \u2014 ${subtitle}`) : ""}`;
|
|
1390
|
+
}
|
|
1391
|
+
function formatSuccessCard(title, rows = []) {
|
|
1392
|
+
const body = rows.filter(([, value]) => value !== undefined && value !== null && String(value).length > 0).map(([key, value]) => `${themeFaint("\u2502")} ${themeDim(key.padEnd(12))} ${value}`);
|
|
1393
|
+
return [formatSection(title), ...body].join(`
|
|
1394
|
+
`);
|
|
1395
|
+
}
|
|
1396
|
+
function formatNextSteps(steps) {
|
|
1397
|
+
if (steps.length === 0)
|
|
1398
|
+
return [];
|
|
1399
|
+
return [pc2.bold("Next"), ...steps.map((step) => `${accent("\u203A")} ${step}`)];
|
|
1400
|
+
}
|
|
1401
|
+
function formatRunList(runs, options = {}) {
|
|
1402
|
+
if (runs.length === 0) {
|
|
1403
|
+
return [
|
|
1404
|
+
formatSection("Runs", "none recorded"),
|
|
1405
|
+
options.source === "server" ? themeDim("No runs recorded on the selected Rig server.") : themeDim("No runs recorded in .rig/runs."),
|
|
1406
|
+
"",
|
|
1407
|
+
...formatNextSteps(["Start one: `rig task run --next`", "Check server: `rig server status`"])
|
|
1408
|
+
].join(`
|
|
1409
|
+
`);
|
|
1410
|
+
}
|
|
1411
|
+
const rows = runs.map((run) => {
|
|
1412
|
+
const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
|
|
1413
|
+
const status = stringField(run, "status", "unknown");
|
|
1414
|
+
const taskId = stringField(run, "taskId", "");
|
|
1415
|
+
const title = stringField(run, "title", taskId || "(untitled)");
|
|
1416
|
+
const runtime = stringField(run, "runtimeAdapter", "");
|
|
1417
|
+
return { runId, status, title, runtime };
|
|
1418
|
+
});
|
|
1419
|
+
const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
|
|
1420
|
+
const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
|
|
1421
|
+
const header = `${pc2.bold(pad("RUN", idWidth))} ${pc2.bold(pad("STATUS", statusWidth))} ${pc2.bold("TITLE")}`;
|
|
1422
|
+
const body = rows.map((row) => [
|
|
1423
|
+
pc2.bold(pad(truncate(row.runId, idWidth), idWidth)),
|
|
1424
|
+
statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
|
|
1425
|
+
`${row.title}${row.runtime ? themeDim(` ${row.runtime}`) : ""}`
|
|
1426
|
+
].join(" "));
|
|
1427
|
+
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(`
|
|
1428
|
+
`);
|
|
1429
|
+
}
|
|
1430
|
+
function formatRunCard(run, options = {}) {
|
|
1431
|
+
const raw = rawObject(run);
|
|
1432
|
+
const merged = { ...raw, ...run };
|
|
1433
|
+
const runId = runIdOf(merged);
|
|
1434
|
+
const status = firstString(merged, ["status"], "unknown");
|
|
1435
|
+
const taskId = taskIdOf(merged);
|
|
1436
|
+
const title = runTitleOf(merged);
|
|
1437
|
+
const runtime = firstString(merged, ["runtimeAdapter", "runtime", "adapter"]);
|
|
1438
|
+
const mode = firstString(merged, ["runtimeMode", "mode"]);
|
|
1439
|
+
const interaction = firstString(merged, ["interactionMode"]);
|
|
1440
|
+
const created = compactDate(firstString(merged, ["createdAt"]));
|
|
1441
|
+
const started = compactDate(firstString(merged, ["startedAt"]));
|
|
1442
|
+
const updated = compactDate(firstString(merged, ["updatedAt"]));
|
|
1443
|
+
const completed = compactDate(firstString(merged, ["completedAt", "finishedAt"]));
|
|
1444
|
+
const worktree = firstString(merged, ["worktreePath", "cwd", "projectRoot"]);
|
|
1445
|
+
const piSession = merged.piSession && typeof merged.piSession === "object" && !Array.isArray(merged.piSession) ? firstString(merged.piSession, ["sessionId", "id"]) : "";
|
|
1446
|
+
const timeline = Array.isArray(merged.timeline) ? merged.timeline.length : null;
|
|
1447
|
+
const approvals = Array.isArray(merged.approvals) ? merged.approvals.length : null;
|
|
1448
|
+
const inputs = Array.isArray(merged.userInputs) ? merged.userInputs.length : null;
|
|
1449
|
+
const rows = [
|
|
1450
|
+
["run", pc2.bold(runId)],
|
|
1451
|
+
["status", formatStatusPill(status)],
|
|
1452
|
+
["task", taskId],
|
|
1453
|
+
["title", title],
|
|
1454
|
+
["runtime", [runtime, mode, interaction].filter(Boolean).join(" \xB7 ")],
|
|
1455
|
+
["created", created],
|
|
1456
|
+
["started", started],
|
|
1457
|
+
["updated", updated],
|
|
1458
|
+
["completed", completed],
|
|
1459
|
+
["worktree", worktree],
|
|
1460
|
+
["pi", piSession],
|
|
1461
|
+
["timeline", timeline],
|
|
1462
|
+
["approvals", approvals],
|
|
1463
|
+
["inputs", inputs]
|
|
1464
|
+
];
|
|
1465
|
+
return [
|
|
1466
|
+
formatSuccessCard(options.title ?? "Run details", rows),
|
|
1467
|
+
"",
|
|
1468
|
+
...formatNextSteps([`Follow live: \`rig run attach ${runId} --follow\``, `Raw payload: \`rig run show ${runId} --raw\``])
|
|
1469
|
+
].join(`
|
|
1470
|
+
`);
|
|
1471
|
+
}
|
|
1472
|
+
function formatRunStatus(summary, options = {}) {
|
|
1473
|
+
const activeRuns = summary.activeRuns ?? [];
|
|
1474
|
+
const recentRuns = summary.recentRuns ?? [];
|
|
1475
|
+
const lines = [formatSection("Run status", options.source === "server" ? "selected server" : "local state")];
|
|
1476
|
+
lines.push("", pc2.bold(`Active runs (${activeRuns.length})`));
|
|
1477
|
+
if (activeRuns.length === 0) {
|
|
1478
|
+
lines.push(themeDim("No active runs."));
|
|
1479
|
+
} else {
|
|
1480
|
+
for (const run of activeRuns) {
|
|
1481
|
+
lines.push(formatRunSummaryLine(run));
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
lines.push("", pc2.bold(`Recent runs (${recentRuns.length})`));
|
|
1485
|
+
if (recentRuns.length === 0) {
|
|
1486
|
+
lines.push(themeDim("No recent terminal runs."));
|
|
1487
|
+
} else {
|
|
1488
|
+
for (const run of recentRuns.slice(0, 10)) {
|
|
1489
|
+
lines.push(formatRunSummaryLine(run));
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
lines.push("", ...formatNextSteps(["Start work: `rig task run --next`", "Attach: `rig run attach <run-id> --follow`", "Details: `rig run show <run-id>`"]));
|
|
1493
|
+
return lines.join(`
|
|
1494
|
+
`);
|
|
1495
|
+
}
|
|
1496
|
+
function formatRunSummaryLine(run) {
|
|
1497
|
+
const record = run;
|
|
1498
|
+
const runId = runIdOf(record);
|
|
1499
|
+
const status = firstString(record, ["status"], "unknown");
|
|
1500
|
+
const taskId = taskIdOf(record);
|
|
1501
|
+
const title = runTitleOf(record);
|
|
1502
|
+
const runtime = firstString(record, ["runtimeAdapter", "runtime", "adapter"]);
|
|
1503
|
+
const descriptor = [taskId, title].filter(Boolean).join(" \xB7 ");
|
|
1504
|
+
return `${themeFaint("\u2502")} ${pc2.bold(runId)} ${formatStatusPill(status)} ${descriptor}${runtime ? themeDim(` ${runtime}`) : ""}`;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// packages/cli/src/commands/inbox.ts
|
|
1508
|
+
async function listInboxRecords(context, kind, filters) {
|
|
1509
|
+
const params = new URLSearchParams;
|
|
1510
|
+
if (filters.run)
|
|
1511
|
+
params.set("runId", filters.run);
|
|
1512
|
+
if (filters.task)
|
|
1513
|
+
params.set("taskId", filters.task);
|
|
1514
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
1515
|
+
const payload = await requestServerJson(context, `/api/inbox/${kind}${query}`);
|
|
1516
|
+
const records = Array.isArray(payload) ? payload : [];
|
|
1517
|
+
return filters.pendingOnly ? records.filter((entry) => (entry.status ?? "pending") !== "resolved") : records;
|
|
1518
|
+
}
|
|
1519
|
+
async function readPendingInboxCounts(context) {
|
|
1520
|
+
try {
|
|
1521
|
+
const [approvals, inputs] = await Promise.all([
|
|
1522
|
+
listInboxRecords(context, "approvals", { pendingOnly: true }),
|
|
1523
|
+
listInboxRecords(context, "inputs", { pendingOnly: true })
|
|
1524
|
+
]);
|
|
1525
|
+
return { approvals: approvals.length, inputs: inputs.length };
|
|
1526
|
+
} catch {
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
async function printPendingInboxFooter(context) {
|
|
1531
|
+
if (context.outputMode !== "text")
|
|
1532
|
+
return;
|
|
1533
|
+
const counts = await readPendingInboxCounts(context);
|
|
1534
|
+
if (!counts || counts.approvals === 0 && counts.inputs === 0)
|
|
1535
|
+
return;
|
|
1536
|
+
const parts = [];
|
|
1537
|
+
if (counts.approvals > 0)
|
|
1538
|
+
parts.push(`${counts.approvals} approval${counts.approvals === 1 ? "" : "s"}`);
|
|
1539
|
+
if (counts.inputs > 0)
|
|
1540
|
+
parts.push(`${counts.inputs} input request${counts.inputs === 1 ? "" : "s"}`);
|
|
1541
|
+
console.log(`
|
|
1542
|
+
\u26A0 ${parts.join(" and ")} pending \u2014 run \`rig inbox\` to review.`);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
409
1545
|
// packages/cli/src/commands/run.ts
|
|
410
1546
|
function normalizeRemoteRunDetails(payload) {
|
|
411
1547
|
const run = payload.run;
|
|
@@ -418,6 +1554,22 @@ function normalizeRemoteRunDetails(payload) {
|
|
|
418
1554
|
...Array.isArray(payload.userInputs) ? { userInputs: payload.userInputs } : {}
|
|
419
1555
|
};
|
|
420
1556
|
}
|
|
1557
|
+
var REMOTE_TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged"]);
|
|
1558
|
+
async function listRunsForSelectedConnection(context, options = {}) {
|
|
1559
|
+
if (isRemoteConnectionSelected(context.projectRoot)) {
|
|
1560
|
+
return { runs: await listRunsViaServer(context, options), source: "server" };
|
|
1561
|
+
}
|
|
1562
|
+
return { runs: listAuthorityRuns(context.projectRoot), source: "local" };
|
|
1563
|
+
}
|
|
1564
|
+
function runStringField(run, key, fallback = "") {
|
|
1565
|
+
const value = run[key];
|
|
1566
|
+
return typeof value === "string" && value.trim() ? value : fallback;
|
|
1567
|
+
}
|
|
1568
|
+
function buildServerRunStatus(runs) {
|
|
1569
|
+
const activeRuns = runs.filter((run) => !REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
|
|
1570
|
+
const recentRuns = runs.filter((run) => REMOTE_TERMINAL_RUN_STATUSES.has(runStringField(run, "status").toLowerCase()));
|
|
1571
|
+
return { activeRuns, recentRuns, runs };
|
|
1572
|
+
}
|
|
421
1573
|
function shouldPromptForEpicSelection(context, command, promptEpic, noEpicPrompt) {
|
|
422
1574
|
if (noEpicPrompt) {
|
|
423
1575
|
return false;
|
|
@@ -438,7 +1590,7 @@ async function promptForEpicSelection(projectRoot, command) {
|
|
|
438
1590
|
options.unshift(defaultEpic);
|
|
439
1591
|
}
|
|
440
1592
|
if (options.length === 0) {
|
|
441
|
-
throw new
|
|
1593
|
+
throw new CliError("No open epic found. Pass --epic <id>.", 1, { hint: "Re-run with `rig run start --epic <id>`." });
|
|
442
1594
|
}
|
|
443
1595
|
console.log(`Select epic for run ${command}:`);
|
|
444
1596
|
options.forEach((id, index) => {
|
|
@@ -460,7 +1612,7 @@ async function promptForEpicSelection(projectRoot, command) {
|
|
|
460
1612
|
return fallback ?? options[0];
|
|
461
1613
|
}
|
|
462
1614
|
if (answer === "q" || answer === "quit") {
|
|
463
|
-
throw new
|
|
1615
|
+
throw new CliError("Run cancelled by user.");
|
|
464
1616
|
}
|
|
465
1617
|
if (/^\d+$/.test(answer)) {
|
|
466
1618
|
const index = Number.parseInt(answer, 10) - 1;
|
|
@@ -479,21 +1631,16 @@ async function promptForEpicSelection(projectRoot, command) {
|
|
|
479
1631
|
}
|
|
480
1632
|
async function executeRun(context, args) {
|
|
481
1633
|
const [command = "status", ...rest] = args;
|
|
482
|
-
const runtimeContext =
|
|
1634
|
+
const runtimeContext = loadRuntimeContextFromEnv() ?? undefined;
|
|
483
1635
|
switch (command) {
|
|
484
1636
|
case "list": {
|
|
485
|
-
requireNoExtraArgs(rest, "
|
|
486
|
-
const runs =
|
|
1637
|
+
requireNoExtraArgs(rest, "rig run list");
|
|
1638
|
+
const { runs, source } = isRemoteConnectionSelected(context.projectRoot) ? await withSpinner("Reading runs from server\u2026", () => listRunsForSelectedConnection(context, { limit: 100 }), { outputMode: context.outputMode }) : await listRunsForSelectedConnection(context, { limit: 100 });
|
|
487
1639
|
if (context.outputMode === "text") {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
} else {
|
|
491
|
-
for (const run of runs) {
|
|
492
|
-
console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.title}`);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
1640
|
+
printFormattedOutput(formatRunList(runs, { source }));
|
|
1641
|
+
await printPendingInboxFooter(context);
|
|
495
1642
|
}
|
|
496
|
-
return { ok: true, group: "run", command, details: { runs } };
|
|
1643
|
+
return { ok: true, group: "run", command, details: { runs, source } };
|
|
497
1644
|
}
|
|
498
1645
|
case "delete": {
|
|
499
1646
|
let pending = rest;
|
|
@@ -501,9 +1648,9 @@ async function executeRun(context, args) {
|
|
|
501
1648
|
pending = run.rest;
|
|
502
1649
|
const purgeArtifacts = takeFlag(pending, "--purge-artifacts");
|
|
503
1650
|
pending = purgeArtifacts.rest;
|
|
504
|
-
requireNoExtraArgs(pending, "
|
|
1651
|
+
requireNoExtraArgs(pending, "rig run delete --run <id> [--purge-artifacts]");
|
|
505
1652
|
if (!run.value) {
|
|
506
|
-
throw new
|
|
1653
|
+
throw new CliError("run delete requires --run <id>.", 1, { hint: "Run `rig run list` to find run ids, then `rig run delete --run <run-id>`." });
|
|
507
1654
|
}
|
|
508
1655
|
const result = await deleteRunState(context.projectRoot, {
|
|
509
1656
|
runId: run.value,
|
|
@@ -531,9 +1678,9 @@ async function executeRun(context, args) {
|
|
|
531
1678
|
pending = keepRuntimes.rest;
|
|
532
1679
|
const keepQueue = takeFlag(pending, "--keep-queue");
|
|
533
1680
|
pending = keepQueue.rest;
|
|
534
|
-
requireNoExtraArgs(pending, "
|
|
1681
|
+
requireNoExtraArgs(pending, "rig run cleanup --all [--keep-artifacts] [--keep-runtimes] [--keep-queue]");
|
|
535
1682
|
if (!all.value) {
|
|
536
|
-
throw new
|
|
1683
|
+
throw new CliError("run cleanup currently requires --all.", 1, { hint: "Run `rig run cleanup --all` (add --keep-artifacts/--keep-runtimes/--keep-queue to retain state)." });
|
|
537
1684
|
}
|
|
538
1685
|
const result = await cleanupRunState(context.projectRoot, {
|
|
539
1686
|
includeArtifacts: !keepArtifacts.value,
|
|
@@ -550,20 +1697,25 @@ async function executeRun(context, args) {
|
|
|
550
1697
|
}
|
|
551
1698
|
case "show": {
|
|
552
1699
|
let pending = rest;
|
|
1700
|
+
const rawResult = takeFlag(pending, "--raw");
|
|
1701
|
+
pending = rawResult.rest;
|
|
553
1702
|
const run = takeOption(pending, "--run");
|
|
554
1703
|
pending = run.rest;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1704
|
+
const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
|
|
1705
|
+
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
1706
|
+
requireNoExtraArgs(extra, "rig run show <id>|--run <id> [--raw]");
|
|
1707
|
+
const runId = run.value ?? positionalRunId;
|
|
1708
|
+
if (!runId) {
|
|
1709
|
+
throw new CliError("run show requires a run id.", 1, { hint: "Run `rig run list` to find run ids, then `rig run show <run-id>`." });
|
|
558
1710
|
}
|
|
559
|
-
const record =
|
|
1711
|
+
const record = readAuthorityRun2(context.projectRoot, runId) ?? normalizeRemoteRunDetails(await withSpinner(`Reading run ${runId} from server\u2026`, () => getRunDetailsViaServer(context, runId).catch(() => ({})), { outputMode: context.outputMode }));
|
|
560
1712
|
if (!record) {
|
|
561
|
-
throw new
|
|
1713
|
+
throw new CliError(`Run not found: ${runId}`, 2, { hint: "Run `rig run list` to see recorded runs." });
|
|
562
1714
|
}
|
|
563
1715
|
if (context.outputMode === "text") {
|
|
564
|
-
|
|
1716
|
+
printFormattedOutput(rawResult.value ? JSON.stringify(record, null, 2) : formatRunCard(record));
|
|
565
1717
|
}
|
|
566
|
-
return { ok: true, group: "run", command, details: record };
|
|
1718
|
+
return { ok: true, group: "run", command, details: { ...record, rawOutput: rawResult.value } };
|
|
567
1719
|
}
|
|
568
1720
|
case "timeline": {
|
|
569
1721
|
let pending = rest;
|
|
@@ -571,38 +1723,69 @@ async function executeRun(context, args) {
|
|
|
571
1723
|
pending = run.rest;
|
|
572
1724
|
const follow = takeFlag(pending, "--follow");
|
|
573
1725
|
pending = follow.rest;
|
|
574
|
-
requireNoExtraArgs(pending, "
|
|
1726
|
+
requireNoExtraArgs(pending, "rig run timeline --run <id> [--follow]");
|
|
575
1727
|
if (!run.value) {
|
|
576
|
-
throw new
|
|
1728
|
+
throw new CliError("run timeline requires --run <id>.", 1, { hint: "Run `rig run list` to find run ids, then `rig run timeline --run <run-id>`." });
|
|
1729
|
+
}
|
|
1730
|
+
const renderer = createPiRunStreamRenderer();
|
|
1731
|
+
let cursor = null;
|
|
1732
|
+
const timelineRunId = run.value;
|
|
1733
|
+
const page = await withSpinner(`Reading timeline for ${timelineRunId}\u2026`, () => getRunTimelineViaServer(context, timelineRunId, { limit: 500 }), { outputMode: context.outputMode });
|
|
1734
|
+
const events = Array.isArray(page.entries) ? page.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1735
|
+
cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
|
|
1736
|
+
if (context.outputMode === "text") {
|
|
1737
|
+
renderer.renderTimeline(events);
|
|
577
1738
|
}
|
|
578
|
-
const timelinePath = resolve3(resolveAuthorityRunDir(context.projectRoot, run.value), "timeline.jsonl");
|
|
579
|
-
const printEvents = () => {
|
|
580
|
-
const events2 = readJsonlFile(timelinePath);
|
|
581
|
-
if (context.outputMode === "text") {
|
|
582
|
-
for (const event of events2) {
|
|
583
|
-
console.log(JSON.stringify(event));
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
return events2;
|
|
587
|
-
};
|
|
588
|
-
const events = printEvents();
|
|
589
1739
|
if (follow.value && context.outputMode === "text") {
|
|
590
|
-
let lastLength = existsSync3(timelinePath) ? readFileSync3(timelinePath, "utf8").length : 0;
|
|
591
1740
|
while (true) {
|
|
592
1741
|
await Bun.sleep(1000);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1742
|
+
const nextPage = await getRunTimelineViaServer(context, run.value, { limit: 500, ...cursor ? { cursor } : {} });
|
|
1743
|
+
const nextEvents = Array.isArray(nextPage.entries) ? nextPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1744
|
+
cursor = typeof nextPage.nextCursor === "string" ? nextPage.nextCursor : cursor;
|
|
1745
|
+
renderer.renderTimeline(nextEvents);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
return { ok: true, group: "run", command, details: { runId: run.value, events, cursor } };
|
|
1749
|
+
}
|
|
1750
|
+
case "replay": {
|
|
1751
|
+
let pending = rest;
|
|
1752
|
+
const run = takeOption(pending, "--run");
|
|
1753
|
+
pending = run.rest;
|
|
1754
|
+
const withSession = takeFlag(pending, "--with-session");
|
|
1755
|
+
pending = withSession.rest;
|
|
1756
|
+
const positionalRunId = pending.length > 0 && pending[0] && !pending[0].startsWith("-") ? pending[0] : undefined;
|
|
1757
|
+
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
1758
|
+
requireNoExtraArgs(extra, "rig run replay <id>|--run <id> [--with-session]");
|
|
1759
|
+
const runId = run.value ?? positionalRunId;
|
|
1760
|
+
if (!runId) {
|
|
1761
|
+
throw new CliError("run replay requires a run id.", 2, { hint: "Run `rig run list` to find run ids, then `rig run replay <run-id>`." });
|
|
1762
|
+
}
|
|
1763
|
+
const replay = buildRunReplay(context.projectRoot, runId, { withSession: withSession.value });
|
|
1764
|
+
if (replay.entryCount === 0 && replay.sessionEntryCount === 0) {
|
|
1765
|
+
throw new CliError(`No run.jsonl lifecycle log found for ${runId} (looked at ${replay.logPath}).`, 2, { hint: "Run `rig run list` to confirm the run id; older runs may predate consolidated run.jsonl logging." });
|
|
1766
|
+
}
|
|
1767
|
+
if (context.outputMode === "text") {
|
|
1768
|
+
console.log(`Replay of ${runId} (${replay.entryCount} lifecycle entries${withSession.value ? `, ${replay.sessionEntryCount} session entries` : ""}):`);
|
|
1769
|
+
for (const line of replay.lines) {
|
|
1770
|
+
console.log(line);
|
|
1771
|
+
}
|
|
1772
|
+
if (withSession.value && !replay.sessionFile) {
|
|
1773
|
+
console.log("(no Pi session file resolvable from the run record; showing run.jsonl only)");
|
|
603
1774
|
}
|
|
604
1775
|
}
|
|
605
|
-
return {
|
|
1776
|
+
return {
|
|
1777
|
+
ok: true,
|
|
1778
|
+
group: "run",
|
|
1779
|
+
command,
|
|
1780
|
+
details: {
|
|
1781
|
+
runId,
|
|
1782
|
+
logPath: replay.logPath,
|
|
1783
|
+
sessionFile: replay.sessionFile,
|
|
1784
|
+
entryCount: replay.entryCount,
|
|
1785
|
+
sessionEntryCount: replay.sessionEntryCount,
|
|
1786
|
+
lines: replay.lines
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
606
1789
|
}
|
|
607
1790
|
case "attach": {
|
|
608
1791
|
let pending = rest;
|
|
@@ -618,41 +1801,40 @@ async function executeRun(context, args) {
|
|
|
618
1801
|
pending = pollMs.rest;
|
|
619
1802
|
const positionalRunId = pending.length > 0 ? pending[0] : undefined;
|
|
620
1803
|
const extra = positionalRunId ? pending.slice(1) : pending;
|
|
621
|
-
requireNoExtraArgs(extra, "
|
|
1804
|
+
requireNoExtraArgs(extra, "rig run attach <run-id>|--run <run-id> [--message <text>] [--once|--follow] [--poll-ms <ms>]");
|
|
622
1805
|
const runId = runOption.value ?? positionalRunId;
|
|
623
1806
|
if (!runId) {
|
|
624
|
-
throw new
|
|
1807
|
+
throw new CliError("run attach requires a run id.", 2, { hint: "Run `rig run list` to find run ids, then `rig run attach <run-id> --follow`." });
|
|
1808
|
+
}
|
|
1809
|
+
let steered = false;
|
|
1810
|
+
const steerMessage = messageOption.value?.trim();
|
|
1811
|
+
if (steerMessage) {
|
|
1812
|
+
await withSpinner("Queueing steering message\u2026", () => steerRunViaServer(context, runId, steerMessage), { outputMode: context.outputMode });
|
|
1813
|
+
steered = true;
|
|
625
1814
|
}
|
|
626
1815
|
const attached = await attachRunOperatorView(context, {
|
|
627
1816
|
runId,
|
|
628
|
-
message:
|
|
1817
|
+
message: null,
|
|
629
1818
|
once: once.value,
|
|
630
1819
|
follow: follow.value,
|
|
631
1820
|
pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
|
|
632
1821
|
});
|
|
633
|
-
return { ok: true, group: "run", command, details: attached };
|
|
1822
|
+
return { ok: true, group: "run", command, details: { ...attached, steered: attached.steered || steered } };
|
|
634
1823
|
}
|
|
635
1824
|
case "status": {
|
|
636
|
-
requireNoExtraArgs(rest, "
|
|
1825
|
+
requireNoExtraArgs(rest, "rig run status");
|
|
637
1826
|
if (context.dryRun) {
|
|
638
1827
|
if (context.outputMode === "text") {
|
|
639
1828
|
console.log("[dry-run] rig run status");
|
|
640
1829
|
}
|
|
641
1830
|
return { ok: true, group: "run", command };
|
|
642
1831
|
}
|
|
643
|
-
const summary = runStatus(context.projectRoot, runtimeContext);
|
|
1832
|
+
const summary = isRemoteConnectionSelected(context.projectRoot) ? buildServerRunStatus(await withSpinner("Reading run status from server\u2026", () => listRunsViaServer(context, { limit: 100 }), { outputMode: context.outputMode })) : runStatus(context.projectRoot, runtimeContext);
|
|
1833
|
+
const activeRuns = Array.isArray(summary.activeRuns) ? summary.activeRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
|
|
1834
|
+
const recentRuns = Array.isArray(summary.recentRuns) ? summary.recentRuns.filter((run) => Boolean(run && typeof run === "object" && !Array.isArray(run))) : [];
|
|
644
1835
|
if (context.outputMode === "text") {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.taskId ?? run.title}`);
|
|
648
|
-
}
|
|
649
|
-
if (summary.recentRuns.length > 0) {
|
|
650
|
-
console.log("");
|
|
651
|
-
console.log("Recent runs:");
|
|
652
|
-
for (const run of summary.recentRuns) {
|
|
653
|
-
console.log(`- ${run.runId} \xB7 ${run.status} \xB7 ${run.taskId ?? run.title}`);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
1836
|
+
printFormattedOutput(formatRunStatus({ activeRuns, recentRuns, runs: Array.isArray(summary.runs) ? summary.runs : [...activeRuns, ...recentRuns] }, { source: isRemoteConnectionSelected(context.projectRoot) ? "server" : "local" }));
|
|
1837
|
+
await printPendingInboxFooter(context);
|
|
656
1838
|
}
|
|
657
1839
|
return { ok: true, group: "run", command, details: summary };
|
|
658
1840
|
}
|
|
@@ -676,12 +1858,12 @@ async function executeRun(context, args) {
|
|
|
676
1858
|
pending = pollResult.rest;
|
|
677
1859
|
const noServerResult = takeFlag(pending, "--no-server");
|
|
678
1860
|
pending = noServerResult.rest;
|
|
679
|
-
requireNoExtraArgs(pending, "
|
|
1861
|
+
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]");
|
|
680
1862
|
if (promptEpicResult.value && noEpicPromptResult.value) {
|
|
681
|
-
throw new
|
|
1863
|
+
throw new CliError("Cannot use --prompt-epic and --no-epic-prompt together.", 1, { hint: "Pass only one of --prompt-epic or --no-epic-prompt." });
|
|
682
1864
|
}
|
|
683
1865
|
if (promptEpicResult.value && (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
684
|
-
throw new
|
|
1866
|
+
throw new CliError("--prompt-epic requires an interactive terminal (TTY) in text mode.", 1, { hint: "Pass the epic explicitly instead: `rig run start --epic <id>`." });
|
|
685
1867
|
}
|
|
686
1868
|
let resolvedEpicId = epicResult.value || undefined;
|
|
687
1869
|
if (!resolvedEpicId && shouldPromptForEpicSelection(context, command, promptEpicResult.value, noEpicPromptResult.value)) {
|
|
@@ -709,7 +1891,7 @@ async function executeRun(context, args) {
|
|
|
709
1891
|
console.log(`Runs: ${result.runIds.join(", ")}`);
|
|
710
1892
|
}
|
|
711
1893
|
if (result.exitCode !== 0) {
|
|
712
|
-
throw new
|
|
1894
|
+
throw new CliError(`run ${command} failed with exit code ${result.exitCode}.`, result.exitCode, { hint: "Inspect with `rig run status` and `rig inspect logs --task <id>`." });
|
|
713
1895
|
}
|
|
714
1896
|
return {
|
|
715
1897
|
ok: true,
|
|
@@ -724,38 +1906,76 @@ async function executeRun(context, args) {
|
|
|
724
1906
|
};
|
|
725
1907
|
}
|
|
726
1908
|
case "resume": {
|
|
727
|
-
|
|
1909
|
+
let pending = rest;
|
|
1910
|
+
const runOpt = takeOption(pending, "--run");
|
|
1911
|
+
pending = runOpt.rest;
|
|
1912
|
+
const positional = pending[0] && !pending[0].startsWith("-") ? pending.shift() : undefined;
|
|
1913
|
+
requireNoExtraArgs(pending, "rig run resume [<run-id>] [--run <id>]");
|
|
1914
|
+
const targetRunId = runOpt.value ?? positional ?? null;
|
|
728
1915
|
if (context.dryRun) {
|
|
729
1916
|
if (context.outputMode === "text") {
|
|
730
1917
|
console.log("[dry-run] rig run resume");
|
|
731
1918
|
}
|
|
732
1919
|
return { ok: true, group: "run", command };
|
|
733
1920
|
}
|
|
734
|
-
const resumed = await
|
|
1921
|
+
const resumed = await withSpinner(targetRunId ? `Resuming run ${targetRunId}\u2026` : "Resuming latest resumable run\u2026", () => resumeRunViaServer(context, targetRunId, { restart: false }), { outputMode: context.outputMode });
|
|
735
1922
|
if (context.outputMode === "text") {
|
|
736
1923
|
console.log(`Resumed run: ${resumed.runId}`);
|
|
737
1924
|
}
|
|
738
1925
|
return { ok: true, group: "run", command, details: resumed };
|
|
739
1926
|
}
|
|
740
1927
|
case "restart": {
|
|
741
|
-
|
|
1928
|
+
let pending = rest;
|
|
1929
|
+
const runOpt = takeOption(pending, "--run");
|
|
1930
|
+
pending = runOpt.rest;
|
|
1931
|
+
const positional = pending[0] && !pending[0].startsWith("-") ? pending.shift() : undefined;
|
|
1932
|
+
requireNoExtraArgs(pending, "rig run restart [<run-id>] [--run <id>]");
|
|
1933
|
+
const targetRunId = runOpt.value ?? positional ?? null;
|
|
742
1934
|
if (context.dryRun) {
|
|
743
1935
|
if (context.outputMode === "text") {
|
|
744
1936
|
console.log("[dry-run] rig run restart");
|
|
745
1937
|
}
|
|
746
1938
|
return { ok: true, group: "run", command };
|
|
747
1939
|
}
|
|
748
|
-
const restarted = await
|
|
1940
|
+
const restarted = await withSpinner(targetRunId ? `Restarting run ${targetRunId}\u2026` : "Restarting latest run\u2026", () => resumeRunViaServer(context, targetRunId, { restart: true }), { outputMode: context.outputMode });
|
|
749
1941
|
if (context.outputMode === "text") {
|
|
750
1942
|
console.log(`Restarted run: ${restarted.runId}`);
|
|
751
1943
|
}
|
|
752
1944
|
return { ok: true, group: "run", command, details: restarted };
|
|
753
1945
|
}
|
|
1946
|
+
case "steer": {
|
|
1947
|
+
const runOption = takeOption(rest, "--run");
|
|
1948
|
+
const messageOption = takeOption(runOption.rest, "--message");
|
|
1949
|
+
const shortMessageOption = takeOption(messageOption.rest, "-m");
|
|
1950
|
+
const positionalRunId = shortMessageOption.rest.length > 0 ? shortMessageOption.rest[0] : undefined;
|
|
1951
|
+
const extra = positionalRunId ? shortMessageOption.rest.slice(1) : shortMessageOption.rest;
|
|
1952
|
+
requireNoExtraArgs(extra, "rig run steer [<run-id>|--run <id>] --message <text>");
|
|
1953
|
+
const runId = runOption.value ?? positionalRunId;
|
|
1954
|
+
const message = messageOption.value ?? shortMessageOption.value;
|
|
1955
|
+
if (!runId) {
|
|
1956
|
+
throw new CliError("run steer requires a run id (positional or --run <id>).", 2, { hint: "Run `rig run list` to find run ids, then `rig run steer <run-id> --message <text>`." });
|
|
1957
|
+
}
|
|
1958
|
+
if (!message?.trim()) {
|
|
1959
|
+
throw new CliError("run steer requires --message <text>.", 2, { hint: 'Re-run as `rig run steer <run-id> --message "your steering note"`.' });
|
|
1960
|
+
}
|
|
1961
|
+
if (context.dryRun) {
|
|
1962
|
+
if (context.outputMode === "text") {
|
|
1963
|
+
console.log(`[dry-run] rig run steer ${runId} --message ${JSON.stringify(message)}`);
|
|
1964
|
+
}
|
|
1965
|
+
return { ok: true, group: "run", command, details: { runId, dryRun: true } };
|
|
1966
|
+
}
|
|
1967
|
+
const trimmedMessage = message.trim();
|
|
1968
|
+
await withSpinner("Queueing steering message\u2026", () => steerRunViaServer(context, runId, trimmedMessage), { outputMode: context.outputMode });
|
|
1969
|
+
if (context.outputMode === "text") {
|
|
1970
|
+
console.log(`Steering message queued for ${runId}.`);
|
|
1971
|
+
}
|
|
1972
|
+
return { ok: true, group: "run", command, details: { runId, queued: true } };
|
|
1973
|
+
}
|
|
754
1974
|
case "stop": {
|
|
755
1975
|
const runOption = takeOption(rest, "--run");
|
|
756
1976
|
const positionalRunId = runOption.rest.length > 0 ? runOption.rest[0] : undefined;
|
|
757
1977
|
const extra = positionalRunId ? runOption.rest.slice(1) : runOption.rest;
|
|
758
|
-
requireNoExtraArgs(extra, "
|
|
1978
|
+
requireNoExtraArgs(extra, "rig run stop [<run-id>|--run <id>]");
|
|
759
1979
|
const runId = runOption.value ?? positionalRunId;
|
|
760
1980
|
if (context.dryRun) {
|
|
761
1981
|
return {
|
|
@@ -766,14 +1986,14 @@ async function executeRun(context, args) {
|
|
|
766
1986
|
};
|
|
767
1987
|
}
|
|
768
1988
|
if (runId) {
|
|
769
|
-
const stopped = await stopRunViaServer(context, runId);
|
|
1989
|
+
const stopped = await withSpinner(`Requesting stop for ${runId}\u2026`, () => stopRunViaServer(context, runId), { outputMode: context.outputMode });
|
|
770
1990
|
if (context.outputMode === "text")
|
|
771
1991
|
console.log(`Stop requested: ${runId}`);
|
|
772
1992
|
return { ok: true, group: "run", command, details: stopped };
|
|
773
1993
|
}
|
|
774
1994
|
const result = await runStop(context.projectRoot);
|
|
775
1995
|
if (result.remaining.length > 0) {
|
|
776
|
-
throw new
|
|
1996
|
+
throw new CliError(`Failed to stop run(s): ${result.remaining.join(", ")}`, 1, { hint: "Check `rig run status`, then retry `rig run stop <run-id>` for the remaining runs." });
|
|
777
1997
|
}
|
|
778
1998
|
if (context.outputMode === "text") {
|
|
779
1999
|
console.log(`Stopped process count: ${result.stopped}`);
|
|
@@ -789,7 +2009,7 @@ async function executeRun(context, args) {
|
|
|
789
2009
|
};
|
|
790
2010
|
}
|
|
791
2011
|
default:
|
|
792
|
-
throw new
|
|
2012
|
+
throw new CliError(`Unknown run command: ${command}`, 1, { hint: "Run `rig run --help` to list available run commands." });
|
|
793
2013
|
}
|
|
794
2014
|
}
|
|
795
2015
|
export {
|