@bli-cockpit/cli 0.2.54 → 0.2.55

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.
Files changed (60) hide show
  1. package/dist/adapters/attribution-core-fallbacks.js +247 -0
  2. package/dist/adapters/attribution-core-paths.js +182 -0
  3. package/dist/adapters/attribution-core-score.js +159 -0
  4. package/dist/adapters/attribution-core-types.js +13 -0
  5. package/dist/adapters/attribution-core.js +13 -565
  6. package/dist/adapters/claude-attribution-discovery.js +186 -0
  7. package/dist/adapters/claude-attribution-score.js +204 -0
  8. package/dist/adapters/claude-attribution-signals.js +180 -0
  9. package/dist/adapters/claude-attribution-types.js +25 -0
  10. package/dist/adapters/claude-attribution.js +14 -569
  11. package/dist/commands/doctor-access.js +129 -0
  12. package/dist/commands/doctor-pipeline.js +326 -0
  13. package/dist/commands/doctor-registration.js +105 -0
  14. package/dist/commands/doctor-report.js +111 -0
  15. package/dist/commands/doctor-update.js +120 -0
  16. package/dist/commands/doctor.js +8 -753
  17. package/dist/commands/heartbeat.js +8 -0
  18. package/dist/commands/jarvis-contracts.js +8 -0
  19. package/dist/commands/jarvis-render.js +413 -0
  20. package/dist/commands/jarvis-turn.js +305 -0
  21. package/dist/commands/jarvis.js +23 -698
  22. package/dist/commands/local-args-collector-setup.js +250 -0
  23. package/dist/commands/local-args-collector-status.js +227 -0
  24. package/dist/commands/local-args-collector-work.js +175 -0
  25. package/dist/commands/local-args-collector.js +19 -624
  26. package/dist/commands/local-args-tower-admin.js +456 -0
  27. package/dist/commands/local-args-tower-chat.js +194 -0
  28. package/dist/commands/local-args-tower-pages.js +314 -0
  29. package/dist/commands/local-args-tower.js +13 -880
  30. package/dist/commands/local-help.js +10 -2
  31. package/dist/commands/onboard-completion.js +136 -0
  32. package/dist/commands/onboard-flows.js +165 -0
  33. package/dist/commands/onboard-setup.js +102 -0
  34. package/dist/commands/onboard.js +5 -392
  35. package/dist/commands/public-root.js +1 -1
  36. package/dist/commands/session-sync-counters.js +55 -0
  37. package/dist/commands/session-sync-health.js +8 -1
  38. package/dist/commands/session-sync-plan.js +47 -7
  39. package/dist/commands/session-sync-scan.js +4 -4
  40. package/dist/commands/session-sync.js +6 -0
  41. package/dist/commands/settings-render.js +27 -0
  42. package/dist/commands/sync-followups.js +5 -1
  43. package/dist/commands/sync.js +5 -1
  44. package/dist/commands/team-device-reasons.js +16 -0
  45. package/dist/commands/team.js +87 -7
  46. package/dist/evidence-upload-client.js +14 -763
  47. package/dist/evidence-upload-object.js +181 -0
  48. package/dist/evidence-upload-plan.js +233 -0
  49. package/dist/evidence-upload-terminal.js +309 -0
  50. package/dist/evidence-upload-transport.js +104 -0
  51. package/dist/spool/local-spool-io.js +122 -0
  52. package/dist/spool/local-spool-mutations.js +174 -0
  53. package/dist/spool/local-spool-parse.js +143 -0
  54. package/dist/spool/local-spool-types.js +22 -0
  55. package/dist/spool/local-spool.js +20 -426
  56. package/dist/upload-evidence-delivery-offer.js +144 -0
  57. package/dist/upload-evidence-delivery-reconcile.js +134 -0
  58. package/dist/upload-evidence-delivery-summary.js +205 -0
  59. package/dist/upload-evidence-delivery.js +12 -482
  60. package/package.json +3 -3
