@h-rig/cli 0.0.6-alpha.25 → 0.0.6-alpha.27

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.
@@ -60,10 +60,8 @@ function normalizeRuntimeAdapter(value) {
60
60
  return "claude-code";
61
61
  }
62
62
 
63
- // packages/cli/src/commands/_server-client.ts
64
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
65
- import { resolve as resolve2 } from "path";
66
- import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
63
+ // packages/cli/src/commands/connect.ts
64
+ import { cancel, isCancel, select } from "@clack/prompts";
67
65
 
68
66
  // packages/cli/src/commands/_connection-state.ts
69
67
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -90,6 +88,11 @@ function readJsonFile(path) {
90
88
  throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
91
89
  }
92
90
  }
91
+ function writeJsonFile2(path, value) {
92
+ mkdirSync(dirname(path), { recursive: true });
93
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
94
+ `, "utf8");
95
+ }
93
96
  function normalizeConnection(value) {
94
97
  if (!value || typeof value !== "object" || Array.isArray(value))
95
98
  return null;
@@ -119,6 +122,18 @@ function readGlobalConnections(options = {}) {
119
122
  }
120
123
  return { connections };
121
124
  }
125
+ function writeGlobalConnections(state, options = {}) {
126
+ writeJsonFile2(resolveGlobalConnectionsPath(options.env ?? process.env), state);
127
+ }
128
+ function upsertGlobalConnection(alias, connection, options = {}) {
129
+ const cleanAlias = alias.trim();
130
+ if (!cleanAlias)
131
+ throw new CliError2("Connection alias is required.", 1);
132
+ const state = readGlobalConnections(options);
133
+ state.connections[cleanAlias] = connection;
134
+ writeGlobalConnections(state, options);
135
+ return state;
136
+ }
122
137
  function readRepoConnection(projectRoot) {
123
138
  const payload = readJsonFile(resolveRepoConnectionPath(projectRoot));
124
139
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
@@ -133,6 +148,9 @@ function readRepoConnection(projectRoot) {
133
148
  linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
134
149
  };
135
150
  }
151
+ function writeRepoConnection(projectRoot, state) {
152
+ writeJsonFile2(resolveRepoConnectionPath(projectRoot), state);
153
+ }
136
154
  function resolveSelectedConnection(projectRoot, options = {}) {
137
155
  const repo = readRepoConnection(projectRoot);
138
156
  if (!repo)
@@ -147,7 +165,184 @@ function resolveSelectedConnection(projectRoot, options = {}) {
147
165
  return { alias: repo.selected, connection };
148
166
  }
149
167
 
168
+ // packages/cli/src/commands/_cli-format.ts
169
+ import pc from "picocolors";
170
+ function truncate(value, width) {
171
+ if (value.length <= width)
172
+ return value;
173
+ if (width <= 1)
174
+ return "\u2026";
175
+ return `${value.slice(0, width - 1)}\u2026`;
176
+ }
177
+ function pad(value, width) {
178
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
179
+ }
180
+ function statusColor(status) {
181
+ const normalized = status.toLowerCase();
182
+ if (["completed", "merged", "closed", "done", "accepted", "pass", "selected"].includes(normalized))
183
+ return pc.green;
184
+ if (["failed", "needs_attention", "needs-attention", "blocked", "error"].includes(normalized))
185
+ return pc.red;
186
+ if (["running", "reviewing", "validating", "in_progress", "in-progress", "remote"].includes(normalized))
187
+ return pc.cyan;
188
+ if (["ready", "open", "queued", "created", "preparing", "local"].includes(normalized))
189
+ return pc.yellow;
190
+ return pc.dim;
191
+ }
192
+ function formatStatusPill(status) {
193
+ const label = status || "unknown";
194
+ return statusColor(label)(`\u25CF ${label}`);
195
+ }
196
+ function formatSection(title, subtitle) {
197
+ return `${pc.bold(pc.cyan("\u25C6"))} ${pc.bold(title)}${subtitle ? pc.dim(` \u2014 ${subtitle}`) : ""}`;
198
+ }
199
+ function formatSuccessCard(title, rows = []) {
200
+ const body = rows.filter(([, value]) => value !== undefined && value !== null && String(value).length > 0).map(([key, value]) => `${pc.dim("\u2502")} ${pc.dim(key.padEnd(9))} ${value}`);
201
+ return [formatSection(title), ...body].join(`
202
+ `);
203
+ }
204
+ function formatNextSteps(steps) {
205
+ if (steps.length === 0)
206
+ return [];
207
+ return [pc.bold("Next"), ...steps.map((step) => `${pc.dim("\u203A")} ${step}`)];
208
+ }
209
+ function formatConnectionList(connections) {
210
+ const rows = [["local", { kind: "local", mode: "auto" }], ...Object.entries(connections)];
211
+ const aliasWidth = Math.min(24, Math.max(5, ...rows.map(([alias]) => alias.length)));
212
+ const lines = rows.map(([alias, connection]) => [
213
+ pc.bold(pad(truncate(alias, aliasWidth), aliasWidth)),
214
+ formatStatusPill(connection.kind),
215
+ connection.kind === "remote" ? connection.baseUrl ?? "" : connection.mode ?? "local"
216
+ ].join(" "));
217
+ 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(`
218
+ `);
219
+ }
220
+ function formatConnectionStatus(selected, connections) {
221
+ const connection = selected === "local" ? { kind: "local", mode: "auto" } : connections[selected];
222
+ const target = !connection ? "not configured" : connection.kind === "remote" ? connection.baseUrl : "local";
223
+ return [
224
+ formatSection("Rig server", "selected for this repo"),
225
+ `${pc.dim("\u2502")} ${pc.dim("selected ")} ${pc.bold(selected)}`,
226
+ `${pc.dim("\u2502")} ${pc.dim("kind ")} ${formatStatusPill(connection?.kind ?? "unknown")}`,
227
+ `${pc.dim("\u2502")} ${pc.dim("target ")} ${target ?? "not configured"}`,
228
+ "",
229
+ ...formatNextSteps(["Change: `rig server use <alias|local>`", "List saved servers: `rig server list`"])
230
+ ].join(`
231
+ `);
232
+ }
233
+
234
+ // packages/cli/src/commands/connect.ts
235
+ function usageName(options) {
236
+ return `rig ${options.group}`;
237
+ }
238
+ function parseConnection(alias, value, options) {
239
+ if (alias === "local" && !value)
240
+ return { kind: "local", mode: "auto" };
241
+ if (!value)
242
+ throw new CliError2(`Missing remote server URL. Usage: ${usageName(options)} add <alias> <url>`, 1);
243
+ let parsed;
244
+ try {
245
+ parsed = new URL(value);
246
+ } catch {
247
+ throw new CliError2(`Invalid Rig server URL: ${value}`, 1);
248
+ }
249
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
250
+ throw new CliError2("Rig remote server URL must be http(s).", 1);
251
+ }
252
+ return { kind: "remote", baseUrl: parsed.toString().replace(/\/+$/, "") };
253
+ }
254
+ function printJsonOrText(context, payload, text) {
255
+ if (context.outputMode === "json") {
256
+ console.log(JSON.stringify(payload, null, 2));
257
+ } else {
258
+ console.log(text);
259
+ }
260
+ }
261
+ async function promptForConnectionAlias(context) {
262
+ const state = readGlobalConnections();
263
+ const repo = readRepoConnection(context.projectRoot);
264
+ const options = [
265
+ { value: "local", label: "local", hint: "Use/start a local Rig server" },
266
+ ...Object.entries(state.connections).map(([alias, connection]) => ({
267
+ value: alias,
268
+ label: alias,
269
+ hint: connection.kind === "remote" ? connection.baseUrl : "local"
270
+ }))
271
+ ].filter((option, index, all) => all.findIndex((candidate) => candidate.value === option.value) === index);
272
+ const answer = await select({
273
+ message: "Select Rig server for this repo",
274
+ initialValue: repo?.selected ?? "local",
275
+ options
276
+ });
277
+ if (isCancel(answer)) {
278
+ cancel("No server selected.");
279
+ throw new CliError2("No server selected.", 3);
280
+ }
281
+ return String(answer);
282
+ }
283
+ async function executeConnectionCommand(context, args, options) {
284
+ const [command, ...rest] = args;
285
+ switch (command ?? "status") {
286
+ case "list": {
287
+ requireNoExtraArgs(rest, `${usageName(options)} list`);
288
+ const state = readGlobalConnections();
289
+ printJsonOrText(context, state, formatConnectionList(state.connections));
290
+ return { ok: true, group: options.group, command: "list", details: state };
291
+ }
292
+ case "add": {
293
+ const [alias, url, ...extra] = rest;
294
+ if (!alias)
295
+ throw new CliError2(`Missing alias. Usage: ${usageName(options)} add <alias> <url>`, 1);
296
+ requireNoExtraArgs(extra, `${usageName(options)} add <alias> <url>`);
297
+ const connection = parseConnection(alias, url, options);
298
+ const state = upsertGlobalConnection(alias, connection);
299
+ printJsonOrText(context, { alias, connection }, formatSuccessCard("Rig server saved", [
300
+ ["alias", alias],
301
+ ["target", connection.kind === "remote" ? connection.baseUrl : "local"],
302
+ ["next", `${usageName(options)} use ${alias}`]
303
+ ]));
304
+ return { ok: true, group: options.group, command: "add", details: { alias, connection, count: Object.keys(state.connections).length } };
305
+ }
306
+ case "use": {
307
+ let [alias, ...extra] = rest;
308
+ requireNoExtraArgs(extra, `${usageName(options)} use <alias|local>`);
309
+ if (!alias && options.interactiveUse && context.outputMode === "text" && process.stdin.isTTY) {
310
+ alias = await promptForConnectionAlias(context);
311
+ }
312
+ if (!alias)
313
+ throw new CliError2(`Missing alias. Usage: ${usageName(options)} use <alias|local>`, 1);
314
+ if (alias !== "local") {
315
+ const state = readGlobalConnections();
316
+ if (!state.connections[alias])
317
+ throw new CliError2(`Unknown Rig server: ${alias}`, 1);
318
+ }
319
+ const repoState = { selected: alias, linkedAt: new Date().toISOString() };
320
+ writeRepoConnection(context.projectRoot, repoState);
321
+ printJsonOrText(context, repoState, formatSuccessCard("Rig server selected", [
322
+ ["selected", alias],
323
+ ["scope", "this repo"],
324
+ ["next", "rig task list"]
325
+ ]));
326
+ return { ok: true, group: options.group, command: "use", details: repoState };
327
+ }
328
+ case "status": {
329
+ requireNoExtraArgs(rest, `${usageName(options)} status`);
330
+ const repo = readRepoConnection(context.projectRoot);
331
+ const global = readGlobalConnections();
332
+ const details = { selected: repo?.selected ?? "local", repo, connections: global.connections };
333
+ printJsonOrText(context, details, formatConnectionStatus(details.selected, global.connections));
334
+ return { ok: true, group: options.group, command: "status", details };
335
+ }
336
+ default:
337
+ throw new CliError2(`Unknown ${options.group} command: ${String(command)}
338
+ Usage: ${usageName(options)} <list|add|use|status>`, 1);
339
+ }
340
+ }
341
+
150
342
  // packages/cli/src/commands/_server-client.ts
343
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
344
+ import { resolve as resolve2 } from "path";
345
+ import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
151
346
  var scopedGitHubBearerTokens = new Map;
152
347
  function cleanToken(value) {
153
348
  const trimmed = value?.trim();
@@ -273,7 +468,10 @@ async function submitTaskRunViaServer(context, input) {
273
468
 
274
469
  // packages/cli/src/commands/server.ts
275
470
  async function executeServer(context, args, options) {
276
- const [command = "start", ...rest] = args;
471
+ const [command = "status", ...rest] = args;
472
+ if (["status", "list", "add", "use"].includes(command)) {
473
+ return executeConnectionCommand(context, [command, ...rest], { group: "server", interactiveUse: true });
474
+ }
277
475
  switch (command) {
278
476
  case "start": {
279
477
  let pending = rest;
@@ -285,7 +483,7 @@ async function executeServer(context, args, options) {
285
483
  pending = pollResult.rest;
286
484
  const authTokenResult = takeOption(pending, "--auth-token");
287
485
  pending = authTokenResult.rest;
288
- requireNoExtraArgs(pending, "bun run rig server start [--host <host>] [--port <n>] [--poll-ms <n>] [--auth-token <token>]");
486
+ requireNoExtraArgs(pending, "rig server start [--host <host>] [--port <n>] [--poll-ms <n>] [--auth-token <token>]");
289
487
  const commandParts = ["rig-server", "start"];
290
488
  if (hostResult.value) {
291
489
  commandParts.push("--host", hostResult.value);
@@ -308,7 +506,7 @@ async function executeServer(context, args, options) {
308
506
  let pending = rest;
309
507
  const eventResult = takeOption(pending, "--event");
310
508
  pending = eventResult.rest;
311
- requireNoExtraArgs(pending, "bun run rig server notify-test [--event <type>]");
509
+ requireNoExtraArgs(pending, "rig server notify-test [--event <type>]");
312
510
  const commandParts = ["rig-server", "notify-test"];
313
511
  if (eventResult.value) {
314
512
  commandParts.push("--event", eventResult.value);
@@ -336,7 +534,7 @@ async function executeServer(context, args, options) {
336
534
  pending = dirtyBaselineResult.rest;
337
535
  const prResult = takeOption(pending, "--pr");
338
536
  pending = prResult.rest;
339
- 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]");
537
+ 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]");
340
538
  if (!taskResult.value && !initialPromptResult.value && !titleResult.value) {
341
539
  throw new CliError2("server task-run requires either --task <id> or --initial-prompt/--title for an ad hoc run.", 2);
342
540
  }
@@ -1647,21 +1647,39 @@ function pad(value, width) {
1647
1647
  }
1648
1648
  function statusColor(status) {
1649
1649
  const normalized = status.toLowerCase();
1650
- if (["completed", "merged", "closed", "done", "accepted"].includes(normalized))
1650
+ if (["completed", "merged", "closed", "done", "accepted", "pass", "selected"].includes(normalized))
1651
1651
  return pc.green;
1652
- if (["failed", "needs_attention", "needs-attention", "blocked"].includes(normalized))
1652
+ if (["failed", "needs_attention", "needs-attention", "blocked", "error"].includes(normalized))
1653
1653
  return pc.red;
1654
- if (["running", "reviewing", "validating", "in_progress", "in-progress"].includes(normalized))
1654
+ if (["running", "reviewing", "validating", "in_progress", "in-progress", "remote"].includes(normalized))
1655
1655
  return pc.cyan;
1656
- if (["ready", "open", "queued", "created", "preparing"].includes(normalized))
1656
+ if (["ready", "open", "queued", "created", "preparing", "local"].includes(normalized))
1657
1657
  return pc.yellow;
1658
1658
  return pc.dim;
1659
1659
  }
1660
+ function formatStatusPill(status) {
1661
+ const label = status || "unknown";
1662
+ return statusColor(label)(`\u25CF ${label}`);
1663
+ }
1664
+ function formatSection(title, subtitle) {
1665
+ return `${pc.bold(pc.cyan("\u25C6"))} ${pc.bold(title)}${subtitle ? pc.dim(` \u2014 ${subtitle}`) : ""}`;
1666
+ }
1667
+ function formatSuccessCard(title, rows = []) {
1668
+ const body = rows.filter(([, value]) => value !== undefined && value !== null && String(value).length > 0).map(([key, value]) => `${pc.dim("\u2502")} ${pc.dim(key.padEnd(9))} ${value}`);
1669
+ return [formatSection(title), ...body].join(`
1670
+ `);
1671
+ }
1672
+ function formatNextSteps(steps) {
1673
+ if (steps.length === 0)
1674
+ return [];
1675
+ return [pc.bold("Next"), ...steps.map((step) => `${pc.dim("\u203A")} ${step}`)];
1676
+ }
1660
1677
  function formatTaskList(tasks, options = {}) {
1661
- if (tasks.length === 0)
1662
- return pc.dim("No matching tasks.");
1663
1678
  if (options.raw)
1664
1679
  return tasks.map((task) => JSON.stringify(task)).join(`
1680
+ `);
1681
+ if (tasks.length === 0)
1682
+ return [formatSection("Tasks", "none found"), ...formatNextSteps(["Try `rig server status` to confirm the selected server.", "Relax filters or run `rig task run --title ... --initial-prompt ...` for ad hoc work."])].join(`
1665
1683
  `);
1666
1684
  const rows = tasks.map((task) => {
1667
1685
  const raw = rawObject(task);
@@ -1684,18 +1702,22 @@ function formatTaskList(tasks, options = {}) {
1684
1702
  `${row.title}${labels}${source}`
1685
1703
  ].join(" ");
1686
1704
  });
1687
- return [pc.bold("Rig tasks"), header, ...body].join(`
1705
+ return [formatSection("Tasks", `${rows.length} shown`), header, ...body, "", ...formatNextSteps(["Run one: `rig task run <id>` or `rig task run --next`", "Attach later: `rig run attach <run-id> --follow`"])].join(`
1688
1706
  `);
1689
1707
  }
1690
1708
  function formatSubmittedRun(input) {
1691
- const lines = [`${pc.green("Run submitted")}: ${pc.bold(input.runId)}`];
1709
+ const rows = [["run", pc.bold(input.runId)]];
1692
1710
  if (input.task) {
1693
1711
  const id = stringField(input.task, "id", "<unknown>");
1694
1712
  const status = stringField(input.task, "status", "unknown");
1695
1713
  const title = stringField(input.task, "title", "Untitled task");
1696
- lines.push(`${pc.dim("task")} ${pc.bold(id)} ${statusColor(status)(status)} ${title}`);
1714
+ rows.push(["task", `${pc.bold(id)} ${formatStatusPill(status)} ${title}`]);
1697
1715
  }
1698
- return lines.join(`
1716
+ return [
1717
+ formatSuccessCard("Run submitted", rows),
1718
+ "",
1719
+ ...formatNextSteps([`Attach: \`rig run attach ${input.runId} --follow\``, `Inspect: \`rig run show --run ${input.runId}\``])
1720
+ ].join(`
1699
1721
  `);
1700
1722
  }
1701
1723