@h-rig/cli 0.0.6-alpha.2 → 0.0.6-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rig.js +1288 -315
- package/dist/src/commands/_authority-runs.js +1 -0
- package/dist/src/commands/_cli-format.js +106 -0
- package/dist/src/commands/_doctor-checks.js +10 -22
- package/dist/src/commands/_operator-surface.js +204 -0
- package/dist/src/commands/_operator-view.js +207 -51
- package/dist/src/commands/_pi-install.js +4 -3
- package/dist/src/commands/_pi-session.js +253 -0
- package/dist/src/commands/_preflight.js +33 -28
- package/dist/src/commands/_server-client.js +80 -27
- package/dist/src/commands/_snapshot-upload.js +7 -20
- package/dist/src/commands/_task-picker.js +44 -16
- package/dist/src/commands/agent.js +2 -0
- package/dist/src/commands/doctor.js +10 -22
- package/dist/src/commands/github.js +9 -22
- package/dist/src/commands/init.js +295 -66
- package/dist/src/commands/queue.js +1 -0
- package/dist/src/commands/run.js +456 -95
- package/dist/src/commands/server.js +9 -22
- package/dist/src/commands/setup.js +10 -22
- package/dist/src/commands/task-run-driver.js +539 -64
- package/dist/src/commands/task.js +502 -130
- package/dist/src/commands.js +1276 -306
- package/dist/src/index.js +1288 -315
- package/dist/src/launcher.js +5 -3
- package/dist/src/runner.js +3 -2
- package/package.json +5 -4
|
@@ -67,6 +67,7 @@ function upsertAgentAuthorityRun(projectRoot, input) {
|
|
|
67
67
|
const runtimeAdapter = normalizeRuntimeAdapter(input.runtimeAdapter ?? existing?.runtimeAdapter ?? "claude-code");
|
|
68
68
|
const title = resolveTaskTitleForAuthorityRun(projectRoot, input.taskId) ?? input.taskId;
|
|
69
69
|
const next = {
|
|
70
|
+
...existing ?? {},
|
|
70
71
|
runId: input.runId,
|
|
71
72
|
projectRoot,
|
|
72
73
|
workspaceId: existing?.workspaceId ?? RIG_WORKSPACE_ID,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/cli/src/commands/_cli-format.ts
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
function stringField(record, key, fallback = "") {
|
|
5
|
+
const value = record[key];
|
|
6
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
7
|
+
}
|
|
8
|
+
function arrayField(record, key) {
|
|
9
|
+
const value = record[key];
|
|
10
|
+
return Array.isArray(value) ? value.flatMap((entry) => typeof entry === "string" && entry.trim() ? [entry.trim()] : []) : [];
|
|
11
|
+
}
|
|
12
|
+
function rawObject(record) {
|
|
13
|
+
const raw = record.raw;
|
|
14
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
15
|
+
}
|
|
16
|
+
function truncate(value, width) {
|
|
17
|
+
if (value.length <= width)
|
|
18
|
+
return value;
|
|
19
|
+
if (width <= 1)
|
|
20
|
+
return "\u2026";
|
|
21
|
+
return `${value.slice(0, width - 1)}\u2026`;
|
|
22
|
+
}
|
|
23
|
+
function pad(value, width) {
|
|
24
|
+
return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
|
|
25
|
+
}
|
|
26
|
+
function statusColor(status) {
|
|
27
|
+
const normalized = status.toLowerCase();
|
|
28
|
+
if (["completed", "merged", "closed", "done", "accepted"].includes(normalized))
|
|
29
|
+
return pc.green;
|
|
30
|
+
if (["failed", "needs_attention", "needs-attention", "blocked"].includes(normalized))
|
|
31
|
+
return pc.red;
|
|
32
|
+
if (["running", "reviewing", "validating", "in_progress", "in-progress"].includes(normalized))
|
|
33
|
+
return pc.cyan;
|
|
34
|
+
if (["ready", "open", "queued", "created", "preparing"].includes(normalized))
|
|
35
|
+
return pc.yellow;
|
|
36
|
+
return pc.dim;
|
|
37
|
+
}
|
|
38
|
+
function formatTaskList(tasks, options = {}) {
|
|
39
|
+
if (tasks.length === 0)
|
|
40
|
+
return pc.dim("No matching tasks.");
|
|
41
|
+
if (options.raw)
|
|
42
|
+
return tasks.map((task) => JSON.stringify(task)).join(`
|
|
43
|
+
`);
|
|
44
|
+
const rows = tasks.map((task) => {
|
|
45
|
+
const raw = rawObject(task);
|
|
46
|
+
const id = stringField(task, "id", "<unknown>");
|
|
47
|
+
const status = stringField(task, "status", "unknown");
|
|
48
|
+
const title = stringField(task, "title", "Untitled task");
|
|
49
|
+
const source = stringField(task, "source", stringField(raw, "source", ""));
|
|
50
|
+
const labels = arrayField(task, "labels").length > 0 ? arrayField(task, "labels") : arrayField(raw, "labels");
|
|
51
|
+
return { id, status, title, source, labels };
|
|
52
|
+
});
|
|
53
|
+
const idWidth = Math.min(18, Math.max(4, ...rows.map((row) => row.id.length)));
|
|
54
|
+
const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
|
|
55
|
+
const header = `${pc.bold(pad("TASK", idWidth))} ${pc.bold(pad("STATUS", statusWidth))} ${pc.bold("TITLE")}`;
|
|
56
|
+
const body = rows.map((row) => {
|
|
57
|
+
const labels = row.labels.length > 0 ? pc.dim(` ${row.labels.slice(0, 4).map((label) => `#${label}`).join(" ")}`) : "";
|
|
58
|
+
const source = row.source ? pc.dim(` ${row.source}`) : "";
|
|
59
|
+
return [
|
|
60
|
+
pc.bold(pad(truncate(row.id, idWidth), idWidth)),
|
|
61
|
+
statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
|
|
62
|
+
`${row.title}${labels}${source}`
|
|
63
|
+
].join(" ");
|
|
64
|
+
});
|
|
65
|
+
return [pc.bold("Rig tasks"), header, ...body].join(`
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
68
|
+
function formatRunList(runs, options = {}) {
|
|
69
|
+
if (runs.length === 0) {
|
|
70
|
+
return pc.dim(options.source === "server" ? "No runs recorded on the selected Rig server." : "No runs recorded in .rig/runs.");
|
|
71
|
+
}
|
|
72
|
+
const rows = runs.map((run) => {
|
|
73
|
+
const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
|
|
74
|
+
const status = stringField(run, "status", "unknown");
|
|
75
|
+
const taskId = stringField(run, "taskId", "");
|
|
76
|
+
const title = stringField(run, "title", taskId || "(untitled)");
|
|
77
|
+
const runtime = stringField(run, "runtimeAdapter", "");
|
|
78
|
+
return { runId, status, title, runtime };
|
|
79
|
+
});
|
|
80
|
+
const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
|
|
81
|
+
const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
|
|
82
|
+
const header = `${pc.bold(pad("RUN", idWidth))} ${pc.bold(pad("STATUS", statusWidth))} ${pc.bold("TITLE")}`;
|
|
83
|
+
const body = rows.map((row) => [
|
|
84
|
+
pc.bold(pad(truncate(row.runId, idWidth), idWidth)),
|
|
85
|
+
statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
|
|
86
|
+
`${row.title}${row.runtime ? pc.dim(` ${row.runtime}`) : ""}`
|
|
87
|
+
].join(" "));
|
|
88
|
+
return [pc.bold(options.source === "server" ? "Rig runs (server)" : "Rig runs"), header, ...body].join(`
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
function formatSubmittedRun(input) {
|
|
92
|
+
const lines = [`${pc.green("Run submitted")}: ${pc.bold(input.runId)}`];
|
|
93
|
+
if (input.task) {
|
|
94
|
+
const id = stringField(input.task, "id", "<unknown>");
|
|
95
|
+
const status = stringField(input.task, "status", "unknown");
|
|
96
|
+
const title = stringField(input.task, "title", "Untitled task");
|
|
97
|
+
lines.push(`${pc.dim("task")} ${pc.bold(id)} ${statusColor(status)(status)} ${title}`);
|
|
98
|
+
}
|
|
99
|
+
return lines.join(`
|
|
100
|
+
`);
|
|
101
|
+
}
|
|
102
|
+
export {
|
|
103
|
+
formatTaskList,
|
|
104
|
+
formatSubmittedRun,
|
|
105
|
+
formatRunList
|
|
106
|
+
};
|
|
@@ -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 {
|
|
@@ -222,7 +209,8 @@ async function loadRigConfigOrNull(projectRoot) {
|
|
|
222
209
|
import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
|
|
223
210
|
import { homedir as homedir2 } from "os";
|
|
224
211
|
import { resolve as resolve3 } from "path";
|
|
225
|
-
var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
|
|
212
|
+
var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
|
|
213
|
+
var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
|
|
226
214
|
async function defaultCommandRunner(command, options = {}) {
|
|
227
215
|
const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
|
|
228
216
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
@@ -241,7 +229,7 @@ function resolvePiHomeDir(inputHomeDir) {
|
|
|
241
229
|
function piListContainsPiRig(output) {
|
|
242
230
|
return output.split(/\r?\n/).some((line) => {
|
|
243
231
|
const normalized = line.trim();
|
|
244
|
-
return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
|
|
232
|
+
return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
|
|
245
233
|
});
|
|
246
234
|
}
|
|
247
235
|
async function safeRun(runner, command, options) {
|
|
@@ -0,0 +1,204 @@
|
|
|
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 parseProviderProtocolLog(title, detail) {
|
|
23
|
+
if (title.trim().toLowerCase() !== "agent output")
|
|
24
|
+
return null;
|
|
25
|
+
if (!detail.startsWith("{") || !detail.endsWith("}"))
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
const record = JSON.parse(detail);
|
|
29
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
30
|
+
return null;
|
|
31
|
+
const type = record.type;
|
|
32
|
+
return typeof type === "string" && [
|
|
33
|
+
"assistant",
|
|
34
|
+
"message_start",
|
|
35
|
+
"message_update",
|
|
36
|
+
"message_end",
|
|
37
|
+
"stream_event",
|
|
38
|
+
"tool_result",
|
|
39
|
+
"tool_execution_start",
|
|
40
|
+
"tool_execution_update",
|
|
41
|
+
"tool_execution_end",
|
|
42
|
+
"turn_start",
|
|
43
|
+
"turn_end"
|
|
44
|
+
].includes(type) ? record : null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function renderProviderProtocolLog(record) {
|
|
50
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
51
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") {
|
|
52
|
+
const toolName = String(record.toolName ?? record.name ?? "tool");
|
|
53
|
+
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";
|
|
54
|
+
return `[Pi tool] ${toolName} ${status}`;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function entryId(entry, fallback) {
|
|
59
|
+
return typeof entry.id === "string" && entry.id.trim() ? entry.id : fallback;
|
|
60
|
+
}
|
|
61
|
+
function renderOperatorSnapshot(snapshot) {
|
|
62
|
+
const run = snapshot.run.run && typeof snapshot.run.run === "object" ? snapshot.run.run : snapshot.run;
|
|
63
|
+
const runId = String(run.runId ?? run.id ?? "run");
|
|
64
|
+
const status = String(run.status ?? "unknown");
|
|
65
|
+
const logs = snapshot.logs ?? [];
|
|
66
|
+
const latestByStage = new Map;
|
|
67
|
+
for (const log of logs) {
|
|
68
|
+
const title = String(log.title ?? "").toLowerCase();
|
|
69
|
+
const stageName = String(log.stage ?? "").toLowerCase();
|
|
70
|
+
const stage = CANONICAL_STAGES.find((candidate) => candidate.toLowerCase() === title || candidate.toLowerCase() === stageName);
|
|
71
|
+
if (stage)
|
|
72
|
+
latestByStage.set(stage, log);
|
|
73
|
+
}
|
|
74
|
+
const stageLines = CANONICAL_STAGES.flatMap((stage) => {
|
|
75
|
+
const match = latestByStage.get(stage);
|
|
76
|
+
return match ? [`${stage}: ${String(match.status ?? status)}${logDetail(match) ? ` \u2014 ${logDetail(match)}` : ""}`] : [];
|
|
77
|
+
});
|
|
78
|
+
return [`Rig run ${runId}: ${status}`, ...stageLines].join(`
|
|
79
|
+
`);
|
|
80
|
+
}
|
|
81
|
+
function createPiRunStreamRenderer(output = process.stdout) {
|
|
82
|
+
let lastSnapshot = "";
|
|
83
|
+
const assistantTextById = new Map;
|
|
84
|
+
const seenTimeline = new Set;
|
|
85
|
+
const seenLogs = new Set;
|
|
86
|
+
const writeLine = (line) => output.write(`${line}
|
|
87
|
+
`);
|
|
88
|
+
return {
|
|
89
|
+
renderSnapshot(snapshot) {
|
|
90
|
+
const rendered = renderOperatorSnapshot(snapshot);
|
|
91
|
+
if (rendered && rendered !== lastSnapshot) {
|
|
92
|
+
writeLine(rendered);
|
|
93
|
+
lastSnapshot = rendered;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
renderTimeline(entries) {
|
|
97
|
+
for (const [index, entry] of entries.entries()) {
|
|
98
|
+
const id = entryId(entry, `timeline:${index}:${String(entry.cursor ?? "")}`);
|
|
99
|
+
if (entry.type === "assistant_message" && typeof entry.text === "string") {
|
|
100
|
+
const text = entry.text;
|
|
101
|
+
const previousText = assistantTextById.get(id) ?? "";
|
|
102
|
+
if (!previousText && text.trim()) {
|
|
103
|
+
writeLine("[Pi assistant]");
|
|
104
|
+
}
|
|
105
|
+
if (text.startsWith(previousText)) {
|
|
106
|
+
const delta = text.slice(previousText.length);
|
|
107
|
+
if (delta)
|
|
108
|
+
output.write(delta);
|
|
109
|
+
} else if (text.trim() && text !== previousText) {
|
|
110
|
+
if (previousText)
|
|
111
|
+
writeLine(`
|
|
112
|
+
[Pi assistant]`);
|
|
113
|
+
output.write(text);
|
|
114
|
+
}
|
|
115
|
+
assistantTextById.set(id, text);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (seenTimeline.has(id))
|
|
119
|
+
continue;
|
|
120
|
+
seenTimeline.add(id);
|
|
121
|
+
if (entry.type === "tool_execution_start" || entry.type === "tool_execution_update" || entry.type === "tool_execution_end" || entry.type === "mcp_tool_call") {
|
|
122
|
+
writeLine(`[Pi tool] ${String(entry.toolName ?? entry.name ?? entry.title ?? entry.type)} ${String(entry.status ?? entry.state ?? "")}`.trim());
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (entry.type === "timeline_warning") {
|
|
126
|
+
writeLine(`[Rig timeline] ${String(entry.detail ?? entry.message ?? "timeline unavailable")}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
renderLogs(entries) {
|
|
131
|
+
for (const [index, entry] of entries.entries()) {
|
|
132
|
+
const id = entryId(entry, `log:${index}:${String(entry.createdAt ?? "")}:${String(entry.title ?? "")}`);
|
|
133
|
+
if (seenLogs.has(id))
|
|
134
|
+
continue;
|
|
135
|
+
seenLogs.add(id);
|
|
136
|
+
const title = String(entry.title ?? "");
|
|
137
|
+
if (CANONICAL_STAGES.some((stage) => stage.toLowerCase() === title.toLowerCase()))
|
|
138
|
+
continue;
|
|
139
|
+
const detail = logDetail(entry);
|
|
140
|
+
if (!detail)
|
|
141
|
+
continue;
|
|
142
|
+
const protocolRecord = parseProviderProtocolLog(title, detail);
|
|
143
|
+
if (protocolRecord) {
|
|
144
|
+
const protocolLine = renderProviderProtocolLog(protocolRecord);
|
|
145
|
+
if (protocolLine)
|
|
146
|
+
writeLine(protocolLine);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
writeLine(`[${title || "Rig log"}] ${detail}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function createOperatorSurface(options = {}) {
|
|
155
|
+
const input = options.input ?? process.stdin;
|
|
156
|
+
const output = options.output ?? process.stdout;
|
|
157
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
158
|
+
const renderer = createPiRunStreamRenderer(output);
|
|
159
|
+
const writeLine = (line) => output.write(`${line}
|
|
160
|
+
`);
|
|
161
|
+
return {
|
|
162
|
+
mode: "pi-compatible-text",
|
|
163
|
+
...renderer,
|
|
164
|
+
info: writeLine,
|
|
165
|
+
error: (message) => errorOutput.write(`${message}
|
|
166
|
+
`),
|
|
167
|
+
attachCommandInput(handler) {
|
|
168
|
+
if (options.interactive === false || !input.isTTY)
|
|
169
|
+
return null;
|
|
170
|
+
const rl = createInterface({ input, output: process.stdout, terminal: false });
|
|
171
|
+
rl.on("line", (line) => {
|
|
172
|
+
Promise.resolve(handler(line)).catch((error) => writeLine(`Operator command failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
173
|
+
});
|
|
174
|
+
return { close: () => rl.close() };
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function taskId(task) {
|
|
179
|
+
return typeof task.id === "string" && task.id.trim() ? task.id : "<unknown>";
|
|
180
|
+
}
|
|
181
|
+
function taskTitle(task) {
|
|
182
|
+
return typeof task.title === "string" && task.title.trim() ? task.title : "Untitled task";
|
|
183
|
+
}
|
|
184
|
+
function taskStatus(task) {
|
|
185
|
+
return typeof task.status === "string" && task.status.trim() ? task.status : "unknown";
|
|
186
|
+
}
|
|
187
|
+
function renderTaskPickerRows(tasks) {
|
|
188
|
+
return tasks.map((task, index) => `${index + 1}. ${taskId(task)} \xB7 ${taskStatus(task)} \xB7 ${taskTitle(task)}`);
|
|
189
|
+
}
|
|
190
|
+
async function promptForTaskSelection(question) {
|
|
191
|
+
const rl = createPromptInterface({ input: process.stdin, output: process.stdout });
|
|
192
|
+
try {
|
|
193
|
+
return await rl.question(question);
|
|
194
|
+
} finally {
|
|
195
|
+
rl.close();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
export {
|
|
199
|
+
renderTaskPickerRows,
|
|
200
|
+
renderOperatorSnapshot,
|
|
201
|
+
promptForTaskSelection,
|
|
202
|
+
createPiRunStreamRenderer,
|
|
203
|
+
createOperatorSurface
|
|
204
|
+
};
|