fullcourtdefense-cli 1.22.7 → 1.22.8
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/dist/backupManifest.d.ts +7 -0
- package/dist/backupManifest.js +108 -0
- package/dist/commands/daemon.js +2 -0
- package/dist/commands/deterministicGuard.js +18 -3
- package/dist/commands/discover.js +5 -0
- package/dist/commands/hook.js +10 -0
- package/dist/commands/installAll.js +10 -4
- package/dist/commands/installCursorHook.js +6 -2
- package/dist/commands/mcpGateway.js +13 -0
- package/dist/commands/onboard.d.ts +2 -0
- package/dist/commands/onboard.js +53 -2
- package/dist/commands/uninstallAll.d.ts +58 -9
- package/dist/commands/uninstallAll.js +359 -37
- package/dist/commands/verifyRemoved.d.ts +24 -0
- package/dist/commands/verifyRemoved.js +293 -0
- package/dist/commands/windowsAudit.d.ts +10 -5
- package/dist/commands/windowsAudit.js +47 -8
- package/dist/index.js +39 -7
- package/dist/localSafetySnapshot.js +6 -3
- package/dist/selfTest.js +31 -0
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -33,10 +33,20 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.listFcdConfigBackups = listFcdConfigBackups;
|
|
37
|
+
exports.removeFcdConfigBackups = removeFcdConfigBackups;
|
|
38
|
+
exports.purgeLocalState = purgeLocalState;
|
|
39
|
+
exports.parseMsiProductCodeFromRegOutput = parseMsiProductCodeFromRegOutput;
|
|
40
|
+
exports.findMsiProductCode = findMsiProductCode;
|
|
41
|
+
exports.detachCommand = detachCommand;
|
|
42
|
+
exports.reportUninstallToFleet = reportUninstallToFleet;
|
|
36
43
|
exports.uninstallAllCommand = uninstallAllCommand;
|
|
37
44
|
const fs = __importStar(require("fs"));
|
|
38
45
|
const os = __importStar(require("os"));
|
|
39
46
|
const path = __importStar(require("path"));
|
|
47
|
+
const child_process_1 = require("child_process");
|
|
48
|
+
const config_1 = require("../config");
|
|
49
|
+
const machineIdentity_1 = require("../machineIdentity");
|
|
40
50
|
const daemon_1 = require("./daemon");
|
|
41
51
|
const autoProtect_1 = require("./autoProtect");
|
|
42
52
|
const mcpGateway_1 = require("./mcpGateway");
|
|
@@ -46,25 +56,190 @@ const installClaudeHook_1 = require("./installClaudeHook");
|
|
|
46
56
|
const shellGuard_1 = require("./shellGuard");
|
|
47
57
|
const posixShellGuard_1 = require("./posixShellGuard");
|
|
48
58
|
const cmdGuard_1 = require("./cmdGuard");
|
|
59
|
+
const credentialStore_1 = require("../credentialStore");
|
|
60
|
+
const machineKeyFile_1 = require("../machineKeyFile");
|
|
61
|
+
const selfUpdate_1 = require("../selfUpdate");
|
|
62
|
+
const backupManifest_1 = require("../backupManifest");
|
|
49
63
|
const BOLD = '\x1b[1m';
|
|
50
64
|
const GREEN = '\x1b[32m';
|
|
51
65
|
const YELLOW = '\x1b[33m';
|
|
52
66
|
const DIM = '\x1b[2m';
|
|
53
67
|
const RESET = '\x1b[0m';
|
|
54
68
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* Machine credentials (~/.fullcourtdefense.yml) and local state are KEPT by
|
|
61
|
-
* default so the machine can re-onboard without a new enrollment token; pass
|
|
62
|
-
* `--purge true` to delete them too. The fleet record in the dashboard is not
|
|
63
|
-
* touched — deprovision it from the console (Machines → machine → Deprovision).
|
|
69
|
+
* Directories where MCP-config edits leave `*.fcd-backup-*` safety copies
|
|
70
|
+
* (see backupFile() in mcpGateway.ts). Scanned non-recursively on purge so a
|
|
71
|
+
* full uninstall leaves no copies of configs — which can embed live
|
|
72
|
+
* credentials — behind. Project-local backups (e.g. repo .cursor/mcp.json)
|
|
73
|
+
* cannot be enumerated from here and are called out in the final message.
|
|
64
74
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
function fcdBackupSearchDirs() {
|
|
76
|
+
const home = os.homedir();
|
|
77
|
+
const dirs = [
|
|
78
|
+
home,
|
|
79
|
+
path.join(home, '.cursor'),
|
|
80
|
+
path.join(home, '.claude'),
|
|
81
|
+
path.join(home, '.codex'),
|
|
82
|
+
path.join(home, '.gemini'),
|
|
83
|
+
path.join(home, '.kiro', 'settings'),
|
|
84
|
+
path.join(home, '.codeium', 'windsurf'),
|
|
85
|
+
];
|
|
86
|
+
if (process.platform === 'win32' && process.env.APPDATA) {
|
|
87
|
+
dirs.push(path.join(process.env.APPDATA, 'Claude'));
|
|
88
|
+
dirs.push(path.join(process.env.APPDATA, 'Code', 'User'));
|
|
89
|
+
}
|
|
90
|
+
else if (process.platform === 'darwin') {
|
|
91
|
+
dirs.push(path.join(home, 'Library', 'Application Support', 'Claude'));
|
|
92
|
+
dirs.push(path.join(home, 'Library', 'Application Support', 'Code', 'User'));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
dirs.push(path.join(home, '.config', 'Claude'));
|
|
96
|
+
dirs.push(path.join(home, '.config', 'Code', 'User'));
|
|
97
|
+
}
|
|
98
|
+
return dirs;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Every `*.fcd-backup-*` config safety copy: the known client dirs (scan)
|
|
102
|
+
* union-ed with the backup manifest, which records PROJECT-LOCAL backups
|
|
103
|
+
* (a repo's .cursor/mcp.json.fcd-backup-*) that no directory scan can find.
|
|
104
|
+
* The scan still covers backups created by pre-manifest CLI versions.
|
|
105
|
+
*/
|
|
106
|
+
function listFcdConfigBackups() {
|
|
107
|
+
const found = new Set();
|
|
108
|
+
for (const dir of fcdBackupSearchDirs()) {
|
|
109
|
+
let entries;
|
|
110
|
+
try {
|
|
111
|
+
entries = fs.readdirSync(dir);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue; // dir absent — fine
|
|
115
|
+
}
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
if (entry.includes('.fcd-backup-'))
|
|
118
|
+
found.add(path.join(dir, entry));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const entry of (0, backupManifest_1.readBackupManifest)()) {
|
|
122
|
+
try {
|
|
123
|
+
if (entry.includes('.fcd-backup-') && fs.existsSync(entry))
|
|
124
|
+
found.add(entry);
|
|
125
|
+
}
|
|
126
|
+
catch { /* unreadable path — skip */ }
|
|
127
|
+
}
|
|
128
|
+
return [...found];
|
|
129
|
+
}
|
|
130
|
+
/** Delete every known `*.fcd-backup-*` config safety copy (scan + manifest). */
|
|
131
|
+
function removeFcdConfigBackups() {
|
|
132
|
+
const removed = [];
|
|
133
|
+
const failures = [];
|
|
134
|
+
for (const file of listFcdConfigBackups()) {
|
|
135
|
+
try {
|
|
136
|
+
fs.rmSync(file, { force: true });
|
|
137
|
+
removed.push(file);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
failures.push(file);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
(0, backupManifest_1.pruneBackupManifest)();
|
|
144
|
+
return { removed, failures };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Delete all local FullCourtDefense state: config + spool + logs, the
|
|
148
|
+
* ~/.fullcourtdefense dir (machine key file lives inside), the Credential
|
|
149
|
+
* Manager entry, %LOCALAPPDATA%\FullCourtDefense, and config backups.
|
|
150
|
+
*/
|
|
151
|
+
function purgeLocalState(config) {
|
|
152
|
+
const failures = [];
|
|
153
|
+
const home = os.homedir();
|
|
154
|
+
// Credential Manager entry first — needs the shieldId from config, which the
|
|
155
|
+
// file deletions below would erase.
|
|
156
|
+
if (process.platform === 'win32' && config.shieldId) {
|
|
157
|
+
(0, credentialStore_1.deleteShieldKeyNative)(config.shieldId);
|
|
158
|
+
}
|
|
159
|
+
(0, machineKeyFile_1.deleteMachineKeyFile)();
|
|
160
|
+
// Config backups BEFORE the state dirs: the backup manifest (which knows the
|
|
161
|
+
// project-local backup paths) lives inside ~/.fullcourtdefense — deleting
|
|
162
|
+
// that dir first would orphan every repo-local credential backup.
|
|
163
|
+
const backups = removeFcdConfigBackups();
|
|
164
|
+
for (const file of backups.removed)
|
|
165
|
+
console.log(`${DIM} removed ${file}${RESET}`);
|
|
166
|
+
for (const file of backups.failures) {
|
|
167
|
+
failures.push(file);
|
|
168
|
+
console.log(`${YELLOW} ⚠ could not remove ${file}${RESET}`);
|
|
169
|
+
}
|
|
170
|
+
const targets = [
|
|
171
|
+
path.join(home, '.fullcourtdefense.yml'),
|
|
172
|
+
path.join(home, '.fullcourtdefense-spool.jsonl'),
|
|
173
|
+
path.join(home, '.fullcourtdefense-runtime.json'),
|
|
174
|
+
path.join(home, '.fullcourtdefense-hook.log'),
|
|
175
|
+
path.join(home, '.fullcourtdefense'),
|
|
176
|
+
];
|
|
177
|
+
if (process.platform === 'win32' && process.env.LOCALAPPDATA) {
|
|
178
|
+
targets.push(path.join(process.env.LOCALAPPDATA, 'FullCourtDefense'));
|
|
179
|
+
}
|
|
180
|
+
for (const target of targets) {
|
|
181
|
+
try {
|
|
182
|
+
if (!fs.existsSync(target))
|
|
183
|
+
continue;
|
|
184
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
185
|
+
console.log(`${DIM} removed ${target}${RESET}`);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
failures.push(target);
|
|
189
|
+
console.log(`${YELLOW} ⚠ could not remove ${target}: ${error instanceof Error ? error.message : String(error)}${RESET}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { failures };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Product code of the installed FullCourtDefense MSI, from the registry
|
|
196
|
+
* uninstall hive (both 64-bit and WOW6432Node views). Undefined on npm
|
|
197
|
+
* installs or when the MSI entry is missing.
|
|
198
|
+
*/
|
|
199
|
+
function parseMsiProductCodeFromRegOutput(output) {
|
|
200
|
+
// `reg query … /s /f FullCourtDefense /d` prints the matching key path line
|
|
201
|
+
// followed by its matching values. We want the {GUID} keyed entry whose
|
|
202
|
+
// DisplayName is FullCourtDefense.
|
|
203
|
+
const lines = output.split(/\r?\n/);
|
|
204
|
+
let currentGuid;
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
const keyMatch = line.match(/\\Uninstall\\(\{[0-9A-Fa-f-]{36}\})\s*$/);
|
|
207
|
+
if (keyMatch) {
|
|
208
|
+
currentGuid = keyMatch[1];
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (currentGuid && /DisplayName\s+REG_SZ\s+FullCourtDefense\s*$/i.test(line.trim())) {
|
|
212
|
+
return currentGuid;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
function findMsiProductCode() {
|
|
218
|
+
if (process.platform !== 'win32')
|
|
219
|
+
return undefined;
|
|
220
|
+
const hives = [
|
|
221
|
+
'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
222
|
+
'HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
223
|
+
];
|
|
224
|
+
for (const hive of hives) {
|
|
225
|
+
const query = (0, child_process_1.spawnSync)('reg', ['query', hive, '/s', '/f', 'FullCourtDefense', '/d'], {
|
|
226
|
+
encoding: 'utf8', windowsHide: true, timeout: 30_000,
|
|
227
|
+
});
|
|
228
|
+
if (query.status !== 0 || !query.stdout)
|
|
229
|
+
continue;
|
|
230
|
+
const code = parseMsiProductCodeFromRegOutput(query.stdout);
|
|
231
|
+
if (code)
|
|
232
|
+
return code;
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The shared local teardown: daemon autostart, self-heal schedule, MCP gateway
|
|
238
|
+
* wraps, IDE hooks, terminal guards, and the daily discovery schedule. Each
|
|
239
|
+
* step is best-effort so a single failure never leaves the machine
|
|
240
|
+
* half-detached. Returns the labels of failed steps.
|
|
241
|
+
*/
|
|
242
|
+
async function runLocalDetach(args, config) {
|
|
68
243
|
const steps = [
|
|
69
244
|
{ label: 'Resident daemon + autostart', run: () => (0, daemon_1.daemonCommand)({ ...args, uninstall: 'true', quiet: 'true' }, config) },
|
|
70
245
|
{ label: 'Self-healing protect schedule', run: () => (0, autoProtect_1.autoProtectCommand)({ ...args, uninstall: 'true' }, config) },
|
|
@@ -93,40 +268,187 @@ async function uninstallAllCommand(args, config) {
|
|
|
93
268
|
}
|
|
94
269
|
console.log('');
|
|
95
270
|
}
|
|
271
|
+
return failures;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* `fullcourtdefense detach` — reverse `onboard`/`install-all` protection but
|
|
275
|
+
* KEEP machine credentials and the installed application, so the machine can
|
|
276
|
+
* re-onboard without a new enrollment token. This is the old `uninstall`
|
|
277
|
+
* behavior, renamed to say what it actually does.
|
|
278
|
+
*/
|
|
279
|
+
async function detachCommand(args, config) {
|
|
280
|
+
console.log(`\n${BOLD}\x1b[36mFullCourtDefense — detach local protection${RESET}`);
|
|
281
|
+
console.log(`${DIM}Daemon -> self-heal schedule -> MCP gateways -> IDE hooks -> terminal guards -> discovery schedule.${RESET}\n`);
|
|
282
|
+
const failures = await runLocalDetach(args, config);
|
|
96
283
|
if (args.purge === 'true') {
|
|
97
284
|
console.log(`${BOLD}› Local state files${RESET}`);
|
|
98
|
-
|
|
99
|
-
const targets = [
|
|
100
|
-
path.join(home, '.fullcourtdefense.yml'),
|
|
101
|
-
path.join(home, '.fullcourtdefense-spool.jsonl'),
|
|
102
|
-
path.join(home, '.fullcourtdefense-runtime.json'),
|
|
103
|
-
path.join(home, '.fullcourtdefense-hook.log'),
|
|
104
|
-
path.join(home, '.fullcourtdefense'),
|
|
105
|
-
];
|
|
106
|
-
for (const target of targets) {
|
|
107
|
-
try {
|
|
108
|
-
if (!fs.existsSync(target))
|
|
109
|
-
continue;
|
|
110
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
111
|
-
console.log(`${DIM} removed ${target}${RESET}`);
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
failures.push(target);
|
|
115
|
-
console.log(`${YELLOW} ⚠ could not remove ${target}: ${error instanceof Error ? error.message : String(error)}${RESET}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
285
|
+
failures.push(...purgeLocalState(config).failures);
|
|
118
286
|
console.log('');
|
|
119
287
|
}
|
|
120
288
|
if (failures.length === 0) {
|
|
121
|
-
console.log(`${GREEN}${BOLD}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
289
|
+
console.log(`${GREEN}${BOLD}Protection detached.${RESET}`);
|
|
290
|
+
console.log(`${DIM}The FullCourtDefense application itself is still installed${args.purge === 'true' ? '' : ' and machine credentials were kept (re-run `fullcourtdefense onboard` to re-protect)'}.${RESET}`);
|
|
291
|
+
console.log(`${DIM}For COMPLETE removal run \`fullcourtdefense uninstall\`${process.platform === 'win32' ? ' or use Windows Settings → Apps → FullCourtDefense → Uninstall' : ''}.${RESET}`);
|
|
125
292
|
console.log(`${DIM}The machine still appears in the fleet console until an admin deprovisions it (Machines → machine → Deprovision).${RESET}`);
|
|
126
293
|
console.log(`${DIM}Restart your AI clients (Cursor, Claude, VS Code, …) so they reload the restored configs.${RESET}`);
|
|
127
294
|
}
|
|
128
295
|
else {
|
|
129
|
-
console.log(`${YELLOW}${BOLD}
|
|
296
|
+
console.log(`${YELLOW}${BOLD}Detach finished with ${failures.length} step(s) needing attention:${RESET} ${failures.join(', ')}`);
|
|
130
297
|
process.exitCode = 1;
|
|
131
298
|
}
|
|
132
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Tell the fleet console this machine is being uninstalled, BEFORE the local
|
|
302
|
+
* credential purge erases the shield key we need to authenticate the call.
|
|
303
|
+
* Best-effort with a short timeout: an offline uninstall must never hang or
|
|
304
|
+
* fail because the control plane is unreachable. On success the console shows
|
|
305
|
+
* the machine as "Uninstalled" instead of silently ghosting until an admin
|
|
306
|
+
* notices the missing heartbeats.
|
|
307
|
+
*/
|
|
308
|
+
async function reportUninstallToFleet(config, reason) {
|
|
309
|
+
try {
|
|
310
|
+
const creds = (0, config_1.resolveCliCredentials)(config);
|
|
311
|
+
if (!creds.shieldId || !creds.shieldKey || !creds.apiUrl)
|
|
312
|
+
return false;
|
|
313
|
+
const resp = await fetch(`${creds.apiUrl}/api/cli/machines/self-report-uninstall`, {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: { 'Content-Type': 'application/json', 'x-shield-key': creds.shieldKey },
|
|
316
|
+
body: JSON.stringify({
|
|
317
|
+
shieldId: creds.shieldId,
|
|
318
|
+
machineId: (0, machineIdentity_1.getMachineIdentity)().machineId,
|
|
319
|
+
...(reason ? { reason: reason.slice(0, 300) } : {}),
|
|
320
|
+
}),
|
|
321
|
+
signal: AbortSignal.timeout(8_000),
|
|
322
|
+
});
|
|
323
|
+
return resp.ok;
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return false; // offline / old backend — uninstall proceeds regardless
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/** Launch the complete Windows uninstall for an MSI install. */
|
|
330
|
+
function launchMsiUninstall(productCode) {
|
|
331
|
+
try {
|
|
332
|
+
// /passive: progress bar only — the user already expressed intent by
|
|
333
|
+
// running `uninstall`, so no second confirmation dialog. UAC still prompts
|
|
334
|
+
// (per-machine MSI), which is exactly the elevation the SYSTEM task and
|
|
335
|
+
// HKLM policy cleanup need.
|
|
336
|
+
const child = (0, child_process_1.spawn)('msiexec.exe', ['/x', productCode, '/passive'], {
|
|
337
|
+
detached: true, stdio: 'ignore', windowsHide: false,
|
|
338
|
+
});
|
|
339
|
+
child.unref();
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* `fullcourtdefense uninstall` — COMPLETE removal.
|
|
348
|
+
*
|
|
349
|
+
* On MSI installs (Windows): runs the local teardown + credential purge, then
|
|
350
|
+
* hands off to the Windows uninstaller (`msiexec /x`) so the SYSTEM updater
|
|
351
|
+
* task, HKLM audit policies, and the installed application are all removed in
|
|
352
|
+
* the same flow — one command, one UAC prompt, machine back to its pre-FCD
|
|
353
|
+
* state. On npm installs: local teardown + purge, then tells the user the one
|
|
354
|
+
* remaining step (removing the npm package itself).
|
|
355
|
+
*
|
|
356
|
+
* To detach protection but keep credentials/the app, use `fullcourtdefense
|
|
357
|
+
* detach` instead.
|
|
358
|
+
*/
|
|
359
|
+
async function uninstallAllCommand(args, config) {
|
|
360
|
+
const isMsi = process.platform === 'win32' && (0, selfUpdate_1.detectInstallKind)() === 'msi';
|
|
361
|
+
const msiHandoff = isMsi && args.localOnly !== 'true';
|
|
362
|
+
console.log(`\n${BOLD}\x1b[36mFullCourtDefense — complete uninstall${RESET}`);
|
|
363
|
+
if (msiHandoff) {
|
|
364
|
+
console.log(`${DIM}This machine was installed with the Windows installer (MSI). After the local teardown, the Windows uninstaller will launch to remove the SYSTEM updater task, machine-wide audit policies, and the application itself.${RESET}\n`);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
console.log(`${DIM}Daemon -> self-heal schedule -> MCP gateways -> IDE hooks -> terminal guards -> discovery schedule -> local state.${RESET}\n`);
|
|
368
|
+
}
|
|
369
|
+
// Report to the fleet FIRST — the credential purge below erases the shield
|
|
370
|
+
// key this call authenticates with.
|
|
371
|
+
console.log(`${BOLD}› Reporting uninstall to the fleet console${RESET}`);
|
|
372
|
+
const fleetNotified = await reportUninstallToFleet(config, args.reason);
|
|
373
|
+
console.log(fleetNotified
|
|
374
|
+
? `${DIM} fleet console notified — the machine is now marked as uninstalled${RESET}`
|
|
375
|
+
: `${DIM} could not reach the fleet console (offline or already detached) — continuing${RESET}`);
|
|
376
|
+
console.log('');
|
|
377
|
+
const failures = await runLocalDetach(args, config);
|
|
378
|
+
// Complete removal always purges: keeping credential files around after an
|
|
379
|
+
// uninstall is exactly the leftover users complain about. `detach` is the
|
|
380
|
+
// command that preserves them.
|
|
381
|
+
console.log(`${BOLD}› Local state files (credentials, caches, config backups)${RESET}`);
|
|
382
|
+
failures.push(...purgeLocalState(config).failures);
|
|
383
|
+
console.log('');
|
|
384
|
+
// PowerShell audit policies (HKLM ScriptBlockLogging + Transcription): the
|
|
385
|
+
// MSI uninstaller removes them itself, so only the non-MSI path cleans them
|
|
386
|
+
// here — otherwise a customer who enabled `--windows-audit` via an npm
|
|
387
|
+
// install keeps machine-wide policies after "uninstall" said it was done.
|
|
388
|
+
// Only FCD-attributed policies are touched (transcript dir is ours); a GPO
|
|
389
|
+
// pointing elsewhere is not ours to remove. One UAC prompt, fail-open.
|
|
390
|
+
if (process.platform === 'win32' && !msiHandoff) {
|
|
391
|
+
const { getWindowsAuditStatus, disableWindowsAudit } = await Promise.resolve().then(() => __importStar(require('./windowsAudit')));
|
|
392
|
+
const audit = getWindowsAuditStatus();
|
|
393
|
+
const fcdAttributed = /fullcourtdefense/i.test(audit.transcriptionPath || '');
|
|
394
|
+
if ((audit.scriptBlockLogging || audit.transcription) && fcdAttributed) {
|
|
395
|
+
console.log(`${BOLD}› PowerShell audit policies (HKLM) — approve the admin prompt to remove them${RESET}`);
|
|
396
|
+
const result = disableWindowsAudit();
|
|
397
|
+
console.log(result.ok ? `${DIM} ${result.message}${RESET}` : `${YELLOW} ⚠ ${result.message}${RESET}`);
|
|
398
|
+
if (!result.ok)
|
|
399
|
+
failures.push('PowerShell audit policies (HKLM)');
|
|
400
|
+
console.log('');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (msiHandoff) {
|
|
404
|
+
const productCode = findMsiProductCode();
|
|
405
|
+
if (productCode && launchMsiUninstall(productCode)) {
|
|
406
|
+
console.log(`${GREEN}${BOLD}Local teardown done — Windows uninstaller launched.${RESET}`);
|
|
407
|
+
console.log(`${DIM}Approve the Windows elevation prompt (UAC) to finish. That step removes the "FullCourtDefense Updater" SYSTEM task, the HKLM PowerShell audit policies FCD enabled, and the application files under Program Files.${RESET}`);
|
|
408
|
+
console.log(`${YELLOW}If you decline the prompt, those privileged components STAY INSTALLED — re-run the uninstall from Windows Settings → Apps → FullCourtDefense.${RESET}`);
|
|
409
|
+
console.log(`${DIM}When the Windows uninstaller finishes, confirm a clean machine with: npx fullcourtdefense-cli verify-removed${RESET}`);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
console.log(`${YELLOW}${BOLD}Local teardown done, but the Windows uninstaller could not be launched automatically.${RESET}`);
|
|
413
|
+
console.log(`${YELLOW}IMPORTANT: this machine is NOT fully clean yet — the SYSTEM updater task, HKLM audit policies, and the application files are still installed.${RESET}`);
|
|
414
|
+
console.log(`${YELLOW}Finish removal from Windows Settings → Apps → FullCourtDefense → Uninstall.${RESET}`);
|
|
415
|
+
process.exitCode = 1;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
// Non-MSI path: nothing else runs after us, so verify the result instead
|
|
420
|
+
// of asserting it. "Removed" is only printed when the machine actually
|
|
421
|
+
// checks out clean — anything left prints with the exact fix.
|
|
422
|
+
const { collectRemnantChecks } = await Promise.resolve().then(() => __importStar(require('./verifyRemoved')));
|
|
423
|
+
let remnants = [];
|
|
424
|
+
try {
|
|
425
|
+
remnants = collectRemnantChecks().filter(check => check.present);
|
|
426
|
+
}
|
|
427
|
+
catch { /* verification is best-effort — fall back to step-failure reporting */ }
|
|
428
|
+
if (failures.length === 0 && remnants.length === 0) {
|
|
429
|
+
console.log(`${GREEN}${BOLD}Verified: local protection and state fully removed.${RESET}`);
|
|
430
|
+
if (!isMsi) {
|
|
431
|
+
console.log(`${DIM}One step remains — the CLI package itself: npm uninstall -g fullcourtdefense-cli${RESET}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
if (failures.length > 0) {
|
|
436
|
+
console.log(`${YELLOW}${BOLD}Uninstall finished with ${failures.length} step(s) needing attention:${RESET} ${failures.join(', ')}`);
|
|
437
|
+
}
|
|
438
|
+
if (remnants.length > 0) {
|
|
439
|
+
console.log(`${YELLOW}${BOLD}Post-uninstall verification found ${remnants.length} artifact(s) still on this machine:${RESET}`);
|
|
440
|
+
for (const remnant of remnants) {
|
|
441
|
+
console.log(` ${YELLOW}✗${RESET} ${remnant.label}`);
|
|
442
|
+
if (remnant.fix)
|
|
443
|
+
console.log(` ${DIM}fix: ${remnant.fix}${RESET}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
process.exitCode = 1;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
console.log(`${DIM}Verify a clean machine any time with: npx fullcourtdefense-cli verify-removed${RESET}`);
|
|
450
|
+
console.log(`${DIM}Note: per-project config backups are tracked and deleted from CLI v1.22.8+; backups made by older versions inside repos (e.g. .cursor/mcp.json.fcd-backup-*) may need manual deletion.${RESET}`);
|
|
451
|
+
console.log(fleetNotified
|
|
452
|
+
? `${DIM}The fleet console now shows this machine as Uninstalled; an admin can remove the record (Machines → machine → Remove).${RESET}`
|
|
453
|
+
: `${DIM}The machine still appears in the fleet console until an admin deprovisions it (Machines → machine → Deprovision).${RESET}`);
|
|
454
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fullcourtdefense verify-removed` — prove a machine is back to its pre-FCD
|
|
3
|
+
* state after an uninstall. Enumerates every artifact FCD is known to create
|
|
4
|
+
* (scheduled tasks, HKLM/HKCU registry entries, install/state directories,
|
|
5
|
+
* profile files, shell-guard wiring, config backups, wrapped client configs)
|
|
6
|
+
* and reports anything still present, with the command that removes it.
|
|
7
|
+
*
|
|
8
|
+
* Exit code 0 = clean, 1 = remnants found — usable as a CI release gate and
|
|
9
|
+
* runnable AFTER the CLI itself is gone via `npx fullcourtdefense-cli
|
|
10
|
+
* verify-removed`.
|
|
11
|
+
*/
|
|
12
|
+
export interface RemnantCheck {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
present: boolean;
|
|
16
|
+
detail?: string;
|
|
17
|
+
fix?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface VerifyRemovedArgs {
|
|
20
|
+
json?: string;
|
|
21
|
+
}
|
|
22
|
+
/** All known-artifact checks for this platform — reused by `uninstall` for its inline self-verification. */
|
|
23
|
+
export declare function collectRemnantChecks(): RemnantCheck[];
|
|
24
|
+
export declare function verifyRemovedCommand(args: VerifyRemovedArgs): Promise<void>;
|