infernoflow 0.37.0 → 0.37.3
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/CHANGELOG.md +125 -0
- package/dist/bin/infernoflow.mjs +29 -277
- package/dist/lib/adopters/angular.mjs +1 -128
- package/dist/lib/adopters/css.mjs +1 -111
- package/dist/lib/adopters/react.mjs +1 -104
- package/dist/lib/ai/ideDetection.mjs +1 -31
- package/dist/lib/ai/localProvider.mjs +1 -88
- package/dist/lib/ai/providerRouter.mjs +2 -295
- package/dist/lib/commands/adopt.mjs +20 -869
- package/dist/lib/commands/adoptWizard.mjs +9 -320
- package/dist/lib/commands/agent.mjs +5 -191
- package/dist/lib/commands/ai.mjs +2 -407
- package/dist/lib/commands/ask.mjs +4 -299
- package/dist/lib/commands/audit.mjs +13 -300
- package/dist/lib/commands/changelog.mjs +26 -594
- package/dist/lib/commands/check.mjs +3 -184
- package/dist/lib/commands/ci.mjs +3 -208
- package/dist/lib/commands/claudeMd.mjs +30 -135
- package/dist/lib/commands/cloud.mjs +10 -773
- package/dist/lib/commands/context.mjs +34 -346
- package/dist/lib/commands/coverage.mjs +2 -282
- package/dist/lib/commands/dashboard.mjs +123 -635
- package/dist/lib/commands/demo.mjs +8 -465
- package/dist/lib/commands/diff.mjs +5 -274
- package/dist/lib/commands/docGate.mjs +2 -81
- package/dist/lib/commands/doctor.mjs +3 -321
- package/dist/lib/commands/explain.mjs +8 -438
- package/dist/lib/commands/export.mjs +10 -239
- package/dist/lib/commands/feedback.mjs +12 -216
- package/dist/lib/commands/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +11 -378
- package/dist/lib/commands/health.mjs +2 -309
- package/dist/lib/commands/impact.mjs +2 -325
- package/dist/lib/commands/implement.mjs +7 -103
- package/dist/lib/commands/init.mjs +45 -631
- package/dist/lib/commands/installCursorHooks.mjs +1 -36
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
- package/dist/lib/commands/link.mjs +2 -342
- package/dist/lib/commands/log.mjs +18 -248
- package/dist/lib/commands/monorepo.mjs +4 -428
- package/dist/lib/commands/notify.mjs +4 -258
- package/dist/lib/commands/onboard.mjs +4 -296
- package/dist/lib/commands/prComment.mjs +2 -361
- package/dist/lib/commands/prImpact.mjs +2 -157
- package/dist/lib/commands/publish.mjs +15 -316
- package/dist/lib/commands/recap.mjs +6 -380
- package/dist/lib/commands/report.mjs +28 -272
- package/dist/lib/commands/review.mjs +9 -223
- package/dist/lib/commands/run.mjs +8 -336
- package/dist/lib/commands/scaffold.mjs +54 -419
- package/dist/lib/commands/scan.mjs +11 -1118
- package/dist/lib/commands/scout.mjs +2 -291
- package/dist/lib/commands/setup.mjs +5 -310
- package/dist/lib/commands/share.mjs +13 -196
- package/dist/lib/commands/snapshot.mjs +3 -383
- package/dist/lib/commands/stability.mjs +2 -293
- package/dist/lib/commands/stats.mjs +5 -402
- package/dist/lib/commands/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- package/dist/lib/commands/switch.mjs +13 -517
- package/dist/lib/commands/syncAuto.mjs +1 -96
- package/dist/lib/commands/synthesize.mjs +10 -228
- package/dist/lib/commands/teamSync.mjs +2 -388
- package/dist/lib/commands/test.mjs +6 -363
- package/dist/lib/commands/theme.mjs +18 -195
- package/dist/lib/commands/uninstall.mjs +13 -406
- package/dist/lib/commands/upgrade.mjs +20 -153
- package/dist/lib/commands/version.mjs +2 -282
- package/dist/lib/commands/vibe.mjs +7 -357
- package/dist/lib/commands/watch.mjs +4 -203
- package/dist/lib/commands/why.mjs +4 -358
- package/dist/lib/cursorHooksInstall.mjs +1 -60
- package/dist/lib/draftToolingInstall.mjs +7 -68
- package/dist/lib/git/detect-drift.mjs +4 -208
- package/dist/lib/learning/adapt.mjs +6 -101
- package/dist/lib/learning/observe.mjs +1 -119
- package/dist/lib/learning/patternDetector.mjs +1 -298
- package/dist/lib/learning/profile.mjs +2 -279
- package/dist/lib/learning/skillSynthesizer.mjs +24 -145
- package/dist/lib/telemetry.mjs +19 -269
- package/dist/lib/templates/index.mjs +1 -131
- package/dist/lib/theme/scanner.mjs +4 -343
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -95
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/package.json +2 -4
- package/scripts/postinstall.js +2 -2
|
@@ -1,869 +1,20 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function safeRead(filePath) {
|
|
24
|
-
try {
|
|
25
|
-
return fs.readFileSync(filePath, "utf8");
|
|
26
|
-
} catch {
|
|
27
|
-
return "";
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const HEURISTICS = [
|
|
32
|
-
{ id: "CreateItem", title: "Create Item", regex: /\b(post|create|add)\b/i },
|
|
33
|
-
{ id: "ReadItems", title: "Read Items", regex: /\b(get|read|list|fetch)\b/i },
|
|
34
|
-
{ id: "UpdateItem", title: "Update Item", regex: /\b(put|patch|update|edit)\b/i },
|
|
35
|
-
{ id: "DeleteItem", title: "Delete Item", regex: /\b(delete|remove)\b/i },
|
|
36
|
-
{ id: "SearchItems", title: "Search Items", regex: /\bsearch\b/i },
|
|
37
|
-
{ id: "FilterItems", title: "Filter Items", regex: /\bfilter\b/i },
|
|
38
|
-
{ id: "SetDueDate", title: "Set Due Date", regex: /\bdueDate|deadline|due\b/i },
|
|
39
|
-
{ id: "SetPriority", title: "Set Priority", regex: /\bpriority\b/i },
|
|
40
|
-
{ id: "ToggleComplete", title: "Toggle Complete", regex: /\bcomplete|completed|toggle\b/i },
|
|
41
|
-
{ id: "ClearCompleted", title: "Clear Completed", regex: /\bclearCompleted|clear completed\b/i },
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
export function discoverCapabilities(cwd) {
|
|
45
|
-
return discoverProjectSignals(cwd).capabilities;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function collectCodeFiles(cwd) {
|
|
49
|
-
const files = [];
|
|
50
|
-
const roots = ["src", "server", "app", "backend", "frontend", "api", "Controllers"];
|
|
51
|
-
for (const r of roots) {
|
|
52
|
-
const root = path.join(cwd, r);
|
|
53
|
-
if (!fs.existsSync(root)) continue;
|
|
54
|
-
const stack = [root];
|
|
55
|
-
while (stack.length) {
|
|
56
|
-
const cur = stack.pop();
|
|
57
|
-
for (const entry of fs.readdirSync(cur, { withFileTypes: true })) {
|
|
58
|
-
const p = path.join(cur, entry.name);
|
|
59
|
-
if (entry.isDirectory()) {
|
|
60
|
-
const SKIP_DIRS = new Set([
|
|
61
|
-
"node_modules", ".git", "dist", "build", "out", "www", "tmp", ".tmp",
|
|
62
|
-
"vendor", "assets", "public", "static", "coverage", ".nyc_output",
|
|
63
|
-
".angular", ".vite", ".cache", ".parcel-cache", ".next", ".nuxt",
|
|
64
|
-
"__pycache__", "e2e", "test", "tests", "spec", "__tests__",
|
|
65
|
-
"fixtures", "mocks", ".turbo", "storybook-static",
|
|
66
|
-
]);
|
|
67
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
68
|
-
stack.push(p);
|
|
69
|
-
} else if (/\.(js|jsx|ts|tsx|mjs|cjs|json|md|html|htm|cs|csproj)$/.test(entry.name)) {
|
|
70
|
-
// Skip minified, bundled, and source-map files
|
|
71
|
-
if (/\.(min|bundle)\.(js|css)$/.test(entry.name)) continue;
|
|
72
|
-
if (/\.map$/.test(entry.name)) continue;
|
|
73
|
-
// Skip generated Angular/framework files
|
|
74
|
-
if (/\.(spec|test)\.(ts|js|tsx|jsx)$/.test(entry.name)) continue;
|
|
75
|
-
files.push(p);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// Include common root-level .NET entry files without scanning the whole repo.
|
|
81
|
-
for (const entry of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
82
|
-
if (!entry.isFile()) continue;
|
|
83
|
-
if (/^(Program\.cs|.+\.csproj)$/i.test(entry.name)) {
|
|
84
|
-
files.push(path.join(cwd, entry.name));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return files;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function detectComponents(files, cwd) {
|
|
91
|
-
const names = new Set();
|
|
92
|
-
for (const filePath of files) {
|
|
93
|
-
const rel = path.relative(cwd, filePath);
|
|
94
|
-
const text = safeRead(filePath);
|
|
95
|
-
const classMatches = text.matchAll(/\bclass\s+([A-Z][A-Za-z0-9_]*?(?:Component|Page|View|Widget|Card))\b/g);
|
|
96
|
-
for (const m of classMatches) names.add(m[1]);
|
|
97
|
-
const selectorMatches = text.matchAll(/\bselector\s*:\s*["']([^"']+)["']/g);
|
|
98
|
-
for (const m of selectorMatches) names.add(m[1]);
|
|
99
|
-
const reactFnMatches = text.matchAll(/\bfunction\s+([A-Z][A-Za-z0-9_]*)\s*\(/g);
|
|
100
|
-
for (const m of reactFnMatches) {
|
|
101
|
-
if (/component|page|view|card|chart|dashboard/i.test(m[1])) names.add(m[1]);
|
|
102
|
-
}
|
|
103
|
-
const relMatch = rel.match(/([^/\\]+)\.(component|page|view|widget|card)\.(ts|tsx|js|jsx)$/i);
|
|
104
|
-
if (relMatch) names.add(relMatch[1]);
|
|
105
|
-
}
|
|
106
|
-
return Array.from(names).sort();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function detectDisplayFields(files) {
|
|
110
|
-
const fields = new Set();
|
|
111
|
-
const methodNames = new Set();
|
|
112
|
-
const stopWords = new Set([
|
|
113
|
-
"if", "for", "while", "const", "let", "var", "return", "function", "class", "import", "export",
|
|
114
|
-
"null", "undefined", "true", "false", "string", "number", "boolean", "any", "unknown", "never",
|
|
115
|
-
"selector", "templateUrl", "styleUrl", "standalone", "imports", "providers", "providedIn",
|
|
116
|
-
"options", "scales", "responsive", "display", "title", "type", "label",
|
|
117
|
-
"component", "service", "routes", "appConfig", "ApplicationConfig",
|
|
118
|
-
]);
|
|
119
|
-
const add = (v) => {
|
|
120
|
-
if (!v) return;
|
|
121
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(v)) return;
|
|
122
|
-
if (v.length <= 1) return;
|
|
123
|
-
if (stopWords.has(v)) return;
|
|
124
|
-
if (/^[A-Z0-9_]+$/.test(v)) return;
|
|
125
|
-
fields.add(v);
|
|
126
|
-
};
|
|
127
|
-
for (const filePath of files) {
|
|
128
|
-
const text = safeRead(filePath);
|
|
129
|
-
if (/\.(html|htm)$/i.test(filePath)) {
|
|
130
|
-
const angularInterpolations = text.matchAll(/\{\{\s*(?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)/g);
|
|
131
|
-
for (const m of angularInterpolations) add(m[1]);
|
|
132
|
-
const ngModels = text.matchAll(/\[\(ngModel\)\]\s*=\s*["']([a-zA-Z_][a-zA-Z0-9_]*)["']/g);
|
|
133
|
-
for (const m of ngModels) add(m[1]);
|
|
134
|
-
const ngInputs = text.matchAll(/\[[a-zA-Z0-9_-]+\]\s*=\s*["'](?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)["']/g);
|
|
135
|
-
for (const m of ngInputs) add(m[1]);
|
|
136
|
-
const ngIfs = text.matchAll(/\*ngIf\s*=\s*["'](?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)/g);
|
|
137
|
-
for (const m of ngIfs) add(m[1]);
|
|
138
|
-
}
|
|
139
|
-
if (/\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(filePath)) {
|
|
140
|
-
const methodDecl = text.matchAll(
|
|
141
|
-
/(?:^|\n)\s*(?:public|private|protected)?\s*(?:async\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)\s*\{/g
|
|
142
|
-
);
|
|
143
|
-
for (const m of methodDecl) methodNames.add(m[1]);
|
|
144
|
-
|
|
145
|
-
const thisRefs = text.matchAll(/\bthis\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g);
|
|
146
|
-
for (const m of thisRefs) add(m[1]);
|
|
147
|
-
|
|
148
|
-
const classProps = text.matchAll(
|
|
149
|
-
/(?:^|\n)\s*(?:public|private|protected)?\s*(?:readonly\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*(?::|=)/g
|
|
150
|
-
);
|
|
151
|
-
for (const m of classProps) {
|
|
152
|
-
add(m[1]);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const inputProps = text.matchAll(/@Input\([^)]*\)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*[:=]/g);
|
|
156
|
-
for (const m of inputProps) add(m[1]);
|
|
157
|
-
|
|
158
|
-
const forEachParams = text.matchAll(/forEach\(\((\w+)\)\s*=>/g);
|
|
159
|
-
for (const m of forEachParams) {
|
|
160
|
-
const item = m[1];
|
|
161
|
-
const propAccess = new RegExp(`\\b${item}\\.([a-zA-Z_][a-zA-Z0-9_]*)\\b`, "g");
|
|
162
|
-
for (const p of text.matchAll(propAccess)) add(p[1]);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return Array.from(fields)
|
|
167
|
-
.filter((name) => !methodNames.has(name))
|
|
168
|
-
.sort()
|
|
169
|
-
.slice(0, 80);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function detectExternalLibraries(cwd) {
|
|
173
|
-
const libs = new Set();
|
|
174
|
-
const pkgPath = path.join(cwd, "package.json");
|
|
175
|
-
if (!fs.existsSync(pkgPath)) return [];
|
|
176
|
-
try {
|
|
177
|
-
const pkg = JSON.parse(safeRead(pkgPath) || "{}");
|
|
178
|
-
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
179
|
-
for (const name of Object.keys(deps)) libs.add(name);
|
|
180
|
-
} catch {}
|
|
181
|
-
return Array.from(libs).sort();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function detectStyling(cwd, files, externalLibraries) {
|
|
185
|
-
const styleFiles = files
|
|
186
|
-
.filter((f) => /\.(css|scss|sass|less|styl)$/i.test(f))
|
|
187
|
-
.map((f) => path.relative(cwd, f))
|
|
188
|
-
.sort();
|
|
189
|
-
|
|
190
|
-
const frameworks = [];
|
|
191
|
-
const hasDep = (name) => externalLibraries.includes(name);
|
|
192
|
-
if (hasDep("tailwindcss")) frameworks.push("Tailwind CSS");
|
|
193
|
-
if (hasDep("bootstrap")) frameworks.push("Bootstrap");
|
|
194
|
-
if (externalLibraries.some((lib) => lib.startsWith("@angular/material"))) frameworks.push("Angular Material");
|
|
195
|
-
if (hasDep("antd")) frameworks.push("Ant Design");
|
|
196
|
-
if (hasDep("styled-components")) frameworks.push("styled-components");
|
|
197
|
-
if (hasDep("emotion") || hasDep("@emotion/react")) frameworks.push("Emotion");
|
|
198
|
-
|
|
199
|
-
const tokenVars = new Set();
|
|
200
|
-
for (const filePath of files) {
|
|
201
|
-
if (!/\.(css|scss|sass|less|styl|html|htm|ts|tsx|js|jsx|mjs|cjs)$/i.test(filePath)) continue;
|
|
202
|
-
const text = safeRead(filePath);
|
|
203
|
-
for (const m of text.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)/g)) tokenVars.add(`--${m[1]}`);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
cssFrameworks: frameworks,
|
|
208
|
-
styleFileCount: styleFiles.length,
|
|
209
|
-
styleFilesSample: styleFiles.slice(0, 12),
|
|
210
|
-
designTokens: Array.from(tokenVars).sort().slice(0, 24),
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function detectUiLayout(files) {
|
|
215
|
-
let usesGrid = false;
|
|
216
|
-
let usesFlex = false;
|
|
217
|
-
const sections = new Set();
|
|
218
|
-
|
|
219
|
-
for (const filePath of files) {
|
|
220
|
-
if (!/\.(html|htm|tsx|jsx|ts|js|mjs|cjs)$/i.test(filePath)) continue;
|
|
221
|
-
const text = safeRead(filePath);
|
|
222
|
-
|
|
223
|
-
if (/\bgrid\b|grid-template|grid-cols-|display:\s*grid/i.test(text)) usesGrid = true;
|
|
224
|
-
if (/\bflex\b|display:\s*flex|flex-row|flex-col|justify-|items-/i.test(text)) usesFlex = true;
|
|
225
|
-
|
|
226
|
-
for (const m of text.matchAll(/<(main|header|footer|section|aside|nav)\b/gi)) {
|
|
227
|
-
sections.add(m[1].toLowerCase());
|
|
228
|
-
}
|
|
229
|
-
for (const m of text.matchAll(/class(?:Name)?\s*=\s*["'`][^"'`]*(dashboard|chart|card|sidebar|content|toolbar|filter|panel|table)[^"'`]*["'`]/gi)) {
|
|
230
|
-
const hit = m[1].toLowerCase();
|
|
231
|
-
sections.add(hit === "filter" ? "filters" : hit);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const layoutType = usesGrid && usesFlex ? "mixed" : usesGrid ? "grid" : usesFlex ? "flex" : "unknown";
|
|
236
|
-
return {
|
|
237
|
-
layoutType,
|
|
238
|
-
usesGrid,
|
|
239
|
-
usesFlex,
|
|
240
|
-
sections: Array.from(sections).sort(),
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function detectDevelopmentProfile(cwd, files, externalLibraries, overrides = {}) {
|
|
245
|
-
const extCount = { ts: 0, js: 0, py: 0, java: 0, go: 0, rb: 0, rs: 0, cs: 0, php: 0 };
|
|
246
|
-
for (const filePath of files) {
|
|
247
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
248
|
-
if (ext === ".ts" || ext === ".tsx") extCount.ts += 1;
|
|
249
|
-
if (ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") extCount.js += 1;
|
|
250
|
-
if (ext === ".py") extCount.py += 1;
|
|
251
|
-
if (ext === ".java") extCount.java += 1;
|
|
252
|
-
if (ext === ".go") extCount.go += 1;
|
|
253
|
-
if (ext === ".rb") extCount.rb += 1;
|
|
254
|
-
if (ext === ".rs") extCount.rs += 1;
|
|
255
|
-
if (ext === ".cs") extCount.cs += 1;
|
|
256
|
-
if (ext === ".php") extCount.php += 1;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const sortedLang = Object.entries(extCount).sort((a, b) => b[1] - a[1]);
|
|
260
|
-
const autoLanguage = sortedLang[0]?.[1] > 0 ? sortedLang[0][0] : "unknown";
|
|
261
|
-
|
|
262
|
-
let autoFramework = "unknown";
|
|
263
|
-
let hasDotnetWebSdk = false;
|
|
264
|
-
let hasMinimalApi = false;
|
|
265
|
-
let hasBlazor = false;
|
|
266
|
-
for (const filePath of files) {
|
|
267
|
-
const base = path.basename(filePath).toLowerCase();
|
|
268
|
-
if (base.endsWith(".csproj")) {
|
|
269
|
-
const text = safeRead(filePath);
|
|
270
|
-
if (/Microsoft\.NET\.Sdk\.Web/i.test(text)) hasDotnetWebSdk = true;
|
|
271
|
-
if (/Blazor/i.test(text) || /Microsoft\.AspNetCore\.Components/i.test(text)) hasBlazor = true;
|
|
272
|
-
}
|
|
273
|
-
if (base === "program.cs") {
|
|
274
|
-
const text = safeRead(filePath);
|
|
275
|
-
if (/app\.Map(Get|Post|Put|Delete|Patch)\s*\(/i.test(text)) hasMinimalApi = true;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
const hasDep = (name) => externalLibraries.includes(name);
|
|
279
|
-
if (externalLibraries.some((d) => d.startsWith("@angular/"))) autoFramework = "angular";
|
|
280
|
-
else if (hasDep("react")) autoFramework = "react";
|
|
281
|
-
else if (hasDep("vue")) autoFramework = "vue";
|
|
282
|
-
else if (hasDep("svelte")) autoFramework = "svelte";
|
|
283
|
-
else if (hasDep("next")) autoFramework = "nextjs";
|
|
284
|
-
else if (hasDep("nuxt")) autoFramework = "nuxt";
|
|
285
|
-
else if (hasDep("express")) autoFramework = "express";
|
|
286
|
-
else if (hasDep("@nestjs/core")) autoFramework = "nestjs";
|
|
287
|
-
else if (hasDep("fastify")) autoFramework = "fastify";
|
|
288
|
-
else if (hasDep("flask")) autoFramework = "flask";
|
|
289
|
-
else if (hasDep("django")) autoFramework = "django";
|
|
290
|
-
else if (hasDep("spring-boot")) autoFramework = "spring";
|
|
291
|
-
else if (hasBlazor) autoFramework = "blazor";
|
|
292
|
-
else if (hasMinimalApi) autoFramework = "minimalapi";
|
|
293
|
-
else if (hasDotnetWebSdk || extCount.cs > 0) autoFramework = "aspnet";
|
|
294
|
-
|
|
295
|
-
let autoProjectType = "fullstack";
|
|
296
|
-
const hasClientRoots = ["src", "frontend", "app"].some((d) => fs.existsSync(path.join(cwd, d)));
|
|
297
|
-
const hasServerRoots = ["server", "backend", "api"].some((d) => fs.existsSync(path.join(cwd, d)));
|
|
298
|
-
if (["react", "angular", "vue", "svelte", "nextjs", "nuxt"].includes(autoFramework)) autoProjectType = "frontend";
|
|
299
|
-
if (["express", "nestjs", "fastify", "flask", "django", "spring", "aspnet", "minimalapi"].includes(autoFramework)) autoProjectType = "backend";
|
|
300
|
-
if (hasClientRoots && hasServerRoots) autoProjectType = "fullstack";
|
|
301
|
-
if (!hasClientRoots && !hasServerRoots) autoProjectType = "library";
|
|
302
|
-
if (autoFramework === "blazor") autoProjectType = "frontend";
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
language: overrides.language || autoLanguage,
|
|
306
|
-
framework: overrides.framework || autoFramework,
|
|
307
|
-
projectType: overrides.projectType || autoProjectType,
|
|
308
|
-
detected: {
|
|
309
|
-
language: autoLanguage,
|
|
310
|
-
framework: autoFramework,
|
|
311
|
-
projectType: autoProjectType,
|
|
312
|
-
},
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function detectApiCalls(cwd, files) {
|
|
317
|
-
const calls = [];
|
|
318
|
-
const seen = new Set();
|
|
319
|
-
const normalizeEndpointPattern = (value) => {
|
|
320
|
-
let out = String(value || "").trim();
|
|
321
|
-
if (!out) return "";
|
|
322
|
-
out = out.replace(/https?:\/\/[^/]+/gi, "");
|
|
323
|
-
out = out.replace(/\$\{[^}]+\}/g, "{var}");
|
|
324
|
-
out = out.replace(/\{[A-Za-z_][A-Za-z0-9_]*\}/g, "{var}");
|
|
325
|
-
out = out.replace(/:[A-Za-z_][A-Za-z0-9_]*/g, "{var}");
|
|
326
|
-
out = out.replace(/\/\d+(?=\/|$)/g, "/{id}");
|
|
327
|
-
out = out.replace(/=[^&\s]+/g, "={value}");
|
|
328
|
-
out = out.replace(/\/+/g, "/");
|
|
329
|
-
return out;
|
|
330
|
-
};
|
|
331
|
-
const addCall = (call) => {
|
|
332
|
-
const endpointPattern = normalizeEndpointPattern(call.endpointPattern);
|
|
333
|
-
if (!endpointPattern) return;
|
|
334
|
-
const key = `${call.method}|${endpointPattern}|${call.sourceFile}|${call.style}`;
|
|
335
|
-
if (seen.has(key)) return;
|
|
336
|
-
seen.add(key);
|
|
337
|
-
calls.push({ ...call, endpointPattern });
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
for (const filePath of files) {
|
|
341
|
-
if (!/\.(ts|tsx|js|jsx|mjs|cjs|cs)$/i.test(filePath)) continue;
|
|
342
|
-
const rel = path.relative(cwd, filePath);
|
|
343
|
-
const text = safeRead(filePath);
|
|
344
|
-
const looksLikeService = /service|api|client|controller|program\.cs/i.test(rel) || /HttpClient|fetch\(|app\.Map(Get|Post|Put|Delete|Patch)\(/i.test(text);
|
|
345
|
-
if (!looksLikeService) continue;
|
|
346
|
-
|
|
347
|
-
const normalized = text.replace(/\r\n/g, "\n");
|
|
348
|
-
|
|
349
|
-
const constStrings = {};
|
|
350
|
-
const normalizeExpr = (expr) => {
|
|
351
|
-
let out = String(expr || "").trim();
|
|
352
|
-
if (!out) return "";
|
|
353
|
-
out = out.replace(/;+$/, "").trim();
|
|
354
|
-
out = out.replace(/\(\s*$/, ""); // e.g. "this._nextPage("
|
|
355
|
-
// unwrap surrounding quotes/templates
|
|
356
|
-
while ((out.startsWith("'") && out.endsWith("'")) || (out.startsWith('"') && out.endsWith('"')) || (out.startsWith("`") && out.endsWith("`"))) {
|
|
357
|
-
out = out.slice(1, -1).trim();
|
|
358
|
-
}
|
|
359
|
-
return out;
|
|
360
|
-
};
|
|
361
|
-
const isLikelyEndpoint = (value) => {
|
|
362
|
-
const v = String(value || "").trim();
|
|
363
|
-
if (!v) return false;
|
|
364
|
-
if (/^https?:\/\//i.test(v)) return true;
|
|
365
|
-
if (v.startsWith("/")) return true;
|
|
366
|
-
if (/\bapi\b/i.test(v)) return true;
|
|
367
|
-
if (/\$\{[^}]+\}/.test(v)) return true;
|
|
368
|
-
if (/\?[^=\s]+=?/.test(v)) return true;
|
|
369
|
-
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_./${}-]+$/.test(v)) return true;
|
|
370
|
-
return false;
|
|
371
|
-
};
|
|
372
|
-
const storeConst = (name, raw) => {
|
|
373
|
-
if (!name || !raw) return;
|
|
374
|
-
constStrings[name] = normalizeExpr(raw);
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
const constPattern = /(?:const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*?);/g;
|
|
378
|
-
for (const m of normalized.matchAll(constPattern)) {
|
|
379
|
-
storeConst(m[1], m[2]);
|
|
380
|
-
}
|
|
381
|
-
const readonlyPattern =
|
|
382
|
-
/(?:public|private|protected)?\s*(?:readonly\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*?);/g;
|
|
383
|
-
for (const m of normalized.matchAll(readonlyPattern)) {
|
|
384
|
-
storeConst(m[1], m[2]);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const resolveExpr = (expr) => {
|
|
388
|
-
const trimmed = normalizeExpr(expr);
|
|
389
|
-
if (!trimmed) return "";
|
|
390
|
-
if (/^['"`][\s\S]*['"`]$/.test(trimmed)) {
|
|
391
|
-
return trimmed.replace(/^['"`]|['"`]$/g, "");
|
|
392
|
-
}
|
|
393
|
-
if (constStrings[trimmed] && isLikelyEndpoint(constStrings[trimmed])) return constStrings[trimmed];
|
|
394
|
-
const thisRef = trimmed.match(/^this\.([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
395
|
-
if (thisRef && constStrings[thisRef[1]] && isLikelyEndpoint(constStrings[thisRef[1]])) return constStrings[thisRef[1]];
|
|
396
|
-
const parts = trimmed.split("+").map((s) => s.trim()).filter(Boolean);
|
|
397
|
-
if (parts.length > 1) {
|
|
398
|
-
const rebuilt = parts
|
|
399
|
-
.map((p) => {
|
|
400
|
-
if (/^['"`][\s\S]*['"`]$/.test(p)) return p.replace(/^['"`]|['"`]$/g, "");
|
|
401
|
-
if (constStrings[p] && isLikelyEndpoint(constStrings[p])) return constStrings[p];
|
|
402
|
-
const thisP = p.match(/^this\.([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
403
|
-
if (thisP && constStrings[thisP[1]] && isLikelyEndpoint(constStrings[thisP[1]])) return constStrings[thisP[1]];
|
|
404
|
-
return `{${p}}`;
|
|
405
|
-
})
|
|
406
|
-
.join("");
|
|
407
|
-
if (rebuilt) return rebuilt;
|
|
408
|
-
}
|
|
409
|
-
const ternary = trimmed.match(/^(.+?)\?(.+?):(.+)$/s);
|
|
410
|
-
if (ternary) {
|
|
411
|
-
const left = resolveExpr(ternary[2]);
|
|
412
|
-
const right = resolveExpr(ternary[3]);
|
|
413
|
-
if (left || right) return `${left || "{optionA}"} | ${right || "{optionB}"}`;
|
|
414
|
-
}
|
|
415
|
-
return trimmed;
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
const httpClientPattern =
|
|
419
|
-
/\.\s*(get|post|put|patch|delete)\s*(?:<[\s\S]*?>)?\s*\(\s*([\s\S]*?)(?:,|\))/gi;
|
|
420
|
-
for (const m of normalized.matchAll(httpClientPattern)) {
|
|
421
|
-
const method = m[1].toUpperCase();
|
|
422
|
-
const raw = resolveExpr(m[2]);
|
|
423
|
-
if (!raw || !isLikelyEndpoint(raw)) continue;
|
|
424
|
-
addCall({
|
|
425
|
-
method,
|
|
426
|
-
endpointPattern: raw,
|
|
427
|
-
style: "httpClient",
|
|
428
|
-
sourceFile: rel,
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const fetchPattern = /\bfetch\s*\(\s*([\s\S]*?)(?:,|\))/gi;
|
|
433
|
-
for (const m of normalized.matchAll(fetchPattern)) {
|
|
434
|
-
const firstArg = resolveExpr(m[1]);
|
|
435
|
-
if (!firstArg || !isLikelyEndpoint(firstArg)) continue;
|
|
436
|
-
const fromIdx = m.index || 0;
|
|
437
|
-
const lookahead = normalized.slice(fromIdx, fromIdx + 260);
|
|
438
|
-
const methodMatch = /method\s*:\s*["'](GET|POST|PUT|PATCH|DELETE)["']/i.exec(lookahead);
|
|
439
|
-
const method = (methodMatch?.[1] || "GET").toUpperCase();
|
|
440
|
-
addCall({
|
|
441
|
-
method,
|
|
442
|
-
endpointPattern: firstArg,
|
|
443
|
-
style: "fetch",
|
|
444
|
-
sourceFile: rel,
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const axiosPattern = /\baxios\.(get|post|put|patch|delete)\s*\(\s*([\s\S]*?)(?:,|\))/gi;
|
|
449
|
-
for (const m of normalized.matchAll(axiosPattern)) {
|
|
450
|
-
const method = m[1].toUpperCase();
|
|
451
|
-
const endpoint = resolveExpr(m[2]);
|
|
452
|
-
if (!endpoint || !isLikelyEndpoint(endpoint)) continue;
|
|
453
|
-
addCall({
|
|
454
|
-
method,
|
|
455
|
-
endpointPattern: endpoint,
|
|
456
|
-
style: "axios",
|
|
457
|
-
sourceFile: rel,
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const axiosObjPattern = /\baxios\s*\(\s*\{([\s\S]*?)\}\s*\)/gi;
|
|
462
|
-
for (const m of normalized.matchAll(axiosObjPattern)) {
|
|
463
|
-
const body = m[1];
|
|
464
|
-
const methodMatch = /\bmethod\s*:\s*["']?(get|post|put|patch|delete)["']?/i.exec(body);
|
|
465
|
-
const urlMatch = /\burl\s*:\s*([^,\n]+)/i.exec(body);
|
|
466
|
-
const method = (methodMatch?.[1] || "get").toUpperCase();
|
|
467
|
-
const endpoint = resolveExpr(urlMatch?.[1] || "");
|
|
468
|
-
if (!endpoint || !isLikelyEndpoint(endpoint)) continue;
|
|
469
|
-
addCall({
|
|
470
|
-
method,
|
|
471
|
-
endpointPattern: endpoint,
|
|
472
|
-
style: "axios-config",
|
|
473
|
-
sourceFile: rel,
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const requestPattern = /\.\s*request\s*\(\s*["'](GET|POST|PUT|PATCH|DELETE)["']\s*,\s*([\s\S]*?)(?:,|\))/gi;
|
|
478
|
-
for (const m of normalized.matchAll(requestPattern)) {
|
|
479
|
-
const method = m[1].toUpperCase();
|
|
480
|
-
const endpoint = resolveExpr(m[2]);
|
|
481
|
-
if (!endpoint || !isLikelyEndpoint(endpoint)) continue;
|
|
482
|
-
addCall({
|
|
483
|
-
method,
|
|
484
|
-
endpointPattern: endpoint,
|
|
485
|
-
style: "request",
|
|
486
|
-
sourceFile: rel,
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (/\.cs$/i.test(filePath)) {
|
|
491
|
-
const mapPattern = /\bapp\.Map(Get|Post|Put|Delete|Patch)\s*\(\s*"([^"]+)"/gi;
|
|
492
|
-
for (const m of normalized.matchAll(mapPattern)) {
|
|
493
|
-
addCall({
|
|
494
|
-
method: m[1].toUpperCase(),
|
|
495
|
-
endpointPattern: m[2],
|
|
496
|
-
style: "csharp-map",
|
|
497
|
-
sourceFile: rel,
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const classRouteMatch = /\[Route\("([^"]+)"\)\][\s\S]*?class\s+\w+/i.exec(normalized);
|
|
502
|
-
const classRoute = classRouteMatch ? classRouteMatch[1] : "";
|
|
503
|
-
const attrPattern = /\[(HttpGet|HttpPost|HttpPut|HttpDelete|HttpPatch)(?:\("([^"]*)"\))?\]/gi;
|
|
504
|
-
for (const m of normalized.matchAll(attrPattern)) {
|
|
505
|
-
const method = m[1].replace("Http", "").toUpperCase();
|
|
506
|
-
const attrRoute = m[2] || "";
|
|
507
|
-
const endpoint = [classRoute, attrRoute].filter(Boolean).join("/").replace(/\/+/g, "/").replace(/\[controller\]/gi, "{controller}");
|
|
508
|
-
addCall({
|
|
509
|
-
method,
|
|
510
|
-
endpointPattern: endpoint || classRoute || "{controller-route}",
|
|
511
|
-
style: "csharp-controller",
|
|
512
|
-
sourceFile: rel,
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const httpClientPattern = /\b(GetAsync|PostAsync|PutAsync|DeleteAsync|SendAsync)\s*\(\s*"([^"]+)"/gi;
|
|
517
|
-
for (const m of normalized.matchAll(httpClientPattern)) {
|
|
518
|
-
const method = m[1].replace("Async", "").replace("Send", "SEND").toUpperCase();
|
|
519
|
-
addCall({
|
|
520
|
-
method,
|
|
521
|
-
endpointPattern: m[2],
|
|
522
|
-
style: "csharp-httpclient",
|
|
523
|
-
sourceFile: rel,
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const byMethod = calls.reduce((acc, c) => {
|
|
530
|
-
acc[c.method] = (acc[c.method] || 0) + 1;
|
|
531
|
-
return acc;
|
|
532
|
-
}, {});
|
|
533
|
-
|
|
534
|
-
return {
|
|
535
|
-
totalCalls: calls.length,
|
|
536
|
-
byMethod,
|
|
537
|
-
calls: calls.slice(0, 80),
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
export function discoverProjectSignals(cwd, profileOverrides = {}) {
|
|
542
|
-
const files = collectCodeFiles(cwd);
|
|
543
|
-
const inferred = new Map();
|
|
544
|
-
const addHit = (cap, filePath) => {
|
|
545
|
-
if (!inferred.has(cap.id)) {
|
|
546
|
-
inferred.set(cap.id, {
|
|
547
|
-
id: cap.id,
|
|
548
|
-
title: cap.title,
|
|
549
|
-
reason: "Detected from code signals",
|
|
550
|
-
sourceFiles: new Set(),
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
inferred.get(cap.id).sourceFiles.add(path.relative(cwd, filePath));
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
for (const filePath of files) {
|
|
557
|
-
const text = safeRead(filePath);
|
|
558
|
-
for (const h of HEURISTICS) {
|
|
559
|
-
if (h.regex.test(text)) {
|
|
560
|
-
addHit(h, filePath);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const pkgPath = path.join(cwd, "package.json");
|
|
566
|
-
if (fs.existsSync(pkgPath)) {
|
|
567
|
-
const pkg = JSON.parse(safeRead(pkgPath) || "{}");
|
|
568
|
-
const name = typeof pkg.name === "string" ? pkg.name : path.basename(cwd);
|
|
569
|
-
const idHint = toCapabilityId(name);
|
|
570
|
-
if (idHint && !inferred.size) {
|
|
571
|
-
inferred.set("ReadItems", { id: "ReadItems", title: "Read Items", reason: `Fallback default for ${name}`, sourceFiles: new Set() });
|
|
572
|
-
inferred.set("CreateItem", { id: "CreateItem", title: "Create Item", reason: `Fallback default for ${name}`, sourceFiles: new Set() });
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
if (!inferred.size) {
|
|
577
|
-
inferred.set("CreateItem", { id: "CreateItem", title: "Create Item", reason: "Fallback default", sourceFiles: new Set() });
|
|
578
|
-
inferred.set("ReadItems", { id: "ReadItems", title: "Read Items", reason: "Fallback default", sourceFiles: new Set() });
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const externalLibraries = detectExternalLibraries(cwd);
|
|
582
|
-
const devProfile = detectDevelopmentProfile(cwd, files, externalLibraries, profileOverrides);
|
|
583
|
-
|
|
584
|
-
// ── Framework-specific scanners ─────────────────────────────────────────
|
|
585
|
-
let frameworkCapabilities = [];
|
|
586
|
-
try {
|
|
587
|
-
if (devProfile.framework === "angular") {
|
|
588
|
-
const angularSignals = scanAngular(cwd, files);
|
|
589
|
-
for (const cap of angularSignals.capabilities) {
|
|
590
|
-
if (!inferred.has(cap.id)) {
|
|
591
|
-
inferred.set(cap.id, { ...cap, sourceFiles: new Set(cap.sourceFiles) });
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
} else if (devProfile.framework === "react" || devProfile.framework === "nextjs") {
|
|
595
|
-
const reactSignals = scanReact(cwd, files);
|
|
596
|
-
for (const cap of reactSignals.capabilities) {
|
|
597
|
-
if (!inferred.has(cap.id)) {
|
|
598
|
-
inferred.set(cap.id, { ...cap, sourceFiles: new Set(cap.sourceFiles) });
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
} catch {}
|
|
603
|
-
|
|
604
|
-
// ── CSS signals ─────────────────────────────────────────────────────────
|
|
605
|
-
let cssSignals = { designTokens: [], colorTokens: [], spacingTokens: [], componentClasses: [], themeVars: [] };
|
|
606
|
-
try { cssSignals = scanCSS(cwd, files); } catch {}
|
|
607
|
-
|
|
608
|
-
const capabilities = Array.from(inferred.values()).map((c) => ({
|
|
609
|
-
...c,
|
|
610
|
-
sourceFiles: Array.from(c.sourceFiles || []),
|
|
611
|
-
}));
|
|
612
|
-
return {
|
|
613
|
-
capabilities,
|
|
614
|
-
components: detectComponents(files, cwd),
|
|
615
|
-
displayFields: detectDisplayFields(files),
|
|
616
|
-
externalLibraries,
|
|
617
|
-
uiLayout: detectUiLayout(files),
|
|
618
|
-
styling: {
|
|
619
|
-
...detectStyling(cwd, files, externalLibraries),
|
|
620
|
-
// Merge in richer CSS scanner results
|
|
621
|
-
designTokens: cssSignals.designTokens.length > 0 ? cssSignals.designTokens : undefined,
|
|
622
|
-
colorTokens: cssSignals.colorTokens,
|
|
623
|
-
spacingTokens: cssSignals.spacingTokens,
|
|
624
|
-
componentClasses: cssSignals.componentClasses,
|
|
625
|
-
themeVars: cssSignals.themeVars,
|
|
626
|
-
},
|
|
627
|
-
developmentProfile: devProfile,
|
|
628
|
-
apiCalls: detectApiCalls(cwd, files),
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
export async function reviewCapabilitiesInteractive(capabilities, yes = false) {
|
|
633
|
-
if (yes) return capabilities;
|
|
634
|
-
|
|
635
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
636
|
-
const list = capabilities.map((c) => c.id).join(", ");
|
|
637
|
-
const answer = await new Promise((resolve) =>
|
|
638
|
-
rl.question(` Inferred capabilities (${list}). Press Enter to accept or type comma list: `, resolve)
|
|
639
|
-
);
|
|
640
|
-
rl.close();
|
|
641
|
-
const trimmed = String(answer).trim();
|
|
642
|
-
if (!trimmed) return capabilities;
|
|
643
|
-
return trimmed
|
|
644
|
-
.split(",")
|
|
645
|
-
.map((s) => s.trim())
|
|
646
|
-
.filter(Boolean)
|
|
647
|
-
.map((id) => ({ id, title: capTitle(id), reason: "User provided during adopt review" }));
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
export function buildAdoptionReport(capabilities) {
|
|
651
|
-
if (!capabilities.length) return "No capabilities inferred.";
|
|
652
|
-
const summarized = summarizeCapabilities(capabilities);
|
|
653
|
-
const totalSignals = summarized.reduce((acc, c) => acc + c.signalCount, 0);
|
|
654
|
-
const byConfidence = {
|
|
655
|
-
high: summarized.filter((c) => c.confidence === "high").length,
|
|
656
|
-
medium: summarized.filter((c) => c.confidence === "medium").length,
|
|
657
|
-
low: summarized.filter((c) => c.confidence === "low").length,
|
|
658
|
-
};
|
|
659
|
-
|
|
660
|
-
const lines = [];
|
|
661
|
-
lines.push("Adoption Analysis");
|
|
662
|
-
lines.push("=".repeat(56));
|
|
663
|
-
lines.push(`Capabilities detected : ${summarized.length}`);
|
|
664
|
-
lines.push(`Signal hits total : ${totalSignals}`);
|
|
665
|
-
lines.push(
|
|
666
|
-
`Confidence mix : high=${byConfidence.high}, medium=${byConfidence.medium}, low=${byConfidence.low}`
|
|
667
|
-
);
|
|
668
|
-
lines.push("-".repeat(56));
|
|
669
|
-
lines.push("Capability Breakdown");
|
|
670
|
-
lines.push("-".repeat(56));
|
|
671
|
-
lines.push("Confidence Signals Capability");
|
|
672
|
-
lines.push("-".repeat(56));
|
|
673
|
-
for (const c of summarized) {
|
|
674
|
-
const confidence = c.confidence.toUpperCase().padEnd(10, " ");
|
|
675
|
-
const signals = String(c.signalCount).padEnd(7, " ");
|
|
676
|
-
lines.push(`${confidence} ${signals} ${c.id} (${c.title})`);
|
|
677
|
-
if (c.signalCount > 0) {
|
|
678
|
-
const sample = c.sourceFiles.slice(0, 3).join(", ");
|
|
679
|
-
lines.push(` sources: ${sample}`);
|
|
680
|
-
if (c.sourceFiles.length > 3) {
|
|
681
|
-
lines.push(` more : +${c.sourceFiles.length - 3} additional files`);
|
|
682
|
-
}
|
|
683
|
-
} else {
|
|
684
|
-
lines.push(" sources: inferred fallback (no strong code signal)");
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
lines.push("=".repeat(56));
|
|
688
|
-
return lines.join("\n");
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
export function buildSignalsReport(signals) {
|
|
692
|
-
const formatList = (title, items, limit = 10) => {
|
|
693
|
-
const lines = [`${title} (${items.length})`];
|
|
694
|
-
lines.push("-".repeat(56));
|
|
695
|
-
if (!items.length) {
|
|
696
|
-
lines.push(" - none");
|
|
697
|
-
return lines.join("\n");
|
|
698
|
-
}
|
|
699
|
-
for (const item of items.slice(0, limit)) {
|
|
700
|
-
lines.push(` - ${item}`);
|
|
701
|
-
}
|
|
702
|
-
if (items.length > limit) {
|
|
703
|
-
lines.push(` - ... +${items.length - limit} more`);
|
|
704
|
-
}
|
|
705
|
-
return lines.join("\n");
|
|
706
|
-
};
|
|
707
|
-
|
|
708
|
-
return [
|
|
709
|
-
"Project Structure Signals",
|
|
710
|
-
"=".repeat(56),
|
|
711
|
-
formatList("Components", signals.components || []),
|
|
712
|
-
formatList("Display fields", signals.displayFields || []),
|
|
713
|
-
formatList("External libraries", signals.externalLibraries || []),
|
|
714
|
-
"UI layout",
|
|
715
|
-
"-".repeat(56),
|
|
716
|
-
` - layout type: ${signals.uiLayout?.layoutType || "unknown"}`,
|
|
717
|
-
` - uses grid : ${signals.uiLayout?.usesGrid ? "yes" : "no"}`,
|
|
718
|
-
` - uses flex : ${signals.uiLayout?.usesFlex ? "yes" : "no"}`,
|
|
719
|
-
` - sections : ${(signals.uiLayout?.sections || []).slice(0, 10).join(", ") || "none"}`,
|
|
720
|
-
"Styling",
|
|
721
|
-
"-".repeat(56),
|
|
722
|
-
` - frameworks : ${(signals.styling?.cssFrameworks || []).join(", ") || "none detected"}`,
|
|
723
|
-
` - style files: ${signals.styling?.styleFileCount ?? 0}`,
|
|
724
|
-
` - tokens : ${(signals.styling?.designTokens || []).slice(0, 8).join(", ") || "none detected"}`,
|
|
725
|
-
"Development profile",
|
|
726
|
-
"-".repeat(56),
|
|
727
|
-
` - language : ${signals.developmentProfile?.language || "unknown"} (auto: ${signals.developmentProfile?.detected?.language || "unknown"})`,
|
|
728
|
-
` - framework : ${signals.developmentProfile?.framework || "unknown"} (auto: ${signals.developmentProfile?.detected?.framework || "unknown"})`,
|
|
729
|
-
` - project type: ${signals.developmentProfile?.projectType || "unknown"} (auto: ${signals.developmentProfile?.detected?.projectType || "unknown"})`,
|
|
730
|
-
"API calls",
|
|
731
|
-
"-".repeat(56),
|
|
732
|
-
` - total calls : ${signals.apiCalls?.totalCalls ?? 0}`,
|
|
733
|
-
` - by method : ${Object.entries(signals.apiCalls?.byMethod || {}).map(([k, v]) => `${k}:${v}`).join(", ") || "none"}`,
|
|
734
|
-
...(signals.apiCalls?.calls || []).slice(0, 6).map((c) => ` - ${c.method} ${c.endpointPattern} [${c.style}] (${c.sourceFile})`),
|
|
735
|
-
...((signals.apiCalls?.calls || []).length > 6
|
|
736
|
-
? [` - ... +${(signals.apiCalls?.calls || []).length - 6} more`]
|
|
737
|
-
: []),
|
|
738
|
-
"=".repeat(56),
|
|
739
|
-
].join("\n");
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
export function summarizeCapabilities(capabilities) {
|
|
743
|
-
return capabilities.map((c) => {
|
|
744
|
-
const hits = c.sourceFiles?.length || 0;
|
|
745
|
-
const confidence = hits >= 3 ? "high" : hits >= 1 ? "medium" : "low";
|
|
746
|
-
return {
|
|
747
|
-
id: c.id,
|
|
748
|
-
title: c.title,
|
|
749
|
-
reason: c.reason,
|
|
750
|
-
confidence,
|
|
751
|
-
sourceFiles: c.sourceFiles || [],
|
|
752
|
-
signalCount: hits,
|
|
753
|
-
};
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* Build the ui section of contract.json from adoption signals.
|
|
759
|
-
*/
|
|
760
|
-
export function buildUiContractSection(signals) {
|
|
761
|
-
if (!signals) return null;
|
|
762
|
-
const components = [
|
|
763
|
-
...(signals.components || []),
|
|
764
|
-
].filter(Boolean).slice(0, 40);
|
|
765
|
-
|
|
766
|
-
const designTokens = (signals.styling?.designTokens || []).slice(0, 20);
|
|
767
|
-
const layout = signals.uiLayout?.layoutType || "unknown";
|
|
768
|
-
const sections = (signals.uiLayout?.sections || []).slice(0, 12);
|
|
769
|
-
const cssFrameworks = signals.styling?.cssFrameworks || [];
|
|
770
|
-
const colorTokens = (signals.styling?.colorTokens || []).slice(0, 10);
|
|
771
|
-
const themeVars = (signals.styling?.themeVars || []).slice(0, 10);
|
|
772
|
-
|
|
773
|
-
return {
|
|
774
|
-
components,
|
|
775
|
-
designTokens,
|
|
776
|
-
colorTokens,
|
|
777
|
-
themeVars,
|
|
778
|
-
cssFrameworks,
|
|
779
|
-
layout,
|
|
780
|
-
sections,
|
|
781
|
-
lastScanned: new Date().toISOString(),
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
/**
|
|
786
|
-
* Merge a fresh ui scan result into an existing contract's ui section.
|
|
787
|
-
* Preserves manually added entries, updates detected ones.
|
|
788
|
-
*/
|
|
789
|
-
export function mergeUiSection(existing, fresh) {
|
|
790
|
-
if (!fresh) return existing;
|
|
791
|
-
if (!existing) return fresh;
|
|
792
|
-
return {
|
|
793
|
-
...fresh,
|
|
794
|
-
// Keep any manually added components not in the fresh scan
|
|
795
|
-
components: [...new Set([...(fresh.components || []), ...(existing.components || [])])].slice(0, 50),
|
|
796
|
-
designTokens: [...new Set([...(fresh.designTokens || []), ...(existing.designTokens || [])])].slice(0, 30),
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
export function writeAdoptionBaseline(infernoDir, policyId, capabilities, signals = null) {
|
|
801
|
-
const capIds = capabilities.map((c) => c.id);
|
|
802
|
-
const uiSection = buildUiContractSection(signals);
|
|
803
|
-
|
|
804
|
-
const contract = {
|
|
805
|
-
policyId,
|
|
806
|
-
policyVersion: 1,
|
|
807
|
-
capabilities: capIds,
|
|
808
|
-
rules: {
|
|
809
|
-
docsRequiredOnCapabilityChange: true,
|
|
810
|
-
requireScenarioForEachCapability: true,
|
|
811
|
-
requireChangelogOnCapabilityChange: true,
|
|
812
|
-
},
|
|
813
|
-
...(uiSection ? { ui: uiSection } : {}),
|
|
814
|
-
};
|
|
815
|
-
fs.mkdirSync(path.join(infernoDir, "scenarios"), { recursive: true });
|
|
816
|
-
fs.writeFileSync(path.join(infernoDir, "contract.json"), JSON.stringify(contract, null, 2) + "\n");
|
|
817
|
-
|
|
818
|
-
const registry = {
|
|
819
|
-
schemaVersion: 1,
|
|
820
|
-
capabilities: capabilities.map((c) => ({ id: c.id, title: c.title || capTitle(c.id), since: "0.1.0" })),
|
|
821
|
-
};
|
|
822
|
-
fs.writeFileSync(path.join(infernoDir, "capabilities.json"), JSON.stringify(registry, null, 2) + "\n");
|
|
823
|
-
|
|
824
|
-
const scenario = {
|
|
825
|
-
scenarioId: "adoption_baseline",
|
|
826
|
-
description: "Baseline inferred from existing codebase during adoption",
|
|
827
|
-
capabilitiesCovered: capIds,
|
|
828
|
-
steps: capIds.map((id) => ({ action: id, expect: `${id} behavior exists in the current project` })),
|
|
829
|
-
};
|
|
830
|
-
fs.writeFileSync(path.join(infernoDir, "scenarios", "adoption_baseline.json"), JSON.stringify(scenario, null, 2) + "\n");
|
|
831
|
-
|
|
832
|
-
if (signals) {
|
|
833
|
-
const profile = {
|
|
834
|
-
profileId: "adoption_profile",
|
|
835
|
-
generatedAt: new Date().toISOString(),
|
|
836
|
-
components: signals.components || [],
|
|
837
|
-
displayFields: signals.displayFields || [],
|
|
838
|
-
externalLibraries: signals.externalLibraries || [],
|
|
839
|
-
uiLayout: signals.uiLayout || { layoutType: "unknown", usesGrid: false, usesFlex: false, sections: [] },
|
|
840
|
-
styling: signals.styling || { cssFrameworks: [], styleFileCount: 0, styleFilesSample: [], designTokens: [] },
|
|
841
|
-
developmentProfile: signals.developmentProfile || {
|
|
842
|
-
language: "unknown",
|
|
843
|
-
framework: "unknown",
|
|
844
|
-
projectType: "unknown",
|
|
845
|
-
detected: { language: "unknown", framework: "unknown", projectType: "unknown" },
|
|
846
|
-
},
|
|
847
|
-
apiCalls: signals.apiCalls || { totalCalls: 0, byMethod: {}, calls: [] },
|
|
848
|
-
};
|
|
849
|
-
fs.writeFileSync(path.join(infernoDir, "adoption_profile.json"), JSON.stringify(profile, null, 2) + "\n");
|
|
850
|
-
|
|
851
|
-
// Seed developer-profile.json from adoption signals
|
|
852
|
-
try {
|
|
853
|
-
seedProfileFromAdoption(infernoDir, signals, capabilities);
|
|
854
|
-
} catch {}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
const changelog = `# Changelog — ${policyId}
|
|
858
|
-
|
|
859
|
-
## Unreleased
|
|
860
|
-
|
|
861
|
-
- Adopted infernoflow into an existing project and generated baseline capabilities.
|
|
862
|
-
- Captured detected components, display fields, and external libraries in adoption profile.
|
|
863
|
-
|
|
864
|
-
## 0.1.0 — Adoption baseline
|
|
865
|
-
|
|
866
|
-
- Initial baseline generated by infernoflow init --adopt
|
|
867
|
-
`;
|
|
868
|
-
fs.writeFileSync(path.join(infernoDir, "CHANGELOG.md"), changelog, "utf8");
|
|
869
|
-
}
|
|
1
|
+
import*as j from"node:fs";import*as A from"node:path";import*as D from"node:readline";import{seedProfileFromAdoption as R}from"../learning/profile.mjs";import{scanAngular as U}from"../adopters/angular.mjs";import{scanReact as M}from"../adopters/react.mjs";import{scanCSS as O}from"../adopters/css.mjs";function H(e){return e.replace(/[^a-zA-Z0-9]+/g," ").trim().split(/\s+/).filter(Boolean).map(r=>r[0].toUpperCase()+r.slice(1)).join("")}function _(e){return e.replace(/([A-Z])/g," $1").trim()}function k(e){try{return j.readFileSync(e,"utf8")}catch{return""}}const W=[{id:"CreateItem",title:"Create Item",regex:/\b(post|create|add)\b/i},{id:"ReadItems",title:"Read Items",regex:/\b(get|read|list|fetch)\b/i},{id:"UpdateItem",title:"Update Item",regex:/\b(put|patch|update|edit)\b/i},{id:"DeleteItem",title:"Delete Item",regex:/\b(delete|remove)\b/i},{id:"SearchItems",title:"Search Items",regex:/\bsearch\b/i},{id:"FilterItems",title:"Filter Items",regex:/\bfilter\b/i},{id:"SetDueDate",title:"Set Due Date",regex:/\bdueDate|deadline|due\b/i},{id:"SetPriority",title:"Set Priority",regex:/\bpriority\b/i},{id:"ToggleComplete",title:"Toggle Complete",regex:/\bcomplete|completed|toggle\b/i},{id:"ClearCompleted",title:"Clear Completed",regex:/\bclearCompleted|clear completed\b/i}];function ie(e){return X(e).capabilities}function G(e){const r=[],i=["src","server","app","backend","frontend","api","Controllers"];for(const o of i){const n=A.join(e,o);if(!j.existsSync(n))continue;const t=[n];for(;t.length;){const p=t.pop();for(const s of j.readdirSync(p,{withFileTypes:!0})){const a=A.join(p,s.name);if(s.isDirectory()){if(new Set(["node_modules",".git","dist","build","out","www","tmp",".tmp","vendor","assets","public","static","coverage",".nyc_output",".angular",".vite",".cache",".parcel-cache",".next",".nuxt","__pycache__","e2e","test","tests","spec","__tests__","fixtures","mocks",".turbo","storybook-static"]).has(s.name))continue;t.push(a)}else if(/\.(js|jsx|ts|tsx|mjs|cjs|json|md|html|htm|cs|csproj)$/.test(s.name)){if(/\.(min|bundle)\.(js|css)$/.test(s.name)||/\.map$/.test(s.name)||/\.(spec|test)\.(ts|js|tsx|jsx)$/.test(s.name))continue;r.push(a)}}}}for(const o of j.readdirSync(e,{withFileTypes:!0}))o.isFile()&&/^(Program\.cs|.+\.csproj)$/i.test(o.name)&&r.push(A.join(e,o.name));return r}function N(e,r){const i=new Set;for(const o of e){const n=A.relative(r,o),t=k(o),p=t.matchAll(/\bclass\s+([A-Z][A-Za-z0-9_]*?(?:Component|Page|View|Widget|Card))\b/g);for(const m of p)i.add(m[1]);const s=t.matchAll(/\bselector\s*:\s*["']([^"']+)["']/g);for(const m of s)i.add(m[1]);const a=t.matchAll(/\bfunction\s+([A-Z][A-Za-z0-9_]*)\s*\(/g);for(const m of a)/component|page|view|card|chart|dashboard/i.test(m[1])&&i.add(m[1]);const d=n.match(/([^/\\]+)\.(component|page|view|widget|card)\.(ts|tsx|js|jsx)$/i);d&&i.add(d[1])}return Array.from(i).sort()}function B(e){const r=new Set,i=new Set,o=new Set(["if","for","while","const","let","var","return","function","class","import","export","null","undefined","true","false","string","number","boolean","any","unknown","never","selector","templateUrl","styleUrl","standalone","imports","providers","providedIn","options","scales","responsive","display","title","type","label","component","service","routes","appConfig","ApplicationConfig"]),n=t=>{t&&/^[A-Za-z_][A-Za-z0-9_]*$/.test(t)&&(t.length<=1||o.has(t)||/^[A-Z0-9_]+$/.test(t)||r.add(t))};for(const t of e){const p=k(t);if(/\.(html|htm)$/i.test(t)){const s=p.matchAll(/\{\{\s*(?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)/g);for(const c of s)n(c[1]);const a=p.matchAll(/\[\(ngModel\)\]\s*=\s*["']([a-zA-Z_][a-zA-Z0-9_]*)["']/g);for(const c of a)n(c[1]);const d=p.matchAll(/\[[a-zA-Z0-9_-]+\]\s*=\s*["'](?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)["']/g);for(const c of d)n(c[1]);const m=p.matchAll(/\*ngIf\s*=\s*["'](?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)/g);for(const c of m)n(c[1])}if(/\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(t)){const s=p.matchAll(/(?:^|\n)\s*(?:public|private|protected)?\s*(?:async\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)\s*\{/g);for(const f of s)i.add(f[1]);const a=p.matchAll(/\bthis\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g);for(const f of a)n(f[1]);const d=p.matchAll(/(?:^|\n)\s*(?:public|private|protected)?\s*(?:readonly\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*(?::|=)/g);for(const f of d)n(f[1]);const m=p.matchAll(/@Input\([^)]*\)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*[:=]/g);for(const f of m)n(f[1]);const c=p.matchAll(/forEach\(\((\w+)\)\s*=>/g);for(const f of c){const C=f[1],S=new RegExp(`\\b${C}\\.([a-zA-Z_][a-zA-Z0-9_]*)\\b`,"g");for(const y of p.matchAll(S))n(y[1])}}}return Array.from(r).filter(t=>!i.has(t)).sort().slice(0,80)}function V(e){const r=new Set,i=A.join(e,"package.json");if(!j.existsSync(i))return[];try{const o=JSON.parse(k(i)||"{}"),n={...o.dependencies||{},...o.devDependencies||{}};for(const t of Object.keys(n))r.add(t)}catch{}return Array.from(r).sort()}function q(e,r,i){const o=r.filter(s=>/\.(css|scss|sass|less|styl)$/i.test(s)).map(s=>A.relative(e,s)).sort(),n=[],t=s=>i.includes(s);t("tailwindcss")&&n.push("Tailwind CSS"),t("bootstrap")&&n.push("Bootstrap"),i.some(s=>s.startsWith("@angular/material"))&&n.push("Angular Material"),t("antd")&&n.push("Ant Design"),t("styled-components")&&n.push("styled-components"),(t("emotion")||t("@emotion/react"))&&n.push("Emotion");const p=new Set;for(const s of r){if(!/\.(css|scss|sass|less|styl|html|htm|ts|tsx|js|jsx|mjs|cjs)$/i.test(s))continue;const a=k(s);for(const d of a.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)/g))p.add(`--${d[1]}`)}return{cssFrameworks:n,styleFileCount:o.length,styleFilesSample:o.slice(0,12),designTokens:Array.from(p).sort().slice(0,24)}}function J(e){let r=!1,i=!1;const o=new Set;for(const t of e){if(!/\.(html|htm|tsx|jsx|ts|js|mjs|cjs)$/i.test(t))continue;const p=k(t);/\bgrid\b|grid-template|grid-cols-|display:\s*grid/i.test(p)&&(r=!0),/\bflex\b|display:\s*flex|flex-row|flex-col|justify-|items-/i.test(p)&&(i=!0);for(const s of p.matchAll(/<(main|header|footer|section|aside|nav)\b/gi))o.add(s[1].toLowerCase());for(const s of p.matchAll(/class(?:Name)?\s*=\s*["'`][^"'`]*(dashboard|chart|card|sidebar|content|toolbar|filter|panel|table)[^"'`]*["'`]/gi)){const a=s[1].toLowerCase();o.add(a==="filter"?"filters":a)}}return{layoutType:r&&i?"mixed":r?"grid":i?"flex":"unknown",usesGrid:r,usesFlex:i,sections:Array.from(o).sort()}}function K(e,r,i,o={}){const n={ts:0,js:0,py:0,java:0,go:0,rb:0,rs:0,cs:0,php:0};for(const y of r){const b=A.extname(y).toLowerCase();(b===".ts"||b===".tsx")&&(n.ts+=1),(b===".js"||b===".jsx"||b===".mjs"||b===".cjs")&&(n.js+=1),b===".py"&&(n.py+=1),b===".java"&&(n.java+=1),b===".go"&&(n.go+=1),b===".rb"&&(n.rb+=1),b===".rs"&&(n.rs+=1),b===".cs"&&(n.cs+=1),b===".php"&&(n.php+=1)}const t=Object.entries(n).sort((y,b)=>b[1]-y[1]),p=t[0]?.[1]>0?t[0][0]:"unknown";let s="unknown",a=!1,d=!1,m=!1;for(const y of r){const b=A.basename(y).toLowerCase();if(b.endsWith(".csproj")){const P=k(y);/Microsoft\.NET\.Sdk\.Web/i.test(P)&&(a=!0),(/Blazor/i.test(P)||/Microsoft\.AspNetCore\.Components/i.test(P))&&(m=!0)}if(b==="program.cs"){const P=k(y);/app\.Map(Get|Post|Put|Delete|Patch)\s*\(/i.test(P)&&(d=!0)}}const c=y=>i.includes(y);i.some(y=>y.startsWith("@angular/"))?s="angular":c("react")?s="react":c("vue")?s="vue":c("svelte")?s="svelte":c("next")?s="nextjs":c("nuxt")?s="nuxt":c("express")?s="express":c("@nestjs/core")?s="nestjs":c("fastify")?s="fastify":c("flask")?s="flask":c("django")?s="django":c("spring-boot")?s="spring":m?s="blazor":d?s="minimalapi":(a||n.cs>0)&&(s="aspnet");let f="fullstack";const C=["src","frontend","app"].some(y=>j.existsSync(A.join(e,y))),S=["server","backend","api"].some(y=>j.existsSync(A.join(e,y)));return["react","angular","vue","svelte","nextjs","nuxt"].includes(s)&&(f="frontend"),["express","nestjs","fastify","flask","django","spring","aspnet","minimalapi"].includes(s)&&(f="backend"),C&&S&&(f="fullstack"),!C&&!S&&(f="library"),s==="blazor"&&(f="frontend"),{language:o.language||p,framework:o.framework||s,projectType:o.projectType||f,detected:{language:p,framework:s,projectType:f}}}function Q(e,r){const i=[],o=new Set,n=s=>{let a=String(s||"").trim();return a?(a=a.replace(/https?:\/\/[^/]+/gi,""),a=a.replace(/\$\{[^}]+\}/g,"{var}"),a=a.replace(/\{[A-Za-z_][A-Za-z0-9_]*\}/g,"{var}"),a=a.replace(/:[A-Za-z_][A-Za-z0-9_]*/g,"{var}"),a=a.replace(/\/\d+(?=\/|$)/g,"/{id}"),a=a.replace(/=[^&\s]+/g,"={value}"),a=a.replace(/\/+/g,"/"),a):""},t=s=>{const a=n(s.endpointPattern);if(!a)return;const d=`${s.method}|${a}|${s.sourceFile}|${s.style}`;o.has(d)||(o.add(d),i.push({...s,endpointPattern:a}))};for(const s of r){if(!/\.(ts|tsx|js|jsx|mjs|cjs|cs)$/i.test(s))continue;const a=A.relative(e,s),d=k(s);if(!(/service|api|client|controller|program\.cs/i.test(a)||/HttpClient|fetch\(|app\.Map(Get|Post|Put|Delete|Patch)\(/i.test(d)))continue;const c=d.replace(/\r\n/g,`
|
|
2
|
+
`),f={},C=u=>{let l=String(u||"").trim();if(!l)return"";for(l=l.replace(/;+$/,"").trim(),l=l.replace(/\(\s*$/,"");l.startsWith("'")&&l.endsWith("'")||l.startsWith('"')&&l.endsWith('"')||l.startsWith("`")&&l.endsWith("`");)l=l.slice(1,-1).trim();return l},S=u=>{const l=String(u||"").trim();return l?!!(/^https?:\/\//i.test(l)||l.startsWith("/")||/\bapi\b/i.test(l)||/\$\{[^}]+\}/.test(l)||/\?[^=\s]+=?/.test(l)||/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_./${}-]+$/.test(l)):!1},y=(u,l)=>{!u||!l||(f[u]=C(l))},b=/(?:const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*?);/g;for(const u of c.matchAll(b))y(u[1],u[2]);const P=/(?:public|private|protected)?\s*(?:readonly\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*?);/g;for(const u of c.matchAll(P))y(u[1],u[2]);const F=u=>{const l=C(u);if(!l)return"";if(/^['"`][\s\S]*['"`]$/.test(l))return l.replace(/^['"`]|['"`]$/g,"");if(f[l]&&S(f[l]))return f[l];const h=l.match(/^this\.([A-Za-z_][A-Za-z0-9_]*)$/);if(h&&f[h[1]]&&S(f[h[1]]))return f[h[1]];const w=l.split("+").map(g=>g.trim()).filter(Boolean);if(w.length>1){const g=w.map(x=>{if(/^['"`][\s\S]*['"`]$/.test(x))return x.replace(/^['"`]|['"`]$/g,"");if(f[x]&&S(f[x]))return f[x];const v=x.match(/^this\.([A-Za-z_][A-Za-z0-9_]*)$/);return v&&f[v[1]]&&S(f[v[1]])?f[v[1]]:`{${x}}`}).join("");if(g)return g}const $=l.match(/^(.+?)\?(.+?):(.+)$/s);if($){const g=F($[2]),x=F($[3]);if(g||x)return`${g||"{optionA}"} | ${x||"{optionB}"}`}return l},T=/\.\s*(get|post|put|patch|delete)\s*(?:<[\s\S]*?>)?\s*\(\s*([\s\S]*?)(?:,|\))/gi;for(const u of c.matchAll(T)){const l=u[1].toUpperCase(),h=F(u[2]);!h||!S(h)||t({method:l,endpointPattern:h,style:"httpClient",sourceFile:a})}const z=/\bfetch\s*\(\s*([\s\S]*?)(?:,|\))/gi;for(const u of c.matchAll(z)){const l=F(u[1]);if(!l||!S(l))continue;const h=u.index||0,w=c.slice(h,h+260),g=(/method\s*:\s*["'](GET|POST|PUT|PATCH|DELETE)["']/i.exec(w)?.[1]||"GET").toUpperCase();t({method:g,endpointPattern:l,style:"fetch",sourceFile:a})}const I=/\baxios\.(get|post|put|patch|delete)\s*\(\s*([\s\S]*?)(?:,|\))/gi;for(const u of c.matchAll(I)){const l=u[1].toUpperCase(),h=F(u[2]);!h||!S(h)||t({method:l,endpointPattern:h,style:"axios",sourceFile:a})}const Z=/\baxios\s*\(\s*\{([\s\S]*?)\}\s*\)/gi;for(const u of c.matchAll(Z)){const l=u[1],h=/\bmethod\s*:\s*["']?(get|post|put|patch|delete)["']?/i.exec(l),w=/\burl\s*:\s*([^,\n]+)/i.exec(l),$=(h?.[1]||"get").toUpperCase(),g=F(w?.[1]||"");!g||!S(g)||t({method:$,endpointPattern:g,style:"axios-config",sourceFile:a})}const E=/\.\s*request\s*\(\s*["'](GET|POST|PUT|PATCH|DELETE)["']\s*,\s*([\s\S]*?)(?:,|\))/gi;for(const u of c.matchAll(E)){const l=u[1].toUpperCase(),h=F(u[2]);!h||!S(h)||t({method:l,endpointPattern:h,style:"request",sourceFile:a})}if(/\.cs$/i.test(s)){const u=/\bapp\.Map(Get|Post|Put|Delete|Patch)\s*\(\s*"([^"]+)"/gi;for(const g of c.matchAll(u))t({method:g[1].toUpperCase(),endpointPattern:g[2],style:"csharp-map",sourceFile:a});const l=/\[Route\("([^"]+)"\)\][\s\S]*?class\s+\w+/i.exec(c),h=l?l[1]:"",w=/\[(HttpGet|HttpPost|HttpPut|HttpDelete|HttpPatch)(?:\("([^"]*)"\))?\]/gi;for(const g of c.matchAll(w)){const x=g[1].replace("Http","").toUpperCase(),v=g[2]||"",L=[h,v].filter(Boolean).join("/").replace(/\/+/g,"/").replace(/\[controller\]/gi,"{controller}");t({method:x,endpointPattern:L||h||"{controller-route}",style:"csharp-controller",sourceFile:a})}const $=/\b(GetAsync|PostAsync|PutAsync|DeleteAsync|SendAsync)\s*\(\s*"([^"]+)"/gi;for(const g of c.matchAll($)){const x=g[1].replace("Async","").replace("Send","SEND").toUpperCase();t({method:x,endpointPattern:g[2],style:"csharp-httpclient",sourceFile:a})}}}const p=i.reduce((s,a)=>(s[a.method]=(s[a.method]||0)+1,s),{});return{totalCalls:i.length,byMethod:p,calls:i.slice(0,80)}}function X(e,r={}){const i=G(e),o=new Map,n=(c,f)=>{o.has(c.id)||o.set(c.id,{id:c.id,title:c.title,reason:"Detected from code signals",sourceFiles:new Set}),o.get(c.id).sourceFiles.add(A.relative(e,f))};for(const c of i){const f=k(c);for(const C of W)C.regex.test(f)&&n(C,c)}const t=A.join(e,"package.json");if(j.existsSync(t)){const c=JSON.parse(k(t)||"{}"),f=typeof c.name=="string"?c.name:A.basename(e);H(f)&&!o.size&&(o.set("ReadItems",{id:"ReadItems",title:"Read Items",reason:`Fallback default for ${f}`,sourceFiles:new Set}),o.set("CreateItem",{id:"CreateItem",title:"Create Item",reason:`Fallback default for ${f}`,sourceFiles:new Set}))}o.size||(o.set("CreateItem",{id:"CreateItem",title:"Create Item",reason:"Fallback default",sourceFiles:new Set}),o.set("ReadItems",{id:"ReadItems",title:"Read Items",reason:"Fallback default",sourceFiles:new Set}));const p=V(e),s=K(e,i,p,r);let a=[];try{if(s.framework==="angular"){const c=U(e,i);for(const f of c.capabilities)o.has(f.id)||o.set(f.id,{...f,sourceFiles:new Set(f.sourceFiles)})}else if(s.framework==="react"||s.framework==="nextjs"){const c=M(e,i);for(const f of c.capabilities)o.has(f.id)||o.set(f.id,{...f,sourceFiles:new Set(f.sourceFiles)})}}catch{}let d={designTokens:[],colorTokens:[],spacingTokens:[],componentClasses:[],themeVars:[]};try{d=O(e,i)}catch{}return{capabilities:Array.from(o.values()).map(c=>({...c,sourceFiles:Array.from(c.sourceFiles||[])})),components:N(i,e),displayFields:B(i),externalLibraries:p,uiLayout:J(i),styling:{...q(e,i,p),designTokens:d.designTokens.length>0?d.designTokens:void 0,colorTokens:d.colorTokens,spacingTokens:d.spacingTokens,componentClasses:d.componentClasses,themeVars:d.themeVars},developmentProfile:s,apiCalls:Q(e,i)}}async function re(e,r=!1){if(r)return e;const i=D.createInterface({input:process.stdin,output:process.stdout}),o=e.map(p=>p.id).join(", "),n=await new Promise(p=>i.question(` Inferred capabilities (${o}). Press Enter to accept or type comma list: `,p));i.close();const t=String(n).trim();return t?t.split(",").map(p=>p.trim()).filter(Boolean).map(p=>({id:p,title:_(p),reason:"User provided during adopt review"})):e}function ae(e){if(!e.length)return"No capabilities inferred.";const r=Y(e),i=r.reduce((t,p)=>t+p.signalCount,0),o={high:r.filter(t=>t.confidence==="high").length,medium:r.filter(t=>t.confidence==="medium").length,low:r.filter(t=>t.confidence==="low").length},n=[];n.push("Adoption Analysis"),n.push("=".repeat(56)),n.push(`Capabilities detected : ${r.length}`),n.push(`Signal hits total : ${i}`),n.push(`Confidence mix : high=${o.high}, medium=${o.medium}, low=${o.low}`),n.push("-".repeat(56)),n.push("Capability Breakdown"),n.push("-".repeat(56)),n.push("Confidence Signals Capability"),n.push("-".repeat(56));for(const t of r){const p=t.confidence.toUpperCase().padEnd(10," "),s=String(t.signalCount).padEnd(7," ");if(n.push(`${p} ${s} ${t.id} (${t.title})`),t.signalCount>0){const a=t.sourceFiles.slice(0,3).join(", ");n.push(` sources: ${a}`),t.sourceFiles.length>3&&n.push(` more : +${t.sourceFiles.length-3} additional files`)}else n.push(" sources: inferred fallback (no strong code signal)")}return n.push("=".repeat(56)),n.join(`
|
|
3
|
+
`)}function ce(e){const r=(i,o,n=10)=>{const t=[`${i} (${o.length})`];if(t.push("-".repeat(56)),!o.length)return t.push(" - none"),t.join(`
|
|
4
|
+
`);for(const p of o.slice(0,n))t.push(` - ${p}`);return o.length>n&&t.push(` - ... +${o.length-n} more`),t.join(`
|
|
5
|
+
`)};return["Project Structure Signals","=".repeat(56),r("Components",e.components||[]),r("Display fields",e.displayFields||[]),r("External libraries",e.externalLibraries||[]),"UI layout","-".repeat(56),` - layout type: ${e.uiLayout?.layoutType||"unknown"}`,` - uses grid : ${e.uiLayout?.usesGrid?"yes":"no"}`,` - uses flex : ${e.uiLayout?.usesFlex?"yes":"no"}`,` - sections : ${(e.uiLayout?.sections||[]).slice(0,10).join(", ")||"none"}`,"Styling","-".repeat(56),` - frameworks : ${(e.styling?.cssFrameworks||[]).join(", ")||"none detected"}`,` - style files: ${e.styling?.styleFileCount??0}`,` - tokens : ${(e.styling?.designTokens||[]).slice(0,8).join(", ")||"none detected"}`,"Development profile","-".repeat(56),` - language : ${e.developmentProfile?.language||"unknown"} (auto: ${e.developmentProfile?.detected?.language||"unknown"})`,` - framework : ${e.developmentProfile?.framework||"unknown"} (auto: ${e.developmentProfile?.detected?.framework||"unknown"})`,` - project type: ${e.developmentProfile?.projectType||"unknown"} (auto: ${e.developmentProfile?.detected?.projectType||"unknown"})`,"API calls","-".repeat(56),` - total calls : ${e.apiCalls?.totalCalls??0}`,` - by method : ${Object.entries(e.apiCalls?.byMethod||{}).map(([i,o])=>`${i}:${o}`).join(", ")||"none"}`,...(e.apiCalls?.calls||[]).slice(0,6).map(i=>` - ${i.method} ${i.endpointPattern} [${i.style}] (${i.sourceFile})`),...(e.apiCalls?.calls||[]).length>6?[` - ... +${(e.apiCalls?.calls||[]).length-6} more`]:[],"=".repeat(56)].join(`
|
|
6
|
+
`)}function Y(e){return e.map(r=>{const i=r.sourceFiles?.length||0,o=i>=3?"high":i>=1?"medium":"low";return{id:r.id,title:r.title,reason:r.reason,confidence:o,sourceFiles:r.sourceFiles||[],signalCount:i}})}function ee(e){if(!e)return null;const r=[...e.components||[]].filter(Boolean).slice(0,40),i=(e.styling?.designTokens||[]).slice(0,20),o=e.uiLayout?.layoutType||"unknown",n=(e.uiLayout?.sections||[]).slice(0,12),t=e.styling?.cssFrameworks||[],p=(e.styling?.colorTokens||[]).slice(0,10),s=(e.styling?.themeVars||[]).slice(0,10);return{components:r,designTokens:i,colorTokens:p,themeVars:s,cssFrameworks:t,layout:o,sections:n,lastScanned:new Date().toISOString()}}function le(e,r){return r?e?{...r,components:[...new Set([...r.components||[],...e.components||[]])].slice(0,50),designTokens:[...new Set([...r.designTokens||[],...e.designTokens||[]])].slice(0,30)}:r:e}function pe(e,r,i,o=null){const n=i.map(m=>m.id),t=ee(o),p={policyId:r,policyVersion:1,capabilities:n,rules:{docsRequiredOnCapabilityChange:!0,requireScenarioForEachCapability:!0,requireChangelogOnCapabilityChange:!0},...t?{ui:t}:{}};j.mkdirSync(A.join(e,"scenarios"),{recursive:!0}),j.writeFileSync(A.join(e,"contract.json"),JSON.stringify(p,null,2)+`
|
|
7
|
+
`);const s={schemaVersion:1,capabilities:i.map(m=>({id:m.id,title:m.title||_(m.id),since:"0.1.0"}))};j.writeFileSync(A.join(e,"capabilities.json"),JSON.stringify(s,null,2)+`
|
|
8
|
+
`);const a={scenarioId:"adoption_baseline",description:"Baseline inferred from existing codebase during adoption",capabilitiesCovered:n,steps:n.map(m=>({action:m,expect:`${m} behavior exists in the current project`}))};if(j.writeFileSync(A.join(e,"scenarios","adoption_baseline.json"),JSON.stringify(a,null,2)+`
|
|
9
|
+
`),o){const m={profileId:"adoption_profile",generatedAt:new Date().toISOString(),components:o.components||[],displayFields:o.displayFields||[],externalLibraries:o.externalLibraries||[],uiLayout:o.uiLayout||{layoutType:"unknown",usesGrid:!1,usesFlex:!1,sections:[]},styling:o.styling||{cssFrameworks:[],styleFileCount:0,styleFilesSample:[],designTokens:[]},developmentProfile:o.developmentProfile||{language:"unknown",framework:"unknown",projectType:"unknown",detected:{language:"unknown",framework:"unknown",projectType:"unknown"}},apiCalls:o.apiCalls||{totalCalls:0,byMethod:{},calls:[]}};j.writeFileSync(A.join(e,"adoption_profile.json"),JSON.stringify(m,null,2)+`
|
|
10
|
+
`);try{R(e,o,i)}catch{}}const d=`# Changelog \u2014 ${r}
|
|
11
|
+
|
|
12
|
+
## Unreleased
|
|
13
|
+
|
|
14
|
+
- Adopted infernoflow into an existing project and generated baseline capabilities.
|
|
15
|
+
- Captured detected components, display fields, and external libraries in adoption profile.
|
|
16
|
+
|
|
17
|
+
## 0.1.0 \u2014 Adoption baseline
|
|
18
|
+
|
|
19
|
+
- Initial baseline generated by infernoflow init --adopt
|
|
20
|
+
`;j.writeFileSync(A.join(e,"CHANGELOG.md"),d,"utf8")}export{ae as buildAdoptionReport,ce as buildSignalsReport,ee as buildUiContractSection,ie as discoverCapabilities,X as discoverProjectSignals,le as mergeUiSection,re as reviewCapabilitiesInteractive,Y as summarizeCapabilities,pe as writeAdoptionBaseline};
|