@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.
- package/dist/bin/rig.js +266 -56
- package/dist/src/commands/_cli-format.js +106 -0
- package/dist/src/commands/_doctor-checks.js +3 -2
- package/dist/src/commands/_pi-install.js +4 -3
- package/dist/src/commands/_pi-session.js +253 -0
- package/dist/src/commands/_preflight.js +3 -2
- package/dist/src/commands/_task-picker.js +31 -11
- package/dist/src/commands/doctor.js +3 -2
- package/dist/src/commands/init.js +4 -3
- package/dist/src/commands/run.js +169 -9
- package/dist/src/commands/setup.js +3 -2
- package/dist/src/commands/task.js +234 -42
- package/dist/src/commands.js +266 -56
- package/dist/src/index.js +266 -56
- package/package.json +5 -5
|
@@ -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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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:
|
|
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;
|
package/dist/src/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|