infernoflow 0.10.0 → 0.10.2

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
@@ -41,6 +41,43 @@ infernoflow check
41
41
  infernoflow doc-gate
42
42
  ```
43
43
 
44
+ ## Adopt Existing Project
45
+
46
+ Use this when your project already has code and you want InfernoFlow to bootstrap from current behavior.
47
+
48
+ ```bash
49
+ # from existing project root
50
+ infernoflow init --adopt
51
+ ```
52
+
53
+ Non-interactive adoption:
54
+
55
+ ```bash
56
+ infernoflow init --adopt --yes
57
+ ```
58
+
59
+ JSON report for CI/logging:
60
+
61
+ ```bash
62
+ infernoflow init --adopt --yes --report-json
63
+ ```
64
+
65
+ JSON-only output (clean machine output, no text logs):
66
+
67
+ ```bash
68
+ infernoflow init --adopt --yes --report-json-only
69
+ ```
70
+
71
+ What adoption creates:
72
+ - `inferno/contract.json` (inferred capability baseline)
73
+ - `inferno/capabilities.json` (inferred registry)
74
+ - `inferno/scenarios/adoption_baseline.json` (coverage baseline)
75
+ - `inferno/CHANGELOG.md` (adoption entry)
76
+
77
+ Safety:
78
+ - Existing `inferno/` is not overwritten unless `--force` is provided.
79
+ - Adoption prints an inferred capability report with source-file hints and confidence.
80
+
44
81
  ## Recommended Workflow
45
82
 
46
83
  ```bash
@@ -117,6 +154,7 @@ infernoflow doc-gate --json
117
154
  ```bash
118
155
  infernoflow init --force # overwrite existing files
119
156
  infernoflow init --yes # skip prompts, use defaults
157
+ infernoflow init --adopt # infer baseline from existing project
120
158
  infernoflow suggest "..." # describe what changed
121
159
  infernoflow implement "..." --mode both
122
160
  infernoflow implement "..." --mode cursor
@@ -8,7 +8,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
9
9
  const VERSION = pkg.version || "0.0.0";
10
10
  const COMMAND_DESCRIPTIONS = {
11
- init: "Scaffold inferno/ in your project",
11
+ init: "Scaffold inferno/ in your project (or adopt existing project)",
12
12
  check: "Validate contract, capabilities, scenarios, changelog",
13
13
  status: "Show contract health at a glance",
14
14
  "doc-gate": "Fail if code changed but docs were not updated",
@@ -43,6 +43,13 @@ const HELP = `
43
43
  ${bold("Commands:")}
44
44
  ${formatCommandsHelp()}
45
45
 
46
+ ${bold("init options:")}
47
+ --adopt Infer capabilities from an existing codebase
48
+ --report-json Print inferred adoption report as JSON
49
+ --report-json-only Print JSON report only (no human-readable logs)
50
+ --yes, -y Skip prompts and accept inferred/default values
51
+ --force, -f Overwrite existing inferno/ files
52
+
46
53
  ${bold("context options:")}
47
54
  --intent "..." What you plan to build next
48
55
  --working "..." What you are building right now
