getculpa 1.0.1 → 1.0.2
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/LICENSE +41 -0
- package/assets/culpa-compose.yml +6 -6
- package/assets/uninstall-culpa.ps1 +115 -5
- package/lib/bootstrap.d.mts +25 -0
- package/lib/bootstrap.mjs +73 -0
- package/lib/docker.d.mts +4 -0
- package/lib/docker.mjs +19 -0
- package/lib/doctor.mjs +21 -8
- package/lib/install-summary.d.mts +18 -0
- package/lib/install-summary.mjs +144 -0
- package/lib/paths.mjs +6 -0
- package/lib/provision.d.mts +30 -1
- package/lib/provision.mjs +86 -5
- package/lib/repair.d.mts +1 -0
- package/lib/repair.mjs +174 -135
- package/lib/start.d.mts +2 -0
- package/lib/start.mjs +91 -4
- package/lib/tty.d.mts +18 -0
- package/lib/tty.mjs +58 -0
- package/package.json +25 -24
- package/scripts/install.js +31 -12
package/lib/provision.d.mts
CHANGED
|
@@ -2,9 +2,31 @@ import type { UpgradeClassification } from "./preflight.d.mts";
|
|
|
2
2
|
|
|
3
3
|
export const PARITY_ASSETS: string[];
|
|
4
4
|
|
|
5
|
+
// T-CF28-6 — the Windows-only subset, and the platform-scoped accessor that
|
|
6
|
+
// provision(), repair() and doctor() all read.
|
|
7
|
+
export const WINDOWS_ONLY_ASSETS: readonly string[];
|
|
8
|
+
export function parityAssetsFor(platform?: string): string[];
|
|
9
|
+
|
|
10
|
+
// T-CF29-8 — the app-dir name culpa-collector.ps1 reads (Node platform-arch),
|
|
11
|
+
// not the Rust target triple the release asset uses.
|
|
12
|
+
export function collectorBinaryName(platform?: string, arch?: string): string;
|
|
13
|
+
|
|
14
|
+
// Exported so lib/repair.mjs stages the collector to the SAME place with the
|
|
15
|
+
// SAME name — the two must never disagree about where capture lives.
|
|
16
|
+
export function stageCollectorBinary(opts: {
|
|
17
|
+
appDir: string;
|
|
18
|
+
vendorDir: string;
|
|
19
|
+
platform?: string;
|
|
20
|
+
arch?: string;
|
|
21
|
+
}): boolean;
|
|
22
|
+
|
|
5
23
|
// CF20-T5: exported for lib/repair.mjs's reuse — see provision.mjs's comment
|
|
6
24
|
// on the export.
|
|
7
|
-
export function stageParityFiles(
|
|
25
|
+
export function stageParityFiles(
|
|
26
|
+
appDir: string,
|
|
27
|
+
env?: Record<string, string | undefined>,
|
|
28
|
+
platform?: string,
|
|
29
|
+
): void;
|
|
8
30
|
export function stageLiveComposeIfAbsent(appDir: string): boolean;
|
|
9
31
|
|
|
10
32
|
export interface ProvisionOptions {
|
|
@@ -12,14 +34,21 @@ export interface ProvisionOptions {
|
|
|
12
34
|
currentVersion: string;
|
|
13
35
|
collectorFetched?: boolean;
|
|
14
36
|
platform?: string;
|
|
37
|
+
arch?: string;
|
|
38
|
+
vendorDir?: string;
|
|
15
39
|
dockerSpawnSync?: (cmd: string, args?: string[], opts?: unknown) => { status: number | null };
|
|
16
40
|
delegateWindowsInstaller?: (appDir: string) => boolean;
|
|
17
41
|
env?: Record<string, string | undefined>;
|
|
18
42
|
}
|
|
19
43
|
|
|
44
|
+
export type DockerState = "missing" | "installed-not-running" | "ready";
|
|
45
|
+
|
|
20
46
|
export interface ProvisionResult {
|
|
21
47
|
classification: UpgradeClassification;
|
|
22
48
|
previousVersion: string | null;
|
|
49
|
+
dockerState: DockerState;
|
|
50
|
+
imagesStaged: boolean;
|
|
51
|
+
collectorStaged: boolean;
|
|
23
52
|
}
|
|
24
53
|
|
|
25
54
|
export function provision(opts: ProvisionOptions): Promise<ProvisionResult>;
|
package/lib/provision.mjs
CHANGED
|
@@ -16,12 +16,20 @@
|
|
|
16
16
|
import { spawnSync as realSpawnSync } from "node:child_process";
|
|
17
17
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
18
|
import path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
19
20
|
import { checkDockerEngineReachable, checkDockerPresent, classifyUpgrade, isIncomingComposeOlder } from "./preflight.mjs";
|
|
20
21
|
import { resolveAssetPath } from "./assets.mjs";
|
|
21
22
|
import { canonicalAppDir } from "./paths.mjs";
|
|
22
23
|
|
|
24
|
+
// T-CF29-8: where lib/fetch.mjs stages the vendored binaries. Same derivation
|
|
25
|
+
// lib/repair.mjs already uses, so the two agree on one vendor directory.
|
|
26
|
+
const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
|
+
|
|
23
28
|
// The full parity file-set (Windows inventory: culpa-setup.iss:43-54, minus
|
|
24
29
|
// the collectord binary, which lib/fetch.mjs stages separately by triple).
|
|
30
|
+
// This is the WINDOWS inventory and stays the canonical full list — use
|
|
31
|
+
// parityAssetsFor(platform) to get what a given platform should actually
|
|
32
|
+
// receive.
|
|
25
33
|
export const PARITY_ASSETS = [
|
|
26
34
|
"culpa-compose.yml",
|
|
27
35
|
"install-culpa.ps1",
|
|
@@ -31,6 +39,29 @@ export const PARITY_ASSETS = [
|
|
|
31
39
|
"register.mjs",
|
|
32
40
|
];
|
|
33
41
|
|
|
42
|
+
// T-CF28-6 — four of the six are PowerShell scripts that only win32 can run:
|
|
43
|
+
// lib/start.mjs delegates to launch-culpa.ps1 only when platform === "win32"
|
|
44
|
+
// (start.mjs:457), lib/uninstall.mjs only delegates to uninstall-culpa.ps1 on
|
|
45
|
+
// win32 (uninstall.mjs:80-82), and install-culpa.ps1/culpa-collector.ps1 are
|
|
46
|
+
// invoked by the Windows installer path alone. Staging them on darwin/linux
|
|
47
|
+
// put four inert files in the user's app dir and made `getculpa doctor` grade
|
|
48
|
+
// them as part of a healthy install.
|
|
49
|
+
export const WINDOWS_ONLY_ASSETS = Object.freeze([
|
|
50
|
+
"install-culpa.ps1",
|
|
51
|
+
"launch-culpa.ps1",
|
|
52
|
+
"uninstall-culpa.ps1",
|
|
53
|
+
"culpa-collector.ps1",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// The single source of truth for "which parity files belong on this platform".
|
|
57
|
+
// provision(), repair() and doctor() ALL read it — if any one of them used the
|
|
58
|
+
// flat list instead, repair would restore what provision skipped, or doctor
|
|
59
|
+
// would fail an install for a file that is correctly absent.
|
|
60
|
+
export function parityAssetsFor(platform = process.platform) {
|
|
61
|
+
if (platform === "win32") return PARITY_ASSETS;
|
|
62
|
+
return PARITY_ASSETS.filter((name) => !WINDOWS_ONLY_ASSETS.includes(name));
|
|
63
|
+
}
|
|
64
|
+
|
|
34
65
|
// CF20-T6 — protects the SHIPPED culpa-compose.yml the same way
|
|
35
66
|
// stageLiveComposeIfAbsent already protects the live one, mirroring
|
|
36
67
|
// launch-culpa.ps1's QW-1 one-direction rule: an older incoming pin must
|
|
@@ -60,10 +91,10 @@ function stageShippedCompose(appDir, env) {
|
|
|
60
91
|
// logic — same reason the task instructions call out reuse over a second
|
|
61
92
|
// copy. Purely additive: behavior and call sites inside this file are
|
|
62
93
|
// unchanged.
|
|
63
|
-
export function stageParityFiles(appDir, env = process.env) {
|
|
94
|
+
export function stageParityFiles(appDir, env = process.env, platform = process.platform) {
|
|
64
95
|
mkdirSync(appDir, { recursive: true });
|
|
65
96
|
stageShippedCompose(appDir, env);
|
|
66
|
-
for (const name of
|
|
97
|
+
for (const name of parityAssetsFor(platform)) {
|
|
67
98
|
if (name === "culpa-compose.yml") continue;
|
|
68
99
|
copyFileSync(resolveAssetPath(name), path.join(appDir, name));
|
|
69
100
|
}
|
|
@@ -79,6 +110,40 @@ export function stageLiveComposeIfAbsent(appDir) {
|
|
|
79
110
|
return true;
|
|
80
111
|
}
|
|
81
112
|
|
|
113
|
+
// T-CF29-8 — the name culpa-collector.ps1:35 reads from the app dir, and the
|
|
114
|
+
// same name culpa-setup.iss:52 installs there. NOT the Rust target triple
|
|
115
|
+
// lib/fetch.mjs uses for the release asset (x86_64-pc-windows-msvc): this is
|
|
116
|
+
// the Node-style platform-arch pair.
|
|
117
|
+
export function collectorBinaryName(platform = process.platform, arch = process.arch) {
|
|
118
|
+
const exe = platform === "win32" ? ".exe" : "";
|
|
119
|
+
return `culpa-collectord-${platform}-${arch}${exe}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// T-CF29-8 — capture was silently OFF on every Windows npm install:
|
|
123
|
+
// "culpa-collectord-win32-x64.exe not present in this package - skipping
|
|
124
|
+
// capture setup". lib/fetch.mjs stages the binary into the PACKAGE's vendor/
|
|
125
|
+
// as culpa-collectord.exe, the .ps1 reads it from the APP DIR under the
|
|
126
|
+
// triple-suffixed name, and nothing bridged the two. The Windows installer
|
|
127
|
+
// path has always put it in the app dir, so this is the npm path catching up
|
|
128
|
+
// to installer parity, not a new convention.
|
|
129
|
+
//
|
|
130
|
+
// Returns whether the binary is really there afterwards — never a hopeful
|
|
131
|
+
// echo of the caller's collectorFetched flag, because "capture is on" is
|
|
132
|
+
// exactly the kind of claim that must be backed by a file on disk.
|
|
133
|
+
export function stageCollectorBinary({ appDir, vendorDir, platform, arch }) {
|
|
134
|
+
const exe = platform === "win32" ? ".exe" : "";
|
|
135
|
+
const src = path.join(vendorDir, `culpa-collectord${exe}`);
|
|
136
|
+
if (!existsSync(src)) return false;
|
|
137
|
+
const dest = path.join(appDir, collectorBinaryName(platform, arch));
|
|
138
|
+
try {
|
|
139
|
+
copyFileSync(src, dest);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.warn(`getculpa: capture collector could not be staged (${e.message}) - capture OFF, everything else works.`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return existsSync(dest);
|
|
145
|
+
}
|
|
146
|
+
|
|
82
147
|
function detectDockerState(dockerSpawnSync) {
|
|
83
148
|
if (!checkDockerPresent(dockerSpawnSync)) return "missing";
|
|
84
149
|
if (!checkDockerEngineReachable(dockerSpawnSync)) return "installed-not-running";
|
|
@@ -152,6 +217,8 @@ export async function provision(opts) {
|
|
|
152
217
|
currentVersion,
|
|
153
218
|
collectorFetched = false,
|
|
154
219
|
platform = process.platform,
|
|
220
|
+
arch = process.arch,
|
|
221
|
+
vendorDir = path.join(packageRoot, "vendor"),
|
|
155
222
|
dockerSpawnSync = realSpawnSync,
|
|
156
223
|
delegateWindowsInstaller = defaultDelegateWindowsInstaller,
|
|
157
224
|
env = process.env,
|
|
@@ -179,10 +246,19 @@ export async function provision(opts) {
|
|
|
179
246
|
`getculpa: this package is older than the installed Culpa ${previousVersion} - skipping the parity file re-stage entirely (compose, scripts, register.mjs) to avoid a mixed-version app dir (set CULPA_ALLOW_DOWNGRADE=1 to override).`,
|
|
180
247
|
);
|
|
181
248
|
} else {
|
|
182
|
-
stageParityFiles(appDir, env);
|
|
249
|
+
stageParityFiles(appDir, env, platform);
|
|
183
250
|
}
|
|
184
251
|
stageLiveComposeIfAbsent(appDir);
|
|
185
252
|
|
|
253
|
+
// T-CF29-8: only attempt it when the fetch reported success; the result is
|
|
254
|
+
// what gets recorded, not the intent.
|
|
255
|
+
const collectorStaged = collectorFetched && stageCollectorBinary({ appDir, vendorDir, platform, arch });
|
|
256
|
+
if (collectorFetched && !collectorStaged) {
|
|
257
|
+
console.warn(
|
|
258
|
+
"getculpa: the capture collector was downloaded but could not be placed in the app directory - capture OFF, everything else works.",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
186
262
|
const dockerState = detectDockerState(dockerSpawnSync);
|
|
187
263
|
let imagesStaged = false;
|
|
188
264
|
if (dockerState === "ready") {
|
|
@@ -229,9 +305,14 @@ export async function provision(opts) {
|
|
|
229
305
|
updatedAt: new Date().toISOString(),
|
|
230
306
|
dockerState,
|
|
231
307
|
imagesStaged,
|
|
232
|
-
collectorStaged
|
|
308
|
+
collectorStaged,
|
|
233
309
|
platform,
|
|
234
310
|
});
|
|
235
311
|
|
|
236
|
-
|
|
312
|
+
// T-CF28-5/7: the Docker and staging facts were already computed above and
|
|
313
|
+
// written to install-state.json, but the caller that has to REPORT them
|
|
314
|
+
// (scripts/install.js) could not see them — which is how a Docker-less
|
|
315
|
+
// install still printed "Culpa installed successfully." Returning them
|
|
316
|
+
// changes no behavior here; it just stops postinstall from having to guess.
|
|
317
|
+
return { classification, previousVersion, dockerState, imagesStaged, collectorStaged };
|
|
237
318
|
}
|
package/lib/repair.d.mts
CHANGED
package/lib/repair.mjs
CHANGED
|
@@ -1,135 +1,174 @@
|
|
|
1
|
-
// CF20-T5 — `getculpa repair`. Re-stages missing/corrupt parity files from
|
|
2
|
-
// the packaged assets, regenerates install-state.json if unreadable,
|
|
3
|
-
// re-fetches a missing launcher/collectord, and does none of this
|
|
4
|
-
// destructively: an EXISTING docker-compose.yml (the live pin) is never
|
|
5
|
-
// touched (the edcf97f invariant this task's reviewer condition names), no
|
|
6
|
-
// `docker volume` command is ever issued (data is never touched), and a
|
|
7
|
-
// second run changes nothing that was already healthy (idempotent).
|
|
8
|
-
//
|
|
9
|
-
// Reuses lib/provision.mjs's own stageParityFiles/stageLiveComposeIfAbsent
|
|
10
|
-
// (exported additively for this purpose — see that file's comment) instead
|
|
11
|
-
// of re-implementing file staging here.
|
|
12
|
-
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
-
import path from "node:path";
|
|
15
|
-
import { fileURLToPath } from "node:url";
|
|
16
|
-
import { spawnSync as realSpawnSync } from "node:child_process";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
1
|
+
// CF20-T5 — `getculpa repair`. Re-stages missing/corrupt parity files from
|
|
2
|
+
// the packaged assets, regenerates install-state.json if unreadable,
|
|
3
|
+
// re-fetches a missing launcher/collectord, and does none of this
|
|
4
|
+
// destructively: an EXISTING docker-compose.yml (the live pin) is never
|
|
5
|
+
// touched (the edcf97f invariant this task's reviewer condition names), no
|
|
6
|
+
// `docker volume` command is ever issued (data is never touched), and a
|
|
7
|
+
// second run changes nothing that was already healthy (idempotent).
|
|
8
|
+
//
|
|
9
|
+
// Reuses lib/provision.mjs's own stageParityFiles/stageLiveComposeIfAbsent
|
|
10
|
+
// (exported additively for this purpose — see that file's comment) instead
|
|
11
|
+
// of re-implementing file staging here.
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { spawnSync as realSpawnSync } from "node:child_process";
|
|
17
|
+
import {
|
|
18
|
+
collectorBinaryName,
|
|
19
|
+
parityAssetsFor,
|
|
20
|
+
stageCollectorBinary,
|
|
21
|
+
stageLiveComposeIfAbsent,
|
|
22
|
+
stageParityFiles,
|
|
23
|
+
} from "./provision.mjs";
|
|
24
|
+
import { fetchCollectord, fetchLauncher } from "./fetch.mjs";
|
|
25
|
+
import { checkDockerEngineReachable, checkDockerPresent } from "./preflight.mjs";
|
|
26
|
+
|
|
27
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const packageRoot = path.join(__dirname, "..");
|
|
29
|
+
|
|
30
|
+
function readJsonSafe(filePath) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function detectDockerState(dockerSpawnSync) {
|
|
39
|
+
if (!checkDockerPresent(dockerSpawnSync)) return "missing";
|
|
40
|
+
if (!checkDockerEngineReachable(dockerSpawnSync)) return "installed-not-running";
|
|
41
|
+
return "ready";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function vendorBinaryPath(vendorDir, name, platform) {
|
|
45
|
+
const exe = platform === "win32" ? ".exe" : "";
|
|
46
|
+
return path.join(vendorDir, `${name}${exe}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function repair(opts) {
|
|
50
|
+
const {
|
|
51
|
+
appDir,
|
|
52
|
+
currentVersion,
|
|
53
|
+
platform = process.platform,
|
|
54
|
+
arch = process.arch,
|
|
55
|
+
vendorDir = path.join(packageRoot, "vendor"),
|
|
56
|
+
dockerSpawnSync = realSpawnSync,
|
|
57
|
+
fetchLauncherFn = fetchLauncher,
|
|
58
|
+
fetchCollectordFn = fetchCollectord,
|
|
59
|
+
log = console.log,
|
|
60
|
+
} = opts;
|
|
61
|
+
|
|
62
|
+
const actions = [];
|
|
63
|
+
|
|
64
|
+
if (!existsSync(appDir)) {
|
|
65
|
+
mkdirSync(appDir, { recursive: true });
|
|
66
|
+
actions.push(`created the missing app directory (${appDir})`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 1. Parity files — re-stage anything missing. stageParityFiles never
|
|
70
|
+
// touches docker-compose.yml (it doesn't reference that name at all),
|
|
71
|
+
// so this step alone already satisfies "never touch a live compose".
|
|
72
|
+
// T-CF28-6: platform-scoped, or repair would restore the four Windows .ps1
|
|
73
|
+
// files provisioning deliberately skips on darwin/linux — and report each
|
|
74
|
+
// one as a "restored missing parity file" action while doing it.
|
|
75
|
+
const missingBefore = parityAssetsFor(platform).filter((name) => !existsSync(path.join(appDir, name)));
|
|
76
|
+
stageParityFiles(appDir, process.env, platform);
|
|
77
|
+
for (const name of missingBefore) {
|
|
78
|
+
actions.push(`restored missing parity file: ${name}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2. Live compose — created ONLY if entirely absent; an existing one is
|
|
82
|
+
// never overwritten (stageLiveComposeIfAbsent's own contract).
|
|
83
|
+
const composeCreated = stageLiveComposeIfAbsent(appDir);
|
|
84
|
+
if (composeCreated) {
|
|
85
|
+
actions.push("live docker-compose.yml was missing - recreated from the shipped culpa-compose.yml");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 3. install-state.json — regenerate only when missing/corrupt. The
|
|
89
|
+
// regenerated state is a conservative snapshot of what is actually on
|
|
90
|
+
// disk right now: dockerState/collectorStaged are re-detected,
|
|
91
|
+
// imagesStaged is set false (safe default — worst case is one extra
|
|
92
|
+
// `docker compose pull` message at the next `getculpa`, never data
|
|
93
|
+
// loss, mirroring deferredPullCatchUp's own contract in lib/start.mjs).
|
|
94
|
+
const existingState = readJsonSafe(path.join(appDir, "install-state.json"));
|
|
95
|
+
if (!existingState) {
|
|
96
|
+
const dockerState = detectDockerState(dockerSpawnSync);
|
|
97
|
+
// Review of 0e4b2e2: this derived collectorStaged from the PACKAGE
|
|
98
|
+
// vendor dir, which is not what culpa-collector.ps1:35 reads. It could
|
|
99
|
+
// therefore regenerate install-state.json saying capture was staged
|
|
100
|
+
// while the app dir held no collector at all — the same false positive
|
|
101
|
+
// T-CF29-8 removed from provision(). One invariant, both writers.
|
|
102
|
+
const collectorStaged = existsSync(path.join(appDir, collectorBinaryName(platform, arch)));
|
|
103
|
+
const regenerated = {
|
|
104
|
+
packageVersion: currentVersion,
|
|
105
|
+
provisionedAt: new Date().toISOString(),
|
|
106
|
+
dockerState,
|
|
107
|
+
imagesStaged: false,
|
|
108
|
+
collectorStaged,
|
|
109
|
+
platform,
|
|
110
|
+
};
|
|
111
|
+
writeFileSync(path.join(appDir, "install-state.json"), `${JSON.stringify(regenerated, null, 2)}\n`);
|
|
112
|
+
actions.push("install-state.json was missing or unreadable - regenerated");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 4. Launcher / collectord — re-fetch only when the vendored binary is
|
|
116
|
+
// genuinely absent. Never re-fetches when present (idempotent, and the
|
|
117
|
+
// task's explicit invocation-recording fact). The launcher is REQUIRED
|
|
118
|
+
// (doctor.mjs's checkVendoredLauncher fails the same way when it's
|
|
119
|
+
// missing) — a failed re-fetch must make repair's own result say so,
|
|
120
|
+
// not just log and report ok:true (CF20-R review). The collector stays
|
|
121
|
+
// optional/non-fatal: capture-off is a valid degraded state.
|
|
122
|
+
const launcherPath = vendorBinaryPath(vendorDir, "culpa-launcher", platform);
|
|
123
|
+
if (!existsSync(launcherPath)) {
|
|
124
|
+
try {
|
|
125
|
+
await fetchLauncherFn({ vendorDir });
|
|
126
|
+
actions.push("the launcher was missing - re-fetched and verified");
|
|
127
|
+
} catch (e) {
|
|
128
|
+
log(`getculpa repair: could not re-fetch the launcher (${e.message}). Run \`getculpa repair\` again once online.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const collectordPath = vendorBinaryPath(vendorDir, "culpa-collectord", platform);
|
|
133
|
+
if (!existsSync(collectordPath)) {
|
|
134
|
+
try {
|
|
135
|
+
await fetchCollectordFn({ vendorDir });
|
|
136
|
+
actions.push("the capture collector was missing - re-fetched and verified");
|
|
137
|
+
} catch (e) {
|
|
138
|
+
// fetchCollectord's own contract: "not published for this release yet"
|
|
139
|
+
// is an expected, non-fatal outcome (see lib/fetch.mjs) — repair must
|
|
140
|
+
// degrade the same way, never abort over an optional component.
|
|
141
|
+
log(`getculpa repair: capture collector not re-fetched (${e.message}).`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Review of 0e4b2e2: re-fetching only refilled the PACKAGE vendor dir, so
|
|
146
|
+
// "re-fetched and verified" claimed a repair that left capture exactly as
|
|
147
|
+
// broken as it found it. The app-dir copy is the one that matters.
|
|
148
|
+
const appCollector = path.join(appDir, collectorBinaryName(platform, arch));
|
|
149
|
+
if (!existsSync(appCollector) && stageCollectorBinary({ appDir, vendorDir, platform, arch })) {
|
|
150
|
+
actions.push("the capture collector was missing from the app directory - staged it");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// The state file may have been regenerated BEFORE the staging above, so
|
|
154
|
+
// correct it to what is now true rather than leaving a stale answer.
|
|
155
|
+
const statePath = path.join(appDir, "install-state.json");
|
|
156
|
+
const finalState = readJsonSafe(statePath);
|
|
157
|
+
const collectorTruth = existsSync(appCollector);
|
|
158
|
+
// Review of 4e45f81: this used to correct the file and say NOTHING, so a
|
|
159
|
+
// run that really did change something reported "nothing to do - this
|
|
160
|
+
// install is already healthy". Same class of false claim the rest of this
|
|
161
|
+
// commit removes, just relocated into the action log.
|
|
162
|
+
if (finalState && finalState.collectorStaged !== collectorTruth) {
|
|
163
|
+
actions.push(
|
|
164
|
+
`install-state.json claimed capture was ${finalState.collectorStaged ? "staged" : "absent"} - corrected to match the app directory`,
|
|
165
|
+
);
|
|
166
|
+
writeFileSync(statePath, `${JSON.stringify({ ...finalState, collectorStaged: collectorTruth }, null, 2)}\n`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (actions.length === 0) log("getculpa repair: nothing to do - this install is already healthy.");
|
|
170
|
+
else for (const a of actions) log(`getculpa repair: ${a}`);
|
|
171
|
+
|
|
172
|
+
const launcherOk = existsSync(launcherPath);
|
|
173
|
+
return { ok: launcherOk, actions };
|
|
174
|
+
}
|
package/lib/start.d.mts
CHANGED
|
@@ -28,6 +28,8 @@ export interface StartOptions {
|
|
|
28
28
|
openBrowser?: (url: string) => void;
|
|
29
29
|
isTTY?: boolean;
|
|
30
30
|
delegateWindowsLaunch?: (appDir: string) => { ok: boolean; output?: string };
|
|
31
|
+
isInteractive?: boolean;
|
|
32
|
+
askYesNo?: (question: string) => Promise<boolean>;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export interface StartResult {
|