@h-rig/cli 0.0.6-alpha.9 → 0.0.6-alpha.90

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 (114) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/build-rig-binaries.d.ts +2 -0
  3. package/dist/bin/rig.d.ts +2 -0
  4. package/dist/bin/rig.js +11052 -6668
  5. package/dist/src/app/board.d.ts +23 -0
  6. package/dist/src/app/board.js +1786 -0
  7. package/dist/src/app/drone-ui.d.ts +37 -0
  8. package/dist/src/app/drone-ui.js +294 -0
  9. package/dist/src/app/theme.d.ts +47 -0
  10. package/dist/src/app/theme.js +150 -0
  11. package/dist/src/commands/_async-ui.d.ts +13 -0
  12. package/dist/src/commands/_async-ui.js +223 -0
  13. package/dist/src/commands/_authority-runs.d.ts +22 -0
  14. package/dist/src/commands/_authority-runs.js +2 -3
  15. package/dist/src/commands/_cli-format.d.ts +49 -0
  16. package/dist/src/commands/_cli-format.js +447 -0
  17. package/dist/src/commands/_connection-state.d.ts +44 -0
  18. package/dist/src/commands/_connection-state.js +30 -11
  19. package/dist/src/commands/_doctor-checks.d.ts +52 -0
  20. package/dist/src/commands/_doctor-checks.js +180 -44
  21. package/dist/src/commands/_help-catalog.d.ts +51 -0
  22. package/dist/src/commands/_help-catalog.js +442 -0
  23. package/dist/src/commands/_json-output.d.ts +11 -0
  24. package/dist/src/commands/_json-output.js +56 -0
  25. package/dist/src/commands/_operator-surface.d.ts +34 -0
  26. package/dist/src/commands/_operator-surface.js +220 -0
  27. package/dist/src/commands/_operator-view.d.ts +30 -0
  28. package/dist/src/commands/_operator-view.js +803 -73
  29. package/dist/src/commands/_parsers.d.ts +15 -0
  30. package/dist/src/commands/_parsers.js +18 -11
  31. package/dist/src/commands/_paths.d.ts +11 -0
  32. package/dist/src/commands/_pi-frontend.d.ts +27 -0
  33. package/dist/src/commands/_pi-frontend.js +742 -0
  34. package/dist/src/commands/_pi-install.d.ts +42 -0
  35. package/dist/src/commands/_pi-install.js +5 -4
  36. package/dist/src/commands/_policy.d.ts +8 -0
  37. package/dist/src/commands/_policy.js +12 -5
  38. package/dist/src/commands/_preflight.d.ts +22 -0
  39. package/dist/src/commands/_preflight.js +190 -128
  40. package/dist/src/commands/_probes.d.ts +1 -0
  41. package/dist/src/commands/_run-driver-helpers.d.ts +99 -0
  42. package/dist/src/commands/_run-driver-helpers.js +75 -22
  43. package/dist/src/commands/_run-replay.d.ts +24 -0
  44. package/dist/src/commands/_run-replay.js +142 -0
  45. package/dist/src/commands/_server-client.d.ts +186 -0
  46. package/dist/src/commands/_server-client.js +360 -61
  47. package/dist/src/commands/_snapshot-upload.d.ts +39 -0
  48. package/dist/src/commands/_snapshot-upload.js +163 -39
  49. package/dist/src/commands/_spinner.d.ts +25 -0
  50. package/dist/src/commands/_spinner.js +65 -0
  51. package/dist/src/commands/_task-picker.d.ts +9 -0
  52. package/dist/src/commands/_task-picker.js +172 -19
  53. package/dist/src/commands/agent.d.ts +3 -0
  54. package/dist/src/commands/agent.js +39 -20
  55. package/dist/src/commands/browser.d.ts +65 -0
  56. package/dist/src/commands/browser.js +334 -51
  57. package/dist/src/commands/connect.d.ts +7 -0
  58. package/dist/src/commands/connect.js +272 -33
  59. package/dist/src/commands/dist.d.ts +28 -0
  60. package/dist/src/commands/dist.js +19 -12
  61. package/dist/src/commands/doctor.d.ts +3 -0
  62. package/dist/src/commands/doctor.js +378 -45
  63. package/dist/src/commands/github.d.ts +3 -0
  64. package/dist/src/commands/github.js +375 -53
  65. package/dist/src/commands/inbox.d.ts +30 -0
  66. package/dist/src/commands/inbox.js +760 -70
  67. package/dist/src/commands/init.d.ts +74 -0
  68. package/dist/src/commands/init.js +963 -119
  69. package/dist/src/commands/inspect.d.ts +3 -0
  70. package/dist/src/commands/inspect.js +588 -32
  71. package/dist/src/commands/inspector.d.ts +3 -0
  72. package/dist/src/commands/inspector.js +20 -13
  73. package/dist/src/commands/pi.d.ts +3 -0
  74. package/dist/src/commands/pi.js +177 -0
  75. package/dist/src/commands/plugin.d.ts +16 -0
  76. package/dist/src/commands/plugin.js +95 -27
  77. package/dist/src/commands/profile-and-review.d.ts +4 -0
  78. package/dist/src/commands/profile-and-review.js +26 -19
  79. package/dist/src/commands/queue.d.ts +3 -0
  80. package/dist/src/commands/queue.js +32 -12
  81. package/dist/src/commands/remote.d.ts +3 -0
  82. package/dist/src/commands/remote.js +43 -36
  83. package/dist/src/commands/repo-git-harness.d.ts +5 -0
  84. package/dist/src/commands/repo-git-harness.js +22 -15
  85. package/dist/src/commands/run.d.ts +3 -0
  86. package/dist/src/commands/run.js +1379 -159
  87. package/dist/src/commands/server.d.ts +7 -0
  88. package/dist/src/commands/server.js +502 -57
  89. package/dist/src/commands/setup.d.ts +16 -0
  90. package/dist/src/commands/setup.js +390 -63
  91. package/dist/src/commands/stats.d.ts +12 -0
  92. package/dist/src/commands/stats.js +1051 -0
  93. package/dist/src/commands/task-report-bug.d.ts +19 -0
  94. package/dist/src/commands/task-report-bug.js +260 -62
  95. package/dist/src/commands/task-run-driver.d.ts +132 -0
  96. package/dist/src/commands/task-run-driver.js +886 -130
  97. package/dist/src/commands/task.d.ts +14 -0
  98. package/dist/src/commands/task.js +1644 -314
  99. package/dist/src/commands/test.d.ts +3 -0
  100. package/dist/src/commands/test.js +15 -8
  101. package/dist/src/commands/workspace.d.ts +3 -0
  102. package/dist/src/commands/workspace.js +18 -11
  103. package/dist/src/commands.d.ts +29 -0
  104. package/dist/src/commands.js +10967 -6637
  105. package/dist/src/index.d.ts +4 -0
  106. package/dist/src/index.js +10168 -5787
  107. package/dist/src/launcher.d.ts +61 -0
  108. package/dist/src/launcher.js +77 -13
  109. package/dist/src/report-bug.d.ts +44 -0
  110. package/dist/src/report-bug.js +3 -3
  111. package/dist/src/runner.d.ts +47 -0
  112. package/dist/src/runner.js +16 -22
  113. package/dist/src/withMutedConsole.d.ts +2 -0
  114. package/package.json +13 -6
