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,483 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Express OpenAPI extractor — generates an OpenAPI spec from Express source code
|
|
3
|
+
by introspecting the app's route stack at runtime. Since Express has no built-in
|
|
4
|
+
OpenAPI generator, we inject a script that requires the app, walks
|
|
5
|
+
app._router.stack, and builds a minimal spec from discovered routes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from .detector import AppLocation, FrameworkInfo
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Node.js script that requires the Express app, walks the router stack,
|
|
20
|
+
# and emits a minimal OpenAPI 3.0.3 spec as JSON to stdout.
|
|
21
|
+
_EXTRACTOR_SCRIPT = r'''
|
|
22
|
+
"use strict";
|
|
23
|
+
|
|
24
|
+
const path = require("path");
|
|
25
|
+
|
|
26
|
+
const projectRoot = process.argv[2];
|
|
27
|
+
const appFile = process.argv[3];
|
|
28
|
+
const appVar = process.argv[4];
|
|
29
|
+
|
|
30
|
+
// Resolve the app module relative to the project root
|
|
31
|
+
const modulePath = path.resolve(projectRoot, appFile);
|
|
32
|
+
|
|
33
|
+
let appModule;
|
|
34
|
+
try {
|
|
35
|
+
appModule = require(modulePath);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.log(JSON.stringify({ error: "Import failed: " + e.message, type: "import" }));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const app = appModule[appVar] || appModule.default || appModule;
|
|
42
|
+
|
|
43
|
+
if (!app || typeof app !== "function" && typeof app !== "object") {
|
|
44
|
+
console.log(JSON.stringify({ error: "Variable '" + appVar + "' not found or not an Express app", type: "app_not_found" }));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check if this looks like an Express app (has _router or use/get methods)
|
|
49
|
+
if (!app._router && !app.get && !app.use) {
|
|
50
|
+
console.log(JSON.stringify({ error: "'" + appVar + "' does not appear to be an Express app (no _router)", type: "not_express" }));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Force Express to initialise its router if lazy
|
|
55
|
+
if (!app._router && typeof app.lazyrouter === "function") {
|
|
56
|
+
app.lazyrouter();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Collect routes -------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function expressParamToOpenAPI(routePath) {
|
|
62
|
+
// Convert Express :param to OpenAPI {param}
|
|
63
|
+
return routePath.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractPathParams(routePath) {
|
|
67
|
+
const params = [];
|
|
68
|
+
const re = /:([A-Za-z0-9_]+)/g;
|
|
69
|
+
let m;
|
|
70
|
+
while ((m = re.exec(routePath)) !== null) {
|
|
71
|
+
params.push(m[1]);
|
|
72
|
+
}
|
|
73
|
+
return params;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function collectRoutes(stack, prefix) {
|
|
77
|
+
const routes = [];
|
|
78
|
+
if (!stack) return routes;
|
|
79
|
+
|
|
80
|
+
for (const layer of stack) {
|
|
81
|
+
if (layer.route) {
|
|
82
|
+
// Direct route on the app
|
|
83
|
+
const routePath = prefix + layer.route.path;
|
|
84
|
+
const methods = Object.keys(layer.route.methods).filter(m => m !== "_all");
|
|
85
|
+
for (const method of methods) {
|
|
86
|
+
routes.push({ path: routePath, method: method.toLowerCase() });
|
|
87
|
+
}
|
|
88
|
+
} else if (layer.name === "router" && layer.handle && layer.handle.stack) {
|
|
89
|
+
// Mounted sub-router via app.use('/prefix', router)
|
|
90
|
+
let mountPath = "";
|
|
91
|
+
if (layer.regexp && layer.keys && layer.keys.length === 0) {
|
|
92
|
+
// Try to extract the mount path from the regexp source
|
|
93
|
+
mountPath = regexpToPath(layer.regexp);
|
|
94
|
+
}
|
|
95
|
+
if (layer.path) {
|
|
96
|
+
mountPath = layer.path;
|
|
97
|
+
}
|
|
98
|
+
// Recurse into the sub-router
|
|
99
|
+
const subRoutes = collectRoutes(layer.handle.stack, prefix + mountPath);
|
|
100
|
+
routes.push(...subRoutes);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return routes;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function regexpToPath(regexp) {
|
|
108
|
+
// Express stores mount paths as regexps. Common pattern:
|
|
109
|
+
// /^\/api\/v1\/?(?=\/|$)/i => /api/v1
|
|
110
|
+
if (!regexp || !regexp.source) return "";
|
|
111
|
+
const src = regexp.source;
|
|
112
|
+
// Strip anchors and optional trailing slash patterns
|
|
113
|
+
let cleaned = src
|
|
114
|
+
.replace(/^\^/, "")
|
|
115
|
+
.replace(/\\\/\?\(\?=\\\/\|\$\)$/i, "")
|
|
116
|
+
.replace(/\\\/\?\(\?:\\\/\)\?$/i, "")
|
|
117
|
+
.replace(/\\\//g, "/")
|
|
118
|
+
.replace(/\$$/,"");
|
|
119
|
+
// Only return if it looks like a clean path
|
|
120
|
+
if (/^[\/A-Za-z0-9_\-\.{}:]+$/.test(cleaned)) {
|
|
121
|
+
return cleaned;
|
|
122
|
+
}
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let routes;
|
|
127
|
+
try {
|
|
128
|
+
routes = collectRoutes(app._router ? app._router.stack : [], "");
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.log(JSON.stringify({ error: "Route extraction failed: " + e.message, type: "route_extraction" }));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (routes.length === 0) {
|
|
135
|
+
console.log(JSON.stringify({ error: "No routes found in app._router.stack", type: "no_routes" }));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Build OpenAPI spec ---------------------------------------------------
|
|
140
|
+
|
|
141
|
+
// Read package.json for metadata
|
|
142
|
+
let pkgName = "Express API";
|
|
143
|
+
let pkgVersion = "1.0.0";
|
|
144
|
+
try {
|
|
145
|
+
const pkg = require(path.resolve(projectRoot, "package.json"));
|
|
146
|
+
pkgName = pkg.name || pkgName;
|
|
147
|
+
pkgVersion = pkg.version || pkgVersion;
|
|
148
|
+
} catch (_) {}
|
|
149
|
+
|
|
150
|
+
const paths = {};
|
|
151
|
+
for (const route of routes) {
|
|
152
|
+
const oapiPath = expressParamToOpenAPI(route.path);
|
|
153
|
+
if (!paths[oapiPath]) {
|
|
154
|
+
paths[oapiPath] = {};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const pathParams = extractPathParams(route.path);
|
|
158
|
+
const parameters = pathParams.map(p => ({
|
|
159
|
+
name: p,
|
|
160
|
+
in: "path",
|
|
161
|
+
required: true,
|
|
162
|
+
schema: { type: "string" },
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
const operation = {
|
|
166
|
+
summary: route.method.toUpperCase() + " " + oapiPath,
|
|
167
|
+
responses: {
|
|
168
|
+
"200": { description: "Successful response" },
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (parameters.length > 0) {
|
|
173
|
+
operation.parameters = parameters;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// POST/PUT/PATCH get a requestBody placeholder
|
|
177
|
+
if (["post", "put", "patch"].includes(route.method)) {
|
|
178
|
+
operation.requestBody = {
|
|
179
|
+
content: {
|
|
180
|
+
"application/json": {
|
|
181
|
+
schema: { type: "object" },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
paths[oapiPath][route.method] = operation;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const spec = {
|
|
191
|
+
openapi: "3.0.3",
|
|
192
|
+
info: {
|
|
193
|
+
title: pkgName,
|
|
194
|
+
version: pkgVersion,
|
|
195
|
+
},
|
|
196
|
+
paths: paths,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
console.log(JSON.stringify(spec));
|
|
200
|
+
'''
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def extract_express_spec(
|
|
204
|
+
info: FrameworkInfo,
|
|
205
|
+
project_dir: str = ".",
|
|
206
|
+
timeout: int = 15,
|
|
207
|
+
node_bin: Optional[str] = None,
|
|
208
|
+
) -> Dict[str, Any]:
|
|
209
|
+
"""
|
|
210
|
+
Extract OpenAPI spec from an Express project.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
info: FrameworkInfo from detect_framework()
|
|
214
|
+
project_dir: Root of the Express project
|
|
215
|
+
timeout: Max seconds for extraction subprocess
|
|
216
|
+
node_bin: Node binary to use (auto-detected if None)
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Dict with keys:
|
|
220
|
+
- success: bool
|
|
221
|
+
- spec: OpenAPI dict (if success)
|
|
222
|
+
- spec_path: Path to temp YAML file (if success)
|
|
223
|
+
- openapi_version: str (if success)
|
|
224
|
+
- paths_count: int (if success)
|
|
225
|
+
- schemas_count: int (if success)
|
|
226
|
+
- error: Error message (if not success)
|
|
227
|
+
- error_type: Error category (if not success)
|
|
228
|
+
"""
|
|
229
|
+
root = Path(project_dir).resolve()
|
|
230
|
+
|
|
231
|
+
if not info.app_locations:
|
|
232
|
+
# Try to auto-detect the app file
|
|
233
|
+
app_loc = _find_express_app_fallback(root)
|
|
234
|
+
if not app_loc:
|
|
235
|
+
return {
|
|
236
|
+
"success": False,
|
|
237
|
+
"error": "No Express app instance found. Looked for module.exports = app or exports patterns.",
|
|
238
|
+
"error_type": "no_app",
|
|
239
|
+
}
|
|
240
|
+
else:
|
|
241
|
+
app_loc = info.app_locations[0]
|
|
242
|
+
|
|
243
|
+
node = node_bin or _find_node(root)
|
|
244
|
+
if not node:
|
|
245
|
+
return {
|
|
246
|
+
"success": False,
|
|
247
|
+
"error": "Node.js not found. Install Node.js 14+ or set node_bin.",
|
|
248
|
+
"error_type": "no_node",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Check node_modules exists (Express needs to be installed)
|
|
252
|
+
if not (root / "node_modules").exists():
|
|
253
|
+
return {
|
|
254
|
+
"success": False,
|
|
255
|
+
"error": "node_modules not found. Run: npm install",
|
|
256
|
+
"error_type": "missing_deps",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# Check express is actually importable
|
|
260
|
+
if not _check_express_installed(node, root):
|
|
261
|
+
return {
|
|
262
|
+
"success": False,
|
|
263
|
+
"error": "Express not installed. Run: npm install express",
|
|
264
|
+
"error_type": "missing_deps",
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# Write extractor script to temp file inside the project (so require() resolves)
|
|
268
|
+
with tempfile.NamedTemporaryFile(
|
|
269
|
+
mode="w", suffix=".js", prefix="_delimit_extract_",
|
|
270
|
+
dir=str(root), delete=False,
|
|
271
|
+
) as f:
|
|
272
|
+
f.write(_EXTRACTOR_SCRIPT)
|
|
273
|
+
script_path = f.name
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
result = subprocess.run(
|
|
277
|
+
[node, script_path, str(root), app_loc.file, app_loc.variable],
|
|
278
|
+
capture_output=True,
|
|
279
|
+
text=True,
|
|
280
|
+
timeout=timeout,
|
|
281
|
+
cwd=str(root),
|
|
282
|
+
env={**os.environ, "NODE_ENV": "production"},
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
stdout = result.stdout.strip()
|
|
286
|
+
stderr = result.stderr.strip()
|
|
287
|
+
|
|
288
|
+
if result.returncode != 0:
|
|
289
|
+
# Try to parse structured error from stdout
|
|
290
|
+
try:
|
|
291
|
+
err = json.loads(stdout)
|
|
292
|
+
return {
|
|
293
|
+
"success": False,
|
|
294
|
+
"error": err.get("error", "Extraction failed"),
|
|
295
|
+
"error_type": err.get("type", "unknown"),
|
|
296
|
+
}
|
|
297
|
+
except json.JSONDecodeError:
|
|
298
|
+
return {
|
|
299
|
+
"success": False,
|
|
300
|
+
"error": stderr[:500] or stdout[:500] or "Express extraction subprocess failed",
|
|
301
|
+
"error_type": "subprocess",
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Parse the OpenAPI spec
|
|
305
|
+
try:
|
|
306
|
+
spec = json.loads(stdout)
|
|
307
|
+
except json.JSONDecodeError:
|
|
308
|
+
return {
|
|
309
|
+
"success": False,
|
|
310
|
+
"error": "Extractor produced invalid JSON",
|
|
311
|
+
"error_type": "parse",
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if "error" in spec and "paths" not in spec:
|
|
315
|
+
return {
|
|
316
|
+
"success": False,
|
|
317
|
+
"error": spec["error"],
|
|
318
|
+
"error_type": spec.get("type", "unknown"),
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Validate it looks like an OpenAPI spec
|
|
322
|
+
if "openapi" not in spec and "swagger" not in spec:
|
|
323
|
+
return {
|
|
324
|
+
"success": False,
|
|
325
|
+
"error": "Output is not a valid OpenAPI spec (missing 'openapi' key)",
|
|
326
|
+
"error_type": "invalid_spec",
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# Write to temp YAML/JSON file for downstream consumption
|
|
330
|
+
spec_path = _write_temp_spec(spec, root)
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
"success": True,
|
|
334
|
+
"spec": spec,
|
|
335
|
+
"spec_path": spec_path,
|
|
336
|
+
"openapi_version": spec.get("openapi", spec.get("swagger", "unknown")),
|
|
337
|
+
"paths_count": len(spec.get("paths", {})),
|
|
338
|
+
"schemas_count": len(spec.get("components", {}).get("schemas", {})),
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
except subprocess.TimeoutExpired:
|
|
342
|
+
return {
|
|
343
|
+
"success": False,
|
|
344
|
+
"error": f"Extraction timed out after {timeout}s. Check for blocking I/O in app startup.",
|
|
345
|
+
"error_type": "timeout",
|
|
346
|
+
}
|
|
347
|
+
finally:
|
|
348
|
+
try:
|
|
349
|
+
os.unlink(script_path)
|
|
350
|
+
except OSError:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _find_node(root: Path) -> Optional[str]:
|
|
355
|
+
"""Find the best Node.js binary."""
|
|
356
|
+
# Check for nvm/local node
|
|
357
|
+
for name in ["node", "nodejs"]:
|
|
358
|
+
try:
|
|
359
|
+
result = subprocess.run(
|
|
360
|
+
[name, "--version"], capture_output=True, text=True, timeout=5,
|
|
361
|
+
)
|
|
362
|
+
if result.returncode == 0:
|
|
363
|
+
return name
|
|
364
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
365
|
+
continue
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _check_express_installed(node: str, root: Path) -> bool:
|
|
370
|
+
"""Check if express is importable with the given Node."""
|
|
371
|
+
try:
|
|
372
|
+
result = subprocess.run(
|
|
373
|
+
[node, "-e", "require('express'); console.log('ok')"],
|
|
374
|
+
capture_output=True,
|
|
375
|
+
text=True,
|
|
376
|
+
timeout=10,
|
|
377
|
+
cwd=str(root),
|
|
378
|
+
)
|
|
379
|
+
return result.returncode == 0
|
|
380
|
+
except Exception:
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _find_express_app_fallback(root: Path) -> Optional[AppLocation]:
|
|
385
|
+
"""Try to find an Express app export in common entry point files."""
|
|
386
|
+
candidates = [
|
|
387
|
+
"app.js", "src/app.js", "server.js", "src/server.js",
|
|
388
|
+
"index.js", "src/index.js", "app.ts", "src/app.ts",
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
for rel_path in candidates:
|
|
392
|
+
full_path = root / rel_path
|
|
393
|
+
if not full_path.exists():
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
content = full_path.read_text()
|
|
398
|
+
except Exception:
|
|
399
|
+
continue
|
|
400
|
+
|
|
401
|
+
# Check for Express app creation patterns
|
|
402
|
+
if not re.search(r"require\s*\(\s*['\"]express['\"]\s*\)", content) and \
|
|
403
|
+
not re.search(r"from\s+['\"]express['\"]", content):
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
# Find the variable name of the Express app
|
|
407
|
+
var_name = _detect_app_variable(content)
|
|
408
|
+
if var_name:
|
|
409
|
+
return AppLocation(file=rel_path, variable=var_name, line=1)
|
|
410
|
+
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _detect_app_variable(content: str) -> Optional[str]:
|
|
415
|
+
"""Detect the variable name of an Express app instance in source code."""
|
|
416
|
+
# Pattern: const app = express()
|
|
417
|
+
m = re.search(r"(?:const|let|var)\s+(\w+)\s*=\s*(?:express\s*\(\s*\)|require\s*\(\s*['\"]express['\"]\s*\)\s*\(\s*\))", content)
|
|
418
|
+
if m:
|
|
419
|
+
return m.group(1)
|
|
420
|
+
|
|
421
|
+
# Pattern: const express = require('express'); ... const app = express();
|
|
422
|
+
# First find what express is called
|
|
423
|
+
express_var = None
|
|
424
|
+
m_req = re.search(r"(?:const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['\"]express['\"]\s*\)", content)
|
|
425
|
+
if m_req:
|
|
426
|
+
express_var = m_req.group(1)
|
|
427
|
+
|
|
428
|
+
if express_var:
|
|
429
|
+
m_app = re.search(rf"(?:const|let|var)\s+(\w+)\s*=\s*{re.escape(express_var)}\s*\(\s*\)", content)
|
|
430
|
+
if m_app:
|
|
431
|
+
return m_app.group(1)
|
|
432
|
+
|
|
433
|
+
# Fallback: look for module.exports = <varname> where varname is likely the app
|
|
434
|
+
m_exp = re.search(r"module\.exports\s*=\s*(\w+)", content)
|
|
435
|
+
if m_exp:
|
|
436
|
+
return m_exp.group(1)
|
|
437
|
+
|
|
438
|
+
# Fallback: exports.app = ...
|
|
439
|
+
m_exp2 = re.search(r"exports\.(\w+)\s*=", content)
|
|
440
|
+
if m_exp2:
|
|
441
|
+
return m_exp2.group(1)
|
|
442
|
+
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _iter_js_files(root: Path, max_files: int = 50) -> List[Path]:
|
|
447
|
+
"""Iterate JS/TS files, skipping node_modules and hidden dirs."""
|
|
448
|
+
skip_dirs = {
|
|
449
|
+
"node_modules", ".git", "dist", "build", "coverage",
|
|
450
|
+
".nyc_output", ".next", ".nuxt",
|
|
451
|
+
}
|
|
452
|
+
count = 0
|
|
453
|
+
files = []
|
|
454
|
+
for path in root.rglob("*.js"):
|
|
455
|
+
if any(part in skip_dirs for part in path.parts):
|
|
456
|
+
continue
|
|
457
|
+
files.append(path)
|
|
458
|
+
count += 1
|
|
459
|
+
if count >= max_files:
|
|
460
|
+
break
|
|
461
|
+
return files
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _write_temp_spec(spec: Dict[str, Any], root: Path) -> str:
|
|
465
|
+
"""Write extracted spec to a temp YAML file."""
|
|
466
|
+
import hashlib
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
import yaml
|
|
470
|
+
formatter = yaml.dump
|
|
471
|
+
ext = ".yaml"
|
|
472
|
+
except ImportError:
|
|
473
|
+
formatter = lambda d: json.dumps(d, indent=2)
|
|
474
|
+
ext = ".json"
|
|
475
|
+
|
|
476
|
+
hash_input = str(root).encode()
|
|
477
|
+
short_hash = hashlib.sha256(hash_input).hexdigest()[:8]
|
|
478
|
+
spec_path = os.path.join(tempfile.gettempdir(), f"delimit-inferred-express-{short_hash}{ext}")
|
|
479
|
+
|
|
480
|
+
with open(spec_path, "w") as f:
|
|
481
|
+
f.write(formatter(spec))
|
|
482
|
+
|
|
483
|
+
return spec_path
|