@h-rig/cli 0.0.6-alpha.19 → 0.0.6-alpha.20

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.
@@ -2,8 +2,8 @@
2
2
  // packages/cli/src/commands/task.ts
3
3
  import { readFileSync as readFileSync4 } from "fs";
4
4
  import { spawnSync } from "child_process";
5
- import { createInterface as createInterface2 } from "readline/promises";
6
5
  import { resolve as resolve4 } from "path";
6
+ import { cancel as cancel2, confirm, isCancel as isCancel2 } from "@clack/prompts";
7
7
 
8
8
  // packages/cli/src/runner.ts
9
9
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
@@ -13,6 +13,9 @@ import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
13
13
  import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
14
14
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
15
15
  import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
16
+ function formatCommand(parts) {
17
+ return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
18
+ }
16
19
  function takeFlag(args, flag) {
17
20
  const rest = [];
18
21
  let value = false;
@@ -395,7 +398,8 @@ async function submitTaskRunViaServer(context, input) {
395
398
  import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
396
399
  import { homedir as homedir2 } from "os";
397
400
  import { resolve as resolve3 } from "path";
398
- var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
401
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
402
+ var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
399
403
  async function defaultCommandRunner(command, options = {}) {
400
404
  const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
401
405
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -408,13 +412,19 @@ async function defaultCommandRunner(command, options = {}) {
408
412
  function resolvePiRigExtensionPath(homeDir) {
409
413
  return resolve3(homeDir, ".pi", "agent", "extensions", "pi-rig");
410
414
  }
415
+ function resolvePiRigPackageSource(projectRoot, exists = existsSync3) {
416
+ const localPackage = resolve3(projectRoot, "packages", "pi-rig");
417
+ if (exists(resolve3(localPackage, "package.json")))
418
+ return localPackage;
419
+ return `npm:${PI_RIG_PACKAGE_NAME}`;
420
+ }
411
421
  function resolvePiHomeDir(inputHomeDir) {
412
422
  return inputHomeDir ?? process.env.RIG_PI_HOME_DIR?.trim() ?? homedir2();
413
423
  }
414
424
  function piListContainsPiRig(output) {
415
425
  return output.split(/\r?\n/).some((line) => {
416
426
  const normalized = line.trim();
417
- return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
427
+ return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
418
428
  });
419
429
  }
420
430
  async function safeRun(runner, command, options) {
@@ -718,6 +728,9 @@ function withMutedConsole(mute, fn) {
718
728
  }
719
729
  }
720
730
 
731
+ // packages/cli/src/commands/_task-picker.ts
732
+ import { cancel, isCancel, select } from "@clack/prompts";
733
+
721
734
  // packages/cli/src/commands/_operator-surface.ts
722
735
  import { createInterface } from "readline";
723
736
  import { createInterface as createPromptInterface } from "readline/promises";
@@ -928,20 +941,37 @@ async function selectTaskWithTextPicker(tasks, io = {}) {
928
941
  if (!isTty) {
929
942
  throw new Error("task run requires an interactive terminal to pick a task; pass --task <id>, --next, or --detach with a task id.");
930
943
  }
931
- const prompt = io.prompt ?? promptForTaskSelection;
932
- const renderer = io.renderer ?? { writeLine: (line) => process.stdout.write(`${line}
944
+ if (io.prompt || io.renderer) {
945
+ const prompt = io.prompt ?? promptForTaskSelection;
946
+ const renderer = io.renderer ?? { writeLine: (line) => process.stdout.write(`${line}
933
947
  `) };
934
- renderer.writeLine("Select Rig task:");
935
- for (const row of renderTaskPickerRows(tasks))
936
- renderer.writeLine(` ${row}`);
937
- const answer = (await prompt(`Task [1-${tasks.length}] or id: `)).trim();
938
- if (!answer)
948
+ renderer.writeLine("Select Rig task:");
949
+ for (const row of renderTaskPickerRows(tasks))
950
+ renderer.writeLine(` ${row}`);
951
+ const answer2 = (await prompt(`Task [1-${tasks.length}] or id: `)).trim();
952
+ if (!answer2)
953
+ return null;
954
+ if (/^\d+$/.test(answer2)) {
955
+ const index2 = Number.parseInt(answer2, 10) - 1;
956
+ return tasks[index2] ?? null;
957
+ }
958
+ return tasks.find((task) => taskId2(task) === answer2) ?? null;
959
+ }
960
+ const options = tasks.map((task, index2) => ({
961
+ value: `${index2}`,
962
+ label: `${taskId2(task)} \xB7 ${typeof task.title === "string" && task.title.trim() ? task.title.trim() : "Untitled task"}`,
963
+ hint: typeof task.status === "string" && task.status.trim() ? task.status.trim() : undefined
964
+ }));
965
+ const answer = await select({
966
+ message: "Select Rig task",
967
+ options
968
+ });
969
+ if (isCancel(answer)) {
970
+ cancel("No task selected.");
939
971
  return null;
940
- if (/^\d+$/.test(answer)) {
941
- const index = Number.parseInt(answer, 10) - 1;
942
- return tasks[index] ?? null;
943
972
  }
944
- return tasks.find((task) => taskId2(task) === answer) ?? null;
973
+ const index = Number.parseInt(String(answer), 10);
974
+ return Number.isFinite(index) ? tasks[index] ?? null : null;
945
975
  }
946
976
 
947
977
  // packages/cli/src/commands/_operator-view.ts
@@ -1029,6 +1059,170 @@ async function attachRunOperatorView(context, input) {
1029
1059
  return { ...snapshot, steered, detached };
1030
1060
  }
1031
1061
 
1062
+ // packages/cli/src/commands/_cli-format.ts
1063
+ import pc from "picocolors";
1064
+ function stringField(record, key, fallback = "") {
1065
+ const value = record[key];
1066
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
1067
+ }
1068
+ function arrayField(record, key) {
1069
+ const value = record[key];
1070
+ return Array.isArray(value) ? value.flatMap((entry) => typeof entry === "string" && entry.trim() ? [entry.trim()] : []) : [];
1071
+ }
1072
+ function rawObject(record) {
1073
+ const raw = record.raw;
1074
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
1075
+ }
1076
+ function truncate(value, width) {
1077
+ if (value.length <= width)
1078
+ return value;
1079
+ if (width <= 1)
1080
+ return "\u2026";
1081
+ return `${value.slice(0, width - 1)}\u2026`;
1082
+ }
1083
+ function pad(value, width) {
1084
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
1085
+ }
1086
+ function statusColor(status) {
1087
+ const normalized = status.toLowerCase();
1088
+ if (["completed", "merged", "closed", "done", "accepted"].includes(normalized))
1089
+ return pc.green;
1090
+ if (["failed", "needs_attention", "needs-attention", "blocked"].includes(normalized))
1091
+ return pc.red;
1092
+ if (["running", "reviewing", "validating", "in_progress", "in-progress"].includes(normalized))
1093
+ return pc.cyan;
1094
+ if (["ready", "open", "queued", "created", "preparing"].includes(normalized))
1095
+ return pc.yellow;
1096
+ return pc.dim;
1097
+ }
1098
+ function formatTaskList(tasks, options = {}) {
1099
+ if (tasks.length === 0)
1100
+ return pc.dim("No matching tasks.");
1101
+ if (options.raw)
1102
+ return tasks.map((task) => JSON.stringify(task)).join(`
1103
+ `);
1104
+ const rows = tasks.map((task) => {
1105
+ const raw = rawObject(task);
1106
+ const id = stringField(task, "id", "<unknown>");
1107
+ const status = stringField(task, "status", "unknown");
1108
+ const title = stringField(task, "title", "Untitled task");
1109
+ const source = stringField(task, "source", stringField(raw, "source", ""));
1110
+ const labels = arrayField(task, "labels").length > 0 ? arrayField(task, "labels") : arrayField(raw, "labels");
1111
+ return { id, status, title, source, labels };
1112
+ });
1113
+ const idWidth = Math.min(18, Math.max(4, ...rows.map((row) => row.id.length)));
1114
+ const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
1115
+ const header = `${pc.bold(pad("TASK", idWidth))} ${pc.bold(pad("STATUS", statusWidth))} ${pc.bold("TITLE")}`;
1116
+ const body = rows.map((row) => {
1117
+ const labels = row.labels.length > 0 ? pc.dim(` ${row.labels.slice(0, 4).map((label) => `#${label}`).join(" ")}`) : "";
1118
+ const source = row.source ? pc.dim(` ${row.source}`) : "";
1119
+ return [
1120
+ pc.bold(pad(truncate(row.id, idWidth), idWidth)),
1121
+ statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
1122
+ `${row.title}${labels}${source}`
1123
+ ].join(" ");
1124
+ });
1125
+ return [pc.bold("Rig tasks"), header, ...body].join(`
1126
+ `);
1127
+ }
1128
+ function formatSubmittedRun(input) {
1129
+ const lines = [`${pc.green("Run submitted")}: ${pc.bold(input.runId)}`];
1130
+ if (input.task) {
1131
+ const id = stringField(input.task, "id", "<unknown>");
1132
+ const status = stringField(input.task, "status", "unknown");
1133
+ const title = stringField(input.task, "title", "Untitled task");
1134
+ lines.push(`${pc.dim("task")} ${pc.bold(id)} ${statusColor(status)(status)} ${title}`);
1135
+ }
1136
+ return lines.join(`
1137
+ `);
1138
+ }
1139
+
1140
+ // packages/cli/src/commands/_pi-session.ts
1141
+ import { spawn } from "child_process";
1142
+ function buildPiRigSessionEnv(input) {
1143
+ return {
1144
+ RIG_PROJECT_ROOT: input.projectRoot,
1145
+ PROJECT_RIG_ROOT: input.projectRoot,
1146
+ RIG_RUN_ID: input.runId,
1147
+ RIG_SERVER_RUN_ID: input.runId,
1148
+ RIG_RUNTIME_ADAPTER: "pi",
1149
+ RIG_SERVER_URL: input.serverUrl,
1150
+ RIG_SERVER_BASE_URL: input.serverUrl,
1151
+ RIG_STEERING_POLL_MS: process.env.RIG_STEERING_POLL_MS?.trim() || "1000",
1152
+ RIG_PI_OPERATOR_SESSION: "1",
1153
+ ...input.taskId ? { RIG_TASK_ID: input.taskId } : {},
1154
+ ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {}
1155
+ };
1156
+ }
1157
+ function shellBinary(name) {
1158
+ const explicit = process.env.RIG_PI_BINARY?.trim();
1159
+ if (explicit)
1160
+ return explicit;
1161
+ return Bun.which(name) || name;
1162
+ }
1163
+ function buildPiRigSessionCommand(input) {
1164
+ const configuredExtension = input.extensionSource ?? process.env.RIG_PI_RIG_EXTENSION_SOURCE?.trim();
1165
+ const extensionSource = configuredExtension && configuredExtension.length > 0 ? configuredExtension : resolvePiRigPackageSource(input.projectRoot);
1166
+ const initialCommand = `/rig attach ${input.runId}`;
1167
+ return [
1168
+ shellBinary("pi"),
1169
+ "--no-extensions",
1170
+ "--extension",
1171
+ extensionSource,
1172
+ initialCommand
1173
+ ];
1174
+ }
1175
+ async function launchPiRigSession(context, input) {
1176
+ if (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY) {
1177
+ return { launched: false, exitCode: null, command: [] };
1178
+ }
1179
+ if (process.env.RIG_DISABLE_PI_LAUNCH === "1") {
1180
+ return { launched: false, exitCode: null, command: [] };
1181
+ }
1182
+ const server = await ensureServerForCli(context.projectRoot);
1183
+ const command = buildPiRigSessionCommand({ ...input, projectRoot: context.projectRoot });
1184
+ const env = {
1185
+ ...process.env,
1186
+ ...buildPiRigSessionEnv({
1187
+ projectRoot: context.projectRoot,
1188
+ runId: input.runId,
1189
+ taskId: input.taskId,
1190
+ serverUrl: server.baseUrl,
1191
+ authToken: server.authToken
1192
+ })
1193
+ };
1194
+ process.stdout.write(`Launching Pi for Rig run ${input.runId}\u2026
1195
+ `);
1196
+ process.stdout.write(`Pi command: ${formatCommand(command)}
1197
+ `);
1198
+ const launchedAt = Date.now();
1199
+ const child = spawn(command[0], command.slice(1), {
1200
+ cwd: context.projectRoot,
1201
+ env,
1202
+ stdio: "inherit"
1203
+ });
1204
+ const launchError = await new Promise((resolve4) => {
1205
+ child.once("error", (error) => {
1206
+ resolve4({ error: error.message });
1207
+ });
1208
+ child.once("close", (code) => resolve4({ code }));
1209
+ });
1210
+ if ("error" in launchError) {
1211
+ process.stderr.write(`Failed to launch Pi; falling back to Rig attach view: ${launchError.error}
1212
+ `);
1213
+ return { launched: false, exitCode: null, command, error: launchError.error };
1214
+ }
1215
+ const exitCode = launchError.code;
1216
+ const elapsedMs = Date.now() - launchedAt;
1217
+ if (typeof exitCode === "number" && exitCode !== 0 && elapsedMs < 5000) {
1218
+ const error = `Pi exited during startup with code ${exitCode}.`;
1219
+ process.stderr.write(`${error} Falling back to Rig attach view.
1220
+ `);
1221
+ return { launched: false, exitCode, command, error };
1222
+ }
1223
+ return { launched: true, exitCode, command };
1224
+ }
1225
+
1032
1226
  // packages/cli/src/commands/task.ts
1033
1227
  import { buildPluginHostContext } from "@rig/runtime/control-plane/plugin-host-context";
1034
1228
  import { loadConfig } from "@rig/core/load-config";
@@ -1138,13 +1332,15 @@ async function resolveDirtyBaselineForTaskRun(context, explicit) {
1138
1332
  if (explicit)
1139
1333
  return { mode: explicit, state };
1140
1334
  if (context.outputMode === "text" && process.stdin.isTTY && process.stdout.isTTY) {
1141
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
1142
- try {
1143
- const answer = (await rl.question("Include current uncommitted changes in run baseline? [y/N] ")).trim().toLowerCase();
1144
- return { mode: answer === "y" || answer === "yes" ? "dirty-snapshot" : "head", state };
1145
- } finally {
1146
- rl.close();
1335
+ const answer = await confirm({
1336
+ message: "Include current uncommitted changes in run baseline?",
1337
+ initialValue: false
1338
+ });
1339
+ if (isCancel2(answer)) {
1340
+ cancel2("Run cancelled.");
1341
+ throw new CliError2("Run cancelled by user.", 1);
1147
1342
  }
1343
+ return { mode: answer ? "dirty-snapshot" : "head", state };
1148
1344
  }
1149
1345
  return { mode: "head", state };
1150
1346
  }
@@ -1179,10 +1375,7 @@ function summarizeTask(task, options = {}) {
1179
1375
  };
1180
1376
  }
1181
1377
  function printTaskSummary(task) {
1182
- const id = readTaskId(task) ?? "<unknown>";
1183
- const title = readTaskString(task, "title") ?? "Untitled task";
1184
- const status = readTaskString(task, "status") ?? "unknown";
1185
- console.log(`- ${id} \xB7 ${status} \xB7 ${title}`);
1378
+ console.log(formatTaskList([task]));
1186
1379
  }
1187
1380
  async function validatorRegistryForTaskCommands(projectRoot) {
1188
1381
  return buildPluginHostContext(projectRoot).then((ctx) => ctx?.validatorRegistry ?? undefined).catch(() => {
@@ -1200,16 +1393,8 @@ async function executeTask(context, args, options) {
1200
1393
  requireNoExtraArgs(remaining, "bun run rig task list [--raw] [--assignee <login|@me>] [--assigned-to <login|me|@me>] [--state open|closed] [--status <status>] [--limit <n>]");
1201
1394
  const tasks = await listWorkspaceTasksViaServer(context, filters);
1202
1395
  if (context.outputMode === "text") {
1203
- if (tasks.length === 0) {
1204
- console.log("No matching tasks.");
1205
- } else {
1206
- for (const task of tasks) {
1207
- if (rawResult.value)
1208
- console.log(JSON.stringify(summarizeTask(task, { raw: true })));
1209
- else
1210
- printTaskSummary(task);
1211
- }
1212
- }
1396
+ const renderedTasks = rawResult.value ? tasks.map((task) => summarizeTask(task, { raw: true })) : tasks.map((task) => summarizeTask(task));
1397
+ console.log(formatTaskList(renderedTasks, { raw: rawResult.value }));
1213
1398
  }
1214
1399
  return {
1215
1400
  ok: true,
@@ -1428,16 +1613,23 @@ async function executeTask(context, args, options) {
1428
1613
  });
1429
1614
  let attachDetails = null;
1430
1615
  if (!detachResult.value && context.outputMode === "text") {
1431
- console.log(`Run submitted: ${submitted.runId}`);
1432
- if (selectedTask) {
1433
- printTaskSummary(selectedTask);
1616
+ console.log(formatSubmittedRun({ runId: submitted.runId, task: selectedTask ? summarizeTask(selectedTask) : null }));
1617
+ if (runtimeAdapter === "pi") {
1618
+ const piSession = await launchPiRigSession(context, {
1619
+ runId: submitted.runId,
1620
+ taskId: selectedTaskId,
1621
+ title: titleResult.value ?? readTaskString(selectedTask ?? {}, "title"),
1622
+ runtimeAdapter
1623
+ });
1624
+ attachDetails = { mode: "pi", ...piSession };
1625
+ if (!piSession.launched) {
1626
+ attachDetails = await attachRunOperatorView(context, { runId: submitted.runId, follow: true });
1627
+ }
1628
+ } else {
1629
+ attachDetails = await attachRunOperatorView(context, { runId: submitted.runId, follow: true });
1434
1630
  }
1435
- attachDetails = await attachRunOperatorView(context, { runId: submitted.runId, follow: true });
1436
1631
  } else if (context.outputMode === "text") {
1437
- console.log(`Run submitted: ${submitted.runId}`);
1438
- if (selectedTask) {
1439
- printTaskSummary(selectedTask);
1440
- }
1632
+ console.log(formatSubmittedRun({ runId: submitted.runId, task: selectedTask ? summarizeTask(selectedTask) : null }));
1441
1633
  }
1442
1634
  return {
1443
1635
  ok: true,