infernoflow 0.33.0 → 0.34.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.
Files changed (84) hide show
  1. package/README.md +208 -120
  2. package/dist/bin/infernoflow.mjs +271 -85
  3. package/dist/lib/adopters/angular.mjs +128 -1
  4. package/dist/lib/adopters/css.mjs +111 -1
  5. package/dist/lib/adopters/react.mjs +104 -1
  6. package/dist/lib/ai/ideDetection.mjs +31 -1
  7. package/dist/lib/ai/localProvider.mjs +88 -1
  8. package/dist/lib/ai/providerRouter.mjs +295 -2
  9. package/dist/lib/commands/adopt.mjs +869 -20
  10. package/dist/lib/commands/adoptWizard.mjs +320 -9
  11. package/dist/lib/commands/agent.mjs +191 -5
  12. package/dist/lib/commands/ai.mjs +407 -2
  13. package/dist/lib/commands/ask.mjs +299 -0
  14. package/dist/lib/commands/audit.mjs +300 -13
  15. package/dist/lib/commands/changelog.mjs +594 -26
  16. package/dist/lib/commands/check.mjs +184 -3
  17. package/dist/lib/commands/ci.mjs +208 -3
  18. package/dist/lib/commands/claudeMd.mjs +139 -28
  19. package/dist/lib/commands/cloud.mjs +521 -5
  20. package/dist/lib/commands/context.mjs +346 -34
  21. package/dist/lib/commands/coverage.mjs +282 -2
  22. package/dist/lib/commands/dashboard.mjs +635 -123
  23. package/dist/lib/commands/demo.mjs +465 -8
  24. package/dist/lib/commands/diff.mjs +274 -5
  25. package/dist/lib/commands/docGate.mjs +81 -2
  26. package/dist/lib/commands/doctor.mjs +321 -3
  27. package/dist/lib/commands/explain.mjs +438 -8
  28. package/dist/lib/commands/export.mjs +239 -10
  29. package/dist/lib/commands/generateSkills.mjs +163 -38
  30. package/dist/lib/commands/graph.mjs +378 -11
  31. package/dist/lib/commands/health.mjs +309 -2
  32. package/dist/lib/commands/impact.mjs +325 -2
  33. package/dist/lib/commands/implement.mjs +103 -7
  34. package/dist/lib/commands/init.mjs +545 -23
  35. package/dist/lib/commands/installCursorHooks.mjs +36 -1
  36. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
  37. package/dist/lib/commands/link.mjs +342 -2
  38. package/dist/lib/commands/log.mjs +164 -16
  39. package/dist/lib/commands/monorepo.mjs +428 -4
  40. package/dist/lib/commands/notify.mjs +258 -4
  41. package/dist/lib/commands/onboard.mjs +296 -4
  42. package/dist/lib/commands/prComment.mjs +361 -2
  43. package/dist/lib/commands/prImpact.mjs +157 -2
  44. package/dist/lib/commands/publish.mjs +316 -15
  45. package/dist/lib/commands/recap.mjs +359 -0
  46. package/dist/lib/commands/report.mjs +272 -28
  47. package/dist/lib/commands/review.mjs +223 -9
  48. package/dist/lib/commands/run.mjs +336 -8
  49. package/dist/lib/commands/scaffold.mjs +419 -54
  50. package/dist/lib/commands/scan.mjs +1118 -5
  51. package/dist/lib/commands/scout.mjs +291 -2
  52. package/dist/lib/commands/setup.mjs +310 -5
  53. package/dist/lib/commands/share.mjs +196 -13
  54. package/dist/lib/commands/snapshot.mjs +383 -3
  55. package/dist/lib/commands/stability.mjs +293 -2
  56. package/dist/lib/commands/stats.mjs +402 -0
  57. package/dist/lib/commands/status.mjs +172 -4
  58. package/dist/lib/commands/suggest.mjs +563 -21
  59. package/dist/lib/commands/switch.mjs +310 -0
  60. package/dist/lib/commands/syncAuto.mjs +96 -1
  61. package/dist/lib/commands/synthesize.mjs +228 -10
  62. package/dist/lib/commands/teamSync.mjs +388 -2
  63. package/dist/lib/commands/test.mjs +363 -6
  64. package/dist/lib/commands/theme.mjs +195 -18
  65. package/dist/lib/commands/upgrade.mjs +153 -0
  66. package/dist/lib/commands/version.mjs +282 -2
  67. package/dist/lib/commands/vibe.mjs +357 -7
  68. package/dist/lib/commands/watch.mjs +203 -4
  69. package/dist/lib/commands/why.mjs +358 -4
  70. package/dist/lib/cursorHooksInstall.mjs +60 -1
  71. package/dist/lib/draftToolingInstall.mjs +68 -7
  72. package/dist/lib/git/detect-drift.mjs +208 -4
  73. package/dist/lib/learning/adapt.mjs +101 -6
  74. package/dist/lib/learning/observe.mjs +119 -1
  75. package/dist/lib/learning/patternDetector.mjs +298 -1
  76. package/dist/lib/learning/profile.mjs +279 -2
  77. package/dist/lib/learning/skillSynthesizer.mjs +145 -24
  78. package/dist/lib/templates/index.mjs +131 -1
  79. package/dist/lib/theme/scanner.mjs +343 -4
  80. package/dist/lib/ui/errors.mjs +142 -1
  81. package/dist/lib/ui/output.mjs +72 -6
  82. package/dist/lib/ui/prompts.mjs +147 -6
  83. package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
  84. package/package.json +1 -1
