getculpa 1.0.9 → 1.1.0
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 +43 -43
- package/README.md +9 -2
- package/assets/culpa-collector.ps1 +137 -137
- package/assets/culpa-compose.yml +161 -146
- package/assets/launch-culpa.ps1 +415 -415
- package/bin/culpa.js +22 -22
- package/bin/getculpa.js +186 -175
- package/lib/assets.d.mts +2 -2
- package/lib/bootstrap.d.mts +48 -29
- package/lib/bootstrap.mjs +163 -97
- package/lib/dispatch.d.mts +12 -12
- package/lib/dispatch.mjs +66 -59
- package/lib/docker.d.mts +76 -76
- package/lib/docker.mjs +290 -270
- package/lib/doctor.d.mts +29 -29
- package/lib/doctor.mjs +454 -420
- package/lib/fetch.d.mts +17 -17
- package/lib/fetch.mjs +211 -211
- package/lib/install-summary.d.mts +20 -18
- package/lib/install-summary.mjs +184 -156
- package/lib/paths.d.mts +9 -9
- package/lib/paths.mjs +61 -61
- package/lib/preflight.d.mts +45 -45
- package/lib/preflight.mjs +322 -274
- package/lib/provision.d.mts +54 -54
- package/lib/provision.mjs +47 -5
- package/lib/repair.d.mts +21 -21
- package/lib/repair.mjs +7 -7
- package/lib/start.d.mts +50 -44
- package/lib/start.mjs +575 -487
- package/lib/status.d.mts +23 -23
- package/lib/status.mjs +74 -74
- package/lib/stop.d.mts +15 -15
- package/lib/stop.mjs +46 -46
- package/lib/tty.d.mts +18 -18
- package/lib/tty.mjs +58 -58
- package/package.json +1 -1
- package/scripts/install.js +9 -2
- package/scripts/prepack.js +53 -53
package/lib/bootstrap.mjs
CHANGED
|
@@ -1,97 +1,163 @@
|
|
|
1
|
-
// T-CF28-4b — stage the CLI payload during `npm i -g getculpa`.
|
|
2
|
-
//
|
|
3
|
-
// THE DEFECT. A fresh install fetched the LAUNCHER (lib/fetch.mjs) but never
|
|
4
|
-
// the PAYLOAD the launcher runs. `state.json` was left with `current: null`,
|
|
5
|
-
// so the very first `getculpa` reached
|
|
6
|
-
// native/culpa-launcher/src/main.rs:79-88, found nothing to exec, and printed
|
|
7
|
-
// the no-payload message. The Mac tester had to work out `getculpa update` for
|
|
8
|
-
// themselves and then run a SECOND command before anything happened. An
|
|
9
|
-
// install that leaves nothing runnable is not an install.
|
|
10
|
-
//
|
|
11
|
-
// WHY THIS IS NOT AN UNCONSENTED UPDATE. `culpa update` is an explicit,
|
|
12
|
-
// user-initiated verb and is not consent-gated: consent gates the BACKGROUND
|
|
13
|
-
// check (`cli.rs:421-452`, `run_background_check` `cli.rs:316-318`). The user
|
|
14
|
-
// ran `npm i -g getculpa`; downloading the program they just asked to install
|
|
15
|
-
// is completing that install. The auto-update consent model is untouched, and
|
|
16
|
-
// nothing here records a preference on the user's behalf.
|
|
17
|
-
//
|
|
18
|
-
// WHY IT SHELLS OUT RATHER THAN DOWNLOADING. The launcher already owns a
|
|
19
|
-
// verified path: manifest fetch, sha256 verification, self-test, atomic
|
|
20
|
-
// promotion. A second downloader in Node would be a second trust boundary to
|
|
21
|
-
// audit and keep in step. This calls the one that already exists.
|
|
22
|
-
//
|
|
23
|
-
// EVERY FAILURE IS SOFT. Offline, a stalled mirror, a missing binary — none of
|
|
24
|
-
// them fail the install. They leave the payload deferred, exactly as before
|
|
25
|
-
// this module existed, and lib/install-summary.mjs says so and names the
|
|
26
|
-
// command. The install never claims more than it did.
|
|
27
|
-
|
|
28
|
-
import { spawnSync as realSpawnSync } from "node:child_process";
|
|
29
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
30
|
-
import path from "node:path";
|
|
31
|
-
import { getAppDir } from "./paths.mjs";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
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
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
}
|
|
1
|
+
// T-CF28-4b — stage the CLI payload during `npm i -g getculpa`.
|
|
2
|
+
//
|
|
3
|
+
// THE DEFECT. A fresh install fetched the LAUNCHER (lib/fetch.mjs) but never
|
|
4
|
+
// the PAYLOAD the launcher runs. `state.json` was left with `current: null`,
|
|
5
|
+
// so the very first `getculpa` reached
|
|
6
|
+
// native/culpa-launcher/src/main.rs:79-88, found nothing to exec, and printed
|
|
7
|
+
// the no-payload message. The Mac tester had to work out `getculpa update` for
|
|
8
|
+
// themselves and then run a SECOND command before anything happened. An
|
|
9
|
+
// install that leaves nothing runnable is not an install.
|
|
10
|
+
//
|
|
11
|
+
// WHY THIS IS NOT AN UNCONSENTED UPDATE. `culpa update` is an explicit,
|
|
12
|
+
// user-initiated verb and is not consent-gated: consent gates the BACKGROUND
|
|
13
|
+
// check (`cli.rs:421-452`, `run_background_check` `cli.rs:316-318`). The user
|
|
14
|
+
// ran `npm i -g getculpa`; downloading the program they just asked to install
|
|
15
|
+
// is completing that install. The auto-update consent model is untouched, and
|
|
16
|
+
// nothing here records a preference on the user's behalf.
|
|
17
|
+
//
|
|
18
|
+
// WHY IT SHELLS OUT RATHER THAN DOWNLOADING. The launcher already owns a
|
|
19
|
+
// verified path: manifest fetch, sha256 verification, self-test, atomic
|
|
20
|
+
// promotion. A second downloader in Node would be a second trust boundary to
|
|
21
|
+
// audit and keep in step. This calls the one that already exists.
|
|
22
|
+
//
|
|
23
|
+
// EVERY FAILURE IS SOFT. Offline, a stalled mirror, a missing binary — none of
|
|
24
|
+
// them fail the install. They leave the payload deferred, exactly as before
|
|
25
|
+
// this module existed, and lib/install-summary.mjs says so and names the
|
|
26
|
+
// command. The install never claims more than it did.
|
|
27
|
+
|
|
28
|
+
import { spawnSync as realSpawnSync } from "node:child_process";
|
|
29
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { getAppDir } from "./paths.mjs";
|
|
32
|
+
import { compareVersions } from "./preflight.mjs";
|
|
33
|
+
|
|
34
|
+
// A real payload is tens of MB over a network Culpa does not control. Long
|
|
35
|
+
// enough not to abort a slow-but-working download; short enough that a hung
|
|
36
|
+
// mirror degrades to a clear deferred message instead of hanging `npm install`
|
|
37
|
+
// indefinitely, which is the failure mode that matters — npm gives no progress
|
|
38
|
+
// output of its own here.
|
|
39
|
+
export const PAYLOAD_STAGE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
40
|
+
|
|
41
|
+
// Where lib/fetch.mjs puts the launcher (see stageStubLauncher in
|
|
42
|
+
// scripts/install.js for the same naming: `culpa-launcher` + platform suffix).
|
|
43
|
+
export function vendoredLauncherPath(packageRoot, platform = process.platform) {
|
|
44
|
+
const exe = platform === "win32" ? ".exe" : "";
|
|
45
|
+
const candidate = path.join(packageRoot, "vendor", `culpa-launcher${exe}`);
|
|
46
|
+
return existsSync(candidate) ? candidate : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** V102 T-V102-15 (M3, D-113): what the launcher's updates/state.json
|
|
50
|
+
* actually says — EVIDENCE for the install summary, never the updater's
|
|
51
|
+
* exit code (which claimed "Culpa CLI: installed" on a Mac whose state read
|
|
52
|
+
* current: null while doctor FAILed the same install).
|
|
53
|
+
* "installed": current is set. "staged": pending only — activates at the
|
|
54
|
+
* next `getculpa`. "missing": neither, no file, or unreadable.
|
|
55
|
+
* NB (review-3): getAppDir honours CULPA_APP_DIR but not CULPA_HOME — the
|
|
56
|
+
* known-open T-CF28-3c asymmetry (see paths.mjs:17-27 and open-loops
|
|
57
|
+
* §T-CF28-3c). A CULPA_HOME-only relocation therefore reads the default
|
|
58
|
+
* location and reports "missing" — underselling, never a false claim. */
|
|
59
|
+
export function readPayloadState(opts = {}) {
|
|
60
|
+
return readPayloadDetail(opts).state;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** T-V1010-1 (ISS-V1010-1): the state AND the versions it was read from.
|
|
64
|
+
* With `expectedVersion` (the npm package's own version) the answer is
|
|
65
|
+
* VERSION-AWARE — a fresh `npm i -g getculpa@1.0.10` on a machine whose
|
|
66
|
+
* versions/ held 1.0.4 used to read "installed" because `current` was merely
|
|
67
|
+
* non-null, and the summary said so while the one download had failed.
|
|
68
|
+
* "installed": current is at, or AHEAD of, expected (the launcher only ever
|
|
69
|
+
* moves forward — cli.rs run_manual_update — so ahead is never
|
|
70
|
+
* a downgrade and never "behind");
|
|
71
|
+
* "staged": pending is at or ahead of expected (activates at launch);
|
|
72
|
+
* "behind": something is present but the newest of current/pending is
|
|
73
|
+
* older than expected — the download did not arrive;
|
|
74
|
+
* "missing": nothing present, or no/corrupt file.
|
|
75
|
+
* Without `expectedVersion` the three-state answer is unchanged (callers that
|
|
76
|
+
* do not know the package version). Unparsable version strings compare as
|
|
77
|
+
* equal (compareVersions fail-open, the launch-culpa.ps1 convention). */
|
|
78
|
+
export function readPayloadDetail(opts = {}) {
|
|
79
|
+
const { env = process.env, expectedVersion } = opts;
|
|
80
|
+
let current = null;
|
|
81
|
+
let pending = null;
|
|
82
|
+
try {
|
|
83
|
+
const raw = readFileSync(path.join(getAppDir({ env }), "updates", "state.json"), "utf8");
|
|
84
|
+
const state = JSON.parse(raw);
|
|
85
|
+
if (typeof state.current === "string" && state.current.length > 0) current = state.current;
|
|
86
|
+
if (typeof state.pending === "string" && state.pending.length > 0) pending = state.pending;
|
|
87
|
+
} catch {
|
|
88
|
+
return { state: "missing", current: null, pending: null }; // absent or corrupt: no evidence, no claim
|
|
89
|
+
}
|
|
90
|
+
if (current === null && pending === null) return { state: "missing", current, pending };
|
|
91
|
+
if (typeof expectedVersion !== "string" || expectedVersion.length === 0) {
|
|
92
|
+
return { state: current !== null ? "installed" : "staged", current, pending };
|
|
93
|
+
}
|
|
94
|
+
if (current !== null && compareVersions(current, expectedVersion) >= 0) return { state: "installed", current, pending };
|
|
95
|
+
if (pending !== null && compareVersions(pending, expectedVersion) >= 0) return { state: "staged", current, pending };
|
|
96
|
+
return { state: "behind", current, pending };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function stageInitialPayload(opts) {
|
|
100
|
+
const {
|
|
101
|
+
launcherPath,
|
|
102
|
+
spawnSync = realSpawnSync,
|
|
103
|
+
log = console.log,
|
|
104
|
+
env = process.env,
|
|
105
|
+
// T-V1010-1: the npm package's own version. When given, the result is
|
|
106
|
+
// judged by what updates/state.json says AFTERWARDS, not by the exit code,
|
|
107
|
+
// and a download that did not bring this version is retried ONCE (the
|
|
108
|
+
// 1.0.10 install's single run failed as "offline" on an online machine).
|
|
109
|
+
// When omitted the function behaves exactly as before: one spawn, the
|
|
110
|
+
// exit code decides.
|
|
111
|
+
expectedVersion,
|
|
112
|
+
readDetail = readPayloadDetail,
|
|
113
|
+
} = opts;
|
|
114
|
+
|
|
115
|
+
// No launcher means the fetch above already failed and said so; there is
|
|
116
|
+
// nothing to invoke and nothing further worth reporting here.
|
|
117
|
+
if (!launcherPath) return { staged: false, reason: "no-launcher" };
|
|
118
|
+
|
|
119
|
+
const runUpdate = () =>
|
|
120
|
+
spawnSync(launcherPath, ["update"], {
|
|
121
|
+
stdio: "inherit",
|
|
122
|
+
timeout: PAYLOAD_STAGE_TIMEOUT_MS,
|
|
123
|
+
// Inherit the install's own environment so CULPA_HOME / CULPA_APP_DIR
|
|
124
|
+
// resolve to the SAME root scripts/install.js provisioned into (D-108
|
|
125
|
+
// unified that chain across the launcher and the Node CLI).
|
|
126
|
+
env,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
log("getculpa: downloading the Culpa CLI (this is the program `getculpa` runs).");
|
|
130
|
+
const result = runUpdate();
|
|
131
|
+
|
|
132
|
+
const versionAware = typeof expectedVersion === "string" && expectedVersion.length > 0;
|
|
133
|
+
if (!versionAware) {
|
|
134
|
+
if (result.error) {
|
|
135
|
+
log(`getculpa: could not download the Culpa CLI (${result.error.message}) - run \`getculpa update\` to finish.`);
|
|
136
|
+
return { staged: false, reason: "spawn-failed" };
|
|
137
|
+
}
|
|
138
|
+
if (result.status !== 0) {
|
|
139
|
+
log("getculpa: could not download the Culpa CLI - run `getculpa update` to finish.");
|
|
140
|
+
return { staged: false, reason: "update-failed" };
|
|
141
|
+
}
|
|
142
|
+
return { staged: true, reason: "staged" };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const arrived = (d) => d.state === "installed" || d.state === "staged";
|
|
146
|
+
let detail = readDetail({ env, expectedVersion });
|
|
147
|
+
if (!arrived(detail)) {
|
|
148
|
+
// One more try: a transient transport failure is classed "offline" by the
|
|
149
|
+
// launcher and a second attempt seconds later routinely succeeds. Bounded
|
|
150
|
+
// the same way; still soft. Known gap: a first run killed by the timeout
|
|
151
|
+
// can leave the launcher's own update lock in place, in which case this
|
|
152
|
+
// retry is a no-op — the summary still reports the real state.
|
|
153
|
+
log(`getculpa: the Culpa CLI v${expectedVersion} did not arrive - trying the download once more.`);
|
|
154
|
+
const again = runUpdate();
|
|
155
|
+
detail = readDetail({ env, expectedVersion });
|
|
156
|
+
if (!arrived(detail)) {
|
|
157
|
+
const why = again.error ? ` (${again.error.message})` : "";
|
|
158
|
+
log(`getculpa: could not download the Culpa CLI v${expectedVersion}${why} - run \`getculpa\` (it downloads it) or \`getculpa update\` to finish.`);
|
|
159
|
+
return { staged: false, reason: again.error ? "spawn-failed" : "update-failed", detail };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { staged: true, reason: "staged", detail };
|
|
163
|
+
}
|
package/lib/dispatch.d.mts
CHANGED
|
@@ -1,12 +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;
|
|
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
CHANGED
|
@@ -1,59 +1,66 @@
|
|
|
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
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
/** T-182.8 (ISS-MAC-11): the environment for a passthrough spawn — the
|
|
33
|
+
* vendored launcher and the culpa CLI spell their usage in the invoked name
|
|
34
|
+
* when CULPA_ARGV0 says `getculpa` (an allowlist on their side). Pure. */
|
|
35
|
+
export function passthroughEnv(base) {
|
|
36
|
+
return { ...base, CULPA_ARGV0: "getculpa" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One verb's help block, prefixed with its usage line. */
|
|
40
|
+
export function verbHelp(verb) {
|
|
41
|
+
const text = VERB_HELP[verb] ?? "No help recorded for this verb.";
|
|
42
|
+
return `Usage: getculpa ${verb}\n\n ${text}\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decide what argv means BEFORE any dispatch. Returns one of:
|
|
47
|
+
* { kind: "help", verb: string | null } print help (general when verb null)
|
|
48
|
+
* { kind: "refuse-uninstall" } non-interactive, no --yes: refuse
|
|
49
|
+
* { kind: "confirm-uninstall" } interactive: ask, then uninstall
|
|
50
|
+
* { kind: "run-uninstall" } consent given via --yes/-y
|
|
51
|
+
* { kind: "dispatch" } fall through to the existing router
|
|
52
|
+
*/
|
|
53
|
+
export function decideDispatch(argv, { isTTY = false } = {}) {
|
|
54
|
+
const [cmd] = argv;
|
|
55
|
+
if (cmd === undefined) return { kind: "dispatch" };
|
|
56
|
+
if (HELP_FLAGS.has(cmd)) return { kind: "help", verb: null };
|
|
57
|
+
if (LOCAL.has(cmd) && argv.slice(1).some((a) => HELP_FLAGS.has(a))) {
|
|
58
|
+
return { kind: "help", verb: cmd };
|
|
59
|
+
}
|
|
60
|
+
if (cmd === "uninstall") {
|
|
61
|
+
if (argv.includes("--yes") || argv.includes("-y")) return { kind: "run-uninstall" };
|
|
62
|
+
if (!isTTY) return { kind: "refuse-uninstall" };
|
|
63
|
+
return { kind: "confirm-uninstall" };
|
|
64
|
+
}
|
|
65
|
+
return { kind: "dispatch" };
|
|
66
|
+
}
|
package/lib/docker.d.mts
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
export type SpawnSyncLike = (
|
|
2
|
-
cmd: string,
|
|
3
|
-
args?: string[],
|
|
4
|
-
opts?: unknown,
|
|
5
|
-
) => { status: number | null; stdout?: string; stderr?: string };
|
|
6
|
-
|
|
7
|
-
export const PROBE_TIMEOUT_MS: number;
|
|
8
|
-
|
|
9
|
-
export function pollUntil(
|
|
10
|
-
probe: () => boolean | Promise<boolean>,
|
|
11
|
-
opts: { maxWaitMs: number; intervalMs: number; sleepFn?: (ms: number) => Promise<void> },
|
|
12
|
-
): Promise<boolean>;
|
|
13
|
-
|
|
14
|
-
export function checkDockerEngineReachable(spawnSync?: SpawnSyncLike): boolean;
|
|
15
|
-
|
|
16
|
-
export function waitForDockerEngine(opts?: {
|
|
17
|
-
spawnSync?: SpawnSyncLike;
|
|
18
|
-
maxWaitMs?: number;
|
|
19
|
-
intervalMs?: number;
|
|
20
|
-
sleepFn?: (ms: number) => Promise<void>;
|
|
21
|
-
}): Promise<boolean>;
|
|
22
|
-
|
|
23
|
-
export type DarwinDockerInstallOutcome = "installed" | "no-brew" | `failed:${string}`;
|
|
24
|
-
|
|
25
|
-
export function installDockerDarwin(spawnSync?: SpawnSyncLike): DarwinDockerInstallOutcome;
|
|
26
|
-
|
|
27
|
-
export function startEngineIfPossible(
|
|
28
|
-
platform: string,
|
|
29
|
-
spawnSync?: SpawnSyncLike,
|
|
30
|
-
log?: (msg: string) => void,
|
|
31
|
-
): { attempted: boolean; ok: boolean };
|
|
32
|
-
|
|
33
|
-
export function isProjectRunning(spawnSync?: SpawnSyncLike): boolean;
|
|
34
|
-
|
|
35
|
-
export type FetchLike = typeof fetch;
|
|
36
|
-
|
|
37
|
-
export function probeHealth(url: string, opts?: { fetchFn?: FetchLike }): Promise<boolean>;
|
|
38
|
-
|
|
39
|
-
export function waitForHealth(
|
|
40
|
-
url: string,
|
|
41
|
-
opts?: { maxWaitMs?: number; intervalMs?: number; sleepFn?: (ms: number) => Promise<void>; fetchFn?: FetchLike },
|
|
42
|
-
): Promise<boolean>;
|
|
43
|
-
|
|
44
|
-
export function dbContainerExists(spawnSync?: SpawnSyncLike): boolean;
|
|
45
|
-
|
|
46
|
-
export function waitForPgReady(opts?: {
|
|
47
|
-
spawnSync?: SpawnSyncLike;
|
|
48
|
-
maxWaitMs?: number;
|
|
49
|
-
intervalMs?: number;
|
|
50
|
-
sleepFn?: (ms: number) => Promise<void>;
|
|
51
|
-
}): Promise<boolean>;
|
|
52
|
-
|
|
53
|
-
export function resolveBackupBaseline(recorded: string | null | undefined, spawnSync?: SpawnSyncLike): string;
|
|
54
|
-
|
|
55
|
-
export function isBackupNeeded(
|
|
56
|
-
dbExists: boolean,
|
|
57
|
-
pinnedVersion: string | null | undefined,
|
|
58
|
-
baseline: string | null | undefined,
|
|
59
|
-
): boolean;
|
|
60
|
-
|
|
61
|
-
export function preMigrationBackup(opts: {
|
|
62
|
-
backupDir: string;
|
|
63
|
-
tag: string;
|
|
64
|
-
spawnSync?: SpawnSyncLike;
|
|
65
|
-
now?: Date;
|
|
66
|
-
}): string;
|
|
67
|
-
|
|
68
|
-
export interface PortOwner {
|
|
69
|
-
pid: number;
|
|
70
|
-
name: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function findPortOwner(
|
|
74
|
-
port: number,
|
|
75
|
-
opts?: { platform?: string; spawnSync?: SpawnSyncLike },
|
|
76
|
-
): PortOwner | null;
|
|
1
|
+
export type SpawnSyncLike = (
|
|
2
|
+
cmd: string,
|
|
3
|
+
args?: string[],
|
|
4
|
+
opts?: unknown,
|
|
5
|
+
) => { status: number | null; stdout?: string; stderr?: string };
|
|
6
|
+
|
|
7
|
+
export const PROBE_TIMEOUT_MS: number;
|
|
8
|
+
|
|
9
|
+
export function pollUntil(
|
|
10
|
+
probe: () => boolean | Promise<boolean>,
|
|
11
|
+
opts: { maxWaitMs: number; intervalMs: number; sleepFn?: (ms: number) => Promise<void> },
|
|
12
|
+
): Promise<boolean>;
|
|
13
|
+
|
|
14
|
+
export function checkDockerEngineReachable(spawnSync?: SpawnSyncLike): boolean;
|
|
15
|
+
|
|
16
|
+
export function waitForDockerEngine(opts?: {
|
|
17
|
+
spawnSync?: SpawnSyncLike;
|
|
18
|
+
maxWaitMs?: number;
|
|
19
|
+
intervalMs?: number;
|
|
20
|
+
sleepFn?: (ms: number) => Promise<void>;
|
|
21
|
+
}): Promise<boolean>;
|
|
22
|
+
|
|
23
|
+
export type DarwinDockerInstallOutcome = "installed" | "no-brew" | `failed:${string}`;
|
|
24
|
+
|
|
25
|
+
export function installDockerDarwin(spawnSync?: SpawnSyncLike): DarwinDockerInstallOutcome;
|
|
26
|
+
|
|
27
|
+
export function startEngineIfPossible(
|
|
28
|
+
platform: string,
|
|
29
|
+
spawnSync?: SpawnSyncLike,
|
|
30
|
+
log?: (msg: string) => void,
|
|
31
|
+
): { attempted: boolean; ok: boolean };
|
|
32
|
+
|
|
33
|
+
export function isProjectRunning(spawnSync?: SpawnSyncLike): boolean;
|
|
34
|
+
|
|
35
|
+
export type FetchLike = typeof fetch;
|
|
36
|
+
|
|
37
|
+
export function probeHealth(url: string, opts?: { fetchFn?: FetchLike }): Promise<boolean>;
|
|
38
|
+
|
|
39
|
+
export function waitForHealth(
|
|
40
|
+
url: string,
|
|
41
|
+
opts?: { maxWaitMs?: number; intervalMs?: number; sleepFn?: (ms: number) => Promise<void>; fetchFn?: FetchLike },
|
|
42
|
+
): Promise<boolean>;
|
|
43
|
+
|
|
44
|
+
export function dbContainerExists(spawnSync?: SpawnSyncLike): boolean;
|
|
45
|
+
|
|
46
|
+
export function waitForPgReady(opts?: {
|
|
47
|
+
spawnSync?: SpawnSyncLike;
|
|
48
|
+
maxWaitMs?: number;
|
|
49
|
+
intervalMs?: number;
|
|
50
|
+
sleepFn?: (ms: number) => Promise<void>;
|
|
51
|
+
}): Promise<boolean>;
|
|
52
|
+
|
|
53
|
+
export function resolveBackupBaseline(recorded: string | null | undefined, spawnSync?: SpawnSyncLike): string;
|
|
54
|
+
|
|
55
|
+
export function isBackupNeeded(
|
|
56
|
+
dbExists: boolean,
|
|
57
|
+
pinnedVersion: string | null | undefined,
|
|
58
|
+
baseline: string | null | undefined,
|
|
59
|
+
): boolean;
|
|
60
|
+
|
|
61
|
+
export function preMigrationBackup(opts: {
|
|
62
|
+
backupDir: string;
|
|
63
|
+
tag: string;
|
|
64
|
+
spawnSync?: SpawnSyncLike;
|
|
65
|
+
now?: Date;
|
|
66
|
+
}): string;
|
|
67
|
+
|
|
68
|
+
export interface PortOwner {
|
|
69
|
+
pid: number;
|
|
70
|
+
name: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function findPortOwner(
|
|
74
|
+
port: number,
|
|
75
|
+
opts?: { platform?: string; spawnSync?: SpawnSyncLike },
|
|
76
|
+
): PortOwner | null;
|