@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.
- package/README.md +1 -1
- package/dist/bin/build-rig-binaries.d.ts +2 -0
- package/dist/bin/rig.d.ts +2 -0
- package/dist/bin/rig.js +11052 -6668
- package/dist/src/app/board.d.ts +23 -0
- package/dist/src/app/board.js +1786 -0
- package/dist/src/app/drone-ui.d.ts +37 -0
- package/dist/src/app/drone-ui.js +294 -0
- package/dist/src/app/theme.d.ts +47 -0
- package/dist/src/app/theme.js +150 -0
- package/dist/src/commands/_async-ui.d.ts +13 -0
- package/dist/src/commands/_async-ui.js +223 -0
- package/dist/src/commands/_authority-runs.d.ts +22 -0
- package/dist/src/commands/_authority-runs.js +2 -3
- package/dist/src/commands/_cli-format.d.ts +49 -0
- package/dist/src/commands/_cli-format.js +447 -0
- package/dist/src/commands/_connection-state.d.ts +44 -0
- package/dist/src/commands/_connection-state.js +30 -11
- package/dist/src/commands/_doctor-checks.d.ts +52 -0
- package/dist/src/commands/_doctor-checks.js +180 -44
- package/dist/src/commands/_help-catalog.d.ts +51 -0
- package/dist/src/commands/_help-catalog.js +442 -0
- package/dist/src/commands/_json-output.d.ts +11 -0
- package/dist/src/commands/_json-output.js +56 -0
- package/dist/src/commands/_operator-surface.d.ts +34 -0
- package/dist/src/commands/_operator-surface.js +220 -0
- package/dist/src/commands/_operator-view.d.ts +30 -0
- package/dist/src/commands/_operator-view.js +803 -73
- package/dist/src/commands/_parsers.d.ts +15 -0
- package/dist/src/commands/_parsers.js +18 -11
- package/dist/src/commands/_paths.d.ts +11 -0
- package/dist/src/commands/_pi-frontend.d.ts +27 -0
- package/dist/src/commands/_pi-frontend.js +742 -0
- package/dist/src/commands/_pi-install.d.ts +42 -0
- package/dist/src/commands/_pi-install.js +5 -4
- package/dist/src/commands/_policy.d.ts +8 -0
- package/dist/src/commands/_policy.js +12 -5
- package/dist/src/commands/_preflight.d.ts +22 -0
- package/dist/src/commands/_preflight.js +190 -128
- package/dist/src/commands/_probes.d.ts +1 -0
- package/dist/src/commands/_run-driver-helpers.d.ts +99 -0
- package/dist/src/commands/_run-driver-helpers.js +75 -22
- package/dist/src/commands/_run-replay.d.ts +24 -0
- package/dist/src/commands/_run-replay.js +142 -0
- package/dist/src/commands/_server-client.d.ts +186 -0
- package/dist/src/commands/_server-client.js +360 -61
- package/dist/src/commands/_snapshot-upload.d.ts +39 -0
- package/dist/src/commands/_snapshot-upload.js +163 -39
- package/dist/src/commands/_spinner.d.ts +25 -0
- package/dist/src/commands/_spinner.js +65 -0
- package/dist/src/commands/_task-picker.d.ts +9 -0
- package/dist/src/commands/_task-picker.js +172 -19
- package/dist/src/commands/agent.d.ts +3 -0
- package/dist/src/commands/agent.js +39 -20
- package/dist/src/commands/browser.d.ts +65 -0
- package/dist/src/commands/browser.js +334 -51
- package/dist/src/commands/connect.d.ts +7 -0
- package/dist/src/commands/connect.js +272 -33
- package/dist/src/commands/dist.d.ts +28 -0
- package/dist/src/commands/dist.js +19 -12
- package/dist/src/commands/doctor.d.ts +3 -0
- package/dist/src/commands/doctor.js +378 -45
- package/dist/src/commands/github.d.ts +3 -0
- package/dist/src/commands/github.js +375 -53
- package/dist/src/commands/inbox.d.ts +30 -0
- package/dist/src/commands/inbox.js +760 -70
- package/dist/src/commands/init.d.ts +74 -0
- package/dist/src/commands/init.js +963 -119
- package/dist/src/commands/inspect.d.ts +3 -0
- package/dist/src/commands/inspect.js +588 -32
- package/dist/src/commands/inspector.d.ts +3 -0
- package/dist/src/commands/inspector.js +20 -13
- package/dist/src/commands/pi.d.ts +3 -0
- package/dist/src/commands/pi.js +177 -0
- package/dist/src/commands/plugin.d.ts +16 -0
- package/dist/src/commands/plugin.js +95 -27
- package/dist/src/commands/profile-and-review.d.ts +4 -0
- package/dist/src/commands/profile-and-review.js +26 -19
- package/dist/src/commands/queue.d.ts +3 -0
- package/dist/src/commands/queue.js +32 -12
- package/dist/src/commands/remote.d.ts +3 -0
- package/dist/src/commands/remote.js +43 -36
- package/dist/src/commands/repo-git-harness.d.ts +5 -0
- package/dist/src/commands/repo-git-harness.js +22 -15
- package/dist/src/commands/run.d.ts +3 -0
- package/dist/src/commands/run.js +1379 -159
- package/dist/src/commands/server.d.ts +7 -0
- package/dist/src/commands/server.js +502 -57
- package/dist/src/commands/setup.d.ts +16 -0
- package/dist/src/commands/setup.js +390 -63
- package/dist/src/commands/stats.d.ts +12 -0
- package/dist/src/commands/stats.js +1051 -0
- package/dist/src/commands/task-report-bug.d.ts +19 -0
- package/dist/src/commands/task-report-bug.js +260 -62
- package/dist/src/commands/task-run-driver.d.ts +132 -0
- package/dist/src/commands/task-run-driver.js +886 -130
- package/dist/src/commands/task.d.ts +14 -0
- package/dist/src/commands/task.js +1644 -314
- package/dist/src/commands/test.d.ts +3 -0
- package/dist/src/commands/test.js +15 -8
- package/dist/src/commands/workspace.d.ts +3 -0
- package/dist/src/commands/workspace.js +18 -11
- package/dist/src/commands.d.ts +29 -0
- package/dist/src/commands.js +10967 -6637
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +10168 -5787
- package/dist/src/launcher.d.ts +61 -0
- package/dist/src/launcher.js +77 -13
- package/dist/src/report-bug.d.ts +44 -0
- package/dist/src/report-bug.js +3 -3
- package/dist/src/runner.d.ts +47 -0
- package/dist/src/runner.js +16 -22
- package/dist/src/withMutedConsole.d.ts +2 -0
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
|
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
|
|
191
|
-
|
|
192
|
-
headers
|
|
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
|
|
205
|
-
|
|
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/
|
|
3
|
-
import {
|
|
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
|
|
22
|
-
if (!
|
|
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
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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,
|