@bli-cockpit/cli 0.2.0 → 0.2.2
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 +12 -2
- package/dist/commands/doctor.js +502 -0
- package/dist/commands/local-args.js +22 -0
- package/dist/commands/local.js +351 -44
- package/dist/commands/public-root.js +1 -0
- package/dist/onboarding-roots.js +183 -18
- package/dist/raw-evidence-gc.js +122 -0
- package/package.json +1 -1
package/dist/commands/local.js
CHANGED
|
@@ -5,21 +5,25 @@ import path from "node:path";
|
|
|
5
5
|
import { createCollectorServer } from "../server.js";
|
|
6
6
|
import { inspectAgentRules, installAgentRules, uninstallAgentRules, } from "../agent-rules.js";
|
|
7
7
|
import { runBackfillCommand } from "./backfill.js";
|
|
8
|
+
import { runDoctor } from "./doctor.js";
|
|
8
9
|
import { inspectBackfillLock } from "../backfill-lock.js";
|
|
9
10
|
import { parseLocalArgs, normalizeUrl } from "./local-args.js";
|
|
10
11
|
import { autostartStatus, installAutostartAgent, uninstallAutostartAgent, } from "../autostart.js";
|
|
11
|
-
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
|
|
12
|
+
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
|
|
12
13
|
import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, defaultCodexSessionDirs, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
13
14
|
import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
|
|
14
15
|
import { backfillCompletionMarkerPath, readBackfillCursor, } from "../cursors/backfill-cursor.js";
|
|
15
16
|
import { acquireSyncLock } from "../sync-lock.js";
|
|
16
17
|
import { discoverGitWorktrees } from "../repo-identity.js";
|
|
17
18
|
import { runAttributedWorktreeSync, } from "./session-sync.js";
|
|
18
|
-
import { COLLECTION_ROOT_REQUIRED, resolveOnboardingRoots, } from "../onboarding-roots.js";
|
|
19
|
+
import { COLLECTION_ROOT_REQUIRED, missingCollectionRootMessage, normalizeRootsDetailed, resolveOnboardingRoots, rootRejectionExplanation, } from "../onboarding-roots.js";
|
|
20
|
+
import { rawEvidenceGcSummary, runRawEvidenceLocalGc, } from "../raw-evidence-gc.js";
|
|
19
21
|
export const rootCommandNames = new Set([
|
|
20
22
|
"onboard",
|
|
21
23
|
"update",
|
|
22
24
|
"upgrade",
|
|
25
|
+
"do-everything",
|
|
26
|
+
"fix",
|
|
23
27
|
"install",
|
|
24
28
|
"login",
|
|
25
29
|
"pair",
|
|
@@ -57,6 +61,11 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
57
61
|
return await runOnboard(command, io);
|
|
58
62
|
case "update":
|
|
59
63
|
return await runUpdate(command, io);
|
|
64
|
+
case "doctor":
|
|
65
|
+
return await runDoctor(command, io, {
|
|
66
|
+
reportInstallEvents: reportInstallEventsBestEffort,
|
|
67
|
+
selfUpdate: runSelfUpdate,
|
|
68
|
+
});
|
|
60
69
|
case "login":
|
|
61
70
|
return await runLogin(command, io);
|
|
62
71
|
case "logout":
|
|
@@ -90,10 +99,12 @@ export function localCommandHelp(command) {
|
|
|
90
99
|
if (command)
|
|
91
100
|
return localSubcommandHelp(command);
|
|
92
101
|
return [
|
|
93
|
-
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--no-auth] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
94
|
-
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--no-auth] [--json]",
|
|
102
|
+
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--branch <name>] [--no-auth] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
103
|
+
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--no-auth] [--json]",
|
|
95
104
|
" cockpit upgrade [same flags as update]",
|
|
96
|
-
" cockpit
|
|
105
|
+
" cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
106
|
+
" cockpit fix [same flags as do-everything]",
|
|
107
|
+
" cockpit install [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--json]",
|
|
97
108
|
" cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
98
109
|
" cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
99
110
|
" cockpit logout",
|
|
@@ -115,7 +126,7 @@ function localSubcommandHelp(command) {
|
|
|
115
126
|
[
|
|
116
127
|
"onboard",
|
|
117
128
|
[
|
|
118
|
-
"Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--no-auth] [--json]",
|
|
129
|
+
"Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--branch <name>] [--no-auth] [--json]",
|
|
119
130
|
"",
|
|
120
131
|
"Installs, pairs, starts work context(s), syncs once, and prints readiness proof.",
|
|
121
132
|
"If --workspace is a parent folder, scans child git repos/worktrees and rolls them up by repo.",
|
|
@@ -129,7 +140,7 @@ function localSubcommandHelp(command) {
|
|
|
129
140
|
[
|
|
130
141
|
"install",
|
|
131
142
|
[
|
|
132
|
-
"Usage: cockpit install [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
143
|
+
"Usage: cockpit install [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--json]",
|
|
133
144
|
"",
|
|
134
145
|
"Writes local collector config. Pair with `cockpit login`, then run `cockpit start` when work begins.",
|
|
135
146
|
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
@@ -139,7 +150,7 @@ function localSubcommandHelp(command) {
|
|
|
139
150
|
[
|
|
140
151
|
"update",
|
|
141
152
|
[
|
|
142
|
-
"Usage: cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--no-auth] [--json]",
|
|
153
|
+
"Usage: cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--no-auth] [--json]",
|
|
143
154
|
"",
|
|
144
155
|
"Updates the global public CLI from npm, then reruns `cockpit onboard`",
|
|
145
156
|
"with the same setup flags so pairing, saved roots, agent rules,",
|
|
@@ -155,6 +166,24 @@ function localSubcommandHelp(command) {
|
|
|
155
166
|
"Alias for `cockpit update`.",
|
|
156
167
|
],
|
|
157
168
|
],
|
|
169
|
+
[
|
|
170
|
+
"do-everything",
|
|
171
|
+
[
|
|
172
|
+
"Usage: cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
173
|
+
"",
|
|
174
|
+
"Converges an already-onboarded intern machine: latest CLI, auth, saved roots, autostart, backfill, raw-evidence GC, and sync freshness.",
|
|
175
|
+
"`cockpit fix` is an alias.",
|
|
176
|
+
"--dry-run prints the checks and would-fix steps without writing config, plists, cursors, or install telemetry.",
|
|
177
|
+
],
|
|
178
|
+
],
|
|
179
|
+
[
|
|
180
|
+
"fix",
|
|
181
|
+
[
|
|
182
|
+
"Usage: cockpit fix [same flags as cockpit do-everything]",
|
|
183
|
+
"",
|
|
184
|
+
"Alias for `cockpit do-everything`.",
|
|
185
|
+
],
|
|
186
|
+
],
|
|
158
187
|
[
|
|
159
188
|
"login",
|
|
160
189
|
[
|
|
@@ -294,49 +323,97 @@ function isLocalHelpRequest(argv) {
|
|
|
294
323
|
return argv.length === 2 && (argv[1] === "--help" || argv[1] === "-h");
|
|
295
324
|
}
|
|
296
325
|
async function runInstall(command, io) {
|
|
297
|
-
const
|
|
326
|
+
const installEvents = [];
|
|
327
|
+
const finish = async (code) => {
|
|
328
|
+
await reportInstallEventsBestEffort({
|
|
329
|
+
homeDir: command.homeDir,
|
|
330
|
+
dashboardUrl: command.dashboardUrl,
|
|
331
|
+
command: "install",
|
|
332
|
+
events: installEvents,
|
|
333
|
+
json: command.json,
|
|
334
|
+
io,
|
|
335
|
+
});
|
|
336
|
+
return code;
|
|
337
|
+
};
|
|
338
|
+
const resolved = resolveInstallCommandRoots(command);
|
|
339
|
+
if (resolved.homeRootOptIn) {
|
|
340
|
+
addInstallEvent(installEvents, "home_root_optin", "ok");
|
|
341
|
+
}
|
|
342
|
+
const result = await installLocalCollector(resolved.command);
|
|
343
|
+
addInstallEvent(installEvents, "install", "ok");
|
|
298
344
|
if (command.json) {
|
|
299
345
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
300
|
-
return 0;
|
|
346
|
+
return finish(0);
|
|
301
347
|
}
|
|
302
348
|
writeLine(io.stdout, "Cockpit local collector installed.");
|
|
303
349
|
writeLine(io.stdout, `Config: ${result.paths.config_file}`);
|
|
304
350
|
writeLine(io.stdout, `Session: ${result.paths.session_file}`);
|
|
305
351
|
writeLine(io.stdout, "Auth: missing; upload stays local-only until pairing/login.");
|
|
306
352
|
writeLine(io.stdout, "Next: run `cockpit login`, then `cockpit start` inside the repo; add `--ticket <id>` only when ticket work begins.");
|
|
307
|
-
return 0;
|
|
353
|
+
return finish(0);
|
|
354
|
+
}
|
|
355
|
+
function resolveInstallCommandRoots(command) {
|
|
356
|
+
const detailed = normalizeRootsDetailed([command.repoRoot ?? process.cwd()], {
|
|
357
|
+
homeDir: command.homeDir,
|
|
358
|
+
allowHomeRoot: command.allowHomeRoot,
|
|
359
|
+
});
|
|
360
|
+
if (detailed.roots.length > 0) {
|
|
361
|
+
const root = detailed.roots[0];
|
|
362
|
+
return {
|
|
363
|
+
command: {
|
|
364
|
+
...command,
|
|
365
|
+
repoRoot: root,
|
|
366
|
+
},
|
|
367
|
+
homeRootOptIn: Boolean(command.allowHomeRoot) &&
|
|
368
|
+
path.resolve(root) === path.resolve(command.homeDir ?? os.homedir()),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (detailed.rejected.length > 0) {
|
|
372
|
+
throw new Error(`${COLLECTION_ROOT_REQUIRED}: ${rootRejectionExplanation(detailed.rejected[0], command)}`);
|
|
373
|
+
}
|
|
374
|
+
throw new Error(`${COLLECTION_ROOT_REQUIRED}: ${missingCollectionRootMessage(command)}`);
|
|
308
375
|
}
|
|
309
376
|
async function runUpdate(command, io) {
|
|
377
|
+
const installEvents = [];
|
|
378
|
+
const finish = async (code) => {
|
|
379
|
+
await reportInstallEventsBestEffort({
|
|
380
|
+
homeDir: command.homeDir,
|
|
381
|
+
dashboardUrl: command.dashboardUrl,
|
|
382
|
+
command: "update",
|
|
383
|
+
events: installEvents,
|
|
384
|
+
json: command.json,
|
|
385
|
+
io,
|
|
386
|
+
});
|
|
387
|
+
return code;
|
|
388
|
+
};
|
|
310
389
|
const exec = io.exec ?? defaultExec();
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
"-g",
|
|
314
|
-
"@bli-cockpit/cli@latest",
|
|
315
|
-
"--prefer-online",
|
|
316
|
-
];
|
|
317
|
-
if (!command.json) {
|
|
318
|
-
writeLine(io.stdout, "Updating Cockpit CLI from npm...");
|
|
390
|
+
try {
|
|
391
|
+
await runSelfUpdate(io, { json: command.json });
|
|
319
392
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (!(error instanceof SelfUpdateError))
|
|
395
|
+
throw error;
|
|
396
|
+
addInstallEvent(installEvents, "npm_install", "fail", error.eacces
|
|
397
|
+
? "npm_install_eacces"
|
|
398
|
+
: "npm_install_failed");
|
|
323
399
|
if (command.json) {
|
|
324
400
|
writeLine(io.stdout, JSON.stringify({
|
|
325
401
|
status: "blocked",
|
|
326
402
|
step: "npm_install",
|
|
327
|
-
command: `npm ${
|
|
328
|
-
exit_code:
|
|
403
|
+
command: `npm ${SELF_UPDATE_INSTALL_ARGS.join(" ")}`,
|
|
404
|
+
exit_code: error.result.code,
|
|
329
405
|
}, null, 2));
|
|
330
406
|
}
|
|
331
407
|
else {
|
|
332
408
|
writeLine(io.stderr, "BLOCKED: npm install failed; Cockpit CLI was not refreshed.");
|
|
333
|
-
if (
|
|
409
|
+
if (error.eacces) {
|
|
334
410
|
writeLine(io.stderr, "Fix Homebrew npm ownership once: sudo chown -R $(whoami) /opt/homebrew/lib/node_modules/@bli-cockpit /opt/homebrew/bin/cockpit");
|
|
335
411
|
writeLine(io.stderr, "Do not use `sudo npm i -g`; it makes the ownership problem come back.");
|
|
336
412
|
}
|
|
337
413
|
}
|
|
338
|
-
return
|
|
414
|
+
return finish(error.result.code || 1);
|
|
339
415
|
}
|
|
416
|
+
addInstallEvent(installEvents, "npm_install", "ok");
|
|
340
417
|
if (!command.json) {
|
|
341
418
|
writeLine(io.stdout, "Cockpit CLI updated. Rechecking onboarding...");
|
|
342
419
|
}
|
|
@@ -345,7 +422,36 @@ async function runUpdate(command, io) {
|
|
|
345
422
|
...updateOnboardArgs(command),
|
|
346
423
|
]);
|
|
347
424
|
writeExecOutput(io, onboard, { stdout: true, stderr: true });
|
|
348
|
-
|
|
425
|
+
addInstallEvent(installEvents, "onboard_rerun", onboard.code === 0 ? "ok" : "fail", onboard.code === 0 ? undefined : updateOnboardFailureCode(onboard));
|
|
426
|
+
return finish(onboard.code);
|
|
427
|
+
}
|
|
428
|
+
const SELF_UPDATE_INSTALL_ARGS = [
|
|
429
|
+
"install",
|
|
430
|
+
"-g",
|
|
431
|
+
"@bli-cockpit/cli@latest",
|
|
432
|
+
"--prefer-online",
|
|
433
|
+
];
|
|
434
|
+
export class SelfUpdateError extends Error {
|
|
435
|
+
result;
|
|
436
|
+
eacces;
|
|
437
|
+
constructor(result) {
|
|
438
|
+
super("npm install failed; Cockpit CLI was not refreshed.");
|
|
439
|
+
this.name = "SelfUpdateError";
|
|
440
|
+
this.result = result;
|
|
441
|
+
this.eacces = isNpmEaccesFailure(result.stderr);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
export async function runSelfUpdate(io, options = {}) {
|
|
445
|
+
const exec = io.exec ?? defaultExec();
|
|
446
|
+
if (!options.json) {
|
|
447
|
+
writeLine(io.stdout, "Updating Cockpit CLI from npm...");
|
|
448
|
+
}
|
|
449
|
+
const install = await exec("npm", [...SELF_UPDATE_INSTALL_ARGS]);
|
|
450
|
+
writeExecOutput(io, install, { stdout: !options.json, stderr: true });
|
|
451
|
+
if (install.code !== 0) {
|
|
452
|
+
throw new SelfUpdateError(install);
|
|
453
|
+
}
|
|
454
|
+
return { updated: true, version: LOCAL_COLLECTOR_VERSION };
|
|
349
455
|
}
|
|
350
456
|
async function runRelease(command, io) {
|
|
351
457
|
const releaseRoot = await findPublicReleaseRoot(process.cwd());
|
|
@@ -459,6 +565,8 @@ function updateOnboardArgs(command) {
|
|
|
459
565
|
args.push("--max-depth", String(command.maxDepth));
|
|
460
566
|
if (command.maxRepos !== undefined)
|
|
461
567
|
args.push("--max-repos", String(command.maxRepos));
|
|
568
|
+
if (command.allowHomeRoot)
|
|
569
|
+
args.push("--allow-home-root");
|
|
462
570
|
if (command.json)
|
|
463
571
|
args.push("--json");
|
|
464
572
|
return args;
|
|
@@ -466,6 +574,17 @@ function updateOnboardArgs(command) {
|
|
|
466
574
|
function isNpmEaccesFailure(stderr) {
|
|
467
575
|
return /EACCES|permission denied/i.test(stderr);
|
|
468
576
|
}
|
|
577
|
+
function updateOnboardFailureCode(result) {
|
|
578
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
579
|
+
if (output.includes(COLLECTION_ROOT_REQUIRED))
|
|
580
|
+
return COLLECTION_ROOT_REQUIRED;
|
|
581
|
+
if (/pairing|approval|device_pairing/i.test(output))
|
|
582
|
+
return "pairing_timeout";
|
|
583
|
+
if (/sync_blocked|spooled|upload failed|network_or_ingest/i.test(output)) {
|
|
584
|
+
return "sync_blocked";
|
|
585
|
+
}
|
|
586
|
+
return "onboard_rerun_failed";
|
|
587
|
+
}
|
|
469
588
|
function updateCollectionRoots(command) {
|
|
470
589
|
const roots = command.collectionRoots?.length
|
|
471
590
|
? command.collectionRoots
|
|
@@ -482,6 +601,108 @@ function updateCollectionRoots(command) {
|
|
|
482
601
|
}
|
|
483
602
|
return deduped;
|
|
484
603
|
}
|
|
604
|
+
function addInstallEvent(events, step, status, errorCode) {
|
|
605
|
+
events.push({
|
|
606
|
+
step,
|
|
607
|
+
status,
|
|
608
|
+
...(errorCode ? { error_code: sanitizeInstallErrorCode(errorCode) } : {}),
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
function addOnboardFailureEvent(events, blocker) {
|
|
612
|
+
const step = onboardFailureStep(blocker, events);
|
|
613
|
+
const existing = events.find((event) => event.step === step && event.status === "fail");
|
|
614
|
+
if (existing)
|
|
615
|
+
return;
|
|
616
|
+
addInstallEvent(events, step, "fail", blocker);
|
|
617
|
+
}
|
|
618
|
+
function addAutostartInstallEvent(events, result) {
|
|
619
|
+
if (!result) {
|
|
620
|
+
addInstallEvent(events, "autostart", "skipped", "runner_unavailable");
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (result.status === "unsupported") {
|
|
624
|
+
addInstallEvent(events, "autostart", "skipped", "unsupported");
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
addInstallEvent(events, "autostart", result.loaded === false ? "fail" : "ok", result.loaded === false ? "autostart_load_failed" : undefined);
|
|
628
|
+
}
|
|
629
|
+
function onboardFailureStep(blocker, events) {
|
|
630
|
+
if (blocker === COLLECTION_ROOT_REQUIRED)
|
|
631
|
+
return "install";
|
|
632
|
+
if (blocker === "device_pairing")
|
|
633
|
+
return "pair";
|
|
634
|
+
if (blocker === "work_context" || blocker === "ticket_binding" || blocker === "ticket") {
|
|
635
|
+
return "work_context";
|
|
636
|
+
}
|
|
637
|
+
if (blocker === "network_or_ingest")
|
|
638
|
+
return "sync";
|
|
639
|
+
if (blocker === "install")
|
|
640
|
+
return "install";
|
|
641
|
+
const lastIncomplete = ["install", "auth", "pair", "work_context", "sync"].find((step) => !events.some((event) => event.step === step));
|
|
642
|
+
return lastIncomplete ?? "sync";
|
|
643
|
+
}
|
|
644
|
+
function sanitizeInstallErrorCode(value) {
|
|
645
|
+
const normalized = value
|
|
646
|
+
.trim()
|
|
647
|
+
.toLowerCase()
|
|
648
|
+
.replace(/[^a-z0-9_]+/gu, "_")
|
|
649
|
+
.replace(/^_+|_+$/gu, "")
|
|
650
|
+
.slice(0, 120);
|
|
651
|
+
return normalized || "unknown";
|
|
652
|
+
}
|
|
653
|
+
export async function reportInstallEventsBestEffort(options) {
|
|
654
|
+
if (options.events.length === 0)
|
|
655
|
+
return;
|
|
656
|
+
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
657
|
+
const session = await readLocalCollectorSessionFile(paths).catch(() => null);
|
|
658
|
+
if (!session ||
|
|
659
|
+
session.session_state !== "valid" ||
|
|
660
|
+
typeof session.device_token !== "string" ||
|
|
661
|
+
!session.device_token) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const controller = new AbortController();
|
|
665
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
666
|
+
try {
|
|
667
|
+
const response = await options.io.fetch(`${options.dashboardUrl}/api/ambient/install-events`, {
|
|
668
|
+
method: "POST",
|
|
669
|
+
headers: {
|
|
670
|
+
"Content-Type": "application/json",
|
|
671
|
+
Authorization: `Bearer ${session.device_token}`,
|
|
672
|
+
},
|
|
673
|
+
body: JSON.stringify({
|
|
674
|
+
cli_version: LOCAL_COLLECTOR_VERSION,
|
|
675
|
+
command: options.command,
|
|
676
|
+
os_platform: os.platform(),
|
|
677
|
+
events: options.events.slice(0, 40),
|
|
678
|
+
}),
|
|
679
|
+
signal: controller.signal,
|
|
680
|
+
});
|
|
681
|
+
if (!response.ok) {
|
|
682
|
+
throw new Error(`http_${response.status}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
if (options.json) {
|
|
687
|
+
writeLine(options.io.stderr, `Install event telemetry skipped: ${classifyInstallTelemetryError(error)}`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
finally {
|
|
691
|
+
clearTimeout(timeout);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function classifyInstallTelemetryError(error) {
|
|
695
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
696
|
+
return "timeout";
|
|
697
|
+
}
|
|
698
|
+
const message = errorMessage(error);
|
|
699
|
+
const status = message.match(/http_(\d{3})/i)?.[1];
|
|
700
|
+
if (status)
|
|
701
|
+
return `http_${status}`;
|
|
702
|
+
if (/fetch|network|ENOTFOUND|ECONNREFUSED/i.test(message))
|
|
703
|
+
return "network";
|
|
704
|
+
return "failed";
|
|
705
|
+
}
|
|
485
706
|
function writeExecOutput(io, result, options) {
|
|
486
707
|
if (options.stdout)
|
|
487
708
|
writeRaw(io.stdout, result.stdout);
|
|
@@ -537,12 +758,29 @@ function onboardingRootPrompt(io) {
|
|
|
537
758
|
return {
|
|
538
759
|
confirm: async (message) => yesByDefault(await readLine(io, message)),
|
|
539
760
|
input: (message) => readLine(io, message),
|
|
761
|
+
message: (message) => writeLine(io.stdout, message),
|
|
540
762
|
};
|
|
541
763
|
}
|
|
542
764
|
function yesByDefault(raw) {
|
|
543
765
|
const answer = raw.trim().split(/\s+/u)[0]?.toLowerCase() ?? "";
|
|
544
766
|
return answer !== "n" && answer !== "no";
|
|
545
767
|
}
|
|
768
|
+
const OTP_CODE_PROMPT = [
|
|
769
|
+
"Email code needed:",
|
|
770
|
+
" Check your latest Cockpit email for a 6- to 10-digit code.",
|
|
771
|
+
"What you can do:",
|
|
772
|
+
" 1) Paste the code here.",
|
|
773
|
+
" 2) No code yet: wait for the resend window, then rerun this command.",
|
|
774
|
+
" 3) Can't use email: rerun with --no-auth for manual approval.",
|
|
775
|
+
"Code: ",
|
|
776
|
+
].join("\n");
|
|
777
|
+
const OTP_INVALID_MESSAGE = [
|
|
778
|
+
"Email code was not accepted:",
|
|
779
|
+
" Cockpit expects digits only, length 6 to 10.",
|
|
780
|
+
"What you can do:",
|
|
781
|
+
" 1) Rerun and paste the latest email code.",
|
|
782
|
+
" 2) Use manual approval: cockpit onboard --no-auth --email <you@buildlaunchiterate.ca>",
|
|
783
|
+
].join("\n");
|
|
546
784
|
async function resolveOnboardEmail(command, roots, config, io) {
|
|
547
785
|
if (command.claimedOwnerEmail)
|
|
548
786
|
return command.claimedOwnerEmail;
|
|
@@ -576,11 +814,17 @@ async function resolveInteractiveLoginEmail(command, io) {
|
|
|
576
814
|
return promptOnboardEmail(io);
|
|
577
815
|
}
|
|
578
816
|
async function requestPairingAccessToken(input, io) {
|
|
817
|
+
return (await requestPairingAccessTokenDetailed(input, io)).accessToken;
|
|
818
|
+
}
|
|
819
|
+
async function requestPairingAccessTokenDetailed(input, io) {
|
|
579
820
|
if (!input.email ||
|
|
580
821
|
input.noAuth ||
|
|
581
822
|
input.json ||
|
|
582
823
|
!isInteractiveStdin(io)) {
|
|
583
|
-
return
|
|
824
|
+
return {
|
|
825
|
+
status: "skipped",
|
|
826
|
+
errorCode: authSkippedReason(input, io),
|
|
827
|
+
};
|
|
584
828
|
}
|
|
585
829
|
try {
|
|
586
830
|
const fetchImpl = io.fetch;
|
|
@@ -590,23 +834,43 @@ async function requestPairingAccessToken(input, io) {
|
|
|
590
834
|
? start.resend_after_seconds
|
|
591
835
|
: 60;
|
|
592
836
|
writeLine(io.stdout, `Code sent; valid 1h, resend in ${resendAfter}s by rerunning this command.`);
|
|
593
|
-
const code = (await readLine(io,
|
|
594
|
-
if (!/^\d{6}$/.test(code)) {
|
|
595
|
-
throw new Error(
|
|
837
|
+
const code = (await readLine(io, OTP_CODE_PROMPT)).trim();
|
|
838
|
+
if (!/^\d{6,10}$/.test(code)) {
|
|
839
|
+
throw new Error(OTP_INVALID_MESSAGE);
|
|
596
840
|
}
|
|
597
841
|
const verified = await postOtpVerify(fetchImpl, input.dashboardUrl, input.email, code);
|
|
598
842
|
if (typeof verified.access_token !== "string" || !verified.access_token) {
|
|
599
843
|
throw new Error("OTP verified but dashboard returned no access token.");
|
|
600
844
|
}
|
|
601
845
|
writeLine(io.stdout, `Signed in as ${input.email}.`);
|
|
602
|
-
return verified.access_token;
|
|
846
|
+
return { status: "ok", accessToken: verified.access_token };
|
|
603
847
|
}
|
|
604
848
|
catch (error) {
|
|
605
849
|
writeLine(io.stderr, `Auth step skipped: ${errorMessage(error)}`);
|
|
606
850
|
writeLine(io.stderr, "Continuing with manual dashboard approval.");
|
|
607
|
-
return
|
|
851
|
+
return { status: "fail", errorCode: classifyAuthError(error) };
|
|
608
852
|
}
|
|
609
853
|
}
|
|
854
|
+
function authSkippedReason(input, io) {
|
|
855
|
+
if (!input.email)
|
|
856
|
+
return "email_missing";
|
|
857
|
+
if (input.noAuth)
|
|
858
|
+
return "no_auth";
|
|
859
|
+
if (input.json)
|
|
860
|
+
return "json_mode";
|
|
861
|
+
if (!isInteractiveStdin(io))
|
|
862
|
+
return "non_interactive";
|
|
863
|
+
return "auth_skipped";
|
|
864
|
+
}
|
|
865
|
+
function classifyAuthError(error) {
|
|
866
|
+
const message = errorMessage(error);
|
|
867
|
+
if (/\b(otp|code|digit|invalid)\b/i.test(message))
|
|
868
|
+
return "otp_invalid";
|
|
869
|
+
if (/fetch|network|ENOTFOUND|ECONNREFUSED|HTTP/i.test(message)) {
|
|
870
|
+
return "network_or_auth";
|
|
871
|
+
}
|
|
872
|
+
return "auth_failed";
|
|
873
|
+
}
|
|
610
874
|
async function pairLocalCollectorWithAuthFallback(options, io) {
|
|
611
875
|
try {
|
|
612
876
|
return await pairLocalCollector(options);
|
|
@@ -790,6 +1054,18 @@ function backgroundSyncLine(result) {
|
|
|
790
1054
|
return result.status;
|
|
791
1055
|
}
|
|
792
1056
|
async function runOnboard(command, io) {
|
|
1057
|
+
const installEvents = [];
|
|
1058
|
+
const finish = async (code) => {
|
|
1059
|
+
await reportInstallEventsBestEffort({
|
|
1060
|
+
homeDir: command.homeDir,
|
|
1061
|
+
dashboardUrl: command.dashboardUrl,
|
|
1062
|
+
command: "onboard",
|
|
1063
|
+
events: installEvents,
|
|
1064
|
+
json: command.json,
|
|
1065
|
+
io,
|
|
1066
|
+
});
|
|
1067
|
+
return code;
|
|
1068
|
+
};
|
|
793
1069
|
let install = null;
|
|
794
1070
|
let pair = null;
|
|
795
1071
|
let sync = null;
|
|
@@ -813,6 +1089,7 @@ async function runOnboard(command, io) {
|
|
|
813
1089
|
explicitRoots: command.collectionRoots ?? (command.repoRoot ? [command.repoRoot] : []),
|
|
814
1090
|
config: existingConfig,
|
|
815
1091
|
interactive,
|
|
1092
|
+
allowHomeRoot: command.allowHomeRoot,
|
|
816
1093
|
prompt: interactive ? onboardingRootPrompt(io) : undefined,
|
|
817
1094
|
});
|
|
818
1095
|
const collectionRoots = rootsResult.roots;
|
|
@@ -831,6 +1108,9 @@ async function runOnboard(command, io) {
|
|
|
831
1108
|
if (!command.json) {
|
|
832
1109
|
writeLine(io.stdout, `Collecting from: ${collectionRoots.join(", ")}`);
|
|
833
1110
|
}
|
|
1111
|
+
if (rootsResult.homeRootOptIn) {
|
|
1112
|
+
addInstallEvent(installEvents, "home_root_optin", "ok");
|
|
1113
|
+
}
|
|
834
1114
|
const claimedOwnerEmail = await resolveOnboardEmail(resolvedCommand, collectionRoots, existingConfig, io);
|
|
835
1115
|
install = await installLocalCollector({
|
|
836
1116
|
homeDir: command.homeDir,
|
|
@@ -840,6 +1120,7 @@ async function runOnboard(command, io) {
|
|
|
840
1120
|
dashboardUrl: command.dashboardUrl,
|
|
841
1121
|
deviceName: command.deviceName,
|
|
842
1122
|
});
|
|
1123
|
+
addInstallEvent(installEvents, "install", "ok");
|
|
843
1124
|
if (!command.json) {
|
|
844
1125
|
writeLine(io.stdout, "1/5 Installed local collector.");
|
|
845
1126
|
writeLine(io.stdout, `Config: ${install.paths.config_file}`);
|
|
@@ -852,6 +1133,8 @@ async function runOnboard(command, io) {
|
|
|
852
1133
|
const installedSession = await readOnboardSessionReuseCandidate(command.homeDir);
|
|
853
1134
|
const canReuseInstalledSession = canReuseOnboardSession(installedSession, claimedOwnerEmail, command.dashboardUrl);
|
|
854
1135
|
if (installedStatus.session_state === "valid" && canReuseInstalledSession) {
|
|
1136
|
+
addInstallEvent(installEvents, "auth", "skipped", "existing_session");
|
|
1137
|
+
addInstallEvent(installEvents, "pair", "skipped", "existing_session");
|
|
855
1138
|
if (!command.json) {
|
|
856
1139
|
writeLine(io.stdout, "2/5 Existing valid device session found; pairing skipped.");
|
|
857
1140
|
}
|
|
@@ -863,12 +1146,13 @@ async function runOnboard(command, io) {
|
|
|
863
1146
|
writeLine(io.stdout, "2/5 Existing valid device session does not match requested owner or dashboard; pairing again.");
|
|
864
1147
|
}
|
|
865
1148
|
}
|
|
866
|
-
const
|
|
1149
|
+
const authResult = await requestPairingAccessTokenDetailed({
|
|
867
1150
|
dashboardUrl: command.dashboardUrl,
|
|
868
1151
|
email: claimedOwnerEmail,
|
|
869
1152
|
noAuth: command.noAuth,
|
|
870
1153
|
json: command.json,
|
|
871
1154
|
}, io);
|
|
1155
|
+
addInstallEvent(installEvents, "auth", authResult.status, authResult.errorCode);
|
|
872
1156
|
pair = await pairLocalCollectorWithAuthFallback({
|
|
873
1157
|
homeDir: command.homeDir,
|
|
874
1158
|
dashboardUrl: command.dashboardUrl,
|
|
@@ -876,12 +1160,13 @@ async function runOnboard(command, io) {
|
|
|
876
1160
|
deviceName: command.deviceName,
|
|
877
1161
|
pollIntervalMs: command.pollIntervalMs,
|
|
878
1162
|
timeoutMs: command.timeoutMs,
|
|
879
|
-
pairingAccessToken,
|
|
1163
|
+
pairingAccessToken: authResult.accessToken,
|
|
880
1164
|
fetch: io.fetch,
|
|
881
1165
|
onPairStarted: command.json
|
|
882
1166
|
? undefined
|
|
883
1167
|
: (request) => writePairingInstructions(io, request),
|
|
884
1168
|
}, io);
|
|
1169
|
+
addInstallEvent(installEvents, "pair", "ok");
|
|
885
1170
|
if (!command.json) {
|
|
886
1171
|
writeLine(io.stdout, "2/5 Device paired.");
|
|
887
1172
|
writeLine(io.stdout, `User: ${pair.session.email ?? pair.session.auth_subject_id}`);
|
|
@@ -891,9 +1176,13 @@ async function runOnboard(command, io) {
|
|
|
891
1176
|
const worktrees = await discoverCommandWorktrees(collectionRoots, { maxDepth: command.maxDepth, maxRepos: command.maxRepos }, io);
|
|
892
1177
|
if (worktrees.length > 1) {
|
|
893
1178
|
const multi = await runMultiRepoOnboard(resolvedCommand, io, worktrees);
|
|
1179
|
+
addInstallEvent(installEvents, "work_context", "ok");
|
|
1180
|
+
addInstallEvent(installEvents, "sync", multi.ok ? "ok" : "fail", multi.ok ? undefined : "sync_blocked");
|
|
894
1181
|
if (multi.ok) {
|
|
895
1182
|
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
1183
|
+
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
896
1184
|
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
1185
|
+
addAutostartInstallEvent(installEvents, autostart);
|
|
897
1186
|
}
|
|
898
1187
|
if (command.json) {
|
|
899
1188
|
writeLine(io.stdout, JSON.stringify({
|
|
@@ -919,7 +1208,7 @@ async function runOnboard(command, io) {
|
|
|
919
1208
|
backfillHint,
|
|
920
1209
|
});
|
|
921
1210
|
}
|
|
922
|
-
return multi.ok ? 0 : 1;
|
|
1211
|
+
return finish(multi.ok ? 0 : 1);
|
|
923
1212
|
}
|
|
924
1213
|
const worktreeRoot = worktrees[0]?.repo_root ?? primaryRoot;
|
|
925
1214
|
const context = await startLocalWorkContext({
|
|
@@ -935,6 +1224,7 @@ async function runOnboard(command, io) {
|
|
|
935
1224
|
writeLine(io.stdout, `Ticket: ${context.active_ticket_id ?? "general ambient"}`);
|
|
936
1225
|
writeLine(io.stdout, `Context: ${context.work_context_id}`);
|
|
937
1226
|
}
|
|
1227
|
+
addInstallEvent(installEvents, "work_context", "ok");
|
|
938
1228
|
const run = await runAttributedWorktreeSync({
|
|
939
1229
|
homeDir: command.homeDir,
|
|
940
1230
|
dashboardUrl: command.dashboardUrl,
|
|
@@ -952,6 +1242,7 @@ async function runOnboard(command, io) {
|
|
|
952
1242
|
branch: command.branch,
|
|
953
1243
|
});
|
|
954
1244
|
if (sync.status !== "uploaded") {
|
|
1245
|
+
addInstallEvent(installEvents, "sync", "fail", "sync_blocked");
|
|
955
1246
|
if (command.json) {
|
|
956
1247
|
writeLine(io.stdout, JSON.stringify({
|
|
957
1248
|
...onboardResult("blocked", resolvedCommand, install, pair, sync, status),
|
|
@@ -965,11 +1256,14 @@ async function runOnboard(command, io) {
|
|
|
965
1256
|
writeLine(io.stderr, `Failure: ${sync.failure_reason}`);
|
|
966
1257
|
writeLine(io.stderr, `Retry: ${sync.retry_command}`);
|
|
967
1258
|
}
|
|
968
|
-
return 1;
|
|
1259
|
+
return finish(1);
|
|
969
1260
|
}
|
|
1261
|
+
addInstallEvent(installEvents, "sync", "ok");
|
|
970
1262
|
if (command.json) {
|
|
971
1263
|
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
1264
|
+
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
972
1265
|
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
1266
|
+
addAutostartInstallEvent(installEvents, autostart);
|
|
973
1267
|
writeLine(io.stdout, JSON.stringify({
|
|
974
1268
|
...onboardResult("pass", resolvedCommand, install, pair, sync, status),
|
|
975
1269
|
collection_roots: collectionRoots,
|
|
@@ -979,7 +1273,7 @@ async function runOnboard(command, io) {
|
|
|
979
1273
|
backfill_hint: backfillHint,
|
|
980
1274
|
codex_sessions: run.summary,
|
|
981
1275
|
}, null, 2));
|
|
982
|
-
return 0;
|
|
1276
|
+
return finish(0);
|
|
983
1277
|
}
|
|
984
1278
|
writeLine(io.stdout, "4/5 Ambient metadata uploaded.");
|
|
985
1279
|
writeLine(io.stdout, `HTTP: ${sync.http_status}`);
|
|
@@ -994,7 +1288,9 @@ async function runOnboard(command, io) {
|
|
|
994
1288
|
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
995
1289
|
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
996
1290
|
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
1291
|
+
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
997
1292
|
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
1293
|
+
addAutostartInstallEvent(installEvents, autostart);
|
|
998
1294
|
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
999
1295
|
pair,
|
|
1000
1296
|
status,
|
|
@@ -1004,7 +1300,7 @@ async function runOnboard(command, io) {
|
|
|
1004
1300
|
initialSyncOk: true,
|
|
1005
1301
|
backfillHint,
|
|
1006
1302
|
});
|
|
1007
|
-
return 0;
|
|
1303
|
+
return finish(0);
|
|
1008
1304
|
}
|
|
1009
1305
|
catch (error) {
|
|
1010
1306
|
const message = errorMessage(error);
|
|
@@ -1017,7 +1313,8 @@ async function runOnboard(command, io) {
|
|
|
1017
1313
|
}).catch(() => null)
|
|
1018
1314
|
: null;
|
|
1019
1315
|
const blocker = classifyOnboardBlocker(message);
|
|
1020
|
-
|
|
1316
|
+
addOnboardFailureEvent(installEvents, blocker);
|
|
1317
|
+
const nextStep = nextStepForOnboardBlocker(blocker, command);
|
|
1021
1318
|
if (command.json) {
|
|
1022
1319
|
writeLine(io.stdout, JSON.stringify({
|
|
1023
1320
|
...onboardResult("blocked", command, install, pair, sync, status),
|
|
@@ -1032,7 +1329,7 @@ async function runOnboard(command, io) {
|
|
|
1032
1329
|
writeLine(io.stderr, `BLOCKED: ${message}`);
|
|
1033
1330
|
writeLine(io.stderr, `Next: ${nextStep}`);
|
|
1034
1331
|
}
|
|
1035
|
-
return 1;
|
|
1332
|
+
return finish(1);
|
|
1036
1333
|
}
|
|
1037
1334
|
}
|
|
1038
1335
|
function canReuseOnboardSession(session, claimedOwnerEmail, dashboardUrl) {
|
|
@@ -1317,10 +1614,10 @@ function classifyOnboardBlocker(message) {
|
|
|
1317
1614
|
return "ticket_binding";
|
|
1318
1615
|
return "unknown";
|
|
1319
1616
|
}
|
|
1320
|
-
function nextStepForOnboardBlocker(blocker) {
|
|
1617
|
+
function nextStepForOnboardBlocker(blocker, options = {}) {
|
|
1321
1618
|
switch (blocker) {
|
|
1322
1619
|
case COLLECTION_ROOT_REQUIRED:
|
|
1323
|
-
return
|
|
1620
|
+
return missingCollectionRootMessage(options);
|
|
1324
1621
|
case "ticket":
|
|
1325
1622
|
case "ticket_binding":
|
|
1326
1623
|
return "Run `cockpit start --ticket <id>` when actual ticket work begins, then run `cockpit sync`.";
|
|
@@ -1435,6 +1732,7 @@ async function runSyncLocked(command, io) {
|
|
|
1435
1732
|
});
|
|
1436
1733
|
if (worktrees.length > 1) {
|
|
1437
1734
|
const rows = run.outcomes.map((outcome) => worktreeSyncRow(outcome, run));
|
|
1735
|
+
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
1438
1736
|
if (command.json) {
|
|
1439
1737
|
writeLine(io.stdout, JSON.stringify({
|
|
1440
1738
|
mode: "multi_repo",
|
|
@@ -1442,6 +1740,7 @@ async function runSyncLocked(command, io) {
|
|
|
1442
1740
|
results: run.outcomes.map((outcome) => outcome.sync),
|
|
1443
1741
|
repos: rows,
|
|
1444
1742
|
codex_sessions: run.summary,
|
|
1743
|
+
raw_evidence_gc: gc,
|
|
1445
1744
|
}, null, 2));
|
|
1446
1745
|
return run.ok ? 0 : 1;
|
|
1447
1746
|
}
|
|
@@ -1453,14 +1752,17 @@ async function runSyncLocked(command, io) {
|
|
|
1453
1752
|
writeLine(uploaded ? io.stdout : io.stderr, `- ${worktree.repo_label}/${worktree.worktree_label} (${worktree.branch}) head:${shortSha(sync.head_sha ?? worktree.head_sha)} ${sync.status} objects:${sync.raw_evidence_uploaded_object_count} chunks:${sync.raw_evidence_uploaded_chunk_count} reused:${sync.raw_evidence_reused_count} failed:${sync.raw_evidence_failed_count} cursor:${sync.cursor_tracked_object_count}${failureSuffix}`);
|
|
1454
1753
|
}
|
|
1455
1754
|
writeAgentSessionSummary(io, run.summary);
|
|
1755
|
+
if (gc && !gc.skipped)
|
|
1756
|
+
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
1456
1757
|
return run.ok ? 0 : 1;
|
|
1457
1758
|
}
|
|
1458
1759
|
const result = run.outcomes[0]?.sync;
|
|
1459
1760
|
if (!result) {
|
|
1460
1761
|
throw new Error("Sync produced no result for the repo worktree.");
|
|
1461
1762
|
}
|
|
1763
|
+
const gc = result.status === "uploaded" ? await runSyncRawEvidenceGc(command, io) : null;
|
|
1462
1764
|
if (command.json) {
|
|
1463
|
-
writeLine(io.stdout, JSON.stringify({ ...result, codex_sessions: run.summary }, null, 2));
|
|
1765
|
+
writeLine(io.stdout, JSON.stringify({ ...result, codex_sessions: run.summary, raw_evidence_gc: gc }, null, 2));
|
|
1464
1766
|
return result.status === "uploaded" ? 0 : 1;
|
|
1465
1767
|
}
|
|
1466
1768
|
if (result.status === "uploaded") {
|
|
@@ -1474,6 +1776,8 @@ async function runSyncLocked(command, io) {
|
|
|
1474
1776
|
writeLine(io.stdout, rawEvidenceSyncLine(result));
|
|
1475
1777
|
writeAgentSessionSummary(io, run.summary);
|
|
1476
1778
|
writeLine(io.stdout, cursorStatusLine(result));
|
|
1779
|
+
if (gc && !gc.skipped)
|
|
1780
|
+
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
1477
1781
|
return 0;
|
|
1478
1782
|
}
|
|
1479
1783
|
writeLine(io.stderr, "Cockpit ambient upload failed; safe retry metadata was spooled.");
|
|
@@ -1482,6 +1786,9 @@ async function runSyncLocked(command, io) {
|
|
|
1482
1786
|
writeLine(io.stderr, `Retry: ${result.retry_command}`);
|
|
1483
1787
|
return 1;
|
|
1484
1788
|
}
|
|
1789
|
+
async function runSyncRawEvidenceGc(command, io) {
|
|
1790
|
+
return runRawEvidenceLocalGc(getCollectorRuntimePaths(command.homeDir), io.env);
|
|
1791
|
+
}
|
|
1485
1792
|
async function runStatus(command, io) {
|
|
1486
1793
|
const backfillCursor = await inspectBackfillCursor(command.homeDir);
|
|
1487
1794
|
const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos }, io);
|