@bli-cockpit/cli 0.1.9 → 0.1.12
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 +25 -14
- package/dist/adapters/agent-image-evidence.js +97 -0
- package/dist/adapters/agent-image-records.js +147 -0
- package/dist/adapters/agent-image-validation.js +88 -0
- package/dist/adapters/raw-evidence.js +145 -16
- package/dist/autostart.js +47 -1
- package/dist/commands/local-args.js +383 -0
- package/dist/commands/local.js +15 -877
- package/dist/commands/public-root.js +3 -0
- package/dist/commands/session-sync.js +504 -0
- package/dist/evidence-upload-client.js +7 -0
- package/dist/local-state.js +1 -1
- package/dist/upload.js +153 -4
- package/package.json +2 -2
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// CLI argument parsing for the local cockpit collector. Split out of
|
|
2
|
+
// commands/local.ts so the command runners read as a table of contents and
|
|
3
|
+
// the parser (pure, independently testable) can change at its own rate.
|
|
4
|
+
//
|
|
5
|
+
// Behavior-preserving extraction: functions moved verbatim, no logic change.
|
|
6
|
+
import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
|
|
7
|
+
export function parseLocalArgs(argv) {
|
|
8
|
+
const command = argv[0];
|
|
9
|
+
switch (command) {
|
|
10
|
+
case "onboard":
|
|
11
|
+
return parseOnboardArgs(argv.slice(1));
|
|
12
|
+
case "install":
|
|
13
|
+
return parseInstallArgs(argv.slice(1));
|
|
14
|
+
case "login":
|
|
15
|
+
case "pair":
|
|
16
|
+
return parseLoginArgs(argv.slice(1));
|
|
17
|
+
case "logout":
|
|
18
|
+
return parseLogoutArgs(argv.slice(1));
|
|
19
|
+
case "start":
|
|
20
|
+
return parseStartArgs(argv.slice(1));
|
|
21
|
+
case "sync":
|
|
22
|
+
return parseSyncArgs(argv.slice(1));
|
|
23
|
+
case "status":
|
|
24
|
+
return parseStatusArgs(argv.slice(1));
|
|
25
|
+
case "sessions":
|
|
26
|
+
return parseSessionsArgs(argv.slice(1));
|
|
27
|
+
case "serve":
|
|
28
|
+
return parseServeArgs(argv.slice(1));
|
|
29
|
+
case "autostart":
|
|
30
|
+
return parseAutostartArgs(argv.slice(1));
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`Unknown local command: ${command ?? ""}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function parseOnboardArgs(args) {
|
|
36
|
+
const values = parseNamedArgs(args, {
|
|
37
|
+
allowedFlags: [
|
|
38
|
+
"--home",
|
|
39
|
+
"--repo",
|
|
40
|
+
"--dashboard-url",
|
|
41
|
+
"--email",
|
|
42
|
+
"--device-name",
|
|
43
|
+
"--ticket",
|
|
44
|
+
"--branch",
|
|
45
|
+
"--json",
|
|
46
|
+
"--poll-interval-ms",
|
|
47
|
+
"--timeout-ms",
|
|
48
|
+
"--max-depth",
|
|
49
|
+
"--max-repos",
|
|
50
|
+
],
|
|
51
|
+
valueFlags: [
|
|
52
|
+
"--home",
|
|
53
|
+
"--repo",
|
|
54
|
+
"--dashboard-url",
|
|
55
|
+
"--email",
|
|
56
|
+
"--device-name",
|
|
57
|
+
"--ticket",
|
|
58
|
+
"--branch",
|
|
59
|
+
"--poll-interval-ms",
|
|
60
|
+
"--timeout-ms",
|
|
61
|
+
"--max-depth",
|
|
62
|
+
"--max-repos",
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
assertNoPositionals(values.positionals, "onboard");
|
|
66
|
+
return {
|
|
67
|
+
kind: "onboard",
|
|
68
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
69
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
70
|
+
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
71
|
+
claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
|
|
72
|
+
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
73
|
+
activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
|
|
74
|
+
branch: optionalNonEmpty(values.flags.get("--branch")),
|
|
75
|
+
json: values.booleans.has("--json"),
|
|
76
|
+
pollIntervalMs: optionalPositiveInteger(values.flags.get("--poll-interval-ms"), "--poll-interval-ms"),
|
|
77
|
+
timeoutMs: optionalPositiveInteger(values.flags.get("--timeout-ms"), "--timeout-ms"),
|
|
78
|
+
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
79
|
+
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function parseInstallArgs(args) {
|
|
83
|
+
const values = parseNamedArgs(args, {
|
|
84
|
+
allowedFlags: [
|
|
85
|
+
"--home",
|
|
86
|
+
"--repo",
|
|
87
|
+
"--dashboard-url",
|
|
88
|
+
"--supabase-url",
|
|
89
|
+
"--json",
|
|
90
|
+
],
|
|
91
|
+
valueFlags: ["--home", "--repo", "--dashboard-url", "--supabase-url"],
|
|
92
|
+
});
|
|
93
|
+
assertNoPositionals(values.positionals, "install");
|
|
94
|
+
return {
|
|
95
|
+
kind: "install",
|
|
96
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
97
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
98
|
+
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
99
|
+
supabaseUrl: optionalNonEmpty(values.flags.get("--supabase-url")),
|
|
100
|
+
json: values.booleans.has("--json"),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function parseLoginArgs(args) {
|
|
104
|
+
const values = parseNamedArgs(args, {
|
|
105
|
+
allowedFlags: [
|
|
106
|
+
"--home",
|
|
107
|
+
"--dashboard-url",
|
|
108
|
+
"--email",
|
|
109
|
+
"--device-name",
|
|
110
|
+
"--json",
|
|
111
|
+
"--poll-interval-ms",
|
|
112
|
+
"--timeout-ms",
|
|
113
|
+
],
|
|
114
|
+
valueFlags: [
|
|
115
|
+
"--home",
|
|
116
|
+
"--dashboard-url",
|
|
117
|
+
"--email",
|
|
118
|
+
"--device-name",
|
|
119
|
+
"--poll-interval-ms",
|
|
120
|
+
"--timeout-ms",
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
assertNoPositionals(values.positionals, "login");
|
|
124
|
+
return {
|
|
125
|
+
kind: "login",
|
|
126
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
127
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
128
|
+
claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
|
|
129
|
+
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
130
|
+
json: values.booleans.has("--json"),
|
|
131
|
+
pollIntervalMs: optionalPositiveInteger(values.flags.get("--poll-interval-ms"), "--poll-interval-ms"),
|
|
132
|
+
timeoutMs: optionalPositiveInteger(values.flags.get("--timeout-ms"), "--timeout-ms"),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function parseLogoutArgs(args) {
|
|
136
|
+
const values = parseNamedArgs(args, {
|
|
137
|
+
allowedFlags: ["--home", "--json"],
|
|
138
|
+
valueFlags: ["--home"],
|
|
139
|
+
});
|
|
140
|
+
assertNoPositionals(values.positionals, "logout");
|
|
141
|
+
return {
|
|
142
|
+
kind: "logout",
|
|
143
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
144
|
+
json: values.booleans.has("--json"),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function parseStartArgs(args) {
|
|
148
|
+
const values = parseNamedArgs(args, {
|
|
149
|
+
allowedFlags: [
|
|
150
|
+
"--home",
|
|
151
|
+
"--repo",
|
|
152
|
+
"--branch",
|
|
153
|
+
"--ticket",
|
|
154
|
+
"--operator-id",
|
|
155
|
+
"--session-id",
|
|
156
|
+
"--json",
|
|
157
|
+
"--max-depth",
|
|
158
|
+
"--max-repos",
|
|
159
|
+
],
|
|
160
|
+
valueFlags: [
|
|
161
|
+
"--home",
|
|
162
|
+
"--repo",
|
|
163
|
+
"--branch",
|
|
164
|
+
"--ticket",
|
|
165
|
+
"--operator-id",
|
|
166
|
+
"--session-id",
|
|
167
|
+
"--max-depth",
|
|
168
|
+
"--max-repos",
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
assertNoPositionals(values.positionals, "start");
|
|
172
|
+
return {
|
|
173
|
+
kind: "start",
|
|
174
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
175
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
176
|
+
branch: optionalNonEmpty(values.flags.get("--branch")),
|
|
177
|
+
activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
|
|
178
|
+
operatorId: optionalNonEmpty(values.flags.get("--operator-id")),
|
|
179
|
+
sessionId: optionalNonEmpty(values.flags.get("--session-id")),
|
|
180
|
+
json: values.booleans.has("--json"),
|
|
181
|
+
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
182
|
+
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function parseSyncArgs(args) {
|
|
186
|
+
const values = parseNamedArgs(args, {
|
|
187
|
+
allowedFlags: ["--home", "--repo", "--dashboard-url", "--json", "--max-depth", "--max-repos"],
|
|
188
|
+
valueFlags: ["--home", "--repo", "--dashboard-url", "--max-depth", "--max-repos"],
|
|
189
|
+
});
|
|
190
|
+
assertNoPositionals(values.positionals, "sync");
|
|
191
|
+
return {
|
|
192
|
+
kind: "sync",
|
|
193
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
194
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
195
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
196
|
+
json: values.booleans.has("--json"),
|
|
197
|
+
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
198
|
+
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function parseStatusArgs(args) {
|
|
202
|
+
const values = parseNamedArgs(args, {
|
|
203
|
+
allowedFlags: ["--home", "--repo", "--json", "--max-depth", "--max-repos"],
|
|
204
|
+
valueFlags: ["--home", "--repo", "--max-depth", "--max-repos"],
|
|
205
|
+
});
|
|
206
|
+
assertNoPositionals(values.positionals, "status");
|
|
207
|
+
return {
|
|
208
|
+
kind: "status",
|
|
209
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
210
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
211
|
+
json: values.booleans.has("--json"),
|
|
212
|
+
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
213
|
+
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function parseSessionsArgs(args) {
|
|
217
|
+
const values = parseNamedArgs(args, {
|
|
218
|
+
allowedFlags: ["--home", "--repo", "--source", "--json", "--max-depth", "--max-repos"],
|
|
219
|
+
valueFlags: ["--home", "--repo", "--source", "--max-depth", "--max-repos"],
|
|
220
|
+
});
|
|
221
|
+
assertNoPositionals(values.positionals, "sessions");
|
|
222
|
+
const source = values.flags.get("--source");
|
|
223
|
+
if (source !== undefined && source !== "codex" && source !== "claude") {
|
|
224
|
+
throw new Error("--source must be 'codex' or 'claude'.");
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
kind: "sessions",
|
|
228
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
229
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
230
|
+
source,
|
|
231
|
+
json: values.booleans.has("--json"),
|
|
232
|
+
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
233
|
+
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function parseServeArgs(args) {
|
|
237
|
+
const values = parseNamedArgs(args, {
|
|
238
|
+
allowedFlags: ["--home", "--repo", "--port"],
|
|
239
|
+
valueFlags: ["--home", "--repo", "--port"],
|
|
240
|
+
});
|
|
241
|
+
assertNoPositionals(values.positionals, "serve");
|
|
242
|
+
const port = Number(values.flags.get("--port") ?? "4174");
|
|
243
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
244
|
+
throw new Error("--port must be a TCP port between 1 and 65535.");
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
kind: "serve",
|
|
248
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
249
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
250
|
+
port,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function parseAutostartArgs(args) {
|
|
254
|
+
const values = parseNamedArgs(args, {
|
|
255
|
+
allowedFlags: [
|
|
256
|
+
"--home",
|
|
257
|
+
"--repo",
|
|
258
|
+
"--dashboard-url",
|
|
259
|
+
"--interval-seconds",
|
|
260
|
+
"--json",
|
|
261
|
+
],
|
|
262
|
+
valueFlags: ["--home", "--repo", "--dashboard-url", "--interval-seconds"],
|
|
263
|
+
});
|
|
264
|
+
if (values.positionals.length > 1) {
|
|
265
|
+
throw new Error("autostart accepts at most one action (install|uninstall|status).");
|
|
266
|
+
}
|
|
267
|
+
const action = values.positionals[0] ?? "install";
|
|
268
|
+
if (action !== "install" && action !== "uninstall" && action !== "status") {
|
|
269
|
+
throw new Error("autostart action must be install, uninstall, or status.");
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
kind: "autostart",
|
|
273
|
+
action,
|
|
274
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
275
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
276
|
+
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
277
|
+
intervalSeconds: optionalPositiveInteger(values.flags.get("--interval-seconds"), "--interval-seconds") ?? 1800,
|
|
278
|
+
json: values.booleans.has("--json"),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function parseNamedArgs(args, options) {
|
|
282
|
+
const allowed = new Set(options.allowedFlags);
|
|
283
|
+
const valueFlags = new Set(options.valueFlags);
|
|
284
|
+
const flags = new Map();
|
|
285
|
+
const booleans = new Set();
|
|
286
|
+
const positionals = [];
|
|
287
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
288
|
+
const arg = args[index] ?? "";
|
|
289
|
+
rejectServiceRoleLikeArgument(arg);
|
|
290
|
+
if (!arg.startsWith("--")) {
|
|
291
|
+
positionals.push(arg);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const [flag, inlineValue] = arg.split("=", 2);
|
|
295
|
+
if (!allowed.has(flag))
|
|
296
|
+
throw new Error(`Unknown flag: ${flag}`);
|
|
297
|
+
if (valueFlags.has(flag)) {
|
|
298
|
+
const value = inlineValue ?? args[index + 1];
|
|
299
|
+
if (!value || value.startsWith("--")) {
|
|
300
|
+
throw new Error(`${flag} requires a value.`);
|
|
301
|
+
}
|
|
302
|
+
rejectServiceRoleLikeArgument(value);
|
|
303
|
+
flags.set(flag, value);
|
|
304
|
+
if (inlineValue === undefined)
|
|
305
|
+
index += 1;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
if (inlineValue !== undefined)
|
|
309
|
+
throw new Error(`${flag} does not accept a value.`);
|
|
310
|
+
booleans.add(flag);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { flags, booleans, positionals };
|
|
314
|
+
}
|
|
315
|
+
function assertNoPositionals(positionals, command) {
|
|
316
|
+
if (positionals.length > 0) {
|
|
317
|
+
throw new Error(`${command} does not accept positional arguments.`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function optionalNonEmpty(value) {
|
|
321
|
+
const trimmed = value?.trim();
|
|
322
|
+
return trimmed ? trimmed : undefined;
|
|
323
|
+
}
|
|
324
|
+
function optionalUrl(value) {
|
|
325
|
+
return value === undefined ? undefined : normalizeUrl(value);
|
|
326
|
+
}
|
|
327
|
+
function optionalEmail(value) {
|
|
328
|
+
const trimmed = value?.trim().toLowerCase();
|
|
329
|
+
if (!trimmed)
|
|
330
|
+
return undefined;
|
|
331
|
+
if (!trimmed.includes("@")) {
|
|
332
|
+
throw new Error("--email must be a valid email address.");
|
|
333
|
+
}
|
|
334
|
+
return trimmed;
|
|
335
|
+
}
|
|
336
|
+
function optionalPositiveInteger(value, flag) {
|
|
337
|
+
if (value === undefined)
|
|
338
|
+
return undefined;
|
|
339
|
+
const parsed = Number(value);
|
|
340
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
341
|
+
throw new Error(`${flag} must be a positive integer.`);
|
|
342
|
+
}
|
|
343
|
+
return parsed;
|
|
344
|
+
}
|
|
345
|
+
export function normalizeUrl(value) {
|
|
346
|
+
const trimmed = value.trim().replace(/\/+$/, "");
|
|
347
|
+
if (!trimmed)
|
|
348
|
+
throw new Error("URL value cannot be empty.");
|
|
349
|
+
return trimmed;
|
|
350
|
+
}
|
|
351
|
+
function rejectServiceRoleLikeArgument(value) {
|
|
352
|
+
if (!looksLikeServiceRoleSecret(value))
|
|
353
|
+
return;
|
|
354
|
+
throw new Error("Service-role credentials are not accepted by local collector commands.");
|
|
355
|
+
}
|
|
356
|
+
function looksLikeServiceRoleSecret(value) {
|
|
357
|
+
if (serviceCredentialNamePattern().test(value))
|
|
358
|
+
return true;
|
|
359
|
+
const parts = value.split(".");
|
|
360
|
+
if (parts.length !== 3)
|
|
361
|
+
return false;
|
|
362
|
+
try {
|
|
363
|
+
const payload = Buffer.from(base64UrlToBase64(parts[1] ?? ""), "base64").toString("utf8");
|
|
364
|
+
return serviceCredentialPayloadPattern().test(payload);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function serviceCredentialNamePattern() {
|
|
371
|
+
return new RegExp([
|
|
372
|
+
["SUPABASE", "SERVICE", "ROLE", "KEY"].join("[_-]?"),
|
|
373
|
+
["service", "role"].join("[_-]?"),
|
|
374
|
+
].join("|"), "i");
|
|
375
|
+
}
|
|
376
|
+
function serviceCredentialPayloadPattern() {
|
|
377
|
+
const privilegedRole = ["service", "role"].join("_");
|
|
378
|
+
return new RegExp(`"role"\\s*:\\s*"${privilegedRole}"`);
|
|
379
|
+
}
|
|
380
|
+
function base64UrlToBase64(value) {
|
|
381
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
382
|
+
return `${normalized}${"=".repeat((4 - (normalized.length % 4)) % 4)}`;
|
|
383
|
+
}
|