getculpa 1.0.2 → 1.0.3
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/assets/culpa-compose.yml +4 -4
- package/bin/getculpa.js +34 -6
- package/lib/bootstrap.d.mts +4 -0
- package/lib/bootstrap.mjs +25 -1
- package/lib/dispatch.d.mts +12 -0
- package/lib/dispatch.mjs +59 -0
- package/lib/doctor.mjs +17 -2
- package/lib/install-summary.d.mts +1 -1
- package/lib/install-summary.mjs +22 -10
- package/lib/start.mjs +8 -3
- package/lib/uninstall.mjs +45 -6
- package/package.json +1 -1
- package/scripts/install.js +7 -3
package/assets/culpa-compose.yml
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# name is used when present, which is how these pins were verified before
|
|
6
6
|
# publication.
|
|
7
7
|
#
|
|
8
|
-
# ── RELEASE STATE: v1.0.
|
|
8
|
+
# ── RELEASE STATE: v1.0.3 PUBLISHED AND SIGNED (2026-08-20) ─────────
|
|
9
9
|
# The line above is REWRITTEN BY installers/publish.sh at real-publish time —
|
|
10
10
|
# this file has shipped a hand-edited, wrong publication claim twice, so the
|
|
11
11
|
# claim is now mechanical, never prose. While it reads NOT PUBLISHED, the pins
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
# cosign verify \
|
|
27
27
|
# --certificate-identity 'info@myaigi.ai' \
|
|
28
28
|
# --certificate-oidc-issuer 'https://github.com/login/oauth' \
|
|
29
|
-
# ghcr.io/myaigidev/culpa-server:v1.0.
|
|
29
|
+
# ghcr.io/myaigidev/culpa-server:v1.0.3
|
|
30
30
|
# Repeat for culpa-dashboard. Both must report VERIFIED.
|
|
31
31
|
#
|
|
32
32
|
# If a pull fails with an authentication or "denied" error rather than a
|
|
@@ -55,7 +55,7 @@ services:
|
|
|
55
55
|
restart: unless-stopped
|
|
56
56
|
|
|
57
57
|
server:
|
|
58
|
-
image: ghcr.io/myaigidev/culpa-server:v1.0.
|
|
58
|
+
image: ghcr.io/myaigidev/culpa-server:v1.0.3
|
|
59
59
|
container_name: culpa-server
|
|
60
60
|
environment:
|
|
61
61
|
DATABASE_URL: postgres://culpa:culpa@db:5432/culpa
|
|
@@ -127,7 +127,7 @@ services:
|
|
|
127
127
|
# black-surface palette — no server, migration or API change, so the server
|
|
128
128
|
# above deliberately stays at v0.11.0 rather than being re-tagged for a
|
|
129
129
|
# release it has no diff in (D-072: never rebuild a tag that already exists).
|
|
130
|
-
image: ghcr.io/myaigidev/culpa-dashboard:v1.0.
|
|
130
|
+
image: ghcr.io/myaigidev/culpa-dashboard:v1.0.3
|
|
131
131
|
container_name: culpa-dashboard
|
|
132
132
|
environment:
|
|
133
133
|
CULPA_API_BASE: http://server:4545
|
package/bin/getculpa.js
CHANGED
|
@@ -121,21 +121,49 @@ async function runUninstall() {
|
|
|
121
121
|
|
|
122
122
|
async function main(argv) {
|
|
123
123
|
const [cmd] = argv;
|
|
124
|
-
if (cmd === undefined || cmd === "start") return runStart();
|
|
125
|
-
if (cmd === "-h" || cmd === "--help" || cmd === "help") {
|
|
126
|
-
console.log(HELP);
|
|
127
|
-
return process.exit(0);
|
|
128
|
-
}
|
|
129
124
|
if (cmd === "-v" || cmd === "--version") {
|
|
130
125
|
printVersion();
|
|
131
126
|
return process.exit(0);
|
|
132
127
|
}
|
|
128
|
+
// V102 T-V102-04 (M8, D-113): `getculpa uninstall --help` once EXECUTED the
|
|
129
|
+
// uninstall — the help flag was only recognised as argv[0]. All help and
|
|
130
|
+
// uninstall-consent decisions now happen here, BEFORE anything is imported
|
|
131
|
+
// or spawned. Passthrough verbs fall through untouched (the launcher owns
|
|
132
|
+
// their help).
|
|
133
|
+
const { decideDispatch, verbHelp } = await import("../lib/dispatch.mjs");
|
|
134
|
+
const decision = decideDispatch(argv, {
|
|
135
|
+
isTTY: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
136
|
+
});
|
|
137
|
+
if (decision.kind === "help") {
|
|
138
|
+
console.log(decision.verb === null ? HELP : verbHelp(decision.verb));
|
|
139
|
+
return process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
if (decision.kind === "refuse-uninstall") {
|
|
142
|
+
console.error(
|
|
143
|
+
"getculpa: uninstall removes Culpa's containers and network. Run it from a terminal to confirm interactively, or pass --yes to consent from a script. Nothing was changed.",
|
|
144
|
+
);
|
|
145
|
+
return process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
if (decision.kind === "confirm-uninstall") {
|
|
148
|
+
const { askYesNo } = await import("../lib/tty.mjs");
|
|
149
|
+
const consented = await askYesNo(
|
|
150
|
+
"This stops and removes Culpa's containers and network (recorded cost data is kept unless you separately confirm deleting it). Continue? (y/N) ",
|
|
151
|
+
{ isInteractive: true },
|
|
152
|
+
);
|
|
153
|
+
if (!consented) {
|
|
154
|
+
console.log("getculpa: uninstall cancelled. Nothing was changed.");
|
|
155
|
+
return process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
return runUninstall();
|
|
158
|
+
}
|
|
159
|
+
if (decision.kind === "run-uninstall") return runUninstall();
|
|
160
|
+
if (cmd === undefined || cmd === "start") return runStart();
|
|
133
161
|
if (cmd === "stop") return runStop();
|
|
134
162
|
if (cmd === "restart") return runRestart();
|
|
135
163
|
if (cmd === "status") return runStatus();
|
|
136
164
|
if (cmd === "doctor") return runDoctor();
|
|
137
165
|
if (cmd === "repair") return runRepair();
|
|
138
|
-
|
|
166
|
+
// `uninstall` never reaches here — every argv shape is decided above.
|
|
139
167
|
// scan / connect / update / config / anything unrecognized: the launcher
|
|
140
168
|
// owns USAGE and exit-code semantics for its own surface, same as culpa.js.
|
|
141
169
|
return runLauncherPassthrough(argv);
|
package/lib/bootstrap.d.mts
CHANGED
|
@@ -23,3 +23,7 @@ export interface StageInitialPayloadResult {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function stageInitialPayload(opts: StageInitialPayloadOptions): StageInitialPayloadResult;
|
|
26
|
+
|
|
27
|
+
export type PayloadState = "installed" | "staged" | "missing";
|
|
28
|
+
|
|
29
|
+
export function readPayloadState(opts?: { env?: Record<string, string | undefined> }): PayloadState;
|
package/lib/bootstrap.mjs
CHANGED
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
// command. The install never claims more than it did.
|
|
27
27
|
|
|
28
28
|
import { spawnSync as realSpawnSync } from "node:child_process";
|
|
29
|
-
import { existsSync } from "node:fs";
|
|
29
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
30
30
|
import path from "node:path";
|
|
31
|
+
import { getAppDir } from "./paths.mjs";
|
|
31
32
|
|
|
32
33
|
// A real payload is tens of MB over a network Culpa does not control. Long
|
|
33
34
|
// enough not to abort a slow-but-working download; short enough that a hung
|
|
@@ -44,6 +45,29 @@ export function vendoredLauncherPath(packageRoot, platform = process.platform) {
|
|
|
44
45
|
return existsSync(candidate) ? candidate : null;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/** V102 T-V102-15 (M3, D-113): what the launcher's updates/state.json
|
|
49
|
+
* actually says — EVIDENCE for the install summary, never the updater's
|
|
50
|
+
* exit code (which claimed "Culpa CLI: installed" on a Mac whose state read
|
|
51
|
+
* current: null while doctor FAILed the same install).
|
|
52
|
+
* "installed": current is set. "staged": pending only — activates at the
|
|
53
|
+
* next `getculpa`. "missing": neither, no file, or unreadable.
|
|
54
|
+
* NB (review-3): getAppDir honours CULPA_APP_DIR but not CULPA_HOME — the
|
|
55
|
+
* known-open T-CF28-3c asymmetry (see paths.mjs:17-27 and open-loops
|
|
56
|
+
* §T-CF28-3c). A CULPA_HOME-only relocation therefore reads the default
|
|
57
|
+
* location and reports "missing" — underselling, never a false claim. */
|
|
58
|
+
export function readPayloadState(opts = {}) {
|
|
59
|
+
const { env = process.env } = opts;
|
|
60
|
+
try {
|
|
61
|
+
const raw = readFileSync(path.join(getAppDir({ env }), "updates", "state.json"), "utf8");
|
|
62
|
+
const state = JSON.parse(raw);
|
|
63
|
+
if (typeof state.current === "string" && state.current.length > 0) return "installed";
|
|
64
|
+
if (typeof state.pending === "string" && state.pending.length > 0) return "staged";
|
|
65
|
+
return "missing";
|
|
66
|
+
} catch {
|
|
67
|
+
return "missing"; // absent or corrupt: no evidence, no claim
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
export function stageInitialPayload(opts) {
|
|
48
72
|
const { launcherPath, spawnSync = realSpawnSync, log = console.log, env = process.env } = opts;
|
|
49
73
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type DispatchDecision =
|
|
2
|
+
| { kind: "help"; verb: string | null }
|
|
3
|
+
| { kind: "refuse-uninstall" }
|
|
4
|
+
| { kind: "confirm-uninstall" }
|
|
5
|
+
| { kind: "run-uninstall" }
|
|
6
|
+
| { kind: "dispatch" };
|
|
7
|
+
|
|
8
|
+
export const LOCAL_VERBS: string[];
|
|
9
|
+
|
|
10
|
+
export function verbHelp(verb: string): string;
|
|
11
|
+
|
|
12
|
+
export function decideDispatch(argv: string[], opts?: { isTTY?: boolean }): DispatchDecision;
|
package/lib/dispatch.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// V102 T-V102-04 (M8/P0-2, D-113) — dispatch decisions for bin/getculpa.js.
|
|
2
|
+
//
|
|
3
|
+
// Executed on real hardware: `getculpa uninstall --help` stopped and removed
|
|
4
|
+
// all three containers and the network, because the router recognised a help
|
|
5
|
+
// flag only as argv[0] and dispatched on argv[0] without reading argv[1].
|
|
6
|
+
// `stop`, `restart` and `repair` shared the shape. The rule now: for every
|
|
7
|
+
// verb THIS bin routes locally, a help flag anywhere in argv decides HELP
|
|
8
|
+
// before anything is imported or spawned. Passthrough verbs (scan, connect,
|
|
9
|
+
// update, config, …) keep their argv untouched — the vendored launcher owns
|
|
10
|
+
// their help and exit-code semantics.
|
|
11
|
+
//
|
|
12
|
+
// Pure decisions, no side effects: the bin acts on the returned kind, and the
|
|
13
|
+
// facts in tests/npm-cli-dispatch.test.ts assert the decisions directly.
|
|
14
|
+
|
|
15
|
+
const HELP_FLAGS = new Set(["-h", "--help", "help"]);
|
|
16
|
+
|
|
17
|
+
/** Every verb bin/getculpa.js routes locally (everything else passes through). */
|
|
18
|
+
export const LOCAL_VERBS = ["start", "stop", "restart", "status", "doctor", "repair", "uninstall"];
|
|
19
|
+
const LOCAL = new Set(LOCAL_VERBS);
|
|
20
|
+
|
|
21
|
+
const VERB_HELP = {
|
|
22
|
+
start: "Start (or resume) the Culpa stack. Data is kept.",
|
|
23
|
+
stop: "Stop the running stack. Containers stop, nothing is removed, all data is kept. Start again with `getculpa`.",
|
|
24
|
+
restart: "Stop, then start the stack. No data is touched.",
|
|
25
|
+
status: "Show install/docker/stack/license status. Read-only.",
|
|
26
|
+
doctor: "Diagnose this install. Read-only.",
|
|
27
|
+
repair: "Re-stage missing or corrupt install files. No data touched.",
|
|
28
|
+
uninstall:
|
|
29
|
+
"Stop and REMOVE Culpa's containers and network. Asks for confirmation first; pass --yes to consent from a script (a non-interactive run without --yes refuses). Recorded cost data is kept unless you separately confirm deleting it.",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** One verb's help block, prefixed with its usage line. */
|
|
33
|
+
export function verbHelp(verb) {
|
|
34
|
+
const text = VERB_HELP[verb] ?? "No help recorded for this verb.";
|
|
35
|
+
return `Usage: getculpa ${verb}\n\n ${text}\n`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Decide what argv means BEFORE any dispatch. Returns one of:
|
|
40
|
+
* { kind: "help", verb: string | null } print help (general when verb null)
|
|
41
|
+
* { kind: "refuse-uninstall" } non-interactive, no --yes: refuse
|
|
42
|
+
* { kind: "confirm-uninstall" } interactive: ask, then uninstall
|
|
43
|
+
* { kind: "run-uninstall" } consent given via --yes/-y
|
|
44
|
+
* { kind: "dispatch" } fall through to the existing router
|
|
45
|
+
*/
|
|
46
|
+
export function decideDispatch(argv, { isTTY = false } = {}) {
|
|
47
|
+
const [cmd] = argv;
|
|
48
|
+
if (cmd === undefined) return { kind: "dispatch" };
|
|
49
|
+
if (HELP_FLAGS.has(cmd)) return { kind: "help", verb: null };
|
|
50
|
+
if (LOCAL.has(cmd) && argv.slice(1).some((a) => HELP_FLAGS.has(a))) {
|
|
51
|
+
return { kind: "help", verb: cmd };
|
|
52
|
+
}
|
|
53
|
+
if (cmd === "uninstall") {
|
|
54
|
+
if (argv.includes("--yes") || argv.includes("-y")) return { kind: "run-uninstall" };
|
|
55
|
+
if (!isTTY) return { kind: "refuse-uninstall" };
|
|
56
|
+
return { kind: "confirm-uninstall" };
|
|
57
|
+
}
|
|
58
|
+
return { kind: "dispatch" };
|
|
59
|
+
}
|
package/lib/doctor.mjs
CHANGED
|
@@ -244,9 +244,24 @@ async function checkHealth(fetchFn, running) {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
function scanLicenseKeyPresence(env, appDir) {
|
|
247
|
-
|
|
247
|
+
// CodeRabbit PR#19 [6]: " " is truthy — trim before believing the env
|
|
248
|
+
if ((env.CULPA_LICENSE_KEY ?? "").trim() !== "") return true;
|
|
248
249
|
const compose = readTextSafe(path.join(appDir, "docker-compose.yml"));
|
|
249
|
-
|
|
250
|
+
// V102 T-V102-15 (M6/W3, D-113): the old bare /CULPA_LICENSE_KEY/ regex
|
|
251
|
+
// matched a COMMENT line in the shipped compose, so doctor reported
|
|
252
|
+
// "[OK] present (redacted)" with no key set — on real hardware, both
|
|
253
|
+
// platforms, while `status` said "License: unknown" beside it. Only an
|
|
254
|
+
// uncommented env ENTRY carrying a non-empty value counts.
|
|
255
|
+
return compose.split(/\r?\n/).some((line) => {
|
|
256
|
+
const t = line.trim();
|
|
257
|
+
if (t.startsWith("#")) return false;
|
|
258
|
+
const m = /^-?\s*["']?CULPA_LICENSE_KEY["']?\s*[:=]\s*(.+)$/.exec(t);
|
|
259
|
+
if (m === null) return false;
|
|
260
|
+
// PR#19 [6]: "KEY: # configured elsewhere" is YAML for an EMPTY value
|
|
261
|
+
// plus a trailing comment — never a configured key
|
|
262
|
+
const value = m[1].trim().replace(/^["']|["']$/g, "").trim();
|
|
263
|
+
return value.length > 0 && !value.startsWith("#");
|
|
264
|
+
});
|
|
250
265
|
}
|
|
251
266
|
|
|
252
267
|
// Extracts ONLY configured/state/plan — even if the (possibly stubbed, in
|
|
@@ -9,7 +9,7 @@ export interface InstallSummaryOptions {
|
|
|
9
9
|
dockerState: DockerState;
|
|
10
10
|
imagesStaged?: boolean;
|
|
11
11
|
collectorStaged?: boolean;
|
|
12
|
-
|
|
12
|
+
payloadState?: "installed" | "staged" | "missing";
|
|
13
13
|
platform?: string;
|
|
14
14
|
isTTY?: boolean;
|
|
15
15
|
env?: Record<string, string | undefined>;
|
package/lib/install-summary.mjs
CHANGED
|
@@ -46,13 +46,23 @@ function headline(classification, version, previousVersion, dockerReady, colour)
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// V102 T-V102-15 (M3, D-113): the row reports the three REAL states read
|
|
50
|
+
// from updates/state.json (readPayloadState), never the updater's exit code
|
|
51
|
+
// — the shipped 1.0.2 printed "Culpa CLI: installed" on a Mac whose
|
|
52
|
+
// state.json read current: null, while doctor FAILed the same install.
|
|
53
|
+
const PAYLOAD_ROW = {
|
|
54
|
+
installed: "installed",
|
|
55
|
+
staged: "staged - activates at your next `getculpa`",
|
|
56
|
+
missing: "deferred - run `getculpa update` to finish",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function inventory(appDir, imagesStaged, collectorStaged, payloadState) {
|
|
50
60
|
return [
|
|
51
61
|
` Location: ${appDir}`,
|
|
52
62
|
// T-CF28-4b: the CLI payload is the program `getculpa` actually runs. When
|
|
53
63
|
// it is missing the install is not usable at all, so this row names the
|
|
54
64
|
// one command that finishes the job rather than leaving a dead first run.
|
|
55
|
-
` Culpa CLI: ${
|
|
65
|
+
` Culpa CLI: ${PAYLOAD_ROW[payloadState] ?? PAYLOAD_ROW.missing}`,
|
|
56
66
|
` Images: ${imagesStaged ? "staged" : "deferred to first run"}`,
|
|
57
67
|
` Capture: ${collectorStaged ? "collector installed" : "off (collector not installed)"}`,
|
|
58
68
|
];
|
|
@@ -109,27 +119,29 @@ export function buildInstallSummary(opts) {
|
|
|
109
119
|
dockerState,
|
|
110
120
|
imagesStaged = false,
|
|
111
121
|
collectorStaged = false,
|
|
112
|
-
// T-CF28-4b. Fail-CLOSED, matching imagesStaged/
|
|
113
|
-
// caller that cannot say whether the payload is
|
|
114
|
-
// "ready", and this function exists precisely
|
|
115
|
-
// claimed without evidence. scripts/install.js
|
|
116
|
-
|
|
122
|
+
// T-CF28-4b / V102 T-V102-15. Fail-CLOSED, matching imagesStaged/
|
|
123
|
+
// collectorStaged above: a caller that cannot say whether the payload is
|
|
124
|
+
// there has no evidence for "ready", and this function exists precisely
|
|
125
|
+
// to stop readiness being claimed without evidence. scripts/install.js
|
|
126
|
+
// passes the real state read from updates/state.json (readPayloadState).
|
|
127
|
+
payloadState = "missing",
|
|
117
128
|
platform = process.platform,
|
|
118
129
|
isTTY = false,
|
|
119
130
|
env = {},
|
|
120
131
|
} = opts;
|
|
121
132
|
|
|
122
133
|
const dockerReady = dockerState === "ready";
|
|
123
|
-
// "ready" means the next command will work. A
|
|
134
|
+
// "ready" means the next command will work. A MISSING CLI payload breaks
|
|
124
135
|
// that just as completely as a missing Docker, so it gates the word too.
|
|
125
|
-
|
|
136
|
+
// "staged" keeps it: the pending version activates at that very launch.
|
|
137
|
+
const ready = dockerReady && payloadState !== "missing";
|
|
126
138
|
const colour = { isTTY, env };
|
|
127
139
|
const lines = [headline(classification, version, previousVersion, ready, colour)];
|
|
128
140
|
|
|
129
141
|
// A refused downgrade staged nothing — an inventory there would describe a
|
|
130
142
|
// state this run did not produce (lib/provision.mjs:175-183).
|
|
131
143
|
if (classification !== "downgrade-package") {
|
|
132
|
-
lines.push(...inventory(appDir, imagesStaged, collectorStaged,
|
|
144
|
+
lines.push(...inventory(appDir, imagesStaged, collectorStaged, payloadState));
|
|
133
145
|
}
|
|
134
146
|
|
|
135
147
|
if (!dockerReady) {
|
package/lib/start.mjs
CHANGED
|
@@ -415,10 +415,15 @@ async function runPosixStateMachine({ appDir, liveCompose, platform, spawnSync,
|
|
|
415
415
|
};
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
//
|
|
419
|
-
//
|
|
418
|
+
// V102 T-V102-14 (M4, D-113): the old line here said "no collector build
|
|
419
|
+
// ships for macOS/Linux yet" — verified FALSE on real hardware (the arm64
|
|
420
|
+
// collectord ships, was staged executable, install-state recorded
|
|
421
|
+
// collectorStaged: true). What IS true: the collector is not auto-started
|
|
422
|
+
// on this platform yet, while the server-side coding-tool log sweep works
|
|
423
|
+
// (proven live on the Mac: calls landed unattended, ~45 min cadence). Say
|
|
424
|
+
// exactly that — neither direction of the old overclaim.
|
|
420
425
|
stagesRun.push(STAGES.START_CULPA);
|
|
421
|
-
log("getculpa:
|
|
426
|
+
log("getculpa: your local coding-tool usage is captured by the built-in log sweep. The production traffic collector is not started automatically on this platform yet - everything else works normally.");
|
|
422
427
|
|
|
423
428
|
stagesRun.push(STAGES.OPEN_UI);
|
|
424
429
|
if (pinnedVersion) writeFileSync(path.join(appDir, "last-boot-version.txt"), `${pinnedVersion}\n`);
|
package/lib/uninstall.mjs
CHANGED
|
@@ -62,12 +62,36 @@ async function promptPgdataRemoval(createInterface, isTTY) {
|
|
|
62
62
|
if (!isTTY) return false;
|
|
63
63
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
64
64
|
const answer = await new Promise((resolve) => {
|
|
65
|
+
// V102 T-V102-16 (D-113): EOF (Ctrl-D, or a stdin that ends mid-prompt)
|
|
66
|
+
// fires `close` and NEVER invokes question()'s callback — this promise
|
|
67
|
+
// used to hang forever at the one prompt guarding DELETION of the cost
|
|
68
|
+
// database. An unanswered question is a NO, exactly like tty.mjs.
|
|
69
|
+
// Optional-call: older injected fakes without `once` keep working.
|
|
70
|
+
rl.once?.("close", () => resolve(""));
|
|
65
71
|
rl.question("Also DELETE all recorded cost data? This cannot be undone. (y/N) ", resolve);
|
|
66
72
|
});
|
|
67
73
|
rl.close();
|
|
68
74
|
return answer.trim().toLowerCase() === "y";
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
// V102 T-V102-16 (M11/W2, D-113): macOS uninstall exited 0 leaving ~7.3 MB
|
|
78
|
+
// (launcher, collector, compose, register.mjs) silently behind — the Windows
|
|
79
|
+
// ps1 removes the app dir and is "the more complete of the two" (W2). Remove
|
|
80
|
+
// the dir ONLY when it is provably a Culpa install dir (install-state.json,
|
|
81
|
+
// which every install writes); a mistargeted CULPA_APP_DIR is kept and said.
|
|
82
|
+
// Truthful beats a surprise rm -rf in both directions.
|
|
83
|
+
function removeAppDir(appDir) {
|
|
84
|
+
if (!existsSync(appDir)) return;
|
|
85
|
+
if (existsSync(path.join(appDir, "install-state.json"))) {
|
|
86
|
+
rmSync(appDir, { recursive: true, force: true });
|
|
87
|
+
console.log(`Removed: ${appDir} (launcher, collector, compose and capture shims).`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
console.log(
|
|
91
|
+
`Kept: ${appDir} - no install-state.json found there, so it is not provably a Culpa install directory. Remove it yourself if you are sure.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
71
95
|
export async function uninstall(opts = {}) {
|
|
72
96
|
const appDir = opts.appDir ?? getAppDir();
|
|
73
97
|
const platform = opts.platform ?? process.platform;
|
|
@@ -82,13 +106,28 @@ export async function uninstall(opts = {}) {
|
|
|
82
106
|
if (!ok) console.error("getculpa: uninstall did not complete cleanly - see the output above.");
|
|
83
107
|
} else {
|
|
84
108
|
ok = composeDown(appDir, spawnSync);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
// Review-4 IMPORTANT + CodeRabbit PR#19 [7] (Major): a FAILED stop skips
|
|
110
|
+
// EVERY destructive step — the ps1's $stackStopped gate, whose comment
|
|
111
|
+
// records the prior real incident (CR-PR5 round 3). Not even the pgdata
|
|
112
|
+
// PROMPT is offered: after a partial teardown the volume can be
|
|
113
|
+
// unreferenced, so a user confirming deletion here could lose the cost
|
|
114
|
+
// database while being told the stack is still running. Nothing is
|
|
115
|
+
// removed; the one honest instruction is to stop the stack and rerun.
|
|
116
|
+
if (!ok) {
|
|
117
|
+
console.log(
|
|
118
|
+
`Files kept: ${appDir} - the stack is still running (\`docker compose down\` failed above). Nothing was removed. Stop the stack, then run this again.`,
|
|
119
|
+
);
|
|
90
120
|
} else {
|
|
91
|
-
|
|
121
|
+
removeShortcutEquivalents(homedir);
|
|
122
|
+
const wipe = await promptPgdataRemoval(createInterface, isTTY);
|
|
123
|
+
if (wipe) {
|
|
124
|
+
spawnSync("docker", ["volume", "rm", "culpa_pgdata"], { stdio: "inherit" });
|
|
125
|
+
console.log("Data volume removed.");
|
|
126
|
+
} else {
|
|
127
|
+
console.log("Data kept (volume culpa_pgdata). Reinstalling later will find it again.");
|
|
128
|
+
}
|
|
129
|
+
// M11/W2: after the data decision, remove the app dir itself (ps1 parity)
|
|
130
|
+
removeAppDir(appDir);
|
|
92
131
|
}
|
|
93
132
|
}
|
|
94
133
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getculpa",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Culpa CLI: `npm i -g getculpa` provisions the full Culpa install (Windows-installer parity) and leaves it dormant. `getculpa` wakes the stack.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"bin": {
|
package/scripts/install.js
CHANGED
|
@@ -119,11 +119,15 @@ async function main() {
|
|
|
119
119
|
// `getculpa` dead-ends — the Mac tester's exact experience. Best-effort by
|
|
120
120
|
// construction: any failure leaves the payload deferred and the summary
|
|
121
121
|
// below names `getculpa update`, which is where we already were.
|
|
122
|
-
const { stageInitialPayload, vendoredLauncherPath } = await import("../lib/bootstrap.mjs");
|
|
123
|
-
|
|
122
|
+
const { readPayloadState, stageInitialPayload, vendoredLauncherPath } = await import("../lib/bootstrap.mjs");
|
|
123
|
+
stageInitialPayload({
|
|
124
124
|
launcherPath: vendoredLauncherPath(path.join(__dirname, "..")),
|
|
125
125
|
env: process.env,
|
|
126
126
|
});
|
|
127
|
+
// V102 T-V102-15 (M3): the summary reports what updates/state.json actually
|
|
128
|
+
// says, never the updater's exit code — "installed" was claimed on a Mac
|
|
129
|
+
// whose state read current: null while doctor FAILed the same install.
|
|
130
|
+
const payloadState = readPayloadState({ env: process.env });
|
|
127
131
|
|
|
128
132
|
// CF20-T6: the completion line names what actually happened — an upgrade
|
|
129
133
|
// is STAGED only; the new version applies at the next `getculpa`, not here.
|
|
@@ -139,7 +143,7 @@ async function main() {
|
|
|
139
143
|
dockerState,
|
|
140
144
|
imagesStaged,
|
|
141
145
|
collectorStaged,
|
|
142
|
-
|
|
146
|
+
payloadState,
|
|
143
147
|
platform: process.platform,
|
|
144
148
|
isTTY: Boolean(process.stdout.isTTY),
|
|
145
149
|
env: process.env,
|