@bli-cockpit/cli 0.2.48 → 0.2.50

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 (83) hide show
  1. package/dist/adapters/raw-evidence-attribution-gaps.js +133 -0
  2. package/dist/adapters/raw-evidence-claude-reader.js +108 -0
  3. package/dist/adapters/raw-evidence-codex-reader.js +147 -0
  4. package/dist/adapters/raw-evidence-collection-state.js +199 -0
  5. package/dist/adapters/raw-evidence-facts.js +338 -0
  6. package/dist/adapters/raw-evidence-git-diff-reader.js +187 -0
  7. package/dist/adapters/raw-evidence-image-reader.js +107 -0
  8. package/dist/adapters/raw-evidence-sanitize.js +56 -0
  9. package/dist/adapters/raw-evidence-transcript-file.js +182 -0
  10. package/dist/adapters/raw-evidence.js +94 -1203
  11. package/dist/autostart-contract.js +79 -0
  12. package/dist/autostart-darwin-plist.js +265 -0
  13. package/dist/autostart-darwin.js +171 -0
  14. package/dist/autostart-windows-scripts.js +310 -0
  15. package/dist/autostart-windows-task-xml.js +260 -0
  16. package/dist/autostart-windows.js +237 -0
  17. package/dist/autostart-xml.js +23 -0
  18. package/dist/autostart.js +35 -1148
  19. package/dist/commands/agent-rules-command.js +55 -0
  20. package/dist/commands/agent-session-report.js +290 -0
  21. package/dist/commands/analyze.js +131 -0
  22. package/dist/commands/autostart-command.js +105 -0
  23. package/dist/commands/backfill-batches.js +34 -0
  24. package/dist/commands/backfill-candidates.js +54 -0
  25. package/dist/commands/backfill-checkpoint.js +101 -0
  26. package/dist/commands/backfill-command-line.js +70 -0
  27. package/dist/commands/backfill-evidence-outcomes.js +104 -0
  28. package/dist/commands/backfill-issues.js +265 -0
  29. package/dist/commands/backfill-output.js +75 -0
  30. package/dist/commands/backfill-plan.js +71 -0
  31. package/dist/commands/backfill-reasons.js +107 -0
  32. package/dist/commands/backfill-report.js +298 -0
  33. package/dist/commands/backfill-result.js +150 -0
  34. package/dist/commands/backfill-scan.js +274 -0
  35. package/dist/commands/backfill-scope.js +114 -0
  36. package/dist/commands/backfill-session-report.js +145 -0
  37. package/dist/commands/backfill-types.js +1 -0
  38. package/dist/commands/backfill-upload.js +212 -0
  39. package/dist/commands/backfill.js +58 -1705
  40. package/dist/commands/cli-io.js +13 -0
  41. package/dist/commands/doctor.js +57 -0
  42. package/dist/commands/jarvis-trace.js +184 -0
  43. package/dist/commands/jarvis.js +323 -7
  44. package/dist/commands/local-arg-values.js +169 -0
  45. package/dist/commands/local-args-collector.js +604 -0
  46. package/dist/commands/local-args-tower.js +891 -0
  47. package/dist/commands/local-args.js +10 -1549
  48. package/dist/commands/local-help.js +30 -5
  49. package/dist/commands/local.js +21 -1786
  50. package/dist/commands/login.js +53 -0
  51. package/dist/commands/logout.js +66 -0
  52. package/dist/commands/memory-install-claude.js +294 -0
  53. package/dist/commands/memory-install-codex.js +205 -0
  54. package/dist/commands/memory-install-contract.js +231 -0
  55. package/dist/commands/memory-install-files.js +63 -0
  56. package/dist/commands/memory-install-skills.js +121 -0
  57. package/dist/commands/memory-install-toml.js +265 -0
  58. package/dist/commands/memory-install.js +378 -0
  59. package/dist/commands/onboard-receipts.js +66 -0
  60. package/dist/commands/onboard-report.js +274 -0
  61. package/dist/commands/onboard.js +449 -0
  62. package/dist/commands/ops-render.js +36 -0
  63. package/dist/commands/public-root.js +1 -1
  64. package/dist/commands/serve.js +13 -0
  65. package/dist/commands/session-sync.js +513 -534
  66. package/dist/commands/settings-render.js +28 -0
  67. package/dist/commands/settings.js +66 -2
  68. package/dist/commands/start.js +47 -0
  69. package/dist/commands/sync-followups.js +308 -0
  70. package/dist/commands/sync.js +387 -0
  71. package/dist/local-state-attributed-target.js +75 -0
  72. package/dist/local-state-config.js +147 -0
  73. package/dist/local-state-files.js +59 -0
  74. package/dist/local-state-identity.js +73 -0
  75. package/dist/local-state-pairing.js +263 -0
  76. package/dist/local-state-paths.js +61 -0
  77. package/dist/local-state-session.js +68 -0
  78. package/dist/local-state-status.js +163 -0
  79. package/dist/local-state-work-context.js +190 -0
  80. package/dist/local-state.js +34 -848
  81. package/dist/tower-client.js +3 -2
  82. package/dist/tower-stream.js +76 -6
  83. package/package.json +2 -1
