@bli-cockpit/cli 0.2.98 → 0.2.100
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/dist/cli.js +13 -0
- package/dist/commands/backfill-checkpoint.js +3 -1
- package/dist/commands/backfill-issues.js +8 -55
- package/dist/commands/backfill-report.js +22 -6
- package/dist/commands/backfill-scan.js +2 -1
- package/dist/commands/backfill-skip-policy.js +134 -0
- package/dist/commands/careers.js +16 -0
- package/dist/commands/doctor-pipeline-verdicts.js +238 -0
- package/dist/commands/doctor-pipeline.js +37 -107
- package/dist/commands/doctor.js +8 -4
- package/dist/commands/local-args-tower-admin.js +17 -6
- package/dist/commands/local-args-tower-careers.js +20 -0
- package/dist/commands/local-args-tower-pages.js +23 -3
- package/dist/commands/local-args-tower-usage.js +8 -0
- package/dist/commands/local-args-tower.js +3 -1
- package/dist/commands/local-args.js +5 -1
- package/dist/commands/local-help-commands-tower.js +32 -5
- package/dist/commands/local-help-commands.js +11 -2
- package/dist/commands/local-help.js +8 -3
- package/dist/commands/local.js +13 -0
- package/dist/commands/memory-hook-counts.js +29 -8
- package/dist/commands/notes-file.js +8 -1
- package/dist/commands/notes-folders.js +35 -0
- package/dist/commands/notes-writes.js +74 -4
- package/dist/commands/notes.js +11 -3
- package/dist/commands/ops-render.js +5 -1
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/usage.js +23 -0
- package/dist/crash-guard.js +167 -0
- package/dist/cursors/backfill-completion-marker.js +135 -0
- package/dist/cursors/backfill-cursor.js +18 -99
- package/dist/process-runner.js +39 -1
- package/dist/sync-lock.js +10 -1
- package/package.json +5 -5
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The all-history completion marker: the one document that says an `--all`
|
|
3
|
+
* backfill finished, and the only proof `cockpit doctor` accepts for its
|
|
4
|
+
* `backfill-complete` row.
|
|
5
|
+
*
|
|
6
|
+
* It lives beside the cursor and not inside it because it answers a different
|
|
7
|
+
* question. The cursor is where the sweep GOT TO and is rewritten constantly;
|
|
8
|
+
* the marker is a claim about a finished scope, with its own schema version,
|
|
9
|
+
* its own 24-hour revalidation, and its own rule about which leftovers are
|
|
10
|
+
* allowed to ride along on a run that still counts as complete (a file over
|
|
11
|
+
* the upload cap, BLI-2727; helper transcripts over the per-session sidecar
|
|
12
|
+
* cap, BLI-4303). Both of those are recorded here, never silently dropped.
|
|
13
|
+
*
|
|
14
|
+
* Every name is re-exported from `./backfill-cursor.js`, the address its
|
|
15
|
+
* callers already know.
|
|
16
|
+
*/
|
|
17
|
+
import fs from "node:fs/promises";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { describeError, isMissingFileFailure } from "../health-detail.js";
|
|
20
|
+
import { backfillCollectionScopeId, parseBackfillCursor, writePrivateBackfillJson, } from "./backfill-cursor.js";
|
|
21
|
+
export const BACKFILL_COMPLETION_MARKER_FILENAME = "backfill-complete.json";
|
|
22
|
+
export const BACKFILL_COVERAGE_VERSION = "redacted-session-backfill.v3";
|
|
23
|
+
export const BACKFILL_COMPLETION_RECHECK_MS = 24 * 60 * 60 * 1_000;
|
|
24
|
+
export async function writeBackfillCompletionMarker(paths, marker) {
|
|
25
|
+
await writePrivateBackfillJson(backfillCompletionMarkerPath(paths), marker);
|
|
26
|
+
}
|
|
27
|
+
export async function readBackfillCompletionMarker(paths) {
|
|
28
|
+
try {
|
|
29
|
+
const raw = JSON.parse(await fs.readFile(backfillCompletionMarkerPath(paths), "utf8"));
|
|
30
|
+
return parseBackfillCompletionMarker(raw);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// No marker means backfill has not finished, which is the ordinary state.
|
|
34
|
+
// A marker that cannot be read means a finished backfill will be re-run,
|
|
35
|
+
// and the machine should say so rather than quietly redo a day of work.
|
|
36
|
+
if (!isMissingFileFailure(error)) {
|
|
37
|
+
console.error("[backfill-cursor] completion marker unreadable, treating backfill as unfinished", JSON.stringify({
|
|
38
|
+
reason: "backfill_marker_unreadable",
|
|
39
|
+
marker_file: BACKFILL_COMPLETION_MARKER_FILENAME,
|
|
40
|
+
...describeError(error),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function backfillCompletionCovers(marker, collectionRoots, requiredSources, now = new Date()) {
|
|
47
|
+
if (!marker)
|
|
48
|
+
return false;
|
|
49
|
+
if (marker.coverage_version !== BACKFILL_COVERAGE_VERSION)
|
|
50
|
+
return false;
|
|
51
|
+
if (marker.collection_scope_id !== backfillCollectionScopeId(collectionRoots)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const revalidateAfter = Date.parse(marker.revalidate_after);
|
|
55
|
+
if (!Number.isFinite(revalidateAfter) || now.getTime() >= revalidateAfter) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const completedSources = new Set(marker.sources);
|
|
59
|
+
return requiredSources.every((source) => completedSources.has(source));
|
|
60
|
+
}
|
|
61
|
+
export function backfillCompletionMarkerPath(paths) {
|
|
62
|
+
return path.join(paths.cursors_dir, BACKFILL_COMPLETION_MARKER_FILENAME);
|
|
63
|
+
}
|
|
64
|
+
function parseBackfillCompletionMarker(value) {
|
|
65
|
+
if (!value || typeof value !== "object")
|
|
66
|
+
return null;
|
|
67
|
+
const record = value;
|
|
68
|
+
if (record["schema_version"] !== "cockpit-backfill-complete.v2" ||
|
|
69
|
+
record["coverage_version"] !== BACKFILL_COVERAGE_VERSION) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const collectionScopeId = optionalString(record["collection_scope_id"]);
|
|
73
|
+
const completedAt = optionalString(record["completed_at"]);
|
|
74
|
+
const revalidateAfter = optionalString(record["revalidate_after"]);
|
|
75
|
+
const rawSources = record["sources"];
|
|
76
|
+
if (!collectionScopeId ||
|
|
77
|
+
!completedAt ||
|
|
78
|
+
!revalidateAfter ||
|
|
79
|
+
!Number.isFinite(Date.parse(revalidateAfter)) ||
|
|
80
|
+
!Array.isArray(rawSources)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const sources = [
|
|
84
|
+
...new Set(rawSources.filter((source) => source === "codex" || source === "claude_code")),
|
|
85
|
+
];
|
|
86
|
+
if (sources.length === 0)
|
|
87
|
+
return null;
|
|
88
|
+
return {
|
|
89
|
+
schema_version: "cockpit-backfill-complete.v2",
|
|
90
|
+
coverage_version: BACKFILL_COVERAGE_VERSION,
|
|
91
|
+
collection_scope_id: collectionScopeId,
|
|
92
|
+
sources,
|
|
93
|
+
completed_at: completedAt,
|
|
94
|
+
revalidate_after: revalidateAfter,
|
|
95
|
+
cursor: parseBackfillCursor(record["cursor"]),
|
|
96
|
+
...(parseOversizedSkips(record["oversized_skips"])
|
|
97
|
+
? { oversized_skips: parseOversizedSkips(record["oversized_skips"]) }
|
|
98
|
+
: {}),
|
|
99
|
+
...(parseSidecarCapSkips(record["sidecar_cap_skips"])
|
|
100
|
+
? { sidecar_cap_skips: parseSidecarCapSkips(record["sidecar_cap_skips"]) }
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function parseSidecarCapSkips(value) {
|
|
105
|
+
if (!value || typeof value !== "object")
|
|
106
|
+
return null;
|
|
107
|
+
const record = value;
|
|
108
|
+
if (record["reason"] !== "claude_sidecar_limit_applied")
|
|
109
|
+
return null;
|
|
110
|
+
const count = optionalNumber(record["count"]);
|
|
111
|
+
if (count === null || count <= 0)
|
|
112
|
+
return null;
|
|
113
|
+
return { reason: "claude_sidecar_limit_applied", count };
|
|
114
|
+
}
|
|
115
|
+
function parseOversizedSkips(value) {
|
|
116
|
+
if (!value || typeof value !== "object")
|
|
117
|
+
return null;
|
|
118
|
+
const record = value;
|
|
119
|
+
if (record["reason"] !== "file_too_large")
|
|
120
|
+
return null;
|
|
121
|
+
const count = optionalNumber(record["count"]);
|
|
122
|
+
if (count === null || count <= 0)
|
|
123
|
+
return null;
|
|
124
|
+
const rawByteSizes = record["byte_sizes"];
|
|
125
|
+
const byteSizes = Array.isArray(rawByteSizes)
|
|
126
|
+
? rawByteSizes.filter((entry) => typeof entry === "number" && Number.isFinite(entry) && entry >= 0)
|
|
127
|
+
: [];
|
|
128
|
+
return { reason: "file_too_large", count, byte_sizes: byteSizes };
|
|
129
|
+
}
|
|
130
|
+
function optionalString(value) {
|
|
131
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
132
|
+
}
|
|
133
|
+
function optionalNumber(value) {
|
|
134
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
135
|
+
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the historical sweep got to, per source: the backfill cursor.
|
|
3
|
+
*
|
|
4
|
+
* Two things live in this folder and they are deliberately separate. This one
|
|
5
|
+
* is a moving position — a newest/oldest watermark, the identities sitting
|
|
6
|
+
* exactly on each boundary, and the census of what was seen — rewritten by
|
|
7
|
+
* every run. Its sibling `backfill-completion-marker.ts` is the claim that a
|
|
8
|
+
* whole scope is FINISHED, with its own schema and its own rules; every name
|
|
9
|
+
* it owns is re-exported at the bottom of this file, so callers keep one
|
|
10
|
+
* address.
|
|
11
|
+
*/
|
|
1
12
|
import crypto from "node:crypto";
|
|
2
13
|
import fs from "node:fs/promises";
|
|
3
14
|
import path from "node:path";
|
|
4
15
|
import { describeError, isMissingFileFailure } from "../health-detail.js";
|
|
5
16
|
export const BACKFILL_CURSOR_FILENAME = "backfill.json";
|
|
6
|
-
export const BACKFILL_COMPLETION_MARKER_FILENAME = "backfill-complete.json";
|
|
7
|
-
export const BACKFILL_COVERAGE_VERSION = "redacted-session-backfill.v3";
|
|
8
|
-
export const BACKFILL_COMPLETION_RECHECK_MS = 24 * 60 * 60 * 1_000;
|
|
9
17
|
export function emptyBackfillCursorState() {
|
|
10
18
|
return {
|
|
11
19
|
schema_version: "cockpit-backfill-cursor.v1",
|
|
@@ -37,29 +45,7 @@ export async function readBackfillCursor(paths) {
|
|
|
37
45
|
}
|
|
38
46
|
export async function writeBackfillCursor(paths, state) {
|
|
39
47
|
const filePath = backfillCursorPath(paths);
|
|
40
|
-
await
|
|
41
|
-
}
|
|
42
|
-
export async function writeBackfillCompletionMarker(paths, marker) {
|
|
43
|
-
await writePrivateJson(backfillCompletionMarkerPath(paths), marker);
|
|
44
|
-
}
|
|
45
|
-
export async function readBackfillCompletionMarker(paths) {
|
|
46
|
-
try {
|
|
47
|
-
const raw = JSON.parse(await fs.readFile(backfillCompletionMarkerPath(paths), "utf8"));
|
|
48
|
-
return parseBackfillCompletionMarker(raw);
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
// No marker means backfill has not finished, which is the ordinary state.
|
|
52
|
-
// A marker that cannot be read means a finished backfill will be re-run,
|
|
53
|
-
// and the machine should say so rather than quietly redo a day of work.
|
|
54
|
-
if (!isMissingFileFailure(error)) {
|
|
55
|
-
console.error("[backfill-cursor] completion marker unreadable, treating backfill as unfinished", JSON.stringify({
|
|
56
|
-
reason: "backfill_marker_unreadable",
|
|
57
|
-
marker_file: BACKFILL_COMPLETION_MARKER_FILENAME,
|
|
58
|
-
...describeError(error),
|
|
59
|
-
}));
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
48
|
+
await writePrivateBackfillJson(filePath, state);
|
|
63
49
|
}
|
|
64
50
|
/**
|
|
65
51
|
* The cursor is only reusable inside the exact approved-root scope that
|
|
@@ -83,21 +69,6 @@ export function prepareBackfillCursorForScope(cursor, collectionRoots, sources)
|
|
|
83
69
|
reset,
|
|
84
70
|
};
|
|
85
71
|
}
|
|
86
|
-
export function backfillCompletionCovers(marker, collectionRoots, requiredSources, now = new Date()) {
|
|
87
|
-
if (!marker)
|
|
88
|
-
return false;
|
|
89
|
-
if (marker.coverage_version !== BACKFILL_COVERAGE_VERSION)
|
|
90
|
-
return false;
|
|
91
|
-
if (marker.collection_scope_id !== backfillCollectionScopeId(collectionRoots)) {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
const revalidateAfter = Date.parse(marker.revalidate_after);
|
|
95
|
-
if (!Number.isFinite(revalidateAfter) || now.getTime() >= revalidateAfter) {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
const completedSources = new Set(marker.sources);
|
|
99
|
-
return requiredSources.every((source) => completedSources.has(source));
|
|
100
|
-
}
|
|
101
72
|
export function backfillCollectionScopeId(collectionRoots) {
|
|
102
73
|
const normalized = [...new Set(collectionRoots.map(normalizeScopeRoot))].sort();
|
|
103
74
|
return `scope-${crypto
|
|
@@ -178,9 +149,6 @@ export function recordBackfillScanCoverage(cursor, sources, coveredThrough, now)
|
|
|
178
149
|
export function backfillCursorPath(paths) {
|
|
179
150
|
return path.join(paths.cursors_dir, BACKFILL_CURSOR_FILENAME);
|
|
180
151
|
}
|
|
181
|
-
export function backfillCompletionMarkerPath(paths) {
|
|
182
|
-
return path.join(paths.cursors_dir, BACKFILL_COMPLETION_MARKER_FILENAME);
|
|
183
|
-
}
|
|
184
152
|
function emptySourceCursor(collectionScopeId = null) {
|
|
185
153
|
return {
|
|
186
154
|
collection_scope_id: collectionScopeId,
|
|
@@ -194,7 +162,7 @@ function emptySourceCursor(collectionScopeId = null) {
|
|
|
194
162
|
reason_counts: {},
|
|
195
163
|
};
|
|
196
164
|
}
|
|
197
|
-
function parseBackfillCursor(value) {
|
|
165
|
+
export function parseBackfillCursor(value) {
|
|
198
166
|
if (!value || typeof value !== "object")
|
|
199
167
|
return emptyBackfillCursorState();
|
|
200
168
|
const record = value;
|
|
@@ -228,58 +196,6 @@ function parseSourceCursor(value) {
|
|
|
228
196
|
reason_counts: parseNumberRecord(record["reason_counts"]),
|
|
229
197
|
};
|
|
230
198
|
}
|
|
231
|
-
function parseBackfillCompletionMarker(value) {
|
|
232
|
-
if (!value || typeof value !== "object")
|
|
233
|
-
return null;
|
|
234
|
-
const record = value;
|
|
235
|
-
if (record["schema_version"] !== "cockpit-backfill-complete.v2" ||
|
|
236
|
-
record["coverage_version"] !== BACKFILL_COVERAGE_VERSION) {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
const collectionScopeId = optionalString(record["collection_scope_id"]);
|
|
240
|
-
const completedAt = optionalString(record["completed_at"]);
|
|
241
|
-
const revalidateAfter = optionalString(record["revalidate_after"]);
|
|
242
|
-
const rawSources = record["sources"];
|
|
243
|
-
if (!collectionScopeId ||
|
|
244
|
-
!completedAt ||
|
|
245
|
-
!revalidateAfter ||
|
|
246
|
-
!Number.isFinite(Date.parse(revalidateAfter)) ||
|
|
247
|
-
!Array.isArray(rawSources)) {
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
const sources = [
|
|
251
|
-
...new Set(rawSources.filter((source) => source === "codex" || source === "claude_code")),
|
|
252
|
-
];
|
|
253
|
-
if (sources.length === 0)
|
|
254
|
-
return null;
|
|
255
|
-
return {
|
|
256
|
-
schema_version: "cockpit-backfill-complete.v2",
|
|
257
|
-
coverage_version: BACKFILL_COVERAGE_VERSION,
|
|
258
|
-
collection_scope_id: collectionScopeId,
|
|
259
|
-
sources,
|
|
260
|
-
completed_at: completedAt,
|
|
261
|
-
revalidate_after: revalidateAfter,
|
|
262
|
-
cursor: parseBackfillCursor(record["cursor"]),
|
|
263
|
-
...(parseOversizedSkips(record["oversized_skips"])
|
|
264
|
-
? { oversized_skips: parseOversizedSkips(record["oversized_skips"]) }
|
|
265
|
-
: {}),
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
function parseOversizedSkips(value) {
|
|
269
|
-
if (!value || typeof value !== "object")
|
|
270
|
-
return null;
|
|
271
|
-
const record = value;
|
|
272
|
-
if (record["reason"] !== "file_too_large")
|
|
273
|
-
return null;
|
|
274
|
-
const count = optionalNumber(record["count"]);
|
|
275
|
-
if (count === null || count <= 0)
|
|
276
|
-
return null;
|
|
277
|
-
const rawByteSizes = record["byte_sizes"];
|
|
278
|
-
const byteSizes = Array.isArray(rawByteSizes)
|
|
279
|
-
? rawByteSizes.filter((entry) => typeof entry === "number" && Number.isFinite(entry) && entry >= 0)
|
|
280
|
-
: [];
|
|
281
|
-
return { reason: "file_too_large", count, byte_sizes: byteSizes };
|
|
282
|
-
}
|
|
283
199
|
function normalizeScopeRoot(value) {
|
|
284
200
|
const windowsStyle = path.win32.isAbsolute(value) && !path.posix.isAbsolute(value);
|
|
285
201
|
if (windowsStyle)
|
|
@@ -305,7 +221,7 @@ function parseStringArray(value) {
|
|
|
305
221
|
...new Set(value.filter((entry) => typeof entry === "string" && entry.trim().length > 0)),
|
|
306
222
|
].sort();
|
|
307
223
|
}
|
|
308
|
-
async function
|
|
224
|
+
export async function writePrivateBackfillJson(filePath, value) {
|
|
309
225
|
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
310
226
|
const tempPath = `${filePath}.tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
311
227
|
const serialized = `${JSON.stringify(value, null, 2)}\n`;
|
|
@@ -334,4 +250,7 @@ function optionalString(value) {
|
|
|
334
250
|
}
|
|
335
251
|
function optionalNumber(value) {
|
|
336
252
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
337
|
-
}
|
|
253
|
+
}
|
|
254
|
+
// Re-exported so every caller keeps importing the completion marker from
|
|
255
|
+
// `./backfill-cursor.js`, the address it has always had.
|
|
256
|
+
export { BACKFILL_COMPLETION_MARKER_FILENAME, BACKFILL_COMPLETION_RECHECK_MS, BACKFILL_COVERAGE_VERSION, backfillCompletionCovers, backfillCompletionMarkerPath, readBackfillCompletionMarker, writeBackfillCompletionMarker, } from "./backfill-completion-marker.js";
|
package/dist/process-runner.js
CHANGED
|
@@ -49,6 +49,34 @@ export function createCapturedExecRunner(options = {}) {
|
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Which stdio a re-exec may inherit (BLI-4110).
|
|
54
|
+
*
|
|
55
|
+
* `stdio: "inherit"` hands the child all three of the parent's handles. Under
|
|
56
|
+
* the Windows Task Scheduler — and any other non-interactive host — the
|
|
57
|
+
* parent's stdin is not a console: it can be a pipe nobody is writing to, or a
|
|
58
|
+
* socket that was never connected, and reading it raises `read ENOTCONN` from
|
|
59
|
+
* inside `child_process.spawn`. That is the error `cockpit do-everything`
|
|
60
|
+
* printed on the founder's box on 2026-09-09, from `reexecDoctor`, in a
|
|
61
|
+
* non-interactive shell.
|
|
62
|
+
*
|
|
63
|
+
* **stdout and stderr are still inherited, always.** They are what makes a
|
|
64
|
+
* scheduled run's output land in the log a person later reads, and dropping
|
|
65
|
+
* them to fix stdin would trade a crash for a silence — which is the failure
|
|
66
|
+
* this whole ticket is about.
|
|
67
|
+
*
|
|
68
|
+
* **stdin is inherited only when there is a console to read from.** A
|
|
69
|
+
* re-execed `do-everything` under a scheduler has nothing to type at it; the
|
|
70
|
+
* handle was pure liability. `"ignore"` gives the child a real, closed stdin
|
|
71
|
+
* rather than a broken one, so a child that does read it gets EOF instead of
|
|
72
|
+
* an error.
|
|
73
|
+
*
|
|
74
|
+
* Pure and exported so both host families are testable without a real TTY,
|
|
75
|
+
* per the supported-fleet contract in AGENTS.md.
|
|
76
|
+
*/
|
|
77
|
+
export function interactiveStdio(input) {
|
|
78
|
+
return [input.stdinIsTty === true ? "inherit" : "ignore", "inherit", "inherit"];
|
|
79
|
+
}
|
|
52
80
|
export function createInteractiveExecRunner(options = {}) {
|
|
53
81
|
return (command, args, runOptions) => new Promise((resolve) => {
|
|
54
82
|
const env = runOptions?.env ?? options.env ?? process.env;
|
|
@@ -56,8 +84,9 @@ export function createInteractiveExecRunner(options = {}) {
|
|
|
56
84
|
platform: options.platform,
|
|
57
85
|
env,
|
|
58
86
|
});
|
|
87
|
+
const stdio = interactiveStdio({ stdinIsTty: process.stdin.isTTY });
|
|
59
88
|
const child = spawn(invocation.command, invocation.args, {
|
|
60
|
-
stdio
|
|
89
|
+
stdio,
|
|
61
90
|
env,
|
|
62
91
|
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
63
92
|
});
|
|
@@ -66,6 +95,15 @@ export function createInteractiveExecRunner(options = {}) {
|
|
|
66
95
|
if (settled)
|
|
67
96
|
return;
|
|
68
97
|
settled = true;
|
|
98
|
+
// Named, because a spawn that never started and a child that ran and
|
|
99
|
+
// failed both used to arrive here as a bare code 1. Metadata only.
|
|
100
|
+
const record = error;
|
|
101
|
+
console.error("[process-runner] spawn failed", JSON.stringify({
|
|
102
|
+
reason: "spawn_failed",
|
|
103
|
+
error_code: record.code ?? null,
|
|
104
|
+
error_syscall: record.syscall ?? null,
|
|
105
|
+
stdin: stdio[0],
|
|
106
|
+
}));
|
|
69
107
|
resolve({ code: 1, stdout: "", stderr: error.message });
|
|
70
108
|
});
|
|
71
109
|
child.on("close", (code) => {
|
package/dist/sync-lock.js
CHANGED
|
@@ -15,7 +15,16 @@ import { describeError } from "./health-detail.js";
|
|
|
15
15
|
*/
|
|
16
16
|
const LOCK_FILENAME = "sync.lock";
|
|
17
17
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* How long a lock may go without a heartbeat before the next sync takes it
|
|
20
|
+
* over. Exported because it is also what "the owner is still alive" MEANS on
|
|
21
|
+
* this machine: a receipt that says `sync_already_running` and carries a
|
|
22
|
+
* heartbeat inside this window was refused by a process that is still running
|
|
23
|
+
* (BLI-4303). Doctor reads it that way instead of calling a contended lock a
|
|
24
|
+
* failure.
|
|
25
|
+
*/
|
|
26
|
+
export const SYNC_LOCK_STALE_TAKEOVER_MS = 2 * 60_000;
|
|
27
|
+
const STALE_TAKEOVER_MS = SYNC_LOCK_STALE_TAKEOVER_MS;
|
|
19
28
|
export async function acquireSyncLock(paths, now = new Date()) {
|
|
20
29
|
const lockPath = path.join(paths.state_dir, LOCK_FILENAME);
|
|
21
30
|
await fs.mkdir(paths.state_dir, { recursive: true, mode: 0o700 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.100",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"pretypecheck": "node ../../scripts/build-workspace-dep.mjs @bli-cockpit/cli",
|
|
25
25
|
"typecheck": "node -e \"await import('./dist/commands/public-root.js')\"",
|
|
26
26
|
"pretest": "node ../../scripts/build-workspace-dep.mjs @bli-cockpit/cli",
|
|
27
|
-
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
27
|
+
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.26",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.31",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.43"
|
|
33
33
|
}
|
|
34
34
|
}
|