@bli-cockpit/cli 0.1.33 → 0.2.1
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 +119 -202
- package/dist/commands/local-args.js +8 -0
- package/dist/commands/local.js +429 -34
- package/dist/local-state.js +6 -3
- 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
|
@@ -8,14 +8,15 @@ import { runBackfillCommand } from "./backfill.js";
|
|
|
8
8
|
import { inspectBackfillLock } from "../backfill-lock.js";
|
|
9
9
|
import { parseLocalArgs, normalizeUrl } from "./local-args.js";
|
|
10
10
|
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";
|
|
11
|
+
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
|
|
12
12
|
import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, defaultCodexSessionDirs, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
13
13
|
import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
|
|
14
14
|
import { backfillCompletionMarkerPath, readBackfillCursor, } from "../cursors/backfill-cursor.js";
|
|
15
15
|
import { acquireSyncLock } from "../sync-lock.js";
|
|
16
16
|
import { discoverGitWorktrees } from "../repo-identity.js";
|
|
17
17
|
import { runAttributedWorktreeSync, } from "./session-sync.js";
|
|
18
|
-
import { COLLECTION_ROOT_REQUIRED, resolveOnboardingRoots, } from "../onboarding-roots.js";
|
|
18
|
+
import { COLLECTION_ROOT_REQUIRED, missingCollectionRootMessage, normalizeRootsDetailed, resolveOnboardingRoots, rootRejectionExplanation, } from "../onboarding-roots.js";
|
|
19
|
+
import { rawEvidenceGcSummary, runRawEvidenceLocalGc, } from "../raw-evidence-gc.js";
|
|
19
20
|
export const rootCommandNames = new Set([
|
|
20
21
|
"onboard",
|
|
21
22
|
"update",
|
|
@@ -90,12 +91,12 @@ export function localCommandHelp(command) {
|
|
|
90
91
|
if (command)
|
|
91
92
|
return localSubcommandHelp(command);
|
|
92
93
|
return [
|
|
93
|
-
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
94
|
-
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
94
|
+
" 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]",
|
|
95
|
+
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--no-auth] [--json]",
|
|
95
96
|
" cockpit upgrade [same flags as update]",
|
|
96
|
-
" cockpit install [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
97
|
-
" cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
98
|
-
" cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
97
|
+
" cockpit install [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--json]",
|
|
98
|
+
" cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
99
|
+
" cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
99
100
|
" cockpit logout",
|
|
100
101
|
" cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--intent <intent>] [--phase <phase>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
101
102
|
" cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
@@ -115,21 +116,21 @@ function localSubcommandHelp(command) {
|
|
|
115
116
|
[
|
|
116
117
|
"onboard",
|
|
117
118
|
[
|
|
118
|
-
"Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--json]",
|
|
119
|
+
"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
120
|
"",
|
|
120
121
|
"Installs, pairs, starts work context(s), syncs once, and prints readiness proof.",
|
|
121
122
|
"If --workspace is a parent folder, scans child git repos/worktrees and rolls them up by repo.",
|
|
122
123
|
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
123
124
|
`Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
|
|
124
125
|
"Pass --dashboard-url only for staging/custom dashboards or to force a different pairing.",
|
|
125
|
-
"Run with no flags in a terminal and it prompts for the dashboard email; pass --email to skip the prompt
|
|
126
|
+
"Run with no flags in a terminal and it prompts for the dashboard email and OTP code; pass --email to skip the email prompt. Use --no-auth to force the manual approval fallback.",
|
|
126
127
|
"Interactive runs also offer to add Cockpit ticket-binding rules to AGENTS.md and CLAUDE.md after readiness proof.",
|
|
127
128
|
],
|
|
128
129
|
],
|
|
129
130
|
[
|
|
130
131
|
"install",
|
|
131
132
|
[
|
|
132
|
-
"Usage: cockpit install [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
133
|
+
"Usage: cockpit install [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--json]",
|
|
133
134
|
"",
|
|
134
135
|
"Writes local collector config. Pair with `cockpit login`, then run `cockpit start` when work begins.",
|
|
135
136
|
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
@@ -139,7 +140,7 @@ function localSubcommandHelp(command) {
|
|
|
139
140
|
[
|
|
140
141
|
"update",
|
|
141
142
|
[
|
|
142
|
-
"Usage: cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
143
|
+
"Usage: cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--no-auth] [--json]",
|
|
143
144
|
"",
|
|
144
145
|
"Updates the global public CLI from npm, then reruns `cockpit onboard`",
|
|
145
146
|
"with the same setup flags so pairing, saved roots, agent rules,",
|
|
@@ -158,16 +159,16 @@ function localSubcommandHelp(command) {
|
|
|
158
159
|
[
|
|
159
160
|
"login",
|
|
160
161
|
[
|
|
161
|
-
"Usage: cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
162
|
+
"Usage: cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
162
163
|
"",
|
|
163
|
-
"
|
|
164
|
+
"Signs in with an email OTP when interactive, starts dashboard device pairing, and stores the approved local session.",
|
|
164
165
|
"Omit --dashboard-url for the production dashboard; pass it only for staging/custom dashboards.",
|
|
165
166
|
],
|
|
166
167
|
],
|
|
167
168
|
[
|
|
168
169
|
"pair",
|
|
169
170
|
[
|
|
170
|
-
"Usage: cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
171
|
+
"Usage: cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
171
172
|
"",
|
|
172
173
|
"Alias for `cockpit login`.",
|
|
173
174
|
],
|
|
@@ -294,19 +295,69 @@ function isLocalHelpRequest(argv) {
|
|
|
294
295
|
return argv.length === 2 && (argv[1] === "--help" || argv[1] === "-h");
|
|
295
296
|
}
|
|
296
297
|
async function runInstall(command, io) {
|
|
297
|
-
const
|
|
298
|
+
const installEvents = [];
|
|
299
|
+
const finish = async (code) => {
|
|
300
|
+
await reportInstallEventsBestEffort({
|
|
301
|
+
homeDir: command.homeDir,
|
|
302
|
+
dashboardUrl: command.dashboardUrl,
|
|
303
|
+
command: "install",
|
|
304
|
+
events: installEvents,
|
|
305
|
+
json: command.json,
|
|
306
|
+
io,
|
|
307
|
+
});
|
|
308
|
+
return code;
|
|
309
|
+
};
|
|
310
|
+
const resolved = resolveInstallCommandRoots(command);
|
|
311
|
+
if (resolved.homeRootOptIn) {
|
|
312
|
+
addInstallEvent(installEvents, "home_root_optin", "ok");
|
|
313
|
+
}
|
|
314
|
+
const result = await installLocalCollector(resolved.command);
|
|
315
|
+
addInstallEvent(installEvents, "install", "ok");
|
|
298
316
|
if (command.json) {
|
|
299
317
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
300
|
-
return 0;
|
|
318
|
+
return finish(0);
|
|
301
319
|
}
|
|
302
320
|
writeLine(io.stdout, "Cockpit local collector installed.");
|
|
303
321
|
writeLine(io.stdout, `Config: ${result.paths.config_file}`);
|
|
304
322
|
writeLine(io.stdout, `Session: ${result.paths.session_file}`);
|
|
305
323
|
writeLine(io.stdout, "Auth: missing; upload stays local-only until pairing/login.");
|
|
306
324
|
writeLine(io.stdout, "Next: run `cockpit login`, then `cockpit start` inside the repo; add `--ticket <id>` only when ticket work begins.");
|
|
307
|
-
return 0;
|
|
325
|
+
return finish(0);
|
|
326
|
+
}
|
|
327
|
+
function resolveInstallCommandRoots(command) {
|
|
328
|
+
const detailed = normalizeRootsDetailed([command.repoRoot ?? process.cwd()], {
|
|
329
|
+
homeDir: command.homeDir,
|
|
330
|
+
allowHomeRoot: command.allowHomeRoot,
|
|
331
|
+
});
|
|
332
|
+
if (detailed.roots.length > 0) {
|
|
333
|
+
const root = detailed.roots[0];
|
|
334
|
+
return {
|
|
335
|
+
command: {
|
|
336
|
+
...command,
|
|
337
|
+
repoRoot: root,
|
|
338
|
+
},
|
|
339
|
+
homeRootOptIn: Boolean(command.allowHomeRoot) &&
|
|
340
|
+
path.resolve(root) === path.resolve(command.homeDir ?? os.homedir()),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (detailed.rejected.length > 0) {
|
|
344
|
+
throw new Error(`${COLLECTION_ROOT_REQUIRED}: ${rootRejectionExplanation(detailed.rejected[0], command)}`);
|
|
345
|
+
}
|
|
346
|
+
throw new Error(`${COLLECTION_ROOT_REQUIRED}: ${missingCollectionRootMessage(command)}`);
|
|
308
347
|
}
|
|
309
348
|
async function runUpdate(command, io) {
|
|
349
|
+
const installEvents = [];
|
|
350
|
+
const finish = async (code) => {
|
|
351
|
+
await reportInstallEventsBestEffort({
|
|
352
|
+
homeDir: command.homeDir,
|
|
353
|
+
dashboardUrl: command.dashboardUrl,
|
|
354
|
+
command: "update",
|
|
355
|
+
events: installEvents,
|
|
356
|
+
json: command.json,
|
|
357
|
+
io,
|
|
358
|
+
});
|
|
359
|
+
return code;
|
|
360
|
+
};
|
|
310
361
|
const exec = io.exec ?? defaultExec();
|
|
311
362
|
const installArgs = [
|
|
312
363
|
"install",
|
|
@@ -320,6 +371,9 @@ async function runUpdate(command, io) {
|
|
|
320
371
|
const install = await exec("npm", installArgs);
|
|
321
372
|
writeExecOutput(io, install, { stdout: !command.json, stderr: true });
|
|
322
373
|
if (install.code !== 0) {
|
|
374
|
+
addInstallEvent(installEvents, "npm_install", "fail", isNpmEaccesFailure(install.stderr)
|
|
375
|
+
? "npm_install_eacces"
|
|
376
|
+
: "npm_install_failed");
|
|
323
377
|
if (command.json) {
|
|
324
378
|
writeLine(io.stdout, JSON.stringify({
|
|
325
379
|
status: "blocked",
|
|
@@ -330,9 +384,14 @@ async function runUpdate(command, io) {
|
|
|
330
384
|
}
|
|
331
385
|
else {
|
|
332
386
|
writeLine(io.stderr, "BLOCKED: npm install failed; Cockpit CLI was not refreshed.");
|
|
387
|
+
if (isNpmEaccesFailure(install.stderr)) {
|
|
388
|
+
writeLine(io.stderr, "Fix Homebrew npm ownership once: sudo chown -R $(whoami) /opt/homebrew/lib/node_modules/@bli-cockpit /opt/homebrew/bin/cockpit");
|
|
389
|
+
writeLine(io.stderr, "Do not use `sudo npm i -g`; it makes the ownership problem come back.");
|
|
390
|
+
}
|
|
333
391
|
}
|
|
334
|
-
return install.code || 1;
|
|
392
|
+
return finish(install.code || 1);
|
|
335
393
|
}
|
|
394
|
+
addInstallEvent(installEvents, "npm_install", "ok");
|
|
336
395
|
if (!command.json) {
|
|
337
396
|
writeLine(io.stdout, "Cockpit CLI updated. Rechecking onboarding...");
|
|
338
397
|
}
|
|
@@ -341,7 +400,8 @@ async function runUpdate(command, io) {
|
|
|
341
400
|
...updateOnboardArgs(command),
|
|
342
401
|
]);
|
|
343
402
|
writeExecOutput(io, onboard, { stdout: true, stderr: true });
|
|
344
|
-
|
|
403
|
+
addInstallEvent(installEvents, "onboard_rerun", onboard.code === 0 ? "ok" : "fail", onboard.code === 0 ? undefined : updateOnboardFailureCode(onboard));
|
|
404
|
+
return finish(onboard.code);
|
|
345
405
|
}
|
|
346
406
|
async function runRelease(command, io) {
|
|
347
407
|
const releaseRoot = await findPublicReleaseRoot(process.cwd());
|
|
@@ -437,6 +497,8 @@ function updateOnboardArgs(command) {
|
|
|
437
497
|
}
|
|
438
498
|
if (command.claimedOwnerEmail)
|
|
439
499
|
args.push("--email", command.claimedOwnerEmail);
|
|
500
|
+
if (command.noAuth)
|
|
501
|
+
args.push("--no-auth");
|
|
440
502
|
if (command.deviceName)
|
|
441
503
|
args.push("--device-name", command.deviceName);
|
|
442
504
|
if (command.activeTicketId)
|
|
@@ -453,10 +515,26 @@ function updateOnboardArgs(command) {
|
|
|
453
515
|
args.push("--max-depth", String(command.maxDepth));
|
|
454
516
|
if (command.maxRepos !== undefined)
|
|
455
517
|
args.push("--max-repos", String(command.maxRepos));
|
|
518
|
+
if (command.allowHomeRoot)
|
|
519
|
+
args.push("--allow-home-root");
|
|
456
520
|
if (command.json)
|
|
457
521
|
args.push("--json");
|
|
458
522
|
return args;
|
|
459
523
|
}
|
|
524
|
+
function isNpmEaccesFailure(stderr) {
|
|
525
|
+
return /EACCES|permission denied/i.test(stderr);
|
|
526
|
+
}
|
|
527
|
+
function updateOnboardFailureCode(result) {
|
|
528
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
529
|
+
if (output.includes(COLLECTION_ROOT_REQUIRED))
|
|
530
|
+
return COLLECTION_ROOT_REQUIRED;
|
|
531
|
+
if (/pairing|approval|device_pairing/i.test(output))
|
|
532
|
+
return "pairing_timeout";
|
|
533
|
+
if (/sync_blocked|spooled|upload failed|network_or_ingest/i.test(output)) {
|
|
534
|
+
return "sync_blocked";
|
|
535
|
+
}
|
|
536
|
+
return "onboard_rerun_failed";
|
|
537
|
+
}
|
|
460
538
|
function updateCollectionRoots(command) {
|
|
461
539
|
const roots = command.collectionRoots?.length
|
|
462
540
|
? command.collectionRoots
|
|
@@ -473,6 +551,108 @@ function updateCollectionRoots(command) {
|
|
|
473
551
|
}
|
|
474
552
|
return deduped;
|
|
475
553
|
}
|
|
554
|
+
function addInstallEvent(events, step, status, errorCode) {
|
|
555
|
+
events.push({
|
|
556
|
+
step,
|
|
557
|
+
status,
|
|
558
|
+
...(errorCode ? { error_code: sanitizeInstallErrorCode(errorCode) } : {}),
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
function addOnboardFailureEvent(events, blocker) {
|
|
562
|
+
const step = onboardFailureStep(blocker, events);
|
|
563
|
+
const existing = events.find((event) => event.step === step && event.status === "fail");
|
|
564
|
+
if (existing)
|
|
565
|
+
return;
|
|
566
|
+
addInstallEvent(events, step, "fail", blocker);
|
|
567
|
+
}
|
|
568
|
+
function addAutostartInstallEvent(events, result) {
|
|
569
|
+
if (!result) {
|
|
570
|
+
addInstallEvent(events, "autostart", "skipped", "runner_unavailable");
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (result.status === "unsupported") {
|
|
574
|
+
addInstallEvent(events, "autostart", "skipped", "unsupported");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
addInstallEvent(events, "autostart", result.loaded === false ? "fail" : "ok", result.loaded === false ? "autostart_load_failed" : undefined);
|
|
578
|
+
}
|
|
579
|
+
function onboardFailureStep(blocker, events) {
|
|
580
|
+
if (blocker === COLLECTION_ROOT_REQUIRED)
|
|
581
|
+
return "install";
|
|
582
|
+
if (blocker === "device_pairing")
|
|
583
|
+
return "pair";
|
|
584
|
+
if (blocker === "work_context" || blocker === "ticket_binding" || blocker === "ticket") {
|
|
585
|
+
return "work_context";
|
|
586
|
+
}
|
|
587
|
+
if (blocker === "network_or_ingest")
|
|
588
|
+
return "sync";
|
|
589
|
+
if (blocker === "install")
|
|
590
|
+
return "install";
|
|
591
|
+
const lastIncomplete = ["install", "auth", "pair", "work_context", "sync"].find((step) => !events.some((event) => event.step === step));
|
|
592
|
+
return lastIncomplete ?? "sync";
|
|
593
|
+
}
|
|
594
|
+
function sanitizeInstallErrorCode(value) {
|
|
595
|
+
const normalized = value
|
|
596
|
+
.trim()
|
|
597
|
+
.toLowerCase()
|
|
598
|
+
.replace(/[^a-z0-9_]+/gu, "_")
|
|
599
|
+
.replace(/^_+|_+$/gu, "")
|
|
600
|
+
.slice(0, 120);
|
|
601
|
+
return normalized || "unknown";
|
|
602
|
+
}
|
|
603
|
+
async function reportInstallEventsBestEffort(options) {
|
|
604
|
+
if (options.events.length === 0)
|
|
605
|
+
return;
|
|
606
|
+
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
607
|
+
const session = await readLocalCollectorSessionFile(paths).catch(() => null);
|
|
608
|
+
if (!session ||
|
|
609
|
+
session.session_state !== "valid" ||
|
|
610
|
+
typeof session.device_token !== "string" ||
|
|
611
|
+
!session.device_token) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const controller = new AbortController();
|
|
615
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
616
|
+
try {
|
|
617
|
+
const response = await options.io.fetch(`${options.dashboardUrl}/api/ambient/install-events`, {
|
|
618
|
+
method: "POST",
|
|
619
|
+
headers: {
|
|
620
|
+
"Content-Type": "application/json",
|
|
621
|
+
Authorization: `Bearer ${session.device_token}`,
|
|
622
|
+
},
|
|
623
|
+
body: JSON.stringify({
|
|
624
|
+
cli_version: LOCAL_COLLECTOR_VERSION,
|
|
625
|
+
command: options.command,
|
|
626
|
+
os_platform: os.platform(),
|
|
627
|
+
events: options.events.slice(0, 40),
|
|
628
|
+
}),
|
|
629
|
+
signal: controller.signal,
|
|
630
|
+
});
|
|
631
|
+
if (!response.ok) {
|
|
632
|
+
throw new Error(`http_${response.status}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
if (options.json) {
|
|
637
|
+
writeLine(options.io.stderr, `Install event telemetry skipped: ${classifyInstallTelemetryError(error)}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
finally {
|
|
641
|
+
clearTimeout(timeout);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function classifyInstallTelemetryError(error) {
|
|
645
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
646
|
+
return "timeout";
|
|
647
|
+
}
|
|
648
|
+
const message = errorMessage(error);
|
|
649
|
+
const status = message.match(/http_(\d{3})/i)?.[1];
|
|
650
|
+
if (status)
|
|
651
|
+
return `http_${status}`;
|
|
652
|
+
if (/fetch|network|ENOTFOUND|ECONNREFUSED/i.test(message))
|
|
653
|
+
return "network";
|
|
654
|
+
return "failed";
|
|
655
|
+
}
|
|
476
656
|
function writeExecOutput(io, result, options) {
|
|
477
657
|
if (options.stdout)
|
|
478
658
|
writeRaw(io.stdout, result.stdout);
|
|
@@ -514,7 +694,7 @@ async function readLine(io, prompt) {
|
|
|
514
694
|
* leniency (the approving admin's account then owns the device).
|
|
515
695
|
*/
|
|
516
696
|
async function promptOnboardEmail(io) {
|
|
517
|
-
const raw = await readLine(io, "
|
|
697
|
+
const raw = await readLine(io, "What's your @buildlaunchiterate.ca email? (press enter to skip): ");
|
|
518
698
|
const answer = raw.trim().toLowerCase();
|
|
519
699
|
if (!answer)
|
|
520
700
|
return undefined;
|
|
@@ -528,12 +708,29 @@ function onboardingRootPrompt(io) {
|
|
|
528
708
|
return {
|
|
529
709
|
confirm: async (message) => yesByDefault(await readLine(io, message)),
|
|
530
710
|
input: (message) => readLine(io, message),
|
|
711
|
+
message: (message) => writeLine(io.stdout, message),
|
|
531
712
|
};
|
|
532
713
|
}
|
|
533
714
|
function yesByDefault(raw) {
|
|
534
715
|
const answer = raw.trim().split(/\s+/u)[0]?.toLowerCase() ?? "";
|
|
535
716
|
return answer !== "n" && answer !== "no";
|
|
536
717
|
}
|
|
718
|
+
const OTP_CODE_PROMPT = [
|
|
719
|
+
"Email code needed:",
|
|
720
|
+
" Check your latest Cockpit email for a 6- to 10-digit code.",
|
|
721
|
+
"What you can do:",
|
|
722
|
+
" 1) Paste the code here.",
|
|
723
|
+
" 2) No code yet: wait for the resend window, then rerun this command.",
|
|
724
|
+
" 3) Can't use email: rerun with --no-auth for manual approval.",
|
|
725
|
+
"Code: ",
|
|
726
|
+
].join("\n");
|
|
727
|
+
const OTP_INVALID_MESSAGE = [
|
|
728
|
+
"Email code was not accepted:",
|
|
729
|
+
" Cockpit expects digits only, length 6 to 10.",
|
|
730
|
+
"What you can do:",
|
|
731
|
+
" 1) Rerun and paste the latest email code.",
|
|
732
|
+
" 2) Use manual approval: cockpit onboard --no-auth --email <you@buildlaunchiterate.ca>",
|
|
733
|
+
].join("\n");
|
|
537
734
|
async function resolveOnboardEmail(command, roots, config, io) {
|
|
538
735
|
if (command.claimedOwnerEmail)
|
|
539
736
|
return command.claimedOwnerEmail;
|
|
@@ -558,6 +755,138 @@ async function resolveOnboardEmail(command, roots, config, io) {
|
|
|
558
755
|
}
|
|
559
756
|
return promptOnboardEmail(io);
|
|
560
757
|
}
|
|
758
|
+
async function resolveInteractiveLoginEmail(command, io) {
|
|
759
|
+
if (command.claimedOwnerEmail)
|
|
760
|
+
return command.claimedOwnerEmail;
|
|
761
|
+
if (command.noAuth || command.json || !isInteractiveStdin(io)) {
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
return promptOnboardEmail(io);
|
|
765
|
+
}
|
|
766
|
+
async function requestPairingAccessToken(input, io) {
|
|
767
|
+
return (await requestPairingAccessTokenDetailed(input, io)).accessToken;
|
|
768
|
+
}
|
|
769
|
+
async function requestPairingAccessTokenDetailed(input, io) {
|
|
770
|
+
if (!input.email ||
|
|
771
|
+
input.noAuth ||
|
|
772
|
+
input.json ||
|
|
773
|
+
!isInteractiveStdin(io)) {
|
|
774
|
+
return {
|
|
775
|
+
status: "skipped",
|
|
776
|
+
errorCode: authSkippedReason(input, io),
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
try {
|
|
780
|
+
const fetchImpl = io.fetch;
|
|
781
|
+
writeLine(io.stdout, `Signing in as ${input.email}.`);
|
|
782
|
+
const start = await postOtpStart(fetchImpl, input.dashboardUrl, input.email);
|
|
783
|
+
const resendAfter = typeof start.resend_after_seconds === "number"
|
|
784
|
+
? start.resend_after_seconds
|
|
785
|
+
: 60;
|
|
786
|
+
writeLine(io.stdout, `Code sent; valid 1h, resend in ${resendAfter}s by rerunning this command.`);
|
|
787
|
+
const code = (await readLine(io, OTP_CODE_PROMPT)).trim();
|
|
788
|
+
if (!/^\d{6,10}$/.test(code)) {
|
|
789
|
+
throw new Error(OTP_INVALID_MESSAGE);
|
|
790
|
+
}
|
|
791
|
+
const verified = await postOtpVerify(fetchImpl, input.dashboardUrl, input.email, code);
|
|
792
|
+
if (typeof verified.access_token !== "string" || !verified.access_token) {
|
|
793
|
+
throw new Error("OTP verified but dashboard returned no access token.");
|
|
794
|
+
}
|
|
795
|
+
writeLine(io.stdout, `Signed in as ${input.email}.`);
|
|
796
|
+
return { status: "ok", accessToken: verified.access_token };
|
|
797
|
+
}
|
|
798
|
+
catch (error) {
|
|
799
|
+
writeLine(io.stderr, `Auth step skipped: ${errorMessage(error)}`);
|
|
800
|
+
writeLine(io.stderr, "Continuing with manual dashboard approval.");
|
|
801
|
+
return { status: "fail", errorCode: classifyAuthError(error) };
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function authSkippedReason(input, io) {
|
|
805
|
+
if (!input.email)
|
|
806
|
+
return "email_missing";
|
|
807
|
+
if (input.noAuth)
|
|
808
|
+
return "no_auth";
|
|
809
|
+
if (input.json)
|
|
810
|
+
return "json_mode";
|
|
811
|
+
if (!isInteractiveStdin(io))
|
|
812
|
+
return "non_interactive";
|
|
813
|
+
return "auth_skipped";
|
|
814
|
+
}
|
|
815
|
+
function classifyAuthError(error) {
|
|
816
|
+
const message = errorMessage(error);
|
|
817
|
+
if (/\b(otp|code|digit|invalid)\b/i.test(message))
|
|
818
|
+
return "otp_invalid";
|
|
819
|
+
if (/fetch|network|ENOTFOUND|ECONNREFUSED|HTTP/i.test(message)) {
|
|
820
|
+
return "network_or_auth";
|
|
821
|
+
}
|
|
822
|
+
return "auth_failed";
|
|
823
|
+
}
|
|
824
|
+
async function pairLocalCollectorWithAuthFallback(options, io) {
|
|
825
|
+
try {
|
|
826
|
+
return await pairLocalCollector(options);
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
if (!options.pairingAccessToken || !isPairingAuthFailure(error)) {
|
|
830
|
+
throw error;
|
|
831
|
+
}
|
|
832
|
+
writeLine(io.stderr, `Authenticated pairing failed: ${errorMessage(error)}`);
|
|
833
|
+
writeLine(io.stderr, "Continuing with manual dashboard approval.");
|
|
834
|
+
return pairLocalCollector({
|
|
835
|
+
...options,
|
|
836
|
+
pairingAccessToken: undefined,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function isPairingAuthFailure(error) {
|
|
841
|
+
return /\b(auth|authorization|bearer|token|jwt|otp)\b/i.test(errorMessage(error));
|
|
842
|
+
}
|
|
843
|
+
async function postOtpStart(fetchImpl, dashboardUrl, email) {
|
|
844
|
+
const response = await fetchImpl(`${dashboardUrl}/api/auth/otp/start`, {
|
|
845
|
+
method: "POST",
|
|
846
|
+
headers: { "Content-Type": "application/json" },
|
|
847
|
+
body: JSON.stringify({ email }),
|
|
848
|
+
});
|
|
849
|
+
const parsed = await readJsonResponse(response);
|
|
850
|
+
if (!response.ok) {
|
|
851
|
+
throw new Error(responseErrorMessage(parsed, "OTP start failed"));
|
|
852
|
+
}
|
|
853
|
+
return parsed;
|
|
854
|
+
}
|
|
855
|
+
async function postOtpVerify(fetchImpl, dashboardUrl, email, code) {
|
|
856
|
+
const response = await fetchImpl(`${dashboardUrl}/api/auth/otp/verify`, {
|
|
857
|
+
method: "POST",
|
|
858
|
+
headers: { "Content-Type": "application/json" },
|
|
859
|
+
body: JSON.stringify({ email, code }),
|
|
860
|
+
});
|
|
861
|
+
const parsed = await readJsonResponse(response);
|
|
862
|
+
if (!response.ok) {
|
|
863
|
+
throw new Error(responseErrorMessage(parsed, "OTP verify failed"));
|
|
864
|
+
}
|
|
865
|
+
return parsed;
|
|
866
|
+
}
|
|
867
|
+
async function readJsonResponse(response) {
|
|
868
|
+
const text = await response.text();
|
|
869
|
+
if (!text)
|
|
870
|
+
return null;
|
|
871
|
+
try {
|
|
872
|
+
return JSON.parse(text);
|
|
873
|
+
}
|
|
874
|
+
catch {
|
|
875
|
+
return text;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function responseErrorMessage(parsed, fallback) {
|
|
879
|
+
if (parsed && typeof parsed === "object") {
|
|
880
|
+
const record = parsed;
|
|
881
|
+
if (typeof record["message"] === "string" && record["message"].trim()) {
|
|
882
|
+
return record["message"];
|
|
883
|
+
}
|
|
884
|
+
if (typeof record["error"] === "string" && record["error"].trim()) {
|
|
885
|
+
return record["error"];
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return fallback;
|
|
889
|
+
}
|
|
561
890
|
async function inferOnboardEmail(homeDir, roots, config) {
|
|
562
891
|
const session = await readOnboardSessionReuseCandidate(homeDir).catch(() => null);
|
|
563
892
|
const sessionEmail = normalizeEmailForComparison(session?.email);
|
|
@@ -675,6 +1004,18 @@ function backgroundSyncLine(result) {
|
|
|
675
1004
|
return result.status;
|
|
676
1005
|
}
|
|
677
1006
|
async function runOnboard(command, io) {
|
|
1007
|
+
const installEvents = [];
|
|
1008
|
+
const finish = async (code) => {
|
|
1009
|
+
await reportInstallEventsBestEffort({
|
|
1010
|
+
homeDir: command.homeDir,
|
|
1011
|
+
dashboardUrl: command.dashboardUrl,
|
|
1012
|
+
command: "onboard",
|
|
1013
|
+
events: installEvents,
|
|
1014
|
+
json: command.json,
|
|
1015
|
+
io,
|
|
1016
|
+
});
|
|
1017
|
+
return code;
|
|
1018
|
+
};
|
|
678
1019
|
let install = null;
|
|
679
1020
|
let pair = null;
|
|
680
1021
|
let sync = null;
|
|
@@ -698,6 +1039,7 @@ async function runOnboard(command, io) {
|
|
|
698
1039
|
explicitRoots: command.collectionRoots ?? (command.repoRoot ? [command.repoRoot] : []),
|
|
699
1040
|
config: existingConfig,
|
|
700
1041
|
interactive,
|
|
1042
|
+
allowHomeRoot: command.allowHomeRoot,
|
|
701
1043
|
prompt: interactive ? onboardingRootPrompt(io) : undefined,
|
|
702
1044
|
});
|
|
703
1045
|
const collectionRoots = rootsResult.roots;
|
|
@@ -716,6 +1058,9 @@ async function runOnboard(command, io) {
|
|
|
716
1058
|
if (!command.json) {
|
|
717
1059
|
writeLine(io.stdout, `Collecting from: ${collectionRoots.join(", ")}`);
|
|
718
1060
|
}
|
|
1061
|
+
if (rootsResult.homeRootOptIn) {
|
|
1062
|
+
addInstallEvent(installEvents, "home_root_optin", "ok");
|
|
1063
|
+
}
|
|
719
1064
|
const claimedOwnerEmail = await resolveOnboardEmail(resolvedCommand, collectionRoots, existingConfig, io);
|
|
720
1065
|
install = await installLocalCollector({
|
|
721
1066
|
homeDir: command.homeDir,
|
|
@@ -725,6 +1070,7 @@ async function runOnboard(command, io) {
|
|
|
725
1070
|
dashboardUrl: command.dashboardUrl,
|
|
726
1071
|
deviceName: command.deviceName,
|
|
727
1072
|
});
|
|
1073
|
+
addInstallEvent(installEvents, "install", "ok");
|
|
728
1074
|
if (!command.json) {
|
|
729
1075
|
writeLine(io.stdout, "1/5 Installed local collector.");
|
|
730
1076
|
writeLine(io.stdout, `Config: ${install.paths.config_file}`);
|
|
@@ -737,6 +1083,8 @@ async function runOnboard(command, io) {
|
|
|
737
1083
|
const installedSession = await readOnboardSessionReuseCandidate(command.homeDir);
|
|
738
1084
|
const canReuseInstalledSession = canReuseOnboardSession(installedSession, claimedOwnerEmail, command.dashboardUrl);
|
|
739
1085
|
if (installedStatus.session_state === "valid" && canReuseInstalledSession) {
|
|
1086
|
+
addInstallEvent(installEvents, "auth", "skipped", "existing_session");
|
|
1087
|
+
addInstallEvent(installEvents, "pair", "skipped", "existing_session");
|
|
740
1088
|
if (!command.json) {
|
|
741
1089
|
writeLine(io.stdout, "2/5 Existing valid device session found; pairing skipped.");
|
|
742
1090
|
}
|
|
@@ -748,18 +1096,27 @@ async function runOnboard(command, io) {
|
|
|
748
1096
|
writeLine(io.stdout, "2/5 Existing valid device session does not match requested owner or dashboard; pairing again.");
|
|
749
1097
|
}
|
|
750
1098
|
}
|
|
751
|
-
|
|
1099
|
+
const authResult = await requestPairingAccessTokenDetailed({
|
|
1100
|
+
dashboardUrl: command.dashboardUrl,
|
|
1101
|
+
email: claimedOwnerEmail,
|
|
1102
|
+
noAuth: command.noAuth,
|
|
1103
|
+
json: command.json,
|
|
1104
|
+
}, io);
|
|
1105
|
+
addInstallEvent(installEvents, "auth", authResult.status, authResult.errorCode);
|
|
1106
|
+
pair = await pairLocalCollectorWithAuthFallback({
|
|
752
1107
|
homeDir: command.homeDir,
|
|
753
1108
|
dashboardUrl: command.dashboardUrl,
|
|
754
1109
|
claimedOwnerEmail,
|
|
755
1110
|
deviceName: command.deviceName,
|
|
756
1111
|
pollIntervalMs: command.pollIntervalMs,
|
|
757
1112
|
timeoutMs: command.timeoutMs,
|
|
1113
|
+
pairingAccessToken: authResult.accessToken,
|
|
758
1114
|
fetch: io.fetch,
|
|
759
1115
|
onPairStarted: command.json
|
|
760
1116
|
? undefined
|
|
761
1117
|
: (request) => writePairingInstructions(io, request),
|
|
762
|
-
});
|
|
1118
|
+
}, io);
|
|
1119
|
+
addInstallEvent(installEvents, "pair", "ok");
|
|
763
1120
|
if (!command.json) {
|
|
764
1121
|
writeLine(io.stdout, "2/5 Device paired.");
|
|
765
1122
|
writeLine(io.stdout, `User: ${pair.session.email ?? pair.session.auth_subject_id}`);
|
|
@@ -769,9 +1126,13 @@ async function runOnboard(command, io) {
|
|
|
769
1126
|
const worktrees = await discoverCommandWorktrees(collectionRoots, { maxDepth: command.maxDepth, maxRepos: command.maxRepos }, io);
|
|
770
1127
|
if (worktrees.length > 1) {
|
|
771
1128
|
const multi = await runMultiRepoOnboard(resolvedCommand, io, worktrees);
|
|
1129
|
+
addInstallEvent(installEvents, "work_context", "ok");
|
|
1130
|
+
addInstallEvent(installEvents, "sync", multi.ok ? "ok" : "fail", multi.ok ? undefined : "sync_blocked");
|
|
772
1131
|
if (multi.ok) {
|
|
773
1132
|
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
1133
|
+
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
774
1134
|
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
1135
|
+
addAutostartInstallEvent(installEvents, autostart);
|
|
775
1136
|
}
|
|
776
1137
|
if (command.json) {
|
|
777
1138
|
writeLine(io.stdout, JSON.stringify({
|
|
@@ -797,7 +1158,7 @@ async function runOnboard(command, io) {
|
|
|
797
1158
|
backfillHint,
|
|
798
1159
|
});
|
|
799
1160
|
}
|
|
800
|
-
return multi.ok ? 0 : 1;
|
|
1161
|
+
return finish(multi.ok ? 0 : 1);
|
|
801
1162
|
}
|
|
802
1163
|
const worktreeRoot = worktrees[0]?.repo_root ?? primaryRoot;
|
|
803
1164
|
const context = await startLocalWorkContext({
|
|
@@ -813,6 +1174,7 @@ async function runOnboard(command, io) {
|
|
|
813
1174
|
writeLine(io.stdout, `Ticket: ${context.active_ticket_id ?? "general ambient"}`);
|
|
814
1175
|
writeLine(io.stdout, `Context: ${context.work_context_id}`);
|
|
815
1176
|
}
|
|
1177
|
+
addInstallEvent(installEvents, "work_context", "ok");
|
|
816
1178
|
const run = await runAttributedWorktreeSync({
|
|
817
1179
|
homeDir: command.homeDir,
|
|
818
1180
|
dashboardUrl: command.dashboardUrl,
|
|
@@ -830,6 +1192,7 @@ async function runOnboard(command, io) {
|
|
|
830
1192
|
branch: command.branch,
|
|
831
1193
|
});
|
|
832
1194
|
if (sync.status !== "uploaded") {
|
|
1195
|
+
addInstallEvent(installEvents, "sync", "fail", "sync_blocked");
|
|
833
1196
|
if (command.json) {
|
|
834
1197
|
writeLine(io.stdout, JSON.stringify({
|
|
835
1198
|
...onboardResult("blocked", resolvedCommand, install, pair, sync, status),
|
|
@@ -843,11 +1206,14 @@ async function runOnboard(command, io) {
|
|
|
843
1206
|
writeLine(io.stderr, `Failure: ${sync.failure_reason}`);
|
|
844
1207
|
writeLine(io.stderr, `Retry: ${sync.retry_command}`);
|
|
845
1208
|
}
|
|
846
|
-
return 1;
|
|
1209
|
+
return finish(1);
|
|
847
1210
|
}
|
|
1211
|
+
addInstallEvent(installEvents, "sync", "ok");
|
|
848
1212
|
if (command.json) {
|
|
849
1213
|
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
1214
|
+
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
850
1215
|
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
1216
|
+
addAutostartInstallEvent(installEvents, autostart);
|
|
851
1217
|
writeLine(io.stdout, JSON.stringify({
|
|
852
1218
|
...onboardResult("pass", resolvedCommand, install, pair, sync, status),
|
|
853
1219
|
collection_roots: collectionRoots,
|
|
@@ -857,7 +1223,7 @@ async function runOnboard(command, io) {
|
|
|
857
1223
|
backfill_hint: backfillHint,
|
|
858
1224
|
codex_sessions: run.summary,
|
|
859
1225
|
}, null, 2));
|
|
860
|
-
return 0;
|
|
1226
|
+
return finish(0);
|
|
861
1227
|
}
|
|
862
1228
|
writeLine(io.stdout, "4/5 Ambient metadata uploaded.");
|
|
863
1229
|
writeLine(io.stdout, `HTTP: ${sync.http_status}`);
|
|
@@ -872,7 +1238,9 @@ async function runOnboard(command, io) {
|
|
|
872
1238
|
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
873
1239
|
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
874
1240
|
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
1241
|
+
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
875
1242
|
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
1243
|
+
addAutostartInstallEvent(installEvents, autostart);
|
|
876
1244
|
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
877
1245
|
pair,
|
|
878
1246
|
status,
|
|
@@ -882,7 +1250,7 @@ async function runOnboard(command, io) {
|
|
|
882
1250
|
initialSyncOk: true,
|
|
883
1251
|
backfillHint,
|
|
884
1252
|
});
|
|
885
|
-
return 0;
|
|
1253
|
+
return finish(0);
|
|
886
1254
|
}
|
|
887
1255
|
catch (error) {
|
|
888
1256
|
const message = errorMessage(error);
|
|
@@ -895,7 +1263,8 @@ async function runOnboard(command, io) {
|
|
|
895
1263
|
}).catch(() => null)
|
|
896
1264
|
: null;
|
|
897
1265
|
const blocker = classifyOnboardBlocker(message);
|
|
898
|
-
|
|
1266
|
+
addOnboardFailureEvent(installEvents, blocker);
|
|
1267
|
+
const nextStep = nextStepForOnboardBlocker(blocker, command);
|
|
899
1268
|
if (command.json) {
|
|
900
1269
|
writeLine(io.stdout, JSON.stringify({
|
|
901
1270
|
...onboardResult("blocked", command, install, pair, sync, status),
|
|
@@ -910,7 +1279,7 @@ async function runOnboard(command, io) {
|
|
|
910
1279
|
writeLine(io.stderr, `BLOCKED: ${message}`);
|
|
911
1280
|
writeLine(io.stderr, `Next: ${nextStep}`);
|
|
912
1281
|
}
|
|
913
|
-
return 1;
|
|
1282
|
+
return finish(1);
|
|
914
1283
|
}
|
|
915
1284
|
}
|
|
916
1285
|
function canReuseOnboardSession(session, claimedOwnerEmail, dashboardUrl) {
|
|
@@ -1107,13 +1476,22 @@ function worktreeSyncRow(outcome, run) {
|
|
|
1107
1476
|
};
|
|
1108
1477
|
}
|
|
1109
1478
|
async function runLogin(command, io) {
|
|
1110
|
-
const
|
|
1479
|
+
const claimedOwnerEmail = await resolveInteractiveLoginEmail(command, io);
|
|
1480
|
+
const dashboardUrl = await resolveLoginDashboardUrl(command);
|
|
1481
|
+
const pairingAccessToken = await requestPairingAccessToken({
|
|
1482
|
+
dashboardUrl,
|
|
1483
|
+
email: claimedOwnerEmail,
|
|
1484
|
+
noAuth: command.noAuth,
|
|
1485
|
+
json: command.json,
|
|
1486
|
+
}, io);
|
|
1487
|
+
const result = await pairLocalCollectorWithAuthFallback({
|
|
1111
1488
|
homeDir: command.homeDir,
|
|
1112
1489
|
dashboardUrl: command.dashboardUrl,
|
|
1113
|
-
claimedOwnerEmail
|
|
1490
|
+
claimedOwnerEmail,
|
|
1114
1491
|
deviceName: command.deviceName,
|
|
1115
1492
|
pollIntervalMs: command.pollIntervalMs,
|
|
1116
1493
|
timeoutMs: command.timeoutMs,
|
|
1494
|
+
pairingAccessToken,
|
|
1117
1495
|
fetch: io.fetch,
|
|
1118
1496
|
onPairStarted: command.json
|
|
1119
1497
|
? undefined
|
|
@@ -1123,7 +1501,7 @@ async function runLogin(command, io) {
|
|
|
1123
1501
|
writeLine(io.stdout, `Code: ${request.user_code}`);
|
|
1124
1502
|
writeLine(io.stdout, "Waiting for dashboard approval...");
|
|
1125
1503
|
},
|
|
1126
|
-
});
|
|
1504
|
+
}, io);
|
|
1127
1505
|
if (command.json) {
|
|
1128
1506
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
1129
1507
|
return 0;
|
|
@@ -1135,6 +1513,13 @@ async function runLogin(command, io) {
|
|
|
1135
1513
|
writeLine(io.stdout, "Next: run `cockpit start` inside the repo.");
|
|
1136
1514
|
return 0;
|
|
1137
1515
|
}
|
|
1516
|
+
async function resolveLoginDashboardUrl(command) {
|
|
1517
|
+
if (command.dashboardUrl)
|
|
1518
|
+
return command.dashboardUrl;
|
|
1519
|
+
const paths = getCollectorRuntimePaths(command.homeDir);
|
|
1520
|
+
const config = await readLocalCollectorConfig(paths);
|
|
1521
|
+
return config.dashboard_url;
|
|
1522
|
+
}
|
|
1138
1523
|
function writePairingInstructions(io, request) {
|
|
1139
1524
|
writeLine(io.stdout, "2/5 Device pairing started.");
|
|
1140
1525
|
writeLine(io.stdout, `Open: ${request.approve_url}`);
|
|
@@ -1179,10 +1564,10 @@ function classifyOnboardBlocker(message) {
|
|
|
1179
1564
|
return "ticket_binding";
|
|
1180
1565
|
return "unknown";
|
|
1181
1566
|
}
|
|
1182
|
-
function nextStepForOnboardBlocker(blocker) {
|
|
1567
|
+
function nextStepForOnboardBlocker(blocker, options = {}) {
|
|
1183
1568
|
switch (blocker) {
|
|
1184
1569
|
case COLLECTION_ROOT_REQUIRED:
|
|
1185
|
-
return
|
|
1570
|
+
return missingCollectionRootMessage(options);
|
|
1186
1571
|
case "ticket":
|
|
1187
1572
|
case "ticket_binding":
|
|
1188
1573
|
return "Run `cockpit start --ticket <id>` when actual ticket work begins, then run `cockpit sync`.";
|
|
@@ -1297,6 +1682,7 @@ async function runSyncLocked(command, io) {
|
|
|
1297
1682
|
});
|
|
1298
1683
|
if (worktrees.length > 1) {
|
|
1299
1684
|
const rows = run.outcomes.map((outcome) => worktreeSyncRow(outcome, run));
|
|
1685
|
+
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
1300
1686
|
if (command.json) {
|
|
1301
1687
|
writeLine(io.stdout, JSON.stringify({
|
|
1302
1688
|
mode: "multi_repo",
|
|
@@ -1304,6 +1690,7 @@ async function runSyncLocked(command, io) {
|
|
|
1304
1690
|
results: run.outcomes.map((outcome) => outcome.sync),
|
|
1305
1691
|
repos: rows,
|
|
1306
1692
|
codex_sessions: run.summary,
|
|
1693
|
+
raw_evidence_gc: gc,
|
|
1307
1694
|
}, null, 2));
|
|
1308
1695
|
return run.ok ? 0 : 1;
|
|
1309
1696
|
}
|
|
@@ -1315,14 +1702,17 @@ async function runSyncLocked(command, io) {
|
|
|
1315
1702
|
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}`);
|
|
1316
1703
|
}
|
|
1317
1704
|
writeAgentSessionSummary(io, run.summary);
|
|
1705
|
+
if (gc && !gc.skipped)
|
|
1706
|
+
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
1318
1707
|
return run.ok ? 0 : 1;
|
|
1319
1708
|
}
|
|
1320
1709
|
const result = run.outcomes[0]?.sync;
|
|
1321
1710
|
if (!result) {
|
|
1322
1711
|
throw new Error("Sync produced no result for the repo worktree.");
|
|
1323
1712
|
}
|
|
1713
|
+
const gc = result.status === "uploaded" ? await runSyncRawEvidenceGc(command, io) : null;
|
|
1324
1714
|
if (command.json) {
|
|
1325
|
-
writeLine(io.stdout, JSON.stringify({ ...result, codex_sessions: run.summary }, null, 2));
|
|
1715
|
+
writeLine(io.stdout, JSON.stringify({ ...result, codex_sessions: run.summary, raw_evidence_gc: gc }, null, 2));
|
|
1326
1716
|
return result.status === "uploaded" ? 0 : 1;
|
|
1327
1717
|
}
|
|
1328
1718
|
if (result.status === "uploaded") {
|
|
@@ -1336,6 +1726,8 @@ async function runSyncLocked(command, io) {
|
|
|
1336
1726
|
writeLine(io.stdout, rawEvidenceSyncLine(result));
|
|
1337
1727
|
writeAgentSessionSummary(io, run.summary);
|
|
1338
1728
|
writeLine(io.stdout, cursorStatusLine(result));
|
|
1729
|
+
if (gc && !gc.skipped)
|
|
1730
|
+
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
1339
1731
|
return 0;
|
|
1340
1732
|
}
|
|
1341
1733
|
writeLine(io.stderr, "Cockpit ambient upload failed; safe retry metadata was spooled.");
|
|
@@ -1344,6 +1736,9 @@ async function runSyncLocked(command, io) {
|
|
|
1344
1736
|
writeLine(io.stderr, `Retry: ${result.retry_command}`);
|
|
1345
1737
|
return 1;
|
|
1346
1738
|
}
|
|
1739
|
+
async function runSyncRawEvidenceGc(command, io) {
|
|
1740
|
+
return runRawEvidenceLocalGc(getCollectorRuntimePaths(command.homeDir), io.env);
|
|
1741
|
+
}
|
|
1347
1742
|
async function runStatus(command, io) {
|
|
1348
1743
|
const backfillCursor = await inspectBackfillCursor(command.homeDir);
|
|
1349
1744
|
const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos }, io);
|