@@ -0,0 +1,449 @@
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 { attributedSyncRunStatus, shortSha, worktreeSyncRow, writeAgentSessionSummary, } from "./collection-report.js";
6
+ import { collectionRootConsentAliases, persistOnboardingRootConfig, resolveOnboardingRootsForCommand, } from "./collection-roots.js";
7
+ import { addInstallEvent, reportInstallEventsBestEffort } from "./install-receipts.js";
8
+ import { canReuseOnboardSession, pairLocalCollectorWithAuthFallback, readOnboardSessionReuseCandidate, requestPairingAccessTokenDetailed, resolveOnboardEmail, } from "./local-auth.js";
9
+ import { discoverCommandWorktrees } from "./local-discovery.js";
10
+ import { addAutostartInstallEvent, addOnboardFailureEvent, classifyOnboardBlocker, onboardAutostartFailed, } from "./onboard-receipts.js";
11
+ import { nextStepForOnboardBlocker, onboardAgentRulesInstallLine, onboardJsonPayload, reportOnboardMultiRepoResult, reportOnboardSyncBlocked, writeOnboardAutostartBlocker, writeOnboardBackfillBlockedResult, writeOnboardBanner, writeOnboardJson, writeOnboardLiveStatus, writeOnboardMultiRepoSyncBlocked, writeOnboardSingleRepoResultJson, writeOnboardUploadNarrative, writeOnboardWorkContextStarted, writePairingInstructions, } from "./onboard-report.js";
12
+ import { runAttributedWorktreeSync, } from "./session-sync.js";
13
+ import { installAgentRules } from "../agent-rules.js";
14
+ import { installAutostartAgent } from "../autostart.js";
15
+ import { inspectLocalCollectorStatus, logoutLocalCollector, startLocalWorkContext, } from "../local-state.js";
16
+ /**
17
+ * The onboarding runbook: resolve roots, install, pair, discover worktrees,
18
+ * then hand off to whichever arm matches what was found — one repo runs the
19
+ * single-repo sync-then-backfill sequence (`runOnboardSingleRepoFlow`),
20
+ * several run the parent-folder one (`runOnboardMultiRepoFlow`). Any failure
21
+ * anywhere in either arm unwinds to the single catch below.
22
+ */
23
+ export async function runOnboard(command, io) {
24
+ const installEvents = [];
25
+ let install = null;
26
+ let pair = null;
27
+ let rootsResult = null;
28
+ // `sync` has to survive a throw from inside either flow function, so it is
29
+ // mutated through this holder rather than returned — a return only happens
30
+ // on the success path, and the failure path needs the value too.
31
+ const syncHolder = {
32
+ current: null,
33
+ };
34
+ try {
35
+ if (!command.json)
36
+ writeOnboardBanner(command, io);
37
+ const roots = await prepareOnboardRoots(command, installEvents, io);
38
+ rootsResult = roots.rootsResult;
39
+ const claimedOwnerEmail = await resolveOnboardEmail(roots.resolvedCommand, roots.collectionRoots, roots.existingConfig, io);
40
+ install = await installOnboardConfig(command, roots.resolvedRoots, installEvents, io);
41
+ pair = await pairForOnboarding(command, { primaryRoot: roots.primaryRoot, claimedOwnerEmail }, installEvents, io);
42
+ const worktrees = await discoverCommandWorktrees(roots.collectionRoots, {
43
+ maxDepth: command.maxDepth,
44
+ maxRepos: command.maxRepos,
45
+ homeDir: command.homeDir,
46
+ allowEmpty: true,
47
+ }, io);
48
+ const ctx = {
49
+ command,
50
+ resolvedCommand: roots.resolvedCommand,
51
+ collectionRoots: roots.collectionRoots,
52
+ rootsResult: roots.rootsResult,
53
+ install,
54
+ pair,
55
+ installEvents,
56
+ io,
57
+ worktrees,
58
+ };
59
+ if (worktrees.length > 1) {
60
+ return await runOnboardMultiRepoFlow(ctx);
61
+ }
62
+ return await runOnboardSingleRepoFlow(ctx, {
63
+ attributionCollectionRoots: roots.attributionCollectionRoots,
64
+ primaryRoot: roots.primaryRoot,
65
+ syncHolder,
66
+ });
67
+ }
68
+ catch (error) {
69
+ return await handleOnboardFailure(error, command, rootsResult, install, pair, syncHolder.current, installEvents, io);
70
+ }
71
+ }
72
+ /** The parent-folder arm: sync every discovered worktree, then backfill and finish setup once. */
73
+ async function runOnboardMultiRepoFlow(ctx) {
74
+ const { command, resolvedCommand, collectionRoots, rootsResult, install, pair, installEvents, io, worktrees, } = ctx;
75
+ const multi = await runMultiRepoOnboard(resolvedCommand, io, worktrees);
76
+ addInstallEvent(installEvents, "work_context", "ok");
77
+ addInstallEvent(installEvents, "sync", multi.ok ? "ok" : "fail", multi.ok ? undefined : "sync_blocked");
78
+ if (!multi.ok) {
79
+ if (command.json)
80
+ writeOnboardMultiRepoSyncBlocked(ctx, multi);
81
+ return finishOnboardRun(command, installEvents, io, 1);
82
+ }
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, null, null, rootsResult),
92
+ extra: {
93
+ mode: "multi_repo",
94
+ repos: multi.results,
95
+ codex_sessions: multi.codex_sessions,
96
+ },
97
+ backfill,
98
+ });
99
+ return finishOnboardRun(command, installEvents, io, 1);
100
+ }
101
+ const multiSetup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
102
+ reportOnboardMultiRepoResult(ctx, {
103
+ multi,
104
+ backfill: completedBackfill,
105
+ setup: multiSetup,
106
+ });
107
+ return finishOnboardRun(command, installEvents, io, multiSetup.ok ? 0 : 1);
108
+ }
109
+ /** The single-repo arm: start the work context, sync it, backfill, then finish setup once. */
110
+ async function runOnboardSingleRepoFlow(ctx, single) {
111
+ const { command, resolvedCommand, collectionRoots, rootsResult, install, pair, installEvents, io, worktrees, } = ctx;
112
+ const worktreeRoot = worktrees[0]?.repo_root ?? single.primaryRoot;
113
+ const context = await startLocalWorkContext({
114
+ homeDir: command.homeDir,
115
+ repoRoot: worktreeRoot,
116
+ branch: command.branch,
117
+ activeTicketId: command.activeTicketId,
118
+ });
119
+ if (!command.json)
120
+ writeOnboardWorkContextStarted(io, context);
121
+ addInstallEvent(installEvents, "work_context", "ok");
122
+ const run = await runAttributedWorktreeSync({
123
+ homeDir: command.homeDir,
124
+ dashboardUrl: command.dashboardUrl,
125
+ collectionRoots: single.attributionCollectionRoots,
126
+ startContexts: false,
127
+ worktrees,
128
+ fetchImpl: io.fetch,
129
+ });
130
+ const sync = run.outcomes[0]?.sync ?? null;
131
+ single.syncHolder.current = sync;
132
+ if (!sync) {
133
+ throw new Error("Onboard sync produced no result for the repo worktree.");
134
+ }
135
+ const status = await inspectLocalCollectorStatus({
136
+ homeDir: command.homeDir,
137
+ repoRoot: worktreeRoot,
138
+ branch: command.branch,
139
+ });
140
+ if (!run.ok || sync.status !== "uploaded") {
141
+ addInstallEvent(installEvents, "sync", "fail", "sync_blocked");
142
+ reportOnboardSyncBlocked(ctx, { run, sync, status, worktreeRoot });
143
+ return finishOnboardRun(command, installEvents, io, 1);
144
+ }
145
+ addInstallEvent(installEvents, "sync", "ok");
146
+ const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
147
+ const completedBackfill = backfill.status === "complete" ? backfill.result : null;
148
+ addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
149
+ ? undefined
150
+ : backfill.failureReason ?? "backfill_incomplete");
151
+ if (!completedBackfill) {
152
+ writeOnboardBackfillBlockedResult(io, {
153
+ json: command.json,
154
+ base: onboardJsonPayload("blocked", resolvedCommand, install, pair, sync, status, rootsResult),
155
+ extra: { codex_sessions: run.summary },
156
+ backfill,
157
+ });
158
+ return finishOnboardRun(command, installEvents, io, 1);
159
+ }
160
+ if (command.json) {
161
+ const jsonSetup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
162
+ writeOnboardSingleRepoResultJson(ctx, {
163
+ run,
164
+ sync,
165
+ status,
166
+ backfill: completedBackfill,
167
+ setup: jsonSetup,
168
+ });
169
+ return finishOnboardRun(command, installEvents, io, jsonSetup.ok ? 0 : 1);
170
+ }
171
+ writeOnboardUploadNarrative(io, { command, run, sync, status });
172
+ const setup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
173
+ const { agentRules, autostart } = setup;
174
+ if (!setup.ok) {
175
+ writeOnboardAutostartBlocker(io, autostart);
176
+ return finishOnboardRun(command, installEvents, io, 1);
177
+ }
178
+ writeLine(io.stdout, "PASS: Tower is set up and collecting.");
179
+ writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
180
+ pair,
181
+ status,
182
+ sync,
183
+ agentRules,
184
+ autostart,
185
+ initialSyncOk: true,
186
+ backfill: completedBackfill,
187
+ });
188
+ return finishOnboardRun(command, installEvents, io, 0);
189
+ }
190
+ /**
191
+ * Every uncaught error from either onboarding arm lands here. `status` is
192
+ * always recomputed fresh against whichever root got furthest, never trusted
193
+ * from before the throw — the arms already recompute it themselves on their
194
+ * own success paths, so nothing upstream is lost by not threading it through.
195
+ */
196
+ async function handleOnboardFailure(error, command, rootsResult, install, pair, sync, installEvents, io) {
197
+ const message = errorMessage(error);
198
+ const statusRoot = rootsResult?.roots[0] ?? command.repoRoot;
199
+ const status = statusRoot
200
+ ? await inspectLocalCollectorStatus({
201
+ homeDir: command.homeDir,
202
+ repoRoot: statusRoot,
203
+ branch: command.branch,
204
+ }).catch(() => null)
205
+ : null;
206
+ const blocker = classifyOnboardBlocker(message);
207
+ addOnboardFailureEvent(installEvents, blocker);
208
+ const nextStep = nextStepForOnboardBlocker(blocker, command);
209
+ if (command.json) {
210
+ writeOnboardJson(io, onboardJsonPayload("blocked", command, install, pair, sync, status, rootsResult, {
211
+ blocker,
212
+ message,
213
+ next_step: nextStep,
214
+ }));
215
+ }
216
+ else {
217
+ writeLine(io.stderr, `BLOCKED: ${message}`);
218
+ writeLine(io.stderr, `Next: ${nextStep}`);
219
+ }
220
+ return finishOnboardRun(command, installEvents, io, 1);
221
+ }
222
+ /** Step 0: which folders are approved, and who claims to own this machine's uploads. */
223
+ async function prepareOnboardRoots(command, installEvents, io) {
224
+ const resolvedRoots = await resolveOnboardingRootsForCommand(command, io);
225
+ const rootsResult = resolvedRoots.rootsResult;
226
+ const collectionRoots = rootsResult.roots;
227
+ const attributionCollectionRoots = await collectionRootConsentAliases(collectionRoots);
228
+ const primaryRoot = resolvedRoots.primaryRoot;
229
+ const resolvedCommand = {
230
+ ...command,
231
+ repoRoot: primaryRoot,
232
+ collectionRoots,
233
+ };
234
+ if (!command.json) {
235
+ writeLine(io.stdout, `Collecting from: ${collectionRoots.join(", ")}`);
236
+ }
237
+ if (rootsResult.homeRootOptIn) {
238
+ addInstallEvent(installEvents, "home_root_optin", "ok");
239
+ }
240
+ return {
241
+ resolvedRoots,
242
+ rootsResult,
243
+ collectionRoots,
244
+ attributionCollectionRoots,
245
+ primaryRoot,
246
+ existingConfig: resolvedRoots.existingConfig,
247
+ resolvedCommand,
248
+ };
249
+ }
250
+ /** Step 1: write the local config that makes this machine a known collector. */
251
+ async function installOnboardConfig(command, resolvedRoots, installEvents, io) {
252
+ const install = await persistOnboardingRootConfig(command, resolvedRoots);
253
+ addInstallEvent(installEvents, "install", "ok");
254
+ if (!command.json) {
255
+ writeLine(io.stdout, "1/5 Installed local collector.");
256
+ writeLine(io.stdout, `Config: ${install.paths.config_file}`);
257
+ }
258
+ return install;
259
+ }
260
+ /**
261
+ * Step 2 of onboarding: reuse the device session when it already belongs to
262
+ * this owner on this dashboard, otherwise sign in and pair — logging the old
263
+ * session out first when it belongs to someone else.
264
+ *
265
+ * Returns null when an existing session was reused, which is why the caller's
266
+ * `pair` stays null and the readiness proof falls back to the status file.
267
+ */
268
+ async function pairForOnboarding(command, input, installEvents, io) {
269
+ const installedStatus = await inspectLocalCollectorStatus({
270
+ homeDir: command.homeDir,
271
+ repoRoot: input.primaryRoot,
272
+ branch: command.branch,
273
+ });
274
+ const installedSession = await readOnboardSessionReuseCandidate(command.homeDir);
275
+ const canReuseInstalledSession = canReuseOnboardSession(installedSession, input.claimedOwnerEmail, command.dashboardUrl);
276
+ if (installedStatus.session_state === "valid" && canReuseInstalledSession) {
277
+ addInstallEvent(installEvents, "auth", "skipped", "existing_session");
278
+ addInstallEvent(installEvents, "pair", "skipped", "existing_session");
279
+ if (!command.json) {
280
+ writeLine(io.stdout, "2/5 Existing valid device session found; pairing skipped.");
281
+ }
282
+ return null;
283
+ }
284
+ if (installedStatus.session_state === "valid") {
285
+ await logoutLocalCollector({ homeDir: command.homeDir });
286
+ if (!command.json) {
287
+ writeLine(io.stdout, "2/5 Existing valid device session does not match requested owner or dashboard; pairing again.");
288
+ }
289
+ }
290
+ const authResult = await requestPairingAccessTokenDetailed({
291
+ dashboardUrl: command.dashboardUrl,
292
+ email: input.claimedOwnerEmail,
293
+ noAuth: command.noAuth,
294
+ json: command.json,
295
+ }, io);
296
+ addInstallEvent(installEvents, "auth", authResult.status, authResult.errorCode);
297
+ const pair = await pairLocalCollectorWithAuthFallback({
298
+ homeDir: command.homeDir,
299
+ dashboardUrl: command.dashboardUrl,
300
+ claimedOwnerEmail: input.claimedOwnerEmail,
301
+ deviceName: command.deviceName,
302
+ pollIntervalMs: command.pollIntervalMs,
303
+ timeoutMs: command.timeoutMs,
304
+ pairingAccessToken: authResult.accessToken,
305
+ fetch: io.fetch,
306
+ onPairStarted: command.json
307
+ ? undefined
308
+ : (request) => writePairingInstructions(io, request),
309
+ }, io);
310
+ addInstallEvent(installEvents, "pair", "ok");
311
+ if (!command.json) {
312
+ writeLine(io.stdout, "2/5 Device paired.");
313
+ writeLine(io.stdout, `User: ${pair.session.email ?? pair.session.auth_subject_id}`);
314
+ writeLine(io.stdout, `Device: ${pair.session.device_name ?? pair.session.device_id ?? "unknown"}`);
315
+ }
316
+ return pair;
317
+ }
318
+ async function runMultiRepoOnboard(command, io, worktrees) {
319
+ if (!command.json) {
320
+ writeLine(io.stdout, `3/5 Parent folder mode: discovered ${worktrees.length} git worktree(s).`);
321
+ }
322
+ const collectionRoots = await collectionRootConsentAliases(command.collectionRoots ??
323
+ (command.repoRoot ? [command.repoRoot] : []));
324
+ const run = await runAttributedWorktreeSync({
325
+ homeDir: command.homeDir,
326
+ dashboardUrl: command.dashboardUrl,
327
+ branch: command.branch,
328
+ activeTicketId: command.activeTicketId,
329
+ collectionRoots,
330
+ startContexts: true,
331
+ worktrees,
332
+ fetchImpl: io.fetch,
333
+ });
334
+ const results = run.outcomes.map((outcome) => worktreeSyncRow(outcome, run));
335
+ const collectionRunStatus = attributedSyncRunStatus(run);
336
+ if (!command.json) {
337
+ for (const [index, outcome] of run.outcomes.entries()) {
338
+ const row = results[index];
339
+ if (!row)
340
+ continue;
341
+ const uploaded = outcome.sync.status === "uploaded";
342
+ 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"]}` : ""}`);
343
+ }
344
+ writeLine(io.stdout, "4/5 Parent worktree sync complete.");
345
+ writeLine(io.stdout, `Uploaded: ${results.filter((row) => row["upload_status"] === "uploaded").length}/${results.length}`);
346
+ writeAgentSessionSummary(io, run.summary);
347
+ if (run.ok) {
348
+ writeLine(io.stdout, "Live sync receipts complete; verifying all-history Codex and Claude backfill.");
349
+ }
350
+ else {
351
+ writeLine(io.stderr, `BLOCKED: Tower collection is ${collectionRunStatus}; retry until every eligible session has a durable receipt or explicit terminal reason.`);
352
+ }
353
+ writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
354
+ }
355
+ return { ok: run.ok, results, codex_sessions: run.summary };
356
+ }
357
+ function requiredOnboardBackfillCommand(command) {
358
+ return {
359
+ homeDir: command.homeDir,
360
+ all: true,
361
+ dryRun: false,
362
+ maxDepth: command.maxDepth,
363
+ maxRepos: command.maxRepos,
364
+ yes: true,
365
+ json: true,
366
+ };
367
+ }
368
+ async function runRequiredOnboardBackfill(command, io) {
369
+ const backfillCommand = requiredOnboardBackfillCommand(command);
370
+ const retryCommand = backfillRetryCommand(backfillCommand);
371
+ try {
372
+ const result = await runBackfill(backfillCommand, io);
373
+ return {
374
+ status: result.status,
375
+ failureReason: result.status === "complete"
376
+ ? null
377
+ : result.failure_reason ?? `backfill_${result.status}`,
378
+ retryCommand: result.retry_command,
379
+ result,
380
+ };
381
+ }
382
+ catch (error) {
383
+ return {
384
+ status: "failed",
385
+ failureReason: errorMessage(error),
386
+ retryCommand,
387
+ result: null,
388
+ };
389
+ }
390
+ }
391
+ async function completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io) {
392
+ const agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
393
+ addInstallEvent(installEvents, "agent_rules", "ok");
394
+ const autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
395
+ addAutostartInstallEvent(installEvents, autostart);
396
+ return { agentRules, autostart, ok: !onboardAutostartFailed(autostart) };
397
+ }
398
+ async function refreshOnboardAgentRules(command, roots, io) {
399
+ const result = await installAgentRules({
400
+ homeDir: command.homeDir,
401
+ scopePaths: roots,
402
+ });
403
+ if (command.json)
404
+ return result;
405
+ writeLine(io.stdout, `Agent rules: ${onboardAgentRulesInstallLine(result)}`);
406
+ for (const target of result.targets) {
407
+ writeLine(io.stdout, `${agentRuleHostLabel(target.host)}: ${target.rules_file}`);
408
+ }
409
+ return result;
410
+ }
411
+ /** Refreshes the host autostart backend after successful onboarding. */
412
+ async function refreshOnboardAutostart(command, roots, io) {
413
+ if (!io.exec)
414
+ return null;
415
+ const result = await installAutostartAgent({
416
+ homeDir: command.homeDir,
417
+ repoRoot: roots[0],
418
+ repoRoots: roots,
419
+ dashboardUrl: command.dashboardUrl,
420
+ exec: io.exec,
421
+ });
422
+ if (command.json)
423
+ return result;
424
+ if (result.status === "unsupported") {
425
+ writeLine(io.stdout, `Background autostart unsupported: ${result.message}`);
426
+ return result;
427
+ }
428
+ writeLine(io.stdout, result.loaded
429
+ ? "Background autostart refreshed; Tower syncs at login and every 15 min."
430
+ : "Background autostart refreshed, but the operating-system scheduler reported a problem; check `cockpit autostart status`.");
431
+ writeLine(io.stdout, autostartLocationLine(result));
432
+ return result;
433
+ }
434
+ /**
435
+ * Every onboard exit funnels through here so the best-effort install-event
436
+ * report always fires, pass or blocked — replaces a `finish` closure that used
437
+ * to live inside `runOnboard` so the flow functions below can call it too.
438
+ */
439
+ async function finishOnboardRun(command, installEvents, io, code) {
440
+ await reportInstallEventsBestEffort({
441
+ homeDir: command.homeDir,
442
+ dashboardUrl: command.dashboardUrl,
443
+ command: "onboard",
444
+ events: installEvents,
445
+ json: command.json,
446
+ io,
447
+ });
448
+ return code;
449
+ }
@@ -82,6 +82,8 @@ export function renderOpsStatus(payload, dim) {
82
82
  }
