getculpa 0.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/README.md +37 -18
- package/assets/culpa-collector.ps1 +137 -0
- package/assets/culpa-compose.yml +144 -0
- package/assets/install-culpa.ps1 +310 -0
- package/assets/launch-culpa.ps1 +350 -0
- package/assets/register.mjs +713 -0
- package/assets/uninstall-culpa.ps1 +227 -0
- package/bin/culpa.js +21 -10
- package/bin/getculpa.js +147 -0
- package/lib/assets.d.mts +2 -0
- package/lib/assets.mjs +36 -0
- package/lib/bootstrap.d.mts +25 -0
- package/lib/bootstrap.mjs +73 -0
- package/lib/docker.d.mts +76 -0
- package/lib/docker.mjs +270 -0
- package/lib/doctor.d.mts +29 -0
- package/lib/doctor.mjs +405 -0
- package/lib/fetch.d.mts +17 -0
- package/lib/fetch.mjs +211 -0
- package/lib/install-summary.d.mts +18 -0
- package/lib/install-summary.mjs +144 -0
- package/lib/paths.d.mts +9 -0
- package/lib/paths.mjs +61 -0
- package/lib/preflight.d.mts +39 -0
- package/lib/preflight.mjs +169 -0
- package/lib/provision.d.mts +54 -0
- package/lib/provision.mjs +318 -0
- package/lib/repair.d.mts +21 -0
- package/lib/repair.mjs +174 -0
- package/lib/start.d.mts +44 -0
- package/lib/start.mjs +479 -0
- package/lib/status.d.mts +23 -0
- package/lib/status.mjs +74 -0
- package/lib/stop.d.mts +15 -0
- package/lib/stop.mjs +46 -0
- package/lib/tty.d.mts +18 -0
- package/lib/tty.mjs +58 -0
- package/lib/uninstall.d.mts +17 -0
- package/lib/uninstall.mjs +99 -0
- package/package.json +14 -25
- package/scripts/install.js +154 -0
- package/scripts/prepack.js +41 -0
- package/index.js +0 -3
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// T-CF28-5 (postinstall output) + T-CF28-7 (the install-time half of the
|
|
2
|
+
// Docker ask) — the lines postinstall prints when it finishes.
|
|
3
|
+
//
|
|
4
|
+
// Extracted from scripts/install.js's main() as a PURE function. The words
|
|
5
|
+
// are the product here: the shipped 1.0.1 printed "Culpa installed
|
|
6
|
+
// successfully." on a fresh Mac with no Docker, where nothing could actually
|
|
7
|
+
// start. That line lived inline in a CommonJS script with no export, so no
|
|
8
|
+
// test could assert it. It is a line-builder now so the claims it makes are
|
|
9
|
+
// checked by tests/npm-core-install-summary.test.ts.
|
|
10
|
+
//
|
|
11
|
+
// TWO RULES THIS FILE EXISTS TO ENFORCE:
|
|
12
|
+
//
|
|
13
|
+
// 1. Never claim "ready" unless it IS ready. The founder's ruled headline is
|
|
14
|
+
// "Culpa is installed and ready." with `ready` in green — printed when
|
|
15
|
+
// Docker is genuinely usable. When Docker is missing or stopped, the
|
|
16
|
+
// headline drops to "Culpa is installed." and the Docker requirement is
|
|
17
|
+
// stated plainly. Printing the ruled line unconditionally would repeat the
|
|
18
|
+
// exact defect that created T-CF28-7.
|
|
19
|
+
//
|
|
20
|
+
// 2. Say what each platform will really do. postinstall CANNOT prompt
|
|
21
|
+
// (scripts/install.js:13-15 — npm installs run headless and a stdin read
|
|
22
|
+
// would hang), so this file only DETECTS and TELLS; the asking happens at
|
|
23
|
+
// first `getculpa`. win32 already ships that flow
|
|
24
|
+
// (installers/windows/launch-culpa.ps1:179-234); darwin gains it in
|
|
25
|
+
// lib/start.mjs; linux stays guidance-only, because installing a system
|
|
26
|
+
// package without explicit consent is out of bounds (the same rule
|
|
27
|
+
// lib/docker.mjs:73-79 already follows for starting the engine).
|
|
28
|
+
|
|
29
|
+
import { green } from "./tty.mjs";
|
|
30
|
+
|
|
31
|
+
const DOCKER_DESKTOP_URL = "https://www.docker.com/products/docker-desktop/";
|
|
32
|
+
const DOCKER_ENGINE_DOCS_URL = "https://docs.docker.com/engine/install/";
|
|
33
|
+
|
|
34
|
+
function headline(classification, version, previousVersion, dockerReady, colour) {
|
|
35
|
+
switch (classification) {
|
|
36
|
+
case "upgrade":
|
|
37
|
+
return `Culpa updated: ${previousVersion} -> ${version} (staged; applies at next \`getculpa\`).`;
|
|
38
|
+
case "downgrade-package":
|
|
39
|
+
return `Culpa ${previousVersion} is already installed; this package (${version}) is older and was not applied.`;
|
|
40
|
+
case "same-version":
|
|
41
|
+
case "repair":
|
|
42
|
+
return "Culpa installation repaired.";
|
|
43
|
+
default:
|
|
44
|
+
// "fresh" — the only case the founder's ruled wording describes.
|
|
45
|
+
return dockerReady ? `Culpa is installed and ${green("ready", colour)}.` : "Culpa is installed.";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function inventory(appDir, imagesStaged, collectorStaged, payloadStaged) {
|
|
50
|
+
return [
|
|
51
|
+
` Location: ${appDir}`,
|
|
52
|
+
// T-CF28-4b: the CLI payload is the program `getculpa` actually runs. When
|
|
53
|
+
// it is missing the install is not usable at all, so this row names the
|
|
54
|
+
// one command that finishes the job rather than leaving a dead first run.
|
|
55
|
+
` Culpa CLI: ${payloadStaged ? "installed" : "deferred - run `getculpa update` to finish"}`,
|
|
56
|
+
` Images: ${imagesStaged ? "staged" : "deferred to first run"}`,
|
|
57
|
+
` Capture: ${collectorStaged ? "collector installed" : "off (collector not installed)"}`,
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Docker is MISSING. Each platform gets only what is true for it.
|
|
62
|
+
function dockerMissingLines(platform) {
|
|
63
|
+
const lines = [
|
|
64
|
+
"Culpa needs Docker to run: its database and services run in containers.",
|
|
65
|
+
"Docker was not found on this machine.",
|
|
66
|
+
];
|
|
67
|
+
if (platform === "darwin") {
|
|
68
|
+
lines.push(
|
|
69
|
+
" Install it with: brew install --cask docker",
|
|
70
|
+
" Homebrew asks for your password, and you must open Docker once from",
|
|
71
|
+
" Applications before Culpa can start - it is not a one-click install.",
|
|
72
|
+
` Or download it: ${DOCKER_DESKTOP_URL}`,
|
|
73
|
+
"Culpa will offer to do this for you the first time you run `getculpa`.",
|
|
74
|
+
);
|
|
75
|
+
} else if (platform === "win32") {
|
|
76
|
+
lines.push(
|
|
77
|
+
"Culpa will offer to install Docker for you the first time you run `getculpa`.",
|
|
78
|
+
` Or download it: ${DOCKER_DESKTOP_URL}`,
|
|
79
|
+
);
|
|
80
|
+
} else {
|
|
81
|
+
lines.push(
|
|
82
|
+
` Install Docker Engine: ${DOCKER_ENGINE_DOCS_URL}`,
|
|
83
|
+
"Culpa will not install system packages for you - install it yourself, then",
|
|
84
|
+
"run `getculpa`.",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return lines;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Docker is PRESENT but the engine is stopped. Starting it is something Culpa
|
|
91
|
+
// really does do on darwin (lib/docker.mjs:69-72, `open -a Docker`) and win32
|
|
92
|
+
// (installers/windows/launch-culpa.ps1:236-244) — but never on linux.
|
|
93
|
+
function dockerStoppedLines(platform) {
|
|
94
|
+
const lines = ["Docker is installed but its engine is not running."];
|
|
95
|
+
if (platform === "linux") {
|
|
96
|
+
lines.push(" Start it yourself, for example: sudo systemctl start docker");
|
|
97
|
+
} else {
|
|
98
|
+
lines.push(" Culpa will start it for you when you run `getculpa`.");
|
|
99
|
+
}
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function buildInstallSummary(opts) {
|
|
104
|
+
const {
|
|
105
|
+
classification,
|
|
106
|
+
version,
|
|
107
|
+
previousVersion = null,
|
|
108
|
+
appDir,
|
|
109
|
+
dockerState,
|
|
110
|
+
imagesStaged = false,
|
|
111
|
+
collectorStaged = false,
|
|
112
|
+
// T-CF28-4b. Fail-CLOSED, matching imagesStaged/collectorStaged above: a
|
|
113
|
+
// caller that cannot say whether the payload is there has no evidence for
|
|
114
|
+
// "ready", and this function exists precisely to stop readiness being
|
|
115
|
+
// claimed without evidence. scripts/install.js always passes the real value.
|
|
116
|
+
payloadStaged = false,
|
|
117
|
+
platform = process.platform,
|
|
118
|
+
isTTY = false,
|
|
119
|
+
env = {},
|
|
120
|
+
} = opts;
|
|
121
|
+
|
|
122
|
+
const dockerReady = dockerState === "ready";
|
|
123
|
+
// "ready" means the next command will work. A missing CLI payload breaks
|
|
124
|
+
// that just as completely as a missing Docker, so it gates the word too.
|
|
125
|
+
const ready = dockerReady && payloadStaged;
|
|
126
|
+
const colour = { isTTY, env };
|
|
127
|
+
const lines = [headline(classification, version, previousVersion, ready, colour)];
|
|
128
|
+
|
|
129
|
+
// A refused downgrade staged nothing — an inventory there would describe a
|
|
130
|
+
// state this run did not produce (lib/provision.mjs:175-183).
|
|
131
|
+
if (classification !== "downgrade-package") {
|
|
132
|
+
lines.push(...inventory(appDir, imagesStaged, collectorStaged, payloadStaged));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!dockerReady) {
|
|
136
|
+
lines.push("");
|
|
137
|
+
lines.push(...(dockerState === "missing" ? dockerMissingLines(platform) : dockerStoppedLines(platform)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
lines.push("");
|
|
141
|
+
lines.push(ready ? "Run:" : "Then run:");
|
|
142
|
+
lines.push(" getculpa");
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
package/lib/paths.d.mts
ADDED
package/lib/paths.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// CF20-T3 — per-OS app dir resolution (Windows-installer parity).
|
|
2
|
+
//
|
|
3
|
+
// Real conventions, verified against the installers themselves:
|
|
4
|
+
// win32: %LOCALAPPDATA%\Culpa (installers/windows/install-culpa.ps1:19)
|
|
5
|
+
// darwin: ~/Library/Application Support/Culpa (installers/macos/install-culpa.command:7)
|
|
6
|
+
// linux: ~/.local/share/culpa, honoring XDG_DATA_HOME (no shipped Linux
|
|
7
|
+
// installer yet — this is the XDG-spec default for that base path)
|
|
8
|
+
//
|
|
9
|
+
// path.win32 / path.posix are used explicitly (never the ambient `path`)
|
|
10
|
+
// so the SAME function returns the correct separator for a simulated
|
|
11
|
+
// platform on any host OS — this is what makes it testable on a Windows
|
|
12
|
+
// dev machine without lying about macOS/Linux output.
|
|
13
|
+
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
// CULPA_APP_DIR is a test-only escape hatch (task spec: "for tests only").
|
|
18
|
+
// It is still readable in production because there is no safe way to tell
|
|
19
|
+
// "a test set this" from "an operator set this" from inside the module, and
|
|
20
|
+
// refusing a real operator's override would be a worse failure mode than
|
|
21
|
+
// honoring it.
|
|
22
|
+
//
|
|
23
|
+
// D-108 (cli-v1.0.2): the vendored Rust launcher honors CULPA_APP_DIR too —
|
|
24
|
+
// as a fallback when its own CULPA_HOME is unset (native/culpa-launcher/
|
|
25
|
+
// src/home.rs). So relocating with this variable moves BOTH halves: the
|
|
26
|
+
// stack/licence state this module resolves AND the launcher's
|
|
27
|
+
// versions/updates state. Keep the two sides in step if either changes.
|
|
28
|
+
export function getAppDir(opts = {}) {
|
|
29
|
+
const env = opts.env ?? process.env;
|
|
30
|
+
const platform = opts.platform ?? process.platform;
|
|
31
|
+
const homedir = opts.homedir ?? os.homedir();
|
|
32
|
+
|
|
33
|
+
if (env.CULPA_APP_DIR) return env.CULPA_APP_DIR;
|
|
34
|
+
|
|
35
|
+
if (platform === "win32") {
|
|
36
|
+
const base = env.LOCALAPPDATA || path.win32.join(homedir, "AppData", "Local");
|
|
37
|
+
return path.win32.join(base, "Culpa");
|
|
38
|
+
}
|
|
39
|
+
if (platform === "darwin") {
|
|
40
|
+
return path.posix.join(homedir, "Library", "Application Support", "Culpa");
|
|
41
|
+
}
|
|
42
|
+
// linux and other POSIX platforms
|
|
43
|
+
const xdgDataHome = env.XDG_DATA_HOME || path.posix.join(homedir, ".local", "share");
|
|
44
|
+
return path.posix.join(xdgDataHome, "culpa");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// CF20-R — the ONE fixed HKCU uninstall key (installers/windows/
|
|
48
|
+
// install-culpa.ps1's Add/Remove Programs registration, culpa-setup.iss's
|
|
49
|
+
// AppId) is per-MACHINE, not per-app-dir. A second app dir on the same
|
|
50
|
+
// Windows machine (CULPA_APP_DIR pointed somewhere non-default) must never
|
|
51
|
+
// register/overwrite that shared key — the gate's historical incident class.
|
|
52
|
+
// canonicalAppDir answers "what would getAppDir() return if CULPA_APP_DIR
|
|
53
|
+
// were NOT set" — the override is stripped, everything else (platform,
|
|
54
|
+
// homedir, LOCALAPPDATA/XDG_DATA_HOME) still applies — so a caller can tell
|
|
55
|
+
// whether the app dir it was actually given is the canonical, registry-owning
|
|
56
|
+
// one.
|
|
57
|
+
export function canonicalAppDir(opts = {}) {
|
|
58
|
+
const env = { ...(opts.env ?? process.env) };
|
|
59
|
+
delete env.CULPA_APP_DIR;
|
|
60
|
+
return getAppDir({ ...opts, env });
|
|
61
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface PlatformSupport {
|
|
2
|
+
supported: boolean;
|
|
3
|
+
message?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function checkPlatformSupport(platform?: string, arch?: string): PlatformSupport;
|
|
7
|
+
|
|
8
|
+
export interface DiskSpaceResult {
|
|
9
|
+
known: boolean;
|
|
10
|
+
availableBytes: number | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function checkDiskSpaceBestEffort(targetDir: string): DiskSpaceResult;
|
|
14
|
+
|
|
15
|
+
export type SpawnSyncLike = (cmd: string, args?: string[], opts?: unknown) => { status: number | null };
|
|
16
|
+
|
|
17
|
+
export function checkDockerPresent(spawnSync?: SpawnSyncLike): boolean;
|
|
18
|
+
export function checkDockerEngineReachable(spawnSync?: SpawnSyncLike): boolean;
|
|
19
|
+
|
|
20
|
+
export function parsePinnedServerVersion(composeText: string | null | undefined): string | null;
|
|
21
|
+
export function compareVersions(a: string | null | undefined, b: string | null | undefined): -1 | 0 | 1;
|
|
22
|
+
|
|
23
|
+
export type InstallKind = "fresh" | "upgrade" | "same-version" | "repair";
|
|
24
|
+
|
|
25
|
+
export function classifyInstall(opts: { appDir: string; currentVersion: string }): InstallKind;
|
|
26
|
+
|
|
27
|
+
export type UpgradeClassification = InstallKind | "downgrade-package";
|
|
28
|
+
|
|
29
|
+
export interface UpgradeClassificationResult {
|
|
30
|
+
classification: UpgradeClassification;
|
|
31
|
+
previousVersion: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function classifyUpgrade(opts: { appDir: string; currentVersion: string }): UpgradeClassificationResult;
|
|
35
|
+
|
|
36
|
+
export function isIncomingComposeOlder(
|
|
37
|
+
incomingComposeText: string | null | undefined,
|
|
38
|
+
stagedComposeText: string | null | undefined,
|
|
39
|
+
): boolean;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// CF20-T3 — preflight checks for the shared install core. Detection only:
|
|
2
|
+
// nothing here starts Docker, pulls an image, or touches the app dir.
|
|
3
|
+
//
|
|
4
|
+
// Docker checks take an injectable spawnSync (default: the real one), the
|
|
5
|
+
// same pattern install-culpa.ps1 uses for Test-DockerInstalled's -Probe —
|
|
6
|
+
// so a test can stub docker's presence/engine state without a real Docker
|
|
7
|
+
// install or a fragile PATH shim.
|
|
8
|
+
|
|
9
|
+
import { spawnSync as realSpawnSync } from "node:child_process";
|
|
10
|
+
import { existsSync, readFileSync, statfsSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { PROBE_TIMEOUT_MS } from "./docker.mjs";
|
|
13
|
+
|
|
14
|
+
const SUPPORTED_TRIPLES = new Set(["win32-x64", "linux-x64", "darwin-x64", "darwin-arm64"]);
|
|
15
|
+
|
|
16
|
+
// Mirrors the launcher triples already load-bearing in scripts/install.js's
|
|
17
|
+
// targetTriple() (win32/x64, linux/x64, darwin/x64, darwin/arm64).
|
|
18
|
+
export function checkPlatformSupport(platform = process.platform, arch = process.arch) {
|
|
19
|
+
if (SUPPORTED_TRIPLES.has(`${platform}-${arch}`)) return { supported: true };
|
|
20
|
+
const message =
|
|
21
|
+
"Culpa currently supports Windows x64, Linux x64, and macOS (Intel or Apple Silicon). " +
|
|
22
|
+
`Detected: ${platform}/${arch}.`;
|
|
23
|
+
return { supported: false, message };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Best-effort: `statfs` is unavailable or throws on some hosts/paths (e.g. a
|
|
27
|
+
// directory that does not exist yet). Unknown is a valid, honestly-reported
|
|
28
|
+
// outcome — this never blocks or fails the install.
|
|
29
|
+
export function checkDiskSpaceBestEffort(targetDir) {
|
|
30
|
+
try {
|
|
31
|
+
const probeDir = nearestExistingAncestor(targetDir);
|
|
32
|
+
const stats = statfsSync(probeDir);
|
|
33
|
+
return { known: true, availableBytes: stats.bavail * stats.bsize };
|
|
34
|
+
} catch {
|
|
35
|
+
return { known: false, availableBytes: null };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function nearestExistingAncestor(startDir) {
|
|
40
|
+
let dir = startDir;
|
|
41
|
+
for (let i = 0; i < 64; i++) {
|
|
42
|
+
if (existsSync(dir)) return dir;
|
|
43
|
+
const parent = path.dirname(dir);
|
|
44
|
+
if (parent === dir) return dir; // reached the filesystem root
|
|
45
|
+
dir = parent;
|
|
46
|
+
}
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function checkDockerPresent(spawnSync = realSpawnSync) {
|
|
51
|
+
// CF20-R: PROBE_TIMEOUT_MS (docker.mjs) — a hung docker CLI must not stall
|
|
52
|
+
// npm install forever.
|
|
53
|
+
const result = spawnSync("docker", ["--version"], { timeout: PROBE_TIMEOUT_MS });
|
|
54
|
+
return result.status === 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Reachability only — never starts the engine (that is launch-time's job).
|
|
58
|
+
export function checkDockerEngineReachable(spawnSync = realSpawnSync) {
|
|
59
|
+
const result = spawnSync("docker", ["info", "--format", "ok"], { timeout: PROBE_TIMEOUT_MS });
|
|
60
|
+
return result.status === 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Same parsing target as launch-culpa.ps1's Get-PinnedServerVersion.
|
|
64
|
+
export function parsePinnedServerVersion(composeText) {
|
|
65
|
+
const m = /culpa-server:([^"\s]+)/.exec(composeText ?? "");
|
|
66
|
+
return m ? m[1] : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Same fail-open-to-"equal" semantics as launch-culpa.ps1's
|
|
70
|
+
// Compare-CulpaVersions: an unparsable tag never brick the comparison, it
|
|
71
|
+
// just reads as 0 (equal). -1 = a older, 1 = a newer.
|
|
72
|
+
export function compareVersions(a, b) {
|
|
73
|
+
const rx = /^v?(\d+)\.(\d+)\.(\d+)/;
|
|
74
|
+
const ma = rx.exec(a ?? "");
|
|
75
|
+
const mb = rx.exec(b ?? "");
|
|
76
|
+
if (!ma || !mb) return 0;
|
|
77
|
+
for (let i = 1; i <= 3; i++) {
|
|
78
|
+
const na = Number(ma[i]);
|
|
79
|
+
const nb = Number(mb[i]);
|
|
80
|
+
if (na < nb) return -1;
|
|
81
|
+
if (na > nb) return 1;
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Shared by classifyInstall and classifyUpgrade so the two can never read
|
|
87
|
+
// "what version is already here" two different ways.
|
|
88
|
+
function resolveInstalledVersionInfo(appDir) {
|
|
89
|
+
const liveComposePath = path.join(appDir, "docker-compose.yml");
|
|
90
|
+
if (!existsSync(appDir) || !existsSync(liveComposePath)) {
|
|
91
|
+
return { exists: false, installedVersion: null, state: null };
|
|
92
|
+
}
|
|
93
|
+
const statePath = path.join(appDir, "install-state.json");
|
|
94
|
+
const state = readJsonSafe(statePath);
|
|
95
|
+
const pinned = parsePinnedServerVersion(readTextSafe(liveComposePath));
|
|
96
|
+
const installedVersion = state?.packageVersion ?? pinned ?? null;
|
|
97
|
+
return { exists: true, installedVersion, state };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// fresh: nothing installed here yet.
|
|
101
|
+
// repair: something is here (a live compose exists) but there is no state
|
|
102
|
+
// file we trust to say what version it is (e.g. a Windows-GUI-only
|
|
103
|
+
// install, or a corrupt state file) — never guess, ask for a repair.
|
|
104
|
+
// upgrade / same-version: read from install-state.json's packageVersion
|
|
105
|
+
// (falling back to the live compose's pinned tag) compared against the
|
|
106
|
+
// version of the package currently running.
|
|
107
|
+
export function classifyInstall({ appDir, currentVersion }) {
|
|
108
|
+
const info = resolveInstalledVersionInfo(appDir);
|
|
109
|
+
if (!info.exists) return "fresh";
|
|
110
|
+
if (!info.installedVersion || !info.state) return "repair";
|
|
111
|
+
|
|
112
|
+
const cmp = compareVersions(info.installedVersion, currentVersion);
|
|
113
|
+
if (cmp === 0) return "same-version";
|
|
114
|
+
if (cmp === -1) return "upgrade";
|
|
115
|
+
return "repair"; // installed reads NEWER than this package — never silently downgrade
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// CF20-T6 — wires classifyInstall (T3 review Minor #4: implemented, tested,
|
|
119
|
+
// ORPHANED — nothing called it) into the postinstall flow, and splits
|
|
120
|
+
// classifyInstall's "repair" bucket into the two shapes postinstall must
|
|
121
|
+
// message and record differently:
|
|
122
|
+
// - a genuinely unknown install (no trusted version info at all), vs.
|
|
123
|
+
// - "downgrade-package": npm delivered an OLDER getculpa than the
|
|
124
|
+
// version already staged here (a stale mirror, a pinned older version,
|
|
125
|
+
// an explicit rollback) — never silently downgrade.
|
|
126
|
+
// previousVersion is best-effort (state's packageVersion first, the live
|
|
127
|
+
// compose's pinned tag as fallback) and is null only for a genuinely fresh
|
|
128
|
+
// install.
|
|
129
|
+
export function classifyUpgrade({ appDir, currentVersion }) {
|
|
130
|
+
const classification = classifyInstall({ appDir, currentVersion });
|
|
131
|
+
const { installedVersion: previousVersion } = resolveInstalledVersionInfo(appDir);
|
|
132
|
+
if (classification === "repair" && previousVersion && compareVersions(previousVersion, currentVersion) === 1) {
|
|
133
|
+
return { classification: "downgrade-package", previousVersion };
|
|
134
|
+
}
|
|
135
|
+
return { classification, previousVersion };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// CF20-T6 — mirrors launch-culpa.ps1's Test-BackwardPin one-direction rule
|
|
139
|
+
// (QW-1/QW-3 semantics) so the SHIPPED culpa-compose.yml can get the same
|
|
140
|
+
// protection the live compose already has via provision.mjs's
|
|
141
|
+
// stageLiveComposeIfAbsent: an older shipped file must never silently win
|
|
142
|
+
// over a newer one already staged — the downgrade-package case. A malformed
|
|
143
|
+
// incoming pin against a well-formed staged baseline fails CLOSED (treated
|
|
144
|
+
// as older, the same asymmetry Test-BackwardPin uses); an absent incoming
|
|
145
|
+
// or staged pin is permissive — there is no provable baseline to violate.
|
|
146
|
+
export function isIncomingComposeOlder(incomingComposeText, stagedComposeText) {
|
|
147
|
+
const incoming = parsePinnedServerVersion(incomingComposeText);
|
|
148
|
+
const staged = parsePinnedServerVersion(stagedComposeText);
|
|
149
|
+
if (!incoming || !staged) return false;
|
|
150
|
+
const rx = /^v?(\d+)\.(\d+)\.(\d+)/;
|
|
151
|
+
if (!rx.test(incoming) && rx.test(staged)) return true;
|
|
152
|
+
return compareVersions(incoming, staged) === -1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readJsonSafe(filePath) {
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readTextSafe(filePath) {
|
|
164
|
+
try {
|
|
165
|
+
return readFileSync(filePath, "utf8");
|
|
166
|
+
} catch {
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { UpgradeClassification } from "./preflight.d.mts";
|
|
2
|
+
|
|
3
|
+
export const PARITY_ASSETS: string[];
|
|
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
|
+
|
|
23
|
+
// CF20-T5: exported for lib/repair.mjs's reuse — see provision.mjs's comment
|
|
24
|
+
// on the export.
|
|
25
|
+
export function stageParityFiles(
|
|
26
|
+
appDir: string,
|
|
27
|
+
env?: Record<string, string | undefined>,
|
|
28
|
+
platform?: string,
|
|
29
|
+
): void;
|
|
30
|
+
export function stageLiveComposeIfAbsent(appDir: string): boolean;
|
|
31
|
+
|
|
32
|
+
export interface ProvisionOptions {
|
|
33
|
+
appDir: string;
|
|
34
|
+
currentVersion: string;
|
|
35
|
+
collectorFetched?: boolean;
|
|
36
|
+
platform?: string;
|
|
37
|
+
arch?: string;
|
|
38
|
+
vendorDir?: string;
|
|
39
|
+
dockerSpawnSync?: (cmd: string, args?: string[], opts?: unknown) => { status: number | null };
|
|
40
|
+
delegateWindowsInstaller?: (appDir: string) => boolean;
|
|
41
|
+
env?: Record<string, string | undefined>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type DockerState = "missing" | "installed-not-running" | "ready";
|
|
45
|
+
|
|
46
|
+
export interface ProvisionResult {
|
|
47
|
+
classification: UpgradeClassification;
|
|
48
|
+
previousVersion: string | null;
|
|
49
|
+
dockerState: DockerState;
|
|
50
|
+
imagesStaged: boolean;
|
|
51
|
+
collectorStaged: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function provision(opts: ProvisionOptions): Promise<ProvisionResult>;
|