@@ -4,18 +4,24 @@ import { mkdir, readdir, readFile, writeFile } from "fs/promises";
4
4
  import { dirname as dirname2, resolve as resolve3, relative, sep } from "path";
5
5
 
6
6
  // packages/cli/src/commands/_server-client.ts
7
- import { spawnSync } from "child_process";
8
7
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
9
8
  import { resolve as resolve2 } from "path";
10
9
 
11
10
  // packages/cli/src/runner.ts
12
11
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
13
- import { CliError } from "@rig/runtime/control-plane/errors";
12
+ import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
14
13
  import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
15
- import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
16
- import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
17
14
  import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
18
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
15
+
16
+ class CliError extends RuntimeCliError {
17
+ hint;
18
+ constructor(message, exitCode = 1, options = {}) {
19
+ super(message, exitCode);
20
+ if (options.hint?.trim()) {
21
+ this.hint = options.hint.trim();
22
+ }
23
+ }
24
+ }
19
25
 
20
26
  // packages/cli/src/commands/_server-client.ts
21
27
  import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
@@ -42,9 +48,14 @@ function readJsonFile(path) {
42
48
  try {
43
49
  return JSON.parse(readFileSync(path, "utf8"));
44
50
  } catch (error) {
45
- throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
51
+ throw new CliError(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1, { hint: "Fix or delete that file, then re-select a server with `rig server use <alias|local>`." });
46
52
  }
47
53
  }
