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/bin/culpa.js CHANGED
@@ -1,22 +1,22 @@
1
- #!/usr/bin/env node
2
- // Thin shim: exec the vendored culpa launcher (installed by
3
- // scripts/install.js at postinstall), forwarding argv/stdio/exit code. The
4
- // launcher owns everything from here — versions/, updates, the payload.
5
-
6
- "use strict";
7
-
8
- const { spawnSync } = require("node:child_process");
9
- const fs = require("node:fs");
10
- const path = require("node:path");
11
-
12
- const exe = process.platform === "win32" ? ".exe" : "";
13
- const launcher = path.join(__dirname, "..", "vendor", `culpa-launcher${exe}`);
14
-
15
- if (!fs.existsSync(launcher)) {
16
- console.error("culpa: the launcher is not installed (postinstall failed or was skipped).");
17
- console.error("culpa: run `npm rebuild getculpa` (or reinstall) to fetch it.");
18
- process.exit(1);
19
- }
20
-
21
- const result = spawnSync(launcher, process.argv.slice(2), { stdio: "inherit" });
22
- process.exit(result.status === null ? 1 : result.status);
1
+ #!/usr/bin/env node
2
+ // Thin shim: exec the vendored culpa launcher (installed by
3
+ // scripts/install.js at postinstall), forwarding argv/stdio/exit code. The
4
+ // launcher owns everything from here — versions/, updates, the payload.
5
+
6
+ "use strict";
7
+
8
+ const { spawnSync } = require("node:child_process");
9
+ const fs = require("node:fs");
10
+ const path = require("node:path");
11
+
12
+ const exe = process.platform === "win32" ? ".exe" : "";
13
+ const launcher = path.join(__dirname, "..", "vendor", `culpa-launcher${exe}`);
14
+
15
+ if (!fs.existsSync(launcher)) {
16
+ console.error("culpa: the launcher is not installed (postinstall failed or was skipped).");
17
+ console.error("culpa: run `npm rebuild getculpa` (or reinstall) to fetch it.");
18
+ process.exit(1);
19
+ }
20
+
21
+ const result = spawnSync(launcher, process.argv.slice(2), { stdio: "inherit" });
22
+ process.exit(result.status === null ? 1 : result.status);
package/bin/getculpa.js CHANGED
@@ -1,175 +1,186 @@
1
- #!/usr/bin/env node
2
- // CF20-T3 — `getculpa` command router.
3
- //
4
- // getculpa | getculpa start -> wake the stack (CF20-T4)
5
- // getculpa doctor / repair -> diagnostics + self-heal (CF20-T5)
6
- // getculpa uninstall -> lib/uninstall.mjs
7
- // getculpa --help / --version -> local, no launcher needed
8
- // anything else (scan, connect, update, config, ...) -> passthrough to the
9
- // vendored launcher, exactly like bin/culpa.js does today.
10
-
11
- "use strict";
12
-
13
- const { spawnSync } = require("node:child_process");
14
- const fs = require("node:fs");
15
- const path = require("node:path");
16
-
17
- const HELP = `getculpa - Culpa, LLM spend forensics and forecasting, local-first
18
-
19
- Usage:
20
- getculpa Start (or resume) the Culpa stack
21
- getculpa start Same as above
22
- getculpa stop Stop the running stack (data is kept)
23
- getculpa restart Stop, then start
24
- getculpa status Show install/docker/stack/license status
25
- getculpa doctor Diagnose this install
26
- getculpa repair Re-stage missing/corrupt install files (no data touched)
27
- getculpa uninstall Remove Culpa (data is kept unless you confirm)
28
- getculpa scan Zero-install local scan (no Docker, no network)
29
- getculpa connect Print the topology-appropriate capture recipe
30
- getculpa update Manage CLI launcher updates
31
- getculpa --help Show this help
32
- getculpa --version Show the getculpa package version
33
- `;
34
-
35
- function printVersion() {
36
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
37
- console.log(pkg.version);
38
- }
39
-
40
- function runLauncherPassthrough(args) {
41
- const exe = process.platform === "win32" ? ".exe" : "";
42
- const launcher = path.join(__dirname, "..", "vendor", `culpa-launcher${exe}`);
43
- if (!fs.existsSync(launcher)) {
44
- console.error("getculpa: the launcher is not installed (postinstall failed or was skipped).");
45
- console.error("getculpa: run `npm rebuild getculpa` (or reinstall) to fetch it.");
46
- process.exit(1);
47
- }
48
- const result = spawnSync(launcher, args, { stdio: "inherit" });
49
- process.exit(result.status === null ? 1 : result.status);
50
- }
51
-
52
- function currentVersion() {
53
- return JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version;
54
- }
55
-
56
- function printResult(result) {
57
- if (result.ok) return;
58
- console.error("");
59
- console.error(`Culpa couldn't start.`);
60
- console.error(result.message ?? "(no further detail)");
61
- if (result.recovery) console.error(result.recovery);
62
- }
63
-
64
- async function runStart() {
65
- const { start } = await import("../lib/start.mjs");
66
- const { getAppDir } = await import("../lib/paths.mjs");
67
- const result = await start({ appDir: getAppDir(), currentVersion: currentVersion() });
68
- if (!result.ok) {
69
- printResult(result);
70
- process.exit(1);
71
- }
72
- process.exit(0);
73
- }
74
-
75
- async function runStop() {
76
- const { stop } = await import("../lib/stop.mjs");
77
- const result = await stop();
78
- process.exit(result.ok ? 0 : 1);
79
- }
80
-
81
- async function runRestart() {
82
- const { stop } = await import("../lib/stop.mjs");
83
- const { start } = await import("../lib/start.mjs");
84
- const { getAppDir } = await import("../lib/paths.mjs");
85
- await stop();
86
- const result = await start({ appDir: getAppDir(), currentVersion: currentVersion() });
87
- if (!result.ok) {
88
- printResult(result);
89
- process.exit(1);
90
- }
91
- process.exit(0);
92
- }
93
-
94
- async function runStatus() {
95
- const { status, formatStatus } = await import("../lib/status.mjs");
96
- const result = await status();
97
- console.log(formatStatus(result));
98
- process.exit(0);
99
- }
100
-
101
- async function runDoctor() {
102
- const { doctor, formatDoctorReport } = await import("../lib/doctor.mjs");
103
- const { getAppDir } = await import("../lib/paths.mjs");
104
- const result = await doctor({ appDir: getAppDir(), currentVersion: currentVersion() });
105
- console.log(formatDoctorReport(result));
106
- process.exit(result.ok ? 0 : 1);
107
- }
108
-
109
- async function runRepair() {
110
- const { repair } = await import("../lib/repair.mjs");
111
- const { getAppDir } = await import("../lib/paths.mjs");
112
- const result = await repair({ appDir: getAppDir(), currentVersion: currentVersion() });
113
- process.exit(result.ok ? 0 : 1);
114
- }
115
-
116
- async function runUninstall() {
117
- const { uninstall } = await import("../lib/uninstall.mjs");
118
- const result = await uninstall();
119
- process.exit(result.ok ? 0 : 1);
120
- }
121
-
122
- async function main(argv) {
123
- const [cmd] = argv;
124
- if (cmd === "-v" || cmd === "--version") {
125
- printVersion();
126
- return process.exit(0);
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();
161
- if (cmd === "stop") return runStop();
162
- if (cmd === "restart") return runRestart();
163
- if (cmd === "status") return runStatus();
164
- if (cmd === "doctor") return runDoctor();
165
- if (cmd === "repair") return runRepair();
166
- // `uninstall` never reaches here — every argv shape is decided above.
167
- // scan / connect / update / config / anything unrecognized: the launcher
168
- // owns USAGE and exit-code semantics for its own surface, same as culpa.js.
169
- return runLauncherPassthrough(argv);
170
- }
171
-
172
- main(process.argv.slice(2)).catch((e) => {
173
- console.error(`getculpa: ${e.message}`);
174
- process.exit(1);
175
- });
1
+ #!/usr/bin/env node
2
+ // CF20-T3 — `getculpa` command router.
3
+ //
4
+ // getculpa | getculpa start -> wake the stack (CF20-T4)
5
+ // getculpa doctor / repair -> diagnostics + self-heal (CF20-T5)
6
+ // getculpa uninstall -> lib/uninstall.mjs
7
+ // getculpa --help / --version -> local, no launcher needed
8
+ // anything else (scan, connect, update, config, ...) -> passthrough to the
9
+ // vendored launcher, exactly like bin/culpa.js does today.
10
+
11
+ "use strict";
12
+
13
+ const { spawnSync } = require("node:child_process");
14
+ const fs = require("node:fs");
15
+ const path = require("node:path");
16
+
17
+ const HELP = `getculpa - Culpa, LLM spend forensics and forecasting, local-first
18
+
19
+ Usage:
20
+ getculpa Start (or resume) the Culpa stack
21
+ getculpa start Same as above
22
+ getculpa stop Stop the running stack (data is kept)
23
+ getculpa restart Stop, then start
24
+ getculpa status Show install/docker/stack/license status
25
+ getculpa doctor Diagnose this install
26
+ getculpa repair Re-stage missing/corrupt install files (no data touched)
27
+ getculpa uninstall Remove Culpa (data is kept unless you confirm)
28
+ getculpa scan Refresh the Culpa ledger (needs the running stack);
29
+ --preview reads local logs offline: no Docker, no network
30
+ getculpa connect Print the topology-appropriate capture recipe
31
+ getculpa update Manage CLI launcher updates
32
+ getculpa <verb> --help scan / connect / update / config: the culpa CLI's own help, under this name
33
+ getculpa --help Show this help
34
+ getculpa --version Show the getculpa package version
35
+ `;
36
+
37
+ function printVersion() {
38
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
39
+ console.log(pkg.version);
40
+ }
41
+
42
+ function runLauncherPassthrough(args, env) {
43
+ const exe = process.platform === "win32" ? ".exe" : "";
44
+ const launcher = path.join(__dirname, "..", "vendor", `culpa-launcher${exe}`);
45
+ if (!fs.existsSync(launcher)) {
46
+ console.error("getculpa: the launcher is not installed (postinstall failed or was skipped).");
47
+ console.error("getculpa: run `npm rebuild getculpa` (or reinstall) to fetch it.");
48
+ process.exit(1);
49
+ }
50
+ // T-182.8 (ISS-MAC-11): the launcher and the CLI spell their usage in
51
+ // the invoked name — the environment carries it (lib/dispatch.mjs)
52
+ const result = spawnSync(launcher, args, { stdio: "inherit", env });
53
+ process.exit(result.status === null ? 1 : result.status);
54
+ }
55
+
56
+ function currentVersion() {
57
+ return JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version;
58
+ }
59
+
60
+ function printResult(result) {
61
+ if (result.ok) return;
62
+ console.error("");
63
+ console.error(`Culpa couldn't start.`);
64
+ console.error(result.message ?? "(no further detail)");
65
+ if (result.recovery) console.error(result.recovery);
66
+ }
67
+
68
+ // T-V1010-1 Task B: the vendored launcher, so start() can fetch the CLI
69
+ // payload that matches this package before the stack comes up.
70
+ async function launcherPath() {
71
+ const { vendoredLauncherPath } = await import("../lib/bootstrap.mjs");
72
+ return vendoredLauncherPath(path.join(__dirname, ".."));
73
+ }
74
+
75
+ async function runStart() {
76
+ const { start } = await import("../lib/start.mjs");
77
+ const { getAppDir } = await import("../lib/paths.mjs");
78
+ const result = await start({ appDir: getAppDir(), currentVersion: currentVersion(), launcherPath: await launcherPath() });
79
+ if (!result.ok) {
80
+ printResult(result);
81
+ process.exit(1);
82
+ }
83
+ process.exit(0);
84
+ }
85
+
86
+ async function runStop() {
87
+ const { stop } = await import("../lib/stop.mjs");
88
+ const result = await stop();
89
+ process.exit(result.ok ? 0 : 1);
90
+ }
91
+
92
+ async function runRestart() {
93
+ const { stop } = await import("../lib/stop.mjs");
94
+ const { start } = await import("../lib/start.mjs");
95
+ const { getAppDir } = await import("../lib/paths.mjs");
96
+ await stop();
97
+ const result = await start({ appDir: getAppDir(), currentVersion: currentVersion(), launcherPath: await launcherPath() });
98
+ if (!result.ok) {
99
+ printResult(result);
100
+ process.exit(1);
101
+ }
102
+ process.exit(0);
103
+ }
104
+
105
+ async function runStatus() {
106
+ const { status, formatStatus } = await import("../lib/status.mjs");
107
+ const result = await status();
108
+ console.log(formatStatus(result));
109
+ process.exit(0);
110
+ }
111
+
112
+ async function runDoctor() {
113
+ const { doctor, formatDoctorReport } = await import("../lib/doctor.mjs");
114
+ const { getAppDir } = await import("../lib/paths.mjs");
115
+ const result = await doctor({ appDir: getAppDir(), currentVersion: currentVersion() });
116
+ console.log(formatDoctorReport(result));
117
+ process.exit(result.ok ? 0 : 1);
118
+ }
119
+
120
+ async function runRepair() {
121
+ const { repair } = await import("../lib/repair.mjs");
122
+ const { getAppDir } = await import("../lib/paths.mjs");
123
+ const result = await repair({ appDir: getAppDir(), currentVersion: currentVersion() });
124
+ process.exit(result.ok ? 0 : 1);
125
+ }
126
+
127
+ async function runUninstall() {
128
+ const { uninstall } = await import("../lib/uninstall.mjs");
129
+ const result = await uninstall();
130
+ process.exit(result.ok ? 0 : 1);
131
+ }
132
+
133
+ async function main(argv) {
134
+ const [cmd] = argv;
135
+ if (cmd === "-v" || cmd === "--version") {
136
+ printVersion();
137
+ return process.exit(0);
138
+ }
139
+ // V102 T-V102-04 (M8, D-113): `getculpa uninstall --help` once EXECUTED the
140
+ // uninstall — the help flag was only recognised as argv[0]. All help and
141
+ // uninstall-consent decisions now happen here, BEFORE anything is imported
142
+ // or spawned. Passthrough verbs fall through untouched (the launcher owns
143
+ // their help).
144
+ const { decideDispatch, verbHelp, passthroughEnv } = await import("../lib/dispatch.mjs");
145
+ const decision = decideDispatch(argv, {
146
+ isTTY: process.stdin.isTTY === true && process.stdout.isTTY === true,
147
+ });
148
+ if (decision.kind === "help") {
149
+ console.log(decision.verb === null ? HELP : verbHelp(decision.verb));
150
+ return process.exit(0);
151
+ }
152
+ if (decision.kind === "refuse-uninstall") {
153
+ console.error(
154
+ "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.",
155
+ );
156
+ return process.exit(1);
157
+ }
158
+ if (decision.kind === "confirm-uninstall") {
159
+ const { askYesNo } = await import("../lib/tty.mjs");
160
+ const consented = await askYesNo(
161
+ "This stops and removes Culpa's containers and network (recorded cost data is kept unless you separately confirm deleting it). Continue? (y/N) ",
162
+ { isInteractive: true },
163
+ );
164
+ if (!consented) {
165
+ console.log("getculpa: uninstall cancelled. Nothing was changed.");
166
+ return process.exit(0);
167
+ }
168
+ return runUninstall();
169
+ }
170
+ if (decision.kind === "run-uninstall") return runUninstall();
171
+ if (cmd === undefined || cmd === "start") return runStart();
172
+ if (cmd === "stop") return runStop();
173
+ if (cmd === "restart") return runRestart();
174
+ if (cmd === "status") return runStatus();
175
+ if (cmd === "doctor") return runDoctor();
176
+ if (cmd === "repair") return runRepair();
177
+ // `uninstall` never reaches here — every argv shape is decided above.
178
+ // scan / connect / update / config / anything unrecognized: the launcher
179
+ // owns USAGE and exit-code semantics for its own surface, same as culpa.js.
180
+ return runLauncherPassthrough(argv, passthroughEnv(process.env));
181
+ }
182
+
183
+ main(process.argv.slice(2)).catch((e) => {
184
+ console.error(`getculpa: ${e.message}`);
185
+ process.exit(1);
186
+ });
package/lib/assets.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- export const ASSET_MANIFEST: Record<string, string>;
2
- export function resolveAssetPath(name: string): string;
1
+ export const ASSET_MANIFEST: Record<string, string>;
2
+ export function resolveAssetPath(name: string): string;
@@ -1,29 +1,48 @@
1
- // Unlike docker.d.mts's SpawnSyncLike, this one carries `error`: a spawn that
2
- // never ran (missing binary, timeout kill) reports it there, not via status.
3
- export type LauncherSpawnSync = (
4
- cmd: string,
5
- args?: string[],
6
- opts?: Record<string, unknown>,
7
- ) => { status: number | null; error?: Error };
8
-
9
- export const PAYLOAD_STAGE_TIMEOUT_MS: number;
10
-
11
- export function vendoredLauncherPath(packageRoot: string, platform?: string): string | null;
12
-
13
- export interface StageInitialPayloadOptions {
14
- launcherPath: string | null;
15
- spawnSync?: LauncherSpawnSync;
16
- log?: (msg: string) => void;
17
- env?: Record<string, string | undefined>;
18
- }
19
-
20
- export interface StageInitialPayloadResult {
21
- staged: boolean;
22
- reason: "no-launcher" | "spawn-failed" | "update-failed" | "staged";
23
- }
24
-
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;
1
+ // Unlike docker.d.mts's SpawnSyncLike, this one carries `error`: a spawn that
2
+ // never ran (missing binary, timeout kill) reports it there, not via status.
3
+ export type LauncherSpawnSync = (
4
+ cmd: string,
5
+ args?: string[],
6
+ opts?: Record<string, unknown>,
7
+ ) => { status: number | null; error?: Error };
8
+
9
+ export const PAYLOAD_STAGE_TIMEOUT_MS: number;
10
+
11
+ export function vendoredLauncherPath(packageRoot: string, platform?: string): string | null;
12
+
13
+ export interface StageInitialPayloadOptions {
14
+ launcherPath: string | null;
15
+ spawnSync?: LauncherSpawnSync;
16
+ log?: (msg: string) => void;
17
+ env?: Record<string, string | undefined>;
18
+ /** T-V1010-1: the npm package's version; enables the version-aware judgement and the single retry. */
19
+ expectedVersion?: string;
20
+ readDetail?: (opts: ReadPayloadOptions) => PayloadDetail;
21
+ }
22
+
23
+ export interface StageInitialPayloadResult {
24
+ staged: boolean;
25
+ reason: "no-launcher" | "spawn-failed" | "update-failed" | "staged";
26
+ /** present only when expectedVersion was given */
27
+ detail?: PayloadDetail;
28
+ }
29
+
30
+ export function stageInitialPayload(opts: StageInitialPayloadOptions): StageInitialPayloadResult;
31
+
32
+ /** "behind" (T-V1010-1) exists only when expectedVersion is known. */
33
+ export type PayloadState = "installed" | "staged" | "behind" | "missing";
34
+
35
+ export interface ReadPayloadOptions {
36
+ env?: Record<string, string | undefined>;
37
+ expectedVersion?: string;
38
+ }
39
+
40
+ export interface PayloadDetail {
41
+ state: PayloadState;
42
+ current: string | null;
43
+ pending: string | null;
44
+ }
45
+
46
+ export function readPayloadState(opts?: ReadPayloadOptions): PayloadState;
47
+
48
+ export function readPayloadDetail(opts?: ReadPayloadOptions): PayloadDetail;