getculpa 1.0.6 → 1.0.9
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/assets/uninstall-culpa.ps1 +251 -227
- package/lib/uninstall.d.mts +20 -17
- package/lib/uninstall.mjs +158 -138
- package/package.json +26 -25
- package/scripts/preuninstall.js +82 -0
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.9 PUBLISHED AND SIGNED (2026-09-03) ─────────
|
|
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.9
|
|
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.9
|
|
59
59
|
container_name: culpa-server
|
|
60
60
|
environment:
|
|
61
61
|
DATABASE_URL: postgres://culpa:culpa@db:5432/culpa
|
|
@@ -129,7 +129,7 @@ services:
|
|
|
129
129
|
# (D-072: never rebuild a tag that already exists). The example this note
|
|
130
130
|
# used to give was CF14's v0.11.0/v0.11.1 pair, which no longer matches
|
|
131
131
|
# either pin in this file.
|
|
132
|
-
image: ghcr.io/myaigidev/culpa-dashboard:v1.0.
|
|
132
|
+
image: ghcr.io/myaigidev/culpa-dashboard:v1.0.9
|
|
133
133
|
container_name: culpa-dashboard
|
|
134
134
|
environment:
|
|
135
135
|
CULPA_API_BASE: http://server:4545
|
|
@@ -1,227 +1,251 @@
|
|
|
1
|
-
# Culpa uninstaller for Windows (T-087, T-IX-01).
|
|
2
|
-
# Stops and removes the containers and shortcuts. Your recorded data is
|
|
3
|
-
# KEPT unless you answer Y to the final question.
|
|
4
|
-
# -FromUninstaller: run by the setup.exe's uninstaller - no interactive
|
|
5
|
-
# prompts, data always kept (never delete data without an explicit yes).
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# -
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
$
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
function
|
|
64
|
-
param(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
$trimmed
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
$
|
|
109
|
-
|
|
110
|
-
$
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
$
|
|
114
|
-
|
|
115
|
-
if ($
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if ($
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
$
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
#
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
1
|
+
# Culpa uninstaller for Windows (T-087, T-IX-01).
|
|
2
|
+
# Stops and removes the containers and shortcuts. Your recorded data is
|
|
3
|
+
# KEPT unless you answer Y to the final question.
|
|
4
|
+
# -FromUninstaller: run by the setup.exe's uninstaller - no interactive
|
|
5
|
+
# prompts, data always kept (never delete data without an explicit yes). Inno
|
|
6
|
+
# removes the installed files and its own Add/Remove key itself, so this mode
|
|
7
|
+
# only cleans the runtime compose file and refuses (exit 1) while running.
|
|
8
|
+
# -Unattended: run by the npm `preuninstall` hook (D-126) - ALSO non-interactive
|
|
9
|
+
# and data-always-kept, but there is NO Inno wizard to clean up after us, so this
|
|
10
|
+
# mode removes the app dir and the (script-install) Add/Remove key itself.
|
|
11
|
+
# Exactly one of these is set by a non-interactive caller; a human CLI run sets
|
|
12
|
+
# neither and gets the prompts.
|
|
13
|
+
#
|
|
14
|
+
# CR-PR5 round 2 (SAFETY ORDERING): the canonical-path check now gates EVERY
|
|
15
|
+
# destructive action - docker down, the data-volume prompt, and file removal.
|
|
16
|
+
# A copy of this script run from anywhere else previously could stop the real
|
|
17
|
+
# `culpa` project and offer to delete culpa_pgdata, the shared data volume.
|
|
18
|
+
# T-CF29-2 params:
|
|
19
|
+
# -Force proceed from THIS directory even when the
|
|
20
|
+
# canonical check disagrees (explicit opt-in;
|
|
21
|
+
# never automatic)
|
|
22
|
+
# -RecordedInstallLocation inject what the registry would return, so the
|
|
23
|
+
# resolution can be tested without touching HKCU
|
|
24
|
+
# (same -Probe precedent as install-culpa.ps1)
|
|
25
|
+
# -CanonicalOverrideForTest inject the fallback directory, for the same reason
|
|
26
|
+
# -PrintPlanOnly resolve, print the decision as JSON, exit 0
|
|
27
|
+
# BEFORE any destructive action
|
|
28
|
+
param(
|
|
29
|
+
[switch]$FromUninstaller,
|
|
30
|
+
[switch]$Force,
|
|
31
|
+
[string]$RecordedInstallLocation,
|
|
32
|
+
[string]$CanonicalOverrideForTest,
|
|
33
|
+
[switch]$PrintPlanOnly,
|
|
34
|
+
[switch]$Unattended
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
$ErrorActionPreference = "SilentlyContinue"
|
|
38
|
+
$AppDir = $PSScriptRoot
|
|
39
|
+
# A human CLI uninstall is the only INTERACTIVE caller: both -FromUninstaller
|
|
40
|
+
# (Inno) and -Unattended (npm preuninstall hook) run without prompts. Every
|
|
41
|
+
# Read-Host below is gated on this so a non-interactive caller can never block.
|
|
42
|
+
$Interactive = (-not $FromUninstaller) -and (-not $Unattended)
|
|
43
|
+
# CF5-F11: the install directory is the founder's choice now, so "canonical"
|
|
44
|
+
# is what SETUP RECORDED, not a hardcoded path. Inno writes InstallLocation
|
|
45
|
+
# into its own uninstall key; fall back to %LOCALAPPDATA%\Culpa for installs
|
|
46
|
+
# made before the directory page existed, and for the zip layout.
|
|
47
|
+
# The guard itself is unchanged and still load-bearing: a stray COPY of this
|
|
48
|
+
# script must never stop containers or delete files belonging to the real
|
|
49
|
+
# install.
|
|
50
|
+
#
|
|
51
|
+
# T-CF29-2: a recorded value that POINTS AT A DELETED DIRECTORY used to be
|
|
52
|
+
# trusted anyway, so the guard below could never match and Culpa became
|
|
53
|
+
# impossible to uninstall. That is not hypothetical — the CF20 npm gate
|
|
54
|
+
# overwrote this shared value with a scratch dir which was then removed.
|
|
55
|
+
# A path that does not exist cannot be shadowing a real install, so it is
|
|
56
|
+
# ignored in favour of the default. Everything else about the guard stands.
|
|
57
|
+
# T-CF29-2: the guard compared raw strings, so two spellings of the SAME
|
|
58
|
+
# directory read as different installs — an 8.3 short path (C:\Users\ZOLANI~1)
|
|
59
|
+
# versus its long form, a trailing backslash, or different casing. Each of
|
|
60
|
+
# those turns the uninstaller into the same dead end a stale registry value
|
|
61
|
+
# does. Resolve-Path returns the canonical long form for a path that exists;
|
|
62
|
+
# a path that does not exist falls back to a trimmed, lowercased comparison.
|
|
63
|
+
function Normalize-Dir {
|
|
64
|
+
param([string]$Path)
|
|
65
|
+
if ([string]::IsNullOrWhiteSpace($Path)) { return "" }
|
|
66
|
+
$trimmed = $Path.TrimEnd('\', '/')
|
|
67
|
+
# GetFullPath expands an 8.3 short component (ZOLANI~1) to its long form;
|
|
68
|
+
# Resolve-Path does NOT (verified on this machine), so it cannot be relied
|
|
69
|
+
# on for that. Both are best-effort: a path that cannot be expanded falls
|
|
70
|
+
# through to the trimmed, lowercased comparison.
|
|
71
|
+
try { $trimmed = [System.IO.Path]::GetFullPath($trimmed).TrimEnd('\', '/') } catch { }
|
|
72
|
+
return $trimmed.ToLowerInvariant()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function Resolve-CanonicalDir {
|
|
76
|
+
param(
|
|
77
|
+
[string]$Recorded,
|
|
78
|
+
[string]$Default,
|
|
79
|
+
[scriptblock]$PathExists = { param($p) Test-Path -LiteralPath $p }
|
|
80
|
+
)
|
|
81
|
+
if ([string]::IsNullOrWhiteSpace($Recorded)) {
|
|
82
|
+
return [PSCustomObject]@{ Dir = $Default; Source = "default" }
|
|
83
|
+
}
|
|
84
|
+
$trimmed = $Recorded.TrimEnd('\')
|
|
85
|
+
if (-not (& $PathExists $trimmed)) {
|
|
86
|
+
return [PSCustomObject]@{ Dir = $Default; Source = "stale-registry" }
|
|
87
|
+
}
|
|
88
|
+
return [PSCustomObject]@{ Dir = $trimmed; Source = "registry" }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# SAFETY (review of 8da2b2e, CRITICAL): -RecordedInstallLocation and
|
|
92
|
+
# -CanonicalOverrideForTest are ordinary parameters on the SHIPPED script, so
|
|
93
|
+
# they are reachable through exactly the `-File` invocation surface Inno and
|
|
94
|
+
# the shortcuts use. As first written they fed the canonical decision directly,
|
|
95
|
+
# which let a stray copy pass -CanonicalOverrideForTest <its own dir> and reach
|
|
96
|
+
# `docker compose -p culpa down` and the culpa_pgdata prompt with NO -Force and
|
|
97
|
+
# none of the -Force warnings. Proven by probe: wouldProceed:true, forced:false.
|
|
98
|
+
# That is the precise scenario CR-PR5 round 2 exists to prevent, and the
|
|
99
|
+
# -Probe precedent cited above does not excuse it: that one is a parameter of
|
|
100
|
+
# an INTERNAL FUNCTION, reachable only by dot-sourcing, never a top-level flag.
|
|
101
|
+
#
|
|
102
|
+
# They are therefore honoured ONLY under -PrintPlanOnly, which cannot touch
|
|
103
|
+
# docker, the data volume, the registry or any file. In a run that could do
|
|
104
|
+
# something destructive they are ignored outright, so the canonical decision
|
|
105
|
+
# always comes from the real registry or the real default.
|
|
106
|
+
$InjectionRequested = $PSBoundParameters.ContainsKey('RecordedInstallLocation') -or
|
|
107
|
+
-not [string]::IsNullOrWhiteSpace($CanonicalOverrideForTest)
|
|
108
|
+
$InjectionHonoured = $InjectionRequested -and $PrintPlanOnly.IsPresent
|
|
109
|
+
|
|
110
|
+
if ($InjectionHonoured -and $PSBoundParameters.ContainsKey('RecordedInstallLocation')) {
|
|
111
|
+
$Recorded = $RecordedInstallLocation
|
|
112
|
+
} else {
|
|
113
|
+
$Recorded = (Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\{B7A6F2C4-9D31-4E5A-A0C8-52C1E7D94F60}_is1" -Name InstallLocation -ErrorAction SilentlyContinue).InstallLocation
|
|
114
|
+
}
|
|
115
|
+
$DefaultDir = if (-not $InjectionHonoured -or [string]::IsNullOrWhiteSpace($CanonicalOverrideForTest)) {
|
|
116
|
+
Join-Path $env:LOCALAPPDATA "Culpa"
|
|
117
|
+
} else {
|
|
118
|
+
$CanonicalOverrideForTest.TrimEnd('\')
|
|
119
|
+
}
|
|
120
|
+
$Resolved = Resolve-CanonicalDir -Recorded $Recorded -Default $DefaultDir
|
|
121
|
+
$Canonical = $Resolved.Dir
|
|
122
|
+
$ComposeDst = Join-Path $AppDir "docker-compose.yml"
|
|
123
|
+
|
|
124
|
+
$Matches_ = ((Normalize-Dir $AppDir) -eq (Normalize-Dir $Canonical))
|
|
125
|
+
$WouldProceed = ($Matches_ -or $Force.IsPresent)
|
|
126
|
+
|
|
127
|
+
if ($PrintPlanOnly) {
|
|
128
|
+
[PSCustomObject]@{
|
|
129
|
+
appDir = $AppDir
|
|
130
|
+
canonical = $Canonical
|
|
131
|
+
canonicalSource = $Resolved.Source
|
|
132
|
+
wouldProceed = $WouldProceed
|
|
133
|
+
forced = ($Force.IsPresent -and -not $Matches_)
|
|
134
|
+
# so plan output can never be mistaken for a real registry read
|
|
135
|
+
injected = $InjectionHonoured
|
|
136
|
+
} | ConvertTo-Json -Compress
|
|
137
|
+
exit 0
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (-not $WouldProceed) {
|
|
141
|
+
# non-canonical copy: touch NOTHING (no docker, no volume, no files)
|
|
142
|
+
Write-Host "This copy is not the installed one - nothing was changed." -ForegroundColor Yellow
|
|
143
|
+
Write-Host "To uninstall Culpa: Windows Settings -> Installed apps -> Culpa,"
|
|
144
|
+
Write-Host "or run uninstall-culpa.ps1 in $Canonical."
|
|
145
|
+
if ($Resolved.Source -eq "stale-registry") {
|
|
146
|
+
Write-Host "(The recorded install location no longer exists, so the default was used.)"
|
|
147
|
+
}
|
|
148
|
+
# T-CF29-2: never leave the user with no way out. If the canonical
|
|
149
|
+
# directory is wrong or gone, this is the escape hatch — explicit, so it
|
|
150
|
+
# can never fire by accident on a stray copy.
|
|
151
|
+
Write-Host "If Culpa really is installed HERE, re-run with -Force:" -ForegroundColor Yellow
|
|
152
|
+
Write-Host " powershell -ExecutionPolicy Bypass -File `"$AppDir\uninstall-culpa.ps1`" -Force"
|
|
153
|
+
if ($Interactive) { Read-Host "Press Enter to close" }
|
|
154
|
+
exit 0
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if ($Force.IsPresent -and -not $Matches_) {
|
|
158
|
+
Write-Host "-Force: uninstalling from $AppDir even though the recorded location is $Canonical." -ForegroundColor Yellow
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# CF19 (CodeRabbit PR#16 #289): collector shutdown runs AFTER the
|
|
162
|
+
# canonical-path guard — a stray COPY of this script must not change the real
|
|
163
|
+
# installation, and stopping its collector is a change. Stopping capture is
|
|
164
|
+
# otherwise always safe, so it precedes the container teardown.
|
|
165
|
+
$CollectorCtl = Join-Path $AppDir "culpa-collector.ps1"
|
|
166
|
+
if (Test-Path $CollectorCtl) { & $CollectorCtl -Stop }
|
|
167
|
+
|
|
168
|
+
Write-Host "==> Stopping Culpa"
|
|
169
|
+
# CR-PR5: judge the native exit code explicitly (SilentlyContinue does NOT
|
|
170
|
+
# catch it), and NEVER infer "stopped" from a missing compose file - a prior
|
|
171
|
+
# failed cleanup can leave containers running. Absent file => ask docker.
|
|
172
|
+
$stackStopped = $false
|
|
173
|
+
# CR-PR5 r4: check docker EXPLICITLY. SilentlyContinue swallows a missing
|
|
174
|
+
# executable and leaves $LASTEXITCODE at its PRIOR value - verified on
|
|
175
|
+
# Windows PowerShell 5.1: after any earlier successful native command that
|
|
176
|
+
# value is 0, so the old implicit test would have read "stopped" having
|
|
177
|
+
# stopped nothing, and deleted files while containers ran. Fail-closed now
|
|
178
|
+
# by construction, not by the accident of call ordering.
|
|
179
|
+
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
|
180
|
+
Write-Host "Docker was not found on PATH - cannot confirm Culpa is stopped." -ForegroundColor Yellow
|
|
181
|
+
} elseif (Test-Path $ComposeDst) {
|
|
182
|
+
docker compose -f $ComposeDst -p culpa down
|
|
183
|
+
$stackStopped = ($LASTEXITCODE -eq 0)
|
|
184
|
+
} else {
|
|
185
|
+
Write-Host "No compose file here - checking for running Culpa containers directly."
|
|
186
|
+
docker compose -p culpa down
|
|
187
|
+
$stackStopped = ($LASTEXITCODE -eq 0)
|
|
188
|
+
}
|
|
189
|
+
if (-not $stackStopped) {
|
|
190
|
+
Write-Host "Culpa did not stop cleanly - leaving files in place so you can retry:" -ForegroundColor Yellow
|
|
191
|
+
Write-Host " docker compose -p culpa down"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
Write-Host "==> Removing shortcuts"
|
|
195
|
+
Remove-Item (Join-Path ([Environment]::GetFolderPath("Desktop")) "Culpa.lnk") -Force
|
|
196
|
+
Remove-Item (Join-Path ([Environment]::GetFolderPath("Desktop")) "Culpa Dashboard.url") -Force
|
|
197
|
+
Remove-Item (Join-Path ([Environment]::GetFolderPath("StartMenu")) "Programs\Culpa") -Recurse -Force
|
|
198
|
+
|
|
199
|
+
# The data-wipe question is the ONLY place data can be deleted, and it is
|
|
200
|
+
# offered ONLY to an interactive human. Both non-interactive callers keep data.
|
|
201
|
+
if ($Interactive) {
|
|
202
|
+
$wipe = Read-Host "Also DELETE all recorded cost data? This cannot be undone. (y/N)"
|
|
203
|
+
if ($wipe -eq "y" -or $wipe -eq "Y") {
|
|
204
|
+
docker volume rm culpa_pgdata
|
|
205
|
+
Write-Host "Data volume removed."
|
|
206
|
+
} else {
|
|
207
|
+
Write-Host "Data kept (volume culpa_pgdata). Reinstalling later will find it again."
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if ($FromUninstaller) {
|
|
212
|
+
# setup.exe's uninstaller removes the installed files AND its own Add/Remove
|
|
213
|
+
# key itself; we only clean up the runtime compose file - and ONLY when the
|
|
214
|
+
# stack actually stopped.
|
|
215
|
+
# CR-PR5 round 3: SIGNAL the failure. Printing alone let Inno delete the
|
|
216
|
+
# scripts while containers were still running; culpa-setup.iss now runs
|
|
217
|
+
# this from InitializeUninstall and CANCELS the uninstall on a nonzero
|
|
218
|
+
# exit, so the files stay put and the retry is possible.
|
|
219
|
+
if (-not $stackStopped) {
|
|
220
|
+
Write-Host "Refusing to remove Culpa while its containers are running." -ForegroundColor Red
|
|
221
|
+
exit 1
|
|
222
|
+
}
|
|
223
|
+
Remove-Item $ComposeDst -Force
|
|
224
|
+
Write-Host "Data kept (volume culpa_pgdata). Reinstalling later will find it again."
|
|
225
|
+
} else {
|
|
226
|
+
# An interactive CLI uninstall OR the npm preuninstall hook (-Unattended):
|
|
227
|
+
# there is NO Inno wizard to clean up after us, so WE remove the install dir
|
|
228
|
+
# and the (script-install) Add/Remove key - but ONLY when the stack stopped.
|
|
229
|
+
if (-not $stackStopped) {
|
|
230
|
+
Write-Host "Files kept because the stack is still running - stop it, then run this again."
|
|
231
|
+
# -Unattended (npm): nothing was removed, so report failure (exit 1) —
|
|
232
|
+
# lib/uninstall.mjs reads this and the hook then warns loudly; npm still
|
|
233
|
+
# removes the CLI. A human sees the message above and can retry.
|
|
234
|
+
if ($Unattended) { exit 1 }
|
|
235
|
+
} else {
|
|
236
|
+
$unins = Join-Path $AppDir "unins000.exe"
|
|
237
|
+
if (Test-Path $unins) {
|
|
238
|
+
# exe install: hand off so the Windows Settings entry is cleaned too
|
|
239
|
+
Start-Process $unins "/VERYSILENT"
|
|
240
|
+
} else {
|
|
241
|
+
# T-171 script/npm install: WE own the Settings entry, so we remove it.
|
|
242
|
+
# An exe install never reaches here - Inno owns and removes its own
|
|
243
|
+
# key, and deleting it from this side would strand the wizard with
|
|
244
|
+
# a listing it can no longer clean up.
|
|
245
|
+
Remove-Item -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\{B7A6F2C4-9D31-4E5A-A0C8-52C1E7D94F60}_is1" -Recurse -Force
|
|
246
|
+
Remove-Item $AppDir -Recurse -Force
|
|
247
|
+
}
|
|
248
|
+
Write-Host "Culpa removed."
|
|
249
|
+
}
|
|
250
|
+
if ($Interactive) { Read-Host "Press Enter to close" }
|
|
251
|
+
}
|
package/lib/uninstall.d.mts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
export interface UninstallOptions {
|
|
2
|
-
appDir?: string;
|
|
3
|
-
platform?: string;
|
|
4
|
-
spawnSync?: (cmd: string, args?: string[], opts?: unknown) => { status: number | null };
|
|
5
|
-
homedir?: string;
|
|
6
|
-
createInterface?: (opts: { input: NodeJS.ReadStream; output: NodeJS.WriteStream }) => {
|
|
7
|
-
question: (prompt: string, cb: (answer: string) => void) => void;
|
|
8
|
-
close: () => void;
|
|
9
|
-
};
|
|
10
|
-
isTTY?: boolean;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
export interface UninstallOptions {
|
|
2
|
+
appDir?: string;
|
|
3
|
+
platform?: string;
|
|
4
|
+
spawnSync?: (cmd: string, args?: string[], opts?: unknown) => { status: number | null };
|
|
5
|
+
homedir?: string;
|
|
6
|
+
createInterface?: (opts: { input: NodeJS.ReadStream; output: NodeJS.WriteStream }) => {
|
|
7
|
+
question: (prompt: string, cb: (answer: string) => void) => void;
|
|
8
|
+
close: () => void;
|
|
9
|
+
};
|
|
10
|
+
isTTY?: boolean;
|
|
11
|
+
/** Set by the npm `preuninstall` hook: suppress the "npm uninstall -g getculpa"
|
|
12
|
+
* next-step hint (we are already inside that command). D-126 / v1.0.9. */
|
|
13
|
+
fromNpmHook?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UninstallResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function uninstall(opts?: UninstallOptions): Promise<UninstallResult>;
|
package/lib/uninstall.mjs
CHANGED
|
@@ -1,138 +1,158 @@
|
|
|
1
|
-
// CF20-T3 — `getculpa uninstall`. win32 delegates to the same
|
|
2
|
-
// uninstall-culpa.ps1 the Windows installer ships (canonical-path guard,
|
|
3
|
-
// collector stop, compose down, pgdata prompt all live there already).
|
|
4
|
-
// macOS/Linux implement the same net effect once: compose down, remove the
|
|
5
|
-
// shortcut equivalents this core lays down, prompt for pgdata removal
|
|
6
|
-
// (TTY only — never destructive without an interactive terminal), then
|
|
7
|
-
// point at `npm uninstall -g getculpa` to remove the CLI itself.
|
|
8
|
-
|
|
9
|
-
import { spawnSync as realSpawnSync } from "node:child_process";
|
|
10
|
-
import { existsSync, rmSync } from "node:fs";
|
|
11
|
-
import os from "node:os";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import readline from "node:readline";
|
|
14
|
-
import { getAppDir } from "./paths.mjs";
|
|
15
|
-
|
|
16
|
-
function delegateWindowsUninstall(appDir, spawnSync) {
|
|
17
|
-
const script = path.join(appDir, "uninstall-culpa.ps1");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// uninstall-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// V102 T-V102-16 (
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
1
|
+
// CF20-T3 — `getculpa uninstall`. win32 delegates to the same
|
|
2
|
+
// uninstall-culpa.ps1 the Windows installer ships (canonical-path guard,
|
|
3
|
+
// collector stop, compose down, pgdata prompt all live there already).
|
|
4
|
+
// macOS/Linux implement the same net effect once: compose down, remove the
|
|
5
|
+
// shortcut equivalents this core lays down, prompt for pgdata removal
|
|
6
|
+
// (TTY only — never destructive without an interactive terminal), then
|
|
7
|
+
// point at `npm uninstall -g getculpa` to remove the CLI itself.
|
|
8
|
+
|
|
9
|
+
import { spawnSync as realSpawnSync } from "node:child_process";
|
|
10
|
+
import { existsSync, rmSync } from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import readline from "node:readline";
|
|
14
|
+
import { getAppDir } from "./paths.mjs";
|
|
15
|
+
|
|
16
|
+
function delegateWindowsUninstall(appDir, spawnSync, isTTY) {
|
|
17
|
+
const script = path.join(appDir, "uninstall-culpa.ps1");
|
|
18
|
+
// D-126: a non-interactive caller (the npm `preuninstall` hook, or any non-TTY
|
|
19
|
+
// run) MUST pass -Unattended so uninstall-culpa.ps1 skips every Read-Host — it
|
|
20
|
+
// would otherwise PROMPT to delete the cost database and BLOCK on "Press Enter"
|
|
21
|
+
// inside a silent `npm uninstall -g`. -Unattended still does the full
|
|
22
|
+
// self-teardown (remove the app dir + the script-install Add/Remove key), which
|
|
23
|
+
// -FromUninstaller (Inno's mode) deliberately does not. powershell's own
|
|
24
|
+
// -NonInteractive is added as a fail-fast: any stray prompt errors, never hangs.
|
|
25
|
+
const nonInteractive = !isTTY;
|
|
26
|
+
const args = ["-NoProfile"];
|
|
27
|
+
if (nonInteractive) args.push("-NonInteractive");
|
|
28
|
+
args.push("-ExecutionPolicy", "Bypass", "-File", script);
|
|
29
|
+
if (nonInteractive) args.push("-Unattended");
|
|
30
|
+
const result = spawnSync("powershell.exe", args, {
|
|
31
|
+
stdio: "inherit",
|
|
32
|
+
});
|
|
33
|
+
// uninstall-culpa.ps1 stops the capture collector itself before stopping
|
|
34
|
+
// the stack (CF19 CodeRabbit PR#16 #289) — non-fatally, with no exit-code
|
|
35
|
+
// gate on that one step, so a collector-stop hiccup alone never fails this
|
|
36
|
+
// delegate. This return value IS the delegate's true overall outcome
|
|
37
|
+
// (stack stop + everything else the script does), inherited straight to
|
|
38
|
+
// the console via stdio:"inherit" either way.
|
|
39
|
+
return result.status === 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// CF20-R: guard the case where there is nothing to bring down (a fresh app
|
|
43
|
+
// dir, or one already fully torn down) — running `docker compose -f
|
|
44
|
+
// <nonexistent file> ... down` would just fail loudly for no real reason.
|
|
45
|
+
// A genuine compose-down failure (the file exists, the command ran, and
|
|
46
|
+
// docker refused) DOES surface as a failure — see the caller.
|
|
47
|
+
function composeDown(appDir, spawnSync) {
|
|
48
|
+
const compose = path.join(appDir, "docker-compose.yml");
|
|
49
|
+
if (!existsSync(compose)) {
|
|
50
|
+
console.log("getculpa: no docker-compose.yml found - nothing to stop.");
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
const result = spawnSync("docker", ["compose", "-f", compose, "-p", "culpa", "down"], { stdio: "inherit" });
|
|
54
|
+
if (result.status !== 0) {
|
|
55
|
+
console.error("getculpa: `docker compose down` did not complete cleanly - see the output above.");
|
|
56
|
+
}
|
|
57
|
+
return result.status === 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The only shortcut this core lays down outside the app dir on macOS is the
|
|
61
|
+
// Desktop .webloc (installers/macos/install-culpa.command:132-142); Linux
|
|
62
|
+
// ships no shortcut convention yet, so there is nothing else to remove.
|
|
63
|
+
function removeShortcutEquivalents(homedir) {
|
|
64
|
+
rmSync(path.join(homedir, "Desktop", "Culpa Dashboard.webloc"), { force: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Never destructive without a real interactive terminal — a non-TTY caller
|
|
68
|
+
// (CI, a script, npm's own postinstall/preuninstall hooks) always gets NO.
|
|
69
|
+
// isTTY is injectable (same pattern as lib/start.mjs's maybeOpenBrowser)
|
|
70
|
+
// rather than read from the live process.stdin.isTTY directly, so tests
|
|
71
|
+
// assert the safe default deliberately instead of relying on vitest's own
|
|
72
|
+
// stdin happening to be non-TTY.
|
|
73
|
+
async function promptPgdataRemoval(createInterface, isTTY) {
|
|
74
|
+
if (!isTTY) return false;
|
|
75
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
76
|
+
const answer = await new Promise((resolve) => {
|
|
77
|
+
// V102 T-V102-16 (D-113): EOF (Ctrl-D, or a stdin that ends mid-prompt)
|
|
78
|
+
// fires `close` and NEVER invokes question()'s callback — this promise
|
|
79
|
+
// used to hang forever at the one prompt guarding DELETION of the cost
|
|
80
|
+
// database. An unanswered question is a NO, exactly like tty.mjs.
|
|
81
|
+
// Optional-call: older injected fakes without `once` keep working.
|
|
82
|
+
rl.once?.("close", () => resolve(""));
|
|
83
|
+
rl.question("Also DELETE all recorded cost data? This cannot be undone. (y/N) ", resolve);
|
|
84
|
+
});
|
|
85
|
+
rl.close();
|
|
86
|
+
return answer.trim().toLowerCase() === "y";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// V102 T-V102-16 (M11/W2, D-113): macOS uninstall exited 0 leaving ~7.3 MB
|
|
90
|
+
// (launcher, collector, compose, register.mjs) silently behind — the Windows
|
|
91
|
+
// ps1 removes the app dir and is "the more complete of the two" (W2). Remove
|
|
92
|
+
// the dir ONLY when it is provably a Culpa install dir (install-state.json,
|
|
93
|
+
// which every install writes); a mistargeted CULPA_APP_DIR is kept and said.
|
|
94
|
+
// Truthful beats a surprise rm -rf in both directions.
|
|
95
|
+
function removeAppDir(appDir) {
|
|
96
|
+
if (!existsSync(appDir)) return;
|
|
97
|
+
if (existsSync(path.join(appDir, "install-state.json"))) {
|
|
98
|
+
rmSync(appDir, { recursive: true, force: true });
|
|
99
|
+
console.log(`Removed: ${appDir} (launcher, collector, compose and capture shims).`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
console.log(
|
|
103
|
+
`Kept: ${appDir} - no install-state.json found there, so it is not provably a Culpa install directory. Remove it yourself if you are sure.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function uninstall(opts = {}) {
|
|
108
|
+
const appDir = opts.appDir ?? getAppDir();
|
|
109
|
+
const platform = opts.platform ?? process.platform;
|
|
110
|
+
const spawnSync = opts.spawnSync ?? realSpawnSync;
|
|
111
|
+
const homedir = opts.homedir ?? os.homedir();
|
|
112
|
+
const createInterface = opts.createInterface ?? readline.createInterface;
|
|
113
|
+
const isTTY = opts.isTTY ?? process.stdin.isTTY;
|
|
114
|
+
// D-126 / v1.0.9: set by the npm `preuninstall` hook (scripts/preuninstall.js).
|
|
115
|
+
// When true we are ALREADY inside `npm uninstall -g getculpa`, so the trailing
|
|
116
|
+
// "To finish removing the Culpa CLI: npm uninstall -g getculpa" hint below is
|
|
117
|
+
// wrong/confusing — the hook owns the closing "what's left behind" summary.
|
|
118
|
+
// Teardown behaviour is otherwise identical.
|
|
119
|
+
const fromNpmHook = opts.fromNpmHook ?? false;
|
|
120
|
+
|
|
121
|
+
let ok;
|
|
122
|
+
if (platform === "win32") {
|
|
123
|
+
ok = delegateWindowsUninstall(appDir, spawnSync, isTTY);
|
|
124
|
+
if (!ok) console.error("getculpa: uninstall did not complete cleanly - see the output above.");
|
|
125
|
+
} else {
|
|
126
|
+
ok = composeDown(appDir, spawnSync);
|
|
127
|
+
// Review-4 IMPORTANT + CodeRabbit PR#19 [7] (Major): a FAILED stop skips
|
|
128
|
+
// EVERY destructive step — the ps1's $stackStopped gate, whose comment
|
|
129
|
+
// records the prior real incident (CR-PR5 round 3). Not even the pgdata
|
|
130
|
+
// PROMPT is offered: after a partial teardown the volume can be
|
|
131
|
+
// unreferenced, so a user confirming deletion here could lose the cost
|
|
132
|
+
// database while being told the stack is still running. Nothing is
|
|
133
|
+
// removed; the one honest instruction is to stop the stack and rerun.
|
|
134
|
+
if (!ok) {
|
|
135
|
+
console.log(
|
|
136
|
+
`Files kept: ${appDir} - the stack is still running (\`docker compose down\` failed above). Nothing was removed. Stop the stack, then run this again.`,
|
|
137
|
+
);
|
|
138
|
+
} else {
|
|
139
|
+
removeShortcutEquivalents(homedir);
|
|
140
|
+
const wipe = await promptPgdataRemoval(createInterface, isTTY);
|
|
141
|
+
if (wipe) {
|
|
142
|
+
spawnSync("docker", ["volume", "rm", "culpa_pgdata"], { stdio: "inherit" });
|
|
143
|
+
console.log("Data volume removed.");
|
|
144
|
+
} else {
|
|
145
|
+
console.log("Data kept (volume culpa_pgdata). Reinstalling later will find it again.");
|
|
146
|
+
}
|
|
147
|
+
// M11/W2: after the data decision, remove the app dir itself (ps1 parity)
|
|
148
|
+
removeAppDir(appDir);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!fromNpmHook) {
|
|
153
|
+
console.log("");
|
|
154
|
+
console.log("To finish removing the Culpa CLI: npm uninstall -g getculpa");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { ok };
|
|
158
|
+
}
|
package/package.json
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "getculpa",
|
|
3
|
-
"version": "1.0.
|
|
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
|
-
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
-
"bin": {
|
|
7
|
-
"getculpa": "bin/getculpa.js",
|
|
8
|
-
"culpa": "bin/culpa.js"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"postinstall": "node scripts/install.js",
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "getculpa",
|
|
3
|
+
"version": "1.0.9",
|
|
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
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"bin": {
|
|
7
|
+
"getculpa": "bin/getculpa.js",
|
|
8
|
+
"culpa": "bin/culpa.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "node scripts/install.js",
|
|
12
|
+
"preuninstall": "node scripts/preuninstall.js",
|
|
13
|
+
"prepack": "node scripts/prepack.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"lib/",
|
|
18
|
+
"scripts/",
|
|
19
|
+
"assets/",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// v1.0.9 (findings: ~/Downloads/10-uninstall-findings.md) — npm `preuninstall`
|
|
2
|
+
// hook so `npm uninstall -g getculpa` actually DEPROVISIONS instead of leaving a
|
|
3
|
+
// running stack, ~400MB of images and the stored credentials behind (the P1
|
|
4
|
+
// findings: the CLI that could clean up was the very thing being deleted).
|
|
5
|
+
//
|
|
6
|
+
// npm runs this NON-INTERACTIVELY (no TTY), so it drives the same teardown as
|
|
7
|
+
// `getculpa uninstall` in its safe, data-preserving default: stop the stack +
|
|
8
|
+
// remove the `culpa` network, the Desktop shortcut and the app dir (which also
|
|
9
|
+
// clears the stale install-state.json — findings P3). It NEVER deletes the data
|
|
10
|
+
// volumes: a silent hook must not destroy the cost database or the stored
|
|
11
|
+
// licence. It then prints exactly what is left behind, with the one-line removal
|
|
12
|
+
// command (the findings' suggested fix).
|
|
13
|
+
//
|
|
14
|
+
// A nonzero exit does NOT stop `npm uninstall -g` (verified: node 20.19.6 /
|
|
15
|
+
// npm 11.x, D-126), so the Windows Inno "cancel-on-nonzero" control cannot be
|
|
16
|
+
// replicated here. The honest ceiling npm allows is a loud warning on a failed
|
|
17
|
+
// stop; we give it and still exit 0 (a nonzero exit only adds "npm error" noise
|
|
18
|
+
// while npm removes the package anyway).
|
|
19
|
+
|
|
20
|
+
"use strict";
|
|
21
|
+
|
|
22
|
+
// Dependency-injected so the summary / stop-failure messaging is unit-testable in
|
|
23
|
+
// isolation (a fake `uninstall`, captured log/err) — mirrors the injection style
|
|
24
|
+
// of tests/npm-core-uninstall.test.ts. The npm entry (bottom) wires the real deps.
|
|
25
|
+
async function runPreuninstall(deps = {}) {
|
|
26
|
+
const importUninstall = deps.importUninstall ?? (() => import("../lib/uninstall.mjs"));
|
|
27
|
+
const log = deps.log ?? console.log;
|
|
28
|
+
const err = deps.err ?? console.error;
|
|
29
|
+
|
|
30
|
+
const { uninstall } = await importUninstall();
|
|
31
|
+
|
|
32
|
+
log("");
|
|
33
|
+
log("getculpa: uninstalling — stopping the Culpa stack and removing install files...");
|
|
34
|
+
|
|
35
|
+
let ok = false;
|
|
36
|
+
try {
|
|
37
|
+
({ ok } = await uninstall({ isTTY: false, fromNpmHook: true }));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// A teardown error must never crash the hook or abort the package removal.
|
|
40
|
+
err(`getculpa: preuninstall teardown hit an error: ${(e && e.message) || e}`);
|
|
41
|
+
ok = false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!ok) {
|
|
45
|
+
// "Refuse to leave a running stack silently" — the ceiling npm allows (D-126):
|
|
46
|
+
// npm removes getculpa regardless, so warn loudly with the manual command.
|
|
47
|
+
err("");
|
|
48
|
+
err("getculpa: WARNING — the Culpa stack may still be RUNNING (its stop did not complete cleanly).");
|
|
49
|
+
err("getculpa: getculpa is about to be removed, so stop the stack yourself with:");
|
|
50
|
+
err(" docker compose -p culpa down");
|
|
51
|
+
err("");
|
|
52
|
+
return { ok: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Data + credentials are KEPT on purpose (never deleted by a silent hook).
|
|
56
|
+
// culpa_pgdata holds the recorded cost data AND the stored licence key + sync
|
|
57
|
+
// secret (culpa/api/claim_client.ts stores them in the SQL settings table);
|
|
58
|
+
// culpa_data holds the machine secret. No host-side licence file exists (D-126).
|
|
59
|
+
log("");
|
|
60
|
+
log("getculpa: Culpa's containers and network are stopped and the install files are removed.");
|
|
61
|
+
log("Two data volumes were KEPT on purpose (reinstalling later reuses them):");
|
|
62
|
+
log(" - culpa_pgdata : your recorded cost data AND your stored licence key + sync secret");
|
|
63
|
+
log(" - culpa_data : the machine secret");
|
|
64
|
+
log("To remove them too (this DELETES your recorded data and stored licence, and cannot be undone):");
|
|
65
|
+
log(" docker volume rm culpa_pgdata culpa_data");
|
|
66
|
+
log("");
|
|
67
|
+
return { ok: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { runPreuninstall };
|
|
71
|
+
|
|
72
|
+
// Always exit 0: a failed teardown is surfaced as a loud warning above, not as a
|
|
73
|
+
// nonzero exit (which npm ignores for uninstall while still removing the package).
|
|
74
|
+
if (require.main === module) {
|
|
75
|
+
runPreuninstall().then(
|
|
76
|
+
() => process.exit(0),
|
|
77
|
+
(e) => {
|
|
78
|
+
console.error(`getculpa: preuninstall hook error (uninstall still proceeds): ${(e && e.message) || e}`);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
}
|