@@ -0,0 +1,193 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as readline from "node:readline";
4
+
5
+ function toCapabilityId(raw) {
6
+ return raw
7
+ .replace(/[^a-zA-Z0-9]+/g, " ")
8
+ .trim()
9
+ .split(/\s+/)
10
+ .filter(Boolean)
11
+ .map((w) => w[0].toUpperCase() + w.slice(1))
12
+ .join("");
13
+ }
14
+
15
+ function capTitle(id) {
16
+ return id.replace(/([A-Z])/g, " $1").trim();
17
+ }
18
+
19
+ function safeRead(filePath) {
20
+ try {
21
+ return fs.readFileSync(filePath, "utf8");
22
+ } catch {
23
+ return "";
24
+ }
25
+ }
26
+
27
+ const HEURISTICS = [
28
+ { id: "CreateItem", title: "Create Item", regex: /\b(post|create|add)\b/i },
29
+ { id: "ReadItems", title: "Read Items", regex: /\b(get|read|list|fetch)\b/i },
30
+ { id: "UpdateItem", title: "Update Item", regex: /\b(put|patch|update|edit)\b/i },
31
+ { id: "DeleteItem", title: "Delete Item", regex: /\b(delete|remove)\b/i },
32
+ { id: "SearchItems", title: "Search Items", regex: /\bsearch\b/i },
33
+ { id: "FilterItems", title: "Filter Items", regex: /\bfilter\b/i },
34
+ { id: "SetDueDate", title: "Set Due Date", regex: /\bdueDate|deadline|due\b/i },
35
+ { id: "SetPriority", title: "Set Priority", regex: /\bpriority\b/i },
36
+ { id: "ToggleComplete", title: "Toggle Complete", regex: /\bcomplete|completed|toggle\b/i },
37
+ { id: "ClearCompleted", title: "Clear Completed", regex: /\bclearCompleted|clear completed\b/i },
38
+ ];
39
+
40
+ export function discoverCapabilities(cwd) {
41
+ const files = [];
42
+ const roots = ["src", "server", "app", "backend", "frontend", "api"];
43
+ for (const r of roots) {
44
+ const root = path.join(cwd, r);
45
+ if (!fs.existsSync(root)) continue;
46
+ const stack = [root];
47
+ while (stack.length) {
48
+ const cur = stack.pop();
49
+ for (const entry of fs.readdirSync(cur, { withFileTypes: true })) {
50
+ const p = path.join(cur, entry.name);
51
+ if (entry.isDirectory()) {
52
+ if (["node_modules", ".git", "dist", "build"].includes(entry.name)) continue;
53
+ stack.push(p);
54
+ } else if (/\.(js|jsx|ts|tsx|mjs|cjs|json|md)$/.test(entry.name)) {
55
+ files.push(p);
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ const inferred = new Map();
62
+ const addHit = (cap, filePath) => {
63
+ if (!inferred.has(cap.id)) {
64
+ inferred.set(cap.id, {
65
+ id: cap.id,
66
+ title: cap.title,
67
+ reason: "Detected from code signals",
68
+ sourceFiles: new Set(),
69
+ });
70
+ }
71
+ inferred.get(cap.id).sourceFiles.add(path.relative(cwd, filePath));
72
+ };
73
+
74
+ for (const filePath of files) {
75
+ const text = safeRead(filePath);
76
+ for (const h of HEURISTICS) {
77
+ if (h.regex.test(text)) {
78
+ addHit(h, filePath);
79
+ }
80
+ }
81
+ }
82
+
83
+ const pkgPath = path.join(cwd, "package.json");
84
+ if (fs.existsSync(pkgPath)) {
85
+ const pkg = JSON.parse(safeRead(pkgPath) || "{}");
86
+ const name = typeof pkg.name === "string" ? pkg.name : path.basename(cwd);
87
+ const idHint = toCapabilityId(name);
88
+ if (idHint && !inferred.size) {
89
+ inferred.set("ReadItems", { id: "ReadItems", title: "Read Items", reason: `Fallback default for ${name}`, sourceFiles: new Set() });
90
+ inferred.set("CreateItem", { id: "CreateItem", title: "Create Item", reason: `Fallback default for ${name}`, sourceFiles: new Set() });
91
+ }
92
+ }
93
+
94
+ if (!inferred.size) {
95
+ inferred.set("CreateItem", { id: "CreateItem", title: "Create Item", reason: "Fallback default", sourceFiles: new Set() });
96
+ inferred.set("ReadItems", { id: "ReadItems", title: "Read Items", reason: "Fallback default", sourceFiles: new Set() });
97
+ }
98
+
99
+ return Array.from(inferred.values()).map((c) => ({
100
+ ...c,
101
+ sourceFiles: Array.from(c.sourceFiles || []),
102
+ }));
103
+ }
104
+
105
+ export async function reviewCapabilitiesInteractive(capabilities, yes = false) {
106
+ if (yes) return capabilities;
107
+
108
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
109
+ const list = capabilities.map((c) => c.id).join(", ");
110
+ const answer = await new Promise((resolve) =>
111
+ rl.question(` Inferred capabilities (${list}). Press Enter to accept or type comma list: `, resolve)
112
+ );
113
+ rl.close();
114
+ const trimmed = String(answer).trim();
115
+ if (!trimmed) return capabilities;
116
+ return trimmed
117
+ .split(",")
118
+ .map((s) => s.trim())
119
+ .filter(Boolean)
120
+ .map((id) => ({ id, title: capTitle(id), reason: "User provided during adopt review" }));
121
+ }
122
+
123
+ export function buildAdoptionReport(capabilities) {
124
+ 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}]`);
128
+ if (c.signalCount > 0) {
129
+ const sample = c.sourceFiles.slice(0, 3).join(", ");
130
+ lines.push(` sources: ${sample}`);
131
+ } else {
132
+ lines.push(` sources: inferred fallback (no strong code signal)`);
133
+ }
134
+ }
135
+ return lines.join("\n");
136
+ }
137
+
138
+ export function summarizeCapabilities(capabilities) {
139
+ return capabilities.map((c) => {
140
+ const hits = c.sourceFiles?.length || 0;
141
+ const confidence = hits >= 3 ? "high" : hits >= 1 ? "medium" : "low";
142
+ return {
143
+ id: c.id,
144
+ title: c.title,
145
+ reason: c.reason,
146
+ confidence,
147
+ sourceFiles: c.sourceFiles || [],
148
+ signalCount: hits,
149
+ };
150
+ });
151
+ }
152
+
153
+ export function writeAdoptionBaseline(infernoDir, policyId, capabilities) {
154
+ const capIds = capabilities.map((c) => c.id);
155
+ const contract = {
156
+ policyId,
157
+ policyVersion: 1,
158
+ capabilities: capIds,
159
+ rules: {
160
+ docsRequiredOnCapabilityChange: true,
161
+ requireScenarioForEachCapability: true,
162
+ requireChangelogOnCapabilityChange: true,
163
+ },
164
+ };
165
+ fs.mkdirSync(path.join(infernoDir, "scenarios"), { recursive: true });
166
+ fs.writeFileSync(path.join(infernoDir, "contract.json"), JSON.stringify(contract, null, 2) + "\n");
167
+
168
+ const registry = {
169
+ schemaVersion: 1,
170
+ capabilities: capabilities.map((c) => ({ id: c.id, title: c.title || capTitle(c.id), since: "0.1.0" })),
171
+ };
172
+ fs.writeFileSync(path.join(infernoDir, "capabilities.json"), JSON.stringify(registry, null, 2) + "\n");
173
+
174
+ const scenario = {
175
+ scenarioId: "adoption_baseline",
176
+ description: "Baseline inferred from existing codebase during adoption",
177
+ capabilitiesCovered: capIds,
178
+ steps: capIds.map((id) => ({ action: id, expect: `${id} behavior exists in the current project` })),
179
+ };
180
+ fs.writeFileSync(path.join(infernoDir, "scenarios", "adoption_baseline.json"), JSON.stringify(scenario, null, 2) + "\n");
181
+
182
+ const changelog = `# Changelog — ${policyId}
183
+
184
+ ## Unreleased
185
+
186
+ - Adopted infernoflow into an existing project and generated baseline capabilities.
187
+
188
+ ## 0.1.0 — Adoption baseline
189
+
190
+ - Initial baseline generated by infernoflow init --adopt
191
+ `;
192
+ fs.writeFileSync(path.join(infernoDir, "CHANGELOG.md"), changelog, "utf8");
193
+ }
@@ -3,6 +3,7 @@ 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
7
 
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
9
 
@@ -19,14 +20,14 @@ function ask(rl, question, defaultVal = "") {
19
20
  });
20
21
  }
21
22
 
22
- function copyFile(src, dst, force) {
23
+ function copyFile(src, dst, force, silent = false) {
23
24
  if (fs.existsSync(dst) && !force) {
24
- warn("Skipped (exists): " + path.relative(process.cwd(), dst));
25
+ if (!silent) warn("Skipped (exists): " + path.relative(process.cwd(), dst));
25
26
  return false;
26
27
  }
27
28
  fs.mkdirSync(path.dirname(dst), { recursive: true });
28
29
  fs.copyFileSync(src, dst);
29
- ok("Created: " + cyan(path.relative(process.cwd(), dst)));
30
+ if (!silent) ok("Created: " + cyan(path.relative(process.cwd(), dst)));
30
31
  return true;
31
32
  }
32
33
 
@@ -40,7 +41,7 @@ function copyDirDeep(srcDir, dstDir, force) {
40
41
  }
41
42
  }
42
43
 
43
- function upsertScripts(cwd) {
44
+ function upsertScripts(cwd, silent = false) {
44
45
  const pkgPath = path.join(cwd, "package.json");
45
46
  if (!fs.existsSync(pkgPath)) return;
46
47
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
@@ -56,7 +57,7 @@ function upsertScripts(cwd) {
56
57
  }
57
58
  if (changed) {
58
59
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
59
- ok("Updated " + cyan("package.json") + " scripts");
60
+ if (!silent) ok("Updated " + cyan("package.json") + " scripts");
60
61
  }
61
62
  }
62
63
 
@@ -132,11 +133,20 @@ export async function initCommand(args) {
132
133
  const cwd = process.cwd();
133
134
  const force = args.includes("--force") || args.includes("-f");
134
135
  const yes = args.includes("--yes") || args.includes("-y");
136
+ const adopt = args.includes("--adopt");
137
+ const reportJson = args.includes("--report-json");
138
+ const reportJsonOnly = args.includes("--report-json-only");
135
139
 
136
- header("init");
140
+ if (!reportJsonOnly) {
141
+ header("init");
142
+ }
137
143
 
138
144
  const infernoDir = path.join(cwd, "inferno");
139
145
  if (fs.existsSync(infernoDir) && !force) {
146
+ if (reportJsonOnly) {
147
+ console.log(JSON.stringify({ ok: false, error: "inferno_exists", hint: "Use --force to overwrite" }, null, 2));
148
+ process.exit(1);
149
+ }
140
150
  warn("inferno/ already exists. Use --force to overwrite.");
141
151
  console.log();
142
152
  process.exit(0);
@@ -148,7 +158,40 @@ export async function initCommand(args) {
148
158
  let policyId = detectedName;
149
159
  let capabilities = defaultCaps.split(",").map(c => c.trim());
150
160
 
151
- if (!yes) {
161
+ if (adopt) {
162
+ const inferred = discoverCapabilities(cwd);
163
+ const summarized = summarizeCapabilities(inferred);
164
+ if (reportJsonOnly) {
165
+ console.log(
166
+ JSON.stringify(
167
+ { mode: "adopt", policyId: detectedName, inferredCapabilities: summarized },
168
+ null,
169
+ 2
170
+ )
171
+ );
172
+ } else {
173
+ console.log();
174
+ console.log(gray(buildAdoptionReport(inferred)));
175
+ console.log();
176
+ if (reportJson) {
177
+ console.log(
178
+ JSON.stringify(
179
+ {
180
+ mode: "adopt",
181
+ policyId: detectedName,
182
+ inferredCapabilities: summarized,
183
+ },
184
+ null,
185
+ 2
186
+ )
187
+ );
188
+ console.log();
189
+ }
190
+ }
191
+ const reviewed = await reviewCapabilitiesInteractive(inferred, yes);
192
+ policyId = detectedName;
193
+ capabilities = reviewed.map((c) => c.id);
194
+ } else if (!yes) {
152
195
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
153
196
  console.log(gray(" Press Enter to accept defaults\n"));
154
197
  policyId = await ask(rl, "Project / policy name", detectedName);
@@ -161,33 +204,49 @@ export async function initCommand(args) {
161
204
  // Write files
162
205
  fs.mkdirSync(infernoDir, { recursive: true });
163
206
 
164
- writeContract(path.join(infernoDir, "contract.json"), policyId, capabilities);
165
- ok("Created: " + cyan("inferno/contract.json"));
207
+ if (adopt) {
208
+ const capDetails = capabilities.map((id) => ({
209
+ id,
210
+ title: id.replace(/([A-Z])/g, " $1").trim(),
211
+ }));
212
+ writeAdoptionBaseline(infernoDir, policyId, capDetails);
213
+ if (!reportJsonOnly) {
214
+ ok("Created: " + cyan("inferno/contract.json"));
215
+ ok("Created: " + cyan("inferno/capabilities.json"));
216
+ ok("Created: " + cyan("inferno/scenarios/adoption_baseline.json"));
217
+ ok("Created: " + cyan("inferno/CHANGELOG.md"));
218
+ }
219
+ } else {
220
+ writeContract(path.join(infernoDir, "contract.json"), policyId, capabilities);
221
+ if (!reportJsonOnly) ok("Created: " + cyan("inferno/contract.json"));
166
222
 
167
- writeCapabilities(path.join(infernoDir, "capabilities.json"), capabilities);
168
- ok("Created: " + cyan("inferno/capabilities.json"));
223
+ writeCapabilities(path.join(infernoDir, "capabilities.json"), capabilities);
224
+ if (!reportJsonOnly) ok("Created: " + cyan("inferno/capabilities.json"));
169
225
 
170
- writeScenario(path.join(infernoDir, "scenarios"), capabilities);
171
- ok("Created: " + cyan("inferno/scenarios/happy_path.json"));
226
+ writeScenario(path.join(infernoDir, "scenarios"), capabilities);
227
+ if (!reportJsonOnly) ok("Created: " + cyan("inferno/scenarios/happy_path.json"));
172
228
 
173
- writeChangelog(path.join(infernoDir, "CHANGELOG.md"), policyId);
174
- ok("Created: " + cyan("inferno/CHANGELOG.md"));
229
+ writeChangelog(path.join(infernoDir, "CHANGELOG.md"), policyId);
230
+ if (!reportJsonOnly) ok("Created: " + cyan("inferno/CHANGELOG.md"));
231
+ }
175
232
 
176
233
  // Copy doc-gate script
177
234
  const templates = getTemplatesRoot();
178
235
  const srcScript = path.join(templates, "scripts", "inferno-doc-gate.mjs");
179
236
  const dstScript = path.join(cwd, "scripts", "inferno-doc-gate.mjs");
180
- copyFile(srcScript, dstScript, force);
237
+ copyFile(srcScript, dstScript, force, reportJsonOnly);
181
238
 
182
- upsertScripts(cwd);
239
+ upsertScripts(cwd, reportJsonOnly);
183
240
 
184
- done("infernoflow initialized!");
241
+ if (!reportJsonOnly) {
242
+ done("infernoflow initialized!");
185
243
 
186
- nextSteps([
187
- cyan("infernoflow status") + " — see your contract at a glance",
188
- cyan("infernoflow check") + " — validate everything",
189
- "Edit " + yellow("inferno/capabilities.json") + " to describe each capability in detail",
190
- "Add more " + yellow("inferno/scenarios/*.json") + " files for edge cases",
191
- "Add " + cyan("inferno:check") + " to your CI pipeline"
192
- ]);
244
+ nextSteps([
245
+ cyan("infernoflow status") + " — see your contract at a glance",
246
+ cyan("infernoflow check") + " — validate everything",
247
+ (adopt ? "Review inferred baseline in " : "Edit ") + yellow("inferno/capabilities.json") + (adopt ? " and refine IDs/titles" : " to describe each capability in detail"),
248
+ "Add more " + yellow("inferno/scenarios/*.json") + " files for edge cases",
249
+ "Add " + cyan("inferno:check") + " to your CI pipeline"
250
+ ]);
251
+ }
193
252
  }
package/package.json CHANGED
@@ -1,43 +1,43 @@
1
- {
2
- "name": "infernoflow",
3
- "version": "0.10.0",
4
- "description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
5
- "type": "module",
6
- "bin": {
7
- "infernoflow": "./bin/infernoflow.mjs"
8
- },
9
- "engines": {
10
- "node": ">=18"
11
- },
12
- "files": [
13
- "bin",
14
- "lib",
15
- "templates",
16
- "README.md"
17
- ],
18
- "scripts": {
19
- "test": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs && node scripts/implement-smoke.mjs",
20
- "test:help": "node bin/infernoflow.mjs --help"
21
- },
22
- "keywords": [
23
- "cli",
24
- "capabilities",
25
- "contract",
26
- "documentation",
27
- "ai",
28
- "liquid-code",
29
- "dx",
30
- "developer-tools"
31
- ],
32
- "author": "infernoflow",
33
- "license": "MIT",
34
- "repository": {
35
- "type": "git",
36
- "url": "https://github.com/ronmiz/infernoflow.git"
37
- },
38
- "homepage": "https://github.com/ronmiz/infernoflow#readme",
39
- "bugs": {
40
- "url": "https://github.com/ronmiz/infernoflow/issues"
41
- },
42
- "dependencies": {}
43
- }
1
+ {
2
+ "name": "infernoflow",
3
+ "version": "0.10.2",
4
+ "description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
5
+ "type": "module",
6
+ "bin": {
7
+ "infernoflow": "bin/infernoflow.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "lib",
15
+ "templates",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "test": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs && node scripts/implement-smoke.mjs",
20
+ "test:help": "node bin/infernoflow.mjs --help"
21
+ },
22
+ "keywords": [
23
+ "cli",
24
+ "capabilities",
25
+ "contract",
26
+ "documentation",
27
+ "ai",
28
+ "liquid-code",
29
+ "dx",
30
+ "developer-tools"
31
+ ],
32
+ "author": "infernoflow",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/ronmiz/infernoflow.git"
37
+ },
38
+ "homepage": "https://github.com/ronmiz/infernoflow#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/ronmiz/infernoflow/issues"
41
+ },
42
+ "dependencies": {}
43
+ }