@@ -64,7 +64,7 @@ export function localCommandHelp(command) {
64
64
  " cockpit ops [status [--job <id>] [--skips] | recompile --person <email|name|id> [--dry-run]] [--dashboard-url <url>] [--json]",
65
65
  " cockpit slack [coverage [--workspace bli|blue_pearl] [--stale-only] | read [--person <p>] [--channel <c>] [--query <text>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>]] [--json]",
66
66
  " cockpit settings [personal [--chat-model <key>] [--brief-model <key>] | switches [set <key> <value>] | models [set --chat <key>] [--memory <id>] | cli-floor [<version>] | env list|set --project <p> --file <f> --content-stdin|delete --id <uuid> [--yes]] [--json]",
67
- " cockpit team [members | invite <email> --role <role> [--team-id <uuid>] | role <userId> --role <role> [--yes]] [--json]",
67
+ " cockpit team [members | invite <email> --role <role> [--team-id <uuid>] | role <userId> --role <role> [--yes] | device [list] | device revoke <id|name> --reason <label> [--note \"<text>\"] [--yes]] [--json]",
68
68
  " cockpit workbook [<project> [<doc>]] [--section <id>] [--markdown] [--width <n>] [--dashboard-url <url>] [--json]",
69
69
  " cockpit brief [edit|rewrite|history] [--for <person>] [--date <YYYY-MM-DD>] [--delta [--against <YYYY-MM-DD>]] [--version <pageId>] [--tldr|--full] [--versions] [--claims] [--days <n>] [--reason \"<why>\"] [--wait|--no-wait] [--dashboard-url <url>] [--json]",
70
70
  " cockpit brief status [--who <person>] [--render] [--dashboard-url <url>] [--json]",
@@ -223,7 +223,7 @@ function localSubcommandHelp(command) {
223
223
  [
224
224
  "team",
225
225
  [
226
- "Usage: cockpit team [members | invite <email> --role <role> | role <userId> --role <role>] [--json]",
226
+ "Usage: cockpit team [members | invite <email> --role <role> | role <userId> --role <role> | device [list] | device revoke <id|name> --reason <label>] [--json]",
227
227
  "",
228
228
  " cockpit team [members]",
229
229
  " Everyone on your team — roster and accounts together, with each person's id.",
@@ -232,6 +232,14 @@ function localSubcommandHelp(command) {
232
232
  " Admin only. Defaults to your own team; Tower emails them a sign-in link.",
233
233
  " cockpit team role <userId> --role <role> [--yes]",
234
234
  " Admin only. Asks first unless --yes. You cannot change your own role.",
235
+ " cockpit team device [list] [--json]",
236
+ " Every paired collector device: name, id, trust state, owner, and why",
237
+ " (revoked reason, or when it was approved, or that it's still waiting).",
238
+ " Admin only.",
239
+ " cockpit team device revoke <id|name> --reason <label> [--note \"<text>\"] [--yes]",
240
+ " Admin only. Names the device, asks first unless --yes, then revokes it —",
241
+ " this cannot be undone. --reason must be one of: stale_test_pairing,",
242
+ " machine_retired, person_left, re_pairing, other.",
235
243
  ],
236
244
  ],
237
245
  [
@@ -0,0 +1,136 @@
1
+ import { errorMessage, writeLine } from "./cli-io.js";
2
+ import { agentRuleHostLabel } from "./agent-rules-command.js";
3
+ import { autostartLocationLine } from "./autostart-command.js";
4
+ import { backfillRetryCommand, runBackfill, } from "./backfill.js";
5
+ import { addInstallEvent, reportInstallEventsBestEffort } from "./install-receipts.js";
6
+ import { addAutostartInstallEvent, addOnboardFailureEvent, classifyOnboardBlocker, onboardAutostartFailed, } from "./onboard-receipts.js";
7
+ import { nextStepForOnboardBlocker, onboardAgentRulesInstallLine, onboardJsonPayload, writeOnboardJson, } from "./onboard-report.js";
8
+ import { installAgentRules } from "../agent-rules.js";
9
+ import { installAutostartAgent } from "../autostart.js";
10
+ import { inspectLocalCollectorStatus, } from "../local-state.js";
11
+ function requiredOnboardBackfillCommand(command) {
12
+ return {
13
+ homeDir: command.homeDir,
14
+ all: true,
15
+ dryRun: false,
16
+ maxDepth: command.maxDepth,
17
+ maxRepos: command.maxRepos,
18
+ yes: true,
19
+ json: true,
20
+ };
21
+ }
22
+ export async function runRequiredOnboardBackfill(command, io) {
23
+ const backfillCommand = requiredOnboardBackfillCommand(command);
24
+ const retryCommand = backfillRetryCommand(backfillCommand);
25
+ try {
26
+ const result = await runBackfill(backfillCommand, io);
27
+ return {
28
+ status: result.status,
29
+ failureReason: result.status === "complete"
30
+ ? null
31
+ : result.failure_reason ?? `backfill_${result.status}`,
32
+ retryCommand: result.retry_command,
33
+ result,
34
+ };
35
+ }
36
+ catch (error) {
37
+ return {
38
+ status: "failed",
39
+ failureReason: errorMessage(error),
40
+ retryCommand,
41
+ result: null,
42
+ };
43
+ }
44
+ }
45
+ /** The last two setup steps of a successful onboard, in the order both arms run them. */
46
+ export async function completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io) {
47
+ const agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
48
+ addInstallEvent(installEvents, "agent_rules", "ok");
49
+ const autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
50
+ addAutostartInstallEvent(installEvents, autostart);
51
+ return { agentRules, autostart, ok: !onboardAutostartFailed(autostart) };
52
+ }
53
+ async function refreshOnboardAgentRules(command, roots, io) {
54
+ const result = await installAgentRules({
55
+ homeDir: command.homeDir,
56
+ scopePaths: roots,
57
+ });
58
+ if (command.json)
59
+ return result;
60
+ writeLine(io.stdout, `Agent rules: ${onboardAgentRulesInstallLine(result)}`);
61
+ for (const target of result.targets) {
62
+ writeLine(io.stdout, `${agentRuleHostLabel(target.host)}: ${target.rules_file}`);
63
+ }
64
+ return result;
65
+ }
66
+ /** Refreshes the host autostart backend after successful onboarding. */
67
+ async function refreshOnboardAutostart(command, roots, io) {
68
+ if (!io.exec)
69
+ return null;
70
+ const result = await installAutostartAgent({
71
+ homeDir: command.homeDir,
72
+ repoRoot: roots[0],
73
+ repoRoots: roots,
74
+ dashboardUrl: command.dashboardUrl,
75
+ exec: io.exec,
76
+ });
77
+ if (command.json)
78
+ return result;
79
+ if (result.status === "unsupported") {
80
+ writeLine(io.stdout, `Background autostart unsupported: ${result.message}`);
81
+ return result;
82
+ }
83
+ writeLine(io.stdout, result.loaded
84
+ ? "Background autostart refreshed; Tower syncs at login and every 15 min."
85
+ : "Background autostart refreshed, but the operating-system scheduler reported a problem; check `cockpit autostart status`.");
86
+ writeLine(io.stdout, autostartLocationLine(result));
87
+ return result;
88
+ }
89
+ /**
90
+ * Every onboard exit funnels through here so the best-effort install-event
91
+ * report always fires, pass or blocked — replaces a `finish` closure that used
92
+ * to live inside `runOnboard` so the flow functions below can call it too.
93
+ */
94
+ export async function finishOnboardRun(command, installEvents, io, code) {
95
+ await reportInstallEventsBestEffort({
96
+ homeDir: command.homeDir,
97
+ dashboardUrl: command.dashboardUrl,
98
+ command: "onboard",
99
+ events: installEvents,
100
+ json: command.json,
101
+ io,
102
+ });
103
+ return code;
104
+ }
105
+ /**
106
+ * Every uncaught error from either onboarding arm lands here. `status` is
107
+ * always recomputed fresh against whichever root got furthest, never trusted
108
+ * from before the throw — the arms already recompute it themselves on their
109
+ * own success paths, so nothing upstream is lost by not threading it through.
110
+ */
111
+ export async function handleOnboardFailure(error, command, rootsResult, install, pair, sync, installEvents, io) {
112
+ const message = errorMessage(error);
113
+ const statusRoot = rootsResult?.roots[0] ?? command.repoRoot;
114
+ const status = statusRoot
115
+ ? await inspectLocalCollectorStatus({
116
+ homeDir: command.homeDir,
117
+ repoRoot: statusRoot,
118
+ branch: command.branch,
119
+ }).catch(() => null)
120
+ : null;
121
+ const blocker = classifyOnboardBlocker(message);
122
+ addOnboardFailureEvent(installEvents, blocker);
123
+ const nextStep = nextStepForOnboardBlocker(blocker, command);
124
+ if (command.json) {
125
+ writeOnboardJson(io, onboardJsonPayload("blocked", command, install, pair, sync, status, rootsResult, {
126
+ blocker,
127
+ message,
128
+ next_step: nextStep,
129
+ }));
130
+ }
131
+ else {
132
+ writeLine(io.stderr, `BLOCKED: ${message}`);
133
+ writeLine(io.stderr, `Next: ${nextStep}`);
134
+ }
135
+ return finishOnboardRun(command, installEvents, io, 1);
136
+ }
@@ -0,0 +1,165 @@
1
+ import { writeLine } from "./cli-io.js";
2
+ import { attributedSyncRunStatus, shortSha, worktreeSyncRow, writeAgentSessionSummary, } from "./collection-report.js";
3
+ import { collectionRootConsentAliases } from "./collection-roots.js";
4
+ import { addInstallEvent } from "./install-receipts.js";
5
+ import { completeOnboardingSetup, finishOnboardRun, runRequiredOnboardBackfill, } from "./onboard-completion.js";
6
+ import { onboardJsonPayload, reportOnboardMultiRepoResult, reportOnboardSyncBlocked, writeOnboardAutostartBlocker, writeOnboardBackfillBlockedResult, writeOnboardLiveStatus, writeOnboardMultiRepoSyncBlocked, writeOnboardSingleRepoResultJson, writeOnboardUploadNarrative, writeOnboardWorkContextStarted, } from "./onboard-report.js";
7
+ import { runAttributedWorktreeSync, } from "./session-sync.js";
8
+ import { inspectLocalCollectorStatus, startLocalWorkContext, } from "../local-state.js";
9
+ /** The parent-folder arm: sync every discovered worktree, then backfill and finish setup once. */
10
+ export async function runOnboardMultiRepoFlow(ctx) {
11
+ const { command, resolvedCommand, collectionRoots, rootsResult, install, pair, installEvents, io, worktrees, } = ctx;
12
+ const multi = await runMultiRepoOnboard(resolvedCommand, io, worktrees);
13
+ addInstallEvent(installEvents, "work_context", "ok");
14
+ addInstallEvent(installEvents, "sync", multi.ok ? "ok" : "fail", multi.ok ? undefined : "sync_blocked");
15
+ if (!multi.ok) {
16
+ if (command.json)
17
+ writeOnboardMultiRepoSyncBlocked(ctx, multi);
18
+ return finishOnboardRun(command, installEvents, io, 1);
19
+ }
20
+ const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
21
+ const completedBackfill = backfill.status === "complete" ? backfill.result : null;
22
+ addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
23
+ ? undefined
24
+ : backfill.failureReason ?? "backfill_incomplete");
25
+ if (!completedBackfill) {
26
+ writeOnboardBackfillBlockedResult(io, {
27
+ json: command.json,
28
+ base: onboardJsonPayload("blocked", resolvedCommand, install, pair, null, null, rootsResult),
29
+ extra: {
30
+ mode: "multi_repo",
31
+ repos: multi.results,
32
+ codex_sessions: multi.codex_sessions,
33
+ },
34
+ backfill,
35
+ });
36
+ return finishOnboardRun(command, installEvents, io, 1);
37
+ }
38
+ const multiSetup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
39
+ reportOnboardMultiRepoResult(ctx, {
40
+ multi,
41
+ backfill: completedBackfill,
42
+ setup: multiSetup,
43
+ });
44
+ return finishOnboardRun(command, installEvents, io, multiSetup.ok ? 0 : 1);
45
+ }
46
+ /** The single-repo arm: start the work context, sync it, backfill, then finish setup once. */
47
+ export async function runOnboardSingleRepoFlow(ctx, single) {
48
+ const { command, resolvedCommand, collectionRoots, rootsResult, install, pair, installEvents, io, worktrees, } = ctx;
49
+ const worktreeRoot = worktrees[0]?.repo_root ?? single.primaryRoot;
50
+ const context = await startLocalWorkContext({
51
+ homeDir: command.homeDir,
52
+ repoRoot: worktreeRoot,
53
+ branch: command.branch,
54
+ activeTicketId: command.activeTicketId,
55
+ });
56
+ if (!command.json)
57
+ writeOnboardWorkContextStarted(io, context);
58
+ addInstallEvent(installEvents, "work_context", "ok");
59
+ const run = await runAttributedWorktreeSync({
60
+ homeDir: command.homeDir,
61
+ dashboardUrl: command.dashboardUrl,
62
+ collectionRoots: single.attributionCollectionRoots,
63
+ startContexts: false,
64
+ worktrees,
65
+ fetchImpl: io.fetch,
66
+ });
67
+ const sync = run.outcomes[0]?.sync ?? null;
68
+ single.syncHolder.current = sync;
69
+ if (!sync) {
70
+ throw new Error("Onboard sync produced no result for the repo worktree.");
71
+ }
72
+ const status = await inspectLocalCollectorStatus({
73
+ homeDir: command.homeDir,
74
+ repoRoot: worktreeRoot,
75
+ branch: command.branch,
76
+ });
77
+ if (!run.ok || sync.status !== "uploaded") {
78
+ addInstallEvent(installEvents, "sync", "fail", "sync_blocked");
79
+ reportOnboardSyncBlocked(ctx, { run, sync, status, worktreeRoot });
80
+ return finishOnboardRun(command, installEvents, io, 1);
81
+ }
82
+ addInstallEvent(installEvents, "sync", "ok");
83
+ const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
84
+ const completedBackfill = backfill.status === "complete" ? backfill.result : null;
85
+ addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
86
+ ? undefined
87
+ : backfill.failureReason ?? "backfill_incomplete");
88
+ if (!completedBackfill) {
89
+ writeOnboardBackfillBlockedResult(io, {
90
+ json: command.json,
91
+ base: onboardJsonPayload("blocked", resolvedCommand, install, pair, sync, status, rootsResult),
92
+ extra: { codex_sessions: run.summary },
93
+ backfill,
94
+ });
95
+ return finishOnboardRun(command, installEvents, io, 1);
96
+ }
97
+ if (command.json) {
98
+ const jsonSetup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
99
+ writeOnboardSingleRepoResultJson(ctx, {
100
+ run,
101
+ sync,
102
+ status,
103
+ backfill: completedBackfill,
104
+ setup: jsonSetup,
105
+ });
106
+ return finishOnboardRun(command, installEvents, io, jsonSetup.ok ? 0 : 1);
107
+ }
108
+ writeOnboardUploadNarrative(io, { command, run, sync, status });
109
+ const setup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
110
+ const { agentRules, autostart } = setup;
111
+ if (!setup.ok) {
112
+ writeOnboardAutostartBlocker(io, autostart);
113
+ return finishOnboardRun(command, installEvents, io, 1);
114
+ }
115
+ writeLine(io.stdout, "PASS: Tower is set up and collecting.");
116
+ writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
117
+ pair,
118
+ status,
119
+ sync,
120
+ agentRules,
121
+ autostart,
122
+ initialSyncOk: true,
123
+ backfill: completedBackfill,
124
+ });
125
+ return finishOnboardRun(command, installEvents, io, 0);
126
+ }
127
+ async function runMultiRepoOnboard(command, io, worktrees) {
128
+ if (!command.json) {
129
+ writeLine(io.stdout, `3/5 Parent folder mode: discovered ${worktrees.length} git worktree(s).`);
130
+ }
131
+ const collectionRoots = await collectionRootConsentAliases(command.collectionRoots ??
132
+ (command.repoRoot ? [command.repoRoot] : []));
133
+ const run = await runAttributedWorktreeSync({
134
+ homeDir: command.homeDir,
135
+ dashboardUrl: command.dashboardUrl,
136
+ branch: command.branch,
137
+ activeTicketId: command.activeTicketId,
138
+ collectionRoots,
139
+ startContexts: true,
140
+ worktrees,
141
+ fetchImpl: io.fetch,
142
+ });
143
+ const results = run.outcomes.map((outcome) => worktreeSyncRow(outcome, run));
144
+ const collectionRunStatus = attributedSyncRunStatus(run);
145
+ if (!command.json) {
146
+ for (const [index, outcome] of run.outcomes.entries()) {
147
+ const row = results[index];
148
+ if (!row)
149
+ continue;
150
+ const uploaded = outcome.sync.status === "uploaded";
151
+ writeLine(uploaded ? io.stdout : io.stderr, `${uploaded ? "SYNCED" : "BLOCKED"} ${row["repo_label"]}/${row["worktree_label"]} (${row["branch"]}) head:${shortSha(outcome.sync.head_sha ?? outcome.worktree.head_sha)} ${outcome.sync.status} objects:${outcome.sync.raw_evidence_uploaded_object_count} chunks:${outcome.sync.raw_evidence_uploaded_chunk_count} reused:${outcome.sync.raw_evidence_reused_count} failed:${outcome.sync.raw_evidence_failed_count} sessions:${row["attributed_session_count"]}${row["failure_reason"] ? ` reason:${row["failure_reason"]}` : ""}`);
152
+ }
153
+ writeLine(io.stdout, "4/5 Parent worktree sync complete.");
154
+ writeLine(io.stdout, `Uploaded: ${results.filter((row) => row["upload_status"] === "uploaded").length}/${results.length}`);
155
+ writeAgentSessionSummary(io, run.summary);
156
+ if (run.ok) {
157
+ writeLine(io.stdout, "Live sync receipts complete; verifying all-history Codex and Claude backfill.");
158
+ }
159
+ else {
160
+ writeLine(io.stderr, `BLOCKED: Tower collection is ${collectionRunStatus}; retry until every eligible session has a durable receipt or explicit terminal reason.`);
161
+ }
162
+ writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
163
+ }
164
+ return { ok: run.ok, results, codex_sessions: run.summary };
165
+ }
@@ -0,0 +1,102 @@
1
+ import { writeLine } from "./cli-io.js";
2
+ import { collectionRootConsentAliases, persistOnboardingRootConfig, resolveOnboardingRootsForCommand, } from "./collection-roots.js";
3
+ import { addInstallEvent } from "./install-receipts.js";
4
+ import { canReuseOnboardSession, pairLocalCollectorWithAuthFallback, readOnboardSessionReuseCandidate, requestPairingAccessTokenDetailed, } from "./local-auth.js";
5
+ import { writePairingInstructions } from "./onboard-report.js";
6
+ import { inspectLocalCollectorStatus, logoutLocalCollector, } from "../local-state.js";
7
+ /** Step 0: which folders are approved, and who claims to own this machine's uploads. */
8
+ export async function prepareOnboardRoots(command, installEvents, io) {
9
+ const resolvedRoots = await resolveOnboardingRootsForCommand(command, io);
10
+ const rootsResult = resolvedRoots.rootsResult;
11
+ const collectionRoots = rootsResult.roots;
12
+ const attributionCollectionRoots = await collectionRootConsentAliases(collectionRoots);
13
+ const primaryRoot = resolvedRoots.primaryRoot;
14
+ const resolvedCommand = {
15
+ ...command,
16
+ repoRoot: primaryRoot,
17
+ collectionRoots,
18
+ };
19
+ if (!command.json) {
20
+ writeLine(io.stdout, `Collecting from: ${collectionRoots.join(", ")}`);
21
+ }
22
+ if (rootsResult.homeRootOptIn) {
23
+ addInstallEvent(installEvents, "home_root_optin", "ok");
24
+ }
25
+ return {
26
+ resolvedRoots,
27
+ rootsResult,
28
+ collectionRoots,
29
+ attributionCollectionRoots,
30
+ primaryRoot,
31
+ existingConfig: resolvedRoots.existingConfig,
32
+ resolvedCommand,
33
+ };
34
+ }
35
+ /** Step 1: write the local config that makes this machine a known collector. */
36
+ export async function installOnboardConfig(command, resolvedRoots, installEvents, io) {
37
+ const install = await persistOnboardingRootConfig(command, resolvedRoots);
38
+ addInstallEvent(installEvents, "install", "ok");
39
+ if (!command.json) {
40
+ writeLine(io.stdout, "1/5 Installed local collector.");
41
+ writeLine(io.stdout, `Config: ${install.paths.config_file}`);
42
+ }
43
+ return install;
44
+ }
45
+ /**
46
+ * Step 2 of onboarding: reuse the device session when it already belongs to
47
+ * this owner on this dashboard, otherwise sign in and pair — logging the old
48
+ * session out first when it belongs to someone else.
49
+ *
50
+ * Returns null when an existing session was reused, which is why the caller's
51
+ * `pair` stays null and the readiness proof falls back to the status file.
52
+ */
53
+ export async function pairForOnboarding(command, input, installEvents, io) {
54
+ const installedStatus = await inspectLocalCollectorStatus({
55
+ homeDir: command.homeDir,
56
+ repoRoot: input.primaryRoot,
57
+ branch: command.branch,
58
+ });
59
+ const installedSession = await readOnboardSessionReuseCandidate(command.homeDir);
60
+ const canReuseInstalledSession = canReuseOnboardSession(installedSession, input.claimedOwnerEmail, command.dashboardUrl);
61
+ if (installedStatus.session_state === "valid" && canReuseInstalledSession) {
62
+ addInstallEvent(installEvents, "auth", "skipped", "existing_session");
63
+ addInstallEvent(installEvents, "pair", "skipped", "existing_session");
64
+ if (!command.json) {
65
+ writeLine(io.stdout, "2/5 Existing valid device session found; pairing skipped.");
66
+ }
67
+ return null;
68
+ }
69
+ if (installedStatus.session_state === "valid") {
70
+ await logoutLocalCollector({ homeDir: command.homeDir });
71
+ if (!command.json) {
72
+ writeLine(io.stdout, "2/5 Existing valid device session does not match requested owner or dashboard; pairing again.");
73
+ }
74
+ }
75
+ const authResult = await requestPairingAccessTokenDetailed({
76
+ dashboardUrl: command.dashboardUrl,
77
+ email: input.claimedOwnerEmail,
78
+ noAuth: command.noAuth,
79
+ json: command.json,
80
+ }, io);
81
+ addInstallEvent(installEvents, "auth", authResult.status, authResult.errorCode);
82
+ const pair = await pairLocalCollectorWithAuthFallback({
83
+ homeDir: command.homeDir,
84
+ dashboardUrl: command.dashboardUrl,
85
+ claimedOwnerEmail: input.claimedOwnerEmail,
86
+ deviceName: command.deviceName,
87
+ pollIntervalMs: command.pollIntervalMs,
88
+ timeoutMs: command.timeoutMs,
89
+ pairingAccessToken: authResult.accessToken,
90
+ fetch: io.fetch,
91
+ onPairStarted: command.json
92
+ ? undefined
93
+ : (request) => writePairingInstructions(io, request),
94
+ }, io);
95
+ addInstallEvent(installEvents, "pair", "ok");
96
+ if (!command.json) {
97
+ writeLine(io.stdout, "2/5 Device paired.");
98
+ writeLine(io.stdout, `User: ${pair.session.email ?? pair.session.auth_subject_id}`);
99
+ writeLine(io.stdout, `Device: ${pair.session.device_name ?? pair.session.device_id ?? "unknown"}`);
100
+ }
101
+ return pair;
102
+ }