fullcourtdefense-cli 1.26.11 → 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));
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.26.11"
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.11",
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": {