@h-rig/cli 0.0.6-alpha.4 → 0.0.6-alpha.41

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.
Files changed (48) hide show
  1. package/dist/bin/rig.js +3905 -1193
  2. package/dist/src/commands/_cli-format.js +369 -0
  3. package/dist/src/commands/_connection-state.js +1 -3
  4. package/dist/src/commands/_doctor-checks.js +13 -27
  5. package/dist/src/commands/_help-catalog.js +445 -0
  6. package/dist/src/commands/_operator-surface.js +220 -0
  7. package/dist/src/commands/_operator-view.js +916 -56
  8. package/dist/src/commands/_parsers.js +0 -2
  9. package/dist/src/commands/_pi-frontend.js +880 -0
  10. package/dist/src/commands/_pi-install.js +4 -3
  11. package/dist/src/commands/_pi-remote-session.js +665 -0
  12. package/dist/src/commands/_pi-worker-bridge-extension.js +676 -0
  13. package/dist/src/commands/_policy.js +0 -2
  14. package/dist/src/commands/_preflight.js +32 -109
  15. package/dist/src/commands/_run-driver-helpers.js +2 -2
  16. package/dist/src/commands/_server-client.js +152 -31
  17. package/dist/src/commands/_snapshot-upload.js +8 -23
  18. package/dist/src/commands/_task-picker.js +44 -16
  19. package/dist/src/commands/agent.js +8 -9
  20. package/dist/src/commands/browser.js +4 -6
  21. package/dist/src/commands/connect.js +132 -25
  22. package/dist/src/commands/dist.js +4 -6
  23. package/dist/src/commands/doctor.js +13 -27
  24. package/dist/src/commands/github.js +10 -25
  25. package/dist/src/commands/inbox.js +351 -31
  26. package/dist/src/commands/init.js +298 -71
  27. package/dist/src/commands/inspect.js +237 -23
  28. package/dist/src/commands/inspector.js +2 -4
  29. package/dist/src/commands/pi.js +168 -0
  30. package/dist/src/commands/plugin.js +81 -22
  31. package/dist/src/commands/profile-and-review.js +8 -10
  32. package/dist/src/commands/queue.js +2 -3
  33. package/dist/src/commands/remote.js +18 -20
  34. package/dist/src/commands/repo-git-harness.js +6 -8
  35. package/dist/src/commands/run.js +1214 -123
  36. package/dist/src/commands/server.js +222 -33
  37. package/dist/src/commands/setup.js +17 -37
  38. package/dist/src/commands/task-report-bug.js +5 -7
  39. package/dist/src/commands/task-run-driver.js +630 -71
  40. package/dist/src/commands/task.js +1653 -252
  41. package/dist/src/commands/test.js +3 -5
  42. package/dist/src/commands/workspace.js +4 -6
  43. package/dist/src/commands.js +3884 -1166
  44. package/dist/src/index.js +3898 -1189
  45. package/dist/src/launcher.js +5 -3
  46. package/dist/src/report-bug.js +3 -3
  47. package/dist/src/runner.js +5 -19
  48. package/package.json +6 -4
@@ -1,14 +1,12 @@
1
1
  // @bun
2
2
  // packages/cli/src/commands/inspect.ts
3
- import { existsSync, readFileSync } from "fs";
4
- import { resolve } from "path";
3
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
4
+ import { resolve as resolve3 } from "path";
5
5
 
6
6
  // packages/cli/src/runner.ts
7
7
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
8
8
  import { CliError } from "@rig/runtime/control-plane/errors";
9
9
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
10
- import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
11
- import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
12
10
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
13
11
  import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
