@bli-cockpit/cli 0.2.5 → 0.2.8
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 +20 -8
- package/dist/adapters/attribution-core.js +152 -31
- package/dist/adapters/claude-attribution.js +14 -7
- package/dist/adapters/codex-attribution.js +8 -2
- package/dist/adapters/raw-evidence.js +33 -12
- package/dist/agent-rules.js +24 -10
- package/dist/autostart.js +460 -20
- package/dist/commands/backfill.js +582 -87
- package/dist/commands/doctor.js +72 -60
- package/dist/commands/local-args.js +19 -2
- package/dist/commands/local.js +418 -241
- package/dist/commands/public-root.js +6 -1
- package/dist/commands/session-sync.js +259 -54
- package/dist/cursors/backfill-cursor.js +169 -1
- package/dist/cursors/raw-evidence-cursor.js +16 -2
- package/dist/evidence-upload-client.js +139 -107
- package/dist/local-state.js +108 -6
- package/dist/onboarding-roots.js +42 -17
- package/dist/process-runner.js +103 -0
- package/dist/raw-evidence-attribution-policy.js +25 -0
- package/dist/repo-identity.js +142 -26
- package/dist/root-normalization.js +29 -16
- package/dist/spool/local-spool.js +335 -58
- package/dist/upload.js +309 -66
- package/package.json +2 -2
package/dist/commands/doctor.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { autostartStatus, installAutostartAgent } from "../autostart.js";
|
|
5
4
|
import { inspectBackfillLock } from "../backfill-lock.js";
|
|
6
|
-
import {
|
|
7
|
-
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths,
|
|
5
|
+
import { backfillCompletionCovers, readBackfillCompletionMarker, readBackfillCursor, } from "../cursors/backfill-cursor.js";
|
|
6
|
+
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, } from "../local-state.js";
|
|
8
7
|
import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
9
8
|
import { runRawEvidenceLocalGc, rawEvidenceGcSummary } from "../raw-evidence-gc.js";
|
|
10
9
|
import { runBackfillCommand } from "./backfill.js";
|
|
10
|
+
import { createInteractiveExecRunner } from "../process-runner.js";
|
|
11
11
|
const GC_MIN_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
12
12
|
export async function runDoctor(command, io, hooks, overrides = {}) {
|
|
13
13
|
const deps = { ...defaultDoctorDeps(hooks), ...overrides };
|
|
@@ -123,7 +123,10 @@ async function checkCliLatest(context) {
|
|
|
123
123
|
}
|
|
124
124
|
async function fixCliLatest(context, state) {
|
|
125
125
|
try {
|
|
126
|
-
await context.deps.selfUpdate(context.io, {
|
|
126
|
+
await context.deps.selfUpdate(context.io, {
|
|
127
|
+
json: context.command.json,
|
|
128
|
+
tag: context.command.updateTag,
|
|
129
|
+
});
|
|
127
130
|
}
|
|
128
131
|
catch (error) {
|
|
129
132
|
return fail("cli-latest", selfUpdateFailureCode(error), selfUpdateFailureMessage(error));
|
|
@@ -156,7 +159,10 @@ async function latestCliVersionFromNpm(context) {
|
|
|
156
159
|
const exec = context.io.exec;
|
|
157
160
|
if (!exec)
|
|
158
161
|
return null;
|
|
159
|
-
const
|
|
162
|
+
const packageSpec = context.command.updateTag
|
|
163
|
+
? `@bli-cockpit/cli@${context.command.updateTag}`
|
|
164
|
+
: "@bli-cockpit/cli";
|
|
165
|
+
const result = await exec("npm", ["view", packageSpec, "version", "--json"]);
|
|
160
166
|
if (result.code !== 0)
|
|
161
167
|
return null;
|
|
162
168
|
return parseNpmVersion(result.stdout);
|
|
@@ -198,11 +204,17 @@ async function readRootState(context) {
|
|
|
198
204
|
async function checkAutostartState(context) {
|
|
199
205
|
const exec = context.io.exec;
|
|
200
206
|
if (!exec) {
|
|
201
|
-
return needsFix("autostart-alive", "runner_unavailable", "
|
|
207
|
+
return needsFix("autostart-alive", "runner_unavailable", "autostart runner unavailable; would refresh autostart");
|
|
202
208
|
}
|
|
203
|
-
const
|
|
209
|
+
const roots = await doctorRoots(context);
|
|
210
|
+
const result = await autostartStatus({
|
|
211
|
+
repoRoot: roots[0],
|
|
212
|
+
repoRoots: roots,
|
|
213
|
+
dashboardUrl: context.command.dashboardUrl,
|
|
214
|
+
exec,
|
|
215
|
+
});
|
|
204
216
|
if (result.status === "loaded") {
|
|
205
|
-
return ok("autostart-alive", "already_installed", "
|
|
217
|
+
return ok("autostart-alive", "already_installed", "autostart scheduler loaded");
|
|
206
218
|
}
|
|
207
219
|
if (result.status === "unsupported") {
|
|
208
220
|
return skipped("autostart-alive", "unsupported", result.message ?? "unsupported");
|
|
@@ -212,7 +224,7 @@ async function checkAutostartState(context) {
|
|
|
212
224
|
async function fixAutostartState(context) {
|
|
213
225
|
const exec = context.io.exec;
|
|
214
226
|
if (!exec) {
|
|
215
|
-
return fail("autostart-alive", "runner_unavailable", "
|
|
227
|
+
return fail("autostart-alive", "runner_unavailable", "autostart runner unavailable");
|
|
216
228
|
}
|
|
217
229
|
const roots = await savedRoots();
|
|
218
230
|
const result = await installAutostartAgent({
|
|
@@ -225,18 +237,20 @@ async function fixAutostartState(context) {
|
|
|
225
237
|
return skipped("autostart-alive", "unsupported", result.message ?? "unsupported");
|
|
226
238
|
}
|
|
227
239
|
if (result.loaded === false) {
|
|
228
|
-
return fail("autostart-alive", "autostart_load_failed", result.message ?? "
|
|
240
|
+
return fail("autostart-alive", "autostart_load_failed", result.message ?? "operating-system scheduler load failed");
|
|
229
241
|
}
|
|
230
242
|
return ok("autostart-alive", "installed", "autostart installed and loaded");
|
|
231
243
|
}
|
|
232
|
-
async function checkBackfillState(
|
|
244
|
+
async function checkBackfillState(context) {
|
|
233
245
|
const paths = getCollectorRuntimePaths();
|
|
234
|
-
|
|
235
|
-
|
|
246
|
+
const roots = await doctorRoots(context);
|
|
247
|
+
const marker = await readBackfillCompletionMarker(paths);
|
|
248
|
+
if (backfillCompletionCovers(marker, roots, ["codex", "claude_code"])) {
|
|
249
|
+
return ok("backfill-complete", "complete", "backfill completion covers the current saved roots and both session sources");
|
|
236
250
|
}
|
|
237
251
|
const lock = await inspectBackfillLock(paths);
|
|
238
252
|
if (lock.held) {
|
|
239
|
-
return
|
|
253
|
+
return needsFix("backfill-complete", "backfill_already_running", `backfill completion is not yet proven; another run holds the lock since ${lock.held_since ?? "unknown"}`);
|
|
240
254
|
}
|
|
241
255
|
const cursor = await readBackfillCursor(paths);
|
|
242
256
|
return needsFix("backfill-complete", cursor.updated_at ? "partial" : "never_run", "backfill completion marker missing");
|
|
@@ -256,7 +270,7 @@ async function fixBackfillState(context) {
|
|
|
256
270
|
}
|
|
257
271
|
const reason = jsonField(output, "failure_reason");
|
|
258
272
|
if (reason === "backfill_already_running") {
|
|
259
|
-
return
|
|
273
|
+
return fail("backfill-complete", "backfill_already_running", "backfill lock is still held and no scope-valid completion marker exists");
|
|
260
274
|
}
|
|
261
275
|
return fail("backfill-complete", reason ?? "backfill_failed", "backfill did not complete");
|
|
262
276
|
}
|
|
@@ -283,36 +297,46 @@ async function fixGcState(context) {
|
|
|
283
297
|
return ok("gc-checked", `removed_${result.removed_dirs}`, rawEvidenceGcSummary(result));
|
|
284
298
|
}
|
|
285
299
|
async function checkSyncState(context) {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (status?.collector_freshness === "fresh") {
|
|
290
|
-
return ok("sync-fresh", "fresh", "last sync is fresh");
|
|
300
|
+
const roots = await doctorRoots(context);
|
|
301
|
+
if (roots.length === 0) {
|
|
302
|
+
return needsFix("sync-fresh", "no_roots", "sync has no saved workspace roots");
|
|
291
303
|
}
|
|
292
|
-
|
|
304
|
+
// Upload state is currently device-global, so it cannot prove that every
|
|
305
|
+
// saved root succeeded. Doctor therefore performs one explicit sync per root
|
|
306
|
+
// and only turns green from those command receipts.
|
|
307
|
+
return needsFix("sync-fresh", "per_root_verification_required", `fresh upload proof is required for ${roots.length} saved root${roots.length === 1 ? "" : "s"}`);
|
|
293
308
|
}
|
|
294
309
|
async function fixSyncState(context) {
|
|
295
310
|
const exec = context.io.exec;
|
|
296
311
|
if (!exec)
|
|
297
312
|
return fail("sync-fresh", "runner_unavailable", "sync runner unavailable");
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
313
|
+
const roots = await doctorRoots(context);
|
|
314
|
+
if (roots.length === 0) {
|
|
315
|
+
return fail("sync-fresh", "no_roots", "sync has no saved workspace roots");
|
|
316
|
+
}
|
|
317
|
+
for (const repoRoot of roots) {
|
|
318
|
+
const args = ["sync", "--json", "--workspace", repoRoot];
|
|
319
|
+
if (context.command.dashboardUrl !== DEFAULT_DASHBOARD_URL) {
|
|
320
|
+
args.push("--dashboard-url", context.command.dashboardUrl);
|
|
321
|
+
}
|
|
322
|
+
const result = await exec("cockpit", args);
|
|
323
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
324
|
+
const status = jsonField(output, "status");
|
|
325
|
+
if (result.code !== 0) {
|
|
326
|
+
return fail("sync-fresh", status ?? "sync_failed", `sync failed for ${repoRoot}`);
|
|
327
|
+
}
|
|
328
|
+
if (status !== "uploaded") {
|
|
329
|
+
return fail("sync-fresh", status ?? "sync_unverified", `sync did not return an uploaded receipt for ${repoRoot}`);
|
|
330
|
+
}
|
|
314
331
|
}
|
|
315
|
-
return
|
|
332
|
+
return ok("sync-fresh", "synced", roots.length === 1
|
|
333
|
+
? "ran `cockpit sync` and received an uploaded receipt"
|
|
334
|
+
: `received uploaded receipts for ${roots.length} saved roots`);
|
|
335
|
+
}
|
|
336
|
+
async function doctorRoots(context) {
|
|
337
|
+
if (context.command.repoRoot)
|
|
338
|
+
return [context.command.repoRoot];
|
|
339
|
+
return savedRoots();
|
|
316
340
|
}
|
|
317
341
|
async function maybeReportDoctorEvents(context, rows) {
|
|
318
342
|
if (context.command.dryRun)
|
|
@@ -398,16 +422,6 @@ async function savedRoots() {
|
|
|
398
422
|
const config = await readLocalCollectorConfig(getCollectorRuntimePaths()).catch(() => null);
|
|
399
423
|
return normalizeCollectionRoots(config?.default_repo_paths ?? []);
|
|
400
424
|
}
|
|
401
|
-
async function hasBackfillCompletionMarker(paths) {
|
|
402
|
-
try {
|
|
403
|
-
const raw = JSON.parse(await fs.readFile(backfillCompletionMarkerPath(paths), "utf8"));
|
|
404
|
-
return (raw.schema_version === "cockpit-backfill-complete.v1" &&
|
|
405
|
-
typeof raw.completed_at === "string");
|
|
406
|
-
}
|
|
407
|
-
catch {
|
|
408
|
-
return false;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
425
|
function parseNpmVersion(stdout) {
|
|
412
426
|
const trimmed = stdout.trim();
|
|
413
427
|
if (!trimmed)
|
|
@@ -427,20 +441,18 @@ function reexecDoctor(command, io) {
|
|
|
427
441
|
if (command.dashboardUrl !== DEFAULT_DASHBOARD_URL) {
|
|
428
442
|
args.push("--dashboard-url", command.dashboardUrl);
|
|
429
443
|
}
|
|
444
|
+
if (command.updateTag)
|
|
445
|
+
args.push("--update-tag", command.updateTag);
|
|
430
446
|
if (command.json)
|
|
431
447
|
args.push("--json");
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
env
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
});
|
|
441
|
-
child.on("error", () => resolve(1));
|
|
442
|
-
child.on("close", (code) => resolve(code ?? 1));
|
|
443
|
-
});
|
|
448
|
+
const exec = io.interactiveExec ?? createInteractiveExecRunner();
|
|
449
|
+
return exec("cockpit", args, {
|
|
450
|
+
env: {
|
|
451
|
+
...process.env,
|
|
452
|
+
...io.env,
|
|
453
|
+
COCKPIT_DOCTOR_REEXEC: "1",
|
|
454
|
+
},
|
|
455
|
+
}).then((result) => result.code);
|
|
444
456
|
}
|
|
445
457
|
function capturedIo(io, forward) {
|
|
446
458
|
const stdoutChunks = [];
|
|
@@ -115,15 +115,26 @@ function parseUpdateArgs(alias, args) {
|
|
|
115
115
|
}
|
|
116
116
|
function parseDoctorArgs(alias, args) {
|
|
117
117
|
const values = parseNamedArgs(args, {
|
|
118
|
-
allowedFlags: [
|
|
119
|
-
|
|
118
|
+
allowedFlags: [
|
|
119
|
+
"--workspace",
|
|
120
|
+
"--dashboard-url",
|
|
121
|
+
"--update-tag",
|
|
122
|
+
"--dry-run",
|
|
123
|
+
"--json",
|
|
124
|
+
],
|
|
125
|
+
valueFlags: ["--workspace", "--dashboard-url", "--update-tag"],
|
|
120
126
|
});
|
|
121
127
|
assertNoPositionals(values.positionals, alias);
|
|
128
|
+
const updateTag = optionalNonEmpty(values.flags.get("--update-tag"));
|
|
129
|
+
if (updateTag && !/^[a-z0-9][a-z0-9._-]*$/u.test(updateTag)) {
|
|
130
|
+
throw new Error("--update-tag must be a lowercase npm dist-tag such as 'next' or 'latest'.");
|
|
131
|
+
}
|
|
122
132
|
return {
|
|
123
133
|
kind: "doctor",
|
|
124
134
|
alias,
|
|
125
135
|
repoRoot: optionalNonEmpty(values.flags.get("--workspace")),
|
|
126
136
|
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
137
|
+
updateTag,
|
|
127
138
|
dryRun: values.booleans.has("--dry-run"),
|
|
128
139
|
json: values.booleans.has("--json"),
|
|
129
140
|
};
|
|
@@ -351,6 +362,8 @@ function parseBackfillArgs(args) {
|
|
|
351
362
|
"--source",
|
|
352
363
|
"--dry-run",
|
|
353
364
|
"--max-files",
|
|
365
|
+
"--max-depth",
|
|
366
|
+
"--max-repos",
|
|
354
367
|
"--yes",
|
|
355
368
|
"--json",
|
|
356
369
|
],
|
|
@@ -361,6 +374,8 @@ function parseBackfillArgs(args) {
|
|
|
361
374
|
"--since-days",
|
|
362
375
|
"--source",
|
|
363
376
|
"--max-files",
|
|
377
|
+
"--max-depth",
|
|
378
|
+
"--max-repos",
|
|
364
379
|
],
|
|
365
380
|
});
|
|
366
381
|
assertNoPositionals(values.positionals, "backfill");
|
|
@@ -382,6 +397,8 @@ function parseBackfillArgs(args) {
|
|
|
382
397
|
all,
|
|
383
398
|
dryRun: values.booleans.has("--dry-run"),
|
|
384
399
|
maxFiles: optionalPositiveInteger(values.flags.get("--max-files"), "--max-files"),
|
|
400
|
+
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
401
|
+
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
385
402
|
yes: values.booleans.has("--yes"),
|
|
386
403
|
json: values.booleans.has("--json"),
|
|
387
404
|
};
|