54
+ function writeJsonFile(path, value) {
55
+ mkdirSync(dirname(path), { recursive: true });
56
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
57
+ `, "utf8");
58
+ }
48
59
  function normalizeConnection(value) {
49
60
  if (!value || typeof value !== "object" || Array.isArray(value))
50
61
  return null;
@@ -85,25 +96,39 @@ function readRepoConnection(projectRoot) {
85
96
  return {
86
97
  selected,
87
98
  project: typeof record.project === "string" ? record.project : undefined,
88
- linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
99
+ linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
100
+ serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined
89
101
  };
90
102
  }
103
+ function writeRepoConnection(projectRoot, state) {
104
+ writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
105
+ }
91
106
  function resolveSelectedConnection(projectRoot, options = {}) {
92
107
  const repo = readRepoConnection(projectRoot);
93
108
  if (!repo)
94
109
  return null;
95
110
  if (repo.selected === "local")
96
- return { alias: "local", connection: { kind: "local", mode: "auto" } };
111
+ return { alias: "local", connection: { kind: "local", mode: "auto" }, serverProjectRoot: repo.serverProjectRoot };
97
112
  const global = readGlobalConnections(options);
98
113
  const connection = global.connections[repo.selected];
99
114
  if (!connection) {
100
- throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
115
+ throw new CliError(`Selected Rig server "${repo.selected}" was not found. Run \`rig server list\` or \`rig server use local\`.`, 1);
101
116
  }
102
- return { alias: repo.selected, connection };
117
+ return { alias: repo.selected, connection, serverProjectRoot: repo.serverProjectRoot };
118
+ }
119
+ function writeRepoServerProjectRoot(projectRoot, serverProjectRoot) {
120
+ const repo = readRepoConnection(projectRoot);
121
+ if (!repo)
122
+ return;
123
+ writeRepoConnection(projectRoot, { ...repo, serverProjectRoot });
103
124
  }
104
125
 
105
126
  // packages/cli/src/commands/_server-client.ts
106
- var cachedGitHubBearerToken;
127
+ var scopedGitHubBearerTokens = new Map;
128
+ var serverPhaseListener = null;
129
+ function reportServerPhase(label) {
130
+ serverPhaseListener?.(label);
131
+ }
107
132
  function cleanToken(value) {
108
133
  const trimmed = value?.trim();
109
134
  return trimmed ? trimmed : null;
@@ -120,49 +145,80 @@ function readPrivateRemoteSessionToken(projectRoot) {
120
145
  }
121
146
  }
