@h-rig/cli 0.0.6-alpha.5 → 0.0.6-alpha.50

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 (49) hide show
  1. package/dist/bin/rig.js +4072 -1204
  2. package/dist/src/commands/_cli-format.js +369 -0
  3. package/dist/src/commands/_connection-state.js +12 -6
  4. package/dist/src/commands/_doctor-checks.js +65 -34
  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 +1109 -63
  8. package/dist/src/commands/_parsers.js +0 -2
  9. package/dist/src/commands/_pi-frontend.js +1066 -0
  10. package/dist/src/commands/_pi-install.js +4 -3
  11. package/dist/src/commands/_pi-remote-session.js +757 -0
  12. package/dist/src/commands/_pi-worker-bridge-extension.js +820 -0
  13. package/dist/src/commands/_policy.js +0 -2
  14. package/dist/src/commands/_preflight.js +84 -116
  15. package/dist/src/commands/_run-driver-helpers.js +2 -2
  16. package/dist/src/commands/_server-client.js +211 -48
  17. package/dist/src/commands/_snapshot-upload.js +60 -30
  18. package/dist/src/commands/_spinner.js +63 -0
  19. package/dist/src/commands/_task-picker.js +44 -16
  20. package/dist/src/commands/agent.js +8 -9
  21. package/dist/src/commands/browser.js +4 -6
  22. package/dist/src/commands/connect.js +134 -26
  23. package/dist/src/commands/dist.js +4 -6
  24. package/dist/src/commands/doctor.js +65 -34
  25. package/dist/src/commands/github.js +62 -32
  26. package/dist/src/commands/inbox.js +396 -31
  27. package/dist/src/commands/init.js +342 -88
  28. package/dist/src/commands/inspect.js +282 -23
  29. package/dist/src/commands/inspector.js +2 -4
  30. package/dist/src/commands/pi.js +168 -0
  31. package/dist/src/commands/plugin.js +81 -22
  32. package/dist/src/commands/profile-and-review.js +8 -10
  33. package/dist/src/commands/queue.js +2 -3
  34. package/dist/src/commands/remote.js +18 -20
  35. package/dist/src/commands/repo-git-harness.js +6 -8
  36. package/dist/src/commands/run.js +1407 -130
  37. package/dist/src/commands/server.js +266 -40
  38. package/dist/src/commands/setup.js +69 -44
  39. package/dist/src/commands/task-report-bug.js +5 -7
  40. package/dist/src/commands/task-run-driver.js +662 -70
  41. package/dist/src/commands/task.js +1846 -259
  42. package/dist/src/commands/test.js +3 -5
  43. package/dist/src/commands/workspace.js +4 -6
  44. package/dist/src/commands.js +4054 -1180
  45. package/dist/src/index.js +4065 -1200
  46. package/dist/src/launcher.js +5 -3
  47. package/dist/src/report-bug.js +3 -3
  48. package/dist/src/runner.js +5 -19
  49. package/package.json +7 -4
@@ -3,8 +3,6 @@
3
3
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
4
4
  import { CliError } from "@rig/runtime/control-plane/errors";
5
5
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
6
- import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
7
- import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
8
6
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
9
7
  import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
10
8
  function takeOption(args, option) {
@@ -34,6 +32,9 @@ Usage: ${usage}`);
34
32
  }
35
33
  }
36
34
 
35
+ // packages/cli/src/commands/server.ts
36
+ import { resolveRigServerCommand } from "@rig/runtime/local-server";
37
+
37
38
  // packages/cli/src/commands/_authority-runs.ts
38
39
  import {
39
40
  readAuthorityRun,
@@ -60,11 +61,8 @@ function normalizeRuntimeAdapter(value) {
60
61
  return "claude-code";
61
62
  }
62
63
 
63
- // packages/cli/src/commands/_server-client.ts
64
- import { spawnSync } from "child_process";
65
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
66
- import { resolve as resolve2 } from "path";
67
- import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
64
+ // packages/cli/src/commands/connect.ts
65
+ import { cancel, isCancel, select } from "@clack/prompts";
68
66
 
69
67
  // packages/cli/src/commands/_connection-state.ts
70
68
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -91,6 +89,11 @@ function readJsonFile(path) {
91
89
  throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
92
90
  }
93
91
  }
92
+ function writeJsonFile2(path, value) {
93
+ mkdirSync(dirname(path), { recursive: true });
94
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
95
+ `, "utf8");
96
+ }
94
97
  function normalizeConnection(value) {
95
98
  if (!value || typeof value !== "object" || Array.isArray(value))
96
99
  return null;
@@ -120,6 +123,18 @@ function readGlobalConnections(options = {}) {
120
123
  }
121
124
  return { connections };
122
125
  }
