lucidscan 0.5.12__py3-none-any.whl

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.
Files changed (91) hide show
  1. lucidscan/__init__.py +12 -0
  2. lucidscan/bootstrap/__init__.py +26 -0
  3. lucidscan/bootstrap/paths.py +160 -0
  4. lucidscan/bootstrap/platform.py +111 -0
  5. lucidscan/bootstrap/validation.py +76 -0
  6. lucidscan/bootstrap/versions.py +119 -0
  7. lucidscan/cli/__init__.py +50 -0
  8. lucidscan/cli/__main__.py +8 -0
  9. lucidscan/cli/arguments.py +405 -0
  10. lucidscan/cli/commands/__init__.py +64 -0
  11. lucidscan/cli/commands/autoconfigure.py +294 -0
  12. lucidscan/cli/commands/help.py +69 -0
  13. lucidscan/cli/commands/init.py +656 -0
  14. lucidscan/cli/commands/list_scanners.py +59 -0
  15. lucidscan/cli/commands/scan.py +307 -0
  16. lucidscan/cli/commands/serve.py +142 -0
  17. lucidscan/cli/commands/status.py +84 -0
  18. lucidscan/cli/commands/validate.py +105 -0
  19. lucidscan/cli/config_bridge.py +152 -0
  20. lucidscan/cli/exit_codes.py +17 -0
  21. lucidscan/cli/runner.py +284 -0
  22. lucidscan/config/__init__.py +29 -0
  23. lucidscan/config/ignore.py +178 -0
  24. lucidscan/config/loader.py +431 -0
  25. lucidscan/config/models.py +316 -0
  26. lucidscan/config/validation.py +645 -0
  27. lucidscan/core/__init__.py +3 -0
  28. lucidscan/core/domain_runner.py +463 -0
  29. lucidscan/core/git.py +174 -0
  30. lucidscan/core/logging.py +34 -0
  31. lucidscan/core/models.py +207 -0
  32. lucidscan/core/streaming.py +340 -0
  33. lucidscan/core/subprocess_runner.py +164 -0
  34. lucidscan/detection/__init__.py +21 -0
  35. lucidscan/detection/detector.py +154 -0
  36. lucidscan/detection/frameworks.py +270 -0
  37. lucidscan/detection/languages.py +328 -0
  38. lucidscan/detection/tools.py +229 -0
  39. lucidscan/generation/__init__.py +15 -0
  40. lucidscan/generation/config_generator.py +275 -0
  41. lucidscan/generation/package_installer.py +330 -0
  42. lucidscan/mcp/__init__.py +20 -0
  43. lucidscan/mcp/formatter.py +510 -0
  44. lucidscan/mcp/server.py +297 -0
  45. lucidscan/mcp/tools.py +1049 -0
  46. lucidscan/mcp/watcher.py +237 -0
  47. lucidscan/pipeline/__init__.py +17 -0
  48. lucidscan/pipeline/executor.py +187 -0
  49. lucidscan/pipeline/parallel.py +181 -0
  50. lucidscan/plugins/__init__.py +40 -0
  51. lucidscan/plugins/coverage/__init__.py +28 -0
  52. lucidscan/plugins/coverage/base.py +160 -0
  53. lucidscan/plugins/coverage/coverage_py.py +454 -0
  54. lucidscan/plugins/coverage/istanbul.py +411 -0
  55. lucidscan/plugins/discovery.py +107 -0
  56. lucidscan/plugins/enrichers/__init__.py +61 -0
  57. lucidscan/plugins/enrichers/base.py +63 -0
  58. lucidscan/plugins/linters/__init__.py +26 -0
  59. lucidscan/plugins/linters/base.py +125 -0
  60. lucidscan/plugins/linters/biome.py +448 -0
  61. lucidscan/plugins/linters/checkstyle.py +393 -0
  62. lucidscan/plugins/linters/eslint.py +368 -0
  63. lucidscan/plugins/linters/ruff.py +498 -0
  64. lucidscan/plugins/reporters/__init__.py +45 -0
  65. lucidscan/plugins/reporters/base.py +30 -0
  66. lucidscan/plugins/reporters/json_reporter.py +79 -0
  67. lucidscan/plugins/reporters/sarif_reporter.py +303 -0
  68. lucidscan/plugins/reporters/summary_reporter.py +61 -0
  69. lucidscan/plugins/reporters/table_reporter.py +81 -0
  70. lucidscan/plugins/scanners/__init__.py +57 -0
  71. lucidscan/plugins/scanners/base.py +60 -0
  72. lucidscan/plugins/scanners/checkov.py +484 -0
  73. lucidscan/plugins/scanners/opengrep.py +464 -0
  74. lucidscan/plugins/scanners/trivy.py +492 -0
  75. lucidscan/plugins/test_runners/__init__.py +27 -0
  76. lucidscan/plugins/test_runners/base.py +111 -0
  77. lucidscan/plugins/test_runners/jest.py +381 -0
  78. lucidscan/plugins/test_runners/karma.py +481 -0
  79. lucidscan/plugins/test_runners/playwright.py +434 -0
  80. lucidscan/plugins/test_runners/pytest.py +598 -0
  81. lucidscan/plugins/type_checkers/__init__.py +27 -0
  82. lucidscan/plugins/type_checkers/base.py +106 -0
  83. lucidscan/plugins/type_checkers/mypy.py +355 -0
  84. lucidscan/plugins/type_checkers/pyright.py +313 -0
  85. lucidscan/plugins/type_checkers/typescript.py +280 -0
  86. lucidscan-0.5.12.dist-info/METADATA +242 -0
  87. lucidscan-0.5.12.dist-info/RECORD +91 -0
  88. lucidscan-0.5.12.dist-info/WHEEL +5 -0
  89. lucidscan-0.5.12.dist-info/entry_points.txt +34 -0
  90. lucidscan-0.5.12.dist-info/licenses/LICENSE +201 -0
  91. lucidscan-0.5.12.dist-info/top_level.txt +1 -0
