@bli-cockpit/cli 0.1.22 → 0.1.24
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 +46 -33
- package/dist/adapters/raw-evidence.js +121 -17
- package/dist/agent-rules.js +49 -22
- package/dist/autostart.js +23 -2
- package/dist/commands/local-args.js +19 -1
- package/dist/commands/local.js +227 -68
- package/dist/commands/public-root.js +4 -3
- package/dist/evidence-upload-client.js +51 -5
- package/dist/local-state.js +20 -3
- package/dist/onboarding-roots.js +104 -0
- package/dist/upload.js +37 -20
- package/package.json +1 -1
|
@@ -74,6 +74,7 @@ function parseOnboardArgs(args) {
|
|
|
74
74
|
kind: "onboard",
|
|
75
75
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
76
76
|
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
77
|
+
collectionRoots: optionalNonEmptyList(workRootFlagValues(values)),
|
|
77
78
|
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
78
79
|
claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
|
|
79
80
|
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
@@ -403,6 +404,7 @@ function parseNamedArgs(args, options) {
|
|
|
403
404
|
const allowed = new Set(options.allowedFlags);
|
|
404
405
|
const valueFlags = new Set(options.valueFlags);
|
|
405
406
|
const flags = new Map();
|
|
407
|
+
const flagValues = new Map();
|
|
406
408
|
const booleans = new Set();
|
|
407
409
|
const positionals = [];
|
|
408
410
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -422,6 +424,9 @@ function parseNamedArgs(args, options) {
|
|
|
422
424
|
}
|
|
423
425
|
rejectServiceRoleLikeArgument(value);
|
|
424
426
|
flags.set(flag, value);
|
|
427
|
+
const existing = flagValues.get(flag) ?? [];
|
|
428
|
+
existing.push(value);
|
|
429
|
+
flagValues.set(flag, existing);
|
|
425
430
|
if (inlineValue === undefined)
|
|
426
431
|
index += 1;
|
|
427
432
|
}
|
|
@@ -431,7 +436,7 @@ function parseNamedArgs(args, options) {
|
|
|
431
436
|
booleans.add(flag);
|
|
432
437
|
}
|
|
433
438
|
}
|
|
434
|
-
return { flags, booleans, positionals };
|
|
439
|
+
return { flags, flagValues, booleans, positionals };
|
|
435
440
|
}
|
|
436
441
|
function workRootFlagValue(values) {
|
|
437
442
|
const provided = WORK_ROOT_FLAGS.filter((flag) => values.flags.has(flag));
|
|
@@ -443,6 +448,19 @@ function workRootFlagValue(values) {
|
|
|
443
448
|
}
|
|
444
449
|
return values.flags.get("--workspace") ?? values.flags.get("--repo");
|
|
445
450
|
}
|
|
451
|
+
function workRootFlagValues(values) {
|
|
452
|
+
const roots = [];
|
|
453
|
+
for (const flag of WORK_ROOT_FLAGS) {
|
|
454
|
+
roots.push(...(values.flagValues.get(flag) ?? []));
|
|
455
|
+
}
|
|
456
|
+
return roots;
|
|
457
|
+
}
|
|
458
|
+
function optionalNonEmptyList(values) {
|
|
459
|
+
const filtered = values
|
|
460
|
+
.map((value) => optionalNonEmpty(value))
|
|
461
|
+
.filter((value) => Boolean(value));
|
|
462
|
+
return filtered.length > 0 ? filtered : undefined;
|
|
463
|
+
}
|
|
446
464
|
function assertNoPositionals(positionals, command) {
|
|
447
465
|
if (positionals.length > 0) {
|
|
448
466
|
throw new Error(`${command} does not accept positional arguments.`);
|
package/dist/commands/local.js
CHANGED
|
@@ -4,13 +4,14 @@ import path from "node:path";
|
|
|
4
4
|
import { createCollectorServer } from "../server.js";
|
|
5
5
|
import { inspectAgentRules, installAgentRules, uninstallAgentRules, } from "../agent-rules.js";
|
|
6
6
|
import { parseLocalArgs, normalizeUrl } from "./local-args.js";
|
|
7
|
-
import { autostartStatus, installAutostartAgent, uninstallAutostartAgent } from "../autostart.js";
|
|
8
|
-
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext } from "../local-state.js";
|
|
7
|
+
import { autostartStatus, installAutostartAgent, uninstallAutostartAgent, } from "../autostart.js";
|
|
8
|
+
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
|
|
9
9
|
import { scanAndAttributeCodexSessions } from "../adapters/codex-attribution.js";
|
|
10
10
|
import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
|
|
11
11
|
import { acquireSyncLock } from "../sync-lock.js";
|
|
12
12
|
import { discoverGitWorktrees } from "../repo-identity.js";
|
|
13
|
-
import { runAttributedWorktreeSync } from "./session-sync.js";
|
|
13
|
+
import { runAttributedWorktreeSync, } from "./session-sync.js";
|
|
14
|
+
import { COLLECTION_ROOT_REQUIRED, resolveOnboardingRoots, } from "../onboarding-roots.js";
|
|
14
15
|
export const rootCommandNames = new Set([
|
|
15
16
|
"onboard",
|
|
16
17
|
"install",
|
|
@@ -281,68 +282,112 @@ async function promptOnboardEmail(io) {
|
|
|
281
282
|
}
|
|
282
283
|
return answer;
|
|
283
284
|
}
|
|
285
|
+
function onboardingRootPrompt(io) {
|
|
286
|
+
return {
|
|
287
|
+
confirm: async (message) => yesByDefault(await readLine(io, message)),
|
|
288
|
+
input: (message) => readLine(io, message),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function yesByDefault(raw) {
|
|
292
|
+
const answer = raw.trim().split(/\s+/u)[0]?.toLowerCase() ?? "";
|
|
293
|
+
return answer !== "n" && answer !== "no";
|
|
294
|
+
}
|
|
295
|
+
async function resolveOnboardEmail(command, roots, config, io) {
|
|
296
|
+
if (command.claimedOwnerEmail)
|
|
297
|
+
return command.claimedOwnerEmail;
|
|
298
|
+
const inferred = await inferOnboardEmail(command.homeDir, roots, config);
|
|
299
|
+
const interactive = !command.json && isInteractiveStdin(io);
|
|
300
|
+
if (!inferred) {
|
|
301
|
+
return interactive ? promptOnboardEmail(io) : undefined;
|
|
302
|
+
}
|
|
303
|
+
if (inferred.source === "session") {
|
|
304
|
+
if (!command.json)
|
|
305
|
+
writeLine(io.stdout, `Dashboard email: ${inferred.email}`);
|
|
306
|
+
return inferred.email;
|
|
307
|
+
}
|
|
308
|
+
if (!interactive)
|
|
309
|
+
return inferred.email;
|
|
310
|
+
const answer = (await readLine(io, `Use ${inferred.email} for Cockpit pairing? [Y/n] `)).trim().toLowerCase();
|
|
311
|
+
const typedEmail = answer.split(/\s+/u).find((part) => part.includes("@"));
|
|
312
|
+
if (typedEmail)
|
|
313
|
+
return typedEmail;
|
|
314
|
+
if (yesByDefault(answer)) {
|
|
315
|
+
return inferred.email;
|
|
316
|
+
}
|
|
317
|
+
return promptOnboardEmail(io);
|
|
318
|
+
}
|
|
319
|
+
async function inferOnboardEmail(homeDir, roots, config) {
|
|
320
|
+
const session = await readOnboardSessionReuseCandidate(homeDir).catch(() => null);
|
|
321
|
+
const sessionEmail = normalizeEmailForComparison(session?.email);
|
|
322
|
+
if (session?.session_state === "valid" && sessionEmail) {
|
|
323
|
+
return { email: sessionEmail, source: "session" };
|
|
324
|
+
}
|
|
325
|
+
const configEmail = normalizeEmailForComparison(config?.claimed_owner_email);
|
|
326
|
+
if (configEmail)
|
|
327
|
+
return { email: configEmail, source: "config" };
|
|
328
|
+
const gitEmail = await inferUniqueGitEmail(roots);
|
|
329
|
+
return gitEmail ? { email: gitEmail, source: "git" } : null;
|
|
330
|
+
}
|
|
331
|
+
async function inferUniqueGitEmail(roots) {
|
|
332
|
+
const emails = new Set();
|
|
333
|
+
for (const root of roots) {
|
|
334
|
+
const email = await readGitConfigEmail(root);
|
|
335
|
+
if (email)
|
|
336
|
+
emails.add(email);
|
|
337
|
+
}
|
|
338
|
+
return emails.size === 1 ? [...emails][0] ?? null : null;
|
|
339
|
+
}
|
|
340
|
+
async function readGitConfigEmail(root) {
|
|
341
|
+
return new Promise((resolve) => {
|
|
342
|
+
execFile("git", ["config", "user.email"], { cwd: root, encoding: "utf8" }, (error, stdout) => {
|
|
343
|
+
if (error) {
|
|
344
|
+
resolve(null);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const email = normalizeEmailForComparison(stdout);
|
|
348
|
+
resolve(email?.includes("@") ? email : null);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
284
352
|
/**
|
|
285
|
-
* After a successful onboard,
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
* exec runner (`io.exec`): headless / piped / spawned onboards and tests that
|
|
289
|
-
* pass no exec skip it entirely. Declining leaves onboarding's success untouched.
|
|
353
|
+
* After a successful onboard, refreshes the launchd autostart agent so intern
|
|
354
|
+
* machines keep syncing without anyone re-running Cockpit. Runtimes without an
|
|
355
|
+
* exec runner skip this because launchd installation cannot be attempted there.
|
|
290
356
|
*/
|
|
291
|
-
async function
|
|
292
|
-
if (
|
|
293
|
-
return;
|
|
294
|
-
const answer = (await readLine(io, "Keep Cockpit syncing in the background, even after restart? [Y/n] "))
|
|
295
|
-
.trim()
|
|
296
|
-
.toLowerCase();
|
|
297
|
-
if (answer === "n" || answer === "no") {
|
|
298
|
-
writeLine(io.stdout, "Skipped background autostart. Run `cockpit autostart install` anytime.");
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
357
|
+
async function refreshOnboardAutostart(command, roots, io) {
|
|
358
|
+
if (!io.exec)
|
|
359
|
+
return null;
|
|
301
360
|
const result = await installAutostartAgent({
|
|
302
361
|
homeDir: command.homeDir,
|
|
303
|
-
repoRoot:
|
|
362
|
+
repoRoot: roots[0],
|
|
363
|
+
repoRoots: roots,
|
|
304
364
|
dashboardUrl: command.dashboardUrl,
|
|
305
365
|
exec: io.exec,
|
|
306
366
|
});
|
|
367
|
+
if (command.json)
|
|
368
|
+
return result;
|
|
307
369
|
if (result.status === "unsupported") {
|
|
308
370
|
writeLine(io.stdout, `Background autostart unsupported: ${result.message}`);
|
|
309
|
-
return;
|
|
371
|
+
return result;
|
|
310
372
|
}
|
|
311
373
|
writeLine(io.stdout, result.loaded
|
|
312
|
-
? "Background autostart
|
|
313
|
-
: "Background autostart
|
|
374
|
+
? "Background autostart refreshed; Cockpit syncs at login and every 15 min."
|
|
375
|
+
: "Background autostart refreshed, but launchctl load reported a problem; check `cockpit autostart status`.");
|
|
314
376
|
writeLine(io.stdout, `Plist: ${result.plist_path}`);
|
|
377
|
+
return result;
|
|
315
378
|
}
|
|
316
|
-
async function
|
|
317
|
-
|
|
318
|
-
return;
|
|
319
|
-
const scopePath = path.resolve(command.repoRoot ?? process.cwd());
|
|
320
|
-
const current = await inspectAgentRules({
|
|
379
|
+
async function refreshOnboardAgentRules(command, roots, io) {
|
|
380
|
+
const result = await installAgentRules({
|
|
321
381
|
homeDir: command.homeDir,
|
|
322
|
-
|
|
382
|
+
scopePaths: roots,
|
|
323
383
|
});
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
const answer = (await readLine(io, `Add Cockpit ticket-binding rules scoped to ${scopePath} to AGENTS.md and CLAUDE.md? [Y/n] `))
|
|
329
|
-
.trim()
|
|
330
|
-
.toLowerCase();
|
|
331
|
-
if (answer === "n" || answer === "no") {
|
|
332
|
-
writeLine(io.stdout, "Skipped agent rules. Run `cockpit agent-rules install --workspace \"$PWD\"` anytime.");
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
const result = await installAgentRules({ homeDir: command.homeDir, scopePath });
|
|
384
|
+
if (command.json)
|
|
385
|
+
return result;
|
|
336
386
|
writeLine(io.stdout, `Agent rules: ${onboardAgentRulesInstallLine(result)}`);
|
|
337
387
|
for (const target of result.targets) {
|
|
338
388
|
writeLine(io.stdout, `${agentRuleHostLabel(target.host)}: ${target.rules_file}`);
|
|
339
389
|
}
|
|
340
|
-
|
|
341
|
-
function onboardAgentRulesAlreadyInstalledLine(result) {
|
|
342
|
-
const hasEquivalent = result.targets.some((target) => target.state === "equivalent");
|
|
343
|
-
return hasEquivalent
|
|
344
|
-
? "Agent rules: matching Cockpit ticket-binding guidance already exists."
|
|
345
|
-
: "Agent rules: already current in AGENTS.md and CLAUDE.md.";
|
|
390
|
+
return result;
|
|
346
391
|
}
|
|
347
392
|
function onboardAgentRulesInstallLine(result) {
|
|
348
393
|
if (result.targets.some((target) => target.stale_block_replaced)) {
|
|
@@ -359,24 +404,78 @@ function onboardAgentRulesInstallLine(result) {
|
|
|
359
404
|
return "not installed.";
|
|
360
405
|
}
|
|
361
406
|
}
|
|
407
|
+
function writeOnboardLiveStatus(io, command, roots, options) {
|
|
408
|
+
writeLine(io.stdout, "You're live.");
|
|
409
|
+
writeLine(io.stdout, "Collecting from:");
|
|
410
|
+
for (const root of roots)
|
|
411
|
+
writeLine(io.stdout, `- ${root}`);
|
|
412
|
+
writeLine(io.stdout, `Paired user: ${options.pair?.session.email ??
|
|
413
|
+
options.pair?.session.auth_subject_id ??
|
|
414
|
+
(options.status?.session_state === "valid" ? "existing valid session" : "pending")}`);
|
|
415
|
+
writeLine(io.stdout, `Background sync: ${backgroundSyncLine(options.autostart)}`);
|
|
416
|
+
writeLine(io.stdout, `Initial sync: ${options.initialSyncOk ? options.sync?.status ?? "uploaded" : "blocked"}`);
|
|
417
|
+
writeLine(io.stdout, `Agent rules: ${options.agentRules ? onboardAgentRulesInstallLine(options.agentRules) : "not refreshed"}`);
|
|
418
|
+
writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}/my-work`);
|
|
419
|
+
writeLine(io.stdout, "Next: cockpit status");
|
|
420
|
+
}
|
|
421
|
+
function backgroundSyncLine(result) {
|
|
422
|
+
if (!result)
|
|
423
|
+
return "not refreshed (launchd runner unavailable)";
|
|
424
|
+
if (result.status === "unsupported")
|
|
425
|
+
return `unsupported (${result.message})`;
|
|
426
|
+
if (result.status === "installed" && result.loaded)
|
|
427
|
+
return "installed";
|
|
428
|
+
if (result.status === "installed") {
|
|
429
|
+
return `installed with warning (${result.message ?? "launchctl load failed"})`;
|
|
430
|
+
}
|
|
431
|
+
return result.status;
|
|
432
|
+
}
|
|
362
433
|
async function runOnboard(command, io) {
|
|
363
434
|
let install = null;
|
|
364
435
|
let pair = null;
|
|
365
436
|
let sync = null;
|
|
366
437
|
let status = null;
|
|
438
|
+
let rootsResult = null;
|
|
439
|
+
let agentRules = null;
|
|
440
|
+
let autostart = null;
|
|
367
441
|
try {
|
|
368
442
|
if (!command.json) {
|
|
369
443
|
writeLine(io.stdout, "Cockpit harvest onboarding");
|
|
370
444
|
writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}`);
|
|
371
445
|
writeLine(io.stdout, `Ticket: ${command.activeTicketId ?? "general ambient"}`);
|
|
372
446
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
447
|
+
const paths = getCollectorRuntimePaths(command.homeDir);
|
|
448
|
+
const existingConfig = await readLocalCollectorConfig(paths).catch(() => null);
|
|
449
|
+
const interactive = !command.json && isInteractiveStdin(io);
|
|
450
|
+
rootsResult = await resolveOnboardingRoots({
|
|
451
|
+
homeDir: command.homeDir,
|
|
452
|
+
explicitRoots: command.collectionRoots ?? (command.repoRoot ? [command.repoRoot] : []),
|
|
453
|
+
config: existingConfig,
|
|
454
|
+
interactive,
|
|
455
|
+
prompt: interactive ? onboardingRootPrompt(io) : undefined,
|
|
456
|
+
});
|
|
457
|
+
const collectionRoots = rootsResult.roots;
|
|
458
|
+
const primaryRoot = collectionRoots[0];
|
|
459
|
+
if (!primaryRoot) {
|
|
460
|
+
throw new Error(`${COLLECTION_ROOT_REQUIRED}: no collection root confirmed.`);
|
|
461
|
+
}
|
|
462
|
+
const resolvedCommand = {
|
|
463
|
+
...command,
|
|
464
|
+
repoRoot: primaryRoot,
|
|
465
|
+
collectionRoots,
|
|
466
|
+
};
|
|
467
|
+
const replaceRepoRoots = rootsResult.source === "prompt" &&
|
|
468
|
+
!command.collectionRoots?.length &&
|
|
469
|
+
(existingConfig?.default_repo_paths.length ?? 0) > 0;
|
|
470
|
+
if (!command.json) {
|
|
471
|
+
writeLine(io.stdout, `Collecting from: ${collectionRoots.join(", ")}`);
|
|
376
472
|
}
|
|
473
|
+
const claimedOwnerEmail = await resolveOnboardEmail(resolvedCommand, collectionRoots, existingConfig, io);
|
|
377
474
|
install = await installLocalCollector({
|
|
378
475
|
homeDir: command.homeDir,
|
|
379
|
-
repoRoot:
|
|
476
|
+
repoRoot: primaryRoot,
|
|
477
|
+
repoRoots: collectionRoots,
|
|
478
|
+
replaceRepoRoots,
|
|
380
479
|
dashboardUrl: command.dashboardUrl,
|
|
381
480
|
deviceName: command.deviceName,
|
|
382
481
|
});
|
|
@@ -386,7 +485,7 @@ async function runOnboard(command, io) {
|
|
|
386
485
|
}
|
|
387
486
|
const installedStatus = await inspectLocalCollectorStatus({
|
|
388
487
|
homeDir: command.homeDir,
|
|
389
|
-
repoRoot:
|
|
488
|
+
repoRoot: primaryRoot,
|
|
390
489
|
branch: command.branch,
|
|
391
490
|
});
|
|
392
491
|
const installedSession = await readOnboardSessionReuseCandidate(command.homeDir);
|
|
@@ -421,26 +520,41 @@ async function runOnboard(command, io) {
|
|
|
421
520
|
writeLine(io.stdout, `Device: ${pair.session.device_name ?? pair.session.device_id ?? "unknown"}`);
|
|
422
521
|
}
|
|
423
522
|
}
|
|
424
|
-
const worktrees = await discoverCommandWorktrees(
|
|
523
|
+
const worktrees = await discoverCommandWorktrees(collectionRoots, { maxDepth: command.maxDepth, maxRepos: command.maxRepos }, io);
|
|
425
524
|
if (worktrees.length > 1) {
|
|
426
|
-
const multi = await runMultiRepoOnboard(
|
|
525
|
+
const multi = await runMultiRepoOnboard(resolvedCommand, io, worktrees);
|
|
526
|
+
if (multi.ok) {
|
|
527
|
+
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
528
|
+
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
529
|
+
}
|
|
427
530
|
if (command.json) {
|
|
428
531
|
writeLine(io.stdout, JSON.stringify({
|
|
429
|
-
...onboardResult(multi.ok ? "pass" : "blocked",
|
|
532
|
+
...onboardResult(multi.ok ? "pass" : "blocked", resolvedCommand, install, pair, null, null),
|
|
533
|
+
collection_roots: collectionRoots,
|
|
534
|
+
root_resolution: rootsResult,
|
|
535
|
+
agent_rules: agentRules,
|
|
536
|
+
autostart,
|
|
430
537
|
mode: "multi_repo",
|
|
431
538
|
repos: multi.results,
|
|
432
539
|
codex_sessions: multi.codex_sessions,
|
|
433
540
|
}, null, 2));
|
|
434
541
|
}
|
|
435
|
-
if (multi.ok) {
|
|
436
|
-
|
|
437
|
-
|
|
542
|
+
if (multi.ok && !command.json) {
|
|
543
|
+
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
544
|
+
pair,
|
|
545
|
+
status,
|
|
546
|
+
sync: null,
|
|
547
|
+
agentRules,
|
|
548
|
+
autostart,
|
|
549
|
+
initialSyncOk: true,
|
|
550
|
+
});
|
|
438
551
|
}
|
|
439
552
|
return multi.ok ? 0 : 1;
|
|
440
553
|
}
|
|
554
|
+
const worktreeRoot = worktrees[0]?.repo_root ?? primaryRoot;
|
|
441
555
|
const context = await startLocalWorkContext({
|
|
442
556
|
homeDir: command.homeDir,
|
|
443
|
-
repoRoot:
|
|
557
|
+
repoRoot: worktreeRoot,
|
|
444
558
|
branch: command.branch,
|
|
445
559
|
activeTicketId: command.activeTicketId,
|
|
446
560
|
});
|
|
@@ -464,13 +578,15 @@ async function runOnboard(command, io) {
|
|
|
464
578
|
}
|
|
465
579
|
status = await inspectLocalCollectorStatus({
|
|
466
580
|
homeDir: command.homeDir,
|
|
467
|
-
repoRoot:
|
|
581
|
+
repoRoot: worktreeRoot,
|
|
468
582
|
branch: command.branch,
|
|
469
583
|
});
|
|
470
584
|
if (sync.status !== "uploaded") {
|
|
471
585
|
if (command.json) {
|
|
472
586
|
writeLine(io.stdout, JSON.stringify({
|
|
473
|
-
...onboardResult("blocked",
|
|
587
|
+
...onboardResult("blocked", resolvedCommand, install, pair, sync, status),
|
|
588
|
+
collection_roots: collectionRoots,
|
|
589
|
+
root_resolution: rootsResult,
|
|
474
590
|
codex_sessions: run.summary,
|
|
475
591
|
}, null, 2));
|
|
476
592
|
}
|
|
@@ -482,8 +598,14 @@ async function runOnboard(command, io) {
|
|
|
482
598
|
return 1;
|
|
483
599
|
}
|
|
484
600
|
if (command.json) {
|
|
601
|
+
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
602
|
+
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
485
603
|
writeLine(io.stdout, JSON.stringify({
|
|
486
|
-
...onboardResult("pass",
|
|
604
|
+
...onboardResult("pass", resolvedCommand, install, pair, sync, status),
|
|
605
|
+
collection_roots: collectionRoots,
|
|
606
|
+
root_resolution: rootsResult,
|
|
607
|
+
agent_rules: agentRules,
|
|
608
|
+
autostart,
|
|
487
609
|
codex_sessions: run.summary,
|
|
488
610
|
}, null, 2));
|
|
489
611
|
return 0;
|
|
@@ -500,22 +622,35 @@ async function runOnboard(command, io) {
|
|
|
500
622
|
writeLine(io.stdout, `Upload state: ${status.upload_state}`);
|
|
501
623
|
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
502
624
|
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
503
|
-
await
|
|
504
|
-
await
|
|
625
|
+
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
626
|
+
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
627
|
+
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
628
|
+
pair,
|
|
629
|
+
status,
|
|
630
|
+
sync,
|
|
631
|
+
agentRules,
|
|
632
|
+
autostart,
|
|
633
|
+
initialSyncOk: true,
|
|
634
|
+
});
|
|
505
635
|
return 0;
|
|
506
636
|
}
|
|
507
637
|
catch (error) {
|
|
508
638
|
const message = errorMessage(error);
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
639
|
+
const statusRoot = rootsResult?.roots[0] ?? command.repoRoot;
|
|
640
|
+
status = statusRoot
|
|
641
|
+
? await inspectLocalCollectorStatus({
|
|
642
|
+
homeDir: command.homeDir,
|
|
643
|
+
repoRoot: statusRoot,
|
|
644
|
+
branch: command.branch,
|
|
645
|
+
}).catch(() => null)
|
|
646
|
+
: null;
|
|
514
647
|
const blocker = classifyOnboardBlocker(message);
|
|
515
648
|
const nextStep = nextStepForOnboardBlocker(blocker);
|
|
516
649
|
if (command.json) {
|
|
517
650
|
writeLine(io.stdout, JSON.stringify({
|
|
518
651
|
...onboardResult("blocked", command, install, pair, sync, status),
|
|
652
|
+
collection_roots: rootsResult?.roots ?? [],
|
|
653
|
+
root_resolution: rootsResult,
|
|
519
654
|
blocker,
|
|
520
655
|
message,
|
|
521
656
|
next_step: nextStep,
|
|
@@ -625,6 +760,13 @@ function cursorStatusLine(sync) {
|
|
|
625
760
|
const DEFAULT_DISCOVERY_MAX_DEPTH = 3;
|
|
626
761
|
const DEFAULT_DISCOVERY_MAX_REPOS = 50;
|
|
627
762
|
async function discoverCommandWorktrees(repoRoot, discovery = {}, io) {
|
|
763
|
+
if (Array.isArray(repoRoot)) {
|
|
764
|
+
const discovered = [];
|
|
765
|
+
for (const root of repoRoot) {
|
|
766
|
+
discovered.push(...(await discoverCommandWorktrees(root, discovery, io)));
|
|
767
|
+
}
|
|
768
|
+
return dedupeWorktrees(discovered);
|
|
769
|
+
}
|
|
628
770
|
const maxWorktrees = discovery.maxRepos ?? DEFAULT_DISCOVERY_MAX_REPOS;
|
|
629
771
|
const worktrees = await discoverGitWorktrees(repoRoot ?? process.cwd(), {
|
|
630
772
|
maxDepth: discovery.maxDepth ?? DEFAULT_DISCOVERY_MAX_DEPTH,
|
|
@@ -638,6 +780,18 @@ async function discoverCommandWorktrees(repoRoot, discovery = {}, io) {
|
|
|
638
780
|
}
|
|
639
781
|
return worktrees;
|
|
640
782
|
}
|
|
783
|
+
function dedupeWorktrees(worktrees) {
|
|
784
|
+
const seen = new Set();
|
|
785
|
+
const deduped = [];
|
|
786
|
+
for (const worktree of worktrees) {
|
|
787
|
+
const key = worktree.worktree_fingerprint || path.resolve(worktree.repo_root);
|
|
788
|
+
if (seen.has(key))
|
|
789
|
+
continue;
|
|
790
|
+
seen.add(key);
|
|
791
|
+
deduped.push(worktree);
|
|
792
|
+
}
|
|
793
|
+
return deduped;
|
|
794
|
+
}
|
|
641
795
|
async function runMultiRepoOnboard(command, io, worktrees) {
|
|
642
796
|
if (!command.json) {
|
|
643
797
|
writeLine(io.stdout, `3/5 Parent folder mode: discovered ${worktrees.length} git worktree(s).`);
|
|
@@ -743,6 +897,7 @@ function onboardResult(resultStatus, command, install, pair, sync, status) {
|
|
|
743
897
|
work_context_id: sync?.work_context_id ?? status?.work_context_id ?? null,
|
|
744
898
|
config_file: install?.paths.config_file ?? status?.config_file ?? null,
|
|
745
899
|
session_file: install?.paths.session_file ?? status?.session_file ?? null,
|
|
900
|
+
collection_roots: command.collectionRoots ?? (command.repoRoot ? [command.repoRoot] : []),
|
|
746
901
|
paired_user: pair?.session.email ?? pair?.session.auth_subject_id ?? null,
|
|
747
902
|
paired_device: pair?.session.device_name ?? pair?.session.device_id ?? null,
|
|
748
903
|
upload_status: sync?.status ?? null,
|
|
@@ -756,6 +911,8 @@ function onboardResult(resultStatus, command, install, pair, sync, status) {
|
|
|
756
911
|
};
|
|
757
912
|
}
|
|
758
913
|
function classifyOnboardBlocker(message) {
|
|
914
|
+
if (message.includes(COLLECTION_ROOT_REQUIRED))
|
|
915
|
+
return COLLECTION_ROOT_REQUIRED;
|
|
759
916
|
if (/ticket/i.test(message))
|
|
760
917
|
return "ticket";
|
|
761
918
|
if (/pair|paired|approval|expired|revoked/i.test(message))
|
|
@@ -772,6 +929,8 @@ function classifyOnboardBlocker(message) {
|
|
|
772
929
|
}
|
|
773
930
|
function nextStepForOnboardBlocker(blocker) {
|
|
774
931
|
switch (blocker) {
|
|
932
|
+
case COLLECTION_ROOT_REQUIRED:
|
|
933
|
+
return "Run `cockpit onboard --workspace <path>` or rerun in a terminal and confirm the collection root.";
|
|
775
934
|
case "ticket":
|
|
776
935
|
case "ticket_binding":
|
|
777
936
|
return "Run `cockpit start --ticket <id>` when actual ticket work begins, then run `cockpit sync`.";
|
|
@@ -23,9 +23,10 @@ function cockpitHelp() {
|
|
|
23
23
|
localCommandHelp(),
|
|
24
24
|
"",
|
|
25
25
|
"Install/update: `npm install -g @bli-cockpit/cli@latest`.",
|
|
26
|
-
"Intern path: run `cockpit onboard
|
|
27
|
-
"
|
|
28
|
-
"
|
|
26
|
+
"Intern path: run `cockpit onboard`; it confirms a `/BLI` collection root before syncing.",
|
|
27
|
+
"Headless/reused laptop path: `cockpit onboard --email <email> --workspace ~/BLI`.",
|
|
28
|
+
"Already onboarded: rerun `cockpit onboard` from anywhere to refresh pairing, roots, agent rules, autostart, and sync.",
|
|
29
|
+
"Agent setup: `cockpit onboard` refreshes AGENTS.md/CLAUDE.md rules; use `cockpit agent-rules install --workspace ~/BLI` for repair.",
|
|
29
30
|
"Dashboard URL is optional for normal production use; pass `--dashboard-url` only for staging/custom dashboards or forced re-pairing.",
|
|
30
31
|
"Manual collector path: `install`, `login`, `start [--ticket <id>] [--topic <label>] [--intent <intent>] [--phase <phase>]`, `sync`, `status`, `agent-rules`.",
|
|
31
32
|
].join("\n");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RAW_EVIDENCE_UPLOAD_DEFAULT_CHUNK_BYTES, RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES, RAW_EVIDENCE_UPLOAD_MAX_OBJECTS_PER_BEGIN, } from "@bli-cockpit/telemetry-core";
|
|
1
|
+
import { RAW_EVIDENCE_UPLOAD_DEFAULT_CHUNK_BYTES, RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES, RAW_EVIDENCE_UPLOAD_MAX_OBJECTS_PER_BEGIN, RawEvidenceRedactionMetadataSchema, RawEvidenceUploadCommitResponseSchema, } from "@bli-cockpit/telemetry-core";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
@@ -114,7 +114,7 @@ export async function uploadRawEvidenceFilesChunked(options) {
|
|
|
114
114
|
function duplicateOutcome(primary, duplicate) {
|
|
115
115
|
return {
|
|
116
116
|
...primary,
|
|
117
|
-
pointer: duplicate.pointer,
|
|
117
|
+
pointer: pointerWithUploadedMetadata(duplicate.pointer, primary.pointer),
|
|
118
118
|
codex_session_id: duplicate.codex_session_id ?? null,
|
|
119
119
|
kind: duplicate.kind ?? "unknown",
|
|
120
120
|
artifact_metadata: duplicate.artifact_metadata,
|
|
@@ -181,9 +181,10 @@ async function uploadOneObject(options, entry, disposition, chunkSizeBytes) {
|
|
|
181
181
|
const commitStatus = commit.body && typeof commit.body === "object"
|
|
182
182
|
? commit.body.status
|
|
183
183
|
: undefined;
|
|
184
|
+
const committedPointer = pointerWithCommitResponse(entry.file.pointer, commit.body);
|
|
184
185
|
if (commitStatus === "already_committed") {
|
|
185
186
|
return {
|
|
186
|
-
pointer:
|
|
187
|
+
pointer: committedPointer,
|
|
187
188
|
object_key: objectKey,
|
|
188
189
|
codex_session_id: entry.file.codex_session_id ?? null,
|
|
189
190
|
kind: entry.file.kind ?? "unknown",
|
|
@@ -194,7 +195,7 @@ async function uploadOneObject(options, entry, disposition, chunkSizeBytes) {
|
|
|
194
195
|
};
|
|
195
196
|
}
|
|
196
197
|
return {
|
|
197
|
-
pointer:
|
|
198
|
+
pointer: committedPointer,
|
|
198
199
|
object_key: objectKey,
|
|
199
200
|
codex_session_id: entry.file.codex_session_id ?? null,
|
|
200
201
|
kind: entry.file.kind ?? "unknown",
|
|
@@ -227,8 +228,9 @@ async function uploadWithLegacyFallback(options, loaded, outcomes) {
|
|
|
227
228
|
],
|
|
228
229
|
});
|
|
229
230
|
if (response.ok) {
|
|
231
|
+
const pointer = pointerWithLegacyUploadResponse(entry.file.pointer, response.body);
|
|
230
232
|
outcome = {
|
|
231
|
-
pointer
|
|
233
|
+
pointer,
|
|
232
234
|
object_key: entry.file.pointer.object_key ?? "",
|
|
233
235
|
codex_session_id: entry.file.codex_session_id ?? null,
|
|
234
236
|
kind: entry.file.kind ?? "unknown",
|
|
@@ -331,6 +333,50 @@ function failedOutcome(file, reason, uploadedChunks = 0) {
|
|
|
331
333
|
uploaded_chunk_count: uploadedChunks,
|
|
332
334
|
};
|
|
333
335
|
}
|
|
336
|
+
function pointerWithCommitResponse(pointer, body) {
|
|
337
|
+
const parsed = RawEvidenceUploadCommitResponseSchema.safeParse(body);
|
|
338
|
+
if (!parsed.success)
|
|
339
|
+
return pointer;
|
|
340
|
+
return pointerWithUploadedMetadata(pointer, {
|
|
341
|
+
content_hash_sha256: parsed.data.content_hash_sha256,
|
|
342
|
+
byte_size: parsed.data.byte_size,
|
|
343
|
+
redaction: parsed.data.redaction,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function pointerWithLegacyUploadResponse(pointer, body) {
|
|
347
|
+
if (!body || typeof body !== "object")
|
|
348
|
+
return pointer;
|
|
349
|
+
const uploaded = body.uploaded;
|
|
350
|
+
if (!Array.isArray(uploaded))
|
|
351
|
+
return pointer;
|
|
352
|
+
const entry = uploaded.find((candidate) => {
|
|
353
|
+
if (!candidate || typeof candidate !== "object")
|
|
354
|
+
return false;
|
|
355
|
+
return (candidate
|
|
356
|
+
.raw_evidence_pointer_id === pointer.raw_evidence_pointer_id);
|
|
357
|
+
});
|
|
358
|
+
if (!entry || typeof entry !== "object")
|
|
359
|
+
return pointer;
|
|
360
|
+
const record = entry;
|
|
361
|
+
return pointerWithUploadedMetadata(pointer, {
|
|
362
|
+
content_hash_sha256: typeof record["content_hash_sha256"] === "string"
|
|
363
|
+
? record["content_hash_sha256"]
|
|
364
|
+
: undefined,
|
|
365
|
+
byte_size: typeof record["byte_size"] === "number" ? record["byte_size"] : undefined,
|
|
366
|
+
redaction: record["redaction"],
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
function pointerWithUploadedMetadata(pointer, metadata) {
|
|
370
|
+
const redaction = RawEvidenceRedactionMetadataSchema.safeParse(metadata.redaction);
|
|
371
|
+
return {
|
|
372
|
+
...pointer,
|
|
373
|
+
content_hash_sha256: typeof metadata.content_hash_sha256 === "string"
|
|
374
|
+
? metadata.content_hash_sha256
|
|
375
|
+
: pointer.content_hash_sha256,
|
|
376
|
+
byte_size: typeof metadata.byte_size === "number" ? metadata.byte_size : pointer.byte_size,
|
|
377
|
+
redaction: redaction.success ? redaction.data : pointer.redaction,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
334
380
|
function summarizeOutcomes(outcomes, usedLegacyFallback) {
|
|
335
381
|
const uploaded = outcomes.filter((outcome) => outcome.upload_state === "uploaded");
|
|
336
382
|
return {
|
package/dist/local-state.js
CHANGED
|
@@ -21,12 +21,15 @@ export function getCollectorRuntimePaths(homeDir = os.homedir()) {
|
|
|
21
21
|
}
|
|
22
22
|
export async function installLocalCollector(options = {}) {
|
|
23
23
|
const homeDir = options.homeDir ?? os.homedir();
|
|
24
|
-
const
|
|
24
|
+
const repoRoots = normalizeRepoRoots(options.repoRoots);
|
|
25
|
+
const repoRoot = path.resolve(options.repoRoot ?? repoRoots[0] ?? process.cwd());
|
|
25
26
|
const paths = getCollectorRuntimePaths(homeDir);
|
|
26
27
|
await ensureRuntimeDirectories(paths);
|
|
27
28
|
const existingConfig = await readLocalCollectorConfig(paths).catch(() => null);
|
|
28
|
-
const defaultRepoPaths = new Set(existingConfig?.default_repo_paths ?? []);
|
|
29
|
-
|
|
29
|
+
const defaultRepoPaths = new Set(options.replaceRepoRoots ? [] : (existingConfig?.default_repo_paths ?? []));
|
|
30
|
+
for (const root of repoRoots.length > 0 ? repoRoots : [repoRoot]) {
|
|
31
|
+
defaultRepoPaths.add(root);
|
|
32
|
+
}
|
|
30
33
|
const rawEvidenceUpload = existingConfig?.raw_evidence_upload === "disabled" ||
|
|
31
34
|
existingConfig?.raw_evidence_upload === "remote_short_retention_opt_in"
|
|
32
35
|
? "remote_durable_opt_in"
|
|
@@ -55,6 +58,20 @@ export async function installLocalCollector(options = {}) {
|
|
|
55
58
|
message: "Local collector installed. Pair/login is still required before remote upload.",
|
|
56
59
|
};
|
|
57
60
|
}
|
|
61
|
+
function normalizeRepoRoots(repoRoots) {
|
|
62
|
+
if (!repoRoots)
|
|
63
|
+
return [];
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const normalized = [];
|
|
66
|
+
for (const root of repoRoots) {
|
|
67
|
+
const resolved = path.resolve(root);
|
|
68
|
+
if (seen.has(resolved))
|
|
69
|
+
continue;
|
|
70
|
+
seen.add(resolved);
|
|
71
|
+
normalized.push(resolved);
|
|
72
|
+
}
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
58
75
|
export async function pairLocalCollector(options = {}) {
|
|
59
76
|
const homeDir = options.homeDir ?? os.homedir();
|
|
60
77
|
const paths = getCollectorRuntimePaths(homeDir);
|