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 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
@@ -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
 
@@ -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
- return Array.from(inferred.values()).map((c) => ({
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 lines = ["Inferred capabilities report:"];
126
- for (const c of summarizeCapabilities(capabilities)) {
127
- lines.push(`- ${c.id} (${c.title}) [confidence: ${c.confidence}]`);
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(` sources: inferred fallback (no strong code signal)`);
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
 
@@ -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 { discoverCapabilities, reviewCapabilitiesInteractive, writeAdoptionBaseline, buildAdoptionReport, summarizeCapabilities } from "./adopt.mjs";
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 (!reportJsonOnly) {
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 (reportJsonOnly) {
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 inferred = discoverCapabilities(cwd);
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
- { mode: "adopt", policyId: detectedName, inferredCapabilities: summarized },
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
- if (reportJson) {
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
- writeAdoptionBaseline(infernoDir, policyId, capDetails);
213
- if (!reportJsonOnly) {
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 (!reportJsonOnly) ok("Created: " + cyan("inferno/contract.json"));
287
+ if (!silent) ok("Created: " + cyan("inferno/contract.json"));
222
288
 
223
289
  writeCapabilities(path.join(infernoDir, "capabilities.json"), capabilities);
224
- if (!reportJsonOnly) ok("Created: " + cyan("inferno/capabilities.json"));
290
+ if (!silent) ok("Created: " + cyan("inferno/capabilities.json"));
225
291
 
226
292
  writeScenario(path.join(infernoDir, "scenarios"), capabilities);
227
- if (!reportJsonOnly) ok("Created: " + cyan("inferno/scenarios/happy_path.json"));
293
+ if (!silent) ok("Created: " + cyan("inferno/scenarios/happy_path.json"));
228
294
 
229
295
  writeChangelog(path.join(infernoDir, "CHANGELOG.md"), policyId);
230
- if (!reportJsonOnly) ok("Created: " + cyan("inferno/CHANGELOG.md"));
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, reportJsonOnly);
303
+ copyFile(srcScript, dstScript, force, silent);
304
+
305
+ upsertScripts(cwd, silent);
238
306
 
239
- upsertScripts(cwd, reportJsonOnly);
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 (!reportJsonOnly) {
323
+ if (!silent) {
242
324
  done("infernoflow initialized!");
243
325
 
244
326
  nextSteps([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.10.3",
3
+ "version": "0.10.5",
4
4
  "description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {