fullcourtdefense-cli 1.26.10 → 1.26.12

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.
@@ -76,3 +76,8 @@ export declare function buildFolderCatalog(options?: {
76
76
  export declare function resolveScanRoots(flag: string | undefined): string[];
77
77
  /** Include config paths for apps that exist on disk even when the MCP file is not created yet. */
78
78
  export declare function discoverScanTargets(cwd: string, extra?: string): ConfigPathCandidate[];
79
+ /**
80
+ * True when the AI *app* is on disk — not when a leftover config folder exists.
81
+ * `~/.codex` / `~/.cursor` without Cursor.exe must not count as installed.
82
+ */
83
+ export declare function detectIdeAppInstalled(clientKey: string, env?: NodeJS.ProcessEnv, home?: string, platform?: NodeJS.Platform): boolean;
@@ -49,6 +49,7 @@ exports.scanRootsForProjectConfigs = scanRootsForProjectConfigs;
49
49
  exports.buildFolderCatalog = buildFolderCatalog;
50
50
  exports.resolveScanRoots = resolveScanRoots;
51
51
  exports.discoverScanTargets = discoverScanTargets;
52
+ exports.detectIdeAppInstalled = detectIdeAppInstalled;
52
53
  const fs = __importStar(require("fs"));
53
54
  const os = __importStar(require("os"));
54
55
  const path = __importStar(require("path"));
@@ -461,3 +462,76 @@ function discoverScanTargets(cwd, extra) {
461
462
  }
462
463
  return [...existing, ...implicit];
463
464
  }
465
+ function fileExists(file) {
466
+ try {
467
+ return fs.existsSync(file) && fs.statSync(file).isFile();
468
+ }
469
+ catch {
470
+ return false;
471
+ }
472
+ }
473
+ function dirHasFile(dir, names) {
474
+ return names.some(name => fileExists(path.join(dir, name)));
475
+ }
476
+ function firstExistingCodexBin(localAppData) {
477
+ const binRoot = path.join(localAppData, 'OpenAI', 'Codex', 'bin');
478
+ try {
479
+ if (!fs.statSync(binRoot).isDirectory())
480
+ return false;
481
+ for (const name of fs.readdirSync(binRoot)) {
482
+ if (fileExists(path.join(binRoot, name, 'codex.exe')) || fileExists(path.join(binRoot, name, 'codex')))
483
+ return true;
484
+ }
485
+ }
486
+ catch { /* missing */ }
487
+ return fileExists(path.join(localAppData, 'OpenAI', 'Codex', 'codex.exe'));
488
+ }
489
+ /**
490
+ * True when the AI *app* is on disk — not when a leftover config folder exists.
491
+ * `~/.codex` / `~/.cursor` without Cursor.exe must not count as installed.
492
+ */
493
+ function detectIdeAppInstalled(clientKey, env = process.env, home = os.homedir(), platform = process.platform) {
494
+ const localAppData = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
495
+ const programFiles = env.ProgramFiles || 'C:\\Program Files';
496
+ const programFilesX86 = env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
497
+ if (clientKey === 'cursor') {
498
+ if (platform === 'darwin' && fs.existsSync('/Applications/Cursor.app'))
499
+ return true;
500
+ return dirHasFile(path.join(localAppData, 'Programs', 'cursor'), ['Cursor.exe'])
501
+ || dirHasFile(path.join(localAppData, 'Programs', 'Cursor'), ['Cursor.exe'])
502
+ || dirHasFile(path.join(programFiles, 'Cursor'), ['Cursor.exe']);
503
+ }
504
+ if (clientKey === 'vscode') {
505
+ if (platform === 'darwin' && fs.existsSync('/Applications/Visual Studio Code.app'))
506
+ return true;
507
+ return dirHasFile(path.join(localAppData, 'Programs', 'Microsoft VS Code'), ['Code.exe'])
508
+ || dirHasFile(path.join(programFiles, 'Microsoft VS Code'), ['Code.exe'])
509
+ || dirHasFile(path.join(programFilesX86, 'Microsoft VS Code'), ['Code.exe']);
510
+ }
511
+ if (clientKey === 'windsurf') {
512
+ if (platform === 'darwin' && fs.existsSync('/Applications/Windsurf.app'))
513
+ return true;
514
+ return dirHasFile(path.join(localAppData, 'Programs', 'Windsurf'), ['Windsurf.exe'])
515
+ || dirHasFile(path.join(localAppData, 'Programs', 'windsurf'), ['Windsurf.exe']);
516
+ }
517
+ if (clientKey === 'claude_desktop') {
518
+ if (platform === 'darwin' && fs.existsSync('/Applications/Claude.app'))
519
+ return true;
520
+ if (platform === 'win32' && claudeDesktopMsixCandidates().length > 0)
521
+ return true;
522
+ if (platform === 'win32' && claudeDesktopWindowsInstallDirs(env, home).some(dir => fs.existsSync(dir)))
523
+ return true;
524
+ return false;
525
+ }
526
+ if (clientKey === 'claude_code') {
527
+ return fileExists(path.join(home, '.local', 'bin', 'claude.exe'))
528
+ || fileExists(path.join(home, '.local', 'bin', 'claude'))
529
+ || dirHasFile(path.join(localAppData, 'Programs', 'claude'), ['claude.exe']);
530
+ }
531
+ if (clientKey === 'codex') {
532
+ if (platform === 'darwin' && (fs.existsSync('/Applications/Codex.app') || fs.existsSync('/Applications/ChatGPT Codex.app')))
533
+ return true;
534
+ return firstExistingCodexBin(localAppData);
535
+ }
536
+ return false;
537
+ }
@@ -13,6 +13,8 @@ export interface ClientCoverageRow {
13
13
  cursorHooksInstalled: boolean;
14
14
  cursorHookEvents: string[];
15
15
  cursorHookShadow: boolean;
16
+ /** False when only leftover config exists — the app binary is not on disk. */
17
+ appInstalled: boolean;
16
18
  }
17
19
  export interface ProxyClassifiableServer {
18
20
  serverName: string;
@@ -45,6 +45,7 @@ exports.buildClientCoverage = buildClientCoverage;
45
45
  const fs = __importStar(require("fs"));
46
46
  const os = __importStar(require("os"));
47
47
  const path = __importStar(require("path"));
48
+ const discoverPaths_1 = require("./discoverPaths");
48
49
  /**
49
50
  * Config sources that no AI client actually loads. A plain `mcp.json` at a
50
51
  * repo root is documentation/sample material — Cursor, Claude Code, VS Code,
@@ -258,6 +259,12 @@ function buildClientCoverage(scanned, servers, cwd) {
258
259
  list.push(server);
259
260
  serversByConfig.set(key, list);
260
261
  }
262
+ const appInstalledByKey = new Map();
263
+ const appInstalledFor = (clientKey) => {
264
+ if (!appInstalledByKey.has(clientKey))
265
+ appInstalledByKey.set(clientKey, (0, discoverPaths_1.detectIdeAppInstalled)(clientKey));
266
+ return appInstalledByKey.get(clientKey) === true;
267
+ };
261
268
  const rows = [];
262
269
  for (const item of scanned) {
263
270
  const list = serversByConfig.get(item.path.toLowerCase()) || [];
@@ -273,6 +280,7 @@ function buildClientCoverage(scanned, servers, cwd) {
273
280
  cursorHooksInstalled: clientKey === 'cursor' ? hooks.installed : false,
274
281
  cursorHookEvents: clientKey === 'cursor' ? hooks.events : [],
275
282
  cursorHookShadow: clientKey === 'cursor' ? hooks.shadow : false,
283
+ appInstalled: appInstalledFor(clientKey),
276
284
  });
277
285
  }
278
286
  // Cursor hooks apply globally even if only project mcp.json was scanned
@@ -287,6 +295,7 @@ function buildClientCoverage(scanned, servers, cwd) {
287
295
  cursorHooksInstalled: true,
288
296
  cursorHookEvents: hooks.events,
289
297
  cursorHookShadow: hooks.shadow,
298
+ appInstalled: appInstalledFor('cursor'),
290
299
  });
291
300
  }
292
301
  return rows.sort((a, b) => a.client.localeCompare(b.client));
@@ -2656,12 +2656,16 @@ function protectAllTargetFiles(extra, allowedClientKeys) {
2656
2656
  seen.add(key);
2657
2657
  files.push(candidate);
2658
2658
  }
2659
- // Admin-selected roots are explicit protection scope. Default/`auto` roots
2660
- // remain discovery-only and are intentionally not consulted here.
2661
- const recursive = (0, discoverPaths_1.scanRootsForProjectConfigs)(extraScanRoots, { excludedRoots: disabledScanRoots });
2659
+ // Same trees discovery scores (`--scan-root auto` + admin extras). MSI
2660
+ // onboard cwd is the install folder, so without this walk project MCP
2661
+ // under C:\dev stays direct while the fleet matrix paints the IDE Exposed.
2662
+ // Skip clientKey `other` — repo-root mcp.json is reference inventory, not
2663
+ // a live client config.
2664
+ const protectionRoots = [...extraScanRoots, ...(0, discoverPaths_1.defaultScanRoots)()];
2665
+ const recursive = (0, discoverPaths_1.scanRootsForProjectConfigs)(protectionRoots, { excludedRoots: disabledScanRoots });
2662
2666
  for (const candidate of recursive.candidates) {
2663
2667
  const key = (0, discoverPaths_1.normalizedPath)(candidate.path);
2664
- if (seen.has(key) || !allow(candidate.clientKey) || (0, discoverPaths_1.isExcludedPath)(candidate.path, disabledScanRoots))
2668
+ if (seen.has(key) || candidate.clientKey === 'other' || !allow(candidate.clientKey) || (0, discoverPaths_1.isExcludedPath)(candidate.path, disabledScanRoots))
2665
2669
  continue;
2666
2670
  seen.add(key);
2667
2671
  files.push(candidate);
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.26.10"
2
+ "version": "1.26.12"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.26.10",
3
+ "version": "1.26.12",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {