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.
@@ -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
- * `fullcourtdefense uninstall` — ONE command that reverses `onboard`/`install-all`:
56
- * daemon autostart, self-heal schedule, MCP gateway wraps, IDE hooks, terminal
57
- * guards, and the daily discovery schedule. Each step is best-effort so a
58
- * single failure never leaves the rest of the machine half-uninstalled.
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
- async function uninstallAllCommand(args, config) {
66
- console.log(`\n${BOLD}\x1b[36mFullCourtDefense — uninstall local protection${RESET}`);
67
- console.log(`${DIM}Daemon -> self-heal schedule -> MCP gateways -> IDE hooks -> terminal guards -> discovery schedule.${RESET}\n`);
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
- const home = os.homedir();
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}Local protection removed.${RESET}`);
122
- if (args.purge !== 'true') {
123
- console.log(`${DIM}Machine credentials were kept (re-run \`fullcourtdefense onboard\` to re-protect). Use --purge true to delete all local state.${RESET}`);
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}Uninstall finished with ${failures.length} step(s) needing attention:${RESET} ${failures.join(', ')}`);
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>;