83
83
  if (payload.fleet)
84
84
  lines.push(...renderFleet(payload.fleet, dim));
85
+ if (payload.coverage)
86
+ lines.push(...renderCoverage(payload.coverage, dim));
85
87
  const slack = payload.skips?.slack;
86
88
  const external = payload.skips?.external;
87
89
  if (slack || external) {
@@ -122,6 +124,40 @@ export function renderFleet(fleet, dim) {
122
124
  lines.push(dim(` ${fleet.noDeviceLine}`));
123
125
  return lines;
124
126
  }
127
+ /**
128
+ * Did the sessions arrive? RED first, then AMBER, then everybody else dimmed.
129
+ *
130
+ * The two days sit under a person only when something is wrong with them: on a
131
+ * clean fleet this is one summary line and a dimmed name per person, because
132
+ * eighteen day-lines nobody reads is how a section gets scrolled past.
133
+ */
134
+ export function renderCoverage(coverage, dim) {
135
+ const lines = ["", `COVERAGE ${coverage.summary ?? "(no summary)"}`];
136
+ if (coverage.readError) {
137
+ lines.push(` coverage could not be read (${coverage.readError}); nothing is known about whether anybody's sessions arrived`);
138
+ return lines;
139
+ }
140
+ const people = coverage.people ?? [];
141
+ if (people.length === 0) {
142
+ lines.push(dim(" nobody on the roster has collection coverage to check"));
143
+ return lines;
144
+ }
145
+ const rank = (colour) => colour === "red" ? 0 : colour === "amber" ? 1 : colour === "unknown" ? 2 : 3;
146
+ for (const person of [...people].sort((left, right) => rank(left.colour) - rank(right.colour))) {
147
+ const text = ` ${person.line ?? person.displayName ?? "(person)"}`;
148
+ const loud = person.colour === "red" || person.colour === "amber";
149
+ // A healthy person is dimmed, never dropped: "whose collection IS complete"
150
+ // is the other half of the question.
151
+ lines.push(loud ? text : dim(text));
152
+ if (!loud)
153
+ continue;
154
+ for (const day of person.days ?? []) {
155
+ if (day.line)
156
+ lines.push(dim(` ${day.line}`));
157
+ }
158
+ }
159
+ return lines;
160
+ }
125
161
  function renderSkipLedger(ledger, dim) {
126
162
  const lines = [];
127
163
  const name = ledger.relation ?? "skips";
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
15
15
  }
16
16
 
17
17
  if (command === "--version" || command === "-V" || command === "version") {
18
- writeLine(io?.stdout ?? process.stdout, "0.2.48");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.50");
19
19
  return 0;
20
20
  }
21
21
 
@@ -0,0 +1,13 @@
1
+ import { writeLine } from "./cli-io.js";
2
+ import { createCollectorServer } from "../server.js";
3
+ export async function runServe(command, io) {
4
+ const server = createCollectorServer(command);
5
+ await new Promise((resolve) => {
6
+ server.listen(command.port, "127.0.0.1", resolve);
7
+ });
8
+ writeLine(io.stdout, `Tower collector serving on http://127.0.0.1:${command.port}`);
9
+ await new Promise((resolve) => {
10
+ server.on("close", resolve);
11
+ });
12
+ return 0;
13
+ }