126
+ function writeGlobalConnections(state, options = {}) {
127
+ writeJsonFile2(resolveGlobalConnectionsPath(options.env ?? process.env), state);
128
+ }
129
+ function upsertGlobalConnection(alias, connection, options = {}) {
130
+ const cleanAlias = alias.trim();
131
+ if (!cleanAlias)
132
+ throw new CliError2("Connection alias is required.", 1);
133
+ const state = readGlobalConnections(options);
134
+ state.connections[cleanAlias] = connection;
135
+ writeGlobalConnections(state, options);
136
+ return state;
137
+ }
123
138
  function readRepoConnection(projectRoot) {
124
139
  const payload = readJsonFile(resolveRepoConnectionPath(projectRoot));
125
140
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
@@ -131,25 +146,213 @@ function readRepoConnection(projectRoot) {
131
146
  return {
132
147
  selected,
133
148
  project: typeof record.project === "string" ? record.project : undefined,
134
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
149
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
150
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
135
151
  };
136
152
  }
153
+ function writeRepoConnection(projectRoot, state) {
154
+ writeJsonFile2(resolveRepoConnectionPath(projectRoot), state);
155
+ }
137
156
  function resolveSelectedConnection(projectRoot, options = {}) {
138
157
  const repo = readRepoConnection(projectRoot);
139
158
  if (!repo)
140
159
  return null;
141
160
  if (repo.selected === "local")
142
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
161
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
143
162
  const global = readGlobalConnections(options);
144
163
  const connection = global.connections[repo.selected];
145
164
  if (!connection) {
146
- throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
165
+ throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
166
+ }
167
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
168
+ }
169
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
170
+ const repo = readRepoConnection(projectRoot);
171
+ if (!repo)
172
+ return;
173
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
174
+ }
175
+
176
+ // packages/cli/src/commands/_cli-format.ts
177
+ import { log, note } from "@clack/prompts";
178
+ import pc from "picocolors";
179
+ function truncate(value, width) {
180
+ if (value.length <= width)
181
+ return value;
182
+ if (width <= 1)
183
+ return "\u2026";
184
+ return `${value.slice(0, width - 1)}\u2026`;
185
+ }
186
+ function pad(value, width) {
187
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
188
+ }
189
+ function statusColor(status) {
190
+ const normalized = status.toLowerCase();
191
+ if (["completed", "merged", "closed", "done", "accepted", "pass", "selected", "approved"].includes(normalized))
192
+ return pc.green;
193
+ if (["failed", "needs_attention", "needs-attention", "blocked", "error", "rejected"].includes(normalized))
194
+ return pc.red;
195
+ if (["running", "reviewing", "validating", "in_progress", "in-progress", "remote"].includes(normalized))
196
+ return pc.cyan;
197
+ if (["ready", "open", "queued", "created", "preparing", "local", "pending"].includes(normalized))
198
+ return pc.yellow;
199
+ return pc.dim;
200
+ }
201
+ function formatStatusPill(status) {
202
+ const label = status || "unknown";
203
+ return statusColor(label)(`\u25CF ${label}`);
204
+ }
205
+ function formatSection(title, subtitle) {
206
+ return `${pc.bold(pc.cyan("\u25C6"))} ${pc.bold(title)}${subtitle ? pc.dim(` \u2014 ${subtitle}`) : ""}`;
207
+ }
208
+ function formatSuccessCard(title, rows = []) {
209
+ const body = rows.filter(([, value]) => value !== undefined && value !== null && String(value).length > 0).map(([key, value]) => `${pc.dim("\u2502")} ${pc.dim(key.padEnd(12))} ${value}`);
210
+ return [formatSection(title), ...body].join(`
211
+ `);
212
+ }
213
+ function formatNextSteps(steps) {
214
+ if (steps.length === 0)
215
+ return [];
216
+ return [pc.bold("Next"), ...steps.map((step) => `${pc.dim("\u203A")} ${step}`)];
217
+ }
218
+ function formatConnectionList(connections) {
219
+ const rows = [["local", { kind: "local", mode: "auto" }], ...Object.entries(connections)];
220
+ const aliasWidth = Math.min(24, Math.max(5, ...rows.map(([alias]) => alias.length)));
221
+ const lines = rows.map(([alias, connection]) => [
222
+ pc.bold(pad(truncate(alias, aliasWidth), aliasWidth)),
223
+ formatStatusPill(connection.kind),
224
+ connection.kind === "remote" ? connection.baseUrl ?? "" : connection.mode ?? "local"
225
+ ].join(" "));
226
+ return [formatSection("Rig servers", `${rows.length} available`), `${pc.bold(pad("ALIAS", aliasWidth))} ${pc.bold("KIND")} ${pc.bold("TARGET")}`, ...lines, "", ...formatNextSteps(["Select one: `rig server use <alias|local>`"])].join(`
227
+ `);
228
+ }
229
+ function formatConnectionStatus(selected, connections) {
230
+ const connection = selected === "local" ? { kind: "local", mode: "auto" } : connections[selected];
231
+ const target = !connection ? "not configured" : connection.kind === "remote" ? connection.baseUrl : "local";
232
+ return [
233
+ formatSection("Rig server", "selected for this repo"),
234
+ `${pc.dim("\u2502")} ${pc.dim("selected ")} ${pc.bold(selected)}`,
235
+ `${pc.dim("\u2502")} ${pc.dim("kind ")} ${formatStatusPill(connection?.kind ?? "unknown")}`,
236
+ `${pc.dim("\u2502")} ${pc.dim("target ")} ${target ?? "not configured"}`,
237
+ "",
238
+ ...formatNextSteps(["Change: `rig server use <alias|local>`", "List saved servers: `rig server list`"])
239
+ ].join(`
240
+ `);
241
+ }
242
+
243
+ // packages/cli/src/commands/connect.ts
244
+ function usageName(options) {
245
+ return `rig ${options.group}`;
246
+ }
247
+ function parseConnection(alias, value, options) {
248
+ if (alias === "local" && !value)
249
+ return { kind: "local", mode: "auto" };
250
+ if (!value)
251
+ throw new CliError2(`Missing remote server URL. Usage: ${usageName(options)} add <alias> <url>`, 1);
252
+ let parsed;
253
+ try {
254
+ parsed = new URL(value);
255
+ } catch {
256
+ throw new CliError2(`Invalid Rig server URL: ${value}`, 1);
257
+ }
258
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
259
+ throw new CliError2("Rig remote server URL must be http(s).", 1);
260
+ }
261
+ return { kind: "remote", baseUrl: parsed.toString().replace(/\/+$/, "") };
262
+ }
263
+ function printJsonOrText(context, payload, text) {
264
+ if (context.outputMode === "json") {
265
+ console.log(JSON.stringify(payload, null, 2));
266
+ } else {
267
+ console.log(text);
268
+ }
269
+ }
270
+ async function promptForConnectionAlias(context) {
271
+ const state = readGlobalConnections();
272
+ const repo = readRepoConnection(context.projectRoot);
273
+ const options = [
274
+ { value: "local", label: "local", hint: "Use/start a local Rig server" },
275
+ ...Object.entries(state.connections).map(([alias, connection]) => ({
276
+ value: alias,
277
+ label: alias,
278
+ hint: connection.kind === "remote" ? connection.baseUrl : "local"
279
+ }))
280
+ ].filter((option, index, all) => all.findIndex((candidate) => candidate.value === option.value) === index);
281
+ const answer = await select({
282
+ message: "Select Rig server for this repo",
283
+ initialValue: repo?.selected ?? "local",
284
+ options
285
+ });
286
+ if (isCancel(answer)) {
287
+ cancel("No server selected.");
288
+ throw new CliError2("No server selected.", 3);
289
+ }
290
+ return String(answer);
291
+ }
292
+ async function executeConnectionCommand(context, args, options) {
293
+ const [command, ...rest] = args;
294
+ switch (command ?? "status") {
295
+ case "list": {
296
+ requireNoExtraArgs(rest, `${usageName(options)} list`);
297
+ const state = readGlobalConnections();
298
+ printJsonOrText(context, state, formatConnectionList(state.connections));
299
+ return { ok: true, group: options.group, command: "list", details: state };
300
+ }
301
+ case "add": {
302
+ const [alias, url, ...extra] = rest;
303
+ if (!alias)
304
+ throw new CliError2(`Missing alias. Usage: ${usageName(options)} add <alias> <url>`, 1);
305
+ requireNoExtraArgs(extra, `${usageName(options)} add <alias> <url>`);
306
+ const connection = parseConnection(alias, url, options);
307
+ const state = upsertGlobalConnection(alias, connection);
308
+ printJsonOrText(context, { alias, connection }, formatSuccessCard("Rig server saved", [
309
+ ["alias", alias],
310
+ ["target", connection.kind === "remote" ? connection.baseUrl : "local"],
311
+ ["next", `${usageName(options)} use ${alias}`]
312
+ ]));
313
+ return { ok: true, group: options.group, command: "add", details: { alias, connection, count: Object.keys(state.connections).length } };
314
+ }
315
+ case "use": {
316
+ let [alias, ...extra] = rest;
317
+ requireNoExtraArgs(extra, `${usageName(options)} use <alias|local>`);
318
+ if (!alias && options.interactiveUse && context.outputMode === "text" && process.stdin.isTTY) {
319
+ alias = await promptForConnectionAlias(context);
320
+ }
321
+ if (!alias)
322
+ throw new CliError2(`Missing alias. Usage: ${usageName(options)} use <alias|local>`, 1);
323
+ if (alias !== "local") {
324
+ const state = readGlobalConnections();
325
+ if (!state.connections[alias])
326
+ throw new CliError2(`Unknown Rig server: ${alias}`, 1);
327
+ }
328
+ const repoState = { selected: alias, linkedAt: new Date().toISOString() };
329
+ writeRepoConnection(context.projectRoot, repoState);
330
+ printJsonOrText(context, repoState, formatSuccessCard("Rig server selected", [
331
+ ["selected", alias],
332
+ ["scope", "this repo"],
333
+ ["next", "rig task list"]
334
+ ]));
335
+ return { ok: true, group: options.group, command: "use", details: repoState };
336
+ }
337
+ case "status": {
338
+ requireNoExtraArgs(rest, `${usageName(options)} status`);
339
+ const repo = readRepoConnection(context.projectRoot);
340
+ const global = readGlobalConnections();
341
+ const details = { selected: repo?.selected ?? "local", repo, connections: global.connections };
342
+ printJsonOrText(context, details, formatConnectionStatus(details.selected, global.connections));
343
+ return { ok: true, group: options.group, command: "status", details };
344
+ }
345
+ default:
346
+ throw new CliError2(`Unknown ${options.group} command: ${String(command)}
347
+ Usage: ${usageName(options)} <list|add|use|status>`, 1);
147
348
  }
148
- return { alias: repo.selected, connection };
149
349
  }
