infernoflow 0.10.3 → 0.10.5
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/README.md +16 -0
- package/bin/infernoflow.mjs +4 -0
- package/lib/commands/adopt.mjs +321 -7
- package/lib/commands/init.mjs +97 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,6 +56,12 @@ Non-interactive adoption:
|
|
|
56
56
|
infernoflow init --adopt --yes
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
Override detected stack during adoption:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
infernoflow init --adopt --lang ts --framework angular --project-type frontend
|
|
63
|
+
```
|
|
64
|
+
|
|
59
65
|
JSON report for CI/logging:
|
|
60
66
|
|
|
61
67
|
```bash
|
|
@@ -68,10 +74,18 @@ JSON-only output (clean machine output, no text logs):
|
|
|
68
74
|
infernoflow init --adopt --yes --report-json-only
|
|
69
75
|
```
|
|
70
76
|
|
|
77
|
+
Human-only output (visual report only, no JSON block):
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
infernoflow init --adopt --yes --report-human-only
|
|
81
|
+
```
|
|
82
|
+
|
|
71
83
|
What adoption creates:
|
|
72
84
|
- `inferno/contract.json` (inferred capability baseline)
|
|
73
85
|
- `inferno/capabilities.json` (inferred registry)
|
|
74
86
|
- `inferno/scenarios/adoption_baseline.json` (coverage baseline)
|
|
87
|
+
- `inferno/adoption_profile.json` (detected components, display fields, external libraries, UI layout, styling hints)
|
|
88
|
+
- `inferno/context-state.json` (saved development profile: language/framework/project type)
|
|
75
89
|
- `inferno/CHANGELOG.md` (adoption entry)
|
|
76
90
|
|
|
77
91
|
Safety:
|
|
@@ -155,6 +169,8 @@ infernoflow doc-gate --json
|
|
|
155
169
|
infernoflow init --force # overwrite existing files
|
|
156
170
|
infernoflow init --yes # skip prompts, use defaults
|
|
157
171
|
infernoflow init --adopt # infer baseline from existing project
|
|
172
|
+
infernoflow init --adopt --lang ts --framework react --project-type frontend
|
|
173
|
+
infernoflow init --adopt --report-human-only
|
|
158
174
|
infernoflow suggest "..." # describe what changed
|
|
159
175
|
infernoflow implement "..." --mode both
|
|
160
176
|
infernoflow implement "..." --mode cursor
|
package/bin/infernoflow.mjs
CHANGED
|
@@ -45,8 +45,12 @@ ${formatCommandsHelp()}
|
|
|
45
45
|
|
|
46
46
|
${bold("init options:")}
|
|
47
47
|
--adopt Infer capabilities from an existing codebase
|
|
48
|
+
--lang <name> Override detected language (e.g. ts, js, py)
|
|
49
|
+
--framework <name> Override detected framework (e.g. react, angular, express)
|
|
50
|
+
--project-type <t> Override project type (frontend|backend|fullstack|cli|library)
|
|
48
51
|
--report-json Print inferred adoption report as JSON
|
|
49
52
|
--report-json-only Print JSON report only (no human-readable logs)
|
|
53
|
+
--report-human-only Print only human-readable adoption report (no JSON block)
|
|
50
54
|
--yes, -y Skip prompts and accept inferred/default values
|
|
51
55
|
--force, -f Overwrite existing inferno/ files
|
|
52
56
|
|
package/lib/commands/adopt.mjs
CHANGED
|
@@ -38,6 +38,10 @@ const HEURISTICS = [
|
|
|
38
38
|
];
|
|
39
39
|
|
|
40
40
|
export function discoverCapabilities(cwd) {
|
|
41
|
+
return discoverProjectSignals(cwd).capabilities;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collectCodeFiles(cwd) {
|
|
41
45
|
const files = [];
|
|
42
46
|
const roots = ["src", "server", "app", "backend", "frontend", "api"];
|
|
43
47
|
for (const r of roots) {
|
|
@@ -51,13 +55,224 @@ export function discoverCapabilities(cwd) {
|
|
|
51
55
|
if (entry.isDirectory()) {
|
|
52
56
|
if (["node_modules", ".git", "dist", "build"].includes(entry.name)) continue;
|
|
53
57
|
stack.push(p);
|
|
54
|
-
} else if (/\.(js|jsx|ts|tsx|mjs|cjs|json|md)$/.test(entry.name)) {
|
|
58
|
+
} else if (/\.(js|jsx|ts|tsx|mjs|cjs|json|md|html|htm)$/.test(entry.name)) {
|
|
55
59
|
files.push(p);
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
}
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function detectComponents(files, cwd) {
|
|
68
|
+
const names = new Set();
|
|
69
|
+
for (const filePath of files) {
|
|
70
|
+
const rel = path.relative(cwd, filePath);
|
|
71
|
+
const text = safeRead(filePath);
|
|
72
|
+
const classMatches = text.matchAll(/\bclass\s+([A-Z][A-Za-z0-9_]*?(?:Component|Page|View|Widget|Card))\b/g);
|
|
73
|
+
for (const m of classMatches) names.add(m[1]);
|
|
74
|
+
const selectorMatches = text.matchAll(/\bselector\s*:\s*["']([^"']+)["']/g);
|
|
75
|
+
for (const m of selectorMatches) names.add(m[1]);
|
|
76
|
+
const reactFnMatches = text.matchAll(/\bfunction\s+([A-Z][A-Za-z0-9_]*)\s*\(/g);
|
|
77
|
+
for (const m of reactFnMatches) {
|
|
78
|
+
if (/component|page|view|card|chart|dashboard/i.test(m[1])) names.add(m[1]);
|
|
79
|
+
}
|
|
80
|
+
const relMatch = rel.match(/([^/\\]+)\.(component|page|view|widget|card)\.(ts|tsx|js|jsx)$/i);
|
|
81
|
+
if (relMatch) names.add(relMatch[1]);
|
|
82
|
+
}
|
|
83
|
+
return Array.from(names).sort();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function detectDisplayFields(files) {
|
|
87
|
+
const fields = new Set();
|
|
88
|
+
const methodNames = new Set();
|
|
89
|
+
const stopWords = new Set([
|
|
90
|
+
"if", "for", "while", "const", "let", "var", "return", "function", "class", "import", "export",
|
|
91
|
+
"null", "undefined", "true", "false", "string", "number", "boolean", "any", "unknown", "never",
|
|
92
|
+
"selector", "templateUrl", "styleUrl", "standalone", "imports", "providers", "providedIn",
|
|
93
|
+
"options", "scales", "responsive", "display", "title", "type", "label",
|
|
94
|
+
"component", "service", "routes", "appConfig", "ApplicationConfig",
|
|
95
|
+
]);
|
|
96
|
+
const add = (v) => {
|
|
97
|
+
if (!v) return;
|
|
98
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(v)) return;
|
|
99
|
+
if (v.length <= 1) return;
|
|
100
|
+
if (stopWords.has(v)) return;
|
|
101
|
+
if (/^[A-Z0-9_]+$/.test(v)) return;
|
|
102
|
+
fields.add(v);
|
|
103
|
+
};
|
|
104
|
+
for (const filePath of files) {
|
|
105
|
+
const text = safeRead(filePath);
|
|
106
|
+
if (/\.(html|htm)$/i.test(filePath)) {
|
|
107
|
+
const angularInterpolations = text.matchAll(/\{\{\s*(?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)/g);
|
|
108
|
+
for (const m of angularInterpolations) add(m[1]);
|
|
109
|
+
const ngModels = text.matchAll(/\[\(ngModel\)\]\s*=\s*["']([a-zA-Z_][a-zA-Z0-9_]*)["']/g);
|
|
110
|
+
for (const m of ngModels) add(m[1]);
|
|
111
|
+
const ngInputs = text.matchAll(/\[[a-zA-Z0-9_-]+\]\s*=\s*["'](?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)["']/g);
|
|
112
|
+
for (const m of ngInputs) add(m[1]);
|
|
113
|
+
const ngIfs = text.matchAll(/\*ngIf\s*=\s*["'](?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)/g);
|
|
114
|
+
for (const m of ngIfs) add(m[1]);
|
|
115
|
+
}
|
|
116
|
+
if (/\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(filePath)) {
|
|
117
|
+
const methodDecl = text.matchAll(
|
|
118
|
+
/(?:^|\n)\s*(?:public|private|protected)?\s*(?:async\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)\s*\{/g
|
|
119
|
+
);
|
|
120
|
+
for (const m of methodDecl) methodNames.add(m[1]);
|
|
121
|
+
|
|
122
|
+
const thisRefs = text.matchAll(/\bthis\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g);
|
|
123
|
+
for (const m of thisRefs) add(m[1]);
|
|
124
|
+
|
|
125
|
+
const classProps = text.matchAll(
|
|
126
|
+
/(?:^|\n)\s*(?:public|private|protected)?\s*(?:readonly\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*(?::|=)/g
|
|
127
|
+
);
|
|
128
|
+
for (const m of classProps) {
|
|
129
|
+
add(m[1]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const inputProps = text.matchAll(/@Input\([^)]*\)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*[:=]/g);
|
|
133
|
+
for (const m of inputProps) add(m[1]);
|
|
134
|
+
|
|
135
|
+
const forEachParams = text.matchAll(/forEach\(\((\w+)\)\s*=>/g);
|
|
136
|
+
for (const m of forEachParams) {
|
|
137
|
+
const item = m[1];
|
|
138
|
+
const propAccess = new RegExp(`\\b${item}\\.([a-zA-Z_][a-zA-Z0-9_]*)\\b`, "g");
|
|
139
|
+
for (const p of text.matchAll(propAccess)) add(p[1]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return Array.from(fields)
|
|
144
|
+
.filter((name) => !methodNames.has(name))
|
|
145
|
+
.sort()
|
|
146
|
+
.slice(0, 80);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function detectExternalLibraries(cwd) {
|
|
150
|
+
const libs = new Set();
|
|
151
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
152
|
+
if (!fs.existsSync(pkgPath)) return [];
|
|
153
|
+
try {
|
|
154
|
+
const pkg = JSON.parse(safeRead(pkgPath) || "{}");
|
|
155
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
156
|
+
for (const name of Object.keys(deps)) libs.add(name);
|
|
157
|
+
} catch {}
|
|
158
|
+
return Array.from(libs).sort();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function detectStyling(cwd, files, externalLibraries) {
|
|
162
|
+
const styleFiles = files
|
|
163
|
+
.filter((f) => /\.(css|scss|sass|less|styl)$/i.test(f))
|
|
164
|
+
.map((f) => path.relative(cwd, f))
|
|
165
|
+
.sort();
|
|
166
|
+
|
|
167
|
+
const frameworks = [];
|
|
168
|
+
const hasDep = (name) => externalLibraries.includes(name);
|
|
169
|
+
if (hasDep("tailwindcss")) frameworks.push("Tailwind CSS");
|
|
170
|
+
if (hasDep("bootstrap")) frameworks.push("Bootstrap");
|
|
171
|
+
if (externalLibraries.some((lib) => lib.startsWith("@angular/material"))) frameworks.push("Angular Material");
|
|
172
|
+
if (hasDep("antd")) frameworks.push("Ant Design");
|
|
173
|
+
if (hasDep("styled-components")) frameworks.push("styled-components");
|
|
174
|
+
if (hasDep("emotion") || hasDep("@emotion/react")) frameworks.push("Emotion");
|
|
175
|
+
|
|
176
|
+
const tokenVars = new Set();
|
|
177
|
+
for (const filePath of files) {
|
|
178
|
+
if (!/\.(css|scss|sass|less|styl|html|htm|ts|tsx|js|jsx|mjs|cjs)$/i.test(filePath)) continue;
|
|
179
|
+
const text = safeRead(filePath);
|
|
180
|
+
for (const m of text.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)/g)) tokenVars.add(`--${m[1]}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
cssFrameworks: frameworks,
|
|
185
|
+
styleFileCount: styleFiles.length,
|
|
186
|
+
styleFilesSample: styleFiles.slice(0, 12),
|
|
187
|
+
designTokens: Array.from(tokenVars).sort().slice(0, 24),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function detectUiLayout(files) {
|
|
192
|
+
let usesGrid = false;
|
|
193
|
+
let usesFlex = false;
|
|
194
|
+
const sections = new Set();
|
|
195
|
+
|
|
196
|
+
for (const filePath of files) {
|
|
197
|
+
if (!/\.(html|htm|tsx|jsx|ts|js|mjs|cjs)$/i.test(filePath)) continue;
|
|
198
|
+
const text = safeRead(filePath);
|
|
199
|
+
|
|
200
|
+
if (/\bgrid\b|grid-template|grid-cols-|display:\s*grid/i.test(text)) usesGrid = true;
|
|
201
|
+
if (/\bflex\b|display:\s*flex|flex-row|flex-col|justify-|items-/i.test(text)) usesFlex = true;
|
|
202
|
+
|
|
203
|
+
for (const m of text.matchAll(/<(main|header|footer|section|aside|nav)\b/gi)) {
|
|
204
|
+
sections.add(m[1].toLowerCase());
|
|
205
|
+
}
|
|
206
|
+
for (const m of text.matchAll(/class(?:Name)?\s*=\s*["'`][^"'`]*(dashboard|chart|card|sidebar|content|toolbar|filter|panel|table)[^"'`]*["'`]/gi)) {
|
|
207
|
+
const hit = m[1].toLowerCase();
|
|
208
|
+
sections.add(hit === "filter" ? "filters" : hit);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
60
211
|
|
|
212
|
+
const layoutType = usesGrid && usesFlex ? "mixed" : usesGrid ? "grid" : usesFlex ? "flex" : "unknown";
|
|
213
|
+
return {
|
|
214
|
+
layoutType,
|
|
215
|
+
usesGrid,
|
|
216
|
+
usesFlex,
|
|
217
|
+
sections: Array.from(sections).sort(),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function detectDevelopmentProfile(cwd, files, externalLibraries, overrides = {}) {
|
|
222
|
+
const extCount = { ts: 0, js: 0, py: 0, java: 0, go: 0, rb: 0, rs: 0, cs: 0, php: 0 };
|
|
223
|
+
for (const filePath of files) {
|
|
224
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
225
|
+
if (ext === ".ts" || ext === ".tsx") extCount.ts += 1;
|
|
226
|
+
if (ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") extCount.js += 1;
|
|
227
|
+
if (ext === ".py") extCount.py += 1;
|
|
228
|
+
if (ext === ".java") extCount.java += 1;
|
|
229
|
+
if (ext === ".go") extCount.go += 1;
|
|
230
|
+
if (ext === ".rb") extCount.rb += 1;
|
|
231
|
+
if (ext === ".rs") extCount.rs += 1;
|
|
232
|
+
if (ext === ".cs") extCount.cs += 1;
|
|
233
|
+
if (ext === ".php") extCount.php += 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const sortedLang = Object.entries(extCount).sort((a, b) => b[1] - a[1]);
|
|
237
|
+
const autoLanguage = sortedLang[0]?.[1] > 0 ? sortedLang[0][0] : "unknown";
|
|
238
|
+
|
|
239
|
+
let autoFramework = "unknown";
|
|
240
|
+
const hasDep = (name) => externalLibraries.includes(name);
|
|
241
|
+
if (externalLibraries.some((d) => d.startsWith("@angular/"))) autoFramework = "angular";
|
|
242
|
+
else if (hasDep("react")) autoFramework = "react";
|
|
243
|
+
else if (hasDep("vue")) autoFramework = "vue";
|
|
244
|
+
else if (hasDep("svelte")) autoFramework = "svelte";
|
|
245
|
+
else if (hasDep("next")) autoFramework = "nextjs";
|
|
246
|
+
else if (hasDep("nuxt")) autoFramework = "nuxt";
|
|
247
|
+
else if (hasDep("express")) autoFramework = "express";
|
|
248
|
+
else if (hasDep("@nestjs/core")) autoFramework = "nestjs";
|
|
249
|
+
else if (hasDep("fastify")) autoFramework = "fastify";
|
|
250
|
+
else if (hasDep("flask")) autoFramework = "flask";
|
|
251
|
+
else if (hasDep("django")) autoFramework = "django";
|
|
252
|
+
else if (hasDep("spring-boot")) autoFramework = "spring";
|
|
253
|
+
|
|
254
|
+
let autoProjectType = "fullstack";
|
|
255
|
+
const hasClientRoots = ["src", "frontend", "app"].some((d) => fs.existsSync(path.join(cwd, d)));
|
|
256
|
+
const hasServerRoots = ["server", "backend", "api"].some((d) => fs.existsSync(path.join(cwd, d)));
|
|
257
|
+
if (["react", "angular", "vue", "svelte", "nextjs", "nuxt"].includes(autoFramework)) autoProjectType = "frontend";
|
|
258
|
+
if (["express", "nestjs", "fastify", "flask", "django", "spring"].includes(autoFramework)) autoProjectType = "backend";
|
|
259
|
+
if (hasClientRoots && hasServerRoots) autoProjectType = "fullstack";
|
|
260
|
+
if (!hasClientRoots && !hasServerRoots) autoProjectType = "library";
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
language: overrides.language || autoLanguage,
|
|
264
|
+
framework: overrides.framework || autoFramework,
|
|
265
|
+
projectType: overrides.projectType || autoProjectType,
|
|
266
|
+
detected: {
|
|
267
|
+
language: autoLanguage,
|
|
268
|
+
framework: autoFramework,
|
|
269
|
+
projectType: autoProjectType,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function discoverProjectSignals(cwd, profileOverrides = {}) {
|
|
275
|
+
const files = collectCodeFiles(cwd);
|
|
61
276
|
const inferred = new Map();
|
|
62
277
|
const addHit = (cap, filePath) => {
|
|
63
278
|
if (!inferred.has(cap.id)) {
|
|
@@ -96,10 +311,20 @@ export function discoverCapabilities(cwd) {
|
|
|
96
311
|
inferred.set("ReadItems", { id: "ReadItems", title: "Read Items", reason: "Fallback default", sourceFiles: new Set() });
|
|
97
312
|
}
|
|
98
313
|
|
|
99
|
-
|
|
314
|
+
const capabilities = Array.from(inferred.values()).map((c) => ({
|
|
100
315
|
...c,
|
|
101
316
|
sourceFiles: Array.from(c.sourceFiles || []),
|
|
102
317
|
}));
|
|
318
|
+
const externalLibraries = detectExternalLibraries(cwd);
|
|
319
|
+
return {
|
|
320
|
+
capabilities,
|
|
321
|
+
components: detectComponents(files, cwd),
|
|
322
|
+
displayFields: detectDisplayFields(files),
|
|
323
|
+
externalLibraries,
|
|
324
|
+
uiLayout: detectUiLayout(files),
|
|
325
|
+
styling: detectStyling(cwd, files, externalLibraries),
|
|
326
|
+
developmentProfile: detectDevelopmentProfile(cwd, files, externalLibraries, profileOverrides),
|
|
327
|
+
};
|
|
103
328
|
}
|
|
104
329
|
|
|
105
330
|
export async function reviewCapabilitiesInteractive(capabilities, yes = false) {
|
|
@@ -122,19 +347,88 @@ export async function reviewCapabilitiesInteractive(capabilities, yes = false) {
|
|
|
122
347
|
|
|
123
348
|
export function buildAdoptionReport(capabilities) {
|
|
124
349
|
if (!capabilities.length) return "No capabilities inferred.";
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
350
|
+
const summarized = summarizeCapabilities(capabilities);
|
|
351
|
+
const totalSignals = summarized.reduce((acc, c) => acc + c.signalCount, 0);
|
|
352
|
+
const byConfidence = {
|
|
353
|
+
high: summarized.filter((c) => c.confidence === "high").length,
|
|
354
|
+
medium: summarized.filter((c) => c.confidence === "medium").length,
|
|
355
|
+
low: summarized.filter((c) => c.confidence === "low").length,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const lines = [];
|
|
359
|
+
lines.push("Adoption Analysis");
|
|
360
|
+
lines.push("=".repeat(56));
|
|
361
|
+
lines.push(`Capabilities detected : ${summarized.length}`);
|
|
362
|
+
lines.push(`Signal hits total : ${totalSignals}`);
|
|
363
|
+
lines.push(
|
|
364
|
+
`Confidence mix : high=${byConfidence.high}, medium=${byConfidence.medium}, low=${byConfidence.low}`
|
|
365
|
+
);
|
|
366
|
+
lines.push("-".repeat(56));
|
|
367
|
+
lines.push("Capability Breakdown");
|
|
368
|
+
lines.push("-".repeat(56));
|
|
369
|
+
lines.push("Confidence Signals Capability");
|
|
370
|
+
lines.push("-".repeat(56));
|
|
371
|
+
for (const c of summarized) {
|
|
372
|
+
const confidence = c.confidence.toUpperCase().padEnd(10, " ");
|
|
373
|
+
const signals = String(c.signalCount).padEnd(7, " ");
|
|
374
|
+
lines.push(`${confidence} ${signals} ${c.id} (${c.title})`);
|
|
128
375
|
if (c.signalCount > 0) {
|
|
129
376
|
const sample = c.sourceFiles.slice(0, 3).join(", ");
|
|
130
377
|
lines.push(` sources: ${sample}`);
|
|
378
|
+
if (c.sourceFiles.length > 3) {
|
|
379
|
+
lines.push(` more : +${c.sourceFiles.length - 3} additional files`);
|
|
380
|
+
}
|
|
131
381
|
} else {
|
|
132
|
-
lines.push(
|
|
382
|
+
lines.push(" sources: inferred fallback (no strong code signal)");
|
|
133
383
|
}
|
|
134
384
|
}
|
|
385
|
+
lines.push("=".repeat(56));
|
|
135
386
|
return lines.join("\n");
|
|
136
387
|
}
|
|
137
388
|
|
|
389
|
+
export function buildSignalsReport(signals) {
|
|
390
|
+
const formatList = (title, items, limit = 10) => {
|
|
391
|
+
const lines = [`${title} (${items.length})`];
|
|
392
|
+
lines.push("-".repeat(56));
|
|
393
|
+
if (!items.length) {
|
|
394
|
+
lines.push(" - none");
|
|
395
|
+
return lines.join("\n");
|
|
396
|
+
}
|
|
397
|
+
for (const item of items.slice(0, limit)) {
|
|
398
|
+
lines.push(` - ${item}`);
|
|
399
|
+
}
|
|
400
|
+
if (items.length > limit) {
|
|
401
|
+
lines.push(` - ... +${items.length - limit} more`);
|
|
402
|
+
}
|
|
403
|
+
return lines.join("\n");
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
return [
|
|
407
|
+
"Project Structure Signals",
|
|
408
|
+
"=".repeat(56),
|
|
409
|
+
formatList("Components", signals.components || []),
|
|
410
|
+
formatList("Display fields", signals.displayFields || []),
|
|
411
|
+
formatList("External libraries", signals.externalLibraries || []),
|
|
412
|
+
"UI layout",
|
|
413
|
+
"-".repeat(56),
|
|
414
|
+
` - layout type: ${signals.uiLayout?.layoutType || "unknown"}`,
|
|
415
|
+
` - uses grid : ${signals.uiLayout?.usesGrid ? "yes" : "no"}`,
|
|
416
|
+
` - uses flex : ${signals.uiLayout?.usesFlex ? "yes" : "no"}`,
|
|
417
|
+
` - sections : ${(signals.uiLayout?.sections || []).slice(0, 10).join(", ") || "none"}`,
|
|
418
|
+
"Styling",
|
|
419
|
+
"-".repeat(56),
|
|
420
|
+
` - frameworks : ${(signals.styling?.cssFrameworks || []).join(", ") || "none detected"}`,
|
|
421
|
+
` - style files: ${signals.styling?.styleFileCount ?? 0}`,
|
|
422
|
+
` - tokens : ${(signals.styling?.designTokens || []).slice(0, 8).join(", ") || "none detected"}`,
|
|
423
|
+
"Development profile",
|
|
424
|
+
"-".repeat(56),
|
|
425
|
+
` - language : ${signals.developmentProfile?.language || "unknown"} (auto: ${signals.developmentProfile?.detected?.language || "unknown"})`,
|
|
426
|
+
` - framework : ${signals.developmentProfile?.framework || "unknown"} (auto: ${signals.developmentProfile?.detected?.framework || "unknown"})`,
|
|
427
|
+
` - project type: ${signals.developmentProfile?.projectType || "unknown"} (auto: ${signals.developmentProfile?.detected?.projectType || "unknown"})`,
|
|
428
|
+
"=".repeat(56),
|
|
429
|
+
].join("\n");
|
|
430
|
+
}
|
|
431
|
+
|
|
138
432
|
export function summarizeCapabilities(capabilities) {
|
|
139
433
|
return capabilities.map((c) => {
|
|
140
434
|
const hits = c.sourceFiles?.length || 0;
|
|
@@ -150,7 +444,7 @@ export function summarizeCapabilities(capabilities) {
|
|
|
150
444
|
});
|
|
151
445
|
}
|
|
152
446
|
|
|
153
|
-
export function writeAdoptionBaseline(infernoDir, policyId, capabilities) {
|
|
447
|
+
export function writeAdoptionBaseline(infernoDir, policyId, capabilities, signals = null) {
|
|
154
448
|
const capIds = capabilities.map((c) => c.id);
|
|
155
449
|
const contract = {
|
|
156
450
|
policyId,
|
|
@@ -179,11 +473,31 @@ export function writeAdoptionBaseline(infernoDir, policyId, capabilities) {
|
|
|
179
473
|
};
|
|
180
474
|
fs.writeFileSync(path.join(infernoDir, "scenarios", "adoption_baseline.json"), JSON.stringify(scenario, null, 2) + "\n");
|
|
181
475
|
|
|
476
|
+
if (signals) {
|
|
477
|
+
const profile = {
|
|
478
|
+
profileId: "adoption_profile",
|
|
479
|
+
generatedAt: new Date().toISOString(),
|
|
480
|
+
components: signals.components || [],
|
|
481
|
+
displayFields: signals.displayFields || [],
|
|
482
|
+
externalLibraries: signals.externalLibraries || [],
|
|
483
|
+
uiLayout: signals.uiLayout || { layoutType: "unknown", usesGrid: false, usesFlex: false, sections: [] },
|
|
484
|
+
styling: signals.styling || { cssFrameworks: [], styleFileCount: 0, styleFilesSample: [], designTokens: [] },
|
|
485
|
+
developmentProfile: signals.developmentProfile || {
|
|
486
|
+
language: "unknown",
|
|
487
|
+
framework: "unknown",
|
|
488
|
+
projectType: "unknown",
|
|
489
|
+
detected: { language: "unknown", framework: "unknown", projectType: "unknown" },
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
fs.writeFileSync(path.join(infernoDir, "adoption_profile.json"), JSON.stringify(profile, null, 2) + "\n");
|
|
493
|
+
}
|
|
494
|
+
|
|
182
495
|
const changelog = `# Changelog — ${policyId}
|
|
183
496
|
|
|
184
497
|
## Unreleased
|
|
185
498
|
|
|
186
499
|
- Adopted infernoflow into an existing project and generated baseline capabilities.
|
|
500
|
+
- Captured detected components, display fields, and external libraries in adoption profile.
|
|
187
501
|
|
|
188
502
|
## 0.1.0 — Adoption baseline
|
|
189
503
|
|
package/lib/commands/init.mjs
CHANGED
|
@@ -3,7 +3,14 @@ import * as path from "node:path";
|
|
|
3
3
|
import * as readline from "node:readline";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { header, ok, warn, done, nextSteps, cyan, yellow, gray } from "../ui/output.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
discoverProjectSignals,
|
|
8
|
+
reviewCapabilitiesInteractive,
|
|
9
|
+
writeAdoptionBaseline,
|
|
10
|
+
buildAdoptionReport,
|
|
11
|
+
summarizeCapabilities,
|
|
12
|
+
buildSignalsReport,
|
|
13
|
+
} from "./adopt.mjs";
|
|
7
14
|
|
|
8
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
16
|
|
|
@@ -20,6 +27,14 @@ function ask(rl, question, defaultVal = "") {
|
|
|
20
27
|
});
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
function getArgValue(args, ...flags) {
|
|
31
|
+
for (const flag of flags) {
|
|
32
|
+
const i = args.indexOf(flag);
|
|
33
|
+
if (i !== -1 && args[i + 1] && !args[i + 1].startsWith("-")) return args[i + 1];
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
function copyFile(src, dst, force, silent = false) {
|
|
24
39
|
if (fs.existsSync(dst) && !force) {
|
|
25
40
|
if (!silent) warn("Skipped (exists): " + path.relative(process.cwd(), dst));
|
|
@@ -136,14 +151,24 @@ export async function initCommand(args) {
|
|
|
136
151
|
const adopt = args.includes("--adopt");
|
|
137
152
|
const reportJson = args.includes("--report-json");
|
|
138
153
|
const reportJsonOnly = args.includes("--report-json-only");
|
|
154
|
+
const reportHumanOnly = args.includes("--report-human-only");
|
|
155
|
+
const langOverride = getArgValue(args, "--lang");
|
|
156
|
+
const frameworkOverride = getArgValue(args, "--framework");
|
|
157
|
+
const projectTypeOverride = getArgValue(args, "--project-type");
|
|
158
|
+
const silent = reportJsonOnly;
|
|
159
|
+
|
|
160
|
+
if (reportJsonOnly && reportHumanOnly) {
|
|
161
|
+
console.error("Error: --report-json-only and --report-human-only cannot be used together.");
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
139
164
|
|
|
140
|
-
if (!
|
|
165
|
+
if (!silent) {
|
|
141
166
|
header("init");
|
|
142
167
|
}
|
|
143
168
|
|
|
144
169
|
const infernoDir = path.join(cwd, "inferno");
|
|
145
170
|
if (fs.existsSync(infernoDir) && !force) {
|
|
146
|
-
if (
|
|
171
|
+
if (silent) {
|
|
147
172
|
console.log(JSON.stringify({ ok: false, error: "inferno_exists", hint: "Use --force to overwrite" }, null, 2));
|
|
148
173
|
process.exit(1);
|
|
149
174
|
}
|
|
@@ -159,12 +184,39 @@ export async function initCommand(args) {
|
|
|
159
184
|
let capabilities = defaultCaps.split(",").map(c => c.trim());
|
|
160
185
|
|
|
161
186
|
if (adopt) {
|
|
162
|
-
const
|
|
187
|
+
const profileOverrides = {
|
|
188
|
+
language: langOverride || undefined,
|
|
189
|
+
framework: frameworkOverride || undefined,
|
|
190
|
+
projectType: projectTypeOverride || undefined,
|
|
191
|
+
};
|
|
192
|
+
let signals = discoverProjectSignals(cwd, profileOverrides);
|
|
193
|
+
if (!yes && !reportJsonOnly) {
|
|
194
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
195
|
+
const profile = signals.developmentProfile || {};
|
|
196
|
+
const detected = profile.detected || {};
|
|
197
|
+
console.log(gray(" Review inferred development stack (press Enter to accept detected values)\n"));
|
|
198
|
+
const language = await ask(rl, "Language", profile.language || detected.language || "unknown");
|
|
199
|
+
const framework = await ask(rl, "Framework", profile.framework || detected.framework || "unknown");
|
|
200
|
+
const projectType = await ask(rl, "Project type", profile.projectType || detected.projectType || "unknown");
|
|
201
|
+
rl.close();
|
|
202
|
+
signals = discoverProjectSignals(cwd, { language, framework, projectType });
|
|
203
|
+
}
|
|
204
|
+
const inferred = signals.capabilities;
|
|
163
205
|
const summarized = summarizeCapabilities(inferred);
|
|
164
206
|
if (reportJsonOnly) {
|
|
165
207
|
console.log(
|
|
166
208
|
JSON.stringify(
|
|
167
|
-
{
|
|
209
|
+
{
|
|
210
|
+
mode: "adopt",
|
|
211
|
+
policyId: detectedName,
|
|
212
|
+
inferredCapabilities: summarized,
|
|
213
|
+
components: signals.components,
|
|
214
|
+
displayFields: signals.displayFields,
|
|
215
|
+
externalLibraries: signals.externalLibraries,
|
|
216
|
+
uiLayout: signals.uiLayout,
|
|
217
|
+
styling: signals.styling,
|
|
218
|
+
developmentProfile: signals.developmentProfile,
|
|
219
|
+
},
|
|
168
220
|
null,
|
|
169
221
|
2
|
|
170
222
|
)
|
|
@@ -173,13 +225,21 @@ export async function initCommand(args) {
|
|
|
173
225
|
console.log();
|
|
174
226
|
console.log(gray(buildAdoptionReport(inferred)));
|
|
175
227
|
console.log();
|
|
176
|
-
|
|
228
|
+
console.log(gray(buildSignalsReport(signals)));
|
|
229
|
+
console.log();
|
|
230
|
+
if (reportJson && !reportHumanOnly) {
|
|
177
231
|
console.log(
|
|
178
232
|
JSON.stringify(
|
|
179
233
|
{
|
|
180
234
|
mode: "adopt",
|
|
181
235
|
policyId: detectedName,
|
|
182
236
|
inferredCapabilities: summarized,
|
|
237
|
+
components: signals.components,
|
|
238
|
+
displayFields: signals.displayFields,
|
|
239
|
+
externalLibraries: signals.externalLibraries,
|
|
240
|
+
uiLayout: signals.uiLayout,
|
|
241
|
+
styling: signals.styling,
|
|
242
|
+
developmentProfile: signals.developmentProfile,
|
|
183
243
|
},
|
|
184
244
|
null,
|
|
185
245
|
2
|
|
@@ -209,36 +269,58 @@ export async function initCommand(args) {
|
|
|
209
269
|
id,
|
|
210
270
|
title: id.replace(/([A-Z])/g, " $1").trim(),
|
|
211
271
|
}));
|
|
212
|
-
|
|
213
|
-
|
|
272
|
+
const signals = discoverProjectSignals(cwd, {
|
|
273
|
+
language: langOverride || undefined,
|
|
274
|
+
framework: frameworkOverride || undefined,
|
|
275
|
+
projectType: projectTypeOverride || undefined,
|
|
276
|
+
});
|
|
277
|
+
writeAdoptionBaseline(infernoDir, policyId, capDetails, signals);
|
|
278
|
+
if (!silent) {
|
|
214
279
|
ok("Created: " + cyan("inferno/contract.json"));
|
|
215
280
|
ok("Created: " + cyan("inferno/capabilities.json"));
|
|
216
281
|
ok("Created: " + cyan("inferno/scenarios/adoption_baseline.json"));
|
|
282
|
+
ok("Created: " + cyan("inferno/adoption_profile.json"));
|
|
217
283
|
ok("Created: " + cyan("inferno/CHANGELOG.md"));
|
|
218
284
|
}
|
|
219
285
|
} else {
|
|
220
286
|
writeContract(path.join(infernoDir, "contract.json"), policyId, capabilities);
|
|
221
|
-
if (!
|
|
287
|
+
if (!silent) ok("Created: " + cyan("inferno/contract.json"));
|
|
222
288
|
|
|
223
289
|
writeCapabilities(path.join(infernoDir, "capabilities.json"), capabilities);
|
|
224
|
-
if (!
|
|
290
|
+
if (!silent) ok("Created: " + cyan("inferno/capabilities.json"));
|
|
225
291
|
|
|
226
292
|
writeScenario(path.join(infernoDir, "scenarios"), capabilities);
|
|
227
|
-
if (!
|
|
293
|
+
if (!silent) ok("Created: " + cyan("inferno/scenarios/happy_path.json"));
|
|
228
294
|
|
|
229
295
|
writeChangelog(path.join(infernoDir, "CHANGELOG.md"), policyId);
|
|
230
|
-
if (!
|
|
296
|
+
if (!silent) ok("Created: " + cyan("inferno/CHANGELOG.md"));
|
|
231
297
|
}
|
|
232
298
|
|
|
233
299
|
// Copy doc-gate script
|
|
234
300
|
const templates = getTemplatesRoot();
|
|
235
301
|
const srcScript = path.join(templates, "scripts", "inferno-doc-gate.mjs");
|
|
236
302
|
const dstScript = path.join(cwd, "scripts", "inferno-doc-gate.mjs");
|
|
237
|
-
copyFile(srcScript, dstScript, force,
|
|
303
|
+
copyFile(srcScript, dstScript, force, silent);
|
|
304
|
+
|
|
305
|
+
upsertScripts(cwd, silent);
|
|
238
306
|
|
|
239
|
-
|
|
307
|
+
if (adopt) {
|
|
308
|
+
const statePath = path.join(infernoDir, "context-state.json");
|
|
309
|
+
let state = {};
|
|
310
|
+
try {
|
|
311
|
+
state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
312
|
+
} catch {}
|
|
313
|
+
const signals = discoverProjectSignals(cwd, {
|
|
314
|
+
language: langOverride || undefined,
|
|
315
|
+
framework: frameworkOverride || undefined,
|
|
316
|
+
projectType: projectTypeOverride || undefined,
|
|
317
|
+
});
|
|
318
|
+
state.stack = signals.developmentProfile;
|
|
319
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
320
|
+
if (!silent) ok("Created: " + cyan("inferno/context-state.json"));
|
|
321
|
+
}
|
|
240
322
|
|
|
241
|
-
if (!
|
|
323
|
+
if (!silent) {
|
|
242
324
|
done("infernoflow initialized!");
|
|
243
325
|
|
|
244
326
|
nextSteps([
|