@@ -0,0 +1,656 @@
1
+ """Init command implementation.
2
+
3
+ Configure AI tools (Claude Code, Cursor) to use LucidScan via MCP.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import shutil
10
+ import sys
11
+ from argparse import Namespace
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
14
+
15
+ if TYPE_CHECKING:
16
+ from lucidscan.config.models import LucidScanConfig
17
+
18
+ from lucidscan.cli.commands import Command
19
+ from lucidscan.cli.exit_codes import EXIT_SUCCESS, EXIT_INVALID_USAGE
20
+ from lucidscan.core.logging import get_logger
21
+
22
+ LOGGER = get_logger(__name__)
23
+
24
+ # MCP server arguments for LucidScan
25
+ LUCIDSCAN_MCP_ARGS = ["serve", "--mcp"]
26
+
27
+ # Instructions to add to CLAUDE.md for proactive lucidscan usage
28
+ LUCIDSCAN_CLAUDE_MD_INSTRUCTIONS = """
29
+ ## Code Quality with LucidScan
30
+
31
+ IMPORTANT: You MUST run LucidScan after completing code changes. This is required, not optional.
32
+
33
+ ### When to Scan
34
+
35
+ Run a scan after completing a batch of related code changes (not after each individual edit):
36
+ - After finishing a feature or fix (before reporting completion to user)
37
+ - Before creating a commit
38
+ - When the user asks you to check code quality
39
+
40
+ ### Fast Scan (After Code Changes)
41
+
42
+ After completing code modifications, run a scan (scans changed files by default):
43
+ ```
44
+ scan(domains=["linting", "type_checking", "sast", "sca"])
45
+ ```
46
+
47
+ For full project scan:
48
+ ```
49
+ scan(domains=["linting", "type_checking", "sast", "sca"], all_files=true)
50
+ ```
51
+
52
+ ### Full Scan (Before Commit)
53
+
54
+ Before committing, run a full scan including tests:
55
+ ```
56
+ scan(domains=["all"])
57
+ ```
58
+
59
+ ### Fixing Issues
60
+
61
+ 1. Run `scan(fix=true)` to auto-fix linting issues
62
+ 2. Use `get_fix_instructions(issue_id)` for detailed guidance on specific issues
63
+ 3. Re-scan after manual fixes to verify resolution
64
+
65
+ ### Workflow Example
66
+
67
+ 1. Make code changes (multiple edits)
68
+ 2. When done with changes, run `scan(domains=["linting", "type_checking"], fix=true)`
69
+ 3. Fix any remaining issues that couldn't be auto-fixed
70
+ 4. Re-scan to verify all issues resolved
71
+ 5. Report completion to user
72
+ """
73
+
74
+ LUCIDSCAN_CLAUDE_MD_MARKER = "## Code Quality with LucidScan"
75
+
76
+ # Cursor rules for proactive lucidscan usage
77
+ LUCIDSCAN_CURSOR_RULES = """---
78
+ description: LucidScan code quality rules
79
+ globs: ["**/*.py", "**/*.ts", "**/*.js", "**/*.tsx", "**/*.jsx"]
80
+ alwaysApply: true
81
+ ---
82
+
83
+ # LucidScan Code Quality
84
+
85
+ IMPORTANT: You MUST run LucidScan after completing code changes. This is required, not optional.
86
+
87
+ ## When to Scan
88
+
89
+ Run a scan after completing a batch of related code changes (not after each individual edit):
90
+ - After finishing a feature or fix (before reporting completion to user)
91
+ - Before creating a commit
92
+ - When the user asks you to check code quality
93
+
94
+ ## Fast Scan (After Code Changes)
95
+
96
+ After completing code modifications, run a scan (scans changed files by default):
97
+ ```
98
+ scan(domains=["linting", "type_checking", "sast", "sca"])
99
+ ```
100
+
101
+ For full project scan:
102
+ ```
103
+ scan(domains=["linting", "type_checking", "sast", "sca"], all_files=true)
104
+ ```
105
+
106
+ ## Full Scan (Before Commit)
107
+
108
+ Before committing, run a full scan including tests:
109
+ ```
110
+ scan(domains=["all"])
111
+ ```
112
+
113
+ ## Fixing Issues
114
+
115
+ 1. Run `scan(fix=true)` to auto-fix linting issues
116
+ 2. Use `get_fix_instructions(issue_id)` for detailed guidance on specific issues
117
+ 3. Re-scan after manual fixes to verify resolution
118
+
119
+ ## Workflow Example
120
+
121
+ 1. Make code changes (multiple edits)
122
+ 2. When done with changes, run `scan(domains=["linting", "type_checking"], fix=true)`
123
+ 3. Fix any remaining issues that couldn't be auto-fixed
124
+ 4. Re-scan to verify all issues resolved
125
+ 5. Report completion to user
126
+ """
127
+
128
+ class InitCommand(Command):
129
+ """Configure AI tools to use LucidScan via MCP."""
130
+
131
+ def __init__(self, version: str):
132
+ """Initialize InitCommand.
133
+
134
+ Args:
135
+ version: Current lucidscan version string.
136
+ """
137
+ self._version = version
138
+
139
+ @property
140
+ def name(self) -> str:
141
+ """Command identifier."""
142
+ return "init"
143
+
144
+ def execute(self, args: Namespace, config: "LucidScanConfig | None" = None) -> int:
145
+ """Execute the init command.
146
+
147
+ Args:
148
+ args: Parsed command-line arguments.
149
+ config: Optional LucidScan configuration (unused).
150
+
151
+ Returns:
152
+ Exit code.
153
+ """
154
+ # Determine which tools to configure
155
+ configure_claude = getattr(args, "claude_code", False)
156
+ configure_cursor = getattr(args, "cursor", False)
157
+ configure_all = getattr(args, "init_all", False)
158
+
159
+ if configure_all:
160
+ configure_claude = True
161
+ configure_cursor = True
162
+
163
+ if not configure_claude and not configure_cursor:
164
+ print("No AI tool specified. Use --claude-code, --cursor, or --all.")
165
+ print("\nRun 'lucidscan init --help' for more options.")
166
+ return EXIT_INVALID_USAGE
167
+
168
+ dry_run = getattr(args, "dry_run", False)
169
+ force = getattr(args, "force", False)
170
+ remove = getattr(args, "remove", False)
171
+
172
+ success = True
173
+
174
+ if configure_claude:
175
+ if not self._setup_claude_code(dry_run, force, remove):
176
+ success = False
177
+
178
+ if configure_cursor:
179
+ if not self._setup_cursor(dry_run, force, remove):
180
+ success = False
181
+
182
+ if success and not dry_run:
183
+ print("\nRestart your AI tool to apply changes.")
184
+
185
+ return EXIT_SUCCESS if success else EXIT_INVALID_USAGE
186
+
187
+ def _setup_claude_code(
188
+ self,
189
+ dry_run: bool = False,
190
+ force: bool = False,
191
+ remove: bool = False,
192
+ ) -> bool:
193
+ """Configure Claude Code MCP settings in project .mcp.json.
194
+
195
+ Args:
196
+ dry_run: If True, only show what would be done.
197
+ force: If True, overwrite existing config.
198
+ remove: If True, remove LucidScan from config.
199
+
200
+ Returns:
201
+ True if successful.
202
+ """
203
+ print("Configuring Claude Code (.mcp.json)...")
204
+
205
+ config_path = self._get_claude_code_config_path()
206
+ if config_path is None:
207
+ print(" Could not determine Claude Code config location.")
208
+ return False
209
+
210
+ mcp_success = self._configure_mcp_tool(
211
+ tool_name="Claude Code",
212
+ config_path=config_path,
213
+ config_key="mcpServers",
214
+ dry_run=dry_run,
215
+ force=force,
216
+ remove=remove,
217
+ use_portable_path=True, # .mcp.json is version controlled
218
+ )
219
+
220
+ # Also configure CLAUDE.md with instructions
221
+ claude_md_success = self._configure_claude_md(
222
+ dry_run=dry_run,
223
+ force=force,
224
+ remove=remove,
225
+ )
226
+
227
+ return mcp_success and claude_md_success
228
+
229
+ def _setup_cursor(
230
+ self,
231
+ dry_run: bool = False,
232
+ force: bool = False,
233
+ remove: bool = False,
234
+ ) -> bool:
235
+ """Configure Cursor MCP settings.
236
+
237
+ Args:
238
+ dry_run: If True, only show what would be done.
239
+ force: If True, overwrite existing config.
240
+ remove: If True, remove LucidScan from config.
241
+
242
+ Returns:
243
+ True if successful.
244
+ """
245
+ print("Configuring Cursor...")
246
+
247
+ config_path = self._get_cursor_config_path()
248
+ if config_path is None:
249
+ print(" Could not determine Cursor config location.")
250
+ return False
251
+
252
+ mcp_success = self._configure_mcp_tool(
253
+ tool_name="Cursor",
254
+ config_path=config_path,
255
+ config_key="mcpServers",
256
+ dry_run=dry_run,
257
+ force=force,
258
+ remove=remove,
259
+ )
260
+
261
+ # Configure Cursor rules for automatic scanning
262
+ rules_success = self._configure_cursor_rules(
263
+ dry_run=dry_run,
264
+ force=force,
265
+ remove=remove,
266
+ )
267
+
268
+ return mcp_success and rules_success
269
+
270
+ def _find_lucidscan_path(self, portable: bool = False) -> Optional[str]:
271
+ """Find the lucidscan executable path.
272
+
273
+ Searches in order:
274
+ 1. PATH via shutil.which
275
+ 2. Same directory as current Python interpreter (for venv installs)
276
+ 3. Scripts directory on Windows
277
+
278
+ Args:
279
+ portable: If True, return a relative path suitable for version control.
280
+
281
+ Returns:
282
+ Path to lucidscan executable, or None if not found.
283
+ """
284
+ # First try PATH (only if not looking for portable path)
285
+ if not portable:
286
+ lucidscan_path = shutil.which("lucidscan")
287
+ if lucidscan_path:
288
+ return lucidscan_path
289
+
290
+ # Try to find in the same directory as the Python interpreter
291
+ # This handles venv installations where lucidscan isn't in global PATH
292
+ python_dir = Path(sys.executable).parent
293
+ cwd = Path.cwd()
294
+
295
+ if sys.platform == "win32":
296
+ # On Windows, check both Scripts and the python directory
297
+ candidates = [
298
+ python_dir / "lucidscan.exe",
299
+ python_dir / "Scripts" / "lucidscan.exe",
300
+ ]
301
+ else:
302
+ # On Unix-like systems
303
+ candidates = [
304
+ python_dir / "lucidscan",
305
+ ]
306
+
307
+ for candidate in candidates:
308
+ if candidate.exists():
309
+ if portable:
310
+ # Try to make it relative to cwd for version control
311
+ try:
312
+ relative = candidate.relative_to(cwd)
313
+ return str(relative)
314
+ except ValueError:
315
+ # Not relative to cwd, can't use portable path
316
+ pass
317
+ else:
318
+ return str(candidate)
319
+
320
+ # For portable, fall back to just "lucidscan"
321
+ if portable:
322
+ return None
323
+
324
+ return None
325
+
326
+ def _build_mcp_config(self, lucidscan_path: Optional[str]) -> dict:
327
+ """Build MCP server configuration.
328
+
329
+ Args:
330
+ lucidscan_path: Full path to lucidscan executable, or None.
331
+
332
+ Returns:
333
+ MCP server configuration dict.
334
+ """
335
+ command = lucidscan_path if lucidscan_path else "lucidscan"
336
+ return {
337
+ "command": command,
338
+ "args": LUCIDSCAN_MCP_ARGS.copy(),
339
+ }
340
+
341
+ def _configure_mcp_tool(
342
+ self,
343
+ tool_name: str,
344
+ config_path: Path,
345
+ config_key: str,
346
+ dry_run: bool = False,
347
+ force: bool = False,
348
+ remove: bool = False,
349
+ use_portable_path: bool = False,
350
+ ) -> bool:
351
+ """Configure an MCP-compatible tool.
352
+
353
+ Args:
354
+ tool_name: Name of the tool for display.
355
+ config_path: Path to the config file.
356
+ config_key: Key in the config for MCP servers.
357
+ dry_run: If True, only show what would be done.
358
+ force: If True, overwrite existing config.
359
+ remove: If True, remove LucidScan from config.
360
+ use_portable_path: If True, use relative path for version control.
361
+
362
+ Returns:
363
+ True if successful.
364
+ """
365
+ # Find lucidscan executable
366
+ lucidscan_path = self._find_lucidscan_path(portable=use_portable_path)
367
+ if lucidscan_path:
368
+ print(f" Using lucidscan command: {lucidscan_path}")
369
+ elif not dry_run:
370
+ print(" Warning: 'lucidscan' command not found in PATH or venv.")
371
+ print(" Using 'lucidscan' as command (must be in PATH at runtime).")
372
+
373
+ # Read existing config
374
+ config, error = self._read_json_config(config_path)
375
+ if error and not remove:
376
+ # For new config, start fresh
377
+ config = {}
378
+
379
+ # Get or create the MCP servers section
380
+ mcp_servers = config.get(config_key, {})
381
+
382
+ if remove:
383
+ # Remove LucidScan from config
384
+ if "lucidscan" in mcp_servers:
385
+ if dry_run:
386
+ print(f" Would remove lucidscan from {config_path}")
387
+ else:
388
+ del mcp_servers["lucidscan"]
389
+ config[config_key] = mcp_servers
390
+ if not mcp_servers:
391
+ del config[config_key]
392
+ self._write_json_config(config_path, config)
393
+ print(f" Removed lucidscan from {config_path}")
394
+ else:
395
+ print(f" lucidscan not found in {config_path}")
396
+ return True
397
+
398
+ # Check if LucidScan is already configured
399
+ if "lucidscan" in mcp_servers and not force:
400
+ print(f" LucidScan already configured in {config_path}")
401
+ print(" Use --force to overwrite.")
402
+ return True
403
+
404
+ # Add LucidScan config with found path
405
+ mcp_config = self._build_mcp_config(lucidscan_path)
406
+ mcp_servers["lucidscan"] = mcp_config
407
+ config[config_key] = mcp_servers
408
+
409
+ if dry_run:
410
+ print(f" Would write to {config_path}:")
411
+ print(f" {json.dumps(config, indent=2)}")
412
+ return True
413
+
414
+ # Ensure parent directory exists
415
+ config_path.parent.mkdir(parents=True, exist_ok=True)
416
+
417
+ # Write config
418
+ success = self._write_json_config(config_path, config)
419
+ if success:
420
+ print(f" Added lucidscan to {config_path}")
421
+ self._print_available_tools()
422
+ return success
423
+
424
+ def _configure_claude_md(
425
+ self,
426
+ dry_run: bool = False,
427
+ force: bool = False,
428
+ remove: bool = False,
429
+ ) -> bool:
430
+ """Configure CLAUDE.md with lucidscan instructions.
431
+
432
+ Args:
433
+ dry_run: If True, only show what would be done.
434
+ force: If True, overwrite existing instructions.
435
+ remove: If True, remove lucidscan instructions.
436
+
437
+ Returns:
438
+ True if successful.
439
+ """
440
+ claude_md_path = Path.cwd() / ".claude" / "CLAUDE.md"
441
+
442
+ print("Configuring CLAUDE.md...")
443
+
444
+ # Read existing content
445
+ existing_content = ""
446
+ if claude_md_path.exists():
447
+ try:
448
+ existing_content = claude_md_path.read_text()
449
+ except Exception as e:
450
+ print(f" Error reading {claude_md_path}: {e}")
451
+ return False
452
+
453
+ has_lucidscan_section = LUCIDSCAN_CLAUDE_MD_MARKER in existing_content
454
+
455
+ if remove:
456
+ if has_lucidscan_section:
457
+ if dry_run:
458
+ print(f" Would remove lucidscan instructions from {claude_md_path}")
459
+ else:
460
+ # Remove the lucidscan section
461
+ new_content = self._remove_lucidscan_section(existing_content)
462
+ try:
463
+ claude_md_path.write_text(new_content)
464
+ print(f" Removed lucidscan instructions from {claude_md_path}")
465
+ except Exception as e:
466
+ print(f" Error writing {claude_md_path}: {e}")
467
+ return False
468
+ else:
469
+ print(f" Lucidscan instructions not found in {claude_md_path}")
470
+ return True
471
+
472
+ if has_lucidscan_section and not force:
473
+ print(f" Lucidscan instructions already in {claude_md_path}")
474
+ print(" Use --force to overwrite.")
475
+ return True
476
+
477
+ # Build new content
478
+ if has_lucidscan_section:
479
+ # Replace existing section
480
+ new_content = self._remove_lucidscan_section(existing_content)
481
+ new_content = new_content.rstrip() + LUCIDSCAN_CLAUDE_MD_INSTRUCTIONS
482
+ else:
483
+ # Append to existing content
484
+ new_content = existing_content.rstrip() + LUCIDSCAN_CLAUDE_MD_INSTRUCTIONS
485
+
486
+ if dry_run:
487
+ print(f" Would add lucidscan instructions to {claude_md_path}")
488
+ return True
489
+
490
+ # Ensure directory exists
491
+ claude_md_path.parent.mkdir(parents=True, exist_ok=True)
492
+
493
+ try:
494
+ claude_md_path.write_text(new_content)
495
+ print(f" Added lucidscan instructions to {claude_md_path}")
496
+ return True
497
+ except Exception as e:
498
+ print(f" Error writing {claude_md_path}: {e}")
499
+ return False
500
+
501
+ def _remove_lucidscan_section(self, content: str) -> str:
502
+ """Remove the lucidscan section from CLAUDE.md content.
503
+
504
+ Args:
505
+ content: The current CLAUDE.md content.
506
+
507
+ Returns:
508
+ Content with lucidscan section removed.
509
+ """
510
+ lines = content.split("\n")
511
+ new_lines = []
512
+ in_lucidscan_section = False
513
+
514
+ for line in lines:
515
+ if line.strip() == LUCIDSCAN_CLAUDE_MD_MARKER.strip():
516
+ in_lucidscan_section = True
517
+ continue
518
+ if in_lucidscan_section:
519
+ # Check if we've hit another section (line starting with ##)
520
+ if line.startswith("## ") and LUCIDSCAN_CLAUDE_MD_MARKER.strip() not in line:
521
+ in_lucidscan_section = False
522
+ new_lines.append(line)
523
+ # Skip lines in the lucidscan section
524
+ continue
525
+ new_lines.append(line)
526
+
527
+ return "\n".join(new_lines)
528
+
529
+ def _configure_cursor_rules(
530
+ self,
531
+ dry_run: bool = False,
532
+ force: bool = False,
533
+ remove: bool = False,
534
+ ) -> bool:
535
+ """Configure Cursor rules for automatic scanning.
536
+
537
+ Args:
538
+ dry_run: If True, only show what would be done.
539
+ force: If True, overwrite existing rules.
540
+ remove: If True, remove lucidscan rules.
541
+
542
+ Returns:
543
+ True if successful.
544
+ """
545
+ rules_dir = Path.cwd() / ".cursor" / "rules"
546
+ rules_file = rules_dir / "lucidscan.mdc"
547
+
548
+ print("Configuring Cursor rules...")
549
+
550
+ if remove:
551
+ if rules_file.exists():
552
+ if dry_run:
553
+ print(f" Would remove {rules_file}")
554
+ else:
555
+ rules_file.unlink()
556
+ print(f" Removed {rules_file}")
557
+ else:
558
+ print(f" LucidScan rules not found at {rules_file}")
559
+ return True
560
+
561
+ if rules_file.exists() and not force:
562
+ print(f" LucidScan rules already exist at {rules_file}")
563
+ print(" Use --force to overwrite.")
564
+ return True
565
+
566
+ if dry_run:
567
+ print(f" Would create {rules_file}")
568
+ return True
569
+
570
+ rules_dir.mkdir(parents=True, exist_ok=True)
571
+ try:
572
+ rules_file.write_text(LUCIDSCAN_CURSOR_RULES.lstrip())
573
+ print(f" Created {rules_file}")
574
+ return True
575
+ except Exception as e:
576
+ print(f" Error writing {rules_file}: {e}")
577
+ return False
578
+
579
+ def _get_claude_code_config_path(self) -> Optional[Path]:
580
+ """Get the Claude Code MCP config file path.
581
+
582
+ Returns:
583
+ Path to .mcp.json at project root.
584
+ """
585
+ # Claude Code project-scoped MCP servers in .mcp.json
586
+ return Path.cwd() / ".mcp.json"
587
+
588
+ def _get_cursor_config_path(self) -> Optional[Path]:
589
+ """Get the Cursor MCP config file path.
590
+
591
+ Returns:
592
+ Path to config file or None if not determinable.
593
+ """
594
+ home = Path.home()
595
+
596
+ if sys.platform == "win32":
597
+ # Windows: %USERPROFILE%\.cursor\mcp.json
598
+ return home / ".cursor" / "mcp.json"
599
+ elif sys.platform == "darwin":
600
+ # macOS: ~/.cursor/mcp.json
601
+ return home / ".cursor" / "mcp.json"
602
+ else:
603
+ # Linux: ~/.cursor/mcp.json
604
+ return home / ".cursor" / "mcp.json"
605
+
606
+ def _read_json_config(self, path: Path) -> Tuple[Dict[str, Any], Optional[str]]:
607
+ """Read a JSON config file.
608
+
609
+ Args:
610
+ path: Path to the config file.
611
+
612
+ Returns:
613
+ Tuple of (config dict, error message or None).
614
+ """
615
+ if not path.exists():
616
+ return {}, f"Config file does not exist: {path}"
617
+
618
+ try:
619
+ with open(path, "r") as f:
620
+ content = f.read().strip()
621
+ if not content:
622
+ return {}, None
623
+ return json.loads(content), None
624
+ except json.JSONDecodeError as e:
625
+ return {}, f"Invalid JSON in {path}: {e}"
626
+ except Exception as e:
627
+ return {}, f"Error reading {path}: {e}"
628
+
629
+ def _write_json_config(self, path: Path, config: Dict[str, Any]) -> bool:
630
+ """Write a JSON config file.
631
+
632
+ Args:
633
+ path: Path to the config file.
634
+ config: Configuration dictionary.
635
+
636
+ Returns:
637
+ True if successful.
638
+ """
639
+ try:
640
+ with open(path, "w") as f:
641
+ json.dump(config, f, indent=2)
642
+ f.write("\n")
643
+ return True
644
+ except Exception as e:
645
+ print(f" Error writing {path}: {e}")
646
+ return False
647
+
648
+ def _print_available_tools(self) -> None:
649
+ """Print available MCP tools."""
650
+ print("\n Available MCP tools:")
651
+ print(" - scan: Run quality checks on the codebase")
652
+ print(" - check_file: Check a specific file")
653
+ print(" - get_fix_instructions: Get detailed fix guidance")
654
+ print(" - apply_fix: Auto-fix linting issues")
655
+ print(" - get_status: Show LucidScan configuration")
656
+ print(" - autoconfigure: Get instructions for generating lucidscan.yml")
@@ -0,0 +1,59 @@
1
+ """List scanners command implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from argparse import Namespace
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from lucidscan.config.models import LucidScanConfig
10
+
11
+ from lucidscan.cli.commands import Command
12
+ from lucidscan.cli.exit_codes import EXIT_SUCCESS
13
+ from lucidscan.plugins.scanners import discover_scanner_plugins
14
+
15
+
16
+ class ListScannersCommand(Command):
17
+ """Lists all available scanner plugins."""
18
+
19
+ @property
20
+ def name(self) -> str:
21
+ """Command identifier."""
22
+ return "list_scanners"
23
+
24
+ def execute(self, args: Namespace, config: "LucidScanConfig | None" = None) -> int:
25
+ """Execute the list-scanners command.
26
+
27
+ Displays all available scanner plugins with their domains and versions.
28
+
29
+ Args:
30
+ args: Parsed command-line arguments.
31
+ config: Optional LucidScan configuration (unused).
32
+
33
+ Returns:
34
+ Exit code (always 0 for list-scanners).
35
+ """
36
+ plugins = discover_scanner_plugins()
37
+
38
+ print("Available scanner plugins:")
39
+ print()
40
+
41
+ if plugins:
42
+ for name, plugin_class in sorted(plugins.items()):
43
+ try:
44
+ plugin = plugin_class()
45
+ domains = ", ".join(d.value.upper() for d in plugin.domains)
46
+ version_str = plugin.get_version()
47
+ print(f" {name}")
48
+ print(f" Domains: {domains}")
49
+ print(f" Version: {version_str}")
50
+ print()
51
+ except Exception as e:
52
+ print(f" {name}: error loading plugin ({e})")
53
+ print()
54
+ else:
55
+ print(" No plugins discovered.")
56
+ print()
57
+ print("Install plugins via pip, e.g.: pip install lucidscan-snyk")
58
+
59
+ return EXIT_SUCCESS