150
350
 
151
351
  // packages/cli/src/commands/_server-client.ts
152
- var cachedGitHubBearerToken;
352
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
353
+ import { resolve as resolve2 } from "path";
354
+ import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
355
+ var scopedGitHubBearerTokens = new Map;
153
356
  function cleanToken(value) {
154
357
  const trimmed = value?.trim();
155
358
  return trimmed ? trimmed : null;
@@ -166,41 +369,33 @@ function readPrivateRemoteSessionToken(projectRoot) {
166
369
  }
167
370
  }
168
371
  function readGitHubBearerTokenForRemote(projectRoot) {
169
- if (cachedGitHubBearerToken !== undefined)
170
- return cachedGitHubBearerToken;
372
+ const scopedKey = resolve2(projectRoot);
373
+ if (scopedGitHubBearerTokens.has(scopedKey))
374
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
171
375
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
172
- if (privateSession) {
173
- cachedGitHubBearerToken = privateSession;
174
- return cachedGitHubBearerToken;
175
- }
176
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
177
- if (envToken) {
178
- cachedGitHubBearerToken = envToken;
179
- return cachedGitHubBearerToken;
180
- }
181
- const result = spawnSync("gh", ["auth", "token"], {
182
- encoding: "utf8",
183
- timeout: 5000,
184
- stdio: ["ignore", "pipe", "ignore"]
185
- });
186
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
187
- return cachedGitHubBearerToken;
376
+ if (privateSession)
377
+ return privateSession;
378
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
188
379
  }
189
380
  async function ensureServerForCli(projectRoot) {
190
381
  try {
191
382
  const selected = resolveSelectedConnection(projectRoot);
192
383
  if (selected?.connection.kind === "remote") {
384
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
385
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
193
386
  return {
194
387
  baseUrl: selected.connection.baseUrl,
195
- authToken: readGitHubBearerTokenForRemote(projectRoot),
196
- connectionKind: "remote"
388
+ authToken,
389
+ connectionKind: "remote",
390
+ serverProjectRoot
197
391
  };
198
392
  }
199
393
  const connection = await ensureLocalRigServerConnection(projectRoot);
200
394
  return {
201
395
  baseUrl: connection.baseUrl,
202
396
  authToken: connection.authToken,
203
- connectionKind: "local"
397
+ connectionKind: "local",
398
+ serverProjectRoot: resolve2(projectRoot)
204
399
  };
205
400
  } catch (error) {
206
401
  if (error instanceof Error) {
@@ -209,6 +404,29 @@ async function ensureServerForCli(projectRoot) {
209
404
  throw error;
210
405
  }
211
406
  }
407
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
408
+ const repo = readRepoConnection(projectRoot);
409
+ const slug = repo?.project?.trim();
410
+ if (!slug)
411
+ return null;
412
+ try {
413
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
414
+ headers: mergeHeaders(undefined, authToken)
415
+ });
416
+ if (!response.ok)
417
+ return null;
418
+ const payload = await response.json();
419
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
420
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
421
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
422
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
423
+ if (path)
424
+ writeRepoServerProjectRoot(projectRoot, path);
425
+ return path;
426
+ } catch {
427
+ return null;
428
+ }
429
+ }
212
430
  function mergeHeaders(headers, authToken) {
213
431
  const merged = new Headers(headers);
214
432
  if (authToken) {
@@ -233,9 +451,12 @@ function diagnosticMessage(payload) {
233
451
  }
234
452
  async function requestServerJson(context, pathname, init = {}) {
235
453
  const server = await ensureServerForCli(context.projectRoot);
454
+ const headers = mergeHeaders(init.headers, server.authToken);
455
+ if (server.serverProjectRoot)
456
+ headers.set("x-rig-project-root", server.serverProjectRoot);
236
457
  const response = await fetch(`${server.baseUrl}${pathname}`, {
237
458
  ...init,
238
- headers: mergeHeaders(init.headers, server.authToken)
459
+ headers
239
460
  });
240
461
  const text = await response.text();
241
462
  const payload = text.trim().length > 0 ? (() => {
@@ -286,7 +507,10 @@ async function submitTaskRunViaServer(context, input) {
286
507
 
287
508
  // packages/cli/src/commands/server.ts
288
509
  async function executeServer(context, args, options) {
289
- const [command = "start", ...rest] = args;
510
+ const [command = "status", ...rest] = args;
511
+ if (["status", "list", "add", "use"].includes(command)) {
512
+ return executeConnectionCommand(context, [command, ...rest], { group: "server", interactiveUse: true });
513
+ }
290
514
  switch (command) {
291
515
  case "start": {
292
516
  let pending = rest;
@@ -298,8 +522,9 @@ async function executeServer(context, args, options) {
298
522
  pending = pollResult.rest;
299
523
  const authTokenResult = takeOption(pending, "--auth-token");
300
524
  pending = authTokenResult.rest;
301
- requireNoExtraArgs(pending, "bun run rig server start [--host <host>] [--port <n>] [--poll-ms <n>] [--auth-token <token>]");
302
- const commandParts = ["bun", "run", "packages/server/src/server.ts", "start"];
525
+ requireNoExtraArgs(pending, "rig server start [--host <host>] [--port <n>] [--poll-ms <n>] [--auth-token <token>]");
526
+ const serverCommand = resolveRigServerCommand(context.projectRoot);
527
+ const commandParts = [serverCommand.command, ...serverCommand.commandArgs, "start"];
303
528
  if (hostResult.value) {
304
529
  commandParts.push("--host", hostResult.value);
305
530
  }
@@ -321,8 +546,9 @@ async function executeServer(context, args, options) {
321
546
  let pending = rest;
322
547
  const eventResult = takeOption(pending, "--event");
323
548
  pending = eventResult.rest;
324
- requireNoExtraArgs(pending, "bun run rig server notify-test [--event <type>]");
325
- const commandParts = ["bun", "run", "packages/server/src/server.ts", "notify-test"];
549
+ requireNoExtraArgs(pending, "rig server notify-test [--event <type>]");
550
+ const serverCommand = resolveRigServerCommand(context.projectRoot);
551
+ const commandParts = [serverCommand.command, ...serverCommand.commandArgs, "notify-test"];
326
552
  if (eventResult.value) {
327
553
  commandParts.push("--event", eventResult.value);
328
554
  }
@@ -349,7 +575,7 @@ async function executeServer(context, args, options) {
349
575
  pending = dirtyBaselineResult.rest;
350
576
  const prResult = takeOption(pending, "--pr");
351
577
  pending = prResult.rest;
352
- requireNoExtraArgs(pending, "bun run rig --run-id <run-id> server task-run [--task <id>] [--title <text>] [--runtime-adapter claude-code|codex|pi] [--model <model>] [--runtime-mode <mode>] [--interaction-mode <mode>] [--initial-prompt <text>] [--dirty-baseline head|dirty-snapshot] [--pr auto|ask|off]");
578
+ requireNoExtraArgs(pending, "rig --run-id <run-id> server task-run [--task <id>] [--title <text>] [--runtime-adapter claude-code|codex|pi] [--model <model>] [--runtime-mode <mode>] [--interaction-mode <mode>] [--initial-prompt <text>] [--dirty-baseline head|dirty-snapshot] [--pr auto|ask|off]");
353
579
  if (!taskResult.value && !initialPromptResult.value && !titleResult.value) {
354
580
  throw new CliError2("server task-run requires either --task <id> or --initial-prompt/--title for an ad hoc run.", 2);
355
581
  }
@@ -9,8 +9,6 @@ import { resolve as resolve6 } from "path";
9
9
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
10
10
  import { CliError } from "@rig/runtime/control-plane/errors";
11
11
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
12
- import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
13
- import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
14
12
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
15
13
  import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
16
14
  function requireNoExtraArgs(args, usage) {
@@ -102,7 +100,8 @@ function resolveControlPlaneDefinitionRoot(projectRoot) {
102
100
  import { existsSync, readFileSync, rmSync } from "fs";
103
101
  import { homedir } from "os";
104
102
  import { resolve as resolve2 } from "path";
105
- var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
103
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
104
+ var LEGACY_PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
106
105
  async function defaultCommandRunner(command, options = {}) {
107
106
  const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
108
107
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -121,7 +120,7 @@ function resolvePiHomeDir(inputHomeDir) {
121
120
  function piListContainsPiRig(output) {
122
121
  return output.split(/\r?\n/).some((line) => {
123
122
  const normalized = line.trim();
124
- return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
123
+ return normalized.includes(PI_RIG_PACKAGE_NAME) || normalized.includes(LEGACY_PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
125
124
  });
126
125
  }
127
126
  async function safeRun(runner, command, options) {
@@ -200,6 +199,11 @@ function readJsonFile(path) {
200
199
  throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
201
200
  }
202
201
  }
202
+ function writeJsonFile(path, value) {
203
+ mkdirSync(dirname(path), { recursive: true });
204
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
205
+ `, "utf8");
206
+ }
203
207
  function normalizeConnection(value) {
204
208
  if (!value || typeof value !== "object" || Array.isArray(value))
205
209
  return null;
@@ -240,29 +244,38 @@ function readRepoConnection(projectRoot) {
240
244
  return {
241
245
  selected,
242
246
  project: typeof record.project === "string" ? record.project : undefined,
243
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
247
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
248
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
244
249
  };
245
250
  }
251
+ function writeRepoConnection(projectRoot, state) {
252
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
253
+ }
246
254
  function resolveSelectedConnection(projectRoot, options = {}) {
247
255
  const repo = readRepoConnection(projectRoot);
248
256
  if (!repo)
249
257
  return null;
250
258
  if (repo.selected === "local")
251
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
259
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
252
260
  const global = readGlobalConnections(options);
253
261
  const connection = global.connections[repo.selected];
254
262
  if (!connection) {
255
- throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
263
+ throw new CliError2(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
256
264
  }
257
- return { alias: repo.selected, connection };
265
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
266
+ }
267
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
268
+ const repo = readRepoConnection(projectRoot);
269
+ if (!repo)
270
+ return;
271
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
258
272
  }
259
273
 
260
274
  // packages/cli/src/commands/_server-client.ts
261
- import { spawnSync } from "child_process";
262
275
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
263
276
  import { resolve as resolve4 } from "path";
264
277
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
265
- var cachedGitHubBearerToken;
278
+ var scopedGitHubBearerTokens = new Map;
266
279
  function cleanToken(value) {
267
280
  const trimmed = value?.trim();
268
281
  return trimmed ? trimmed : null;
@@ -279,41 +292,33 @@ function readPrivateRemoteSessionToken(projectRoot) {
279
292
  }
280
293
  }
281
294
  function readGitHubBearerTokenForRemote(projectRoot) {
282
- if (cachedGitHubBearerToken !== undefined)
283
- return cachedGitHubBearerToken;
295
+ const scopedKey = resolve4(projectRoot);
296
+ if (scopedGitHubBearerTokens.has(scopedKey))
297
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
284
298
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
285
- if (privateSession) {
286
- cachedGitHubBearerToken = privateSession;
287
- return cachedGitHubBearerToken;
288
- }
289
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
290
- if (envToken) {
291
- cachedGitHubBearerToken = envToken;
292
- return cachedGitHubBearerToken;
293
- }
294
- const result = spawnSync("gh", ["auth", "token"], {
295
- encoding: "utf8",
296
- timeout: 5000,
297
- stdio: ["ignore", "pipe", "ignore"]
298
- });
299
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
300
- return cachedGitHubBearerToken;
299
+ if (privateSession)
300
+ return privateSession;
301
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
301
302
  }
302
303
  async function ensureServerForCli(projectRoot) {
303
304
  try {
304
305
  const selected = resolveSelectedConnection(projectRoot);
305
306
  if (selected?.connection.kind === "remote") {
307
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
308
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
306
309
  return {
307
310
  baseUrl: selected.connection.baseUrl,
308
- authToken: readGitHubBearerTokenForRemote(projectRoot),
309
- connectionKind: "remote"
311
+ authToken,
312
+ connectionKind: "remote",
313
+ serverProjectRoot
310
314
  };
311
315
  }
312
316
  const connection = await ensureLocalRigServerConnection(projectRoot);
313
317
  return {
314
318
  baseUrl: connection.baseUrl,
315
319
  authToken: connection.authToken,
316
- connectionKind: "local"
320
+ connectionKind: "local",
321
+ serverProjectRoot: resolve4(projectRoot)
317
322
  };
318
323
  } catch (error) {
319
324
  if (error instanceof Error) {
@@ -322,6 +327,29 @@ async function ensureServerForCli(projectRoot) {
322
327
  throw error;
323
328
  }
324
329
  }
330
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
331
+ const repo = readRepoConnection(projectRoot);
332
+ const slug = repo?.project?.trim();
333
+ if (!slug)
334
+ return null;
335
+ try {
336
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
337
+ headers: mergeHeaders(undefined, authToken)
338
+ });
339
+ if (!response.ok)
340
+ return null;
341
+ const payload = await response.json();
342
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
343
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
344
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
345
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
346
+ if (path)
347
+ writeRepoServerProjectRoot(projectRoot, path);
348
+ return path;
349
+ } catch {
350
+ return null;
351
+ }
352
+ }
325
353
  function mergeHeaders(headers, authToken) {
326
354
  const merged = new Headers(headers);
327
355
  if (authToken) {
@@ -346,9 +374,12 @@ function diagnosticMessage(payload) {
346
374
  }
347
375
  async function requestServerJson(context, pathname, init = {}) {
348
376
  const server = await ensureServerForCli(context.projectRoot);
377
+ const headers = mergeHeaders(init.headers, server.authToken);
378
+ if (server.serverProjectRoot)
379
+ headers.set("x-rig-project-root", server.serverProjectRoot);
349
380
  const response = await fetch(`${server.baseUrl}${pathname}`, {
350
381
  ...init,
351
- headers: mergeHeaders(init.headers, server.authToken)
382
+ headers
352
383
  });
353
384
  const text = await response.text();
354
385
  const payload = text.trim().length > 0 ? (() => {
@@ -493,7 +524,7 @@ async function runRigDoctorChecks(options) {
493
524
  const taskSourceKind = config?.taskSource?.kind;
494
525
  checks.push(taskSourceKind ? check("task-source", "task source configured", "pass", taskSourceKind) : check("task-source", "task source configured", "fail", "missing taskSource", "Configure taskSource in rig.config.ts."));
495
526
  const repo = readRepoConnection(projectRoot);
496
- checks.push(repo ? check("project-link", "repo selected Rig connection", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to link this checkout to a GitHub repo slug.") : check("project-link", "repo selected Rig connection", "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig connect use <alias|local>`."));
527
+ checks.push(repo ? check("project-link", "repo selected Rig server", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to link this checkout to a GitHub repo slug.") : check("project-link", "repo selected Rig server", "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig server use <alias|local>`."));
497
528
  const selected = (() => {
498
529
  try {
499
530
  return resolveSelectedConnection(projectRoot);
@@ -501,7 +532,7 @@ async function runRigDoctorChecks(options) {
501
532
  return null;
502
533
  }
503
534
  })();
504
- checks.push(selected ? check("connection", "selected server connection", "pass", selected.connection.kind === "remote" ? selected.connection.baseUrl : "local auto") : check("connection", "selected server connection", repo ? "fail" : "warn", repo ? "selected alias is missing" : "will auto-start local server", repo ? "Run `rig connect list` and `rig connect use <alias|local>`." : undefined));
535
+ checks.push(selected ? check("connection", "selected server connection", "pass", selected.connection.kind === "remote" ? selected.connection.baseUrl : "local auto") : check("connection", "selected server", repo ? "fail" : "warn", repo ? "selected alias is missing" : "will auto-start local server", repo ? "Run `rig server list` and `rig server use <alias|local>`." : undefined));
505
536
  let server = null;
506
537
  try {
507
538
  server = await (options.resolveServer ?? ensureServerForCli)(projectRoot);
@@ -587,7 +618,7 @@ async function executeSetup(context, args) {
587
618
  const [command = "check", ...rest] = args;
588
619
  switch (command) {
589
620
  case "bootstrap":
590
- requireNoExtraArgs(rest, "bun run rig setup bootstrap");
621
+ requireNoExtraArgs(rest, "rig setup bootstrap");
591
622
  {
592
623
  const hostBash = Bun.which("bash") || "/bin/bash";
593
624
  const env = { ...process.env };
@@ -610,25 +641,19 @@ async function executeSetup(context, args) {
610
641
  }
611
642
  return { ok: true, group: "setup", command };
612
643
  case "check":
613
- requireNoExtraArgs(rest, `bun run rig setup ${command}`);
644
+ requireNoExtraArgs(rest, `rig setup ${command}`);
614
645
  {
615
646
  const checks = await withMutedConsole(context.outputMode === "json", () => runSetupCheck(context.projectRoot));
616
647
  return { ok: true, group: "setup", command, details: { checks, failures: countDoctorFailures(checks) } };
617
648
  }
618
649
  case "setup":
619
- requireNoExtraArgs(rest, "bun run rig setup setup");
650
+ requireNoExtraArgs(rest, "rig setup setup");
620
651
  withMutedConsole(context.outputMode === "json", () => runSetupInit(context.projectRoot));
621
652
  return { ok: true, group: "setup", command };
622
653
  case "preflight":
623
- requireNoExtraArgs(rest, "bun run rig setup preflight");
654
+ requireNoExtraArgs(rest, "rig setup preflight");
624
655
  await withMutedConsole(context.outputMode === "json", () => runSetupPreflight(context.projectRoot));
625
656
  return { ok: true, group: "setup", command };
626
- case "install-agent-shell":
627
- requireNoExtraArgs(rest, "bun run rig setup install-agent-shell");
628
- if (context.outputMode === "text") {
629
- console.log("install-agent-shell is deprecated. Runtime shells now use compiled rig-agent directly.");
630
- }
631
- return { ok: true, group: "setup", command };
632
657
  default:
633
658
  throw new CliError2(`Unknown setup command: ${command}`);
634
659
  }