infernoflow 0.43.11 → 0.44.0
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/dist/bin/infernoflow.mjs +30 -33
- package/dist/lib/amp/io.mjs +8 -8
- package/dist/lib/cleanTree.mjs +12 -0
- package/dist/lib/commands/ai.mjs +2 -2
- package/dist/lib/commands/amp.mjs +4 -4
- package/dist/lib/commands/ask.mjs +2 -2
- package/dist/lib/commands/context.mjs +18 -18
- package/dist/lib/commands/doctor.mjs +2 -3
- package/dist/lib/commands/init.mjs +31 -32
- package/dist/lib/commands/log.mjs +13 -19
- package/dist/lib/commands/recap.mjs +3 -3
- package/dist/lib/commands/refresh.mjs +5 -0
- package/dist/lib/commands/status.mjs +6 -7
- package/dist/lib/commands/switch.mjs +5 -5
- package/dist/lib/commands/sync.mjs +41 -0
- package/dist/lib/git/branch.mjs +2 -0
- package/dist/lib/projectRoot.mjs +1 -0
- package/dist/lib/ruleFiles.mjs +9 -5
- package/dist/lib/upgradeCheck.mjs +1 -1
- package/dist/templates/cursor/inferno-mcp-server.mjs +170 -325
- package/package.json +13 -5
- package/dist/lib/commands/changelog.mjs +0 -21
- package/dist/lib/commands/ci.mjs +0 -3
- package/dist/lib/commands/claudeMd.mjs +0 -116
- package/dist/lib/commands/coverage.mjs +0 -2
- package/dist/lib/commands/demo.mjs +0 -113
- package/dist/lib/commands/diff.mjs +0 -5
- package/dist/lib/commands/explain.mjs +0 -8
- package/dist/lib/commands/feedback.mjs +0 -12
- package/dist/lib/commands/graph.mjs +0 -76
- package/dist/lib/commands/impact.mjs +0 -2
- package/dist/lib/commands/implement.mjs +0 -7
- package/dist/lib/commands/monorepo.mjs +0 -4
- package/dist/lib/commands/notify.mjs +0 -4
- package/dist/lib/commands/prImpact.mjs +0 -2
- package/dist/lib/commands/publish.mjs +0 -21
- package/dist/lib/commands/review.mjs +0 -24
- package/dist/lib/commands/run.mjs +0 -10
- package/dist/lib/commands/scaffold.mjs +0 -124
- package/dist/lib/commands/scan.mjs +0 -42
- package/dist/lib/commands/stability.mjs +0 -2
- package/dist/lib/commands/stats.mjs +0 -4
- package/dist/lib/commands/suggest.mjs +0 -62
- package/dist/lib/commands/syncAuto.mjs +0 -1
- package/dist/lib/commands/test.mjs +0 -6
- package/dist/lib/commands/theme.mjs +0 -18
- package/dist/lib/commands/upgrade.mjs +0 -20
- package/dist/lib/commands/watch.mjs +0 -7
- package/dist/lib/commands/why.mjs +0 -4
|
@@ -2,19 +2,127 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find the root of the infernoflow package, regardless of how this file was
|
|
12
|
+
* launched. Tried in order:
|
|
13
|
+
* 1. Walk UP from this file's own location looking for a package.json with
|
|
14
|
+
* name=infernoflow. This works whether the template is run from inside
|
|
15
|
+
* infernoflow-pkg/, from a project's .cursor/ copy (via require.resolve),
|
|
16
|
+
* or from a test temp dir.
|
|
17
|
+
* 2. require.resolve("infernoflow/package.json") — works if infernoflow is
|
|
18
|
+
* in node_modules of the CWD or one of its parents.
|
|
19
|
+
* Returns null if neither finds infernoflow.
|
|
20
|
+
*/
|
|
21
|
+
function findInfernoflowRoot() {
|
|
22
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
while (true) {
|
|
24
|
+
const pj = path.join(dir, "package.json");
|
|
25
|
+
if (fs.existsSync(pj)) {
|
|
26
|
+
try {
|
|
27
|
+
const meta = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
28
|
+
if (meta && meta.name === "infernoflow") return dir;
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
31
|
+
const parent = path.dirname(dir);
|
|
32
|
+
if (parent === dir) break;
|
|
33
|
+
dir = parent;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return path.dirname(require.resolve("infernoflow/package.json"));
|
|
37
|
+
} catch {}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const INFERNOFLOW_ROOT = findInfernoflowRoot();
|
|
5
42
|
|
|
6
43
|
function send(obj) { process.stdout.write(JSON.stringify(obj) + "\n"); }
|
|
7
44
|
function sendResult(id, result) { send({ jsonrpc: "2.0", id, result }); }
|
|
8
45
|
function sendError(id, code, message) { send({ jsonrpc: "2.0", id, error: { code, message } }); }
|
|
9
46
|
|
|
47
|
+
// ── Infernoflow resolution ─────────────────────────────────────────────────
|
|
48
|
+
// Avoid `npx infernoflow`. npx may resolve to a different (registry-fetched)
|
|
49
|
+
// version than what the user installed, which silently breaks subcommands.
|
|
50
|
+
// Resolve a deterministic location once at startup, in priority order:
|
|
51
|
+
// 1. infernoflow installed in the project's node_modules (npm i / npm link)
|
|
52
|
+
// 2. `where`/`which` the global binary
|
|
53
|
+
// Returns null if nothing is found; runCmd surfaces a clear error in that case.
|
|
54
|
+
function resolveInfernoflowBin() {
|
|
55
|
+
if (INFERNOFLOW_ROOT) {
|
|
56
|
+
for (const c of [
|
|
57
|
+
path.join(INFERNOFLOW_ROOT, "dist", "bin", "infernoflow.mjs"),
|
|
58
|
+
path.join(INFERNOFLOW_ROOT, "bin", "infernoflow.mjs"),
|
|
59
|
+
]) if (fs.existsSync(c)) return c;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const lookup = process.platform === "win32" ? "where infernoflow" : "which infernoflow";
|
|
63
|
+
const out = execSync(lookup, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
64
|
+
const first = out.split(/\r?\n/)[0];
|
|
65
|
+
if (first && fs.existsSync(first)) return first;
|
|
66
|
+
} catch {}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const INFERNOFLOW_BIN = resolveInfernoflowBin();
|
|
71
|
+
|
|
72
|
+
// In-process AMP I/O loader. When available, amp_write / amp_read bypass the
|
|
73
|
+
// CLI entirely — no subprocess, no version skew, no flag-mapping field loss.
|
|
74
|
+
// Falls back to shell-out via runCmd() if the AMP layer can't be loaded.
|
|
75
|
+
let ampIo = null;
|
|
76
|
+
let refreshRuleFiles = null;
|
|
77
|
+
if (INFERNOFLOW_ROOT) {
|
|
78
|
+
try {
|
|
79
|
+
for (const c of [
|
|
80
|
+
path.join(INFERNOFLOW_ROOT, "lib", "amp", "io.mjs"),
|
|
81
|
+
path.join(INFERNOFLOW_ROOT, "dist", "lib", "amp", "io.mjs"),
|
|
82
|
+
]) {
|
|
83
|
+
if (fs.existsSync(c)) { ampIo = await import(pathToFileURL(c).href); break; }
|
|
84
|
+
}
|
|
85
|
+
for (const c of [
|
|
86
|
+
path.join(INFERNOFLOW_ROOT, "lib", "ruleFiles.mjs"),
|
|
87
|
+
path.join(INFERNOFLOW_ROOT, "dist", "lib", "ruleFiles.mjs"),
|
|
88
|
+
]) {
|
|
89
|
+
if (fs.existsSync(c)) { refreshRuleFiles = (await import(pathToFileURL(c).href)).refreshRuleFilesFromMemory; break; }
|
|
90
|
+
}
|
|
91
|
+
} catch { /* swallow — fallback path handles it */ }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Clean-tree policy: regenerate rule files ONCE at boot ──────────────────
|
|
95
|
+
// Historically, every amp_write rewrote CLAUDE.md / .cursorrules. That
|
|
96
|
+
// dirtied tracked files dozens of times per session and blocked git
|
|
97
|
+
// checkout. Now we regenerate exactly once when the MCP server starts —
|
|
98
|
+
// enough for the next AI session to boot warm — and never again during
|
|
99
|
+
// the session. amp_read serves runtime queries; rule-file content is
|
|
100
|
+
// for cold-start injection only.
|
|
101
|
+
if (refreshRuleFiles) {
|
|
102
|
+
try { refreshRuleFiles(process.cwd()); } catch { /* non-fatal */ }
|
|
103
|
+
}
|
|
104
|
+
|
|
10
105
|
/**
|
|
11
106
|
* Run the infernoflow CLI. Returns either the stdout string OR a structured
|
|
12
107
|
* error object so call sites can decide whether to surface it via JSON-RPC
|
|
13
108
|
* sendError() instead of returning gibberish text to the agent.
|
|
14
109
|
*/
|
|
15
110
|
function runCmd(args, env = {}) {
|
|
111
|
+
if (!INFERNOFLOW_BIN) {
|
|
112
|
+
return {
|
|
113
|
+
__error: true,
|
|
114
|
+
message: "infernoflow not installed — install it locally (`npm i infernoflow`) or globally (`npm i -g infernoflow`)",
|
|
115
|
+
stderr: "",
|
|
116
|
+
stdout: "",
|
|
117
|
+
status: 127,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
16
120
|
try {
|
|
17
|
-
|
|
121
|
+
const isMjs = INFERNOFLOW_BIN.toLowerCase().endsWith(".mjs");
|
|
122
|
+
const cmd = isMjs
|
|
123
|
+
? `"${process.execPath}" "${INFERNOFLOW_BIN}" ${args}`
|
|
124
|
+
: `"${INFERNOFLOW_BIN}" ${args}`;
|
|
125
|
+
return execSync(cmd, {
|
|
18
126
|
encoding: "utf8",
|
|
19
127
|
cwd: process.cwd(),
|
|
20
128
|
timeout: 30000,
|
|
@@ -36,26 +144,27 @@ function isCmdError(result) {
|
|
|
36
144
|
return typeof result === "object" && result !== null && result.__error === true;
|
|
37
145
|
}
|
|
38
146
|
|
|
147
|
+
// ── MCP tool surface after Phase 4 truth audit ────────────────────────────
|
|
148
|
+
// Mission: session memory. The off-mission contract-iteration tools
|
|
149
|
+
// (infernoflow_run / _apply / _implement / _review / _scan_ui) were cut —
|
|
150
|
+
// they duplicated CLI commands that are themselves gone, and the fragile
|
|
151
|
+
// env-var subprocess handoff for _apply was a recurring bug source.
|
|
152
|
+
// What remains is everything an AI agent needs to capture, query, and
|
|
153
|
+
// hand off memory between sessions, plus the two read-only contract
|
|
154
|
+
// helpers that pair cleanly with the kept CLI surface.
|
|
39
155
|
const TOOLS = [
|
|
40
|
-
|
|
41
|
-
{ name: "infernoflow_apply", description: "Apply an infernoflow suggestion JSON returned by the agent. Call this after responding to infernoflow_run.", inputSchema: { type: "object", properties: { json: { type: "string", description: "The JSON suggestion from the agent" } }, required: ["json"] } },
|
|
42
|
-
{ name: "infernoflow_check", description: "Validate infernoflow contract and capabilities", inputSchema: { type: "object", properties: {} } },
|
|
43
|
-
{ name: "infernoflow_status", description: "Show contract health at a glance", inputSchema: { type: "object", properties: {} } },
|
|
44
|
-
{ name: "infernoflow_context", description: "Generate AI-ready context", inputSchema: { type: "object", properties: { intent: { type: "string" }, working: { type: "string" } } } },
|
|
45
|
-
{ name: "infernoflow_git_drift", description: "Detect which capabilities may be affected by recent code changes. Compares git-changed files to the capability registry and returns suggestions for contract updates.", inputSchema: { type: "object", properties: { sinceCommits: { type: "number", description: "How many commits back to check (default: 1)" } } } },
|
|
46
|
-
{ name: "infernoflow_implement", description: "Generate a structured code implementation prompt for a task. Uses the contract and stack context to produce step-by-step coding instructions for the agent.", inputSchema: { type: "object", properties: { task: { type: "string", description: "What to implement" }, mode: { type: "string", enum: ["cursor", "generic", "both"], description: "Prompt style (default: both)" } }, required: ["task"] } },
|
|
47
|
-
{ name: "infernoflow_scan_ui", description: "Scan components and styles for UI changes vs the stored contract. Returns new/changed components, design token changes, and suggested contract updates.", inputSchema: { type: "object", properties: {} } },
|
|
48
|
-
{ name: "infernoflow_review", description: "Pre-merge capability drift check. Compares all changed files in the current branch against the capability contract and reports drift risk before you merge.", inputSchema: { type: "object", properties: { branch: { type: "string", description: "Branch to compare against (default: main)" } } } },
|
|
49
|
-
|
|
50
|
-
// ── AMP-spec MCP tools (per docs/protocol/PROTOCOL.md §7.3) ────────────────
|
|
51
|
-
// These are the standard names any AMP-compliant MCP server should expose.
|
|
52
|
-
// They're thin wrappers around the existing infernoflow_* tools so AMP-only
|
|
53
|
-
// clients don't need to know the infernoflow_ vendor prefix.
|
|
156
|
+
// ── AMP-spec memory tools (the product) ──────────────────────────────────
|
|
54
157
|
{ name: "amp_read", description: "AMP: read session memory entries with optional filters.", inputSchema: { type: "object", properties: { file: { type: "string" }, type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] }, query: { type: "string" }, limit: { type: "number" } } } },
|
|
55
158
|
{ name: "amp_write", description: "AMP: log a new entry. Required: type + msg. Optional: file, line, tags.", inputSchema: { type: "object", properties: { type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] }, msg: { type: "string" }, file: { type: "string" }, line: { type: "number" }, tags: { type: "array", items: { type: "string" } } }, required: ["type","msg"] } },
|
|
56
|
-
{ name: "amp_handoff", description: "AMP: generate the handoff document for the next AI session. format=markdown|json (default: markdown).", inputSchema: { type: "object", properties: { format: { type: "string", enum: ["markdown","json"] } } } },
|
|
57
159
|
{ name: "amp_search", description: "AMP: search entries by keyword. Optional type filter.", inputSchema: { type: "object", properties: { query: { type: "string" }, type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] } }, required: ["query"] } },
|
|
160
|
+
{ name: "amp_handoff", description: "AMP: generate the handoff document for the next AI session. format=markdown|json (default: markdown).", inputSchema: { type: "object", properties: { format: { type: "string", enum: ["markdown","json"] } } } },
|
|
58
161
|
{ name: "amp_health", description: "AMP: get the session health score (0-100, A-F grade).", inputSchema: { type: "object", properties: {} } },
|
|
162
|
+
|
|
163
|
+
// ── Read-only contract helpers ───────────────────────────────────────────
|
|
164
|
+
{ name: "infernoflow_status", description: "Show project memory + contract health at a glance.", inputSchema: { type: "object", properties: {} } },
|
|
165
|
+
{ name: "infernoflow_check", description: "Validate the capability contract (read-only).", inputSchema: { type: "object", properties: {} } },
|
|
166
|
+
{ name: "infernoflow_context", description: "Generate AI-ready context for a task.", inputSchema: { type: "object", properties: { intent: { type: "string" }, working: { type: "string" } } } },
|
|
167
|
+
{ name: "infernoflow_git_drift", description: "Detect which capabilities may be affected by recent code changes — useful when memory needs branch-aware revalidation.", inputSchema: { type: "object", properties: { sinceCommits: { type: "number", description: "How many commits back to check (default: 1)" } } } },
|
|
59
168
|
];
|
|
60
169
|
|
|
61
170
|
// ── git drift detection (inline — no external imports in this template file) ─
|
|
@@ -173,305 +282,11 @@ function detectGitDrift(sinceCommits) {
|
|
|
173
282
|
return lines.join("\n");
|
|
174
283
|
}
|
|
175
284
|
|
|
176
|
-
// ── infernoflow_scan_ui ────────────────────────────────────────────────────
|
|
177
|
-
function scanUi() {
|
|
178
|
-
const cwd = process.cwd();
|
|
179
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
180
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
181
|
-
if (!fs.existsSync(contractPath)) return "inferno/ not found — run infernoflow init first";
|
|
182
|
-
|
|
183
|
-
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
184
|
-
const storedUi = contract.ui || {};
|
|
185
|
-
|
|
186
|
-
// Collect style + component files
|
|
187
|
-
const styleExts = /\.(css|scss|sass|less|ts|tsx|js|jsx|html)$/;
|
|
188
|
-
const SKIP = new Set(["node_modules", ".git", "dist", "build", ".angular", ".next", "vendor", "coverage"]);
|
|
189
|
-
const files = [];
|
|
190
|
-
const walk = (dir) => {
|
|
191
|
-
try {
|
|
192
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
193
|
-
const full = path.join(dir, entry.name);
|
|
194
|
-
if (entry.isDirectory()) { if (!SKIP.has(entry.name)) walk(full); }
|
|
195
|
-
else if (styleExts.test(entry.name) && !entry.name.includes(".min.") && !entry.name.endsWith(".map")) files.push(full);
|
|
196
|
-
}
|
|
197
|
-
} catch {}
|
|
198
|
-
};
|
|
199
|
-
for (const root of ["src", "app", "frontend", "components", "styles"]) {
|
|
200
|
-
const p = path.join(cwd, root);
|
|
201
|
-
if (fs.existsSync(p)) walk(p);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Extract current components from TS/TSX files
|
|
205
|
-
const currentComponents = new Set();
|
|
206
|
-
const currentTokens = new Set();
|
|
207
|
-
|
|
208
|
-
for (const f of files) {
|
|
209
|
-
const text = fs.existsSync(f) ? fs.readFileSync(f, "utf8") : "";
|
|
210
|
-
// Components
|
|
211
|
-
for (const m of text.matchAll(/@Component[\s\S]*?class\s+([A-Z][A-Za-z0-9_]*Component)/g)) currentComponents.add(m[1].replace(/Component$/, ""));
|
|
212
|
-
for (const m of text.matchAll(/export\s+(?:default\s+)?function\s+([A-Z][A-Za-z0-9_]*)/g)) currentComponents.add(m[1]);
|
|
213
|
-
// Design tokens
|
|
214
|
-
for (const m of text.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g)) currentTokens.add(`--${m[1]}`);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const storedComponents = new Set(storedUi.components || []);
|
|
218
|
-
const storedTokens = new Set(storedUi.designTokens || []);
|
|
219
|
-
|
|
220
|
-
const newComponents = [...currentComponents].filter(c => !storedComponents.has(c));
|
|
221
|
-
const removedComponents = [...storedComponents].filter(c => !currentComponents.has(c));
|
|
222
|
-
const newTokens = [...currentTokens].filter(t => !storedTokens.has(t));
|
|
223
|
-
const removedTokens = [...storedTokens].filter(t => !currentTokens.has(t));
|
|
224
|
-
|
|
225
|
-
const lines = ["## infernoflow UI scan report", ""];
|
|
226
|
-
|
|
227
|
-
if (!newComponents.length && !removedComponents.length && !newTokens.length && !removedTokens.length) {
|
|
228
|
-
lines.push("✔ No UI changes detected since last scan.");
|
|
229
|
-
return lines.join("\n");
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (newComponents.length) {
|
|
233
|
-
lines.push(`### New components (${newComponents.length})`);
|
|
234
|
-
newComponents.slice(0, 15).forEach(c => lines.push(` + ${c}`));
|
|
235
|
-
lines.push("");
|
|
236
|
-
}
|
|
237
|
-
if (removedComponents.length) {
|
|
238
|
-
lines.push(`### Removed components (${removedComponents.length})`);
|
|
239
|
-
removedComponents.slice(0, 10).forEach(c => lines.push(` - ${c}`));
|
|
240
|
-
lines.push("");
|
|
241
|
-
}
|
|
242
|
-
if (newTokens.length) {
|
|
243
|
-
lines.push(`### New design tokens (${newTokens.length})`);
|
|
244
|
-
newTokens.slice(0, 10).forEach(t => lines.push(` + ${t}`));
|
|
245
|
-
lines.push("");
|
|
246
|
-
}
|
|
247
|
-
if (removedTokens.length) {
|
|
248
|
-
lines.push(`### Removed design tokens (${removedTokens.length})`);
|
|
249
|
-
removedTokens.slice(0, 10).forEach(t => lines.push(` - ${t}`));
|
|
250
|
-
lines.push("");
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
lines.push("### Suggested action");
|
|
254
|
-
if (newComponents.length) {
|
|
255
|
-
const newCaps = newComponents.slice(0, 5).map(c => `View${c}`).join(", ");
|
|
256
|
-
lines.push(`Consider adding these capabilities: ${newCaps}`);
|
|
257
|
-
lines.push(`Call infernoflow_run with task "add UI capabilities for new components: ${newComponents.slice(0,3).join(", ")}" to update the contract.`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return lines.join("\n");
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ── infernoflow_review ─────────────────────────────────────────────────────
|
|
264
|
-
function reviewDrift(baseBranch) {
|
|
265
|
-
const cwd = process.cwd();
|
|
266
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
267
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
268
|
-
if (!fs.existsSync(contractPath)) return "inferno/ not found — run infernoflow init first";
|
|
269
|
-
|
|
270
|
-
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
271
|
-
|
|
272
|
-
// Get changed files vs base branch
|
|
273
|
-
const runGit = (cmd) => { try { return execSync(cmd, { cwd, encoding: "utf8", timeout: 15_000 }); } catch { return ""; } };
|
|
274
|
-
|
|
275
|
-
const diffOutput = runGit(`git diff --name-only ${baseBranch}...HEAD`);
|
|
276
|
-
const changedFiles = diffOutput.split("\n").map(l => l.trim()).filter(Boolean);
|
|
277
|
-
|
|
278
|
-
if (!changedFiles.length) return `No changes detected vs ${baseBranch}. Safe to merge.`;
|
|
279
|
-
|
|
280
|
-
// Categorise changed files
|
|
281
|
-
const infraFiles = changedFiles.filter(f => /\.(json|yaml|yml|env|config|lock)$/.test(f) || f.includes("inferno/"));
|
|
282
|
-
const sourceFiles = changedFiles.filter(f => /\.(ts|tsx|js|jsx|mjs|cs|py|go|java)$/.test(f));
|
|
283
|
-
const styleFiles = changedFiles.filter(f => /\.(css|scss|sass|less)$/.test(f));
|
|
284
|
-
const contractChanged = changedFiles.some(f => f.startsWith("inferno/"));
|
|
285
|
-
|
|
286
|
-
// Keyword-based drift detection on changed source files
|
|
287
|
-
const HEURISTICS = [
|
|
288
|
-
{ kw: ["search"], id: "SearchItems" }, { kw: ["filter"], id: "FilterItems" },
|
|
289
|
-
{ kw: ["auth", "login", "logout"], id: "Authentication" },
|
|
290
|
-
{ kw: ["create", "add", "new"], id: "CreateItem" },
|
|
291
|
-
{ kw: ["update", "edit", "patch"], id: "UpdateItem" },
|
|
292
|
-
{ kw: ["delete", "remove"], id: "DeleteItem" },
|
|
293
|
-
{ kw: ["list", "read", "fetch", "get"], id: "ReadItems" },
|
|
294
|
-
{ kw: ["due", "deadline"], id: "SetDueDate" },
|
|
295
|
-
{ kw: ["priority"], id: "SetPriority" },
|
|
296
|
-
{ kw: ["complete", "toggle"], id: "ToggleComplete" },
|
|
297
|
-
{ kw: ["export", "download"], id: "ExportData" },
|
|
298
|
-
{ kw: ["import", "upload"], id: "ImportData" },
|
|
299
|
-
{ kw: ["notify", "notification", "email"], id: "SendNotification" },
|
|
300
|
-
{ kw: ["payment", "checkout", "stripe"], id: "ProcessPayment" },
|
|
301
|
-
];
|
|
302
|
-
|
|
303
|
-
const capHits = new Map();
|
|
304
|
-
const registeredCaps = new Set(contract.capabilities || []);
|
|
305
|
-
|
|
306
|
-
for (const file of sourceFiles) {
|
|
307
|
-
const lower = file.toLowerCase();
|
|
308
|
-
for (const rule of HEURISTICS) {
|
|
309
|
-
if (rule.kw.some(k => lower.includes(k))) {
|
|
310
|
-
if (!capHits.has(rule.id)) capHits.set(rule.id, []);
|
|
311
|
-
capHits.get(rule.id).push(file);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const newCapSignals = [...capHits.entries()].filter(([id]) => !registeredCaps.has(id));
|
|
317
|
-
const existingCapSignals = [...capHits.entries()].filter(([id]) => registeredCaps.has(id));
|
|
318
|
-
|
|
319
|
-
const lines = [
|
|
320
|
-
`## infernoflow PR review — drift check vs \`${baseBranch}\``,
|
|
321
|
-
`Changed files: ${changedFiles.length} | Source: ${sourceFiles.length} | Styles: ${styleFiles.length} | Infra: ${infraFiles.length}`,
|
|
322
|
-
"",
|
|
323
|
-
];
|
|
324
|
-
|
|
325
|
-
// Risk assessment
|
|
326
|
-
let riskLevel = "LOW";
|
|
327
|
-
if (newCapSignals.length > 0) riskLevel = "MEDIUM";
|
|
328
|
-
if (newCapSignals.length >= 3 || (newCapSignals.length >= 1 && !contractChanged)) riskLevel = "HIGH";
|
|
329
|
-
|
|
330
|
-
const riskEmoji = riskLevel === "HIGH" ? "🔴" : riskLevel === "MEDIUM" ? "🟡" : "🟢";
|
|
331
|
-
lines.push(`### ${riskEmoji} Drift risk: ${riskLevel}`);
|
|
332
|
-
lines.push("");
|
|
333
|
-
|
|
334
|
-
if (contractChanged) {
|
|
335
|
-
lines.push("✔ inferno/ contract files were updated in this PR — good practice.");
|
|
336
|
-
lines.push("");
|
|
337
|
-
} else if (sourceFiles.length > 0) {
|
|
338
|
-
lines.push("⚠ Source files changed but inferno/ contract was NOT updated.");
|
|
339
|
-
lines.push(" Consider running: infernoflow_run to check if capabilities need updating.");
|
|
340
|
-
lines.push("");
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (newCapSignals.length > 0) {
|
|
344
|
-
lines.push(`### Possible new capabilities (not in contract):`);
|
|
345
|
-
for (const [id, files] of newCapSignals.slice(0, 6)) {
|
|
346
|
-
lines.push(` - **${id}** — suggested by: ${files.slice(0,2).join(", ")}`);
|
|
347
|
-
}
|
|
348
|
-
lines.push("");
|
|
349
|
-
lines.push(`Suggested action: call infernoflow_run with task "review new capabilities: ${newCapSignals.slice(0,3).map(([id])=>id).join(', ')}"`);
|
|
350
|
-
lines.push("");
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (existingCapSignals.length > 0) {
|
|
354
|
-
lines.push(`### Existing capabilities touched:`);
|
|
355
|
-
for (const [id, files] of existingCapSignals.slice(0, 6)) {
|
|
356
|
-
lines.push(` - **${id}** — ${files.slice(0,2).join(", ")}`);
|
|
357
|
-
}
|
|
358
|
-
lines.push("");
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (styleFiles.length > 0) {
|
|
362
|
-
lines.push(`### Style changes (${styleFiles.length} files) — run infernoflow_scan_ui to check UI contract`);
|
|
363
|
-
styleFiles.slice(0, 5).forEach(f => lines.push(` - ${f}`));
|
|
364
|
-
lines.push("");
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (riskLevel === "LOW" && !newCapSignals.length) {
|
|
368
|
-
lines.push("✔ No new capability signals detected. Safe to merge (run infernoflow_check as final gate).");
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return lines.join("\n");
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function buildImplementPrompt(task, mode) {
|
|
375
|
-
const cwd = process.cwd();
|
|
376
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
377
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
378
|
-
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
379
|
-
const profilePath = path.join(infernoDir, "developer-profile.json");
|
|
380
|
-
|
|
381
|
-
if (!fs.existsSync(contractPath)) return "inferno/ not found — run infernoflow init first";
|
|
382
|
-
|
|
383
|
-
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
384
|
-
const caps = fs.existsSync(capsPath) ? JSON.parse(fs.readFileSync(capsPath, "utf8")) : {};
|
|
385
|
-
const profile = fs.existsSync(profilePath) ? JSON.parse(fs.readFileSync(profilePath, "utf8")) : {};
|
|
386
|
-
|
|
387
|
-
const capList = (caps.capabilities || []).map(c => ` - ${c.id}: ${c.title || c.id}`).join("\n");
|
|
388
|
-
const stack = profile.stack || {};
|
|
389
|
-
const stackLine = [stack.framework, stack.language, stack.projectType].filter(Boolean).join(" / ") || "unknown";
|
|
390
|
-
const namingStyle = profile.namingStyle || "PascalCase";
|
|
391
|
-
|
|
392
|
-
const cursorPrompt = `## Cursor Agent Implementation Prompt
|
|
393
|
-
Task: "${task}"
|
|
394
|
-
Project: ${contract.policyId} (${stackLine})
|
|
395
|
-
Naming convention: ${namingStyle}
|
|
396
|
-
|
|
397
|
-
### Current capabilities
|
|
398
|
-
${capList || " (none registered)"}
|
|
399
|
-
|
|
400
|
-
### Implementation instructions
|
|
401
|
-
1. Implement "${task}" following the existing code patterns in this project
|
|
402
|
-
2. Use ${namingStyle} for any new identifiers, matching the existing capability naming
|
|
403
|
-
3. Keep changes minimal — only touch files relevant to this task
|
|
404
|
-
4. After implementing, call \`infernoflow_run\` with task "${task}" to update the contract
|
|
405
|
-
5. Then call \`infernoflow_check\` to validate everything is in sync
|
|
406
|
-
|
|
407
|
-
### Definition of done
|
|
408
|
-
- Feature works as described
|
|
409
|
-
- Contract updated via infernoflow_run → infernoflow_apply
|
|
410
|
-
- infernoflow_check passes`;
|
|
411
|
-
|
|
412
|
-
const genericPrompt = `## Implementation Prompt
|
|
413
|
-
Task: "${task}"
|
|
414
|
-
Project: ${contract.policyId}
|
|
415
|
-
Stack: ${stackLine}
|
|
416
|
-
Capabilities already in contract: ${(contract.capabilities || []).join(", ")}
|
|
417
|
-
|
|
418
|
-
Implement the task above. When done, run:
|
|
419
|
-
infernoflow suggest "${task}"
|
|
420
|
-
infernoflow check`;
|
|
421
|
-
|
|
422
|
-
if (mode === "cursor") return cursorPrompt;
|
|
423
|
-
if (mode === "generic") return genericPrompt;
|
|
424
|
-
return cursorPrompt + "\n\n---\n\n" + genericPrompt;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function buildPrompt(task) {
|
|
428
|
-
const infernoDir = path.join(process.cwd(), "inferno");
|
|
429
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
430
|
-
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
431
|
-
if (!fs.existsSync(contractPath)) return null;
|
|
432
|
-
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
433
|
-
const caps = fs.existsSync(capsPath) ? JSON.parse(fs.readFileSync(capsPath, "utf8")) : {};
|
|
434
|
-
const capList = (caps.capabilities || []).map(c => ` - ${c.id}: ${c.title || c.id}`).join("\n");
|
|
435
|
-
return `You are a developer assistant for the infernoflow CLI tool.
|
|
436
|
-
Analyze this task and suggest updates to the infernoflow contract files.
|
|
437
|
-
|
|
438
|
-
## Current contract
|
|
439
|
-
policyId: ${contract.policyId}
|
|
440
|
-
policyVersion: ${contract.policyVersion}
|
|
441
|
-
capabilities: [${(contract.capabilities || []).join(", ")}]
|
|
442
|
-
|
|
443
|
-
## Capabilities registry
|
|
444
|
-
${capList || " (none)"}
|
|
445
|
-
|
|
446
|
-
## Task
|
|
447
|
-
"${task}"
|
|
448
|
-
|
|
449
|
-
## Instructions
|
|
450
|
-
Respond with ONLY a valid JSON object:
|
|
451
|
-
{
|
|
452
|
-
"summary": "one-line summary of what changed",
|
|
453
|
-
"newCapabilities": [{ "id": "PascalCase", "title": "Human readable title", "reason": "why this is new" }],
|
|
454
|
-
"removedCapabilities": [],
|
|
455
|
-
"updatedScenarios": [],
|
|
456
|
-
"changelogEntry": "- Short description for CHANGELOG.md"
|
|
457
|
-
}`;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
285
|
function handleTool(id, name, input) {
|
|
461
286
|
try {
|
|
462
287
|
let text = "";
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (!prompt) { sendError(id, -32000, "inferno/ not found — run infernoflow init first"); return; }
|
|
466
|
-
const promptFile = path.join(process.cwd(), "inferno", "agent-prompt.md");
|
|
467
|
-
fs.writeFileSync(promptFile, prompt, "utf8");
|
|
468
|
-
text = `## infernoflow task: "${input.task}"\n\n${prompt}\n\n---\nRespond with the JSON, then call **infernoflow_apply** with your JSON string.`;
|
|
469
|
-
} else if (name === "infernoflow_apply") {
|
|
470
|
-
const responseFile = path.join(process.cwd(), "inferno", "agent-response.json");
|
|
471
|
-
let json = input.json.trim().replace(/^```json?\n?/, "").replace(/\n?```$/, "");
|
|
472
|
-
fs.writeFileSync(responseFile, json, "utf8");
|
|
473
|
-
text = runCmd(`run "apply"`, { INFERNO_AGENT_RESPONSE_FILE: responseFile, INFERNO_AGENT_AVAILABLE: "1" });
|
|
474
|
-
} else if (name === "infernoflow_check") {
|
|
288
|
+
// ── Read-only contract helpers ─────────────────────────────────────────
|
|
289
|
+
if (name === "infernoflow_check") {
|
|
475
290
|
text = runCmd("check");
|
|
476
291
|
} else if (name === "infernoflow_status") {
|
|
477
292
|
text = runCmd("status");
|
|
@@ -482,14 +297,8 @@ function handleTool(id, name, input) {
|
|
|
482
297
|
text = runCmd("context " + parts.join(" "));
|
|
483
298
|
} else if (name === "infernoflow_git_drift") {
|
|
484
299
|
text = detectGitDrift(input.sinceCommits || 1);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
} else if (name === "infernoflow_scan_ui") {
|
|
488
|
-
text = scanUi();
|
|
489
|
-
} else if (name === "infernoflow_review") {
|
|
490
|
-
text = reviewDrift(input.branch || "main");
|
|
491
|
-
|
|
492
|
-
// ── AMP-spec aliases ───────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
// ── AMP-spec memory tools ──────────────────────────────────────────────
|
|
493
302
|
} else if (name === "amp_read") {
|
|
494
303
|
const args = [];
|
|
495
304
|
if (input.query) args.push(JSON.stringify(input.query));
|
|
@@ -497,11 +306,47 @@ function handleTool(id, name, input) {
|
|
|
497
306
|
if (input.limit) args.push("--limit", String(input.limit));
|
|
498
307
|
text = runCmd("ask " + args.join(" "));
|
|
499
308
|
} else if (name === "amp_write") {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (
|
|
504
|
-
|
|
309
|
+
// Prefer in-process write: no subprocess, no `npx` version skew, and
|
|
310
|
+
// file/line/tags reach disk unchanged. The CLI fallback below is only
|
|
311
|
+
// used when infernoflow's AMP layer can't be imported.
|
|
312
|
+
if (ampIo) {
|
|
313
|
+
const entry = {
|
|
314
|
+
ts: new Date().toISOString(),
|
|
315
|
+
type: input.type || "note",
|
|
316
|
+
summary: input.msg || "",
|
|
317
|
+
agent: process.env.INFERNOFLOW_AGENT
|
|
318
|
+
|| (process.env.CLAUDE_CODE_SESSION ? "claude"
|
|
319
|
+
: process.env.CURSOR_SESSION ? "cursor"
|
|
320
|
+
: process.env.COPILOT_SESSION ? "copilot"
|
|
321
|
+
: "claude"),
|
|
322
|
+
};
|
|
323
|
+
if (input.file) entry.file = input.file;
|
|
324
|
+
if (input.line) entry.line = input.line;
|
|
325
|
+
if (input.tags && input.tags.length) entry.tags = input.tags;
|
|
326
|
+
try {
|
|
327
|
+
const written = ampIo.appendEntry(process.cwd(), entry);
|
|
328
|
+
// NOTE: rule-file refresh deliberately NOT called here — clean-tree
|
|
329
|
+
// policy regenerates them once at MCP boot only. Doing it on every
|
|
330
|
+
// write dirties tracked files and blocks `git checkout`. Within a
|
|
331
|
+
// session, the agent uses amp_read for fresh queries; rule files
|
|
332
|
+
// are for cold-start injection of the *next* session.
|
|
333
|
+
text = `✔ Logged [${written.type}] ${written.id}\n msg: ${written.msg}` +
|
|
334
|
+
(written.file ? `\n file: ${written.file}${written.line ? ":" + written.line : ""}` : "") +
|
|
335
|
+
(written.tags ? `\n tags: ${written.tags.join(", ")}` : "");
|
|
336
|
+
} catch (err) {
|
|
337
|
+
return sendError(id, -32000, `amp_write failed (in-process): ${err.message}`);
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
// Fallback: shell-out. Pass --file/--line/--tags through the CLI so
|
|
341
|
+
// they're not silently dropped like in the original implementation.
|
|
342
|
+
const t = (input.type || "note").replace(/[^a-z]/g, "");
|
|
343
|
+
const m = JSON.stringify(input.msg || "");
|
|
344
|
+
const extras = [];
|
|
345
|
+
if (input.file) extras.push("--file", JSON.stringify(input.file));
|
|
346
|
+
if (input.line) extras.push("--line", String(input.line));
|
|
347
|
+
if (input.tags && input.tags.length) extras.push("--tags", JSON.stringify(input.tags.join(",")));
|
|
348
|
+
text = runCmd(`log ${m} --type ${t} ${extras.join(" ")}`);
|
|
349
|
+
}
|
|
505
350
|
} else if (name === "amp_handoff") {
|
|
506
351
|
// switch writes a file; we read it back to return the content
|
|
507
352
|
const switchResult = runCmd("switch");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infernoflow",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Persistent memory for AI coding sessions
|
|
3
|
+
"version": "0.44.0",
|
|
4
|
+
"description": "Persistent memory for AI coding sessions — captures what agents can't infer from code alone. Works with Copilot, Cursor, Claude, and Windsurf.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"infernoflow": "dist/bin/infernoflow.mjs"
|
|
@@ -16,13 +16,17 @@
|
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "
|
|
19
|
+
"test": "tsc --noEmit && vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"test:vitest": "vitest run",
|
|
22
|
+
"test:smoke": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs",
|
|
23
|
+
"test:install": "cross-env RUN_INSTALL_TESTS=1 vitest run tests/tarball-install.test.mjs",
|
|
20
24
|
"test:help": "node bin/infernoflow.mjs --help",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
21
26
|
"build": "node build.mjs",
|
|
22
27
|
"prepublishOnly": "echo skipping build",
|
|
23
28
|
"inferno:promote-draft": "node scripts/inferno-promote-draft.mjs"
|
|
24
29
|
},
|
|
25
|
-
"dependencies": {},
|
|
26
30
|
"keywords": [
|
|
27
31
|
"ai",
|
|
28
32
|
"ai-memory",
|
|
@@ -56,6 +60,10 @@
|
|
|
56
60
|
"url": "https://github.com/ronmiz/infernoflow/issues"
|
|
57
61
|
},
|
|
58
62
|
"devDependencies": {
|
|
59
|
-
"
|
|
63
|
+
"@types/node": "^25.9.0",
|
|
64
|
+
"cross-env": "^10.1.0",
|
|
65
|
+
"esbuild": "^0.28.0",
|
|
66
|
+
"typescript": "^6.0.3",
|
|
67
|
+
"vitest": "^4.1.6"
|
|
60
68
|
}
|
|
61
69
|
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import*as g from"node:fs";import*as S from"node:path";import{execSync as v}from"node:child_process";import{header as O,ok as R,fail as C,warn as m,info as b,done as G,bold as E,cyan as h,gray as f,green as F,yellow as L}from"../ui/output.mjs";function p(e,n){try{return v(e,{cwd:n,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()}catch{return null}}function y(e){return p("git describe --tags --abbrev=0",e)}function $(e,n){const o=e?`${e}..HEAD`:"",t=p(`git log ${o} --format=%H%x1f%s%x1f%b%x1e`,n);return t?t.split("").map(s=>s.trim()).filter(Boolean).map(s=>{const r=s.split("");return{hash:(r[0]||"").trim().slice(0,8),subject:(r[1]||"").trim(),body:(r[2]||"").trim()}}).filter(s=>s.subject):[]}function M(e,n){return p(`git log -1 --format=%ci "${e}"`,n)?.slice(0,10)||null}const w={feat:"Added",feature:"Added",add:"Added",fix:"Fixed",bugfix:"Fixed",hotfix:"Fixed",perf:"Changed",refactor:"Changed",change:"Changed",chore:"Changed",docs:"Changed",style:"Changed",test:"Changed",ci:"Changed",remove:"Removed",revert:"Removed",deprecate:"Removed"};function x(e){const n=e.match(/^(\w+)(?:\([^)]+\))?[!]?:\s*(.+)/);if(n){const t=n[1].toLowerCase();return{section:w[t]||"Changed",message:n[2].trim(),breaking:e.includes("!")}}const o=e.toLowerCase();for(const[t,s]of Object.entries(w))if(o.startsWith(t+" ")||o.startsWith(t+":"))return{section:s,message:e,breaking:!1};return{section:"Changed",message:e,breaking:!1}}function j(e){const n={Added:[],Fixed:[],Changed:[],Removed:[],Breaking:[]};for(const o of e){const{section:t,message:s,breaking:r}=x(o.subject);r&&n.Breaking.push(s),n[t].push(s)}for(const o of Object.keys(n))n[o]=[...new Set(n[o])];return n}function H(e,n){const o=["## Unreleased",""];n&&o.push(`> Changes since ${n}`,"");const t=["Breaking","Added","Fixed","Changed","Removed"];let s=!1;for(const r of t){const i=e[r];if(!(!i||!i.length)){s=!0,o.push(`### ${r}`);for(const c of i)o.push(`- ${c}`);o.push("")}}return s||o.push("- No significant changes",""),o.join(`
|
|
2
|
-
`)}function k(e){return g.existsSync(e)?g.readFileSync(e,"utf8"):null}function D(e){const n=e.match(/^## Unreleased[\s\S]*?(?=\n## |\n---|\z)/im);return n?n[0].trim():null}function U(e,n){return/^## Unreleased/im.test(e)?e.replace(/^## Unreleased[\s\S]*?(?=\n## |\n---)/im,n+`
|
|
3
|
-
|
|
4
|
-
`):/^# .+/im.test(e)?e.replace(/^(# .+\n)/im,`$1
|
|
5
|
-
${n}
|
|
6
|
-
|
|
7
|
-
`):`${n}
|
|
8
|
-
|
|
9
|
-
${e}`}function J(e,n){const o=n.split(`
|
|
10
|
-
`).filter(t=>t.startsWith("- ")).join(`
|
|
11
|
-
`);return o?/^## Unreleased/im.test(e)?e.replace(/(^## Unreleased[\s\S]*?)(\n## )/im,`$1
|
|
12
|
-
${o}
|
|
13
|
-
$2`):U(e,n):e}function W(e,n){const o=n||y(e),t=$(o,e);if(!t.length){b(`No commits since ${o||"beginning"}`);return}console.log(`
|
|
14
|
-
${E("Commits since")} ${h(o||"beginning")} ${f("("+t.length+")")}
|
|
15
|
-
`);for(const s of t){const{section:r}=x(s.subject),i=r==="Added"?F:r==="Fixed"?L:f;console.log(` ${f(s.hash)} ${i(s.subject)}`)}console.log()}function B(e){const n=k(e);if(!n){C("CHANGELOG.md not found");return}const o=D(n);if(!o){m("No ## Unreleased section found in CHANGELOG.md");return}console.log(`
|
|
16
|
-
`+o+`
|
|
17
|
-
`)}async function T(e,n,o){const{ref:t,dryRun:s,append:r,asJson:i}=o,c=t||y(e),a=$(c,e);if(!a.length){if(i){console.log(JSON.stringify({ok:!0,ref:c,commits:0,message:"No new commits"}));return}m(`No commits found since ${c||"beginning of repo"}`),console.log();return}const d=j(a),l=H(d,c);if(i){console.log(JSON.stringify({ok:!0,ref:c,commits:a.length,sections:{breaking:d.Breaking,added:d.Added,fixed:d.Fixed,changed:d.Changed,removed:d.Removed},markdown:l},null,2));return}if(console.log(),console.log(f(" \u2500\u2500\u2500 Drafted entry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),l.split(`
|
|
18
|
-
`).forEach(N=>console.log(" "+N)),console.log(f(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(),b(`${a.length} commit${a.length>1?"s":""} since ${h(c||"beginning")}`),s){m("Dry run \u2014 CHANGELOG.md not modified"),console.log();return}let u=k(n);u||(u=`# Changelog
|
|
19
|
-
|
|
20
|
-
`);const A=r?J(u,l):U(u,l);g.writeFileSync(n,A),R(`CHANGELOG.md updated ${f("("+(r?"appended":"replaced")+" ## Unreleased)")}`),console.log(),G("Changelog drafted \u2014 review and edit before your next release"),console.log(` Run ${h("infernoflow publish")} when ready to cut the release
|
|
21
|
-
`)}async function P(e){const n=e.slice(1),o=n.find(l=>!l.startsWith("-"))||"update",t=n.includes("--dry-run"),s=n.includes("--append"),r=n.includes("--json"),i=n.indexOf("--ref"),c=i!==-1?n[i+1]:null,a=process.cwd(),d=S.join(a,"CHANGELOG.md");if(r||O("changelog "+o),o==="list"){W(a,c);return}if(o==="show"){B(d);return}if(o==="update"){await T(a,d,{ref:c,dryRun:t,append:s,asJson:r});return}C(`Unknown sub-command: ${o}`,"Use: update | show | list"),process.exit(1)}export{P as changelogCommand};
|
package/dist/lib/commands/ci.mjs
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import*as d from"node:fs";import*as p from"node:path";import{fileURLToPath as m}from"node:url";import{spawnSync as h}from"node:child_process";import"../ui/output.mjs";function b(){return process.env.GITHUB_ACTIONS==="true"?"github":process.env.GITLAB_CI==="true"?"gitlab":process.env.BITBUCKET_BUILD_NUMBER?"bitbucket":process.env.CIRCLECI==="true"?"circleci":process.env.JENKINS_URL?"jenkins":process.env.CI==="true"?"generic":"local"}function w(n,e){try{const[f,...t]=n.split(" "),o=h(process.execPath,[p.join(p.dirname(p.dirname(m(import.meta.url))),"..","bin","infernoflow.mjs"),...n.split(" ").slice(1)],{cwd:e,encoding:"utf8",timeout:3e4}).stdout?.trim();if(o)return JSON.parse(o)}catch{}return null}function P(n,e){try{return h(process.execPath,[p.join(p.dirname(p.dirname(m(import.meta.url))),"..","bin","infernoflow.mjs"),...n],{cwd:e,encoding:"utf8",timeout:3e4}).stdout?.trim()||""}catch{return""}}function $(n,e,f){const t=n?.status||"unknown",a=n?.issues||[],o=n?.capabilities||0,c=e?.added?.length||0,s=e?.removed?.length||0,l=e?.changed?.length||0;t==="error"?a.filter(r=>r.severity==="error").forEach(r=>{console.log(`::error::infernoflow: ${r.message}`)}):t==="warning"&&a.filter(r=>r.severity==="warning").forEach(r=>{console.log(`::warning::infernoflow: ${r.message}`)}),c>0&&console.log(`::notice::infernoflow: ${c} new capability${c!==1?"ies":"y"} added`),s>0&&console.log(`::warning::infernoflow: ${s} capability${s!==1?"ies":"y"} removed`);const u=process.env.GITHUB_STEP_SUMMARY;if(u){const i=["## \u{1F525} infernoflow CI report","",`${t==="ok"?"\u2705":t==="warning"?"\u26A0\uFE0F":"\u274C"} **Status:** ${t.toUpperCase()} \xB7 **Capabilities:** ${o}`,""];(c||s||l)&&(i.push("### Capability changes"),c&&i.push(`- \u2705 **${c}** added`),s&&i.push(`- \u274C **${s}** removed`),l&&i.push(`- \u{1F4DD} **${l}** changed`),i.push("")),a.length&&(i.push("### Issues"),a.forEach(g=>i.push(`- **${g.severity?.toUpperCase()||"INFO"}**: ${g.message}`)),i.push("")),i.push("---"),i.push("*Generated by [infernoflow](https://github.com/ronmiz/infernoflow)*");try{d.appendFileSync(u,i.join(`
|
|
2
|
-
`)+`
|
|
3
|
-
`)}catch{}}}function v(n,e){const t=(n?.issues||[]).map((o,c)=>({description:o.message||"infernoflow issue",fingerprint:Buffer.from(`infernoflow-${c}-${o.message}`).toString("hex").slice(0,40),severity:o.severity==="error"?"critical":"minor",location:{path:"inferno/contract.json",lines:{begin:1}}})),a=p.join(e,"gl-code-quality-report.json");d.writeFileSync(a,JSON.stringify(t,null,2)),console.log("infernoflow: GitLab code quality report written \u2192 gl-code-quality-report.json")}function y(n,e,f){const t=n?.status||"unknown",a=n?.capabilities||0,o=e?.added?.length||0,c=e?.removed?.length||0;console.log(`[infernoflow] platform=${f} status=${t} capabilities=${a} added=${o} removed=${c}`),n?.issues?.length&&n.issues.forEach(s=>{console.log(`[infernoflow] ${(s.severity||"info").toUpperCase()}: ${s.message}`)})}async function J(n){const e=n.slice(1),f=e.includes("--json"),t=e.includes("--platform")?e[e.indexOf("--platform")+1]:null,a=e.includes("--fail-on")?e[e.indexOf("--fail-on")+1]:"error",o=process.cwd(),c=p.join(o,"inferno");d.existsSync(c)||(console.log(f?JSON.stringify({ok:!1,error:"inferno/ not found"}):"[infernoflow] inferno/ not found \u2014 skipping CI check"),process.exit(0));const s=t||b();f||console.log(`[infernoflow] running CI check (platform: ${s})`);const l=w("check --json",o),u=w("diff --json",o),r=l?.status||"unknown";switch(s){case"github":$(l,u,a);break;case"gitlab":v(l,o),y(l,u,s);break;default:y(l,u,s)}f&&console.log(JSON.stringify({ok:r==="ok"||r==="warning",platform:s,status:r,capabilities:l?.capabilities||0,issues:l?.issues||[],diff:{added:u?.added||[],removed:u?.removed||[],changed:u?.changed||[]}}));const i=a==="warning"?r==="error"||r==="warning":r==="error";process.exit(i?1:0)}export{J as ciCommand};
|