infernoflow 0.10.7 → 0.10.8

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.
@@ -1,103 +1,103 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { execSync } from "node:child_process";
4
- import { header, section, info, warn, cyan, gray, errorAndExit } from "../ui/output.mjs";
5
- import {
6
- loadImplementContext,
7
- buildCursorImplementPrompt,
8
- buildGenericImplementPrompt,
9
- } from "../ui/prompts.mjs";
10
-
11
- function getFlagValue(args, flag) {
12
- const idx = args.indexOf(flag);
13
- return idx !== -1 && args[idx + 1] ? args[idx + 1] : null;
14
- }
15
-
16
- function extractTask(args) {
17
- const skipNextFor = new Set(["--mode"]);
18
- const parts = [];
19
- for (let i = 0; i < args.length; i += 1) {
20
- const token = args[i];
21
- if (token.startsWith("-")) {
22
- if (skipNextFor.has(token)) i += 1;
23
- continue;
24
- }
25
- if (i === 0) continue; // command name
26
- parts.push(token);
27
- }
28
- return parts.join(" ").trim();
29
- }
30
-
31
- function copyToClipboard(text) {
32
- try {
33
- const p = process.platform;
34
- if (p === "win32") execSync("clip", { input: text });
35
- else if (p === "darwin") execSync("pbcopy", { input: text });
36
- else {
37
- try { execSync("xclip -selection clipboard", { input: text }); }
38
- catch { execSync("xsel --clipboard --input", { input: text }); }
39
- }
40
- return true;
41
- } catch {
42
- return false;
43
- }
44
- }
45
-
46
- export async function implementCommand(args = []) {
47
- header("implement");
48
-
49
- const cwd = process.cwd();
50
- const infernoDir = path.join(cwd, "inferno");
51
- if (!fs.existsSync(infernoDir)) {
52
- errorAndExit("inferno/ not found", "Run: infernoflow init");
53
- }
54
-
55
- const mode = (getFlagValue(args, "--mode") || "both").toLowerCase();
56
- const copyFlag = args.includes("--copy") || args.includes("-c");
57
- if (!["cursor", "generic", "both"].includes(mode)) {
58
- errorAndExit("Invalid --mode value", "Use: --mode cursor|generic|both");
59
- }
60
-
61
- const rawTask = extractTask(args);
62
- if (!rawTask) {
63
- errorAndExit("No task provided", 'Usage: infernoflow implement "your task description"');
64
- }
65
-
66
- const context = loadImplementContext(cwd);
67
- const cursorPrompt = buildCursorImplementPrompt({ task: rawTask, ...context });
68
- const genericPrompt = buildGenericImplementPrompt({ task: rawTask, ...context });
69
-
70
- info(`Task: ${cyan(rawTask)}`);
71
- info(`Mode: ${cyan(mode)}`);
72
- warn("If you hit model high-load/resource-exhausted, retry with Auto/another model.");
73
-
74
- if (mode === "cursor" || mode === "both") {
75
- section("Cursor Agent Prompt");
76
- console.log();
77
- console.log(gray("─".repeat(50)));
78
- console.log(cursorPrompt);
79
- console.log(gray("─".repeat(50)));
80
- }
81
-
82
- if (mode === "generic" || mode === "both") {
83
- section("Generic Agent Prompt");
84
- console.log();
85
- console.log(gray("─".repeat(50)));
86
- console.log(genericPrompt);
87
- console.log(gray("─".repeat(50)));
88
- }
89
-
90
- if (copyFlag) {
91
- const textToCopy =
92
- mode === "cursor"
93
- ? cursorPrompt
94
- : mode === "generic"
95
- ? genericPrompt
96
- : `## Cursor Agent Prompt\n\n${cursorPrompt}\n\n## Generic Agent Prompt\n\n${genericPrompt}`;
97
- const ok = copyToClipboard(textToCopy);
98
- if (ok) info(`Copied ${mode} prompt${mode === "both" ? "s" : ""} to clipboard.`);
99
- else warn("Clipboard copy failed. Copy from terminal output.");
100
- }
101
-
102
- console.log();
103
- }
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import { header, section, info, warn, cyan, gray, errorAndExit } from "../ui/output.mjs";
5
+ import {
6
+ loadImplementContext,
7
+ buildCursorImplementPrompt,
8
+ buildGenericImplementPrompt,
9
+ } from "../ui/prompts.mjs";
10
+
11
+ function getFlagValue(args, flag) {
12
+ const idx = args.indexOf(flag);
13
+ return idx !== -1 && args[idx + 1] ? args[idx + 1] : null;
14
+ }
15
+
16
+ function extractTask(args) {
17
+ const skipNextFor = new Set(["--mode"]);
18
+ const parts = [];
19
+ for (let i = 0; i < args.length; i += 1) {
20
+ const token = args[i];
21
+ if (token.startsWith("-")) {
22
+ if (skipNextFor.has(token)) i += 1;
23
+ continue;
24
+ }
25
+ if (i === 0) continue; // command name
26
+ parts.push(token);
27
+ }
28
+ return parts.join(" ").trim();
29
+ }
30
+
31
+ function copyToClipboard(text) {
32
+ try {
33
+ const p = process.platform;
34
+ if (p === "win32") execSync("clip", { input: text });
35
+ else if (p === "darwin") execSync("pbcopy", { input: text });
36
+ else {
37
+ try { execSync("xclip -selection clipboard", { input: text }); }
38
+ catch { execSync("xsel --clipboard --input", { input: text }); }
39
+ }
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ export async function implementCommand(args = []) {
47
+ header("implement");
48
+
49
+ const cwd = process.cwd();
50
+ const infernoDir = path.join(cwd, "inferno");
51
+ if (!fs.existsSync(infernoDir)) {
52
+ errorAndExit("inferno/ not found", "Run: infernoflow init");
53
+ }
54
+
55
+ const mode = (getFlagValue(args, "--mode") || "both").toLowerCase();
56
+ const copyFlag = args.includes("--copy") || args.includes("-c");
57
+ if (!["cursor", "generic", "both"].includes(mode)) {
58
+ errorAndExit("Invalid --mode value", "Use: --mode cursor|generic|both");
59
+ }
60
+
61
+ const rawTask = extractTask(args);
62
+ if (!rawTask) {
63
+ errorAndExit("No task provided", 'Usage: infernoflow implement "your task description"');
64
+ }
65
+
66
+ const context = loadImplementContext(cwd);
67
+ const cursorPrompt = buildCursorImplementPrompt({ task: rawTask, ...context });
68
+ const genericPrompt = buildGenericImplementPrompt({ task: rawTask, ...context });
69
+
70
+ info(`Task: ${cyan(rawTask)}`);
71
+ info(`Mode: ${cyan(mode)}`);
72
+ warn("If you hit model high-load/resource-exhausted, retry with Auto/another model.");
73
+
74
+ if (mode === "cursor" || mode === "both") {
75
+ section("Cursor Agent Prompt");
76
+ console.log();
77
+ console.log(gray("─".repeat(50)));
78
+ console.log(cursorPrompt);
79
+ console.log(gray("─".repeat(50)));
80
+ }
81
+
82
+ if (mode === "generic" || mode === "both") {
83
+ section("Generic Agent Prompt");
84
+ console.log();
85
+ console.log(gray("─".repeat(50)));
86
+ console.log(genericPrompt);
87
+ console.log(gray("─".repeat(50)));
88
+ }
89
+
90
+ if (copyFlag) {
91
+ const textToCopy =
92
+ mode === "cursor"
93
+ ? cursorPrompt
94
+ : mode === "generic"
95
+ ? genericPrompt
96
+ : `## Cursor Agent Prompt\n\n${cursorPrompt}\n\n## Generic Agent Prompt\n\n${genericPrompt}`;
97
+ const ok = copyToClipboard(textToCopy);
98
+ if (ok) info(`Copied ${mode} prompt${mode === "both" ? "s" : ""} to clipboard.`);
99
+ else warn("Clipboard copy failed. Copy from terminal output.");
100
+ }
101
+
102
+ console.log();
103
+ }
@@ -68,6 +68,7 @@ function upsertScripts(cwd, silent = false) {
68
68
  "inferno:gate": "infernoflow doc-gate",
69
69
  "inferno:impact": "infernoflow pr-impact --json",
70
70
  "inferno:sync": "infernoflow sync --auto --json",
71
+ "inferno:run": "infernoflow run \"sync check\" --json",
71
72
  "inferno:hooks": "node scripts/inferno-install-hooks.mjs"
72
73
  };
73
74
  for (const [k, v] of Object.entries(toAdd)) {
@@ -1,157 +1,157 @@
1
- import { execSync } from "node:child_process";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { header, section, ok, warn, fail, gray, cyan, yellow } from "../ui/output.mjs";
5
-
6
- const CODE_PREFIXES = ["src/", "frontend/", "backend/", "app/", "pages/", "components/", "lib/", "api/", "server/", "Controllers/"];
7
-
8
- function sh(cmd) {
9
- return execSync(cmd, { stdio: ["ignore", "pipe", "pipe"] }).toString("utf8").trim();
10
- }
11
-
12
- function readJson(filePath, fallback = null) {
13
- try {
14
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
15
- } catch {
16
- return fallback;
17
- }
18
- }
19
-
20
- function readFile(filePath, fallback = "") {
21
- try {
22
- return fs.readFileSync(filePath, "utf8");
23
- } catch {
24
- return fallback;
25
- }
26
- }
27
-
28
- function getChangedFiles(base, head) {
29
- const out = base && head
30
- ? sh(`git diff --name-only ${base}..${head}`)
31
- : sh("git diff --name-only HEAD");
32
- return out ? out.split("\n").map((s) => s.trim()).filter(Boolean) : [];
33
- }
34
-
35
- function buildCapabilityHints(cwd) {
36
- const infernoDir = path.join(cwd, "inferno");
37
- const contract = readJson(path.join(infernoDir, "contract.json"), { capabilities: [] });
38
- const registry = readJson(path.join(infernoDir, "capabilities.json"), { capabilities: [] });
39
- const titleById = new Map((registry.capabilities || []).map((c) => [c.id, c.title || c.id]));
40
- return (contract.capabilities || []).map((id) => {
41
- const title = titleById.get(id) || id;
42
- const keywords = new Set(
43
- `${id} ${title}`
44
- .replace(/([A-Z])/g, " $1")
45
- .toLowerCase()
46
- .split(/[^a-z0-9]+/)
47
- .filter((k) => k.length >= 4)
48
- );
49
- return { id, title, keywords: Array.from(keywords) };
50
- });
51
- }
52
-
53
- function inferImpactedCapabilities(cwd, changedCodeFiles) {
54
- const hints = buildCapabilityHints(cwd);
55
- const impacted = [];
56
- for (const hint of hints) {
57
- const matched = [];
58
- for (const rel of changedCodeFiles) {
59
- const abs = path.join(cwd, rel);
60
- const text = readFile(abs, "").toLowerCase();
61
- if (!text) continue;
62
- if (hint.keywords.some((k) => text.includes(k))) {
63
- matched.push(rel);
64
- }
65
- }
66
- if (matched.length) {
67
- impacted.push({ id: hint.id, title: hint.title, matchedFiles: matched.slice(0, 5) });
68
- }
69
- }
70
- return impacted;
71
- }
72
-
73
- export async function prImpactCommand(args = []) {
74
- const asJson = args.includes("--json");
75
- const cwd = process.cwd();
76
- const base = process.env.BASE_SHA || null;
77
- const head = process.env.HEAD_SHA || null;
78
-
79
- let changedFiles = [];
80
- try {
81
- changedFiles = getChangedFiles(base, head);
82
- } catch {
83
- const payload = { ok: true, skipped: true, reason: "no_git_available" };
84
- if (asJson) {
85
- console.log(JSON.stringify(payload, null, 2));
86
- return;
87
- }
88
- header("pr-impact");
89
- warn("git not available; cannot compute PR impact");
90
- console.log();
91
- return;
92
- }
93
-
94
- const changedCodeFiles = changedFiles.filter((f) => CODE_PREFIXES.some((p) => f.startsWith(p)));
95
- const changedInfernoFiles = changedFiles.filter((f) => f.startsWith("inferno/"));
96
- const impactedCapabilities = inferImpactedCapabilities(cwd, changedCodeFiles);
97
- const inferredBehaviorChange = changedCodeFiles.length > 0;
98
- const missingInfernoUpdate = inferredBehaviorChange && changedInfernoFiles.length === 0;
99
- const confidence = impactedCapabilities.length > 0 ? "high" : inferredBehaviorChange ? "medium" : "low";
100
- const reasonCodes = [];
101
- if (inferredBehaviorChange) reasonCodes.push("CODE_CHANGED");
102
- if (missingInfernoUpdate) reasonCodes.push("INFERNO_NOT_UPDATED");
103
- if (impactedCapabilities.length > 0) reasonCodes.push("CAPABILITY_HINT_MATCH");
104
- if (!reasonCodes.length) reasonCodes.push("NO_BEHAVIOR_SIGNAL");
105
-
106
- const payload = {
107
- ok: !missingInfernoUpdate,
108
- base: base || "HEAD",
109
- head: head || "WORKTREE",
110
- changedFiles,
111
- changedCodeFiles,
112
- changedInfernoFiles,
113
- inferredBehaviorChange,
114
- impactedCapabilities,
115
- confidence,
116
- reasonCodes,
117
- recommendations: missingInfernoUpdate
118
- ? ["Run infernoflow suggest \"describe behavior change\" and update inferno/", "Run infernoflow check --json"]
119
- : ["Run infernoflow check --json to validate final state"],
120
- };
121
-
122
- if (asJson) {
123
- console.log(JSON.stringify(payload, null, 2));
124
- process.exit(payload.ok ? 0 : 1);
125
- }
126
-
127
- header("pr-impact");
128
-
129
- section("Diff Scope");
130
- ok(`Changed files: ${cyan(String(changedFiles.length))}`);
131
- ok(`Code files: ${cyan(String(changedCodeFiles.length))}`);
132
- ok(`Inferno files: ${cyan(String(changedInfernoFiles.length))}`);
133
-
134
- section("Capability Impact");
135
- if (impactedCapabilities.length === 0) {
136
- warn("No capability hints matched changed code files");
137
- } else {
138
- impactedCapabilities.forEach((c) => {
139
- console.log(` ${cyan("•")} ${c.id} ${gray(`(${c.title})`)}`);
140
- c.matchedFiles.slice(0, 3).forEach((f) => console.log(` ${gray("- " + f)}`));
141
- });
142
- }
143
-
144
- section("Doc Sync");
145
- if (missingInfernoUpdate) {
146
- fail("Code changed but inferno/ was not updated", "Run infernoflow suggest and then infernoflow check");
147
- } else {
148
- ok("No immediate inferno drift signal from changed files");
149
- }
150
- ok(`Confidence: ${cyan(confidence)}`);
151
-
152
- section("Suggested Next");
153
- payload.recommendations.forEach((r) => console.log(` ${yellow("→")} ${r}`));
154
- console.log();
155
- process.exit(payload.ok ? 0 : 1);
156
- }
157
-
1
+ import { execSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { header, section, ok, warn, fail, gray, cyan, yellow } from "../ui/output.mjs";
5
+
6
+ const CODE_PREFIXES = ["src/", "frontend/", "backend/", "app/", "pages/", "components/", "lib/", "api/", "server/", "Controllers/"];
7
+
8
+ function sh(cmd) {
9
+ return execSync(cmd, { stdio: ["ignore", "pipe", "pipe"] }).toString("utf8").trim();
10
+ }
11
+
12
+ function readJson(filePath, fallback = null) {
13
+ try {
14
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
15
+ } catch {
16
+ return fallback;
17
+ }
18
+ }
19
+
20
+ function readFile(filePath, fallback = "") {
21
+ try {
22
+ return fs.readFileSync(filePath, "utf8");
23
+ } catch {
24
+ return fallback;
25
+ }
26
+ }
27
+
28
+ function getChangedFiles(base, head) {
29
+ const out = base && head
30
+ ? sh(`git diff --name-only ${base}..${head}`)
31
+ : sh("git diff --name-only HEAD");
32
+ return out ? out.split("\n").map((s) => s.trim()).filter(Boolean) : [];
33
+ }
34
+
35
+ function buildCapabilityHints(cwd) {
36
+ const infernoDir = path.join(cwd, "inferno");
37
+ const contract = readJson(path.join(infernoDir, "contract.json"), { capabilities: [] });
38
+ const registry = readJson(path.join(infernoDir, "capabilities.json"), { capabilities: [] });
39
+ const titleById = new Map((registry.capabilities || []).map((c) => [c.id, c.title || c.id]));
40
+ return (contract.capabilities || []).map((id) => {
41
+ const title = titleById.get(id) || id;
42
+ const keywords = new Set(
43
+ `${id} ${title}`
44
+ .replace(/([A-Z])/g, " $1")
45
+ .toLowerCase()
46
+ .split(/[^a-z0-9]+/)
47
+ .filter((k) => k.length >= 4)
48
+ );
49
+ return { id, title, keywords: Array.from(keywords) };
50
+ });
51
+ }
52
+
53
+ function inferImpactedCapabilities(cwd, changedCodeFiles) {
54
+ const hints = buildCapabilityHints(cwd);
55
+ const impacted = [];
56
+ for (const hint of hints) {
57
+ const matched = [];
58
+ for (const rel of changedCodeFiles) {
59
+ const abs = path.join(cwd, rel);
60
+ const text = readFile(abs, "").toLowerCase();
61
+ if (!text) continue;
62
+ if (hint.keywords.some((k) => text.includes(k))) {
63
+ matched.push(rel);
64
+ }
65
+ }
66
+ if (matched.length) {
67
+ impacted.push({ id: hint.id, title: hint.title, matchedFiles: matched.slice(0, 5) });
68
+ }
69
+ }
70
+ return impacted;
71
+ }
72
+
73
+ export async function prImpactCommand(args = []) {
74
+ const asJson = args.includes("--json");
75
+ const cwd = process.cwd();
76
+ const base = process.env.BASE_SHA || null;
77
+ const head = process.env.HEAD_SHA || null;
78
+
79
+ let changedFiles = [];
80
+ try {
81
+ changedFiles = getChangedFiles(base, head);
82
+ } catch {
83
+ const payload = { ok: true, skipped: true, reason: "no_git_available" };
84
+ if (asJson) {
85
+ console.log(JSON.stringify(payload, null, 2));
86
+ return;
87
+ }
88
+ header("pr-impact");
89
+ warn("git not available; cannot compute PR impact");
90
+ console.log();
91
+ return;
92
+ }
93
+
94
+ const changedCodeFiles = changedFiles.filter((f) => CODE_PREFIXES.some((p) => f.startsWith(p)));
95
+ const changedInfernoFiles = changedFiles.filter((f) => f.startsWith("inferno/"));
96
+ const impactedCapabilities = inferImpactedCapabilities(cwd, changedCodeFiles);
97
+ const inferredBehaviorChange = changedCodeFiles.length > 0;
98
+ const missingInfernoUpdate = inferredBehaviorChange && changedInfernoFiles.length === 0;
99
+ const confidence = impactedCapabilities.length > 0 ? "high" : inferredBehaviorChange ? "medium" : "low";
100
+ const reasonCodes = [];
101
+ if (inferredBehaviorChange) reasonCodes.push("CODE_CHANGED");
102
+ if (missingInfernoUpdate) reasonCodes.push("INFERNO_NOT_UPDATED");
103
+ if (impactedCapabilities.length > 0) reasonCodes.push("CAPABILITY_HINT_MATCH");
104
+ if (!reasonCodes.length) reasonCodes.push("NO_BEHAVIOR_SIGNAL");
105
+
106
+ const payload = {
107
+ ok: !missingInfernoUpdate,
108
+ base: base || "HEAD",
109
+ head: head || "WORKTREE",
110
+ changedFiles,
111
+ changedCodeFiles,
112
+ changedInfernoFiles,
113
+ inferredBehaviorChange,
114
+ impactedCapabilities,
115
+ confidence,
116
+ reasonCodes,
117
+ recommendations: missingInfernoUpdate
118
+ ? ["Run infernoflow suggest \"describe behavior change\" and update inferno/", "Run infernoflow check --json"]
119
+ : ["Run infernoflow check --json to validate final state"],
120
+ };
121
+
122
+ if (asJson) {
123
+ console.log(JSON.stringify(payload, null, 2));
124
+ process.exit(payload.ok ? 0 : 1);
125
+ }
126
+
127
+ header("pr-impact");
128
+
129
+ section("Diff Scope");
130
+ ok(`Changed files: ${cyan(String(changedFiles.length))}`);
131
+ ok(`Code files: ${cyan(String(changedCodeFiles.length))}`);
132
+ ok(`Inferno files: ${cyan(String(changedInfernoFiles.length))}`);
133
+
134
+ section("Capability Impact");
135
+ if (impactedCapabilities.length === 0) {
136
+ warn("No capability hints matched changed code files");
137
+ } else {
138
+ impactedCapabilities.forEach((c) => {
139
+ console.log(` ${cyan("•")} ${c.id} ${gray(`(${c.title})`)}`);
140
+ c.matchedFiles.slice(0, 3).forEach((f) => console.log(` ${gray("- " + f)}`));
141
+ });
142
+ }
143
+
144
+ section("Doc Sync");
145
+ if (missingInfernoUpdate) {
146
+ fail("Code changed but inferno/ was not updated", "Run infernoflow suggest and then infernoflow check");
147
+ } else {
148
+ ok("No immediate inferno drift signal from changed files");
149
+ }
150
+ ok(`Confidence: ${cyan(confidence)}`);
151
+
152
+ section("Suggested Next");
153
+ payload.recommendations.forEach((r) => console.log(` ${yellow("→")} ${r}`));
154
+ console.log();
155
+ process.exit(payload.ok ? 0 : 1);
156
+ }
157
+