122
147
  function readGitHubBearerTokenForRemote(projectRoot) {
123
- if (cachedGitHubBearerToken !== undefined)
124
- return cachedGitHubBearerToken;
148
+ const scopedKey = resolve2(projectRoot);
149
+ if (scopedGitHubBearerTokens.has(scopedKey))
150
+ return scopedGitHubBearerTokens.get(scopedKey) ?? null;
125
151
  const privateSession = readPrivateRemoteSessionToken(projectRoot);
126
- if (privateSession) {
127
- cachedGitHubBearerToken = privateSession;
128
- return cachedGitHubBearerToken;
129
- }
130
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
131
- if (envToken) {
132
- cachedGitHubBearerToken = envToken;
133
- return cachedGitHubBearerToken;
152
+ if (privateSession)
153
+ return privateSession;
154
+ return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
155
+ }
156
+ function readStoredGitHubAuthToken(projectRoot) {
157
+ const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
158
+ if (!existsSync2(path))
159
+ return null;
160
+ try {
161
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
162
+ return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
163
+ } catch {
164
+ return null;
134
165
  }
135
- const result = spawnSync("gh", ["auth", "token"], {
136
- encoding: "utf8",
137
- timeout: 5000,
138
- stdio: ["ignore", "pipe", "ignore"]
139
- });
140
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
141
- return cachedGitHubBearerToken;
166
+ }
167
+ function readLocalConnectionFallbackToken(projectRoot) {
168
+ return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
142
169
  }
143
170
  async function ensureServerForCli(projectRoot) {
144
171
  try {
145
172
  const selected = resolveSelectedConnection(projectRoot);
146
173
  if (selected?.connection.kind === "remote") {
174
+ reportServerPhase(`Connecting to ${selected.alias}\u2026`);
175
+ const authToken = readGitHubBearerTokenForRemote(projectRoot);
176
+ const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
147
177
  return {
148
178
  baseUrl: selected.connection.baseUrl,
149
- authToken: readGitHubBearerTokenForRemote(projectRoot),
150
- connectionKind: "remote"
179
+ authToken,
180
+ connectionKind: "remote",
181
+ serverProjectRoot
151
182
  };
152
183
  }
184
+ reportServerPhase("Starting local Rig server\u2026");
153
185
  const connection = await ensureLocalRigServerConnection(projectRoot);
154
186
  return {
155
187
  baseUrl: connection.baseUrl,
156
- authToken: connection.authToken,
157
- connectionKind: "local"
188
+ authToken: connection.authToken ?? readLocalConnectionFallbackToken(projectRoot),
189
+ connectionKind: "local",
190
+ serverProjectRoot: resolve2(projectRoot)
158
191
  };
159
192
  } catch (error) {
160
193
  if (error instanceof Error) {
161
- throw new CliError2(error.message, 1);
194
+ throw new CliError(error.message, 1);
162
195
  }
163
196
  throw error;
164
197
  }
165
198
  }
199
+ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken) {
200
+ const repo = readRepoConnection(projectRoot);
201
+ const slug = repo?.project?.trim();
202
+ if (!slug)
203
+ return null;
204
+ try {
205
+ const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
206
+ headers: mergeHeaders(undefined, authToken)
207
+ });
208
+ if (!response.ok)
209
+ return null;
210
+ const payload = await response.json();
211
+ const project = payload.project && typeof payload.project === "object" && !Array.isArray(payload.project) ? payload.project : null;
212
+ const checkouts = Array.isArray(project?.checkouts) ? project.checkouts : [];
213
+ const latestCheckout = [...checkouts].reverse().find((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.path === "string"));
214
+ const path = typeof latestCheckout?.path === "string" && latestCheckout.path.trim() ? latestCheckout.path.trim() : null;
215
+ if (path)
216
+ writeRepoServerProjectRoot(projectRoot, path);
217
+ return path;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
166
222
  function mergeHeaders(headers, authToken) {
167
223
  const merged = new Headers(headers);
168
224
  if (authToken) {
@@ -185,12 +241,65 @@ function diagnosticMessage(payload) {
185
241
  });
186
242
  return messages.length > 0 ? messages.join("; ") : null;
187
243
  }
