@h-rig/cli 0.0.6-alpha.12 → 0.0.6-alpha.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rig.js +710 -226
- package/dist/src/commands/_doctor-checks.js +7 -20
- package/dist/src/commands/_operator-surface.js +157 -0
- package/dist/src/commands/_operator-view.js +160 -51
- package/dist/src/commands/_preflight.js +30 -26
- package/dist/src/commands/_server-client.js +46 -22
- package/dist/src/commands/_snapshot-upload.js +7 -20
- package/dist/src/commands/_task-picker.js +21 -13
- package/dist/src/commands/agent.js +1 -0
- package/dist/src/commands/doctor.js +7 -20
- package/dist/src/commands/github.js +9 -22
- package/dist/src/commands/init.js +183 -44
- package/dist/src/commands/queue.js +1 -0
- package/dist/src/commands/run.js +172 -76
- package/dist/src/commands/server.js +7 -20
- package/dist/src/commands/setup.js +7 -20
- package/dist/src/commands/task-run-driver.js +446 -53
- package/dist/src/commands/task.js +231 -98
- package/dist/src/commands.js +702 -218
- package/dist/src/index.js +710 -226
- package/package.json +5 -5
|
@@ -100,11 +100,10 @@ function resolveSelectedConnection(projectRoot, options = {}) {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// packages/cli/src/commands/_server-client.ts
|
|
103
|
-
import { spawnSync } from "child_process";
|
|
104
103
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
105
104
|
import { resolve as resolve2 } from "path";
|
|
106
105
|
import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
|
|
107
|
-
var
|
|
106
|
+
var scopedGitHubBearerTokens = new Map;
|
|
108
107
|
function cleanToken(value) {
|
|
109
108
|
const trimmed = value?.trim();
|
|
110
109
|
return trimmed ? trimmed : null;
|
|
@@ -121,25 +120,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
|
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
122
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
const scopedKey = resolve2(projectRoot);
|
|
124
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
125
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
126
126
|
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
127
|
-
if (privateSession)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
|
|
132
|
-
if (envToken) {
|
|
133
|
-
cachedGitHubBearerToken = envToken;
|
|
134
|
-
return cachedGitHubBearerToken;
|
|
135
|
-
}
|
|
136
|
-
const result = spawnSync("gh", ["auth", "token"], {
|
|
137
|
-
encoding: "utf8",
|
|
138
|
-
timeout: 5000,
|
|
139
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
140
|
-
});
|
|
141
|
-
cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
|
|
142
|
-
return cachedGitHubBearerToken;
|
|
127
|
+
if (privateSession)
|
|
128
|
+
return privateSession;
|
|
129
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
143
130
|
}
|
|
144
131
|
async function ensureServerForCli(projectRoot) {
|
|
145
132
|
try {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli/src/commands/_operator-surface.ts
|
|
3
|
+
import { createInterface } from "readline";
|
|
4
|
+
import { createInterface as createPromptInterface } from "readline/promises";
|
|
5
|
+
var CANONICAL_STAGES = [
|
|
6
|
+
"Connect",
|
|
7
|
+
"GitHub/task sync",
|
|
8
|
+
"Prepare workspace",
|
|
9
|
+
"Launch Pi",
|
|
10
|
+
"Plan",
|
|
11
|
+
"Implement",
|
|
12
|
+
"Validate",
|
|
13
|
+
"Commit",
|
|
14
|
+
"Open PR",
|
|
15
|
+
"Review/CI",
|
|
16
|
+
"Merge",
|
|
17
|
+
"Complete"
|
|
18
|
+
];
|
|
19
|
+
function logDetail(log) {
|
|
20
|
+
return typeof log.detail === "string" ? log.detail.trim() : "";
|
|
21
|
+
}
|
|
22
|
+
function entryId(entry, fallback) {
|
|
23
|
+
return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
|
|
24
|
+
}
|
|
25
|
+
function renderOperatorSnapshot(snapshot) {
|
|
26
|
+
const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
|
|
27
|
+
const runId = String(run.runId ?? run.id ?? "run");
|
|
28
|
+
const status = String(run.status ?? "unknown");
|
|
29
|
+
const logs = snapshot.logs ?? [];
|
|
30
|
+
const latestByStage = new Map;
|
|
31
|
+
for (const log of logs) {
|
|
32
|
+
const title = String(log.title ?? "").toLowerCase();
|
|
33
|
+
const stageName = String(log.stage ?? "").toLowerCase();
|
|
34
|
+
const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
|
|
35
|
+
if (stage)
|
|
36
|
+
latestByStage.set(stage, log);
|
|
37
|
+
}
|
|
38
|
+
const stageLines = CANONICAL_STAGES.flatMap((stage) => {
|
|
39
|
+
const match = latestByStage.get(stage);
|
|
40
|
+
return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
|
|
41
|
+
});
|
|
42
|
+
return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
|
|
43
|
+
`);
|
|
44
|
+
}
|
|
45
|
+
function createPiRunStreamRenderer(output = process.stdout) {
|
|
46
|
+
let lastSnapshot = "";
|
|
47
|
+
const assistantTextById = new Map;
|
|
48
|
+
const seenTimeline = new Set;
|
|
49
|
+
const seenLogs = new Set;
|
|
50
|
+
const writeLine = (line) => output.write(`${line}
|
|
51
|
+
`);
|
|
52
|
+
return {
|
|
53
|
+
renderSnapshot(snapshot) {
|
|
54
|
+
const rendered = renderOperatorSnapshot(snapshot);
|
|
55
|
+
if (rendered && rendered !== lastSnapshot) {
|
|
56
|
+
writeLine(rendered);
|
|
57
|
+
lastSnapshot = rendered;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
renderTimeline(entries) {
|
|
61
|
+
for (const [index, entry] of entries.entries()) {
|
|
62
|
+
const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
|
|
63
|
+
if (entry.type === "assistant_message" && typeof entry.text === "string") {
|
|
64
|
+
const text = entry.text;
|
|
65
|
+
const previousText = assistantTextById.get(id) ?? "";
|
|
66
|
+
if (text.startsWith(previousText)) {
|
|
67
|
+
const delta = text.slice(previousText.length);
|
|
68
|
+
if (delta)
|
|
69
|
+
output.write(delta);
|
|
70
|
+
} else if (text.trim() && text !== previousText) {
|
|
71
|
+
writeLine(`
|
|
72
|
+
[Pi assistant]`);
|
|
73
|
+
output.write(text);
|
|
74
|
+
}
|
|
75
|
+
assistantTextById.set(id, text);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (seenTimeline.has(id))
|
|
79
|
+
continue;
|
|
80
|
+
seenTimeline.add(id);
|
|
81
|
+
if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
|
|
82
|
+
writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (entry.type === "timeline_warning") {
|
|
86
|
+
writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
renderLogs(entries) {
|
|
91
|
+
for (const [index, entry] of entries.entries()) {
|
|
92
|
+
const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
|
|
93
|
+
if (seenLogs.has(id))
|
|
94
|
+
continue;
|
|
95
|
+
seenLogs.add(id);
|
|
96
|
+
const title = String(entry.title ?? "");
|
|
97
|
+
if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
|
|
98
|
+
continue;
|
|
99
|
+
const detail = logDetail(entry);
|
|
100
|
+
if (!detail)
|
|
101
|
+
continue;
|
|
102
|
+
writeLine(`[${title || "Rig log"}] ${detail}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function createOperatorSurface(options = {}) {
|
|
108
|
+
const input = options.input ?? process.stdin;
|
|
109
|
+
const output = options.output ?? process.stdout;
|
|
110
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
111
|
+
const renderer = createPiRunStreamRenderer(output);
|
|
112
|
+
const writeLine = (line) => output.write(`${line}
|
|
113
|
+
`);
|
|
114
|
+
return {
|
|
115
|
+
mode: "pi-compatible-text",
|
|
116
|
+
...renderer,
|
|
117
|
+
info: writeLine,
|
|
118
|
+
error: (message) => errorOutput.write(`${message}
|
|
119
|
+
`),
|
|
120
|
+
attachCommandInput(handler) {
|
|
121
|
+
if (options.interactive === false || !input.isTTY)
|
|
122
|
+
return null;
|
|
123
|
+
const rl = createInterface({ input, output: process.stdout, terminal: false });
|
|
124
|
+
rl.on("line", (line) => {
|
|
125
|
+
Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
126
|
+
});
|
|
127
|
+
return { close: () => rl.close() };
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function taskId(task) {
|
|
132
|
+
return typeof task.id === "string" && task.id.trim() ? task.id : "<unknown>";
|
|
133
|
+
}
|
|
134
|
+
function taskTitle(task) {
|
|
135
|
+
return typeof task.title === "string" && task.title.trim() ? task.title : "Untitled task";
|
|
136
|
+
}
|
|
137
|
+
function taskStatus(task) {
|
|
138
|
+
return typeof task.status === "string" && task.status.trim() ? task.status : "unknown";
|
|
139
|
+
}
|
|
140
|
+
function renderTaskPickerRows(tasks) {
|
|
141
|
+
return tasks.map((task, index) => `${index + 1}. ${taskId(task)} \xB7 ${taskStatus(task)} \xB7 ${taskTitle(task)}`);
|
|
142
|
+
}
|
|
143
|
+
async function promptForTaskSelection(question) {
|
|
144
|
+
const rl = createPromptInterface({ input: process.stdin, output: process.stdout });
|
|
145
|
+
try {
|
|
146
|
+
return await rl.question(question);
|
|
147
|
+
} finally {
|
|
148
|
+
rl.close();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export {
|
|
152
|
+
renderTaskPickerRows,
|
|
153
|
+
renderOperatorSnapshot,
|
|
154
|
+
promptForTaskSelection,
|
|
155
|
+
createPiRunStreamRenderer,
|
|
156
|
+
createOperatorSurface
|
|
157
|
+
};
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// packages/cli/src/commands/_operator-view.ts
|
|
3
|
-
import { createInterface } from "readline";
|
|
4
|
-
|
|
5
2
|
// packages/cli/src/commands/_server-client.ts
|
|
6
|
-
import { spawnSync } from "child_process";
|
|
7
3
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
8
4
|
import { resolve as resolve2 } from "path";
|
|
9
5
|
|
|
@@ -102,7 +98,7 @@ function resolveSelectedConnection(projectRoot, options = {}) {
|
|
|
102
98
|
}
|
|
103
99
|
|
|
104
100
|
// packages/cli/src/commands/_server-client.ts
|
|
105
|
-
var
|
|
101
|
+
var scopedGitHubBearerTokens = new Map;
|
|
106
102
|
function cleanToken(value) {
|
|
107
103
|
const trimmed = value?.trim();
|
|
108
104
|
return trimmed ? trimmed : null;
|
|
@@ -119,25 +115,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
|
|
|
119
115
|
}
|
|
120
116
|
}
|
|
121
117
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
const scopedKey = resolve2(projectRoot);
|
|
119
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
120
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
124
121
|
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
125
|
-
if (privateSession)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
|
|
130
|
-
if (envToken) {
|
|
131
|
-
cachedGitHubBearerToken = envToken;
|
|
132
|
-
return cachedGitHubBearerToken;
|
|
133
|
-
}
|
|
134
|
-
const result = spawnSync("gh", ["auth", "token"], {
|
|
135
|
-
encoding: "utf8",
|
|
136
|
-
timeout: 5000,
|
|
137
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
138
|
-
});
|
|
139
|
-
cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
|
|
140
|
-
return cachedGitHubBearerToken;
|
|
122
|
+
if (privateSession)
|
|
123
|
+
return privateSession;
|
|
124
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
141
125
|
}
|
|
142
126
|
async function ensureServerForCli(projectRoot) {
|
|
143
127
|
try {
|
|
@@ -218,6 +202,15 @@ async function getRunLogsViaServer(context, runId, options = {}) {
|
|
|
218
202
|
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
219
203
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
220
204
|
}
|
|
205
|
+
async function getRunTimelineViaServer(context, runId, options = {}) {
|
|
206
|
+
const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/timeline`);
|
|
207
|
+
if (options.limit !== undefined)
|
|
208
|
+
url.searchParams.set("limit", String(options.limit));
|
|
209
|
+
if (options.cursor)
|
|
210
|
+
url.searchParams.set("cursor", options.cursor);
|
|
211
|
+
const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
|
|
212
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
|
|
213
|
+
}
|
|
221
214
|
async function stopRunViaServer(context, runId) {
|
|
222
215
|
const payload = await requestServerJson(context, "/api/runs/stop", {
|
|
223
216
|
method: "POST",
|
|
@@ -235,8 +228,8 @@ async function steerRunViaServer(context, runId, message) {
|
|
|
235
228
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
236
229
|
}
|
|
237
230
|
|
|
238
|
-
// packages/cli/src/commands/_operator-
|
|
239
|
-
|
|
231
|
+
// packages/cli/src/commands/_operator-surface.ts
|
|
232
|
+
import { createInterface } from "readline";
|
|
240
233
|
var CANONICAL_STAGES = [
|
|
241
234
|
"Connect",
|
|
242
235
|
"GitHub/task sync",
|
|
@@ -251,18 +244,121 @@ var CANONICAL_STAGES = [
|
|
|
251
244
|
"Merge",
|
|
252
245
|
"Complete"
|
|
253
246
|
];
|
|
247
|
+
function logDetail(log) {
|
|
248
|
+
return typeof log.detail === "string" ? log.detail.trim() : "";
|
|
249
|
+
}
|
|
250
|
+
function entryId(entry, fallback) {
|
|
251
|
+
return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
|
|
252
|
+
}
|
|
254
253
|
function renderOperatorSnapshot(snapshot) {
|
|
255
254
|
const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
|
|
256
255
|
const runId = String(run.runId ?? run.id ?? "run");
|
|
257
256
|
const status = String(run.status ?? "unknown");
|
|
258
257
|
const logs = snapshot.logs ?? [];
|
|
258
|
+
const latestByStage = new Map;
|
|
259
|
+
for (const log of logs) {
|
|
260
|
+
const title = String(log.title ?? "").toLowerCase();
|
|
261
|
+
const stageName = String(log.stage ?? "").toLowerCase();
|
|
262
|
+
const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
|
|
263
|
+
if (stage)
|
|
264
|
+
latestByStage.set(stage, log);
|
|
265
|
+
}
|
|
259
266
|
const stageLines = CANONICAL_STAGES.flatMap((stage) => {
|
|
260
|
-
const match =
|
|
261
|
-
return match ? [`${stage}: ${String(match.status ?? status)}`] : [];
|
|
267
|
+
const match = latestByStage.get(stage);
|
|
268
|
+
return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
|
|
262
269
|
});
|
|
263
270
|
return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
|
|
264
271
|
`);
|
|
265
272
|
}
|
|
273
|
+
function createPiRunStreamRenderer(output = process.stdout) {
|
|
274
|
+
let lastSnapshot = "";
|
|
275
|
+
const assistantTextById = new Map;
|
|
276
|
+
const seenTimeline = new Set;
|
|
277
|
+
const seenLogs = new Set;
|
|
278
|
+
const writeLine = (line) => output.write(`${line}
|
|
279
|
+
`);
|
|
280
|
+
return {
|
|
281
|
+
renderSnapshot(snapshot) {
|
|
282
|
+
const rendered = renderOperatorSnapshot(snapshot);
|
|
283
|
+
if (rendered && rendered !== lastSnapshot) {
|
|
284
|
+
writeLine(rendered);
|
|
285
|
+
lastSnapshot = rendered;
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
renderTimeline(entries) {
|
|
289
|
+
for (const [index, entry] of entries.entries()) {
|
|
290
|
+
const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
|
|
291
|
+
if (entry.type === "assistant_message" && typeof entry.text === "string") {
|
|
292
|
+
const text = entry.text;
|
|
293
|
+
const previousText = assistantTextById.get(id) ?? "";
|
|
294
|
+
if (text.startsWith(previousText)) {
|
|
295
|
+
const delta = text.slice(previousText.length);
|
|
296
|
+
if (delta)
|
|
297
|
+
output.write(delta);
|
|
298
|
+
} else if (text.trim() && text !== previousText) {
|
|
299
|
+
writeLine(`
|
|
300
|
+
[Pi assistant]`);
|
|
301
|
+
output.write(text);
|
|
302
|
+
}
|
|
303
|
+
assistantTextById.set(id, text);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (seenTimeline.has(id))
|
|
307
|
+
continue;
|
|
308
|
+
seenTimeline.add(id);
|
|
309
|
+
if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
|
|
310
|
+
writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (entry.type === "timeline_warning") {
|
|
314
|
+
writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
renderLogs(entries) {
|
|
319
|
+
for (const [index, entry] of entries.entries()) {
|
|
320
|
+
const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
|
|
321
|
+
if (seenLogs.has(id))
|
|
322
|
+
continue;
|
|
323
|
+
seenLogs.add(id);
|
|
324
|
+
const title = String(entry.title ?? "");
|
|
325
|
+
if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
|
|
326
|
+
continue;
|
|
327
|
+
const detail = logDetail(entry);
|
|
328
|
+
if (!detail)
|
|
329
|
+
continue;
|
|
330
|
+
writeLine(`[${title || "Rig log"}] ${detail}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function createOperatorSurface(options = {}) {
|
|
336
|
+
const input = options.input ?? process.stdin;
|
|
337
|
+
const output = options.output ?? process.stdout;
|
|
338
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
339
|
+
const renderer = createPiRunStreamRenderer(output);
|
|
340
|
+
const writeLine = (line) => output.write(`${line}
|
|
341
|
+
`);
|
|
342
|
+
return {
|
|
343
|
+
mode: "pi-compatible-text",
|
|
344
|
+
...renderer,
|
|
345
|
+
info: writeLine,
|
|
346
|
+
error: (message) => errorOutput.write(`${message}
|
|
347
|
+
`),
|
|
348
|
+
attachCommandInput(handler) {
|
|
349
|
+
if (options.interactive === false || !input.isTTY)
|
|
350
|
+
return null;
|
|
351
|
+
const rl = createInterface({ input, output: process.stdout, terminal: false });
|
|
352
|
+
rl.on("line", (line) => {
|
|
353
|
+
Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
354
|
+
});
|
|
355
|
+
return { close: () => rl.close() };
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// packages/cli/src/commands/_operator-view.ts
|
|
361
|
+
var TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "stopped", "cancelled", "canceled", "closed", "merged", "needs_attention", "needs-attention"]);
|
|
266
362
|
function runStatusFromPayload(payload) {
|
|
267
363
|
const run = payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
268
364
|
return String(run.status ?? "unknown").toLowerCase();
|
|
@@ -284,11 +380,22 @@ async function applyOperatorCommand(context, input, deps = {}) {
|
|
|
284
380
|
await (deps.steer ?? steerRunViaServer)(context, input.runId, userMessage);
|
|
285
381
|
return { action: "continue", message: "Steering message queued." };
|
|
286
382
|
}
|
|
287
|
-
async function readOperatorSnapshot(context, runId) {
|
|
383
|
+
async function readOperatorSnapshot(context, runId, options = {}) {
|
|
288
384
|
const run = await getRunDetailsViaServer(context, runId);
|
|
289
385
|
const logsPage = await getRunLogsViaServer(context, runId, { limit: 100 });
|
|
290
|
-
const
|
|
291
|
-
|
|
386
|
+
const timelinePage = await getRunTimelineViaServer(context, runId, { limit: 200, ...options.timelineCursor ? { cursor: options.timelineCursor } : {} }).catch((error) => ({
|
|
387
|
+
entries: [{
|
|
388
|
+
id: `timeline-unavailable:${runId}`,
|
|
389
|
+
type: "timeline_warning",
|
|
390
|
+
detail: `Selected Rig server did not provide run timeline events: ${error instanceof Error ? error.message : String(error)}`,
|
|
391
|
+
createdAt: new Date().toISOString()
|
|
392
|
+
}],
|
|
393
|
+
nextCursor: options.timelineCursor ?? null
|
|
394
|
+
}));
|
|
395
|
+
const logs = Array.isArray(logsPage.entries) ? logsPage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))).toReversed() : [];
|
|
396
|
+
const timeline = Array.isArray(timelinePage.entries) ? timelinePage.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
397
|
+
const timelineCursor = typeof timelinePage.nextCursor === "string" ? timelinePage.nextCursor : options.timelineCursor ?? null;
|
|
398
|
+
return { run, logs, timeline, timelineCursor, rendered: renderOperatorSnapshot({ run, logs, timeline }) };
|
|
292
399
|
}
|
|
293
400
|
async function attachRunOperatorView(context, input) {
|
|
294
401
|
let steered = false;
|
|
@@ -296,45 +403,47 @@ async function attachRunOperatorView(context, input) {
|
|
|
296
403
|
await steerRunViaServer(context, input.runId, input.message.trim());
|
|
297
404
|
steered = true;
|
|
298
405
|
}
|
|
406
|
+
const surface = createOperatorSurface({ interactive: input.interactive !== false });
|
|
299
407
|
let snapshot = await readOperatorSnapshot(context, input.runId);
|
|
300
408
|
if (context.outputMode === "text") {
|
|
301
|
-
|
|
409
|
+
surface.renderSnapshot(snapshot);
|
|
410
|
+
surface.renderTimeline(snapshot.timeline);
|
|
411
|
+
surface.renderLogs(snapshot.logs);
|
|
302
412
|
if (steered)
|
|
303
|
-
|
|
413
|
+
surface.info("Steering message queued.");
|
|
304
414
|
}
|
|
305
415
|
let detached = false;
|
|
306
|
-
let
|
|
416
|
+
let commandInput = null;
|
|
307
417
|
if (input.follow && !input.once && context.outputMode === "text") {
|
|
308
418
|
if (input.interactive !== false && process.stdin.isTTY) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
}).catch((error) => console.log(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
419
|
+
surface.info("Controls: /user <message>, /stop, /detach");
|
|
420
|
+
commandInput = surface.attachCommandInput(async (line) => {
|
|
421
|
+
const result = await applyOperatorCommand(context, { runId: input.runId, line });
|
|
422
|
+
if (result.message)
|
|
423
|
+
surface.info(result.message);
|
|
424
|
+
if (result.action === "detach" || result.action === "stopped") {
|
|
425
|
+
detached = true;
|
|
426
|
+
commandInput?.close();
|
|
427
|
+
}
|
|
320
428
|
});
|
|
321
429
|
}
|
|
322
|
-
let lastRendered = snapshot.rendered;
|
|
323
430
|
const pollMs = Math.max(250, Math.trunc(input.pollMs ?? 2000));
|
|
431
|
+
let timelineCursor = snapshot.timelineCursor;
|
|
324
432
|
while (!detached && !TERMINAL_RUN_STATUSES.has(runStatusFromPayload(snapshot.run))) {
|
|
325
433
|
await Bun.sleep(pollMs);
|
|
326
|
-
snapshot = await readOperatorSnapshot(context, input.runId);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
434
|
+
snapshot = await readOperatorSnapshot(context, input.runId, { timelineCursor });
|
|
435
|
+
timelineCursor = snapshot.timelineCursor;
|
|
436
|
+
surface.renderSnapshot(snapshot);
|
|
437
|
+
surface.renderTimeline(snapshot.timeline);
|
|
438
|
+
surface.renderLogs(snapshot.logs);
|
|
331
439
|
}
|
|
332
|
-
|
|
440
|
+
commandInput?.close();
|
|
333
441
|
}
|
|
334
442
|
return { ...snapshot, steered, detached };
|
|
335
443
|
}
|
|
336
444
|
export {
|
|
337
445
|
renderOperatorSnapshot,
|
|
446
|
+
createPiRunStreamRenderer,
|
|
338
447
|
attachRunOperatorView,
|
|
339
448
|
applyOperatorCommand
|
|
340
449
|
};
|
|
@@ -94,11 +94,10 @@ function resolveSelectedConnection(projectRoot, options = {}) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// packages/cli/src/commands/_server-client.ts
|
|
97
|
-
import { spawnSync } from "child_process";
|
|
98
97
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
99
98
|
import { resolve as resolve2 } from "path";
|
|
100
99
|
import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
|
|
101
|
-
var
|
|
100
|
+
var scopedGitHubBearerTokens = new Map;
|
|
102
101
|
function cleanToken(value) {
|
|
103
102
|
const trimmed = value?.trim();
|
|
104
103
|
return trimmed ? trimmed : null;
|
|
@@ -115,25 +114,13 @@ function readPrivateRemoteSessionToken(projectRoot) {
|
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
116
|
function readGitHubBearerTokenForRemote(projectRoot) {
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
const scopedKey = resolve2(projectRoot);
|
|
118
|
+
if (scopedGitHubBearerTokens.has(scopedKey))
|
|
119
|
+
return scopedGitHubBearerTokens.get(scopedKey) ?? null;
|
|
120
120
|
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
121
|
-
if (privateSession)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
|
|
126
|
-
if (envToken) {
|
|
127
|
-
cachedGitHubBearerToken = envToken;
|
|
128
|
-
return cachedGitHubBearerToken;
|
|
129
|
-
}
|
|
130
|
-
const result = spawnSync("gh", ["auth", "token"], {
|
|
131
|
-
encoding: "utf8",
|
|
132
|
-
timeout: 5000,
|
|
133
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
134
|
-
});
|
|
135
|
-
cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
|
|
136
|
-
return cachedGitHubBearerToken;
|
|
121
|
+
if (privateSession)
|
|
122
|
+
return privateSession;
|
|
123
|
+
return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
|
|
137
124
|
}
|
|
138
125
|
async function ensureServerForCli(projectRoot) {
|
|
139
126
|
try {
|
|
@@ -329,6 +316,9 @@ function permissionAllowsPr(payload) {
|
|
|
329
316
|
}
|
|
330
317
|
return null;
|
|
331
318
|
}
|
|
319
|
+
function isNotFoundError(error) {
|
|
320
|
+
return /\b(404|not found)\b/i.test(message(error));
|
|
321
|
+
}
|
|
332
322
|
function projectCheckoutReady(payload) {
|
|
333
323
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
334
324
|
return null;
|
|
@@ -361,19 +351,33 @@ async function runFastTaskRunPreflight(context, options = {}) {
|
|
|
361
351
|
const checks = [];
|
|
362
352
|
const request = options.requestJson ?? ((pathname, init) => requestServerJson(context, pathname, init));
|
|
363
353
|
const taskId = options.taskId?.trim() || null;
|
|
354
|
+
const requiresCurrentRunApi = Boolean(taskId);
|
|
355
|
+
const selectedServer = options.requestJson ? null : await ensureServerForCli(context.projectRoot).catch(() => null);
|
|
356
|
+
const allowLocalLegacyTaskRunCompatibility = selectedServer?.connectionKind === "local";
|
|
357
|
+
let legacyServerCompatibility = false;
|
|
364
358
|
try {
|
|
365
359
|
await request("/api/server/status");
|
|
366
360
|
checks.push(preflightCheck("server", "Rig server reachable", "pass"));
|
|
367
361
|
} catch (error) {
|
|
368
|
-
|
|
362
|
+
if (isNotFoundError(error)) {
|
|
363
|
+
try {
|
|
364
|
+
await request("/health");
|
|
365
|
+
legacyServerCompatibility = !requiresCurrentRunApi || allowLocalLegacyTaskRunCompatibility;
|
|
366
|
+
checks.push(requiresCurrentRunApi && !allowLocalLegacyTaskRunCompatibility ? preflightCheck("server", "Rig server reachable", "fail", "legacy /health endpoint only; current task-run APIs are required", "Upgrade/select the Rig server before launching a task run.") : preflightCheck("server", "Rig server reachable", "pass", allowLocalLegacyTaskRunCompatibility ? "local legacy /health endpoint; submit endpoint will be authoritative" : "legacy /health endpoint"));
|
|
367
|
+
} catch (healthError) {
|
|
368
|
+
checks.push(preflightCheck("server", "Rig server reachable", "fail", message(healthError), "Start or select a reachable Rig server."));
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
checks.push(preflightCheck("server", "Rig server reachable", "fail", message(error), "Start or select a reachable Rig server."));
|
|
372
|
+
}
|
|
369
373
|
}
|
|
370
374
|
const repo = readRepoConnection(context.projectRoot);
|
|
371
|
-
checks.push(repo ? preflightCheck("project-link", "project linked to Rig connection", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to record the GitHub repo slug.") : preflightCheck("project-link", "project linked to Rig connection", "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig connect use <alias|local>`."));
|
|
375
|
+
checks.push(repo ? preflightCheck("project-link", "project linked to Rig connection", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to record the GitHub repo slug.") : preflightCheck("project-link", "project linked to Rig connection", legacyServerCompatibility ? "warn" : "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig connect use <alias|local>`."));
|
|
372
376
|
try {
|
|
373
377
|
const auth = await request("/api/github/auth/status");
|
|
374
|
-
checks.push(isAuthenticated(auth) ? preflightCheck("github-auth", "GitHub auth valid", "pass") : preflightCheck("github-auth", "GitHub auth valid", "fail", "not authenticated", "Run `rig github auth import-gh` or `rig github auth token --token <token>`."));
|
|
378
|
+
checks.push(isAuthenticated(auth) ? preflightCheck("github-auth", "GitHub auth valid", "pass") : preflightCheck("github-auth", "GitHub auth valid", legacyServerCompatibility ? "warn" : "fail", "not authenticated", "Run `rig github auth import-gh` or `rig github auth token --token <token>`."));
|
|
375
379
|
} catch (error) {
|
|
376
|
-
checks.push(preflightCheck("github-auth", "GitHub auth valid", "fail", message(error), "Fix GitHub auth on the selected Rig server."));
|
|
380
|
+
checks.push(preflightCheck("github-auth", "GitHub auth valid", legacyServerCompatibility ? "warn" : "fail", message(error), "Fix GitHub auth on the selected Rig server."));
|
|
377
381
|
}
|
|
378
382
|
try {
|
|
379
383
|
const projection = await request("/api/workspace/task-projection");
|
|
@@ -401,9 +405,9 @@ async function runFastTaskRunPreflight(context, options = {}) {
|
|
|
401
405
|
try {
|
|
402
406
|
const tasks = await request(`/api/workspace/tasks?limit=200&refresh=1`);
|
|
403
407
|
const found = Array.isArray(tasks) && tasks.some((task) => taskMatchesId(task, taskId));
|
|
404
|
-
checks.push(found ? preflightCheck("issue", "task/issue accessible", "pass", taskId) : preflightCheck("issue", "task/issue accessible", "fail", taskId, "Confirm the issue exists and matches the configured task filters."));
|
|
408
|
+
checks.push(found ? preflightCheck("issue", "task/issue accessible", "pass", taskId) : preflightCheck("issue", "task/issue accessible", legacyServerCompatibility ? "warn" : "fail", taskId, "Confirm the issue exists and matches the configured task filters."));
|
|
405
409
|
} catch (error) {
|
|
406
|
-
checks.push(preflightCheck("issue", "task/issue accessible", "fail", message(error), "Fix the task source before launching a run."));
|
|
410
|
+
checks.push(preflightCheck("issue", "task/issue accessible", legacyServerCompatibility ? "warn" : "fail", message(error), "Fix the task source before launching a run."));
|
|
407
411
|
}
|
|
408
412
|
try {
|
|
409
413
|
const runs = await request("/api/runs?limit=200");
|