@bli-cockpit/cli 0.2.101 → 0.2.102

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.
@@ -14,7 +14,7 @@ import { runBackfillCommand } from "./backfill.js";
14
14
  import { diskRowMessage, mib, redeliveryLine } from "./doctor-disk-words.js";
15
15
  import { doctorRoots } from "./doctor-access.js";
16
16
  import { backfillCompletionStepState, backfillFixVerdict, jsonField, parseDoctorBackfillJson, parseDoctorSyncJson, syncBacklogDrainingVerdict, syncStandAsideVerdict, } from "./doctor-pipeline-verdicts.js";
17
- import { fail, needsFix, ok, skipped } from "./doctor-report.js";
17
+ import { asRecord, fail, needsFix, ok, skipped } from "./doctor-report.js";
18
18
  /**
19
19
  * The `backfill-complete`, `gc-checked`, `disk-bounded`, and `sync-fresh`
20
20
  * check family: does the collection pipeline itself have everything it
@@ -47,33 +47,69 @@ export async function checkBackfillState(context) {
47
47
  return needsFix("backfill-complete", cursor.updated_at ? "partial" : "never_run", "the catch-up over your old sessions has not finished");
48
48
  }
49
49
  export async function fixBackfillState(context) {
50
- return withDoctorLockWait(context, () => runBackfillRepair(context));
51
- }
52
- async function runBackfillRepair(context) {
53
- const capture = capturedIo(context.io, !context.command.json);
54
- const code = await runBackfillCommand({
55
- homeDir: context.command.homeDir,
56
- repoRoot: context.command.repoRoot,
57
- all: true,
58
- dryRun: false,
59
- yes: true,
60
- json: true,
61
- }, capture.io);
62
- const stdout = capture.stdout();
63
- const output = `${stdout}\n${capture.stderr()}`;
64
- if (code === 0) {
65
- // Re-read the marker this run just wrote instead of hand-rolling a second
66
- // message: `checkBackfillState`'s pure core already knows how to say
67
- // "complete" vs "complete_with_oversized_skips" (BLI-2727), and this way
68
- // the two can never say something different for the same marker.
69
- const recheck = await checkBackfillState(context);
70
- if (recheck.status === "ok")
71
- return recheck;
72
- return recheck;
50
+ const started = Date.now();
51
+ const deadline = started + (context.command.backfillBudgetSeconds ?? 900) * 1000;
52
+ let caughtUp = 0;
53
+ let remaining = null;
54
+ const unfinished = (code, reason) => ({
55
+ ...needsFix("backfill-complete", code, `caught up on ${caughtUp} old sessions this run, ${remaining ?? "unknown"} still to go; ${reason}; run \`cockpit doctor\` again to continue`),
56
+ nextAction: "cockpit doctor",
57
+ });
58
+ const progress = setInterval(() => {
59
+ const seconds = Math.floor((Date.now() - started) / 1000);
60
+ context.io.stderr.write(`backfill: ${caughtUp} caught up, ${remaining ?? "unknown"} to go, ${Math.floor(seconds / 60)}m${seconds % 60}s\n`);
61
+ console.error("[doctor] backfill-complete progress", JSON.stringify({ caught_up: caughtUp, remaining, elapsed_seconds: seconds }));
62
+ }, 30_000);
63
+ try {
64
+ while (Date.now() < deadline) {
65
+ // Every chunk reacquires the collector's locks. Bound lock waiting by
66
+ // the overall budget too; never abandon an in-flight cursor write.
67
+ const chunkContext = { ...context, command: { ...context.command,
68
+ lockWaitSeconds: Math.min(context.command.lockWaitSeconds ?? 600, (deadline - Date.now()) / 1000),
69
+ } };
70
+ const row = await withDoctorLockWait(chunkContext, async () => {
71
+ if (Date.now() >= deadline)
72
+ return unfinished("backfill_budget_timeout", "backfill time budget reached");
73
+ const capture = capturedIo(context.io, false);
74
+ const code = await runBackfillCommand({
75
+ homeDir: context.command.homeDir, repoRoot: context.command.repoRoot,
76
+ all: true, dryRun: false, yes: true, json: true,
77
+ }, capture.io);
78
+ const parsed = parseDoctorBackfillJson(capture.stdout());
79
+ const counts = asRecord(parsed?.counts);
80
+ const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
81
+ caughtUp += count(counts?.["backfilled"]) ?? 0;
82
+ remaining = count(counts?.["remaining"]) ?? remaining;
83
+ if (code === 0 && parsed?.status === "complete") {
84
+ // Reporting totals include permanent caps. Preserve their marker
85
+ // note instead of retrying history the collector says is complete.
86
+ const checked = await checkBackfillState(context);
87
+ return ok("backfill-complete", checked.status === "ok" ? checked.code : "complete", `caught up on ${caughtUp} old sessions this run` + (checked.status === "ok" ? `; ${checked.message}` : ""));
88
+ }
89
+ const verdict = backfillFixVerdict(parsed, jsonField(capture.stderr(), "failure_reason"));
90
+ if (parsed?.status === "partial" &&
91
+ (parsed.failure_reason === "deferred_budget_exhausted" || !parsed.failure_reason)) {
92
+ return needsFix("backfill-complete", "backfill_chunk_pending", "more history remains");
93
+ }
94
+ return { ...verdict, message: typeof parsed?.failure_reason === "string" ? parsed.failure_reason : "backfill failed without a valid completion receipt" };
95
+ });
96
+ if (row.status === "ok") {
97
+ console.error("[doctor] backfill-complete finished", JSON.stringify({ caught_up: caughtUp, remaining, elapsed_seconds: Math.floor((Date.now() - started) / 1000) }));
98
+ return row;
99
+ }
100
+ if (row.code !== "backfill_chunk_pending") {
101
+ return unfinished(row.code === "lock_wait_timeout" && Date.now() >= deadline ? "backfill_budget_timeout" : row.code, row.message);
102
+ }
103
+ }
104
+ return unfinished("backfill_budget_timeout", "backfill time budget reached");
105
+ }
106
+ catch (error) {
107
+ console.error("[doctor] backfill-complete failed", JSON.stringify({ reason: "backfill_threw", error_name: error instanceof Error ? error.name : "unknown" }));
108
+ return unfinished("backfill_threw", "backfill failed");
109
+ }
110
+ finally {
111
+ clearInterval(progress);
73
112
  }
74
- const verdict = backfillFixVerdict(parseDoctorBackfillJson(stdout), jsonField(output, "failure_reason"));
75
- console.error("[cockpit-doctor] catch-up run did not finish", JSON.stringify({ reason: verdict.code, row_status: verdict.status, exit_code: code }));
76
- return verdict;
77
113
  }
78
114
  export async function checkGcState(context) {
79
115
  if (context.io.env["COCKPIT_DISABLE_GC"] === "1") {
@@ -71,6 +71,8 @@ export function reexecDoctor(command, io) {
71
71
  const args = ["doctor"];
72
72
  if (command.homeDir)
73
73
  args.push("--home", command.homeDir);
74
+ if (command.backfillBudgetSeconds)
75
+ args.push("--backfill-budget", String(command.backfillBudgetSeconds));
74
76
  if (command.lockWaitSeconds)
75
77
  args.push("--lock-wait", String(command.lockWaitSeconds));
76
78
  if (command.allowHomeRoot)
@@ -101,6 +101,7 @@ export function parseDoctorArgs(alias, args) {
101
101
  "--check",
102
102
  "--no-repair",
103
103
  "--lock-wait",
104
+ "--backfill-budget",
104
105
  "--json",
105
106
  "--allow-home-root",
106
107
  "--max-depth",
@@ -117,6 +118,7 @@ export function parseDoctorArgs(alias, args) {
117
118
  ],
118
119
  valueFlags: [
119
120
  "--lock-wait",
121
+ "--backfill-budget",
120
122
  "--home",
121
123
  "--repo",
122
124
  "--workspace",
@@ -140,6 +142,7 @@ export function parseDoctorArgs(alias, args) {
140
142
  return {
141
143
  kind: "doctor",
142
144
  checkOnly: values.booleans.has("--check") || values.booleans.has("--no-repair"),
145
+ backfillBudgetSeconds: optionalPositiveInteger(values.flags.get("--backfill-budget"), "--backfill-budget") ?? 900,
143
146
  lockWaitSeconds: optionalPositiveInteger(values.flags.get("--lock-wait"), "--lock-wait") ?? 600,
144
147
  alias,
145
148
  homeDir: optionalNonEmpty(values.flags.get("--home")),
@@ -71,11 +71,12 @@ export function localSubcommandHelp(command) {
71
71
  [
72
72
  "do-everything",
73
73
  [
74
- "Usage: cockpit do-everything [--workspace <path>] [--check | --no-repair] [--lock-wait <seconds>] [--json]",
74
+ "Usage: cockpit do-everything [--workspace <path>] [--check | --no-repair] [--lock-wait <seconds>] [--backfill-budget <seconds>] [--json]",
75
75
  "",
76
76
  "Diagnose, repair, and verify this machine. doctor, do-everything, and fix run the same job.",
77
77
  "Repairs CLI, background sync, sign-in, roots, memory and hooks, agent rules, catch-up, uploads, and cleanup.",
78
78
  "--check or --no-repair diagnoses without repairs. --dry-run previews repairs.",
79
+ "--backfill-budget defaults to 900 seconds; catch-up repeats chunks until complete, with progress every 30 seconds.",
79
80
  "--lock-wait defaults to 600 seconds; progress prints every 30 seconds.",
80
81
  "--json includes steps, repairs, and needs_person. Exit 0 means nothing needs you.",
81
82
  "Maintainers: --update-tag next keeps self-update on the prerelease candidate.",
@@ -84,11 +85,12 @@ export function localSubcommandHelp(command) {
84
85
  [
85
86
  "fix",
86
87
  [
87
- "Usage: cockpit fix [--workspace <path>] [--check | --no-repair] [--lock-wait <seconds>] [--json]",
88
+ "Usage: cockpit fix [--workspace <path>] [--check | --no-repair] [--lock-wait <seconds>] [--backfill-budget <seconds>] [--json]",
88
89
  "",
89
90
  "Diagnose, repair, and verify this machine. doctor, do-everything, and fix run the same job.",
90
91
  "Repairs CLI, background sync, sign-in, roots, memory and hooks, agent rules, catch-up, uploads, and cleanup.",
91
92
  "--check or --no-repair diagnoses without repairs. --dry-run previews repairs.",
93
+ "--backfill-budget defaults to 900 seconds; catch-up repeats chunks until complete, with progress every 30 seconds.",
92
94
  "--lock-wait defaults to 600 seconds; progress prints every 30 seconds.",
93
95
  "--json includes steps, repairs, and needs_person. Exit 0 means nothing needs you.",
94
96
  "Maintainers: --update-tag next keeps self-update on the prerelease candidate.",
@@ -97,11 +99,12 @@ export function localSubcommandHelp(command) {
97
99
  [
98
100
  "doctor",
99
101
  [
100
- "Usage: cockpit doctor [--workspace <path>] [--check | --no-repair] [--lock-wait <seconds>] [--json]",
102
+ "Usage: cockpit doctor [--workspace <path>] [--check | --no-repair] [--lock-wait <seconds>] [--backfill-budget <seconds>] [--json]",
101
103
  "",
102
104
  "Diagnose, repair, and verify this machine. doctor, do-everything, and fix run the same job.",
103
105
  "Repairs CLI, background sync, sign-in, roots, memory and hooks, agent rules, catch-up, uploads, and cleanup.",
104
106
  "--check or --no-repair diagnoses without repairs. --dry-run previews repairs.",
107
+ "--backfill-budget defaults to 900 seconds; catch-up repeats chunks until complete, with progress every 30 seconds.",
105
108
  "--lock-wait defaults to 600 seconds; progress prints every 30 seconds.",
106
109
  "--json includes steps, repairs, and needs_person. Exit 0 means nothing needs you.",
107
110
  "Maintainers: --update-tag next keeps self-update on the prerelease candidate.",
@@ -64,7 +64,7 @@ export function localCommandHelp(command) {
64
64
  " cockpit upgrade [same flags as update]",
65
65
  " cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--update-tag <tag>] [--dry-run] [--json]",
66
66
  " cockpit fix [same flags as do-everything]",
67
- " cockpit doctor [--check | --no-repair] [--lock-wait <seconds>] [--json]",
67
+ " cockpit doctor [--check | --no-repair] [--lock-wait <seconds>] [--backfill-budget <seconds>] [--json]",
68
68
  " cockpit install [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--json]",
69
69
  " cockpit login [--pair <code>] [--no-browser] [--legacy-pair] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
70
70
  " cockpit pair [--pair <code>] [--no-browser] [--legacy-pair] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
@@ -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.101");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.102");
19
19
  return 0;
20
20
  }
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.101",
3
+ "version": "0.2.102",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {