@bli-cockpit/cli 0.2.23 → 0.2.25
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.
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { autostartStatus, installAutostartAgent, } from "./autostart.js";
|
|
4
|
+
export const AUTOSTART_REPAIR_THROTTLE_MARKER = ".last-autostart-repair";
|
|
5
|
+
const AUTOSTART_REPAIR_MIN_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
const DETAIL_MAX_CHARS = 300;
|
|
7
|
+
export async function runAutostartSelfHeal(paths, options) {
|
|
8
|
+
const platform = options.platform ?? process.platform;
|
|
9
|
+
if (platform !== "win32")
|
|
10
|
+
return null;
|
|
11
|
+
if (options.repoRoots.length === 0)
|
|
12
|
+
return null;
|
|
13
|
+
const status = await autostartStatus({
|
|
14
|
+
homeDir: options.homeDir,
|
|
15
|
+
repoRoot: options.repoRoots[0],
|
|
16
|
+
repoRoots: options.repoRoots,
|
|
17
|
+
dashboardUrl: options.dashboardUrl,
|
|
18
|
+
exec: options.exec,
|
|
19
|
+
platform,
|
|
20
|
+
});
|
|
21
|
+
// Healthy is the steady state and stays silent; absent means the operator
|
|
22
|
+
// (or onboarding) owns the decision, not this tick.
|
|
23
|
+
if (status.status !== "not_loaded")
|
|
24
|
+
return null;
|
|
25
|
+
// Attempts are throttled like the self-update's (marker mtime, written for
|
|
26
|
+
// the attempt not the outcome) so a persistently failing repair cannot spawn
|
|
27
|
+
// a registration every 15 minutes. The status probe above still runs every
|
|
28
|
+
// tick — it is one schtasks query.
|
|
29
|
+
const now = options.now ?? new Date();
|
|
30
|
+
const marker = path.join(paths.state_dir, AUTOSTART_REPAIR_THROTTLE_MARKER);
|
|
31
|
+
const lastAttempt = await fs.stat(marker).catch(() => null);
|
|
32
|
+
if (lastAttempt &&
|
|
33
|
+
now.getTime() - lastAttempt.mtimeMs < AUTOSTART_REPAIR_MIN_INTERVAL_MS) {
|
|
34
|
+
return { status: "skipped", reason: "repair_throttled_recent_attempt" };
|
|
35
|
+
}
|
|
36
|
+
await fs.mkdir(paths.state_dir, { recursive: true }).catch(() => undefined);
|
|
37
|
+
await fs.writeFile(marker, now.toISOString()).catch(() => undefined);
|
|
38
|
+
const problem = (status.message ?? "task not loaded").slice(0, DETAIL_MAX_CHARS);
|
|
39
|
+
const repaired = await installAutostartAgent({
|
|
40
|
+
homeDir: options.homeDir,
|
|
41
|
+
repoRoot: options.repoRoots[0],
|
|
42
|
+
repoRoots: options.repoRoots,
|
|
43
|
+
dashboardUrl: options.dashboardUrl,
|
|
44
|
+
exec: options.exec,
|
|
45
|
+
platform,
|
|
46
|
+
});
|
|
47
|
+
if (repaired.loaded) {
|
|
48
|
+
return { status: "ok", reason: "autostart_repaired", detail: problem };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
status: "fail",
|
|
52
|
+
reason: "autostart_repair_failed",
|
|
53
|
+
detail: (repaired.message ?? problem).slice(0, DETAIL_MAX_CHARS),
|
|
54
|
+
};
|
|
55
|
+
}
|
package/dist/commands/local.js
CHANGED
|
@@ -22,6 +22,7 @@ import { runAttributedWorktreeSync, matchesLiveSyncWorktree, } from "./session-s
|
|
|
22
22
|
import { COLLECTION_ROOT_REQUIRED, missingCollectionRootMessage, normalizeRootsDetailed, resolveOnboardingRoots, rootRejectionExplanation, } from "../onboarding-roots.js";
|
|
23
23
|
import { rawEvidenceGcSummary, runRawEvidenceLocalGc, } from "../raw-evidence-gc.js";
|
|
24
24
|
import { envWithNodeRuntimeOnPath, runScheduledSelfUpdate, } from "../scheduled-self-update.js";
|
|
25
|
+
import { runAutostartSelfHeal, } from "../autostart-self-heal.js";
|
|
25
26
|
import { enqueueInstallEventEntry, readPendingInstallEventEntries, recordInstallEventAttemptFailure, removeInstallEventEntry, } from "../spool/install-event-outbox.js";
|
|
26
27
|
import { createCapturedExecRunner, createInteractiveExecRunner, } from "../process-runner.js";
|
|
27
28
|
import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
@@ -2172,6 +2173,7 @@ async function runSync(command, io) {
|
|
|
2172
2173
|
// BLI-2601: self-update runs only after collection's own outcome above is
|
|
2173
2174
|
// already decided and reported, win or lose. See the function doc.
|
|
2174
2175
|
await runScheduledSelfUpdateAfterSync(command, io, dashboardUrl, minCliVersion ?? minCliVersionAtStart);
|
|
2176
|
+
await runAutostartSelfHealAfterSync(command, io, dashboardUrl);
|
|
2175
2177
|
return result.exitCode;
|
|
2176
2178
|
}
|
|
2177
2179
|
catch (error) {
|
|
@@ -2191,9 +2193,59 @@ async function runSync(command, io) {
|
|
|
2191
2193
|
io,
|
|
2192
2194
|
});
|
|
2193
2195
|
await runScheduledSelfUpdateAfterSync(command, io, dashboardUrl, minCliVersion ?? minCliVersionAtStart);
|
|
2196
|
+
await runAutostartSelfHealAfterSync(command, io, dashboardUrl);
|
|
2194
2197
|
throw error;
|
|
2195
2198
|
}
|
|
2196
2199
|
}
|
|
2200
|
+
/**
|
|
2201
|
+
* BLI-2721: after the tick's collection and self-update are done and
|
|
2202
|
+
* reported, repair a broken/legacy autostart registration in place (Windows
|
|
2203
|
+
* only — see autostart-self-heal.ts for why macOS is excluded). Every error
|
|
2204
|
+
* path is swallowed like the self-update's: heal outcomes are their own
|
|
2205
|
+
* receipts, never a sync failure.
|
|
2206
|
+
*/
|
|
2207
|
+
async function runAutostartSelfHealAfterSync(command, io, dashboardUrl) {
|
|
2208
|
+
let result;
|
|
2209
|
+
try {
|
|
2210
|
+
const rawExec = io.exec;
|
|
2211
|
+
if (!rawExec)
|
|
2212
|
+
return;
|
|
2213
|
+
const spawnEnv = envWithNodeRuntimeOnPath(io.env ?? process.env);
|
|
2214
|
+
const exec = (cmd, args, options) => rawExec(cmd, args, { ...options, env: options?.env ?? spawnEnv });
|
|
2215
|
+
result = await runAutostartSelfHeal(getCollectorRuntimePaths(command.homeDir), {
|
|
2216
|
+
homeDir: command.homeDir,
|
|
2217
|
+
repoRoots: await resolveAutostartRoots(command.homeDir, undefined),
|
|
2218
|
+
dashboardUrl: command.dashboardUrl,
|
|
2219
|
+
exec,
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
catch (error) {
|
|
2223
|
+
result = {
|
|
2224
|
+
status: "fail",
|
|
2225
|
+
reason: "autostart_self_heal_threw",
|
|
2226
|
+
detail: redactedSyncErrorDetail(error),
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
// Steady state (healthy, absent, non-Windows, no roots) and the daily
|
|
2230
|
+
// throttle are silent; an actual repair attempt reports either way.
|
|
2231
|
+
if (!result || result.reason === "repair_throttled_recent_attempt")
|
|
2232
|
+
return;
|
|
2233
|
+
await reportInstallEventsBestEffort({
|
|
2234
|
+
homeDir: command.homeDir,
|
|
2235
|
+
dashboardUrl,
|
|
2236
|
+
command: "sync",
|
|
2237
|
+
events: [
|
|
2238
|
+
{
|
|
2239
|
+
step: "autostart_repair",
|
|
2240
|
+
status: result.status,
|
|
2241
|
+
...(result.status === "ok" ? {} : { error_code: result.reason }),
|
|
2242
|
+
...(result.detail ? { error_detail: result.detail } : {}),
|
|
2243
|
+
},
|
|
2244
|
+
],
|
|
2245
|
+
json: command.json,
|
|
2246
|
+
io,
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2197
2249
|
/**
|
|
2198
2250
|
* BLI-2601: the fleet keeps itself current on npm `latest` without anyone
|
|
2199
2251
|
* re-running `npm i -g @bli-cockpit/cli` by hand after day 0. This always
|
|
@@ -2478,6 +2530,31 @@ async function runSyncLocked(command, io) {
|
|
|
2478
2530
|
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
2479
2531
|
return syncResult(run);
|
|
2480
2532
|
}
|
|
2533
|
+
// Zero worktrees is a legitimate steady state, not a failure: an approved
|
|
2534
|
+
// root can hold no git repos, and sessions upload independently of
|
|
2535
|
+
// worktrees (session-first, BLI-2581). This used to throw "Sync produced no
|
|
2536
|
+
// result", which painted ~90 false-red sync_failed receipts per day on one
|
|
2537
|
+
// fleet machine with a single empty root and taught people to ignore
|
|
2538
|
+
// sync_failed (BLI-2722). A genuinely broken run still fails via run.ok.
|
|
2539
|
+
if (run.outcomes.length === 0) {
|
|
2540
|
+
const runStatus = attributedSyncRunStatus(run);
|
|
2541
|
+
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
2542
|
+
if (command.json) {
|
|
2543
|
+
writeLine(io.stdout, JSON.stringify({
|
|
2544
|
+
mode: "no_worktrees",
|
|
2545
|
+
status: runStatus,
|
|
2546
|
+
collection_complete: run.ok,
|
|
2547
|
+
codex_sessions: run.summary,
|
|
2548
|
+
raw_evidence_gc: gc,
|
|
2549
|
+
}, null, 2));
|
|
2550
|
+
return syncResult(run);
|
|
2551
|
+
}
|
|
2552
|
+
writeLine(run.ok ? io.stdout : io.stderr, `Cockpit sync ${runStatus}: no git worktrees under this root; session scan ran.`);
|
|
2553
|
+
writeAgentSessionSummary(io, run.summary);
|
|
2554
|
+
if (gc && !gc.skipped)
|
|
2555
|
+
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
2556
|
+
return syncResult(run);
|
|
2557
|
+
}
|
|
2481
2558
|
const result = run.outcomes[0]?.sync;
|
|
2482
2559
|
if (!result) {
|
|
2483
2560
|
throw new Error("Sync produced no result for the repo worktree.");
|
|
@@ -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.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.25");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|