@bli-cockpit/cli 0.1.0

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.
@@ -0,0 +1,699 @@
1
+ import { createCollectorServer } from "../server.js";
2
+ import { DEFAULT_DASHBOARD_URL, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, startLocalWorkContext, } from "../local-state.js";
3
+ import { syncLocalAmbientEnvelope } from "../upload.js";
4
+ export const rootCommandNames = new Set([
5
+ "onboard",
6
+ "install",
7
+ "login",
8
+ "pair",
9
+ "logout",
10
+ "start",
11
+ "sync",
12
+ "status",
13
+ "serve",
14
+ ]);
15
+ export async function runLocalCockpitCli(argv, io = defaultIo()) {
16
+ let command;
17
+ try {
18
+ command = parseLocalArgs(argv);
19
+ }
20
+ catch (error) {
21
+ writeLine(io.stderr, errorMessage(error));
22
+ writeLine(io.stderr, "");
23
+ writeLine(io.stderr, localCommandHelp());
24
+ return 1;
25
+ }
26
+ try {
27
+ switch (command.kind) {
28
+ case "install":
29
+ return await runInstall(command, io);
30
+ case "onboard":
31
+ return await runOnboard(command, io);
32
+ case "login":
33
+ return await runLogin(command, io);
34
+ case "logout":
35
+ return await runLogout(command, io);
36
+ case "start":
37
+ return await runStart(command, io);
38
+ case "sync":
39
+ return await runSync(command, io);
40
+ case "status":
41
+ return await runStatus(command, io);
42
+ case "serve":
43
+ return await runServe(command, io);
44
+ }
45
+ }
46
+ catch (error) {
47
+ writeLine(io.stderr, errorMessage(error));
48
+ return 1;
49
+ }
50
+ }
51
+ export function localCommandHelp() {
52
+ return [
53
+ " cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--repo <path>]",
54
+ " cockpit install [--dashboard-url <url>] [--repo <path>]",
55
+ " cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>]",
56
+ " cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>]",
57
+ " cockpit logout",
58
+ " cockpit start [--ticket <id>] [--repo <path>] [--branch <name>]",
59
+ " cockpit sync [--repo <path>] [--dashboard-url <url>]",
60
+ " cockpit status [--repo <path>]",
61
+ " cockpit serve [--port <port>] [--repo <path>]",
62
+ ].join("\n");
63
+ }
64
+ function parseLocalArgs(argv) {
65
+ const command = argv[0];
66
+ switch (command) {
67
+ case "onboard":
68
+ return parseOnboardArgs(argv.slice(1));
69
+ case "install":
70
+ return parseInstallArgs(argv.slice(1));
71
+ case "login":
72
+ case "pair":
73
+ return parseLoginArgs(argv.slice(1));
74
+ case "logout":
75
+ return parseLogoutArgs(argv.slice(1));
76
+ case "start":
77
+ return parseStartArgs(argv.slice(1));
78
+ case "sync":
79
+ return parseSyncArgs(argv.slice(1));
80
+ case "status":
81
+ return parseStatusArgs(argv.slice(1));
82
+ case "serve":
83
+ return parseServeArgs(argv.slice(1));
84
+ default:
85
+ throw new Error(`Unknown local command: ${command ?? ""}`);
86
+ }
87
+ }
88
+ function parseOnboardArgs(args) {
89
+ const values = parseNamedArgs(args, {
90
+ allowedFlags: [
91
+ "--home",
92
+ "--repo",
93
+ "--dashboard-url",
94
+ "--email",
95
+ "--device-name",
96
+ "--ticket",
97
+ "--branch",
98
+ "--json",
99
+ "--poll-interval-ms",
100
+ "--timeout-ms",
101
+ ],
102
+ valueFlags: [
103
+ "--home",
104
+ "--repo",
105
+ "--dashboard-url",
106
+ "--email",
107
+ "--device-name",
108
+ "--ticket",
109
+ "--branch",
110
+ "--poll-interval-ms",
111
+ "--timeout-ms",
112
+ ],
113
+ });
114
+ assertNoPositionals(values.positionals, "onboard");
115
+ return {
116
+ kind: "onboard",
117
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
118
+ repoRoot: optionalNonEmpty(values.flags.get("--repo")),
119
+ dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
120
+ claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
121
+ deviceName: optionalNonEmpty(values.flags.get("--device-name")),
122
+ activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
123
+ branch: optionalNonEmpty(values.flags.get("--branch")),
124
+ json: values.booleans.has("--json"),
125
+ pollIntervalMs: optionalPositiveInteger(values.flags.get("--poll-interval-ms"), "--poll-interval-ms"),
126
+ timeoutMs: optionalPositiveInteger(values.flags.get("--timeout-ms"), "--timeout-ms"),
127
+ };
128
+ }
129
+ function parseInstallArgs(args) {
130
+ const values = parseNamedArgs(args, {
131
+ allowedFlags: [
132
+ "--home",
133
+ "--repo",
134
+ "--dashboard-url",
135
+ "--supabase-url",
136
+ "--json",
137
+ ],
138
+ valueFlags: ["--home", "--repo", "--dashboard-url", "--supabase-url"],
139
+ });
140
+ assertNoPositionals(values.positionals, "install");
141
+ return {
142
+ kind: "install",
143
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
144
+ repoRoot: optionalNonEmpty(values.flags.get("--repo")),
145
+ dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
146
+ supabaseUrl: optionalNonEmpty(values.flags.get("--supabase-url")),
147
+ json: values.booleans.has("--json"),
148
+ };
149
+ }
150
+ function parseLoginArgs(args) {
151
+ const values = parseNamedArgs(args, {
152
+ allowedFlags: [
153
+ "--home",
154
+ "--dashboard-url",
155
+ "--email",
156
+ "--device-name",
157
+ "--json",
158
+ "--poll-interval-ms",
159
+ "--timeout-ms",
160
+ ],
161
+ valueFlags: [
162
+ "--home",
163
+ "--dashboard-url",
164
+ "--email",
165
+ "--device-name",
166
+ "--poll-interval-ms",
167
+ "--timeout-ms",
168
+ ],
169
+ });
170
+ assertNoPositionals(values.positionals, "login");
171
+ return {
172
+ kind: "login",
173
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
174
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
175
+ claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
176
+ deviceName: optionalNonEmpty(values.flags.get("--device-name")),
177
+ json: values.booleans.has("--json"),
178
+ pollIntervalMs: optionalPositiveInteger(values.flags.get("--poll-interval-ms"), "--poll-interval-ms"),
179
+ timeoutMs: optionalPositiveInteger(values.flags.get("--timeout-ms"), "--timeout-ms"),
180
+ };
181
+ }
182
+ function parseLogoutArgs(args) {
183
+ const values = parseNamedArgs(args, {
184
+ allowedFlags: ["--home", "--json"],
185
+ valueFlags: ["--home"],
186
+ });
187
+ assertNoPositionals(values.positionals, "logout");
188
+ return {
189
+ kind: "logout",
190
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
191
+ json: values.booleans.has("--json"),
192
+ };
193
+ }
194
+ function parseStartArgs(args) {
195
+ const values = parseNamedArgs(args, {
196
+ allowedFlags: [
197
+ "--home",
198
+ "--repo",
199
+ "--branch",
200
+ "--ticket",
201
+ "--operator-id",
202
+ "--session-id",
203
+ "--json",
204
+ ],
205
+ valueFlags: [
206
+ "--home",
207
+ "--repo",
208
+ "--branch",
209
+ "--ticket",
210
+ "--operator-id",
211
+ "--session-id",
212
+ ],
213
+ });
214
+ assertNoPositionals(values.positionals, "start");
215
+ return {
216
+ kind: "start",
217
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
218
+ repoRoot: optionalNonEmpty(values.flags.get("--repo")),
219
+ branch: optionalNonEmpty(values.flags.get("--branch")),
220
+ activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
221
+ operatorId: optionalNonEmpty(values.flags.get("--operator-id")),
222
+ sessionId: optionalNonEmpty(values.flags.get("--session-id")),
223
+ json: values.booleans.has("--json"),
224
+ };
225
+ }
226
+ function parseSyncArgs(args) {
227
+ const values = parseNamedArgs(args, {
228
+ allowedFlags: ["--home", "--repo", "--dashboard-url", "--json"],
229
+ valueFlags: ["--home", "--repo", "--dashboard-url"],
230
+ });
231
+ assertNoPositionals(values.positionals, "sync");
232
+ return {
233
+ kind: "sync",
234
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
235
+ repoRoot: optionalNonEmpty(values.flags.get("--repo")),
236
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
237
+ json: values.booleans.has("--json"),
238
+ };
239
+ }
240
+ function parseStatusArgs(args) {
241
+ const values = parseNamedArgs(args, {
242
+ allowedFlags: ["--home", "--repo", "--json"],
243
+ valueFlags: ["--home", "--repo"],
244
+ });
245
+ assertNoPositionals(values.positionals, "status");
246
+ return {
247
+ kind: "status",
248
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
249
+ repoRoot: optionalNonEmpty(values.flags.get("--repo")),
250
+ json: values.booleans.has("--json"),
251
+ };
252
+ }
253
+ function parseServeArgs(args) {
254
+ const values = parseNamedArgs(args, {
255
+ allowedFlags: ["--home", "--repo", "--port"],
256
+ valueFlags: ["--home", "--repo", "--port"],
257
+ });
258
+ assertNoPositionals(values.positionals, "serve");
259
+ const port = Number(values.flags.get("--port") ?? "4174");
260
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
261
+ throw new Error("--port must be a TCP port between 1 and 65535.");
262
+ }
263
+ return {
264
+ kind: "serve",
265
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
266
+ repoRoot: optionalNonEmpty(values.flags.get("--repo")),
267
+ port,
268
+ };
269
+ }
270
+ function parseNamedArgs(args, options) {
271
+ const allowed = new Set(options.allowedFlags);
272
+ const valueFlags = new Set(options.valueFlags);
273
+ const flags = new Map();
274
+ const booleans = new Set();
275
+ const positionals = [];
276
+ for (let index = 0; index < args.length; index += 1) {
277
+ const arg = args[index] ?? "";
278
+ rejectServiceRoleLikeArgument(arg);
279
+ if (!arg.startsWith("--")) {
280
+ positionals.push(arg);
281
+ continue;
282
+ }
283
+ const [flag, inlineValue] = arg.split("=", 2);
284
+ if (!allowed.has(flag))
285
+ throw new Error(`Unknown flag: ${flag}`);
286
+ if (valueFlags.has(flag)) {
287
+ const value = inlineValue ?? args[index + 1];
288
+ if (!value || value.startsWith("--")) {
289
+ throw new Error(`${flag} requires a value.`);
290
+ }
291
+ rejectServiceRoleLikeArgument(value);
292
+ flags.set(flag, value);
293
+ if (inlineValue === undefined)
294
+ index += 1;
295
+ }
296
+ else {
297
+ if (inlineValue !== undefined)
298
+ throw new Error(`${flag} does not accept a value.`);
299
+ booleans.add(flag);
300
+ }
301
+ }
302
+ return { flags, booleans, positionals };
303
+ }
304
+ async function runInstall(command, io) {
305
+ const result = await installLocalCollector(command);
306
+ if (command.json) {
307
+ writeLine(io.stdout, JSON.stringify(result, null, 2));
308
+ return 0;
309
+ }
310
+ writeLine(io.stdout, "Cockpit local collector installed.");
311
+ writeLine(io.stdout, `Config: ${result.paths.config_file}`);
312
+ writeLine(io.stdout, `Session: ${result.paths.session_file}`);
313
+ writeLine(io.stdout, "Auth: missing; upload stays local-only until pairing/login.");
314
+ writeLine(io.stdout, "Next: run `cockpit login`, then `cockpit start --ticket <id>`.");
315
+ return 0;
316
+ }
317
+ async function runOnboard(command, io) {
318
+ let install = null;
319
+ let pair = null;
320
+ let sync = null;
321
+ let status = null;
322
+ try {
323
+ if (!command.json) {
324
+ writeLine(io.stdout, "Cockpit harvest onboarding");
325
+ writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}`);
326
+ writeLine(io.stdout, `Ticket: ${command.activeTicketId ?? "general ambient"}`);
327
+ }
328
+ install = await installLocalCollector({
329
+ homeDir: command.homeDir,
330
+ repoRoot: command.repoRoot,
331
+ dashboardUrl: command.dashboardUrl,
332
+ deviceName: command.deviceName,
333
+ });
334
+ if (!command.json) {
335
+ writeLine(io.stdout, "1/5 Installed local collector.");
336
+ writeLine(io.stdout, `Config: ${install.paths.config_file}`);
337
+ }
338
+ const installedStatus = await inspectLocalCollectorStatus({
339
+ homeDir: command.homeDir,
340
+ repoRoot: command.repoRoot,
341
+ branch: command.branch,
342
+ });
343
+ if (installedStatus.session_state === "valid") {
344
+ if (!command.json) {
345
+ writeLine(io.stdout, "2/5 Existing valid device session found; pairing skipped.");
346
+ }
347
+ }
348
+ else {
349
+ pair = await pairLocalCollector({
350
+ homeDir: command.homeDir,
351
+ dashboardUrl: command.dashboardUrl,
352
+ claimedOwnerEmail: command.claimedOwnerEmail,
353
+ deviceName: command.deviceName,
354
+ pollIntervalMs: command.pollIntervalMs,
355
+ timeoutMs: command.timeoutMs,
356
+ fetch: io.fetch,
357
+ onPairStarted: command.json
358
+ ? undefined
359
+ : (request) => writePairingInstructions(io, request),
360
+ });
361
+ if (!command.json) {
362
+ writeLine(io.stdout, "2/5 Device paired.");
363
+ writeLine(io.stdout, `User: ${pair.session.email ?? pair.session.auth_subject_id}`);
364
+ writeLine(io.stdout, `Device: ${pair.session.device_name ?? pair.session.device_id ?? "unknown"}`);
365
+ }
366
+ }
367
+ const context = await startLocalWorkContext({
368
+ homeDir: command.homeDir,
369
+ repoRoot: command.repoRoot,
370
+ branch: command.branch,
371
+ activeTicketId: command.activeTicketId,
372
+ });
373
+ if (!command.json) {
374
+ writeLine(io.stdout, "3/5 Work context active.");
375
+ writeLine(io.stdout, `Repo: ${context.repo}`);
376
+ writeLine(io.stdout, `Branch: ${context.branch}`);
377
+ writeLine(io.stdout, `Ticket: ${context.active_ticket_id ?? "general ambient"}`);
378
+ writeLine(io.stdout, `Context: ${context.work_context_id}`);
379
+ }
380
+ sync = await syncLocalAmbientEnvelope({
381
+ homeDir: command.homeDir,
382
+ repoRoot: command.repoRoot,
383
+ dashboardUrl: command.dashboardUrl,
384
+ fetch: io.fetch,
385
+ });
386
+ status = await inspectLocalCollectorStatus({
387
+ homeDir: command.homeDir,
388
+ repoRoot: command.repoRoot,
389
+ branch: command.branch,
390
+ });
391
+ if (sync.status !== "uploaded") {
392
+ if (command.json) {
393
+ writeLine(io.stdout, JSON.stringify(onboardResult("blocked", command, install, pair, sync, status), null, 2));
394
+ }
395
+ else {
396
+ writeLine(io.stderr, "BLOCKED: ambient upload failed; safe retry metadata was spooled.");
397
+ writeLine(io.stderr, `Failure: ${sync.failure_reason}`);
398
+ writeLine(io.stderr, `Retry: ${sync.retry_command}`);
399
+ }
400
+ return 1;
401
+ }
402
+ if (command.json) {
403
+ writeLine(io.stdout, JSON.stringify(onboardResult("pass", command, install, pair, sync, status), null, 2));
404
+ return 0;
405
+ }
406
+ writeLine(io.stdout, "4/5 Ambient metadata uploaded.");
407
+ writeLine(io.stdout, `HTTP: ${sync.http_status}`);
408
+ writeLine(io.stdout, `Facts: ${sync.event_count}`);
409
+ writeLine(io.stdout, `Sources: ${sync.source_scan_count}`);
410
+ writeLine(io.stdout, `Risk flags: ${sync.risk_flag_count}`);
411
+ writeLine(io.stdout, "5/5 Status ready.");
412
+ writeLine(io.stdout, `Upload state: ${status.upload_state}`);
413
+ writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
414
+ writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
415
+ return 0;
416
+ }
417
+ catch (error) {
418
+ const message = errorMessage(error);
419
+ status = await inspectLocalCollectorStatus({
420
+ homeDir: command.homeDir,
421
+ repoRoot: command.repoRoot,
422
+ branch: command.branch,
423
+ }).catch(() => null);
424
+ const blocker = classifyOnboardBlocker(message);
425
+ const nextStep = nextStepForOnboardBlocker(blocker);
426
+ if (command.json) {
427
+ writeLine(io.stdout, JSON.stringify({
428
+ ...onboardResult("blocked", command, install, pair, sync, status),
429
+ blocker,
430
+ message,
431
+ next_step: nextStep,
432
+ }, null, 2));
433
+ }
434
+ else {
435
+ writeLine(io.stderr, `BLOCKED: ${message}`);
436
+ writeLine(io.stderr, `Next: ${nextStep}`);
437
+ }
438
+ return 1;
439
+ }
440
+ }
441
+ async function runLogin(command, io) {
442
+ const result = await pairLocalCollector({
443
+ homeDir: command.homeDir,
444
+ dashboardUrl: command.dashboardUrl,
445
+ claimedOwnerEmail: command.claimedOwnerEmail,
446
+ deviceName: command.deviceName,
447
+ pollIntervalMs: command.pollIntervalMs,
448
+ timeoutMs: command.timeoutMs,
449
+ fetch: io.fetch,
450
+ onPairStarted: command.json
451
+ ? undefined
452
+ : (request) => {
453
+ writeLine(io.stdout, "Cockpit device pairing started.");
454
+ writeLine(io.stdout, `Open: ${request.approve_url}`);
455
+ writeLine(io.stdout, `Code: ${request.user_code}`);
456
+ writeLine(io.stdout, "Waiting for dashboard approval...");
457
+ },
458
+ });
459
+ if (command.json) {
460
+ writeLine(io.stdout, JSON.stringify(result, null, 2));
461
+ return 0;
462
+ }
463
+ writeLine(io.stdout, "Cockpit collector paired.");
464
+ writeLine(io.stdout, `Session: ${result.session_file}`);
465
+ writeLine(io.stdout, `User: ${result.session.email ?? result.session.auth_subject_id}`);
466
+ writeLine(io.stdout, `Device: ${result.session.device_name ?? result.session.device_id ?? "unknown"}`);
467
+ writeLine(io.stdout, "Next: run `cockpit start` inside the repo.");
468
+ return 0;
469
+ }
470
+ function writePairingInstructions(io, request) {
471
+ writeLine(io.stdout, "2/5 Device pairing started.");
472
+ writeLine(io.stdout, `Open: ${request.approve_url}`);
473
+ writeLine(io.stdout, `Code: ${request.user_code}`);
474
+ writeLine(io.stdout, "Waiting for dashboard approval...");
475
+ }
476
+ function onboardResult(resultStatus, command, install, pair, sync, status) {
477
+ return {
478
+ status: resultStatus,
479
+ dashboard_url: command.dashboardUrl,
480
+ ticket_id: sync?.ticket_id ?? command.activeTicketId ?? null,
481
+ work_context_id: sync?.work_context_id ?? status?.work_context_id ?? null,
482
+ config_file: install?.paths.config_file ?? status?.config_file ?? null,
483
+ session_file: install?.paths.session_file ?? status?.session_file ?? null,
484
+ paired_user: pair?.session.email ?? pair?.session.auth_subject_id ?? null,
485
+ paired_device: pair?.session.device_name ?? pair?.session.device_id ?? null,
486
+ upload_status: sync?.status ?? null,
487
+ upload_state: status?.upload_state ?? null,
488
+ event_count: sync?.event_count ?? null,
489
+ source_scan_count: sync?.source_scan_count ?? null,
490
+ risk_flag_count: sync?.risk_flag_count ?? null,
491
+ pending_upload_count: status?.pending_upload_count ?? null,
492
+ last_upload_success_at: status?.last_upload_success_at ?? null,
493
+ next_dashboard_path: `${command.dashboardUrl}/my-work`,
494
+ };
495
+ }
496
+ function classifyOnboardBlocker(message) {
497
+ if (/ticket/i.test(message))
498
+ return "ticket";
499
+ if (/pair|paired|approval|expired|revoked/i.test(message))
500
+ return "device_pairing";
501
+ if (/fetch|network|ENOTFOUND|ECONNREFUSED|HTTP/i.test(message))
502
+ return "network_or_ingest";
503
+ if (/config|install/i.test(message))
504
+ return "install";
505
+ if (/context|start/i.test(message))
506
+ return "work_context";
507
+ if (/bound|binding/i.test(message))
508
+ return "ticket_binding";
509
+ return "unknown";
510
+ }
511
+ function nextStepForOnboardBlocker(blocker) {
512
+ switch (blocker) {
513
+ case "ticket":
514
+ case "ticket_binding":
515
+ return "Run `cockpit start --ticket <id>` when actual ticket work begins, then run `cockpit sync`.";
516
+ case "device_pairing":
517
+ return "Open the pairing URL, approve the exact code in Cockpit, then rerun `cockpit onboard`.";
518
+ case "network_or_ingest":
519
+ return "Check dashboard URL/network, then run `cockpit sync --json` or rerun `cockpit onboard`.";
520
+ case "install":
521
+ return "Rerun `cockpit onboard` from the repo root; it will reinstall local config.";
522
+ case "work_context":
523
+ return "Run `cockpit start --ticket <id> --repo \"$PWD\"`, then retry `cockpit sync`.";
524
+ default:
525
+ return "Run `cockpit status --json` and report the blocker label plus last failure reason.";
526
+ }
527
+ }
528
+ async function runLogout(command, io) {
529
+ const result = await logoutLocalCollector({ homeDir: command.homeDir });
530
+ if (command.json) {
531
+ writeLine(io.stdout, JSON.stringify(result, null, 2));
532
+ return 0;
533
+ }
534
+ writeLine(io.stdout, result.removed
535
+ ? "Cockpit collector session removed."
536
+ : "No Cockpit collector session found.");
537
+ return 0;
538
+ }
539
+ async function runStart(command, io) {
540
+ const context = await startLocalWorkContext(command);
541
+ if (command.json) {
542
+ writeLine(io.stdout, JSON.stringify(context, null, 2));
543
+ return 0;
544
+ }
545
+ writeLine(io.stdout, "Cockpit work context active.");
546
+ writeLine(io.stdout, `Repo: ${context.repo}`);
547
+ writeLine(io.stdout, `Branch: ${context.branch}`);
548
+ writeLine(io.stdout, `Ticket: ${displayTicketId(context.active_ticket_id)}`);
549
+ writeLine(io.stdout, `Context: ${context.work_context_id}`);
550
+ return 0;
551
+ }
552
+ async function runSync(command, io) {
553
+ const result = await syncLocalAmbientEnvelope({
554
+ homeDir: command.homeDir,
555
+ repoRoot: command.repoRoot,
556
+ dashboardUrl: command.dashboardUrl,
557
+ fetch: io.fetch,
558
+ });
559
+ if (command.json) {
560
+ writeLine(io.stdout, JSON.stringify(result, null, 2));
561
+ return result.status === "uploaded" ? 0 : 1;
562
+ }
563
+ if (result.status === "uploaded") {
564
+ writeLine(io.stdout, "Cockpit ambient envelope uploaded.");
565
+ writeLine(io.stdout, `Ticket: ${displayTicketId(result.ticket_id)}`);
566
+ writeLine(io.stdout, `Context: ${result.work_context_id}`);
567
+ writeLine(io.stdout, `Facts: ${result.event_count}`);
568
+ writeLine(io.stdout, `Risk flags: ${result.risk_flag_count}`);
569
+ return 0;
570
+ }
571
+ writeLine(io.stderr, "Cockpit ambient upload failed; safe retry metadata was spooled.");
572
+ writeLine(io.stderr, `Ticket: ${displayTicketId(result.ticket_id)}`);
573
+ writeLine(io.stderr, `Failure: ${result.failure_reason}`);
574
+ writeLine(io.stderr, `Retry: ${result.retry_command}`);
575
+ return 1;
576
+ }
577
+ async function runStatus(command, io) {
578
+ const status = await inspectLocalCollectorStatus(command);
579
+ if (command.json) {
580
+ writeLine(io.stdout, JSON.stringify(status, null, 2));
581
+ return 0;
582
+ }
583
+ writeLine(io.stdout, "Cockpit local status");
584
+ writeLine(io.stdout, `installed: ${status.installed}`);
585
+ writeLine(io.stdout, `session_state: ${status.session_state}`);
586
+ writeLine(io.stdout, `repo: ${status.repo}`);
587
+ writeLine(io.stdout, `branch: ${status.branch}`);
588
+ writeLine(io.stdout, `ticket: ${displayTicketId(status.active_ticket_id)}`);
589
+ writeLine(io.stdout, `collector_freshness: ${status.collector_freshness}`);
590
+ writeLine(io.stdout, `upload_state: ${status.upload_state}`);
591
+ writeLine(io.stdout, `last_upload_attempt: ${status.last_upload_attempt_at ?? "never"}`);
592
+ writeLine(io.stdout, `last_upload_success: ${status.last_upload_success_at ?? "never"}`);
593
+ writeLine(io.stdout, `last_upload_failure: ${status.last_upload_failure_reason ?? "none"}`);
594
+ writeLine(io.stdout, `pending_uploads: ${status.pending_upload_count}`);
595
+ for (const detail of status.details)
596
+ writeLine(io.stdout, `- ${detail}`);
597
+ return 0;
598
+ }
599
+ function displayTicketId(ticketId) {
600
+ return ticketId ?? "general ambient";
601
+ }
602
+ async function runServe(command, io) {
603
+ const server = createCollectorServer(command);
604
+ await new Promise((resolve) => {
605
+ server.listen(command.port, "127.0.0.1", resolve);
606
+ });
607
+ writeLine(io.stdout, `Cockpit collector serving on http://127.0.0.1:${command.port}`);
608
+ await new Promise((resolve) => {
609
+ server.on("close", resolve);
610
+ });
611
+ return 0;
612
+ }
613
+ function assertNoPositionals(positionals, command) {
614
+ if (positionals.length > 0) {
615
+ throw new Error(`${command} does not accept positional arguments.`);
616
+ }
617
+ }
618
+ function optionalNonEmpty(value) {
619
+ const trimmed = value?.trim();
620
+ return trimmed ? trimmed : undefined;
621
+ }
622
+ function optionalUrl(value) {
623
+ return value === undefined ? undefined : normalizeUrl(value);
624
+ }
625
+ function optionalEmail(value) {
626
+ const trimmed = value?.trim().toLowerCase();
627
+ if (!trimmed)
628
+ return undefined;
629
+ if (!trimmed.includes("@")) {
630
+ throw new Error("--email must be a valid email address.");
631
+ }
632
+ return trimmed;
633
+ }
634
+ function optionalPositiveInteger(value, flag) {
635
+ if (value === undefined)
636
+ return undefined;
637
+ const parsed = Number(value);
638
+ if (!Number.isInteger(parsed) || parsed < 1) {
639
+ throw new Error(`${flag} must be a positive integer.`);
640
+ }
641
+ return parsed;
642
+ }
643
+ function normalizeUrl(value) {
644
+ const trimmed = value.trim().replace(/\/+$/, "");
645
+ if (!trimmed)
646
+ throw new Error("URL value cannot be empty.");
647
+ return trimmed;
648
+ }
649
+ function rejectServiceRoleLikeArgument(value) {
650
+ if (!looksLikeServiceRoleSecret(value))
651
+ return;
652
+ throw new Error("Service-role credentials are not accepted by local collector commands.");
653
+ }
654
+ function looksLikeServiceRoleSecret(value) {
655
+ if (serviceCredentialNamePattern().test(value))
656
+ return true;
657
+ const parts = value.split(".");
658
+ if (parts.length !== 3)
659
+ return false;
660
+ try {
661
+ const payload = Buffer.from(base64UrlToBase64(parts[1] ?? ""), "base64").toString("utf8");
662
+ return serviceCredentialPayloadPattern().test(payload);
663
+ }
664
+ catch {
665
+ return false;
666
+ }
667
+ }
668
+ function serviceCredentialNamePattern() {
669
+ return new RegExp([
670
+ ["SUPABASE", "SERVICE", "ROLE", "KEY"].join("[_-]?"),
671
+ ["service", "role"].join("[_-]?"),
672
+ ].join("|"), "i");
673
+ }
674
+ function serviceCredentialPayloadPattern() {
675
+ const privilegedRole = ["service", "role"].join("_");
676
+ return new RegExp(`"role"\\s*:\\s*"${privilegedRole}"`);
677
+ }
678
+ function base64UrlToBase64(value) {
679
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
680
+ return `${normalized}${"=".repeat((4 - (normalized.length % 4)) % 4)}`;
681
+ }
682
+ function defaultIo() {
683
+ if (!globalThis.fetch) {
684
+ throw new Error("global fetch is unavailable; use Node.js 20 or newer.");
685
+ }
686
+ return {
687
+ stdin: process.stdin,
688
+ stdout: process.stdout,
689
+ stderr: process.stderr,
690
+ env: process.env,
691
+ fetch: globalThis.fetch.bind(globalThis),
692
+ };
693
+ }
694
+ function writeLine(stream, text) {
695
+ stream.write(`${text}\n`);
696
+ }
697
+ function errorMessage(error) {
698
+ return error instanceof Error ? error.message : String(error);
699
+ }