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.
- lucidscan/__init__.py +12 -0
- lucidscan/bootstrap/__init__.py +26 -0
- lucidscan/bootstrap/paths.py +160 -0
- lucidscan/bootstrap/platform.py +111 -0
- lucidscan/bootstrap/validation.py +76 -0
- lucidscan/bootstrap/versions.py +119 -0
- lucidscan/cli/__init__.py +50 -0
- lucidscan/cli/__main__.py +8 -0
- lucidscan/cli/arguments.py +405 -0
- lucidscan/cli/commands/__init__.py +64 -0
- lucidscan/cli/commands/autoconfigure.py +294 -0
- lucidscan/cli/commands/help.py +69 -0
- lucidscan/cli/commands/init.py +656 -0
- lucidscan/cli/commands/list_scanners.py +59 -0
- lucidscan/cli/commands/scan.py +307 -0
- lucidscan/cli/commands/serve.py +142 -0
- lucidscan/cli/commands/status.py +84 -0
- lucidscan/cli/commands/validate.py +105 -0
- lucidscan/cli/config_bridge.py +152 -0
- lucidscan/cli/exit_codes.py +17 -0
- lucidscan/cli/runner.py +284 -0
- lucidscan/config/__init__.py +29 -0
- lucidscan/config/ignore.py +178 -0
- lucidscan/config/loader.py +431 -0
- lucidscan/config/models.py +316 -0
- lucidscan/config/validation.py +645 -0
- lucidscan/core/__init__.py +3 -0
- lucidscan/core/domain_runner.py +463 -0
- lucidscan/core/git.py +174 -0
- lucidscan/core/logging.py +34 -0
- lucidscan/core/models.py +207 -0
- lucidscan/core/streaming.py +340 -0
- lucidscan/core/subprocess_runner.py +164 -0
- lucidscan/detection/__init__.py +21 -0
- lucidscan/detection/detector.py +154 -0
- lucidscan/detection/frameworks.py +270 -0
- lucidscan/detection/languages.py +328 -0
- lucidscan/detection/tools.py +229 -0
- lucidscan/generation/__init__.py +15 -0
- lucidscan/generation/config_generator.py +275 -0
- lucidscan/generation/package_installer.py +330 -0
- lucidscan/mcp/__init__.py +20 -0
- lucidscan/mcp/formatter.py +510 -0
- lucidscan/mcp/server.py +297 -0
- lucidscan/mcp/tools.py +1049 -0
- lucidscan/mcp/watcher.py +237 -0
- lucidscan/pipeline/__init__.py +17 -0
- lucidscan/pipeline/executor.py +187 -0
- lucidscan/pipeline/parallel.py +181 -0
- lucidscan/plugins/__init__.py +40 -0
- lucidscan/plugins/coverage/__init__.py +28 -0
- lucidscan/plugins/coverage/base.py +160 -0
- lucidscan/plugins/coverage/coverage_py.py +454 -0
- lucidscan/plugins/coverage/istanbul.py +411 -0
- lucidscan/plugins/discovery.py +107 -0
- lucidscan/plugins/enrichers/__init__.py +61 -0
- lucidscan/plugins/enrichers/base.py +63 -0
- lucidscan/plugins/linters/__init__.py +26 -0
- lucidscan/plugins/linters/base.py +125 -0
- lucidscan/plugins/linters/biome.py +448 -0
- lucidscan/plugins/linters/checkstyle.py +393 -0
- lucidscan/plugins/linters/eslint.py +368 -0
- lucidscan/plugins/linters/ruff.py +498 -0
- lucidscan/plugins/reporters/__init__.py +45 -0
- lucidscan/plugins/reporters/base.py +30 -0
- lucidscan/plugins/reporters/json_reporter.py +79 -0
- lucidscan/plugins/reporters/sarif_reporter.py +303 -0
- lucidscan/plugins/reporters/summary_reporter.py +61 -0
- lucidscan/plugins/reporters/table_reporter.py +81 -0
- lucidscan/plugins/scanners/__init__.py +57 -0
- lucidscan/plugins/scanners/base.py +60 -0
- lucidscan/plugins/scanners/checkov.py +484 -0
- lucidscan/plugins/scanners/opengrep.py +464 -0
- lucidscan/plugins/scanners/trivy.py +492 -0
- lucidscan/plugins/test_runners/__init__.py +27 -0
- lucidscan/plugins/test_runners/base.py +111 -0
- lucidscan/plugins/test_runners/jest.py +381 -0
- lucidscan/plugins/test_runners/karma.py +481 -0
- lucidscan/plugins/test_runners/playwright.py +434 -0
- lucidscan/plugins/test_runners/pytest.py +598 -0
- lucidscan/plugins/type_checkers/__init__.py +27 -0
- lucidscan/plugins/type_checkers/base.py +106 -0
- lucidscan/plugins/type_checkers/mypy.py +355 -0
- lucidscan/plugins/type_checkers/pyright.py +313 -0
- lucidscan/plugins/type_checkers/typescript.py +280 -0
- lucidscan-0.5.12.dist-info/METADATA +242 -0
- lucidscan-0.5.12.dist-info/RECORD +91 -0
- lucidscan-0.5.12.dist-info/WHEEL +5 -0
- lucidscan-0.5.12.dist-info/entry_points.txt +34 -0
- lucidscan-0.5.12.dist-info/licenses/LICENSE +201 -0
- lucidscan-0.5.12.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Configuration file generator.
|
|
2
|
+
|
|
3
|
+
Generates lucidscan.yml configuration files based on detected project
|
|
4
|
+
characteristics and user choices.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from lucidscan.detection import ProjectContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class InitChoices:
|
|
20
|
+
"""User choices made during initialization."""
|
|
21
|
+
|
|
22
|
+
# Linting
|
|
23
|
+
linter: Optional[str] = None # "ruff", "eslint", "biome", or None
|
|
24
|
+
linter_config: Optional[str] = None # Path to existing config or None
|
|
25
|
+
|
|
26
|
+
# Type checking
|
|
27
|
+
type_checker: Optional[str] = None # "mypy", "pyright", "typescript", or None
|
|
28
|
+
type_checker_strict: bool = False
|
|
29
|
+
|
|
30
|
+
# Security
|
|
31
|
+
security_enabled: bool = True
|
|
32
|
+
security_tools: list[str] = field(default_factory=lambda: ["trivy", "opengrep"])
|
|
33
|
+
|
|
34
|
+
# Testing
|
|
35
|
+
test_runner: Optional[str] = None # "pytest", "jest", or None
|
|
36
|
+
coverage_enabled: bool = False
|
|
37
|
+
coverage_threshold: int = 80
|
|
38
|
+
|
|
39
|
+
# Fail thresholds
|
|
40
|
+
fail_on_linting: str = "error" # "error", "warning", "none"
|
|
41
|
+
fail_on_security: str = "high" # "critical", "high", "medium", "low", "none"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ConfigGenerator:
|
|
45
|
+
"""Generates lucidscan.yml configuration files."""
|
|
46
|
+
|
|
47
|
+
def generate(
|
|
48
|
+
self,
|
|
49
|
+
context: ProjectContext,
|
|
50
|
+
choices: InitChoices,
|
|
51
|
+
) -> str:
|
|
52
|
+
"""Generate lucidscan.yml content.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
context: Detected project context.
|
|
56
|
+
choices: User initialization choices.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
YAML string for lucidscan.yml.
|
|
60
|
+
"""
|
|
61
|
+
config = self._build_config(context, choices)
|
|
62
|
+
return self._to_yaml(config)
|
|
63
|
+
|
|
64
|
+
def write(
|
|
65
|
+
self,
|
|
66
|
+
context: ProjectContext,
|
|
67
|
+
choices: InitChoices,
|
|
68
|
+
output_path: Optional[Path] = None,
|
|
69
|
+
) -> Path:
|
|
70
|
+
"""Generate and write lucidscan.yml file.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
context: Detected project context.
|
|
74
|
+
choices: User initialization choices.
|
|
75
|
+
output_path: Output file path (default: project_root/lucidscan.yml).
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Path to the written file.
|
|
79
|
+
"""
|
|
80
|
+
if output_path is None:
|
|
81
|
+
output_path = context.root / "lucidscan.yml"
|
|
82
|
+
|
|
83
|
+
content = self.generate(context, choices)
|
|
84
|
+
output_path.write_text(content)
|
|
85
|
+
return output_path
|
|
86
|
+
|
|
87
|
+
def _build_config(
|
|
88
|
+
self,
|
|
89
|
+
context: ProjectContext,
|
|
90
|
+
choices: InitChoices,
|
|
91
|
+
) -> dict:
|
|
92
|
+
"""Build configuration dictionary.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
context: Detected project context.
|
|
96
|
+
choices: User initialization choices.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Configuration dictionary.
|
|
100
|
+
"""
|
|
101
|
+
config = {
|
|
102
|
+
"version": 1,
|
|
103
|
+
"project": self._build_project_section(context),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Add pipeline section
|
|
107
|
+
pipeline = {}
|
|
108
|
+
|
|
109
|
+
# Linting
|
|
110
|
+
if choices.linter:
|
|
111
|
+
pipeline["linting"] = self._build_linting_section(choices)
|
|
112
|
+
|
|
113
|
+
# Type checking
|
|
114
|
+
if choices.type_checker:
|
|
115
|
+
pipeline["type_checking"] = self._build_type_checking_section(choices)
|
|
116
|
+
|
|
117
|
+
# Security
|
|
118
|
+
if choices.security_enabled and choices.security_tools:
|
|
119
|
+
pipeline["security"] = self._build_security_section(choices)
|
|
120
|
+
|
|
121
|
+
# Testing
|
|
122
|
+
if choices.test_runner:
|
|
123
|
+
pipeline["testing"] = self._build_testing_section(choices)
|
|
124
|
+
|
|
125
|
+
# Coverage
|
|
126
|
+
if choices.coverage_enabled:
|
|
127
|
+
pipeline["coverage"] = self._build_coverage_section(choices)
|
|
128
|
+
|
|
129
|
+
if pipeline:
|
|
130
|
+
config["pipeline"] = pipeline
|
|
131
|
+
|
|
132
|
+
# Fail thresholds
|
|
133
|
+
config["fail_on"] = self._build_fail_on_section(choices)
|
|
134
|
+
|
|
135
|
+
# Ignore patterns
|
|
136
|
+
config["ignore"] = self._build_ignore_patterns(context)
|
|
137
|
+
|
|
138
|
+
return config
|
|
139
|
+
|
|
140
|
+
def _build_project_section(self, context: ProjectContext) -> dict:
|
|
141
|
+
"""Build project section."""
|
|
142
|
+
languages = [lang.name for lang in context.languages]
|
|
143
|
+
return {
|
|
144
|
+
"name": context.root.name,
|
|
145
|
+
"languages": languages,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def _build_linting_section(self, choices: InitChoices) -> dict:
|
|
149
|
+
"""Build linting pipeline section."""
|
|
150
|
+
tools: list[dict] = []
|
|
151
|
+
section = {
|
|
152
|
+
"enabled": True,
|
|
153
|
+
"tools": tools,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
tool: dict = {"name": choices.linter}
|
|
157
|
+
if choices.linter_config:
|
|
158
|
+
tool["config"] = choices.linter_config
|
|
159
|
+
|
|
160
|
+
tools.append(tool)
|
|
161
|
+
return section
|
|
162
|
+
|
|
163
|
+
def _build_type_checking_section(self, choices: InitChoices) -> dict:
|
|
164
|
+
"""Build type checking pipeline section."""
|
|
165
|
+
tools: list[dict] = []
|
|
166
|
+
section = {
|
|
167
|
+
"enabled": True,
|
|
168
|
+
"tools": tools,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
tool: dict = {"name": choices.type_checker}
|
|
172
|
+
if choices.type_checker_strict:
|
|
173
|
+
tool["strict"] = True
|
|
174
|
+
|
|
175
|
+
tools.append(tool)
|
|
176
|
+
return section
|
|
177
|
+
|
|
178
|
+
def _build_security_section(self, choices: InitChoices) -> dict:
|
|
179
|
+
"""Build security pipeline section."""
|
|
180
|
+
tools: list[dict] = []
|
|
181
|
+
section = {
|
|
182
|
+
"enabled": True,
|
|
183
|
+
"tools": tools,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for tool_name in choices.security_tools:
|
|
187
|
+
tool: dict = {"name": tool_name}
|
|
188
|
+
|
|
189
|
+
# Add domains based on tool
|
|
190
|
+
if tool_name == "trivy":
|
|
191
|
+
tool["domains"] = ["sca"]
|
|
192
|
+
elif tool_name == "opengrep":
|
|
193
|
+
tool["domains"] = ["sast"]
|
|
194
|
+
elif tool_name == "checkov":
|
|
195
|
+
tool["domains"] = ["iac"]
|
|
196
|
+
|
|
197
|
+
tools.append(tool)
|
|
198
|
+
|
|
199
|
+
return section
|
|
200
|
+
|
|
201
|
+
def _build_testing_section(self, choices: InitChoices) -> dict:
|
|
202
|
+
"""Build testing pipeline section."""
|
|
203
|
+
section = {
|
|
204
|
+
"enabled": True,
|
|
205
|
+
"tools": [{"name": choices.test_runner}],
|
|
206
|
+
}
|
|
207
|
+
return section
|
|
208
|
+
|
|
209
|
+
def _build_coverage_section(self, choices: InitChoices) -> dict:
|
|
210
|
+
"""Build coverage pipeline section."""
|
|
211
|
+
return {
|
|
212
|
+
"enabled": True,
|
|
213
|
+
"threshold": choices.coverage_threshold,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
def _build_fail_on_section(self, choices: InitChoices) -> dict:
|
|
217
|
+
"""Build fail_on thresholds section."""
|
|
218
|
+
return {
|
|
219
|
+
"linting": choices.fail_on_linting,
|
|
220
|
+
"type_checking": "error",
|
|
221
|
+
"security": choices.fail_on_security,
|
|
222
|
+
"testing": "any",
|
|
223
|
+
"coverage": "below_threshold" if choices.coverage_enabled else "none",
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
def _build_ignore_patterns(self, context: ProjectContext) -> list:
|
|
227
|
+
"""Build ignore patterns list."""
|
|
228
|
+
patterns = [
|
|
229
|
+
"**/__pycache__/**",
|
|
230
|
+
"**/node_modules/**",
|
|
231
|
+
"**/.venv/**",
|
|
232
|
+
"**/venv/**",
|
|
233
|
+
"**/dist/**",
|
|
234
|
+
"**/build/**",
|
|
235
|
+
"**/.git/**",
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
# Add language-specific patterns
|
|
239
|
+
if context.has_python:
|
|
240
|
+
patterns.extend([
|
|
241
|
+
"**/*.egg-info/**",
|
|
242
|
+
"**/.pytest_cache/**",
|
|
243
|
+
"**/.mypy_cache/**",
|
|
244
|
+
"**/.ruff_cache/**",
|
|
245
|
+
])
|
|
246
|
+
|
|
247
|
+
if context.has_javascript:
|
|
248
|
+
patterns.extend([
|
|
249
|
+
"**/coverage/**",
|
|
250
|
+
"**/.next/**",
|
|
251
|
+
"**/.nuxt/**",
|
|
252
|
+
])
|
|
253
|
+
|
|
254
|
+
return patterns
|
|
255
|
+
|
|
256
|
+
def _to_yaml(self, config: dict) -> str:
|
|
257
|
+
"""Convert config dict to YAML string.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
config: Configuration dictionary.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
YAML string.
|
|
264
|
+
"""
|
|
265
|
+
# Add header comment
|
|
266
|
+
header = "# LucidScan Configuration\n# Generated by lucidscan init\n\n"
|
|
267
|
+
|
|
268
|
+
yaml_content = yaml.dump(
|
|
269
|
+
config,
|
|
270
|
+
default_flow_style=False,
|
|
271
|
+
sort_keys=False,
|
|
272
|
+
allow_unicode=True,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return header + yaml_content
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Package manager installer.
|
|
2
|
+
|
|
3
|
+
Adds development tools to package manager configuration files
|
|
4
|
+
(pyproject.toml, package.json).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List
|
|
13
|
+
|
|
14
|
+
from lucidscan.core.logging import get_logger
|
|
15
|
+
from lucidscan.detection import ProjectContext
|
|
16
|
+
|
|
17
|
+
LOGGER = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
# Tool to package mapping
|
|
20
|
+
PYTHON_PACKAGES: Dict[str, str] = {
|
|
21
|
+
"ruff": "ruff>=0.8.0",
|
|
22
|
+
"mypy": "mypy>=1.0",
|
|
23
|
+
"pyright": "pyright>=1.1",
|
|
24
|
+
"pytest": "pytest>=7.0",
|
|
25
|
+
"pytest-cov": "pytest-cov>=4.0",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
JAVASCRIPT_PACKAGES: Dict[str, str] = {
|
|
29
|
+
"eslint": "eslint@^9.0.0",
|
|
30
|
+
"biome": "@biomejs/biome@^1.0.0",
|
|
31
|
+
"typescript": "typescript@^5.0.0",
|
|
32
|
+
"jest": "jest@^29.0.0",
|
|
33
|
+
"vitest": "vitest@^2.0.0",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PackageInstaller:
|
|
38
|
+
"""Adds tools to package manager configuration files."""
|
|
39
|
+
|
|
40
|
+
def install_tools(
|
|
41
|
+
self,
|
|
42
|
+
context: ProjectContext,
|
|
43
|
+
tools: List[str],
|
|
44
|
+
) -> Dict[str, Path]:
|
|
45
|
+
"""Install tools to appropriate package files.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
context: Detected project context.
|
|
49
|
+
tools: List of tool names to install.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dict mapping tool name to the file it was added to.
|
|
53
|
+
"""
|
|
54
|
+
installed: Dict[str, Path] = {}
|
|
55
|
+
|
|
56
|
+
# Separate tools by language
|
|
57
|
+
python_tools = [t for t in tools if t in PYTHON_PACKAGES]
|
|
58
|
+
js_tools = [t for t in tools if t in JAVASCRIPT_PACKAGES]
|
|
59
|
+
|
|
60
|
+
# Install Python tools
|
|
61
|
+
if python_tools and context.has_python:
|
|
62
|
+
pyproject = context.root / "pyproject.toml"
|
|
63
|
+
requirements = context.root / "requirements-dev.txt"
|
|
64
|
+
|
|
65
|
+
if pyproject.exists():
|
|
66
|
+
added = self._add_to_pyproject(pyproject, python_tools)
|
|
67
|
+
for tool in added:
|
|
68
|
+
installed[tool] = pyproject
|
|
69
|
+
elif requirements.exists():
|
|
70
|
+
added = self._add_to_requirements(requirements, python_tools)
|
|
71
|
+
for tool in added:
|
|
72
|
+
installed[tool] = requirements
|
|
73
|
+
else:
|
|
74
|
+
# Create pyproject.toml if no package file exists
|
|
75
|
+
added = self._create_pyproject(context.root, python_tools)
|
|
76
|
+
for tool in added:
|
|
77
|
+
installed[tool] = context.root / "pyproject.toml"
|
|
78
|
+
|
|
79
|
+
# Install JavaScript tools
|
|
80
|
+
if js_tools and context.has_javascript:
|
|
81
|
+
package_json = context.root / "package.json"
|
|
82
|
+
|
|
83
|
+
if package_json.exists():
|
|
84
|
+
added = self._add_to_package_json(package_json, js_tools)
|
|
85
|
+
for tool in added:
|
|
86
|
+
installed[tool] = package_json
|
|
87
|
+
else:
|
|
88
|
+
# Create package.json if it doesn't exist
|
|
89
|
+
added = self._create_package_json(context.root, js_tools)
|
|
90
|
+
for tool in added:
|
|
91
|
+
installed[tool] = context.root / "package.json"
|
|
92
|
+
|
|
93
|
+
return installed
|
|
94
|
+
|
|
95
|
+
def _add_to_pyproject(
|
|
96
|
+
self,
|
|
97
|
+
pyproject_path: Path,
|
|
98
|
+
tools: List[str],
|
|
99
|
+
) -> List[str]:
|
|
100
|
+
"""Add tools to pyproject.toml dev dependencies.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
pyproject_path: Path to pyproject.toml.
|
|
104
|
+
tools: Tools to add.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of tools that were added.
|
|
108
|
+
"""
|
|
109
|
+
content = pyproject_path.read_text()
|
|
110
|
+
added = []
|
|
111
|
+
|
|
112
|
+
# Check which tools are already present
|
|
113
|
+
packages_to_add = []
|
|
114
|
+
for tool in tools:
|
|
115
|
+
package = PYTHON_PACKAGES.get(tool)
|
|
116
|
+
if package:
|
|
117
|
+
# Check if already in file (simple check)
|
|
118
|
+
tool_name = package.split(">=")[0].split("[")[0]
|
|
119
|
+
if tool_name not in content:
|
|
120
|
+
packages_to_add.append(package)
|
|
121
|
+
added.append(tool)
|
|
122
|
+
|
|
123
|
+
if not packages_to_add:
|
|
124
|
+
return added
|
|
125
|
+
|
|
126
|
+
# Check if [project.optional-dependencies] exists
|
|
127
|
+
if "[project.optional-dependencies]" in content:
|
|
128
|
+
# Check if dev section exists
|
|
129
|
+
if re.search(r'\[project\.optional-dependencies\].*?dev\s*=', content, re.DOTALL):
|
|
130
|
+
# Add to existing dev list
|
|
131
|
+
content = self._append_to_dev_deps(content, packages_to_add)
|
|
132
|
+
else:
|
|
133
|
+
# Add dev section after [project.optional-dependencies]
|
|
134
|
+
dev_section = f'\ndev = [\n {self._format_deps(packages_to_add)}\n]'
|
|
135
|
+
content = content.replace(
|
|
136
|
+
"[project.optional-dependencies]",
|
|
137
|
+
f"[project.optional-dependencies]{dev_section}"
|
|
138
|
+
)
|
|
139
|
+
elif "[project]" in content:
|
|
140
|
+
# Add optional-dependencies section
|
|
141
|
+
deps_section = f'\n[project.optional-dependencies]\ndev = [\n {self._format_deps(packages_to_add)}\n]\n'
|
|
142
|
+
# Find a good place to insert (after dependencies if exists, or at end)
|
|
143
|
+
if "dependencies = [" in content:
|
|
144
|
+
# Find end of dependencies section
|
|
145
|
+
match = re.search(r'dependencies\s*=\s*\[.*?\]', content, re.DOTALL)
|
|
146
|
+
if match:
|
|
147
|
+
insert_pos = match.end()
|
|
148
|
+
content = content[:insert_pos] + deps_section + content[insert_pos:]
|
|
149
|
+
else:
|
|
150
|
+
# Add at end of [project] section
|
|
151
|
+
content += deps_section
|
|
152
|
+
else:
|
|
153
|
+
# No project section, add one
|
|
154
|
+
content += f'\n[project.optional-dependencies]\ndev = [\n {self._format_deps(packages_to_add)}\n]\n'
|
|
155
|
+
|
|
156
|
+
pyproject_path.write_text(content)
|
|
157
|
+
LOGGER.info(f"Added {len(added)} tools to {pyproject_path}")
|
|
158
|
+
return added
|
|
159
|
+
|
|
160
|
+
def _append_to_dev_deps(self, content: str, packages: List[str]) -> str:
|
|
161
|
+
"""Append packages to existing dev dependencies list."""
|
|
162
|
+
# Find the dev = [...] section and append
|
|
163
|
+
pattern = r'(dev\s*=\s*\[)(.*?)(\])'
|
|
164
|
+
|
|
165
|
+
def replacer(match):
|
|
166
|
+
existing = match.group(2).strip()
|
|
167
|
+
if existing and not existing.endswith(","):
|
|
168
|
+
existing += ","
|
|
169
|
+
new_deps = self._format_deps(packages)
|
|
170
|
+
return f'{match.group(1)}{existing}\n {new_deps}\n{match.group(3)}'
|
|
171
|
+
|
|
172
|
+
return re.sub(pattern, replacer, content, flags=re.DOTALL)
|
|
173
|
+
|
|
174
|
+
def _format_deps(self, packages: List[str]) -> str:
|
|
175
|
+
"""Format packages as TOML array items."""
|
|
176
|
+
return ",\n ".join(f'"{pkg}"' for pkg in packages)
|
|
177
|
+
|
|
178
|
+
def _add_to_requirements(
|
|
179
|
+
self,
|
|
180
|
+
requirements_path: Path,
|
|
181
|
+
tools: List[str],
|
|
182
|
+
) -> List[str]:
|
|
183
|
+
"""Add tools to requirements-dev.txt.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
requirements_path: Path to requirements file.
|
|
187
|
+
tools: Tools to add.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
List of tools that were added.
|
|
191
|
+
"""
|
|
192
|
+
content = requirements_path.read_text()
|
|
193
|
+
added = []
|
|
194
|
+
|
|
195
|
+
for tool in tools:
|
|
196
|
+
package = PYTHON_PACKAGES.get(tool)
|
|
197
|
+
if package:
|
|
198
|
+
tool_name = package.split(">=")[0]
|
|
199
|
+
if tool_name not in content:
|
|
200
|
+
content += f"\n{package}"
|
|
201
|
+
added.append(tool)
|
|
202
|
+
|
|
203
|
+
if added:
|
|
204
|
+
requirements_path.write_text(content.strip() + "\n")
|
|
205
|
+
LOGGER.info(f"Added {len(added)} tools to {requirements_path}")
|
|
206
|
+
|
|
207
|
+
return added
|
|
208
|
+
|
|
209
|
+
def _create_pyproject(
|
|
210
|
+
self,
|
|
211
|
+
project_root: Path,
|
|
212
|
+
tools: List[str],
|
|
213
|
+
) -> List[str]:
|
|
214
|
+
"""Create pyproject.toml with dev dependencies.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
project_root: Project root directory.
|
|
218
|
+
tools: Tools to add.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
List of tools that were added.
|
|
222
|
+
"""
|
|
223
|
+
packages = [PYTHON_PACKAGES[t] for t in tools if t in PYTHON_PACKAGES]
|
|
224
|
+
|
|
225
|
+
content = f'''[project]
|
|
226
|
+
name = "{project_root.name}"
|
|
227
|
+
version = "0.3.0"
|
|
228
|
+
requires-python = ">=3.10"
|
|
229
|
+
|
|
230
|
+
[project.optional-dependencies]
|
|
231
|
+
dev = [
|
|
232
|
+
{self._format_deps(packages)}
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
[build-system]
|
|
236
|
+
requires = ["setuptools>=64"]
|
|
237
|
+
build-backend = "setuptools.build_meta"
|
|
238
|
+
'''
|
|
239
|
+
|
|
240
|
+
pyproject_path = project_root / "pyproject.toml"
|
|
241
|
+
pyproject_path.write_text(content)
|
|
242
|
+
LOGGER.info(f"Created {pyproject_path}")
|
|
243
|
+
return tools
|
|
244
|
+
|
|
245
|
+
def _add_to_package_json(
|
|
246
|
+
self,
|
|
247
|
+
package_json_path: Path,
|
|
248
|
+
tools: List[str],
|
|
249
|
+
) -> List[str]:
|
|
250
|
+
"""Add tools to package.json devDependencies.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
package_json_path: Path to package.json.
|
|
254
|
+
tools: Tools to add.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
List of tools that were added.
|
|
258
|
+
"""
|
|
259
|
+
content = package_json_path.read_text()
|
|
260
|
+
data = json.loads(content)
|
|
261
|
+
added = []
|
|
262
|
+
|
|
263
|
+
if "devDependencies" not in data:
|
|
264
|
+
data["devDependencies"] = {}
|
|
265
|
+
|
|
266
|
+
for tool in tools:
|
|
267
|
+
package = JAVASCRIPT_PACKAGES.get(tool)
|
|
268
|
+
if package:
|
|
269
|
+
# Parse package name and version
|
|
270
|
+
if "@" in package and not package.startswith("@"):
|
|
271
|
+
name, version = package.rsplit("@", 1)
|
|
272
|
+
elif package.startswith("@"):
|
|
273
|
+
# Scoped package like @biomejs/biome@^1.0.0
|
|
274
|
+
parts = package.split("@")
|
|
275
|
+
name = f"@{parts[1]}"
|
|
276
|
+
version = parts[2] if len(parts) > 2 else "latest"
|
|
277
|
+
else:
|
|
278
|
+
name = package
|
|
279
|
+
version = "latest"
|
|
280
|
+
|
|
281
|
+
if name not in data["devDependencies"]:
|
|
282
|
+
data["devDependencies"][name] = version
|
|
283
|
+
added.append(tool)
|
|
284
|
+
|
|
285
|
+
if added:
|
|
286
|
+
package_json_path.write_text(
|
|
287
|
+
json.dumps(data, indent=2) + "\n"
|
|
288
|
+
)
|
|
289
|
+
LOGGER.info(f"Added {len(added)} tools to {package_json_path}")
|
|
290
|
+
|
|
291
|
+
return added
|
|
292
|
+
|
|
293
|
+
def _create_package_json(
|
|
294
|
+
self,
|
|
295
|
+
project_root: Path,
|
|
296
|
+
tools: List[str],
|
|
297
|
+
) -> List[str]:
|
|
298
|
+
"""Create package.json with devDependencies.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
project_root: Project root directory.
|
|
302
|
+
tools: Tools to add.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
List of tools that were added.
|
|
306
|
+
"""
|
|
307
|
+
data = {
|
|
308
|
+
"name": project_root.name,
|
|
309
|
+
"version": "1.0.0",
|
|
310
|
+
"devDependencies": {},
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for tool in tools:
|
|
314
|
+
package = JAVASCRIPT_PACKAGES.get(tool)
|
|
315
|
+
if package:
|
|
316
|
+
if "@" in package and not package.startswith("@"):
|
|
317
|
+
name, version = package.rsplit("@", 1)
|
|
318
|
+
elif package.startswith("@"):
|
|
319
|
+
parts = package.split("@")
|
|
320
|
+
name = f"@{parts[1]}"
|
|
321
|
+
version = parts[2] if len(parts) > 2 else "latest"
|
|
322
|
+
else:
|
|
323
|
+
name = package
|
|
324
|
+
version = "latest"
|
|
325
|
+
data["devDependencies"][name] = version # type: ignore[index]
|
|
326
|
+
|
|
327
|
+
package_json_path = project_root / "package.json"
|
|
328
|
+
package_json_path.write_text(json.dumps(data, indent=2) + "\n")
|
|
329
|
+
LOGGER.info(f"Created {package_json_path}")
|
|
330
|
+
return tools
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) integration for LucidScan.
|
|
2
|
+
|
|
3
|
+
This package provides MCP server functionality for AI agent integration,
|
|
4
|
+
enabling tools like Claude Code and Cursor to invoke LucidScan checks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from lucidscan.mcp.server import LucidScanMCPServer
|
|
10
|
+
from lucidscan.mcp.formatter import InstructionFormatter, FixInstruction
|
|
11
|
+
from lucidscan.mcp.tools import MCPToolExecutor
|
|
12
|
+
from lucidscan.mcp.watcher import LucidScanFileWatcher
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"LucidScanMCPServer",
|
|
16
|
+
"InstructionFormatter",
|
|
17
|
+
"FixInstruction",
|
|
18
|
+
"MCPToolExecutor",
|
|
19
|
+
"LucidScanFileWatcher",
|
|
20
|
+
]
|