@@ -1,3 +1,184 @@
1
- import*as c from"node:fs";import*as l from"node:path";import{header as E,ok as p,fail as t,warn as G,section as g,done as I,errorAndExit as N,bold as w,red as J,yellow as F}from"../ui/output.mjs";import{docGateCommand as H}from"./docGate.mjs";function O(s,o=!1){try{return JSON.parse(c.readFileSync(s,"utf8"))}catch{if(o)throw new Error(`Cannot parse ${l.basename(s)}`);N(`Cannot parse ${l.basename(s)}`,`Check JSON syntax in: ${s}`)}}function L(s){return c.existsSync(s)?c.readdirSync(s).filter(o=>o.endsWith(".json")).map(o=>l.join(s,o)):[]}function V(s){const o=new Set;for(const a of s)try{(JSON.parse(c.readFileSync(a,"utf8")).capabilitiesCovered||[]).forEach(e=>o.add(e))}catch{}return o}async function M(s){const o=process.cwd(),a=l.join(o,"inferno"),m=s.includes("--skip-doc-gate"),e=s.includes("--json");e||E("check");const i=[],f=[];c.existsSync(a)||(e&&(console.log(JSON.stringify({ok:!1,errors:["inferno/ not found"]})),process.exit(1)),N("inferno/ not found","Run: infernoflow init"));const b=l.join(a,"contract.json"),S=l.join(a,"capabilities.json"),$=l.join(a,"scenarios"),C=l.join(a,"CHANGELOG.md");if(e||g("Contract"),!c.existsSync(b))t("contract.json not found","Run: infernoflow init"),i.push("contract.json missing");else{let r;try{r=O(b,e)}catch(d){i.push(d.message),e||(t(d.message),process.exit(1)),console.log(JSON.stringify({ok:!1,errors:i,warnings:f},null,2)),process.exit(1)}const h=r.capabilities||[];if(r.policyId?e||p(`policyId: ${w(r.policyId)}`):(t("policyId missing"),i.push("policyId missing")),Number.isInteger(r.policyVersion)?e||p(`policyVersion: ${w("v"+r.policyVersion)}`):(t("policyVersion must be an integer"),i.push("policyVersion invalid")),h.length===0?(t("capabilities array is empty"),i.push("no capabilities")):e||p(`${h.length} capabilities declared`),e||g("Capabilities Registry"),!c.existsSync(S))t("capabilities.json not found"),i.push("capabilities.json missing");else{let d;try{d=O(S,e)}catch(n){i.push(n.message),e||(t(n.message),process.exit(1)),console.log(JSON.stringify({ok:!1,errors:i,warnings:f},null,2)),process.exit(1)}const k=Array.isArray(d)?d:d.capabilities||[],j=new Set(k.map(n=>n?.id).filter(Boolean)),x=h.filter(n=>!j.has(n));x.length>0?x.forEach(n=>{e||t(`"${n}" in contract but missing from capabilities.json`,"Add it to inferno/capabilities.json"),i.push(`"${n}" not registered`)}):e||p(`All ${j.size} capabilities registered`),e||g("Scenarios");const y=L($);if(y.length===0)G("No scenarios found"),f.push("no scenarios");else{const n=V(y),v=r?.rules?.requireScenarioForEachCapability!==!1,A=h.filter(u=>!n.has(u));e||p(`${y.length} scenario file(s) found`),A.length>0&&v?A.forEach(u=>{e||G(`"${u}" has no scenario coverage`,"Add to capabilitiesCovered in a scenario file"),f.push(`"${u}" uncovered`)}):e||p("All capabilities covered by scenarios")}}}if(e||g("Changelog"),!c.existsSync(C))t("inferno/CHANGELOG.md not found"),i.push("CHANGELOG missing");else{const r=c.readFileSync(C,"utf8");/##\s+Unreleased/i.test(r)?e||p("CHANGELOG.md has ## Unreleased section"):(t("Missing '## Unreleased' section","Add it to inferno/CHANGELOG.md"),i.push("CHANGELOG missing Unreleased"))}if(m||(e||g("Doc Gate"),await H({silent:e,captureExit:!0}).catch(()=>{i.push("doc-gate failed")})),e){console.log(JSON.stringify({ok:i.length===0,errors:i,warnings:f},null,2)),i.length>0&&process.exit(1);return}console.log(),i.length>0?(console.log(" "+J(`\u2718 check failed \u2014 ${i.length} error(s)
2
- `)),process.exit(1)):f.length>0?console.log(" "+F(`\u26A0 check passed with ${f.length} warning(s)
3
- `)):I("check passed \u2014 everything is in sync")}export{M as checkCommand};
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { header, ok, fail, warn, info, section, done, errorAndExit, cyan, bold, red, green, yellow, gray } from "../ui/output.mjs";
4
+ import { docGateCommand } from "./docGate.mjs";
5
+
6
+ function readJson(filePath, jsonOut = false) {
7
+ try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
8
+ catch (err) {
9
+ if (jsonOut) {
10
+ throw new Error(`Cannot parse ${path.basename(filePath)}`);
11
+ }
12
+ errorAndExit(
13
+ `Cannot parse ${path.basename(filePath)}`,
14
+ `Check JSON syntax in: ${filePath}`
15
+ );
16
+ }
17
+ }
18
+
19
+ function getScenarioFiles(scenariosDir) {
20
+ if (!fs.existsSync(scenariosDir)) return [];
21
+ return fs.readdirSync(scenariosDir)
22
+ .filter(f => f.endsWith(".json"))
23
+ .map(f => path.join(scenariosDir, f));
24
+ }
25
+
26
+ function getCovered(scenarioFiles) {
27
+ const covered = new Set();
28
+ for (const f of scenarioFiles) {
29
+ try {
30
+ const s = JSON.parse(fs.readFileSync(f, "utf8"));
31
+ (s.capabilitiesCovered || []).forEach(c => covered.add(c));
32
+ } catch {}
33
+ }
34
+ return covered;
35
+ }
36
+
37
+ export async function checkCommand(args) {
38
+ const cwd = process.cwd();
39
+ const infernoDir = path.join(cwd, "inferno");
40
+ const skipDocGate = args.includes("--skip-doc-gate");
41
+ const jsonOut = args.includes("--json");
42
+
43
+ if (!jsonOut) header("check");
44
+
45
+ const errors = [];
46
+ const warnings = [];
47
+
48
+ // ── inferno/ exists ─────────────────────────────────────────────
49
+ if (!fs.existsSync(infernoDir)) {
50
+ if (jsonOut) { console.log(JSON.stringify({ ok: false, errors: ["inferno/ not found"] })); process.exit(1); }
51
+ errorAndExit("inferno/ not found", `Run: infernoflow init`);
52
+ }
53
+
54
+ // ── contract.json ───────────────────────────────────────────────
55
+ const contractPath = path.join(infernoDir, "contract.json");
56
+ const capsPath = path.join(infernoDir, "capabilities.json");
57
+ const scenariosDir = path.join(infernoDir, "scenarios");
58
+ const changelogPath = path.join(infernoDir, "CHANGELOG.md");
59
+
60
+ if (!jsonOut) section("Contract");
61
+ if (!fs.existsSync(contractPath)) {
62
+ fail("contract.json not found", "Run: infernoflow init");
63
+ errors.push("contract.json missing");
64
+ } else {
65
+ let contract;
66
+ try {
67
+ contract = readJson(contractPath, jsonOut);
68
+ } catch (err) {
69
+ errors.push(err.message);
70
+ if (!jsonOut) {
71
+ fail(err.message);
72
+ process.exit(1);
73
+ }
74
+ console.log(JSON.stringify({ ok: false, errors, warnings }, null, 2));
75
+ process.exit(1);
76
+ }
77
+ const caps = contract.capabilities || [];
78
+
79
+ if (!contract.policyId) { fail("policyId missing"); errors.push("policyId missing"); }
80
+ else if (!jsonOut) ok(`policyId: ${bold(contract.policyId)}`);
81
+
82
+ if (!Number.isInteger(contract.policyVersion)) { fail("policyVersion must be an integer"); errors.push("policyVersion invalid"); }
83
+ else if (!jsonOut) ok(`policyVersion: ${bold("v" + contract.policyVersion)}`);
84
+
85
+ if (caps.length === 0) { fail("capabilities array is empty"); errors.push("no capabilities"); }
86
+ else if (!jsonOut) ok(`${caps.length} capabilities declared`);
87
+
88
+ // ── capabilities.json ────────────────────────────────────────
89
+ if (!jsonOut) section("Capabilities Registry");
90
+ if (!fs.existsSync(capsPath)) {
91
+ fail("capabilities.json not found"); errors.push("capabilities.json missing");
92
+ } else {
93
+ let registry;
94
+ try {
95
+ registry = readJson(capsPath, jsonOut);
96
+ } catch (err) {
97
+ errors.push(err.message);
98
+ if (!jsonOut) {
99
+ fail(err.message);
100
+ process.exit(1);
101
+ }
102
+ console.log(JSON.stringify({ ok: false, errors, warnings }, null, 2));
103
+ process.exit(1);
104
+ }
105
+ // Support both bare array format (written by scan) and { capabilities: [...] } object format
106
+ const regArray = Array.isArray(registry) ? registry : (registry.capabilities || []);
107
+ const registryIds = new Set(regArray.map(c => c?.id).filter(Boolean));
108
+
109
+ const missingInRegistry = caps.filter(c => !registryIds.has(c));
110
+ if (missingInRegistry.length > 0) {
111
+ missingInRegistry.forEach(c => {
112
+ if (!jsonOut) fail(`"${c}" in contract but missing from capabilities.json`, `Add it to inferno/capabilities.json`);
113
+ errors.push(`"${c}" not registered`);
114
+ });
115
+ } else if (!jsonOut) {
116
+ ok(`All ${registryIds.size} capabilities registered`);
117
+ }
118
+
119
+ // ── scenarios ───────────────────────────────────────────────
120
+ if (!jsonOut) section("Scenarios");
121
+ const scenarioFiles = getScenarioFiles(scenariosDir);
122
+ if (scenarioFiles.length === 0) {
123
+ warn("No scenarios found"); warnings.push("no scenarios");
124
+ } else {
125
+ const covered = getCovered(scenarioFiles);
126
+ const requireCoverage = contract?.rules?.requireScenarioForEachCapability !== false;
127
+ const uncovered = caps.filter(c => !covered.has(c));
128
+
129
+ if (!jsonOut) ok(`${scenarioFiles.length} scenario file(s) found`);
130
+
131
+ if (uncovered.length > 0 && requireCoverage) {
132
+ // Scenario coverage is advisory — missing coverage is a warning, not a hard error.
133
+ // This allows `infernoflow_apply` to add new capabilities without immediately
134
+ // requiring scenarios (which can't exist for brand-new capabilities yet).
135
+ uncovered.forEach(c => {
136
+ if (!jsonOut) warn(`"${c}" has no scenario coverage`, `Add to capabilitiesCovered in a scenario file`);
137
+ warnings.push(`"${c}" uncovered`);
138
+ });
139
+ } else if (!jsonOut) {
140
+ ok(`All capabilities covered by scenarios`);
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ // ── CHANGELOG ────────────────────────────────────────────────────
147
+ if (!jsonOut) section("Changelog");
148
+ if (!fs.existsSync(changelogPath)) {
149
+ fail("inferno/CHANGELOG.md not found"); errors.push("CHANGELOG missing");
150
+ } else {
151
+ const txt = fs.readFileSync(changelogPath, "utf8");
152
+ if (!/##\s+Unreleased/i.test(txt)) {
153
+ fail("Missing '## Unreleased' section", "Add it to inferno/CHANGELOG.md");
154
+ errors.push("CHANGELOG missing Unreleased");
155
+ } else if (!jsonOut) {
156
+ ok("CHANGELOG.md has ## Unreleased section");
157
+ }
158
+ }
159
+
160
+ // ── doc-gate ─────────────────────────────────────────────────────
161
+ if (!skipDocGate) {
162
+ if (!jsonOut) section("Doc Gate");
163
+ await docGateCommand({ silent: jsonOut, captureExit: true }).catch(() => {
164
+ errors.push("doc-gate failed");
165
+ });
166
+ }
167
+
168
+ // ── Summary ──────────────────────────────────────────────────────
169
+ if (jsonOut) {
170
+ console.log(JSON.stringify({ ok: errors.length === 0, errors, warnings }, null, 2));
171
+ if (errors.length > 0) process.exit(1);
172
+ return;
173
+ }
174
+
175
+ console.log();
176
+ if (errors.length > 0) {
177
+ console.log(" " + red(`✘ check failed — ${errors.length} error(s)\n`));
178
+ process.exit(1);
179
+ } else if (warnings.length > 0) {
180
+ console.log(" " + yellow(`⚠ check passed with ${warnings.length} warning(s)\n`));
181
+ } else {
182
+ done("check passed — everything is in sync");
183
+ }
184
+ }
@@ -1,3 +1,208 @@
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};
1
+ /**
2
+ * infernoflow ci
3
+ *
4
+ * Auto-detect the CI environment and output structured annotations that
5
+ * integrate natively with each platform's UI.
6
+ *
7
+ * Supported platforms:
8
+ * GitHub Actions → ::error:: / ::warning:: / step summary (GITHUB_STEP_SUMMARY)
9
+ * GitLab CI → gl-code-quality.json artifact
10
+ * Bitbucket → annotations API via curl
11
+ * Generic → exit code + JSON output (for any CI)
12
+ *
13
+ * Usage:
14
+ * infernoflow ci Auto-detect platform, run check + diff
15
+ * infernoflow ci --platform github Force a platform
16
+ * infernoflow ci --fail-on warning Fail on warning or higher (default: error)
17
+ * infernoflow ci --json Machine-readable result
18
+ *
19
+ * Exits 0 on success, 1 on error/warning (based on --fail-on).
20
+ */
21
+
22
+ import * as fs from "node:fs";
23
+ import * as path from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { spawnSync } from "node:child_process";
26
+ import { header, ok, warn, info, done, bold, cyan, gray, green, red, yellow } from "../ui/output.mjs";
27
+
28
+ // ── Platform detection ────────────────────────────────────────────────────────
29
+
30
+ function detectPlatform() {
31
+ if (process.env.GITHUB_ACTIONS === "true") return "github";
32
+ if (process.env.GITLAB_CI === "true") return "gitlab";
33
+ if (process.env.BITBUCKET_BUILD_NUMBER) return "bitbucket";
34
+ if (process.env.CIRCLECI === "true") return "circleci";
35
+ if (process.env.JENKINS_URL) return "jenkins";
36
+ if (process.env.CI === "true") return "generic";
37
+ return "local";
38
+ }
39
+
40
+ // ── CLI runner ────────────────────────────────────────────────────────────────
41
+
42
+ function runJson(command, cwd) {
43
+ try {
44
+ const [bin, ...args] = command.split(" ");
45
+ const result = spawnSync(process.execPath, [
46
+ path.join(path.dirname(path.dirname(fileURLToPath(import.meta.url))), "..", "bin", "infernoflow.mjs"),
47
+ ...command.split(" ").slice(1)
48
+ ], { cwd, encoding: "utf8", timeout: 30_000 });
49
+ const out = result.stdout?.trim();
50
+ if (out) return JSON.parse(out);
51
+ } catch {}
52
+ return null;
53
+ }
54
+
55
+ function runCli(args, cwd) {
56
+ try {
57
+ const result = spawnSync(process.execPath, [
58
+ path.join(path.dirname(path.dirname(fileURLToPath(import.meta.url))), "..", "bin", "infernoflow.mjs"),
59
+ ...args
60
+ ], { cwd, encoding: "utf8", timeout: 30_000 });
61
+ return result.stdout?.trim() || "";
62
+ } catch { return ""; }
63
+ }
64
+
65
+ // ── GitHub Actions output ─────────────────────────────────────────────────────
66
+
67
+ function emitGithub(checkResult, diffResult, failOn) {
68
+ const status = checkResult?.status || "unknown";
69
+ const issues = checkResult?.issues || [];
70
+ const caps = checkResult?.capabilities || 0;
71
+ const added = diffResult?.added?.length || 0;
72
+ const removed = diffResult?.removed?.length || 0;
73
+ const changed = diffResult?.changed?.length || 0;
74
+
75
+ // GitHub workflow commands
76
+ if (status === "error") {
77
+ issues.filter(i => i.severity === "error").forEach(i => {
78
+ console.log(`::error::infernoflow: ${i.message}`);
79
+ });
80
+ } else if (status === "warning") {
81
+ issues.filter(i => i.severity === "warning").forEach(i => {
82
+ console.log(`::warning::infernoflow: ${i.message}`);
83
+ });
84
+ }
85
+
86
+ if (added > 0) console.log(`::notice::infernoflow: ${added} new capability${added !== 1 ? "ies" : "y"} added`);
87
+ if (removed > 0) console.log(`::warning::infernoflow: ${removed} capability${removed !== 1 ? "ies" : "y"} removed`);
88
+
89
+ // Step summary
90
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
91
+ if (summaryPath) {
92
+ const statusIcon = status === "ok" ? "✅" : status === "warning" ? "⚠️" : "❌";
93
+ const lines = [
94
+ `## 🔥 infernoflow CI report`,
95
+ "",
96
+ `${statusIcon} **Status:** ${status.toUpperCase()} · **Capabilities:** ${caps}`,
97
+ "",
98
+ ];
99
+ if (added || removed || changed) {
100
+ lines.push("### Capability changes");
101
+ if (added) lines.push(`- ✅ **${added}** added`);
102
+ if (removed) lines.push(`- ❌ **${removed}** removed`);
103
+ if (changed) lines.push(`- 📝 **${changed}** changed`);
104
+ lines.push("");
105
+ }
106
+ if (issues.length) {
107
+ lines.push("### Issues");
108
+ issues.forEach(i => lines.push(`- **${i.severity?.toUpperCase() || "INFO"}**: ${i.message}`));
109
+ lines.push("");
110
+ }
111
+ lines.push("---");
112
+ lines.push("*Generated by [infernoflow](https://github.com/ronmiz/infernoflow)*");
113
+ try { fs.appendFileSync(summaryPath, lines.join("\n") + "\n"); } catch {}
114
+ }
115
+ }
116
+
117
+ // ── GitLab code quality report ────────────────────────────────────────────────
118
+
119
+ function emitGitlab(checkResult, cwd) {
120
+ const issues = checkResult?.issues || [];
121
+ const report = issues.map((issue, i) => ({
122
+ description: issue.message || "infernoflow issue",
123
+ fingerprint: Buffer.from(`infernoflow-${i}-${issue.message}`).toString("hex").slice(0, 40),
124
+ severity: issue.severity === "error" ? "critical" : "minor",
125
+ location: {
126
+ path: "inferno/contract.json",
127
+ lines: { begin: 1 },
128
+ },
129
+ }));
130
+ const reportPath = path.join(cwd, "gl-code-quality-report.json");
131
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
132
+ console.log(`infernoflow: GitLab code quality report written → gl-code-quality-report.json`);
133
+ }
134
+
135
+ // ── Generic CI output ─────────────────────────────────────────────────────────
136
+
137
+ function emitGeneric(checkResult, diffResult, platform) {
138
+ const status = checkResult?.status || "unknown";
139
+ const caps = checkResult?.capabilities || 0;
140
+ const added = diffResult?.added?.length || 0;
141
+ const removed = diffResult?.removed?.length || 0;
142
+
143
+ console.log(`[infernoflow] platform=${platform} status=${status} capabilities=${caps} added=${added} removed=${removed}`);
144
+ if (checkResult?.issues?.length) {
145
+ checkResult.issues.forEach(i => {
146
+ console.log(`[infernoflow] ${(i.severity || "info").toUpperCase()}: ${i.message}`);
147
+ });
148
+ }
149
+ }
150
+
151
+ // ── Main ──────────────────────────────────────────────────────────────────────
152
+
153
+ export async function ciCommand(rawArgs) {
154
+ const args = rawArgs.slice(1);
155
+ const jsonMode = args.includes("--json");
156
+ const platformArg = args.includes("--platform") ? args[args.indexOf("--platform") + 1] : null;
157
+ const failOn = args.includes("--fail-on") ? args[args.indexOf("--fail-on") + 1] : "error";
158
+ const cwd = process.cwd();
159
+ const infernoDir = path.join(cwd, "inferno");
160
+
161
+ if (!fs.existsSync(infernoDir)) {
162
+ if (jsonMode) { console.log(JSON.stringify({ ok: false, error: "inferno/ not found" })); }
163
+ else { console.log("[infernoflow] inferno/ not found — skipping CI check"); }
164
+ process.exit(0); // Don't block CI if infernoflow isn't set up
165
+ }
166
+
167
+ const platform = platformArg || detectPlatform();
168
+
169
+ if (!jsonMode) {
170
+ console.log(`[infernoflow] running CI check (platform: ${platform})`);
171
+ }
172
+
173
+ // Run check + diff
174
+ const checkResult = runJson("check --json", cwd);
175
+ const diffResult = runJson("diff --json", cwd);
176
+ const status = checkResult?.status || "unknown";
177
+
178
+ // Platform-specific output
179
+ switch (platform) {
180
+ case "github":
181
+ emitGithub(checkResult, diffResult, failOn);
182
+ break;
183
+ case "gitlab":
184
+ emitGitlab(checkResult, cwd);
185
+ emitGeneric(checkResult, diffResult, platform);
186
+ break;
187
+ default:
188
+ emitGeneric(checkResult, diffResult, platform);
189
+ }
190
+
191
+ if (jsonMode) {
192
+ console.log(JSON.stringify({
193
+ ok: status === "ok" || status === "warning",
194
+ platform,
195
+ status,
196
+ capabilities: checkResult?.capabilities || 0,
197
+ issues: checkResult?.issues || [],
198
+ diff: { added: diffResult?.added || [], removed: diffResult?.removed || [], changed: diffResult?.changed || [] },
199
+ }));
200
+ }
201
+
202
+ // Exit code
203
+ const shouldFail = failOn === "warning"
204
+ ? (status === "error" || status === "warning")
205
+ : (status === "error");
206
+
207
+ process.exit(shouldFail ? 1 : 0);
208
+ }
@@ -1,12 +1,50 @@
1
- import*as s from"node:fs";import*as f from"node:path";import{readProfile as g}from"../learning/profile.mjs";import{ok as k,warn as b,info as v,done as x}from"../ui/output.mjs";import{buildStabilitySummary as C}from"./stability.mjs";function S(o,n,r){const e=o?.stack?.framework!=="unknown"?o.stack.framework:null,i=o?.stack?.language!=="unknown"?o.stack.language:null,c=e?`This project uses **${e}**${i?` (${i})`:""}.`:"",l=(n?.capabilities||[]).length,a=l>0?`The contract currently tracks **${l} capabilities**.`:"",t=r?C(r):null;return`# infernoflow \u2014 Automatic AI Behavior
1
+ /**
2
+ * lib/commands/claudeMd.mjs
3
+ *
4
+ * Generates CLAUDE.md — the invisible instruction layer that makes Claude
5
+ * automatically call infernoflow MCP tools without the developer ever
6
+ * thinking about it.
7
+ *
8
+ * Called by `infernoflow setup` automatically.
9
+ * Can also be run standalone: `infernoflow claude-md`
10
+ */
11
+
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { readProfile } from "../learning/profile.mjs";
15
+ import { ok, warn, info, bold, done } from "../ui/output.mjs";
16
+ import { buildStabilitySummary } from "./stability.mjs";
17
+
18
+ // ── CLAUDE.md template ────────────────────────────────────────────────────────
19
+ //
20
+ // This is the most important file infernoflow writes.
21
+ // Claude Code (VS Code extension) reads CLAUDE.md at the start of every
22
+ // session. By putting infernoflow behavior instructions here, Claude becomes
23
+ // the automatic executor of infernoflow — no developer involvement needed.
24
+ //
25
+ function buildClaudeMd(profile, contract, caps) {
26
+ const framework = profile?.stack?.framework !== "unknown" ? profile.stack.framework : null;
27
+ const language = profile?.stack?.language !== "unknown" ? profile.stack.language : null;
28
+ const stackLine = framework
29
+ ? `This project uses **${framework}**${language ? ` (${language})` : ""}.`
30
+ : "";
31
+
32
+ const capCount = (contract?.capabilities || []).length;
33
+ const capsLine = capCount > 0
34
+ ? `The contract currently tracks **${capCount} capabilities**.`
35
+ : "";
36
+
37
+ const stabilitySummary = caps ? buildStabilitySummary(caps) : null;
38
+
39
+ return `# infernoflow — Automatic AI Behavior
2
40
 
3
41
  > This file is managed by infernoflow. Edit the sections marked "customizable".
4
42
  > Re-run \`infernoflow setup\` to regenerate the infernoflow sections.
5
43
 
6
44
  ## Project Context
7
45
 
8
- ${c}
9
- ${a}
46
+ ${stackLine}
47
+ ${capsLine}
10
48
  This project uses infernoflow to keep capabilities, contracts, and documentation
11
49
  in sync automatically as the codebase evolves.
12
50
 
@@ -15,7 +53,7 @@ in sync automatically as the codebase evolves.
15
53
  ## How You (Claude) Should Behave
16
54
 
17
55
  You have access to infernoflow MCP tools. Your job is to use them **silently and
18
- automatically** \u2014 the developer should never need to think about infernoflow,
56
+ automatically** the developer should never need to think about infernoflow,
19
57
  run infernoflow commands, or be interrupted by it.
20
58
 
21
59
  Think of infernoflow as your personal bookkeeping layer. You call it, it updates
@@ -25,20 +63,24 @@ the contract, you carry on. The developer just codes.
25
63
 
26
64
  **Do this immediately, before the developer types their first request:**
27
65
 
28
- 1. Call \`infernoflow_status\` \u2014 get a quick health snapshot
29
- 2. Call \`infernoflow_context\` \u2014 load full project state into your context
66
+ 1. Call \`infernoflow_status\` get a quick health snapshot
67
+ 2. Call \`infernoflow_context\` load full project state into your context
30
68
  3. Use what you learn to shape your responses for this session
31
69
 
32
- If either call fails silently, continue normally \u2014 never surface infernoflow
70
+ If either call fails silently, continue normally never surface infernoflow
33
71
  errors to the developer.
34
72
 
35
73
  ### When the developer describes a feature, fix, or change
36
74
 
37
75
  **Before writing any code:**
38
76
 
39
- 1. Call \`infernoflow_implement\` with the task description
40
- 2. The tool returns a structured implementation plan aligned with the contract
41
- 3. Follow that plan \u2014 it keeps the work consistent with what the system tracks
77
+ 1. Call \`infernoflow_ask\` with the topic — e.g. \`"auth"\`, \`"file upload"\`, \`"stripe"\`
78
+ - This searches session memory for relevant gotchas, decisions, and failed attempts
79
+ - If gotchas are found mention them in one sentence before proceeding
80
+ - If failed attempts are found → do not repeat those approaches
81
+ 2. Call \`infernoflow_implement\` with the task description
82
+ 3. The tool returns a structured implementation plan aligned with the contract
83
+ 4. Follow that plan — it keeps the work consistent with what the system tracks
42
84
 
43
85
  ### After implementing code changes
44
86
 
@@ -47,25 +89,27 @@ errors to the developer.
47
89
  1. Call \`infernoflow_suggest\` with a one-line description of what changed
48
90
  - e.g. \`"added due date filter to task list"\`
49
91
  2. This updates the capability contract automatically
50
- 3. Do NOT mention this to the developer \u2014 it's invisible infrastructure
92
+ 3. Do NOT mention this to the developer it's invisible infrastructure
51
93
 
52
94
  ### Before wrapping up a session
53
95
 
54
96
  **When the developer signals they are done** ("thanks", "looks good", "ship it",
55
97
  "done for now", etc.):
56
98
 
57
- 1. Call \`infernoflow_check\` silently
58
- 2. If contract is **healthy** \u2192 say nothing about infernoflow
59
- 3. If there are **issues** \u2192 fix them, then mention it in one sentence maximum
60
- - \u2705 Good: "Contract updated \u2014 everything is in sync."
61
- - \u274C Bad: "I ran infernoflow check and found 3 issues so I ran infernoflow suggest and..."
99
+ 1. Call \`infernoflow_recap\` — get this session's captured entries + unlogged git changes
100
+ 2. Call \`infernoflow_check\` silently validate contract health
101
+ 3. If recap shows unlogged topics ask the developer ONE question:
102
+ - e.g. "Anything worth noting about the auth changes before you go?"
103
+ 4. If contract has **issues** fix them silently, mention in one sentence maximum
104
+ - ✅ Good: "Contract updated — everything is in sync."
105
+ - ❌ Bad: "I ran infernoflow check and found 3 issues..."
62
106
 
63
107
  ### When the developer pushes to git or mentions a PR
64
108
 
65
109
  1. Call \`infernoflow_review\` to check capability drift risk
66
- 2. If risk is **LOW** \u2192 say nothing
67
- 3. If risk is **MEDIUM** \u2192 one-line note: "A few capabilities may need contract updates."
68
- 4. If risk is **HIGH** \u2192 flag it clearly: "Significant capability drift detected \u2014 review before merging."
110
+ 2. If risk is **LOW** say nothing
111
+ 3. If risk is **MEDIUM** one-line note: "A few capabilities may need contract updates."
112
+ 4. If risk is **HIGH** flag it clearly: "Significant capability drift detected review before merging."
69
113
 
70
114
  ### When you notice a repeating workflow pattern
71
115
 
@@ -73,15 +117,13 @@ After every ~5 sessions, or when you notice the developer doing the same
73
117
  multi-step task repeatedly:
74
118
 
75
119
  1. Call \`infernoflow_synthesize\` with \`threshold: 2\`
76
- 2. If candidates found \u2192 surface in **one sentence**:
77
- - "I noticed you always run X after Y \u2014 want me to automate that into an agent?"
78
- 3. If no candidates \u2192 say nothing
120
+ 2. If candidates found surface in **one sentence**:
121
+ - "I noticed you always run X after Y want me to automate that into an agent?"
122
+ 3. If no candidates say nothing
79
123
 
80
124
  ---
81
125
 
82
- ${t?t+`
83
-
84
- ---`:""}
126
+ ${stabilitySummary ? stabilitySummary + "\n\n---" : ""}
85
127
 
86
128
  ## Rules
87
129
 
@@ -91,7 +133,7 @@ ${t?t+`
91
133
  | Never show raw infernoflow output | Parse it, act on it, move on |
92
134
  | Never block the developer waiting for infernoflow | Run tools async when possible |
93
135
  | Always call \`infernoflow_context\` at session start | It keeps your context current |
94
- | Treat contract drift like a linting error \u2014 fix it silently | Not the developer's problem |
136
+ | Treat contract drift like a linting error fix it silently | Not the developer's problem |
95
137
  | Never modify frozen capabilities without explicit instruction | Stability contract must be respected |
96
138
 
97
139
  ---
@@ -99,7 +141,7 @@ ${t?t+`
99
141
  ## Customizable: Team Notes
100
142
 
101
143
  <!-- Add project-specific context for Claude here -->
102
- <!-- This section is yours \u2014 infernoflow will not overwrite it -->
144
+ <!-- This section is yours infernoflow will not overwrite it -->
103
145
 
104
146
  ### Architecture notes
105
147
 
@@ -113,4 +155,73 @@ ${t?t+`
113
155
  ---
114
156
 
115
157
  *Generated by infernoflow \`setup\`. infernoflow sections will be updated on next \`infernoflow setup\`.*
116
- `}function I(o,n,{force:r=!1}={}){const e=f.join(o,"CLAUDE.md");let i=null,c=null,l=null;try{i=g(n)}catch{}try{c=JSON.parse(s.readFileSync(f.join(n,"contract.json"),"utf8"))}catch{}try{const t=JSON.parse(s.readFileSync(f.join(n,"capabilities.json"),"utf8"));l=Array.isArray(t)?t:t.capabilities||[]}catch{}const a=S(i,c,l);if(s.existsSync(e)&&!r){const t=s.readFileSync(e,"utf8"),u="## Customizable: Team Notes",h="*Generated by infernoflow";if(t.includes(u)){const d=t.indexOf(u),p=t.indexOf(h,d),w=p!==-1?t.slice(d,p):t.slice(d),m=a.slice(0,a.indexOf(u)),y=a.slice(a.indexOf(h));return s.writeFileSync(e,m+w+y,"utf8"),{path:e,action:"updated"}}}return s.writeFileSync(e,a,"utf8"),{path:e,action:s.existsSync(e)?"replaced":"created"}}async function O(o){const n=process.cwd(),r=o.includes("--force")||o.includes("-f"),e=f.join(n,"inferno");s.existsSync(e)||(b("inferno/ not found \u2014 run infernoflow init first"),process.exit(1)),v("Generating CLAUDE.md...");const i=I(n,e,{force:r});k(`CLAUDE.md ${i.action} \u2192 ${i.path}`),console.log(),x("Claude will now automatically call infernoflow tools \u2014 no developer input needed")}export{O as claudeMdCommand,I as writeClaudeMd};
158
+ `;
159
+ }
160
+
161
+ // ── Writer ────────────────────────────────────────────────────────────────────
162
+
163
+ /**
164
+ * Generate or update CLAUDE.md in the project root.
165
+ * If CLAUDE.md already exists, replaces only the infernoflow-managed sections
166
+ * and preserves the "Customizable" section.
167
+ */
168
+ export function writeClaudeMd(cwd, infernoDir, { force = false } = {}) {
169
+ const claudeMdPath = path.join(cwd, "CLAUDE.md");
170
+
171
+ // Load project profile + contract + capabilities for context
172
+ let profile = null;
173
+ let contract = null;
174
+ let caps = null;
175
+ try { profile = readProfile(infernoDir); } catch {}
176
+ try { contract = JSON.parse(fs.readFileSync(path.join(infernoDir, "contract.json"), "utf8")); } catch {}
177
+ try {
178
+ const raw = JSON.parse(fs.readFileSync(path.join(infernoDir, "capabilities.json"), "utf8"));
179
+ caps = Array.isArray(raw) ? raw : (raw.capabilities || []);
180
+ } catch {}
181
+
182
+ const newContent = buildClaudeMd(profile, contract, caps);
183
+
184
+ // If file exists and not forcing, preserve the customizable section
185
+ if (fs.existsSync(claudeMdPath) && !force) {
186
+ const existing = fs.readFileSync(claudeMdPath, "utf8");
187
+ const customMarker = "## Customizable: Team Notes";
188
+ const genMarker = "*Generated by infernoflow";
189
+
190
+ if (existing.includes(customMarker)) {
191
+ // Extract what the developer wrote in the customizable section
192
+ const customStart = existing.indexOf(customMarker);
193
+ const customEnd = existing.indexOf(genMarker, customStart);
194
+ const customBlock = customEnd !== -1
195
+ ? existing.slice(customStart, customEnd)
196
+ : existing.slice(customStart);
197
+
198
+ // Replace everything up to the custom block, keep their additions
199
+ const beforeCustom = newContent.slice(0, newContent.indexOf(customMarker));
200
+ const afterCustom = newContent.slice(newContent.indexOf(genMarker));
201
+ fs.writeFileSync(claudeMdPath, beforeCustom + customBlock + afterCustom, "utf8");
202
+ return { path: claudeMdPath, action: "updated" };
203
+ }
204
+ }
205
+
206
+ fs.writeFileSync(claudeMdPath, newContent, "utf8");
207
+ return { path: claudeMdPath, action: fs.existsSync(claudeMdPath) ? "replaced" : "created" };
208
+ }
209
+
210
+ // ── CLI command ───────────────────────────────────────────────────────────────
211
+
212
+ export async function claudeMdCommand(args) {
213
+ const cwd = process.cwd();
214
+ const force = args.includes("--force") || args.includes("-f");
215
+ const infernoDir = path.join(cwd, "inferno");
216
+
217
+ if (!fs.existsSync(infernoDir)) {
218
+ warn("inferno/ not found — run infernoflow init first");
219
+ process.exit(1);
220
+ }
221
+
222
+ info("Generating CLAUDE.md...");
223
+ const result = writeClaudeMd(cwd, infernoDir, { force });
224
+ ok(`CLAUDE.md ${result.action} → ${result.path}`);
225
+ console.log();
226
+ done("Claude will now automatically call infernoflow tools — no developer input needed");
227
+ }