14
12
  function takeOption(args, option) {
@@ -55,19 +53,235 @@ import {
55
53
  import { changedFilesForTask } from "@rig/runtime/control-plane/native/task-ops";
56
54
  import { resolveHarnessPaths, resolveMonorepoRoot, runCapture } from "@rig/runtime/control-plane/native/utils";
57
55
  import { readTaskArtifactPreview } from "@rig/runtime/control-plane/native/workspace-ops";
56
+
57
+ // packages/cli/src/commands/_server-client.ts
58
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
59
+ import { resolve as resolve2 } from "path";
60
+ import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
61
+
62
+ // packages/cli/src/commands/_connection-state.ts
63
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
64
+ import { homedir } from "os";
65
+ import { dirname, resolve } from "path";
66
+ function resolveGlobalConnectionsPath(env = process.env) {
67
+ const explicit = env.RIG_CONNECTIONS_FILE?.trim();
68
+ if (explicit)
69
+ return resolve(explicit);
70
+ const stateDir = env.RIG_GLOBAL_STATE_DIR?.trim();
71
+ if (stateDir)
72
+ return resolve(stateDir, "connections.json");
73
+ return resolve(homedir(), ".rig", "connections.json");
74
+ }
75
+ function resolveRepoConnectionPath(projectRoot) {
76
+ return resolve(projectRoot, ".rig", "state", "connection.json");
77
+ }
78
+ function readJsonFile(path) {
79
+ if (!existsSync(path))
80
+ return null;
81
+ try {
82
+ return JSON.parse(readFileSync(path, "utf8"));
83
+ } catch (error) {
84
+ throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
85
+ }
86
+ }
87
+ function normalizeConnection(value) {
88
+ if (!value || typeof value !== "object" || Array.isArray(value))
89
+ return null;
90
+ const record = value;
91
+ if (record.kind === "local")
92
+ return { kind: "local", mode: "auto" };
93
+ if (record.kind === "remote" && typeof record.baseUrl === "string" && record.baseUrl.trim()) {
94
+ const baseUrl = record.baseUrl.trim().replace(/\/+$/, "");
95
+ return { kind: "remote", baseUrl };
96
+ }
97
+ return null;
98
+ }
99
+ function readGlobalConnections(options = {}) {
100
+ const path = resolveGlobalConnectionsPath(options.env ?? process.env);
101
+ const payload = readJsonFile(path);
102
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
103
+ return { connections: {} };
104
+ }
105
+ const rawConnections = payload.connections;
106
+ const connections = {};
107
+ if (rawConnections && typeof rawConnections === "object" && !Array.isArray(rawConnections)) {
108
+ for (const [alias, raw] of Object.entries(rawConnections)) {
109
+ const connection = normalizeConnection(raw);
110
+ if (connection)
111
+ connections[alias] = connection;
112
+ }
113
+ }
114
+ return { connections };
115
+ }
116
+ function readRepoConnection(projectRoot) {
117
+ const payload = readJsonFile(resolveRepoConnectionPath(projectRoot));
118
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
119
+ return null;
120
+ const record = payload;
121
+ const selected = typeof record.selected === "string" ? record.selected.trim() : "";
122
+ if (!selected)
123
+ return null;
124
+ return {
125
+ selected,
126
+ project: typeof record.project === "string" ? record.project : undefined,
127
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
128
+ };
129
+ }
130
+ function resolveSelectedConnection(projectRoot, options = {}) {
131
+ const repo = readRepoConnection(projectRoot);
132
+ if (!repo)
133
+ return null;
134
+ if (repo.selected === "local")
135
+ return { alias: "local", connection: { kind: "local", mode: "auto" } };
136
+ const global = readGlobalConnections(options);
137
+ const connection = global.connections[repo.selected];
138
+ if (!connection) {
139
+ throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
140
+ }
141
+ return { alias: repo.selected, connection };
142
+ }
143
+
144
+ // packages/cli/src/commands/_server-client.ts
145
+ var scopedGitHubBearerTokens = new Map;
146
+ function cleanToken(value) {
147
+ const trimmed = value?.trim();
148
+ return trimmed ? trimmed : null;
149
+ }
150
+ function readPrivateRemoteSessionToken(projectRoot) {
151
+ const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
152
+ if (!existsSync2(path))
153
+ return null;
154
+ try {
155
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
156
+ return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+ function readGitHubBearerTokenForRemote(projectRoot) {
162
+ const scopedKey = resolve2(projectRoot);
163
+ if (scopedGitHubBearerTokens.has(scopedKey))
164
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
165
+ const privateSession = readPrivateRemoteSessionToken(projectRoot);
166
+ if (privateSession)
167
+ return privateSession;
168
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
169
+ }
170
+ async function ensureServerForCli(projectRoot) {
171
+ try {
172
+ const selected = resolveSelectedConnection(projectRoot);
173
+ if (selected?.connection.kind === "remote") {
174
+ return {
175
+ baseUrl: selected.connection.baseUrl,
176
+ authToken: readGitHubBearerTokenForRemote(projectRoot),
177
+ connectionKind: "remote"
178
+ };
179
+ }
180
+ const connection = await ensureLocalRigServerConnection(projectRoot);
181
+ return {
182
+ baseUrl: connection.baseUrl,
183
+ authToken: connection.authToken,
184
+ connectionKind: "local"
185
+ };
186
+ } catch (error) {
187
+ if (error instanceof Error) {
188
+ throw new CliError2(error.message, 1);
189
+ }
190
+ throw error;
191
+ }
192
+ }
193
+ function mergeHeaders(headers, authToken) {
194
+ const merged = new Headers(headers);
195
+ if (authToken) {
196
+ merged.set("authorization", `Bearer ${authToken}`);
197
+ }
198
+ return merged;
199
+ }
200
+ function diagnosticMessage(payload) {
201
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
202
+ return null;
203
+ const record = payload;
204
+ const diagnostics = Array.isArray(record.diagnostics) ? record.diagnostics : [];
205
+ const messages = diagnostics.flatMap((entry) => {
206
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
207
+ return [];
208
+ const diagnostic = entry;
209
+ const kind = typeof diagnostic.kind === "string" ? diagnostic.kind : "task-source";
210
+ const message = typeof diagnostic.message === "string" ? diagnostic.message : null;
211
+ return message ? [`${kind}: ${message}`] : [];
212
+ });
213
+ return messages.length > 0 ? messages.join("; ") : null;
214
+ }
215
+ async function requestServerJson(context, pathname, init = {}) {
216
+ const server = await ensureServerForCli(context.projectRoot);
217
+ const response = await fetch(`${server.baseUrl}${pathname}`, {
218
+ ...init,
219
+ headers: mergeHeaders(init.headers, server.authToken)
220
+ });
221
+ const text = await response.text();
222
+ const payload = text.trim().length > 0 ? (() => {
223
+ try {
224
+ return JSON.parse(text);
225
+ } catch {
226
+ return null;
227
+ }
228
+ })() : null;
229
+ if (!response.ok) {
230
+ const diagnostics = diagnosticMessage(payload);
231
+ const detail = diagnostics ?? (text || response.statusText);
232
+ throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
233
+ }
234
+ return payload;
235
+ }
236
+ async function listRunsViaServer(context, options = {}) {
237
+ const url = new URL("http://rig.local/api/runs");
238
+ if (options.limit !== undefined)
239
+ url.searchParams.set("limit", String(options.limit));
240
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
241
+ const runs = Array.isArray(payload) ? payload : payload && typeof payload === "object" && !Array.isArray(payload) && Array.isArray(payload.runs) ? payload.runs : [];
242
+ return runs.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
243
+ }
244
+ async function getRunLogsViaServer(context, runId, options = {}) {
245
+ const url = new URL(`http://rig.local/api/runs/${encodeURIComponent(runId)}/logs`);
246
+ if (options.limit !== undefined)
247
+ url.searchParams.set("limit", String(options.limit));
248
+ if (options.cursor)
249
+ url.searchParams.set("cursor", options.cursor);
250
+ const payload = await requestServerJson(context, `${url.pathname}${url.search}`);
251
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { entries: [] };
252
+ }
253
+
254
+ // packages/cli/src/commands/inspect.ts
58
255
  async function executeInspect(context, args) {
59
256
  const [command = "failures", ...rest] = args;
60
257
  switch (command) {
61
258
  case "logs": {
62
259
  const { value: task, rest: remaining } = takeOption(rest, "--task");
63
- requireNoExtraArgs(remaining, "bun run rig inspect logs --task <beads-id>");
64
- const requiredTask = requireTask(task, "bun run rig inspect logs --task <beads-id>");
260
+ requireNoExtraArgs(remaining, "rig inspect logs --task <task-id>");
261
+ const requiredTask = requireTask(task, "rig inspect logs --task <task-id>");
65
262
  const latestRun = listAuthorityRuns(context.projectRoot).map((entry) => readAuthorityRun(context.projectRoot, entry.runId)).filter((run) => Boolean(run)).filter((run) => run.taskId === requiredTask).sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")))[0];
66
263
  if (!latestRun) {
67
- throw new CliError2(`No runs found for ${requiredTask}.`);
264
+ const serverRuns = await listRunsViaServer(context, { limit: 200 }).catch(() => []);
265
+ const serverRun = serverRuns.filter((run) => String(run.taskId ?? "") === requiredTask).sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")))[0];
266
+ if (!serverRun || typeof serverRun.runId !== "string") {
267
+ throw new CliError2(`No runs found for ${requiredTask} (local or on the selected server).`);
268
+ }
269
+ const page = await getRunLogsViaServer(context, serverRun.runId, { limit: 500 });
270
+ const entries = Array.isArray(page.entries) ? page.entries : [];
271
+ if (context.outputMode === "text") {
272
+ for (const entry of entries) {
273
+ const record = entry && typeof entry === "object" ? entry : {};
274
+ const title = String(record.title ?? "");
275
+ const detail = String(record.detail ?? "");
276
+ console.log([title, detail].filter(Boolean).join(" \u2014 "));
277
+ }
278
+ if (entries.length === 0)
279
+ console.log(`(no log entries for run ${serverRun.runId})`);
280
+ }
281
+ return { ok: true, group: "inspect", command, details: { task: requiredTask, runId: serverRun.runId, source: "server", entries } };
68
282
  }
69
- const logsPath = resolve(resolveAuthorityRunDir(context.projectRoot, latestRun.runId), "logs.jsonl");
70
- if (!existsSync(logsPath)) {
283
+ const logsPath = resolve3(resolveAuthorityRunDir(context.projectRoot, latestRun.runId), "logs.jsonl");
284
+ if (!existsSync3(logsPath)) {
71
285
  throw new CliError2(`No logs found for run ${latestRun.runId}.`);
72
286
  }
73
287
  await context.runCommand(["cat", logsPath]);
@@ -75,9 +289,9 @@ async function executeInspect(context, args) {
75
289
  }
76
290
  case "artifacts": {
77
291
  const { value: task, rest: remaining } = takeOption(rest, "--task");
78
- requireNoExtraArgs(remaining, "bun run rig inspect artifacts --task <beads-id>");
79
- const requiredTask = requireTask(task, "bun run rig inspect artifacts --task <beads-id>");
80
- const artifactRoot = resolveTaskArtifactDirs(context.projectRoot, requiredTask).find((path) => existsSync(path));
292
+ requireNoExtraArgs(remaining, "rig inspect artifacts --task <task-id>");
293
+ const requiredTask = requireTask(task, "rig inspect artifacts --task <task-id>");
294
+ const artifactRoot = resolveTaskArtifactDirs(context.projectRoot, requiredTask).find((path) => existsSync3(path));
81
295
  if (!artifactRoot) {
82
296
  throw new CliError2(`No artifacts found for ${requiredTask}.`);
83
297
  }
@@ -90,8 +304,8 @@ async function executeInspect(context, args) {
90
304
  previewPending = task.rest;
91
305
  const file = takeOption(previewPending, "--file");
92
306
  previewPending = file.rest;
93
- requireNoExtraArgs(previewPending, "bun run rig inspect artifact --task <beads-id> --file <name>");
94
- const requiredTask = requireTask(task.value, "bun run rig inspect artifact --task <beads-id> --file <name>");
307
+ requireNoExtraArgs(previewPending, "rig inspect artifact --task <task-id> --file <name>");
308
+ const requiredTask = requireTask(task.value, "rig inspect artifact --task <task-id> --file <name>");
95
309
  if (!file.value) {
96
310
  throw new CliError2("Missing --file for rig inspect artifact.");
97
311
  }
@@ -120,7 +334,7 @@ async function executeInspect(context, args) {
120
334
  }
121
335
  case "diff": {
122
336
  const { value: task, rest: remaining } = takeOption(rest, "--task");
123
- requireNoExtraArgs(remaining, "bun run rig inspect diff [--task <beads-id>]");
337
+ requireNoExtraArgs(remaining, "rig inspect diff [--task <task-id>]");
124
338
  if (task) {
125
339
  const files = changedFilesForTask(context.projectRoot, task, false);
126
340
  for (const file of files) {
@@ -132,17 +346,17 @@ async function executeInspect(context, args) {
132
346
  return { ok: true, group: "inspect", command, details: { task: task || null } };
133
347
  }
134
348
  case "failures": {
135
- requireNoExtraArgs(rest, "bun run rig inspect failures");
349
+ requireNoExtraArgs(rest, "rig inspect failures");
136
350
  const failed = resolveHarnessPaths(context.projectRoot).failedApproachesPath;
137
- if (!existsSync(failed)) {
351
+ if (!existsSync3(failed)) {
138
352
  console.log("No failures recorded.");
139
353
  } else {
140
- process.stdout.write(readFileSync(failed, "utf-8"));
354
+ process.stdout.write(readFileSync3(failed, "utf-8"));
141
355
  }
142
356
  return { ok: true, group: "inspect", command };
143
357
  }
144
358
  case "graph":
145
- requireNoExtraArgs(rest, "bun run rig inspect graph");
359
+ requireNoExtraArgs(rest, "rig inspect graph");
146
360
  {
147
361
  const monorepoRoot = resolveMonorepoRoot(context.projectRoot);
148
362
  const result = runCapture(["br", "--no-db", "list", "--pretty"], monorepoRoot);
@@ -153,12 +367,12 @@ async function executeInspect(context, args) {
153
367
  }
154
368
  return { ok: true, group: "inspect", command };
155
369
  case "audit": {
156
- requireNoExtraArgs(rest, "bun run rig inspect audit");
157
- const auditPath = resolve(resolveHarnessPaths(context.projectRoot).logsDir, "audit.jsonl");
158
- if (!existsSync(auditPath)) {
370
+ requireNoExtraArgs(rest, "rig inspect audit");
371
+ const auditPath = resolve3(resolveHarnessPaths(context.projectRoot).logsDir, "audit.jsonl");
372
+ if (!existsSync3(auditPath)) {
159
373
  console.log("No audit log found.");
160
374
  } else {
161
- const lines = readFileSync(auditPath, "utf-8").split(/\r?\n/).filter(Boolean).slice(-20);
375
+ const lines = readFileSync3(auditPath, "utf-8").split(/\r?\n/).filter(Boolean).slice(-20);
162
376
  for (const line of lines) {
163
377
  console.log(line);
164
378
  }
@@ -6,8 +6,6 @@ import { iterateServerSentEvents } from "@rig/client";
6
6
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
7
7
  import { CliError } from "@rig/runtime/control-plane/errors";
8
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
9
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
12
10
  import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
13
11
  function takeFlag(args, flag) {
@@ -104,7 +102,7 @@ async function executeInspector(context, args) {
104
102
  pending = secondsResult.rest;
105
103
  const pollMsResult = takeOption(pending, "--poll-ms");
106
104
  pending = pollMsResult.rest;
107
- requireNoExtraArgs(pending, "bun run rig inspector stream [--once] [--seconds <n>] [--poll-ms <n>]");
105
+ requireNoExtraArgs(pending, "rig inspector stream [--once] [--seconds <n>] [--poll-ms <n>]");
108
106
  const seconds = secondsResult.value ? parseRequiredPositiveInt(secondsResult.value, "--seconds") : null;
109
107
  const pollMs = pollMsResult.value ? parseRequiredPositiveInt(pollMsResult.value, "--poll-ms") : null;
110
108
  if (context.outputMode === "json" && !onceResult.value && !seconds) {
@@ -198,7 +196,7 @@ async function executeInspector(context, args) {
198
196
  let pending = rest;
199
197
  const scanIdResult = takeOption(pending, "--scan-id");
200
198
  pending = scanIdResult.rest;
201
- requireNoExtraArgs(pending, "bun run rig inspector scan-upstream-drift [--scan-id <id>]");
199
+ requireNoExtraArgs(pending, "rig inspector scan-upstream-drift [--scan-id <id>]");
202
200
  const connection = await ensureLocalRigServerConnection(context.projectRoot);
203
201
  const response = await fetch(new URL("/api/inspector/tools/invoke", connection.baseUrl), {
204
202
  method: "POST",
@@ -0,0 +1,168 @@
1
+ // @bun
2
+ // packages/cli/src/commands/pi.ts
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { dirname, resolve } from "path";
6
+
7
+ // packages/cli/src/runner.ts
8
+ import { EventBus } from "@rig/runtime/control-plane/runtime/events";
9
+ import { CliError } from "@rig/runtime/control-plane/errors";
10
+ import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
11
+ import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
12
+ import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
13
+ function requireNoExtraArgs(args, usage) {
14
+ if (args.length > 0) {
15
+ throw new CliError(`Unexpected arguments: ${args.join(" ")}
16
+ Usage: ${usage}`);
17
+ }
18
+ }
19
+
20
+ // packages/cli/src/commands/pi.ts
21
+ function settingsPath(root) {
22
+ return resolve(root, ".pi", "settings.json");
23
+ }
24
+ function userSettingsPath() {
25
+ return resolve(homedir(), ".pi", "agent", "settings.json");
26
+ }
27
+ function readJson(path, fallback) {
28
+ if (!existsSync(path))
29
+ return fallback;
30
+ try {
31
+ return JSON.parse(readFileSync(path, "utf-8"));
32
+ } catch {
33
+ return fallback;
34
+ }
35
+ }
36
+ function packageKey(entry) {
37
+ if (typeof entry === "string")
38
+ return entry;
39
+ if (entry && typeof entry === "object" && typeof entry.source === "string") {
40
+ return entry.source;
41
+ }
42
+ return JSON.stringify(entry);
43
+ }
44
+ function writeSettings(path, settings) {
45
+ mkdirSync(dirname(path), { recursive: true });
46
+ writeFileSync(path, `${JSON.stringify(settings, null, 2)}
47
+ `, "utf-8");
48
+ }
49
+ async function searchNpmForPiExtensions(term) {
50
+ const query = encodeURIComponent(term ? `${term} pi extension` : "pi extension");
51
+ const url = `https://registry.npmjs.org/-/v1/search?text=${query}&size=20`;
52
+ const response = await fetch(url);
53
+ if (!response.ok) {
54
+ throw new CliError2(`npm registry search failed (${response.status}).`, 2);
55
+ }
56
+ const payload = await response.json();
57
+ const results = [];
58
+ for (const entry of payload.objects ?? []) {
59
+ const pkg = entry.package;
60
+ if (!pkg?.name)
61
+ continue;
62
+ const keywords = (pkg.keywords ?? []).map((k) => k.toLowerCase());
63
+ const piLike = pkg.name.startsWith("pi-") || pkg.name.includes("-pi") || keywords.includes("pi") || keywords.includes("pi-extension") || (pkg.description ?? "").toLowerCase().includes("pi extension") || (pkg.description ?? "").toLowerCase().includes("pi coding agent");
64
+ if (!piLike)
65
+ continue;
66
+ results.push({ name: pkg.name, version: pkg.version ?? "", description: pkg.description ?? "" });
67
+ }
68
+ return results;
69
+ }
70
+ async function executePi(context, args) {
71
+ const [command = "list", ...rest] = args;
72
+ const projectSettingsPath = settingsPath(context.projectRoot);
73
+ const managedRecordPath = resolve(context.projectRoot, ".rig", "state", "pi-managed-packages.json");
74
+ switch (command) {
75
+ case "list": {
76
+ requireNoExtraArgs(rest, "rig pi list");
77
+ const project = readJson(projectSettingsPath, {});
78
+ const managed = new Set(readJson(managedRecordPath, []));
79
+ const user = readJson(userSettingsPath(), {});
80
+ const projectPackages = (Array.isArray(project.packages) ? project.packages : []).map((entry) => ({
81
+ source: packageKey(entry),
82
+ managedByRigConfig: managed.has(packageKey(entry))
83
+ }));
84
+ const userPackages = (Array.isArray(user.packages) ? user.packages : []).map(packageKey);
85
+ if (context.outputMode === "text") {
86
+ console.log("Project Pi packages (.pi/settings.json):");
87
+ if (projectPackages.length === 0)
88
+ console.log(" (none)");
89
+ for (const pkg of projectPackages) {
90
+ console.log(` ${pkg.source}${pkg.managedByRigConfig ? " [from rig.config runtime.pi.packages]" : ""}`);
91
+ }
92
+ console.log("User Pi packages (~/.pi/agent/settings.json):");
93
+ if (userPackages.length === 0)
94
+ console.log(" (none)");
95
+ for (const pkg of userPackages)
96
+ console.log(` ${pkg}`);
97
+ console.log("Add more: `rig pi add <npm-package>` \xB7 discover: `rig pi search <term>`");
98
+ }
99
+ return { ok: true, group: "pi", command, details: { projectPackages, userPackages } };
100
+ }
101
+ case "add": {
102
+ const [source, ...extra] = rest;
103
+ requireNoExtraArgs(extra, "rig pi add <package-source>");
104
+ if (!source) {
105
+ throw new CliError2("Usage: rig pi add <package-source> (npm name, name@version, or git URL)", 2);
106
+ }
107
+ const settings = readJson(projectSettingsPath, {});
108
+ const packages = Array.isArray(settings.packages) ? settings.packages : [];
109
+ if (packages.some((entry) => packageKey(entry) === source)) {
110
+ throw new CliError2(`"${source}" is already in ${projectSettingsPath}.`, 2);
111
+ }
112
+ writeSettings(projectSettingsPath, { ...settings, packages: [...packages, source] });
113
+ if (context.outputMode === "text") {
114
+ console.log(`Added ${source} to ${projectSettingsPath}.`);
115
+ console.log("Pi installs missing packages automatically at the next session start (local and worker).");
116
+ }
117
+ return { ok: true, group: "pi", command, details: { source, settingsPath: projectSettingsPath } };
118
+ }
119
+ case "remove": {
120
+ const [source, ...extra] = rest;
121
+ requireNoExtraArgs(extra, "rig pi remove <package-source>");
122
+ if (!source) {
123
+ throw new CliError2("Usage: rig pi remove <package-source>", 2);
124
+ }
125
+ const managed = new Set(readJson(managedRecordPath, []));
126
+ if (managed.has(source)) {
127
+ throw new CliError2(`"${source}" is managed by rig.config.ts (runtime.pi.packages); remove it there instead.`, 2);
128
+ }
129
+ const settings = readJson(projectSettingsPath, {});
130
+ const packages = Array.isArray(settings.packages) ? settings.packages : [];
131
+ const next = packages.filter((entry) => packageKey(entry) !== source);
132
+ if (next.length === packages.length) {
133
+ throw new CliError2(`"${source}" is not in ${projectSettingsPath}.`, 2);
134
+ }
135
+ const nextSettings = { ...settings };
136
+ if (next.length > 0)
137
+ nextSettings.packages = next;
138
+ else
139
+ delete nextSettings.packages;
140
+ writeSettings(projectSettingsPath, nextSettings);
141
+ if (context.outputMode === "text") {
142
+ console.log(`Removed ${source} from ${projectSettingsPath}.`);
143
+ }
144
+ return { ok: true, group: "pi", command, details: { source } };
145
+ }
146
+ case "search": {
147
+ const term = rest.join(" ").trim();
148
+ const results = await searchNpmForPiExtensions(term);
149
+ if (context.outputMode === "text") {
150
+ if (results.length === 0) {
151
+ console.log(`No Pi extension packages found on npm${term ? ` for "${term}"` : ""}.`);
152
+ } else {
153
+ console.log(`Pi extension packages on npm${term ? ` matching "${term}"` : ""}:`);
154
+ for (const pkg of results) {
155
+ console.log(` ${pkg.name}@${pkg.version} ${pkg.description.slice(0, 80)}`);
156
+ }
157
+ console.log("Install one: `rig pi add <name>`");
158
+ }
159
+ }
160
+ return { ok: true, group: "pi", command, details: { term, results } };
161
+ }
162
+ default:
163
+ throw new CliError2(`Unknown pi command: ${command}. Use list|add|remove|search.`);
164
+ }
165
+ }
166
+ export {
167
+ executePi
168
+ };