delimit-cli 2.4.0 → 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.
Files changed (112) hide show
  1. package/.dockerignore +7 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/CODE_OF_CONDUCT.md +48 -0
  4. package/CONTRIBUTING.md +67 -0
  5. package/Dockerfile +9 -0
  6. package/LICENSE +21 -0
  7. package/README.md +18 -69
  8. package/SECURITY.md +42 -0
  9. package/adapters/gemini-forge.js +11 -0
  10. package/adapters/gemini-jamsons.js +152 -0
  11. package/bin/delimit-cli.js +8 -0
  12. package/bin/delimit-setup.js +258 -0
  13. package/gateway/ai/backends/__init__.py +0 -0
  14. package/gateway/ai/backends/async_utils.py +21 -0
  15. package/gateway/ai/backends/deploy_bridge.py +150 -0
  16. package/gateway/ai/backends/gateway_core.py +261 -0
  17. package/gateway/ai/backends/generate_bridge.py +38 -0
  18. package/gateway/ai/backends/governance_bridge.py +196 -0
  19. package/gateway/ai/backends/intel_bridge.py +59 -0
  20. package/gateway/ai/backends/memory_bridge.py +93 -0
  21. package/gateway/ai/backends/ops_bridge.py +137 -0
  22. package/gateway/ai/backends/os_bridge.py +82 -0
  23. package/gateway/ai/backends/repo_bridge.py +117 -0
  24. package/gateway/ai/backends/ui_bridge.py +118 -0
  25. package/gateway/ai/backends/vault_bridge.py +129 -0
  26. package/gateway/ai/server.py +1182 -0
  27. package/gateway/core/__init__.py +3 -0
  28. package/gateway/core/__pycache__/__init__.cpython-310.pyc +0 -0
  29. package/gateway/core/__pycache__/auto_baseline.cpython-310.pyc +0 -0
  30. package/gateway/core/__pycache__/ci_formatter.cpython-310.pyc +0 -0
  31. package/gateway/core/__pycache__/contract_ledger.cpython-310.pyc +0 -0
  32. package/gateway/core/__pycache__/dependency_graph.cpython-310.pyc +0 -0
  33. package/gateway/core/__pycache__/dependency_manifest.cpython-310.pyc +0 -0
  34. package/gateway/core/__pycache__/diff_engine_v2.cpython-310.pyc +0 -0
  35. package/gateway/core/__pycache__/event_backbone.cpython-310.pyc +0 -0
  36. package/gateway/core/__pycache__/event_schema.cpython-310.pyc +0 -0
  37. package/gateway/core/__pycache__/explainer.cpython-310.pyc +0 -0
  38. package/gateway/core/__pycache__/gateway.cpython-310.pyc +0 -0
  39. package/gateway/core/__pycache__/gateway_v2.cpython-310.pyc +0 -0
  40. package/gateway/core/__pycache__/gateway_v3.cpython-310.pyc +0 -0
  41. package/gateway/core/__pycache__/impact_analyzer.cpython-310.pyc +0 -0
  42. package/gateway/core/__pycache__/policy_engine.cpython-310.pyc +0 -0
  43. package/gateway/core/__pycache__/registry.cpython-310.pyc +0 -0
  44. package/gateway/core/__pycache__/registry_v2.cpython-310.pyc +0 -0
  45. package/gateway/core/__pycache__/registry_v3.cpython-310.pyc +0 -0
  46. package/gateway/core/__pycache__/semver_classifier.cpython-310.pyc +0 -0
  47. package/gateway/core/__pycache__/spec_detector.cpython-310.pyc +0 -0
  48. package/gateway/core/__pycache__/surface_bridge.cpython-310.pyc +0 -0
  49. package/gateway/core/auto_baseline.py +304 -0
  50. package/gateway/core/ci_formatter.py +283 -0
  51. package/gateway/core/complexity_analyzer.py +386 -0
  52. package/gateway/core/contract_ledger.py +345 -0
  53. package/gateway/core/dependency_graph.py +218 -0
  54. package/gateway/core/dependency_manifest.py +223 -0
  55. package/gateway/core/diff_engine_v2.py +477 -0
  56. package/gateway/core/diff_engine_v2.py.bak +426 -0
  57. package/gateway/core/event_backbone.py +268 -0
  58. package/gateway/core/event_schema.py +258 -0
  59. package/gateway/core/explainer.py +438 -0
  60. package/gateway/core/gateway.py +128 -0
  61. package/gateway/core/gateway_v2.py +154 -0
  62. package/gateway/core/gateway_v3.py +224 -0
  63. package/gateway/core/impact_analyzer.py +163 -0
  64. package/gateway/core/policies/default.yml +13 -0
  65. package/gateway/core/policies/relaxed.yml +48 -0
  66. package/gateway/core/policies/strict.yml +55 -0
  67. package/gateway/core/policy_engine.py +464 -0
  68. package/gateway/core/registry.py +52 -0
  69. package/gateway/core/registry_v2.py +132 -0
  70. package/gateway/core/registry_v3.py +134 -0
  71. package/gateway/core/semver_classifier.py +152 -0
  72. package/gateway/core/spec_detector.py +130 -0
  73. package/gateway/core/surface_bridge.py +307 -0
  74. package/gateway/core/zero_spec/__init__.py +4 -0
  75. package/gateway/core/zero_spec/__pycache__/__init__.cpython-310.pyc +0 -0
  76. package/gateway/core/zero_spec/__pycache__/detector.cpython-310.pyc +0 -0
  77. package/gateway/core/zero_spec/__pycache__/express_extractor.cpython-310.pyc +0 -0
  78. package/gateway/core/zero_spec/__pycache__/fastapi_extractor.cpython-310.pyc +0 -0
  79. package/gateway/core/zero_spec/__pycache__/nestjs_extractor.cpython-310.pyc +0 -0
  80. package/gateway/core/zero_spec/detector.py +353 -0
  81. package/gateway/core/zero_spec/express_extractor.py +483 -0
  82. package/gateway/core/zero_spec/fastapi_extractor.py +254 -0
  83. package/gateway/core/zero_spec/nestjs_extractor.py +369 -0
  84. package/gateway/tasks/__init__.py +1 -0
  85. package/gateway/tasks/__pycache__/__init__.cpython-310.pyc +0 -0
  86. package/gateway/tasks/__pycache__/check_policy.cpython-310.pyc +0 -0
  87. package/gateway/tasks/__pycache__/check_policy_v2.cpython-310.pyc +0 -0
  88. package/gateway/tasks/__pycache__/check_policy_v3.cpython-310.pyc +0 -0
  89. package/gateway/tasks/__pycache__/explain_diff.cpython-310.pyc +0 -0
  90. package/gateway/tasks/__pycache__/explain_diff_v2.cpython-310.pyc +0 -0
  91. package/gateway/tasks/__pycache__/validate_api.cpython-310.pyc +0 -0
  92. package/gateway/tasks/__pycache__/validate_api_v2.cpython-310.pyc +0 -0
  93. package/gateway/tasks/__pycache__/validate_api_v3.cpython-310.pyc +0 -0
  94. package/gateway/tasks/check_policy.py +177 -0
  95. package/gateway/tasks/check_policy_v2.py +255 -0
  96. package/gateway/tasks/check_policy_v3.py +255 -0
  97. package/gateway/tasks/explain_diff.py +305 -0
  98. package/gateway/tasks/explain_diff_v2.py +267 -0
  99. package/gateway/tasks/validate_api.py +131 -0
  100. package/gateway/tasks/validate_api_v2.py +208 -0
  101. package/gateway/tasks/validate_api_v3.py +163 -0
  102. package/package.json +2 -2
  103. package/adapters/codex-skill.js +0 -87
  104. package/adapters/cursor-extension.js +0 -190
  105. package/adapters/gemini-action.js +0 -93
  106. package/adapters/openai-function.js +0 -112
  107. package/adapters/xai-plugin.js +0 -151
  108. package/test-decision-engine.js +0 -181
  109. package/test-hook.js +0 -27
  110. package/tests/cli.test.js +0 -359
  111. package/tests/fixtures/openapi-changed.yaml +0 -56
  112. package/tests/fixtures/openapi.yaml +0 -87
@@ -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