244
+ var serverReachabilityCache = new Map;
245
+ async function probeServerReachability(baseUrl, authToken) {
246
+ try {
247
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/server/status`, {
248
+ headers: mergeHeaders(undefined, authToken),
249
+ signal: AbortSignal.timeout(1500)
250
+ });
251
+ return response.ok;
252
+ } catch {
253
+ return false;
254
+ }
255
+ }
256
+ function cachedServerReachability(projectRoot, baseUrl, authToken) {
257
+ const key = resolve2(projectRoot);
258
+ const cached = serverReachabilityCache.get(key);
259
+ if (cached)
260
+ return cached;
261
+ const probe = probeServerReachability(baseUrl, authToken);
262
+ serverReachabilityCache.set(key, probe);
263
+ return probe;
264
+ }
265
+ function describeSelectedServer(projectRoot, server) {
266
+ try {
267
+ const selected = resolveSelectedConnection(projectRoot);
268
+ if (selected) {
269
+ return {
270
+ alias: selected.alias,
271
+ target: selected.connection.kind === "remote" ? selected.connection.baseUrl : server.baseUrl
272
+ };
273
+ }
274
+ } catch {}
275
+ return { alias: server.connectionKind === "remote" ? "remote" : "local", target: server.baseUrl };
276
+ }
277
+ async function buildServerFailureContext(projectRoot, server) {
278
+ const { alias, target } = describeSelectedServer(projectRoot, server);
279
+ const reachable = await cachedServerReachability(projectRoot, server.baseUrl, server.authToken);
280
+ const reachability = reachable ? "server is reachable" : "server is unreachable";
281
+ return {
282
+ contextLine: `Currently connected to: ${alias} at ${target} (${reachability}).`,
283
+ hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
284
+ };
285
+ }
188
286
  async function requestServerJson(context, pathname, init = {}) {
189
287
  const server = await ensureServerForCli(context.projectRoot);
190
- const response = await fetch(`${server.baseUrl}${pathname}`, {
191
- ...init,
192
- headers: mergeHeaders(init.headers, server.authToken)
193
- });
288
+ const headers = mergeHeaders(init.headers, server.authToken);
289
+ if (server.serverProjectRoot)
290
+ headers.set("x-rig-project-root", server.serverProjectRoot);
291
+ reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
292
+ let response;
293
+ try {
294
+ response = await fetch(`${server.baseUrl}${pathname}`, {
295
+ ...init,
296
+ headers
297
+ });
298
+ } catch (error) {
299
+ const failure = await buildServerFailureContext(context.projectRoot, server);
300
+ throw new CliError(`Rig server request failed: ${error instanceof Error ? error.message : String(error)}
301
+ ${failure.contextLine}`, 1, { hint: failure.hint });
302
+ }
194
303
  const text = await response.text();
195
304
  const payload = text.trim().length > 0 ? (() => {
196
305
  try {
@@ -201,11 +310,26 @@ async function requestServerJson(context, pathname, init = {}) {
201
310
  })() : null;
202
311
  if (!response.ok) {
203
312
  const diagnostics = diagnosticMessage(payload);
204
- const detail = diagnostics ?? (text || response.statusText);
205
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
313
+ const rawDetail = diagnostics ?? (text || response.statusText);
314
+ const detail = diagnostics ? rawDetail : rawDetail.split(`
315
+ `).map((line) => line.trim()).find((line) => line.length > 0)?.slice(0, 200) ?? response.statusText;
316
+ const failure = await buildServerFailureContext(context.projectRoot, server);
317
+ throw new CliError(`Rig server request failed (${response.status}): ${detail}
318
+ ${failure.contextLine}`, 1, { hint: failure.hint });
206
319
  }
207
320
  return payload;
208
321
  }
322
+ var RESUMABLE_RUN_STATUSES = new Set([
323
+ "created",
324
+ "preparing",
325
+ "running",
326
+ "validating",
327
+ "reviewing",
328
+ "stopped",
329
+ "failed",
330
+ "needs-attention",
331
+ "needs_attention"
332
+ ]);
209
333
 
210
334
  // packages/cli/src/commands/_snapshot-upload.ts
211
335
  var UPLOADED_SNAPSHOT_PR_MARKER = "<!-- rig:uploaded-snapshot -->";
@@ -0,0 +1,25 @@
1
+ export declare const SPINNER_FRAMES: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2
+ export type TtySpinner = {
3
+ setLabel(label: string): void;
4
+ /** Erase the spinner line so other output can print cleanly. */
5
+ pause(): void;
6
+ /** Resume rendering after pause(). */
7
+ resume(): void;
8
+ /** Stop for good; optionally leave a final line. */
9
+ stop(finalLine?: string): void;
10
+ };
11
+ /**
12
+ * A line-rewriting spinner for async CLI waits. On non-TTY outputs it prints
13
+ * the label once (and label changes) so logs stay readable without ANSI.
14
+ */
15
+ export declare function createTtySpinner(input: {
16
+ label: string;
17
+ output?: Pick<NodeJS.WriteStream, "write"> & {
18
+ readonly isTTY?: boolean;
19
+ };
20
+ intervalMs?: number;
21
+ /** Frame glyph cycle; defaults to SPINNER_FRAMES. */
22
+ frames?: readonly string[];
23
+ /** Optional glyph decorator (e.g. color), applied on TTY renders only. */
24
+ styleFrame?: (frame: string) => string;
25
+ }): TtySpinner;
@@ -0,0 +1,65 @@
1
+ // @bun
2
+ // packages/cli/src/commands/_spinner.ts
3
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4
+ function createTtySpinner(input) {
5
+ const output = input.output ?? process.stdout;
6
+ const isTty = output.isTTY === true;
7
+ const frames = input.frames && input.frames.length > 0 ? input.frames : SPINNER_FRAMES;
8
+ let label = input.label;
9
+ let frame = 0;
10
+ let paused = false;
11
+ let stopped = false;
12
+ let lastPrintedLabel = "";
13
+ const render = () => {
14
+ if (stopped || paused)
15
+ return;
16
+ if (!isTty) {
17
+ if (label !== lastPrintedLabel) {
18
+ output.write(`${label}
19
+ `);
20
+ lastPrintedLabel = label;
21
+ }
22
+ return;
23
+ }
24
+ frame = (frame + 1) % frames.length;
25
+ const glyph = frames[frame] ?? frames[0] ?? "";
26
+ output.write(`\r\x1B[2K${input.styleFrame ? input.styleFrame(glyph) : glyph} ${label}`);
27
+ };
28
+ const clearLine = () => {
29
+ if (isTty)
30
+ output.write("\r\x1B[2K");
31
+ };
32
+ render();
33
+ const timer = isTty ? setInterval(render, input.intervalMs ?? 120) : null;
34
+ return {
35
+ setLabel(next) {
36
+ label = next;
37
+ render();
38
+ },
39
+ pause() {
40
+ paused = true;
41
+ clearLine();
42
+ },
43
+ resume() {
44
+ if (stopped)
45
+ return;
46
+ paused = false;
47
+ render();
48
+ },
49
+ stop(finalLine) {
50
+ if (stopped)
51
+ return;
52
+ stopped = true;
53
+ if (timer)
54
+ clearInterval(timer);
55
+ clearLine();
56
+ if (finalLine)
57
+ output.write(`${finalLine}
58
+ `);
59
+ }
60
+ };
61
+ }
62
+ export {
63
+ createTtySpinner,
64
+ SPINNER_FRAMES
65
+ };
@@ -0,0 +1,9 @@
1
+ import type { WorkspaceTaskRecord } from "./_server-client";
2
+ import { renderTaskPickerRows, type TaskPickerRenderer } from "./_operator-surface";
3
+ export { renderTaskPickerRows, type TaskPickerRenderer };
4
+ export type TaskPickerIO = {
5
+ readonly isTty?: boolean;
6
+ readonly prompt?: (question: string) => Promise<string>;
7
+ readonly renderer?: TaskPickerRenderer;
8
+ };
9
+ export declare function selectTaskWithTextPicker(tasks: readonly WorkspaceTaskRecord[], io?: TaskPickerIO): Promise<WorkspaceTaskRecord | null>;
@@ -1,6 +1,134 @@
1
1
  // @bun
2
- // packages/cli/src/commands/_task-picker.ts
3
- import { createInterface } from "readline/promises";
2
+ // packages/cli/src/app/drone-ui.ts
3
+ import { ProcessTerminal, TUI, Text, Input, SelectList, matchesKey } from "@earendil-works/pi-tui";
4
+
5
+ // packages/cli/src/app/theme.ts
6
+ var RIG_PALETTE = {
7
+ ink: "#f2f3f6",
8
+ ink2: "#aeb0ba",
9
+ ink3: "#6c6e79",
10
+ ink4: "#44464f",
11
+ accent: "#ccff4d",
12
+ accentDim: "#a9d63f",
13
+ cyan: "#56d8ff",
14
+ red: "#ff5d5d",
15
+ yellow: "#ffd24d"
16
+ };
17
+ function hexToRgb(hex) {
18
+ const value = hex.replace("#", "");
19
+ return [
20
+ Number.parseInt(value.slice(0, 2), 16),
21
+ Number.parseInt(value.slice(2, 4), 16),
22
+ Number.parseInt(value.slice(4, 6), 16)
23
+ ];
24
+ }
25
+ function fg(hex) {
26
+ const [r, g, b] = hexToRgb(hex);
27
+ return (text) => `\x1B[38;2;${r};${g};${b}m${text}\x1B[39m`;
28
+ }
29
+ var ink = fg(RIG_PALETTE.ink);
30
+ var ink2 = fg(RIG_PALETTE.ink2);
31
+ var ink3 = fg(RIG_PALETTE.ink3);
32
+ var ink4 = fg(RIG_PALETTE.ink4);
33
+ var accent = fg(RIG_PALETTE.accent);
34
+ var accentDim = fg(RIG_PALETTE.accentDim);
35
+ var cyan = fg(RIG_PALETTE.cyan);
36
+ var red = fg(RIG_PALETTE.red);
37
+ var yellow = fg(RIG_PALETTE.yellow);
38
+ function bold(text) {
39
+ return `\x1B[1m${text}\x1B[22m`;
40
+ }
41
+ var DRONE_ART = [
42
+ " .-=-. .-=-. ",
43
+ " ( !!! ) ( !!! ) ",
44
+ " '-=-'._ _.'-=-' ",
45
+ " '._ _.' ",
46
+ " '=$$$$$$$=.' ",
47
+ " =$$$$$$$$$$$= ",
48
+ " $$$@@@@@@@@@@$$$ ",
49
+ " $$$@@ @@$$$ ",
50
+ " $$@ ? @$$$ ",
51
+ " $$$@ '-' @$$$ ",
52
+ " $$$@@ @@$$$ ",
53
+ " $$$@@@@@@@@@@$$$ ",
54
+ " =$$$$$$$$$$$= ",
55
+ " '=$$$$$$$=.' ",
56
+ " _.' '._ ",
57
+ " .-=-.' '.-=-. ",
58
+ " ( !!! ) ( !!! ) ",
59
+ " '-=-' '-=-' "
60
+ ];
61
+ var EYE_FRAMES = ["@", "o", "."];
62
+ var DRONE_WIDTH = DRONE_ART[0].length;
63
+ var DRONE_HEIGHT = DRONE_ART.length;
64
+ var MICRO_BLADES = ["---", "\\\\\\", "|||", "///"];
65
+ function microDroneFrame(tick) {
66
+ const blade = MICRO_BLADES[tick % MICRO_BLADES.length];
67
+ const eye = EYE_FRAMES[Math.floor(tick / 2) % EYE_FRAMES.length];
68
+ return `(${blade})${eye}(${blade})`;
69
+ }
70
+ var MICRO_DRONE_FRAMES = Array.from({ length: 12 }, (_, index) => microDroneFrame(index));
71
+
72
+ // packages/cli/src/app/drone-ui.ts
73
+ var isTty = () => Boolean(process.stdout.isTTY);
74
+ function droneCancel(text) {
75
+ console.log(` ${red("\u2716")} ${ink3(text)}`);
76
+ }
77
+ var SELECT_THEME = {
78
+ selectedPrefix: (text) => accent(text),
79
+ selectedText: (text) => bold(ink(text)),
80
+ description: (text) => ink3(text),
81
+ scrollInfo: (text) => ink4(text),
82
+ noMatch: (text) => ink3(text)
83
+ };
84
+ async function runMiniTui(build) {
85
+ const terminal = new ProcessTerminal;
86
+ const tui = new TUI(terminal);
87
+ let settled = false;
88
+ return await new Promise((resolve) => {
89
+ const finish = (result) => {
90
+ if (settled)
91
+ return;
92
+ settled = true;
93
+ tui.stop();
94
+ resolve(result);
95
+ };
96
+ build(tui, finish);
97
+ tui.start();
98
+ });
99
+ }
100
+ async function droneSelect(input) {
101
+ if (!isTty() || input.options.length === 0) {
102
+ return input.initialValue ?? input.options[0]?.value ?? null;
103
+ }
104
+ return runMiniTui((tui, finish) => {
105
+ tui.addChild(new Text(` ${accent("\u258D")}${bold(ink(input.message))}`));
106
+ const items = input.options.map((option) => ({
107
+ value: option.value,
108
+ label: option.label,
109
+ ...option.hint ? { description: option.hint } : {}
110
+ }));
111
+ const list = new SelectList(items, Math.min(items.length, 12), SELECT_THEME);
112
+ const initialIndex = input.initialValue ? items.findIndex((item) => item.value === input.initialValue) : -1;
113
+ if (initialIndex > 0)
114
+ list.setSelectedIndex(initialIndex);
115
+ list.onSelect = (item) => finish(item.value);
116
+ list.onCancel = () => finish(null);
117
+ tui.addChild(list);
118
+ tui.addChild(new Text(ink4(" \u2191\u2193 navigate \xB7 enter select \xB7 esc cancel")));
119
+ tui.setFocus(list);
120
+ tui.addInputListener((data) => {
121
+ if (matchesKey(data, "ctrl+c") || matchesKey(data, "escape")) {
122
+ finish(null);
123
+ return { consume: true };
124
+ }
125
+ return;
126
+ });
127
+ });
128
+ }
129
+
130
+ // packages/cli/src/commands/_operator-surface.ts
131
+ import { createInterface as createPromptInterface } from "readline/promises";
4
132
  function taskId(task) {
5
133
  return typeof task.id === "string" && task.id.trim() ? task.id : "<unknown>";
6
134
  }
@@ -13,34 +141,59 @@ function taskStatus(task) {
13
141
  function renderTaskPickerRows(tasks) {
14
142
  return tasks.map((task, index) => `${index + 1}. ${taskId(task)} \xB7 ${taskStatus(task)} \xB7 ${taskTitle(task)}`);
15
143
  }
144
+ async function promptForTaskSelection(question) {
145
+ const rl = createPromptInterface({ input: process.stdin, output: process.stdout });
146
+ try {
147
+ return await rl.question(question);
148
+ } finally {
149
+ rl.close();
150
+ }
151
+ }
152
+
153
+ // packages/cli/src/commands/_task-picker.ts
154
+ function taskId2(task) {
155
+ return typeof task.id === "string" && task.id.trim() ? task.id : "<unknown>";
156
+ }
16
157
  async function selectTaskWithTextPicker(tasks, io = {}) {
17
158
  if (tasks.length === 0)
18
159
  return null;
19
160
  if (tasks.length === 1)
20
161
  return tasks[0];
21
- const isTty = io.isTty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
22
- if (!isTty) {
162
+ const isTty2 = io.isTty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
163
+ if (!isTty2) {
23
164
  throw new Error("task run requires an interactive terminal to pick a task; pass --task <id>, --next, or --detach with a task id.");
24
165
  }
25
- const prompt = io.prompt ?? (async (question) => {
26
- const rl = createInterface({ input: process.stdin, output: process.stdout });
27
- try {
28
- return await rl.question(question);
29
- } finally {
30
- rl.close();
166
+ if (io.prompt || io.renderer) {
167
+ const prompt = io.prompt ?? promptForTaskSelection;
168
+ const renderer = io.renderer ?? { writeLine: (line) => process.stdout.write(`${line}
169
+ `) };
170
+ renderer.writeLine("Select Rig task:");
171
+ for (const row of renderTaskPickerRows(tasks))
172
+ renderer.writeLine(` ${row}`);
173
+ const answer2 = (await prompt(`Task [1-${tasks.length}] or id: `)).trim();
174
+ if (!answer2)
175
+ return null;
176
+ if (/^\d+$/.test(answer2)) {
177
+ const index2 = Number.parseInt(answer2, 10) - 1;
178
+ return tasks[index2] ?? null;
31
179
  }
180
+ return tasks.find((task) => taskId2(task) === answer2) ?? null;
181
+ }
182
+ const options = tasks.map((task, index2) => ({
183
+ value: `${index2}`,
184
+ label: `${taskId2(task)} \xB7 ${typeof task.title === "string" && task.title.trim() ? task.title.trim() : "Untitled task"}`,
185
+ hint: typeof task.status === "string" && task.status.trim() ? task.status.trim() : undefined
186
+ }));
187
+ const answer = await droneSelect({
188
+ message: "Select Rig task",
189
+ options
32
190
  });
33
- console.log("Select Rig task:");
34
- for (const row of renderTaskPickerRows(tasks))
35
- console.log(` ${row}`);
36
- const answer = (await prompt(`Task [1-${tasks.length}] or id: `)).trim();
37
- if (!answer)
191
+ if (answer === null) {
192
+ droneCancel("No task selected.");
38
193
  return null;
39
- if (/^\d+$/.test(answer)) {
40
- const index = Number.parseInt(answer, 10) - 1;
41
- return tasks[index] ?? null;
42
194
  }
43
- return tasks.find((task) => taskId(task) === answer) ?? null;
195
+ const index = Number.parseInt(String(answer), 10);
196
+ return Number.isFinite(index) ? tasks[index] ?? null : null;
44
197
  }
45
198
  export {
46
199
  selectTaskWithTextPicker,
@@ -0,0 +1,3 @@
1
+ import { type RunnerContext } from "../runner";
2
+ import type { CommandOutcome } from "@rig/runtime/control-plane/runtime/types";
3
+ export declare function executeAgent(context: RunnerContext, args: string[]): Promise<CommandOutcome>;