delimit-cli 2.3.2 → 3.0.0
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.
- package/.dockerignore +7 -0
- package/.github/workflows/ci.yml +22 -0
- package/CHANGELOG.md +33 -0
- package/CODE_OF_CONDUCT.md +48 -0
- package/CONTRIBUTING.md +67 -0
- package/Dockerfile +9 -0
- package/LICENSE +21 -0
- package/README.md +51 -130
- package/SECURITY.md +42 -0
- package/adapters/codex-forge.js +107 -0
- package/adapters/codex-jamsons.js +142 -0
- package/adapters/codex-security.js +94 -0
- package/adapters/gemini-forge.js +120 -0
- package/adapters/gemini-jamsons.js +152 -0
- package/bin/delimit-cli.js +52 -2
- package/bin/delimit-setup.js +258 -0
- package/gateway/ai/backends/__init__.py +0 -0
- package/gateway/ai/backends/async_utils.py +21 -0
- package/gateway/ai/backends/deploy_bridge.py +150 -0
- package/gateway/ai/backends/gateway_core.py +261 -0
- package/gateway/ai/backends/generate_bridge.py +38 -0
- package/gateway/ai/backends/governance_bridge.py +196 -0
- package/gateway/ai/backends/intel_bridge.py +59 -0
- package/gateway/ai/backends/memory_bridge.py +93 -0
- package/gateway/ai/backends/ops_bridge.py +137 -0
- package/gateway/ai/backends/os_bridge.py +82 -0
- package/gateway/ai/backends/repo_bridge.py +117 -0
- package/gateway/ai/backends/ui_bridge.py +118 -0
- package/gateway/ai/backends/vault_bridge.py +129 -0
- package/gateway/ai/server.py +1182 -0
- package/gateway/core/__init__.py +3 -0
- package/gateway/core/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/auto_baseline.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/ci_formatter.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/contract_ledger.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/dependency_graph.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/dependency_manifest.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/diff_engine_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/event_backbone.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/event_schema.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/explainer.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway_v3.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/impact_analyzer.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/policy_engine.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry_v3.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/semver_classifier.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/spec_detector.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/surface_bridge.cpython-310.pyc +0 -0
- package/gateway/core/auto_baseline.py +304 -0
- package/gateway/core/ci_formatter.py +283 -0
- package/gateway/core/complexity_analyzer.py +386 -0
- package/gateway/core/contract_ledger.py +345 -0
- package/gateway/core/dependency_graph.py +218 -0
- package/gateway/core/dependency_manifest.py +223 -0
- package/gateway/core/diff_engine_v2.py +477 -0
- package/gateway/core/diff_engine_v2.py.bak +426 -0
- package/gateway/core/event_backbone.py +268 -0
- package/gateway/core/event_schema.py +258 -0
- package/gateway/core/explainer.py +438 -0
- package/gateway/core/gateway.py +128 -0
- package/gateway/core/gateway_v2.py +154 -0
- package/gateway/core/gateway_v3.py +224 -0
- package/gateway/core/impact_analyzer.py +163 -0
- package/gateway/core/policies/default.yml +13 -0
- package/gateway/core/policies/relaxed.yml +48 -0
- package/gateway/core/policies/strict.yml +55 -0
- package/gateway/core/policy_engine.py +464 -0
- package/gateway/core/registry.py +52 -0
- package/gateway/core/registry_v2.py +132 -0
- package/gateway/core/registry_v3.py +134 -0
- package/gateway/core/semver_classifier.py +152 -0
- package/gateway/core/spec_detector.py +130 -0
- package/gateway/core/surface_bridge.py +307 -0
- package/gateway/core/zero_spec/__init__.py +4 -0
- package/gateway/core/zero_spec/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/detector.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/express_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/fastapi_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/nestjs_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/detector.py +353 -0
- package/gateway/core/zero_spec/express_extractor.py +483 -0
- package/gateway/core/zero_spec/fastapi_extractor.py +254 -0
- package/gateway/core/zero_spec/nestjs_extractor.py +369 -0
- package/gateway/tasks/__init__.py +1 -0
- package/gateway/tasks/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy_v3.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/explain_diff.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/explain_diff_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api_v3.cpython-310.pyc +0 -0
- package/gateway/tasks/check_policy.py +177 -0
- package/gateway/tasks/check_policy_v2.py +255 -0
- package/gateway/tasks/check_policy_v3.py +255 -0
- package/gateway/tasks/explain_diff.py +305 -0
- package/gateway/tasks/explain_diff_v2.py +267 -0
- package/gateway/tasks/validate_api.py +131 -0
- package/gateway/tasks/validate_api_v2.py +208 -0
- package/gateway/tasks/validate_api_v3.py +163 -0
- package/package.json +3 -3
- package/adapters/codex-skill.js +0 -87
- package/adapters/cursor-extension.js +0 -190
- package/adapters/gemini-action.js +0 -93
- package/adapters/openai-function.js +0 -112
- package/adapters/xai-plugin.js +0 -151
- package/test-decision-engine.js +0 -181
- package/test-hook.js +0 -27
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI OpenAPI extractor — generates an OpenAPI spec from FastAPI source code
|
|
3
|
+
without running the server. Uses subprocess + app.openapi() for full fidelity.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from .detector import AppLocation, FrameworkInfo
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Script template injected into the user's project context.
|
|
18
|
+
# It imports the module, finds the FastAPI app, and prints the OpenAPI JSON.
|
|
19
|
+
_EXTRACTOR_SCRIPT = '''\
|
|
20
|
+
import sys, json, importlib, importlib.util, os
|
|
21
|
+
|
|
22
|
+
# Prevent the app from actually starting a server
|
|
23
|
+
os.environ["DELIMIT_EXTRACT"] = "1"
|
|
24
|
+
|
|
25
|
+
project_root = sys.argv[1]
|
|
26
|
+
module_path = sys.argv[2]
|
|
27
|
+
app_var = sys.argv[3]
|
|
28
|
+
|
|
29
|
+
# Add project root to sys.path so imports resolve
|
|
30
|
+
sys.path.insert(0, project_root)
|
|
31
|
+
|
|
32
|
+
# Convert file path to module name
|
|
33
|
+
# e.g. "app/main.py" -> "app.main"
|
|
34
|
+
module_name = module_path.replace(os.sep, ".").removesuffix(".py")
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
module = importlib.import_module(module_name)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(json.dumps({"error": f"Import failed: {e}", "type": "import"}))
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
app = getattr(module, app_var, None)
|
|
43
|
+
if app is None:
|
|
44
|
+
print(json.dumps({"error": f"Variable '{app_var}' not found in {module_name}", "type": "app_not_found"}))
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
if not hasattr(app, "openapi"):
|
|
48
|
+
print(json.dumps({"error": f"'{app_var}' is not a FastAPI app (no openapi method)", "type": "not_fastapi"}))
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
spec = app.openapi()
|
|
53
|
+
print(json.dumps(spec, default=str))
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(json.dumps({"error": f"OpenAPI generation failed: {e}", "type": "openapi_error"}))
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
'''
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def extract_fastapi_spec(
|
|
61
|
+
info: FrameworkInfo,
|
|
62
|
+
project_dir: str = ".",
|
|
63
|
+
timeout: int = 15,
|
|
64
|
+
python_bin: Optional[str] = None,
|
|
65
|
+
) -> Dict[str, Any]:
|
|
66
|
+
"""
|
|
67
|
+
Extract OpenAPI spec from a FastAPI project.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
info: FrameworkInfo from detect_framework()
|
|
71
|
+
project_dir: Root of the FastAPI project
|
|
72
|
+
timeout: Max seconds for extraction subprocess
|
|
73
|
+
python_bin: Python binary to use (auto-detected if None)
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dict with keys:
|
|
77
|
+
- success: bool
|
|
78
|
+
- spec: OpenAPI dict (if success)
|
|
79
|
+
- spec_path: Path to temp YAML file (if success)
|
|
80
|
+
- error: Error message (if not success)
|
|
81
|
+
- error_type: Error category (if not success)
|
|
82
|
+
"""
|
|
83
|
+
root = Path(project_dir).resolve()
|
|
84
|
+
|
|
85
|
+
if not info.app_locations:
|
|
86
|
+
return {
|
|
87
|
+
"success": False,
|
|
88
|
+
"error": "No FastAPI app instance found. Looked for `app = FastAPI()` in project files.",
|
|
89
|
+
"error_type": "no_app",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
app_loc = info.app_locations[0]
|
|
93
|
+
python = python_bin or _find_python(root)
|
|
94
|
+
|
|
95
|
+
if not python:
|
|
96
|
+
return {
|
|
97
|
+
"success": False,
|
|
98
|
+
"error": "Python not found. Install Python 3.8+ or set python_bin.",
|
|
99
|
+
"error_type": "no_python",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Check if fastapi is importable
|
|
103
|
+
dep_check = _check_fastapi_installed(python, root)
|
|
104
|
+
if not dep_check["installed"]:
|
|
105
|
+
return {
|
|
106
|
+
"success": False,
|
|
107
|
+
"error": "FastAPI not installed. Run: pip install fastapi",
|
|
108
|
+
"error_type": "missing_deps",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Write extractor script to temp file
|
|
112
|
+
with tempfile.NamedTemporaryFile(
|
|
113
|
+
mode="w", suffix=".py", prefix="_delimit_extract_", delete=False
|
|
114
|
+
) as f:
|
|
115
|
+
f.write(_EXTRACTOR_SCRIPT)
|
|
116
|
+
script_path = f.name
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
[python, script_path, str(root), app_loc.file, app_loc.variable],
|
|
121
|
+
capture_output=True,
|
|
122
|
+
text=True,
|
|
123
|
+
timeout=timeout,
|
|
124
|
+
cwd=str(root),
|
|
125
|
+
env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if result.returncode != 0:
|
|
129
|
+
# Try to parse structured error from stdout
|
|
130
|
+
try:
|
|
131
|
+
err = json.loads(result.stdout)
|
|
132
|
+
return {
|
|
133
|
+
"success": False,
|
|
134
|
+
"error": err.get("error", "Extraction failed"),
|
|
135
|
+
"error_type": err.get("type", "unknown"),
|
|
136
|
+
}
|
|
137
|
+
except json.JSONDecodeError:
|
|
138
|
+
stderr = result.stderr.strip()
|
|
139
|
+
return {
|
|
140
|
+
"success": False,
|
|
141
|
+
"error": stderr or "Extraction subprocess failed",
|
|
142
|
+
"error_type": "subprocess",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Parse the OpenAPI spec
|
|
146
|
+
try:
|
|
147
|
+
spec = json.loads(result.stdout)
|
|
148
|
+
except json.JSONDecodeError:
|
|
149
|
+
return {
|
|
150
|
+
"success": False,
|
|
151
|
+
"error": "Extractor produced invalid JSON",
|
|
152
|
+
"error_type": "parse",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if "error" in spec:
|
|
156
|
+
return {
|
|
157
|
+
"success": False,
|
|
158
|
+
"error": spec["error"],
|
|
159
|
+
"error_type": spec.get("type", "unknown"),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Validate it looks like an OpenAPI spec
|
|
163
|
+
if "openapi" not in spec and "swagger" not in spec:
|
|
164
|
+
return {
|
|
165
|
+
"success": False,
|
|
166
|
+
"error": "Output is not a valid OpenAPI spec (missing 'openapi' key)",
|
|
167
|
+
"error_type": "invalid_spec",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Write to temp YAML file for downstream consumption
|
|
171
|
+
spec_path = _write_temp_spec(spec, root)
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"success": True,
|
|
175
|
+
"spec": spec,
|
|
176
|
+
"spec_path": spec_path,
|
|
177
|
+
"openapi_version": spec.get("openapi", spec.get("swagger", "unknown")),
|
|
178
|
+
"paths_count": len(spec.get("paths", {})),
|
|
179
|
+
"schemas_count": len(spec.get("components", {}).get("schemas", {})),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
except subprocess.TimeoutExpired:
|
|
183
|
+
return {
|
|
184
|
+
"success": False,
|
|
185
|
+
"error": f"Extraction timed out after {timeout}s. Check for blocking I/O in app startup.",
|
|
186
|
+
"error_type": "timeout",
|
|
187
|
+
}
|
|
188
|
+
finally:
|
|
189
|
+
try:
|
|
190
|
+
os.unlink(script_path)
|
|
191
|
+
except OSError:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _find_python(root: Path) -> Optional[str]:
|
|
196
|
+
"""Find the best Python binary for the project."""
|
|
197
|
+
# Check for project venv
|
|
198
|
+
for venv_dir in [root / "venv", root / ".venv", root / "env"]:
|
|
199
|
+
venv_python = venv_dir / "bin" / "python"
|
|
200
|
+
if venv_python.exists():
|
|
201
|
+
return str(venv_python)
|
|
202
|
+
|
|
203
|
+
# Fall back to system python
|
|
204
|
+
for name in ["python3", "python"]:
|
|
205
|
+
try:
|
|
206
|
+
result = subprocess.run(
|
|
207
|
+
[name, "--version"], capture_output=True, text=True, timeout=5
|
|
208
|
+
)
|
|
209
|
+
if result.returncode == 0:
|
|
210
|
+
return name
|
|
211
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _check_fastapi_installed(python: str, root: Path) -> Dict[str, Any]:
|
|
218
|
+
"""Check if FastAPI is importable with the given Python."""
|
|
219
|
+
try:
|
|
220
|
+
result = subprocess.run(
|
|
221
|
+
[python, "-c", "import fastapi; print(fastapi.__version__)"],
|
|
222
|
+
capture_output=True,
|
|
223
|
+
text=True,
|
|
224
|
+
timeout=10,
|
|
225
|
+
cwd=str(root),
|
|
226
|
+
)
|
|
227
|
+
if result.returncode == 0:
|
|
228
|
+
return {"installed": True, "version": result.stdout.strip()}
|
|
229
|
+
return {"installed": False}
|
|
230
|
+
except Exception:
|
|
231
|
+
return {"installed": False}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _write_temp_spec(spec: Dict[str, Any], root: Path) -> str:
|
|
235
|
+
"""Write extracted spec to a temp YAML file."""
|
|
236
|
+
import hashlib
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
import yaml
|
|
240
|
+
formatter = yaml.dump
|
|
241
|
+
ext = ".yaml"
|
|
242
|
+
except ImportError:
|
|
243
|
+
formatter = lambda d: json.dumps(d, indent=2)
|
|
244
|
+
ext = ".json"
|
|
245
|
+
|
|
246
|
+
# Deterministic filename based on project path
|
|
247
|
+
hash_input = str(root).encode()
|
|
248
|
+
short_hash = hashlib.sha256(hash_input).hexdigest()[:8]
|
|
249
|
+
spec_path = os.path.join(tempfile.gettempdir(), f"delimit-inferred-{short_hash}{ext}")
|
|
250
|
+
|
|
251
|
+
with open(spec_path, "w") as f:
|
|
252
|
+
f.write(formatter(spec))
|
|
253
|
+
|
|
254
|
+
return spec_path
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NestJS OpenAPI extractor — generates an OpenAPI spec from NestJS source code
|
|
3
|
+
without running the server. Uses subprocess + SwaggerModule.createDocument()
|
|
4
|
+
for full fidelity.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from .detector import AppLocation, FrameworkInfo
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Extraction script that imports the NestJS app, creates a document via
|
|
18
|
+
# SwaggerModule, and prints the OpenAPI JSON to stdout.
|
|
19
|
+
_EXTRACTOR_SCRIPT_TS = '''\
|
|
20
|
+
import {{ NestFactory }} from "@nestjs/core";
|
|
21
|
+
import {{ SwaggerModule, DocumentBuilder }} from "@nestjs/swagger";
|
|
22
|
+
|
|
23
|
+
async function extract() {{
|
|
24
|
+
// Dynamically import the app module
|
|
25
|
+
const mod = await import("{module_path}");
|
|
26
|
+
const ModuleClass = mod.{module_export} || (mod as any).default;
|
|
27
|
+
|
|
28
|
+
if (!ModuleClass) {{
|
|
29
|
+
console.error(JSON.stringify({{ error: "AppModule not found in {module_path}", type: "module_not_found" }}));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}}
|
|
32
|
+
|
|
33
|
+
try {{
|
|
34
|
+
const app = await NestFactory.create(ModuleClass, {{ logger: false }});
|
|
35
|
+
|
|
36
|
+
const config = new DocumentBuilder()
|
|
37
|
+
.setTitle("{title}")
|
|
38
|
+
.setVersion("{version}")
|
|
39
|
+
.build();
|
|
40
|
+
|
|
41
|
+
const document = SwaggerModule.createDocument(app, config);
|
|
42
|
+
console.log(JSON.stringify(document));
|
|
43
|
+
|
|
44
|
+
await app.close();
|
|
45
|
+
}} catch (e) {{
|
|
46
|
+
console.error(JSON.stringify({{ error: `NestFactory failed: ${{e.message}}`, type: "nest_create_error" }}));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}}
|
|
49
|
+
}}
|
|
50
|
+
|
|
51
|
+
extract().catch(e => {{
|
|
52
|
+
console.error(JSON.stringify({{ error: `Extraction failed: ${{e.message}}`, type: "extract_error" }}));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}});
|
|
55
|
+
'''
|
|
56
|
+
|
|
57
|
+
_EXTRACTOR_SCRIPT_JS = '''\
|
|
58
|
+
const {{ NestFactory }} = require("@nestjs/core");
|
|
59
|
+
const {{ SwaggerModule, DocumentBuilder }} = require("@nestjs/swagger");
|
|
60
|
+
|
|
61
|
+
async function extract() {{
|
|
62
|
+
const mod = require("{module_path}");
|
|
63
|
+
const ModuleClass = mod.{module_export} || mod.default;
|
|
64
|
+
|
|
65
|
+
if (!ModuleClass) {{
|
|
66
|
+
console.error(JSON.stringify({{ error: "AppModule not found in {module_path}", type: "module_not_found" }}));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}}
|
|
69
|
+
|
|
70
|
+
try {{
|
|
71
|
+
const app = await NestFactory.create(ModuleClass, {{ logger: false }});
|
|
72
|
+
|
|
73
|
+
const config = new DocumentBuilder()
|
|
74
|
+
.setTitle("{title}")
|
|
75
|
+
.setVersion("{version}")
|
|
76
|
+
.build();
|
|
77
|
+
|
|
78
|
+
const document = SwaggerModule.createDocument(app, config);
|
|
79
|
+
console.log(JSON.stringify(document));
|
|
80
|
+
|
|
81
|
+
await app.close();
|
|
82
|
+
}} catch (e) {{
|
|
83
|
+
console.error(JSON.stringify({{ error: `NestFactory failed: ${{e.message}}`, type: "nest_create_error" }}));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}}
|
|
86
|
+
}}
|
|
87
|
+
|
|
88
|
+
extract().catch(e => {{
|
|
89
|
+
console.error(JSON.stringify({{ error: `Extraction failed: ${{e.message}}`, type: "extract_error" }}));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}});
|
|
92
|
+
'''
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_nestjs_spec(
|
|
96
|
+
info: FrameworkInfo,
|
|
97
|
+
project_dir: str = ".",
|
|
98
|
+
timeout: int = 30,
|
|
99
|
+
node_bin: Optional[str] = None,
|
|
100
|
+
) -> Dict[str, Any]:
|
|
101
|
+
"""
|
|
102
|
+
Extract OpenAPI spec from a NestJS project.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
info: FrameworkInfo from detect_framework()
|
|
106
|
+
project_dir: Root of the NestJS project
|
|
107
|
+
timeout: Max seconds for extraction subprocess
|
|
108
|
+
node_bin: Node binary to use (auto-detected if None)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Dict with success/spec/error keys matching FastAPI extractor format.
|
|
112
|
+
"""
|
|
113
|
+
root = Path(project_dir).resolve()
|
|
114
|
+
|
|
115
|
+
# Check @nestjs/swagger is installed
|
|
116
|
+
if not _has_swagger_package(root):
|
|
117
|
+
return {
|
|
118
|
+
"success": False,
|
|
119
|
+
"error": "@nestjs/swagger not found. Run: npm install @nestjs/swagger",
|
|
120
|
+
"error_type": "missing_deps",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Check node_modules exists
|
|
124
|
+
if not (root / "node_modules").exists():
|
|
125
|
+
return {
|
|
126
|
+
"success": False,
|
|
127
|
+
"error": "node_modules not found. Run: npm install",
|
|
128
|
+
"error_type": "missing_deps",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Detect project structure
|
|
132
|
+
is_typescript = (root / "tsconfig.json").exists()
|
|
133
|
+
app_module = _find_app_module(root)
|
|
134
|
+
entry_info = _parse_nest_cli(root)
|
|
135
|
+
|
|
136
|
+
module_path = app_module or "./src/app.module"
|
|
137
|
+
module_export = "AppModule"
|
|
138
|
+
title = _get_package_name(root) or "API"
|
|
139
|
+
version = _get_package_version(root) or "1.0.0"
|
|
140
|
+
|
|
141
|
+
# Generate extraction script
|
|
142
|
+
if is_typescript:
|
|
143
|
+
script_content = _EXTRACTOR_SCRIPT_TS.format(
|
|
144
|
+
module_path=module_path,
|
|
145
|
+
module_export=module_export,
|
|
146
|
+
title=title,
|
|
147
|
+
version=version,
|
|
148
|
+
)
|
|
149
|
+
ext = ".ts"
|
|
150
|
+
else:
|
|
151
|
+
script_content = _EXTRACTOR_SCRIPT_JS.format(
|
|
152
|
+
module_path=module_path,
|
|
153
|
+
module_export=module_export,
|
|
154
|
+
title=title,
|
|
155
|
+
version=version,
|
|
156
|
+
)
|
|
157
|
+
ext = ".js"
|
|
158
|
+
|
|
159
|
+
# Write temp script
|
|
160
|
+
with tempfile.NamedTemporaryFile(
|
|
161
|
+
mode="w", suffix=ext, prefix="_delimit_extract_",
|
|
162
|
+
dir=str(root), delete=False,
|
|
163
|
+
) as f:
|
|
164
|
+
f.write(script_content)
|
|
165
|
+
script_path = f.name
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
# Build command
|
|
169
|
+
cmd = _build_command(root, script_path, is_typescript, node_bin)
|
|
170
|
+
if cmd is None:
|
|
171
|
+
return {
|
|
172
|
+
"success": False,
|
|
173
|
+
"error": "No suitable Node.js runner found. Install ts-node or tsx for TypeScript projects.",
|
|
174
|
+
"error_type": "no_runner",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
result = subprocess.run(
|
|
178
|
+
cmd,
|
|
179
|
+
capture_output=True,
|
|
180
|
+
text=True,
|
|
181
|
+
timeout=timeout,
|
|
182
|
+
cwd=str(root),
|
|
183
|
+
env={**os.environ, "NODE_ENV": "development"},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Parse stdout for the JSON spec
|
|
187
|
+
stdout = result.stdout.strip()
|
|
188
|
+
stderr = result.stderr.strip()
|
|
189
|
+
|
|
190
|
+
if result.returncode != 0:
|
|
191
|
+
# Try parsing structured error from stderr
|
|
192
|
+
try:
|
|
193
|
+
err = json.loads(stderr)
|
|
194
|
+
return {
|
|
195
|
+
"success": False,
|
|
196
|
+
"error": err.get("error", "Extraction failed"),
|
|
197
|
+
"error_type": err.get("type", "unknown"),
|
|
198
|
+
}
|
|
199
|
+
except json.JSONDecodeError:
|
|
200
|
+
return {
|
|
201
|
+
"success": False,
|
|
202
|
+
"error": stderr[:500] or "NestJS extraction subprocess failed",
|
|
203
|
+
"error_type": "subprocess",
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Parse spec from stdout
|
|
207
|
+
try:
|
|
208
|
+
spec = json.loads(stdout)
|
|
209
|
+
except json.JSONDecodeError:
|
|
210
|
+
return {
|
|
211
|
+
"success": False,
|
|
212
|
+
"error": "Extractor produced invalid JSON",
|
|
213
|
+
"error_type": "parse",
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if "error" in spec and "paths" not in spec:
|
|
217
|
+
return {
|
|
218
|
+
"success": False,
|
|
219
|
+
"error": spec["error"],
|
|
220
|
+
"error_type": spec.get("type", "unknown"),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Validate it's an OpenAPI spec
|
|
224
|
+
if "openapi" not in spec and "swagger" not in spec:
|
|
225
|
+
return {
|
|
226
|
+
"success": False,
|
|
227
|
+
"error": "Output is not a valid OpenAPI spec (missing 'openapi' key)",
|
|
228
|
+
"error_type": "invalid_spec",
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Write to temp YAML file
|
|
232
|
+
spec_path = _write_temp_spec(spec, root)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
"success": True,
|
|
236
|
+
"spec": spec,
|
|
237
|
+
"spec_path": spec_path,
|
|
238
|
+
"openapi_version": spec.get("openapi", spec.get("swagger", "unknown")),
|
|
239
|
+
"paths_count": len(spec.get("paths", {})),
|
|
240
|
+
"schemas_count": len(spec.get("components", {}).get("schemas", {})),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
except subprocess.TimeoutExpired:
|
|
244
|
+
return {
|
|
245
|
+
"success": False,
|
|
246
|
+
"error": f"Extraction timed out after {timeout}s. Check for blocking I/O in app startup.",
|
|
247
|
+
"error_type": "timeout",
|
|
248
|
+
}
|
|
249
|
+
finally:
|
|
250
|
+
try:
|
|
251
|
+
os.unlink(script_path)
|
|
252
|
+
except OSError:
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _has_swagger_package(root: Path) -> bool:
|
|
257
|
+
"""Check if @nestjs/swagger is in package.json."""
|
|
258
|
+
pkg = root / "package.json"
|
|
259
|
+
if not pkg.exists():
|
|
260
|
+
return False
|
|
261
|
+
try:
|
|
262
|
+
data = json.loads(pkg.read_text())
|
|
263
|
+
all_deps = {}
|
|
264
|
+
all_deps.update(data.get("dependencies", {}))
|
|
265
|
+
all_deps.update(data.get("devDependencies", {}))
|
|
266
|
+
return "@nestjs/swagger" in all_deps
|
|
267
|
+
except Exception:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _find_app_module(root: Path) -> Optional[str]:
|
|
272
|
+
"""Find the AppModule file in a NestJS project."""
|
|
273
|
+
candidates = [
|
|
274
|
+
"src/app.module.ts",
|
|
275
|
+
"src/app.module.js",
|
|
276
|
+
"app/app.module.ts",
|
|
277
|
+
"app/app.module.js",
|
|
278
|
+
]
|
|
279
|
+
for candidate in candidates:
|
|
280
|
+
if (root / candidate).exists():
|
|
281
|
+
return "./" + candidate.rsplit(".", 1)[0] # Remove extension
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _parse_nest_cli(root: Path) -> Dict[str, Any]:
|
|
286
|
+
"""Parse nest-cli.json for project configuration."""
|
|
287
|
+
cli_path = root / "nest-cli.json"
|
|
288
|
+
if not cli_path.exists():
|
|
289
|
+
return {}
|
|
290
|
+
try:
|
|
291
|
+
return json.loads(cli_path.read_text())
|
|
292
|
+
except Exception:
|
|
293
|
+
return {}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _get_package_name(root: Path) -> Optional[str]:
|
|
297
|
+
"""Get package name from package.json."""
|
|
298
|
+
try:
|
|
299
|
+
data = json.loads((root / "package.json").read_text())
|
|
300
|
+
return data.get("name")
|
|
301
|
+
except Exception:
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _get_package_version(root: Path) -> Optional[str]:
|
|
306
|
+
"""Get package version from package.json."""
|
|
307
|
+
try:
|
|
308
|
+
data = json.loads((root / "package.json").read_text())
|
|
309
|
+
return data.get("version")
|
|
310
|
+
except Exception:
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _build_command(root: Path, script_path: str, is_typescript: bool, node_bin: Optional[str] = None):
|
|
315
|
+
"""Build the subprocess command to run the extraction script."""
|
|
316
|
+
if is_typescript:
|
|
317
|
+
# Try ts-node first, then tsx, then npx ts-node
|
|
318
|
+
for runner in ["ts-node", "tsx"]:
|
|
319
|
+
local = root / "node_modules" / ".bin" / runner
|
|
320
|
+
if local.exists():
|
|
321
|
+
return [str(local), script_path]
|
|
322
|
+
|
|
323
|
+
# Try npx
|
|
324
|
+
try:
|
|
325
|
+
result = subprocess.run(
|
|
326
|
+
["npx", "--yes", "ts-node", "--version"],
|
|
327
|
+
capture_output=True, timeout=10, cwd=str(root),
|
|
328
|
+
)
|
|
329
|
+
if result.returncode == 0:
|
|
330
|
+
return ["npx", "--yes", "ts-node", script_path]
|
|
331
|
+
except Exception:
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
result = subprocess.run(
|
|
336
|
+
["npx", "--yes", "tsx", "--version"],
|
|
337
|
+
capture_output=True, timeout=10, cwd=str(root),
|
|
338
|
+
)
|
|
339
|
+
if result.returncode == 0:
|
|
340
|
+
return ["npx", "--yes", "tsx", script_path]
|
|
341
|
+
except Exception:
|
|
342
|
+
pass
|
|
343
|
+
|
|
344
|
+
return None
|
|
345
|
+
else:
|
|
346
|
+
node = node_bin or "node"
|
|
347
|
+
return [node, script_path]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _write_temp_spec(spec: Dict[str, Any], root: Path) -> str:
|
|
351
|
+
"""Write extracted spec to a temp YAML file."""
|
|
352
|
+
import hashlib
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
import yaml
|
|
356
|
+
formatter = yaml.dump
|
|
357
|
+
ext = ".yaml"
|
|
358
|
+
except ImportError:
|
|
359
|
+
formatter = lambda d: json.dumps(d, indent=2)
|
|
360
|
+
ext = ".json"
|
|
361
|
+
|
|
362
|
+
hash_input = str(root).encode()
|
|
363
|
+
short_hash = hashlib.sha256(hash_input).hexdigest()[:8]
|
|
364
|
+
spec_path = os.path.join(tempfile.gettempdir(), f"delimit-inferred-nestjs-{short_hash}{ext}")
|
|
365
|
+
|
|
366
|
+
with open(spec_path, "w") as f:
|
|
367
|
+
f.write(formatter(spec))
|
|
368
|
+
|
|
369
|
+
return spec_path
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Task modules will auto-register when imported
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|