@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.
@@ -0,0 +1,253 @@
1
+ // @bun
2
+ // packages/cli/src/commands/_pi-session.ts
3
+ import { spawn } from "child_process";
4
+
5
+ // packages/cli/src/runner.ts
6
+ import { EventBus } from "@rig/runtime/control-plane/runtime/events";
7
+ import { CliError } from "@rig/runtime/control-plane/errors";
8
+ import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
9
+ import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
10
+ import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
11
+ import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
12
+ import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
13
+ function formatCommand(parts) {
14
+ return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
15
+ }
16
+
17
+ // packages/cli/src/commands/_server-client.ts
18
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
19
+ import { resolve as resolve2 } from "path";
20
+ import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
21
+
22
+ // packages/cli/src/commands/_connection-state.ts
23
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
24
+ import { homedir } from "os";
25
+ import { dirname, resolve } from "path";
26
+ function resolveGlobalConnectionsPath(env = process.env) {
27
+ const explicit = env.RIG_CONNECTIONS_FILE?.trim();
28
+ if (explicit)
29
+ return resolve(explicit);
30
+ const stateDir = env.RIG_GLOBAL_STATE_DIR?.trim();
31
+ if (stateDir)
32
+ return resolve(stateDir, "connections.json");
33
+ return resolve(homedir(), ".rig", "connections.json");
34
+ }
35
+ function resolveRepoConnectionPath(projectRoot) {
36
+ return resolve(projectRoot, ".rig", "state", "connection.json");
37
+ }
38
+ function readJsonFile(path) {
39
+ if (!existsSync(path))
40
+ return null;
41
+ try {
42
+ return JSON.parse(readFileSync(path, "utf8"));
43
+ } catch (error) {
44
+ throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
45
+ }
46
+ }
47
+ function normalizeConnection(value) {
48
+ if (!value || typeof value !== "object" || Array.isArray(value))
49
+ return null;
50
+ const record = value;
51
+ if (record.kind === "local")
52
+ return { kind: "local", mode: "auto" };
53
+ if (record.kind === "remote" && typeof record.baseUrl === "string" && record.baseUrl.trim()) {
54
+ const baseUrl = record.baseUrl.trim().replace(/\/+$/, "");
55
+ return { kind: "remote", baseUrl };
56
+ }
57
+ return null;
58
+ }
59
+ function readGlobalConnections(options = {}) {
60
+ const path = resolveGlobalConnectionsPath(options.env ?? process.env);
61
+ const payload = readJsonFile(path);
62
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
63
+ return { connections: {} };
64
+ }
65
+ const rawConnections = payload.connections;
66
+ const connections = {};
67
+ if (rawConnections && typeof rawConnections === "object" && !Array.isArray(rawConnections)) {
68
+ for (const [alias, raw] of Object.entries(rawConnections)) {
69
+ const connection = normalizeConnection(raw);
70
+ if (connection)
71
+ connections[alias] = connection;
72
+ }
73
+ }
74
+ return { connections };
75
+ }
76
+ function readRepoConnection(projectRoot) {
77
+ const payload = readJsonFile(resolveRepoConnectionPath(projectRoot));
78
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
79
+ return null;
80
+ const record = payload;
81
+ const selected = typeof record.selected === "string" ? record.selected.trim() : "";
82
+ if (!selected)
83
+ return null;
84
+ return {
85
+ selected,
86
+ project: typeof record.project === "string" ? record.project : undefined,
87
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
88
+ };
89
+ }
90
+ function resolveSelectedConnection(projectRoot, options = {}) {
91
+ const repo = readRepoConnection(projectRoot);
92
+ if (!repo)
93
+ return null;
94
+ if (repo.selected === "local")
95
+ return { alias: "local", connection: { kind: "local", mode: "auto" } };
96
+ const global = readGlobalConnections(options);
97
+ const connection = global.connections[repo.selected];
98
+ if (!connection) {
99
+ throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
100
+ }
101
+ return { alias: repo.selected, connection };
102
+ }
103
+
104
+ // packages/cli/src/commands/_server-client.ts
105
+ var scopedGitHubBearerTokens = new Map;
106
+ function cleanToken(value) {
107
+ const trimmed = value?.trim();
108
+ return trimmed ? trimmed : null;
109
+ }
110
+ function readPrivateRemoteSessionToken(projectRoot) {
111
+ const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
112
+ if (!existsSync2(path))
113
+ return null;
114
+ try {
115
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
116
+ return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+ function readGitHubBearerTokenForRemote(projectRoot) {
122
+ const scopedKey = resolve2(projectRoot);
123
+ if (scopedGitHubBearerTokens.has(scopedKey))
124
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
125
+ const privateSession = readPrivateRemoteSessionToken(projectRoot);
126
+ if (privateSession)
127
+ return privateSession;
128
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
129
+ }
130
+ async function ensureServerForCli(projectRoot) {
131
+ try {
132
+ const selected = resolveSelectedConnection(projectRoot);
133
+ if (selected?.connection.kind === "remote") {
134
+ return {
135
+ baseUrl: selected.connection.baseUrl,
136
+ authToken: readGitHubBearerTokenForRemote(projectRoot),
137
+ connectionKind: "remote"
138
+ };
139
+ }
140
+ const connection = await ensureLocalRigServerConnection(projectRoot);
141
+ return {
142
+ baseUrl: connection.baseUrl,
143
+ authToken: connection.authToken,
144
+ connectionKind: "local"
145
+ };
146
+ } catch (error) {
147
+ if (error instanceof Error) {
148
+ throw new CliError2(error.message, 1);
149
+ }
150
+ throw error;
151
+ }
152
+ }
153
+
154
+ // packages/cli/src/commands/_pi-install.ts
155
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
156
+ import { resolve as resolve3 } from "path";
157
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
158
+ function resolvePiRigPackageSource(projectRoot, exists = existsSync3) {
159
+ const localPackage = resolve3(projectRoot, "packages", "pi-rig");
160
+ if (exists(resolve3(localPackage, "package.json")))
161
+ return localPackage;
162
+ return `npm:${PI_RIG_PACKAGE_NAME}`;
163
+ }
164
+
165
+ // packages/cli/src/commands/_pi-session.ts
166
+ function buildPiRigSessionEnv(input) {
167
+ return {
168
+ RIG_PROJECT_ROOT: input.projectRoot,
169
+ PROJECT_RIG_ROOT: input.projectRoot,
170
+ RIG_RUN_ID: input.runId,
171
+ RIG_SERVER_RUN_ID: input.runId,
172
+ RIG_RUNTIME_ADAPTER: "pi",
173
+ RIG_SERVER_URL: input.serverUrl,
174
+ RIG_SERVER_BASE_URL: input.serverUrl,
175
+ RIG_STEERING_POLL_MS: process.env.RIG_STEERING_POLL_MS?.trim() || "1000",
176
+ RIG_PI_OPERATOR_SESSION: "1",
177
+ ...input.taskId ? { RIG_TASK_ID: input.taskId } : {},
178
+ ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {}
179
+ };
180
+ }
181
+ function shellBinary(name) {
182
+ const explicit = process.env.RIG_PI_BINARY?.trim();
183
+ if (explicit)
184
+ return explicit;
185
+ return Bun.which(name) || name;
186
+ }
187
+ function buildPiRigSessionCommand(input) {
188
+ const configuredExtension = input.extensionSource ?? process.env.RIG_PI_RIG_EXTENSION_SOURCE?.trim();
189
+ const extensionSource = configuredExtension && configuredExtension.length > 0 ? configuredExtension : resolvePiRigPackageSource(input.projectRoot);
190
+ const initialCommand = `/rig attach ${input.runId}`;
191
+ return [
192
+ shellBinary("pi"),
193
+ "--no-extensions",
194
+ "--extension",
195
+ extensionSource,
196
+ initialCommand
197
+ ];
198
+ }
199
+ async function launchPiRigSession(context, input) {
200
+ if (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY) {
201
+ return { launched: false, exitCode: null, command: [] };
202
+ }
203
+ if (process.env.RIG_DISABLE_PI_LAUNCH === "1") {
204
+ return { launched: false, exitCode: null, command: [] };
205
+ }
206
+ const server = await ensureServerForCli(context.projectRoot);
207
+ const command = buildPiRigSessionCommand({ ...input, projectRoot: context.projectRoot });
208
+ const env = {
209
+ ...process.env,
210
+ ...buildPiRigSessionEnv({
211
+ projectRoot: context.projectRoot,
212
+ runId: input.runId,
213
+ taskId: input.taskId,
214
+ serverUrl: server.baseUrl,
215
+ authToken: server.authToken
216
+ })
217
+ };
218
+ process.stdout.write(`Launching Pi for Rig run ${input.runId}\u2026
219
+ `);
220
+ process.stdout.write(`Pi command: ${formatCommand(command)}
221
+ `);
222
+ const launchedAt = Date.now();
223
+ const child = spawn(command[0], command.slice(1), {
224
+ cwd: context.projectRoot,
225
+ env,
226
+ stdio: "inherit"
227
+ });
228
+ const launchError = await new Promise((resolve4) => {
229
+ child.once("error", (error) => {
230
+ resolve4({ error: error.message });
231
+ });
232
+ child.once("close", (code) => resolve4({ code }));
233
+ });
234
+ if ("error" in launchError) {
235
+ process.stderr.write(`Failed to launch Pi; falling back to Rig attach view: ${launchError.error}
236
+ `);
237
+ return { launched: false, exitCode: null, command, error: launchError.error };
238
+ }
239
+ const exitCode = launchError.code;
240
+ const elapsedMs = Date.now() - launchedAt;
241
+ if (typeof exitCode === "number" && exitCode !== 0 && elapsedMs < 5000) {
242
+ const error = `Pi exited during startup with code ${exitCode}.`;
243
+ process.stderr.write(`${error} Falling back to Rig attach view.
244
+ `);
245
+ return { launched: false, exitCode, command, error };
246
+ }
247
+ return { launched: true, exitCode, command };
248
+ }
249
+ export {
250
+ launchPiRigSession,
251
+ buildPiRigSessionEnv,
252
+ buildPiRigSessionCommand
253
+ };
@@ -193,7 +193,8 @@ async function requestServerJson(context, pathname, init = {}) {
193
193
  import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
194
194
  import { homedir as homedir2 } from "os";
195
195
  import { resolve as resolve3 } from "path";
196
- var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
196
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
197
+ var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
197
198
  async function defaultCommandRunner(command, options = {}) {
198
199
  const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
199
200
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -212,7 +213,7 @@ function resolvePiHomeDir(inputHomeDir) {
212
213
  function piListContainsPiRig(output) {
213
214
  return output.split(/\r?\n/).some((line) => {
214
215
  const normalized = line.trim();
215
- return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
216
+ return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
216
217
  });
217
218
  }
218
219
  async function safeRun(runner, command, options) {
@@ -1,4 +1,7 @@
1
1
  // @bun
2
+ // packages/cli/src/commands/_task-picker.ts
3
+ import { cancel, isCancel, select } from "@clack/prompts";
4
+
2
5
  // packages/cli/src/commands/_operator-surface.ts
3
6
  import { createInterface as createPromptInterface } from "readline/promises";
4
7
  function taskId(task) {
@@ -35,20 +38,37 @@ async function selectTaskWithTextPicker(tasks, io = {}) {
35
38
  if (!isTty) {
36
39
  throw new Error("task run requires an interactive terminal to pick a task; pass --task <id>, --next, or --detach with a task id.");
37
40
  }
38
- const prompt = io.prompt ?? promptForTaskSelection;
39
- const renderer = io.renderer ?? { writeLine: (line) => process.stdout.write(`${line}
41
+ if (io.prompt || io.renderer) {
42
+ const prompt = io.prompt ?? promptForTaskSelection;
43
+ const renderer = io.renderer ?? { writeLine: (line) => process.stdout.write(`${line}
40
44
  `) };
41
- renderer.writeLine("Select Rig task:");
42
- for (const row of renderTaskPickerRows(tasks))
43
- renderer.writeLine(` ${row}`);
44
- const answer = (await prompt(`Task [1-${tasks.length}] or id: `)).trim();
45
- if (!answer)
45
+ renderer.writeLine("Select Rig task:");
46
+ for (const row of renderTaskPickerRows(tasks))
47
+ renderer.writeLine(` ${row}`);
48
+ const answer2 = (await prompt(`Task [1-${tasks.length}] or id: `)).trim();
49
+ if (!answer2)
50
+ return null;
51
+ if (/^\d+$/.test(answer2)) {
52
+ const index2 = Number.parseInt(answer2, 10) - 1;
53
+ return tasks[index2] ?? null;
54
+ }
55
+ return tasks.find((task) => taskId2(task) === answer2) ?? null;
56
+ }
57
+ const options = tasks.map((task, index2) => ({
58
+ value: `${index2}`,
59
+ label: `${taskId2(task)} \xB7 ${typeof task.title === "string" && task.title.trim() ? task.title.trim() : "Untitled task"}`,
60
+ hint: typeof task.status === "string" && task.status.trim() ? task.status.trim() : undefined
61
+ }));
62
+ const answer = await select({
63
+ message: "Select Rig task",
64
+ options
65
+ });
66
+ if (isCancel(answer)) {
67
+ cancel("No task selected.");
46
68
  return null;
47
- if (/^\d+$/.test(answer)) {
48
- const index = Number.parseInt(answer, 10) - 1;
49
- return tasks[index] ?? null;
50
69
  }
51
- return tasks.find((task) => taskId2(task) === answer) ?? null;
70
+ const index = Number.parseInt(String(answer), 10);
71
+ return Number.isFinite(index) ? tasks[index] ?? null : null;
52
72
  }
53
73
  export {
54
74
  selectTaskWithTextPicker,
@@ -213,7 +213,8 @@ async function loadRigConfigOrNull(projectRoot) {
213
213
  import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
214
214
  import { homedir as homedir2 } from "os";
215
215
  import { resolve as resolve3 } from "path";
216
- var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
216
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
217
+ var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
217
218
  async function defaultCommandRunner(command, options = {}) {
218
219
  const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
219
220
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -232,7 +233,7 @@ function resolvePiHomeDir(inputHomeDir) {
232
233
  function piListContainsPiRig(output) {
233
234
  return output.split(/\r?\n/).some((line) => {
234
235
  const normalized = line.trim();
235
- return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
236
+ return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
236
237
  });
237
238
  }
238
239
  async function safeRun(runner, command, options) {
@@ -345,7 +345,8 @@ async function getGitHubProjectStatusFieldViaServer(context, projectId) {
345
345
  import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
346
346
  import { homedir as homedir2 } from "os";
347
347
  import { resolve as resolve3 } from "path";
348
- var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
348
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
349
+ var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
349
350
  var LEGACY_PI_RIG_MARKER = `// Managed by Rig. Source package: @rig/pi-rig.
350
351
  export { default } from '@rig/pi-rig';
351
352
  `;
@@ -373,7 +374,7 @@ function resolvePiHomeDir(inputHomeDir) {
373
374
  function piListContainsPiRig(output) {
374
375
  return output.split(/\r?\n/).some((line) => {
375
376
  const normalized = line.trim();
376
- return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
377
+ return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
377
378
  });
378
379
  }
379
380
  async function safeRun(runner, command, options) {
@@ -489,7 +490,7 @@ async function ensureRemotePiRigInstalled(input) {
489
490
  const payload = await input.requestJson("/api/pi-rig/install", {
490
491
  method: "POST",
491
492
  headers: { "content-type": "application/json" },
492
- body: JSON.stringify({ package: "@rig/pi-rig", scope: "global" })
493
+ body: JSON.stringify({ package: PI_RIG_PACKAGE_NAME, scope: "global" })
493
494
  });
494
495
  const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
495
496
  const piOk = record.piOk === true || record.ok === true;
@@ -10,6 +10,9 @@ import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
10
10
  import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
11
11
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
12
12
  import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
13
+ function formatCommand(parts) {
14
+ return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
15
+ }
13
16
  function takeFlag(args, flag) {
14
17
  const rest = [];
15
18
  let value = false;
@@ -567,6 +570,157 @@ async function attachRunOperatorView(context, input) {
567
570
  return { ...snapshot, steered, detached };
568
571
  }
569
572
 
573
+ // packages/cli/src/commands/_cli-format.ts
574
+ import pc from "picocolors";
575
+ function stringField(record, key, fallback = "") {
576
+ const value = record[key];
577
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
578
+ }
579
+ function truncate(value, width) {
580
+ if (value.length <= width)
581
+ return value;
582
+ if (width <= 1)
583
+ return "\u2026";
584
+ return `${value.slice(0, width - 1)}\u2026`;
585
+ }
586
+ function pad(value, width) {
587
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
588
+ }
589
+ function statusColor(status) {
590
+ const normalized = status.toLowerCase();
591
+ if (["completed", "merged", "closed", "done", "accepted"].includes(normalized))
592
+ return pc.green;
593
+ if (["failed", "needs_attention", "needs-attention", "blocked"].includes(normalized))
594
+ return pc.red;
595
+ if (["running", "reviewing", "validating", "in_progress", "in-progress"].includes(normalized))
596
+ return pc.cyan;
597
+ if (["ready", "open", "queued", "created", "preparing"].includes(normalized))
598
+ return pc.yellow;
599
+ return pc.dim;
600
+ }
601
+ function formatRunList(runs, options = {}) {
602
+ if (runs.length === 0) {
603
+ return pc.dim(options.source === "server" ? "No runs recorded on the selected Rig server." : "No runs recorded in .rig/runs.");
604
+ }
605
+ const rows = runs.map((run) => {
606
+ const runId = stringField(run, "runId", stringField(run, "id", "(unknown-run)"));
607
+ const status = stringField(run, "status", "unknown");
608
+ const taskId = stringField(run, "taskId", "");
609
+ const title = stringField(run, "title", taskId || "(untitled)");
610
+ const runtime = stringField(run, "runtimeAdapter", "");
611
+ return { runId, status, title, runtime };
612
+ });
613
+ const idWidth = Math.min(36, Math.max(6, ...rows.map((row) => row.runId.length)));
614
+ const statusWidth = Math.min(16, Math.max(6, ...rows.map((row) => row.status.length)));
615
+ const header = `${pc.bold(pad("RUN", idWidth))} ${pc.bold(pad("STATUS", statusWidth))} ${pc.bold("TITLE")}`;
616
+ const body = rows.map((row) => [
617
+ pc.bold(pad(truncate(row.runId, idWidth), idWidth)),
618
+ statusColor(row.status)(pad(truncate(row.status, statusWidth), statusWidth)),
619
+ `${row.title}${row.runtime ? pc.dim(` ${row.runtime}`) : ""}`
620
+ ].join(" "));
621
+ return [pc.bold(options.source === "server" ? "Rig runs (server)" : "Rig runs"), header, ...body].join(`
622
+ `);
623
+ }
624
+
625
+ // packages/cli/src/commands/_pi-session.ts
626
+ import { spawn } from "child_process";
627
+
628
+ // packages/cli/src/commands/_pi-install.ts
629
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
630
+ import { resolve as resolve3 } from "path";
631
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
632
+ function resolvePiRigPackageSource(projectRoot, exists = existsSync3) {
633
+ const localPackage = resolve3(projectRoot, "packages", "pi-rig");
634
+ if (exists(resolve3(localPackage, "package.json")))
635
+ return localPackage;
636
+ return `npm:${PI_RIG_PACKAGE_NAME}`;
637
+ }
638
+
639
+ // packages/cli/src/commands/_pi-session.ts
640
+ function buildPiRigSessionEnv(input) {
641
+ return {
642
+ RIG_PROJECT_ROOT: input.projectRoot,
643
+ PROJECT_RIG_ROOT: input.projectRoot,
644
+ RIG_RUN_ID: input.runId,
645
+ RIG_SERVER_RUN_ID: input.runId,
646
+ RIG_RUNTIME_ADAPTER: "pi",
647
+ RIG_SERVER_URL: input.serverUrl,
648
+ RIG_SERVER_BASE_URL: input.serverUrl,
649
+ RIG_STEERING_POLL_MS: process.env.RIG_STEERING_POLL_MS?.trim() || "1000",
650
+ RIG_PI_OPERATOR_SESSION: "1",
651
+ ...input.taskId ? { RIG_TASK_ID: input.taskId } : {},
652
+ ...input.authToken ? { RIG_AUTH_TOKEN: input.authToken } : {}
653
+ };
654
+ }
655
+ function shellBinary(name) {
656
+ const explicit = process.env.RIG_PI_BINARY?.trim();
657
+ if (explicit)
658
+ return explicit;
659
+ return Bun.which(name) || name;
660
+ }
661
+ function buildPiRigSessionCommand(input) {
662
+ const configuredExtension = input.extensionSource ?? process.env.RIG_PI_RIG_EXTENSION_SOURCE?.trim();
663
+ const extensionSource = configuredExtension && configuredExtension.length > 0 ? configuredExtension : resolvePiRigPackageSource(input.projectRoot);
664
+ const initialCommand = `/rig attach ${input.runId}`;
665
+ return [
666
+ shellBinary("pi"),
667
+ "--no-extensions",
668
+ "--extension",
669
+ extensionSource,
670
+ initialCommand
671
+ ];
672
+ }
673
+ async function launchPiRigSession(context, input) {
674
+ if (context.outputMode !== "text" || !process.stdin.isTTY || !process.stdout.isTTY) {
675
+ return { launched: false, exitCode: null, command: [] };
676
+ }
677
+ if (process.env.RIG_DISABLE_PI_LAUNCH === "1") {
678
+ return { launched: false, exitCode: null, command: [] };
679
+ }
680
+ const server = await ensureServerForCli(context.projectRoot);
681
+ const command = buildPiRigSessionCommand({ ...input, projectRoot: context.projectRoot });
682
+ const env = {
683
+ ...process.env,
684
+ ...buildPiRigSessionEnv({
685
+ projectRoot: context.projectRoot,
686
+ runId: input.runId,
687
+ taskId: input.taskId,
688
+ serverUrl: server.baseUrl,
689
+ authToken: server.authToken
690
+ })
691
+ };
692
+ process.stdout.write(`Launching Pi for Rig run ${input.runId}\u2026
693
+ `);
694
+ process.stdout.write(`Pi command: ${formatCommand(command)}
695
+ `);
696
+ const launchedAt = Date.now();
697
+ const child = spawn(command[0], command.slice(1), {
698
+ cwd: context.projectRoot,
699
+ env,
700
+ stdio: "inherit"
701
+ });
702
+ const launchError = await new Promise((resolve4) => {
703
+ child.once("error", (error) => {
704
+ resolve4({ error: error.message });
705
+ });
706
+ child.once("close", (code) => resolve4({ code }));
707
+ });
708
+ if ("error" in launchError) {
709
+ process.stderr.write(`Failed to launch Pi; falling back to Rig attach view: ${launchError.error}
710
+ `);
711
+ return { launched: false, exitCode: null, command, error: launchError.error };
712
+ }
713
+ const exitCode = launchError.code;
714
+ const elapsedMs = Date.now() - launchedAt;
715
+ if (typeof exitCode === "number" && exitCode !== 0 && elapsedMs < 5000) {
716
+ const error = `Pi exited during startup with code ${exitCode}.`;
717
+ process.stderr.write(`${error} Falling back to Rig attach view.
718
+ `);
719
+ return { launched: false, exitCode, command, error };
720
+ }
721
+ return { launched: true, exitCode, command };
722
+ }
723
+
570
724
  // packages/cli/src/commands/run.ts
571
725
  function normalizeRemoteRunDetails(payload) {
572
726
  const run = payload.run;
@@ -668,13 +822,7 @@ async function executeRun(context, args) {
668
822
  requireNoExtraArgs(rest, "bun run rig run list");
669
823
  const { runs, source } = await listRunsForSelectedConnection(context, { limit: 100 });
670
824
  if (context.outputMode === "text") {
671
- if (runs.length === 0) {
672
- console.log(source === "server" ? "No runs recorded on the selected Rig server." : "No runs recorded in .rig/runs.");
673
- } else {
674
- for (const run of runs) {
675
- console.log(`- ${runStringField(run, "runId", "(unknown-run)")} \xB7 ${runStringField(run, "status", "unknown")} \xB7 ${runDisplayTitle(run)}`);
676
- }
677
- }
825
+ console.log(formatRunList(runs, { source }));
678
826
  }
679
827
  return { ok: true, group: "run", command, details: { runs, source } };
680
828
  }
@@ -796,14 +944,26 @@ async function executeRun(context, args) {
796
944
  if (!runId) {
797
945
  throw new CliError2("run attach requires a run id.", 2);
798
946
  }
947
+ let steered = false;
948
+ const shouldTryPiAttach = context.outputMode === "text" && follow.value && !once.value && Boolean(process.stdin.isTTY && process.stdout.isTTY) && process.env.RIG_DISABLE_PI_LAUNCH !== "1";
949
+ if (shouldTryPiAttach && messageOption.value?.trim()) {
950
+ await steerRunViaServer(context, runId, messageOption.value.trim());
951
+ steered = true;
952
+ }
953
+ if (shouldTryPiAttach) {
954
+ const piSession = await launchPiRigSession(context, { runId });
955
+ if (piSession.launched) {
956
+ return { ok: true, group: "run", command, details: { runId, steered, mode: "pi", ...piSession } };
957
+ }
958
+ }
799
959
  const attached = await attachRunOperatorView(context, {
800
960
  runId,
801
- message: messageOption.value ?? null,
961
+ message: shouldTryPiAttach ? null : messageOption.value ?? null,
802
962
  once: once.value,
803
963
  follow: follow.value,
804
964
  pollMs: parsePositiveInt(pollMs.value, "--poll-ms", 2000)
805
965
  });
806
- return { ok: true, group: "run", command, details: attached };
966
+ return { ok: true, group: "run", command, details: { ...attached, steered: attached.steered || steered } };
807
967
  }
808
968
  case "status": {
809
969
  requireNoExtraArgs(rest, "bun run rig run status");
@@ -102,7 +102,8 @@ function resolveControlPlaneDefinitionRoot(projectRoot) {
102
102
  import { existsSync, readFileSync, rmSync } from "fs";
103
103
  import { homedir } from "os";
104
104
  import { resolve as resolve2 } from "path";
105
- var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
105
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
106
+ var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
106
107
  async function defaultCommandRunner(command, options = {}) {
107
108
  const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
108
109
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -121,7 +122,7 @@ function resolvePiHomeDir(inputHomeDir) {
121
122
  function piListContainsPiRig(output) {
122
123
  return output.split(/\r?\n/).some((line) => {
123
124
  const normalized = line.trim();
124
- return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
125
+ return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
125
126
  });
126
127
  }
127
128
  async function safeRun(runner, command, options) {