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,10 +1,239 @@
1
- import*as d from"node:fs";import*as a from"node:path";import{done as w,warn as u,bold as l,cyan as k,gray as h}from"../ui/output.mjs";function v(t){for(const o of["contract.json","capabilities.json"]){const n=a.join(t,o);if(d.existsSync(n))try{return JSON.parse(d.readFileSync(n,"utf8"))}catch{}}return null}function O(t){return(t?.capabilities||[]).map(n=>typeof n=="string"?{id:n,description:"",tags:[],status:"active"}:{id:n.id||n.name||"unknown",description:n.description||"",tags:n.tags||[],status:n.status||"active",since:n.since||"",owner:n.owner||""})}function x(t){const o=a.join(a.dirname(t),"package.json");try{const n=JSON.parse(d.readFileSync(o,"utf8"));return{name:n.name||"my-service",version:n.version||"0.0.0",description:n.description||""}}catch{return{name:a.basename(a.dirname(t)),version:"0.0.0",description:""}}}function C(t,o){const n={};for(const e of t){const i=`/${e.id.replace(/_/g,"-")}`;n[i]={get:{operationId:e.id,summary:e.description||e.id,tags:e.tags.length?e.tags:[o.name],parameters:[],responses:{200:{description:"Success",content:{"application/json":{schema:{type:"object"}}}},401:{description:"Unauthorized"},500:{description:"Internal Server Error"}}}}}return JSON.stringify({openapi:"3.1.0",info:{title:o.name,version:o.version,description:o.description||`Capability contract for ${o.name}`},paths:n,components:{schemas:{},securitySchemes:{}}},null,2)}function N(t,o){const n=[...new Set(t.flatMap(i=>i.tags))].slice(0,10);return["apiVersion: backstage.io/v1alpha1","kind: Component","metadata:",` name: ${o.name}`,` description: "${(o.description||o.name).replace(/"/g,'\\"')}"`," annotations:",` infernoflow/capability-count: "${t.length}"`,` infernoflow/generated-at: "${new Date().toISOString()}"`,n.length?` tags:
2
- ${n.map(i=>` - ${i}`).join(`
3
- `)}`:"","spec:"," type: service"," lifecycle: production"," owner: team-default"," providesApis: []","---","# Capabilities",...t.map(i=>[`# ${i.id}`,`# ${i.description||"(no description)"}`,`# tags: ${i.tags.join(", ")||"none"}`,`# status: ${i.status}`].join(`
4
- `))].filter(Boolean).join(`
5
- `)+`
6
- `}function J(t){const o=["id","description","tags","status","since","owner"],n=t.map(e=>[e.id,(e.description||"").replace(/"/g,'""'),e.tags.join("|"),e.status,e.since,e.owner].map(i=>`"${i}"`).join(","));return[o.join(","),...n].join(`
7
- `)+`
8
- `}function I(t,o){return[`# ${o.name} \u2014 Capability Contract`,"",`> Generated by infernoflow on ${new Date().toISOString().slice(0,10)}`,`> ${t.length} capabilities tracked`,"","| Capability | Description | Tags | Status |","|---|---|---|---|",...t.map(e=>`| \`${e.id}\` | ${e.description||""} | ${e.tags.join(", ")||"\u2014"} | ${e.status} |`),""].join(`
9
- `)}function A(t,o){return JSON.stringify({name:o.name,version:o.version,exportedAt:new Date().toISOString(),capabilities:t},null,2)+`
10
- `}const S={openapi:"openapi.json",backstage:"catalog-info.yaml",csv:"capabilities.csv",markdown:"capabilities.md",json:"capabilities-export.json"};async function D(t){const o=t.slice(1),n=o.includes("--json"),e=process.cwd(),i=a.join(e,"inferno");if(!d.existsSync(i)){const s="inferno/ not found. Run: infernoflow init";n?console.log(JSON.stringify({ok:!1,error:s})):u(s),process.exit(1)}const m=o.indexOf("--format"),p=m!==-1?o[m+1]:null,b=o.indexOf("--out"),$=b!==-1?o[b+1]:null,y=Object.keys(S);if(!p||!y.includes(p)){const s=`Usage: infernoflow export --format <${y.join("|")}> [--out <path>]`;n?console.log(JSON.stringify({ok:!1,error:s})):(u(s),console.log(),console.log(` ${h("Available formats:")}`),console.log(` ${l("openapi")} \u2014 OpenAPI 3.1 JSON with a stub path per capability`),console.log(` ${l("backstage")} \u2014 Backstage catalog-info.yaml component definition`),console.log(` ${l("csv")} \u2014 Spreadsheet-ready CSV`),console.log(` ${l("markdown")} \u2014 Confluence/Notion table`),console.log(` ${l("json")} \u2014 Clean normalised JSON`),console.log());return}const j=v(i);if(!j){const s="No contract.json or capabilities.json found.";n?console.log(JSON.stringify({ok:!1,error:s})):u(s),process.exit(1)}const r=O(j),f=x(i),g=$||a.join(e,S[p]);let c;switch(p){case"openapi":c=C(r,f);break;case"backstage":c=N(r,f);break;case"csv":c=J(r);break;case"markdown":c=I(r,f);break;case"json":c=A(r,f);break}d.writeFileSync(g,c),n?console.log(JSON.stringify({ok:!0,format:p,file:g,capabilities:r.length})):(w(`Exported ${l(String(r.length))} capabilities \u2192 ${k(a.relative(e,g))}`),console.log())}export{D as exportCommand};
1
+ /**
2
+ * infernoflow export
3
+ *
4
+ * Export the capability contract to external formats so it can travel
5
+ * outside the repo — into API docs, service catalogs, spreadsheets, wikis.
6
+ *
7
+ * Formats:
8
+ * openapi OpenAPI 3.1 JSON (stubs for each capability)
9
+ * backstage Backstage catalog-info.yaml
10
+ * csv Spreadsheet-ready CSV
11
+ * markdown Confluence/Notion-ready Markdown table
12
+ * json Clean JSON (normalised, no internal infernoflow fields)
13
+ *
14
+ * Usage:
15
+ * infernoflow export --format openapi
16
+ * infernoflow export --format backstage --out catalog-info.yaml
17
+ * infernoflow export --format csv --out capabilities.csv
18
+ * infernoflow export --format markdown
19
+ * infernoflow export --format json --out contract-export.json
20
+ */
21
+
22
+ import * as fs from "node:fs";
23
+ import * as path from "node:path";
24
+ import { done, warn, info, bold, cyan, gray } from "../ui/output.mjs";
25
+
26
+ // ── Contract reader ───────────────────────────────────────────────────────────
27
+
28
+ function readContract(infernoDir) {
29
+ for (const f of ["contract.json", "capabilities.json"]) {
30
+ const p = path.join(infernoDir, f);
31
+ if (!fs.existsSync(p)) continue;
32
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
33
+ }
34
+ return null;
35
+ }
36
+
37
+ function normaliseCaps(contract) {
38
+ const raw = contract?.capabilities || [];
39
+ return raw.map(c => {
40
+ if (typeof c === "string") return { id: c, description: "", tags: [], status: "active" };
41
+ return {
42
+ id: c.id || c.name || "unknown",
43
+ description: c.description || "",
44
+ tags: c.tags || [],
45
+ status: c.status || "active",
46
+ since: c.since || "",
47
+ owner: c.owner || "",
48
+ };
49
+ });
50
+ }
51
+
52
+ function readMeta(infernoDir) {
53
+ const pkgPath = path.join(path.dirname(infernoDir), "package.json");
54
+ try {
55
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
56
+ return { name: pkg.name || "my-service", version: pkg.version || "0.0.0", description: pkg.description || "" };
57
+ } catch {
58
+ return { name: path.basename(path.dirname(infernoDir)), version: "0.0.0", description: "" };
59
+ }
60
+ }
61
+
62
+ // ── Formatters ────────────────────────────────────────────────────────────────
63
+
64
+ function toOpenApi(caps, meta) {
65
+ const paths = {};
66
+ for (const cap of caps) {
67
+ const route = `/${cap.id.replace(/_/g, "-")}`;
68
+ paths[route] = {
69
+ get: {
70
+ operationId: cap.id,
71
+ summary: cap.description || cap.id,
72
+ tags: cap.tags.length ? cap.tags : [meta.name],
73
+ parameters: [],
74
+ responses: {
75
+ "200": { description: "Success", content: { "application/json": { schema: { type: "object" } } } },
76
+ "401": { description: "Unauthorized" },
77
+ "500": { description: "Internal Server Error" },
78
+ },
79
+ },
80
+ };
81
+ }
82
+
83
+ return JSON.stringify({
84
+ openapi: "3.1.0",
85
+ info: {
86
+ title: meta.name,
87
+ version: meta.version,
88
+ description: meta.description || `Capability contract for ${meta.name}`,
89
+ },
90
+ paths,
91
+ components: { schemas: {}, securitySchemes: {} },
92
+ }, null, 2);
93
+ }
94
+
95
+ function toBackstage(caps, meta) {
96
+ const tags = [...new Set(caps.flatMap(c => c.tags))].slice(0, 10);
97
+ const lines = [
98
+ `apiVersion: backstage.io/v1alpha1`,
99
+ `kind: Component`,
100
+ `metadata:`,
101
+ ` name: ${meta.name}`,
102
+ ` description: "${(meta.description || meta.name).replace(/"/g, '\\"')}"`,
103
+ ` annotations:`,
104
+ ` infernoflow/capability-count: "${caps.length}"`,
105
+ ` infernoflow/generated-at: "${new Date().toISOString()}"`,
106
+ tags.length ? ` tags:\n${tags.map(t => ` - ${t}`).join("\n")}` : "",
107
+ `spec:`,
108
+ ` type: service`,
109
+ ` lifecycle: production`,
110
+ ` owner: team-default`,
111
+ ` providesApis: []`,
112
+ `---`,
113
+ `# Capabilities`,
114
+ ...caps.map(c => [
115
+ `# ${c.id}`,
116
+ `# ${c.description || "(no description)"}`,
117
+ `# tags: ${c.tags.join(", ") || "none"}`,
118
+ `# status: ${c.status}`,
119
+ ].join("\n")),
120
+ ].filter(Boolean);
121
+ return lines.join("\n") + "\n";
122
+ }
123
+
124
+ function toCsv(caps) {
125
+ const header = ["id", "description", "tags", "status", "since", "owner"];
126
+ const rows = caps.map(c => [
127
+ c.id,
128
+ (c.description || "").replace(/"/g, '""'),
129
+ c.tags.join("|"),
130
+ c.status,
131
+ c.since,
132
+ c.owner,
133
+ ].map(v => `"${v}"`).join(","));
134
+ return [header.join(","), ...rows].join("\n") + "\n";
135
+ }
136
+
137
+ function toMarkdown(caps, meta) {
138
+ const lines = [
139
+ `# ${meta.name} — Capability Contract`,
140
+ ``,
141
+ `> Generated by infernoflow on ${new Date().toISOString().slice(0, 10)}`,
142
+ `> ${caps.length} capabilities tracked`,
143
+ ``,
144
+ `| Capability | Description | Tags | Status |`,
145
+ `|---|---|---|---|`,
146
+ ...caps.map(c =>
147
+ `| \`${c.id}\` | ${c.description || ""} | ${c.tags.join(", ") || "—"} | ${c.status} |`
148
+ ),
149
+ ``,
150
+ ];
151
+ return lines.join("\n");
152
+ }
153
+
154
+ function toCleanJson(caps, meta) {
155
+ return JSON.stringify({
156
+ name: meta.name,
157
+ version: meta.version,
158
+ exportedAt: new Date().toISOString(),
159
+ capabilities: caps,
160
+ }, null, 2) + "\n";
161
+ }
162
+
163
+ // ── Entry ─────────────────────────────────────────────────────────────────────
164
+
165
+ const FORMAT_EXT = {
166
+ openapi: "openapi.json",
167
+ backstage: "catalog-info.yaml",
168
+ csv: "capabilities.csv",
169
+ markdown: "capabilities.md",
170
+ json: "capabilities-export.json",
171
+ };
172
+
173
+ export async function exportCommand(rawArgs) {
174
+ const args = rawArgs.slice(1);
175
+ const jsonMode = args.includes("--json");
176
+ const cwd = process.cwd();
177
+ const infernoDir = path.join(cwd, "inferno");
178
+
179
+ if (!fs.existsSync(infernoDir)) {
180
+ const msg = "inferno/ not found. Run: infernoflow init";
181
+ if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
182
+ else warn(msg);
183
+ process.exit(1);
184
+ }
185
+
186
+ const fmtIdx = args.indexOf("--format");
187
+ const format = fmtIdx !== -1 ? args[fmtIdx + 1] : null;
188
+ const outIdx = args.indexOf("--out");
189
+ const outArg = outIdx !== -1 ? args[outIdx + 1] : null;
190
+
191
+ const validFormats = Object.keys(FORMAT_EXT);
192
+
193
+ if (!format || !validFormats.includes(format)) {
194
+ const msg = `Usage: infernoflow export --format <${validFormats.join("|")}> [--out <path>]`;
195
+ if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
196
+ else {
197
+ warn(msg);
198
+ console.log();
199
+ console.log(` ${gray("Available formats:")}`);
200
+ console.log(` ${bold("openapi")} — OpenAPI 3.1 JSON with a stub path per capability`);
201
+ console.log(` ${bold("backstage")} — Backstage catalog-info.yaml component definition`);
202
+ console.log(` ${bold("csv")} — Spreadsheet-ready CSV`);
203
+ console.log(` ${bold("markdown")} — Confluence/Notion table`);
204
+ console.log(` ${bold("json")} — Clean normalised JSON`);
205
+ console.log();
206
+ }
207
+ return;
208
+ }
209
+
210
+ const contract = readContract(infernoDir);
211
+ if (!contract) {
212
+ const msg = "No contract.json or capabilities.json found.";
213
+ if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
214
+ else warn(msg);
215
+ process.exit(1);
216
+ }
217
+
218
+ const caps = normaliseCaps(contract);
219
+ const meta = readMeta(infernoDir);
220
+ const outPath = outArg || path.join(cwd, FORMAT_EXT[format]);
221
+
222
+ let output;
223
+ switch (format) {
224
+ case "openapi": output = toOpenApi(caps, meta); break;
225
+ case "backstage": output = toBackstage(caps, meta); break;
226
+ case "csv": output = toCsv(caps); break;
227
+ case "markdown": output = toMarkdown(caps, meta); break;
228
+ case "json": output = toCleanJson(caps, meta); break;
229
+ }
230
+
231
+ fs.writeFileSync(outPath, output);
232
+
233
+ if (jsonMode) {
234
+ console.log(JSON.stringify({ ok: true, format, file: outPath, capabilities: caps.length }));
235
+ } else {
236
+ done(`Exported ${bold(String(caps.length))} capabilities → ${cyan(path.relative(cwd, outPath))}`);
237
+ console.log();
238
+ }
239
+ }
@@ -1,40 +1,90 @@
1
- import*as l from"node:fs";import*as s from"node:path";import{readProfile as k}from"../learning/profile.mjs";import{header as y,ok as w,warn as b,done as $,nextSteps as C,cyan as m,yellow as j}from"../ui/output.mjs";const p=s.join("inferno","generated-skills");function f(e,n,i){return l.existsSync(e)&&!i?(b(`Already exists (use --force to overwrite): ${s.relative(process.cwd(),e)}`),!1):(l.mkdirSync(s.dirname(e),{recursive:!0}),l.writeFileSync(e,n,"utf8"),w(`Generated: ${s.relative(process.cwd(),e)}`),!0)}function S(e,n){const i=e.namingStyle!=="unknown"?e.namingStyle:"PascalCase",r=e.preferredVerbs.length>0?e.preferredVerbs.slice(0,5).join(", "):"Add, Update, Remove",t=e.stack?.framework||"unknown",o=e.featureClusters.slice(0,3),a=e.sessionCount>=20?"experienced":e.sessionCount>=5?"intermediate":"new",c=o.map(u=>`- When touching [${u.slice(0,3).join(", ")}], check whether related capabilities also need updating`).join(`
2
- `);return`# infernoflow \u2014 Cursor Rules (auto-generated)
3
- # Project: ${n?.policyId||"unknown"} | Stack: ${t}
1
+ /**
2
+ * infernoflow generate-skills
3
+ *
4
+ * Reads inferno/developer-profile.json and generates personalised skill files:
5
+ * - inferno/generated-skills/cursor-rules.md → copy to .cursor/rules/infernoflow.md
6
+ * - inferno/generated-skills/quick-restore.md → session startup skill
7
+ * - inferno/generated-skills/naming-guide.md → detected naming conventions
8
+ * - inferno/generated-skills/feature-scaffold.md → personal feature checklist
9
+ *
10
+ * Run: infernoflow generate-skills [--cursor] [--force]
11
+ * --cursor Also copy generated rules to .cursor/rules/infernoflow.md
12
+ * --force Overwrite existing generated files
13
+ */
14
+
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ import { readProfile } from "../learning/profile.mjs";
18
+ import { header, ok, warn, done, nextSteps, cyan, yellow, bold, gray } from "../ui/output.mjs";
19
+
20
+ const SKILLS_DIR = path.join("inferno", "generated-skills");
21
+
22
+ function write(filePath, content, force) {
23
+ if (fs.existsSync(filePath) && !force) {
24
+ warn(`Already exists (use --force to overwrite): ${path.relative(process.cwd(), filePath)}`);
25
+ return false;
26
+ }
27
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
28
+ fs.writeFileSync(filePath, content, "utf8");
29
+ ok(`Generated: ${path.relative(process.cwd(), filePath)}`);
30
+ return true;
31
+ }
32
+
33
+ function buildCursorRules(profile, contract) {
34
+ const naming = profile.namingStyle !== "unknown" ? profile.namingStyle : "PascalCase";
35
+ const verbs = profile.preferredVerbs.length > 0 ? profile.preferredVerbs.slice(0, 5).join(", ") : "Add, Update, Remove";
36
+ const framework = profile.stack?.framework || "unknown";
37
+ const clusters = profile.featureClusters.slice(0, 3);
38
+ const sessionStyle = profile.sessionCount >= 20 ? "experienced" : profile.sessionCount >= 5 ? "intermediate" : "new";
39
+
40
+ const clusterRules = clusters.map(cluster =>
41
+ `- When touching [${cluster.slice(0, 3).join(", ")}], check whether related capabilities also need updating`
42
+ ).join("\n");
43
+
44
+ return `# infernoflow — Cursor Rules (auto-generated)
45
+ # Project: ${contract?.policyId || "unknown"} | Stack: ${framework}
4
46
  # Regenerate with: infernoflow generate-skills --cursor
5
47
 
6
48
  ## Contract awareness
7
49
  - This project uses infernoflow to track capability contracts
8
50
  - After implementing any feature, always call \`infernoflow_run\` then \`infernoflow_apply\`
9
51
  - Run \`infernoflow_check\` before every commit
10
- - Current capabilities: [${(n?.capabilities||[]).join(", ")}]
52
+ - Current capabilities: [${(contract?.capabilities || []).join(", ")}]
11
53
 
12
54
  ## Naming conventions (detected from this developer's history)
13
- - Capability IDs use **${i}** (e.g. ${r.split(", ").slice(0,2).map(u=>u+"Item").join(", ")})
14
- - Preferred action verbs: ${r}
55
+ - Capability IDs use **${naming}** (e.g. ${verbs.split(", ").slice(0, 2).map(v => v + "Item").join(", ")})
56
+ - Preferred action verbs: ${verbs}
15
57
  - Match this style when suggesting new capability names
16
58
 
17
59
  ## Feature clusters (capabilities this developer adds together)
18
- ${c||"- No clusters detected yet \u2014 build more features to train this"}
60
+ ${clusterRules || "- No clusters detected yet build more features to train this"}
19
61
 
20
- ## Session style (${a})
21
- ${a==="experienced"?`- Skip basic explanations \u2014 this developer knows the codebase well
22
- - Be direct and minimal in responses`:`- Include brief context for non-obvious decisions
23
- - Explain infernoflow commands when used`}
62
+ ## Session style (${sessionStyle})
63
+ ${sessionStyle === "experienced"
64
+ ? "- Skip basic explanations — this developer knows the codebase well\n- Be direct and minimal in responses"
65
+ : "- Include brief context for non-obvious decisions\n- Explain infernoflow commands when used"}
24
66
 
25
67
  ## Workflow reminders
26
68
  - Start sessions with: \`infernoflow context --show\`
27
69
  - Use \`infernoflow_git_drift\` to check what's changed before starting work
28
70
  - Use \`infernoflow_implement\` to get a structured coding prompt before writing code
29
- - Changelog entries should be ${e.changelogVerbosity==="detailed"?"detailed (include context and impact)":"brief (one line, action-focused)"}
30
- `}function R(e,n){const r=e.stack?.framework||"unknown",t=(n?.capabilities||[]).slice(0,6);return`# Quick Restore \u2014 ${n?.policyId||"this project"}
71
+ - Changelog entries should be ${profile.changelogVerbosity === "detailed" ? "detailed (include context and impact)" : "brief (one line, action-focused)"}
72
+ `;
73
+ }
74
+
75
+ function buildQuickRestore(profile, contract) {
76
+ const working = ""; // will be filled at runtime from context-state.json
77
+ const framework = profile.stack?.framework || "unknown";
78
+ const capabilities = (contract?.capabilities || []).slice(0, 6);
79
+
80
+ return `# Quick Restore — ${contract?.policyId || "this project"}
31
81
  # Paste this at the start of any new AI session to restore context instantly.
32
82
  # Regenerate with: infernoflow generate-skills
33
83
 
34
84
  ## Project snapshot
35
- - **Project:** ${n?.policyId||"unknown"}
36
- - **Stack:** ${r} / ${e.stack?.language||"unknown"} (${e.stack?.projectType||"unknown"})
37
- - **Capabilities:** ${t.join(", ")}${(n?.capabilities||[]).length>6?` +${(n?.capabilities||[]).length-6} more`:""}
85
+ - **Project:** ${contract?.policyId || "unknown"}
86
+ - **Stack:** ${framework} / ${profile.stack?.language || "unknown"} (${profile.stack?.projectType || "unknown"})
87
+ - **Capabilities:** ${capabilities.join(", ")}${(contract?.capabilities || []).length > 6 ? ` +${(contract?.capabilities || []).length - 6} more` : ""}
38
88
 
39
89
  ## How to start a session
40
90
  1. Run: \`infernoflow context --show\`
@@ -43,42 +93,64 @@ ${a==="experienced"?`- Skip basic explanations \u2014 this developer knows the c
43
93
  4. Pick up where you left off
44
94
 
45
95
  ## infernoflow tools available (in Cursor / VS Code Agent mode)
46
- - \`infernoflow_run\` \u2014 generate a contract update prompt
47
- - \`infernoflow_apply\` \u2014 apply a JSON response
48
- - \`infernoflow_implement\` \u2014 get a structured coding prompt
49
- - \`infernoflow_git_drift\` \u2014 see what capabilities may have drifted
50
- - \`infernoflow_check\` \u2014 validate contract is in sync
51
- - \`infernoflow_status\` \u2014 quick health check
96
+ - \`infernoflow_run\` generate a contract update prompt
97
+ - \`infernoflow_apply\` apply a JSON response
98
+ - \`infernoflow_implement\` get a structured coding prompt
99
+ - \`infernoflow_git_drift\` see what capabilities may have drifted
100
+ - \`infernoflow_check\` validate contract is in sync
101
+ - \`infernoflow_status\` quick health check
52
102
 
53
103
  ## Definition of done (every feature branch)
54
104
  - [ ] Code works as intended
55
- - [ ] \`infernoflow_run\` \u2192 \`infernoflow_apply\` completed
105
+ - [ ] \`infernoflow_run\` \`infernoflow_apply\` completed
56
106
  - [ ] \`infernoflow_check\` passes
57
107
  - [ ] Commit message references the capability changed
58
- `}function _(e){const n=e.namingStyle!=="unknown"?e.namingStyle:"PascalCase",i=e.preferredVerbs.length>0?e.preferredVerbs:["Create","Read","Update","Delete","Search"],r=i.slice(0,5).map(t=>n==="PascalCase"?` - ${t}Item, ${t}Task, ${t}User`:n==="camelCase"?` - ${t.toLowerCase()}Item, ${t.toLowerCase()}Task`:` - ${t.toLowerCase()}-item, ${t.toLowerCase()}-task`).join(`
59
- `);return`# Naming Guide \u2014 auto-generated from your capability history
108
+ `;
109
+ }
110
+
111
+ function buildNamingGuide(profile) {
112
+ const naming = profile.namingStyle !== "unknown" ? profile.namingStyle : "PascalCase";
113
+ const verbs = profile.preferredVerbs.length > 0 ? profile.preferredVerbs : ["Create", "Read", "Update", "Delete", "Search"];
114
+
115
+ const examples = verbs.slice(0, 5).map(v => {
116
+ if (naming === "PascalCase") return ` - ${v}Item, ${v}Task, ${v}User`;
117
+ if (naming === "camelCase") return ` - ${v.toLowerCase()}Item, ${v.toLowerCase()}Task`;
118
+ return ` - ${v.toLowerCase()}-item, ${v.toLowerCase()}-task`;
119
+ }).join("\n");
60
120
 
61
- ## Detected style: ${n}
121
+ return `# Naming Guide — auto-generated from your capability history
122
+
123
+ ## Detected style: ${naming}
62
124
 
63
125
  ### Your preferred action verbs
64
- ${i.map(t=>`- **${t}**`).join(`
65
- `)}
126
+ ${verbs.map(v => `- **${v}**`).join("\n")}
66
127
 
67
128
  ### Examples matching your style
68
- ${r}
129
+ ${examples}
69
130
 
70
131
  ### Rules
71
132
  - All capability IDs in \`inferno/contract.json\` must follow this style
72
- - New capabilities suggested by AI should match \u2014 reject any that don't
133
+ - New capabilities suggested by AI should match reject any that don't
73
134
  - If you rename a capability, update contract.json + capabilities.json + any scenarios
74
135
 
75
136
  ### When naming a new capability, ask:
76
137
  1. Does it describe a single, discrete behavior? (If not, split it)
77
138
  2. Does it start with one of your preferred verbs?
78
- 3. Is it in ${n}?
79
- 4. Is it unique \u2014 not already in contract.json?
80
- `}function I(e,n){const r=e.featureClusters.slice(0,2)[0]||[],t=e.stack?.framework||"unknown",o=r.slice(0,4).map(a=>`- [ ] Does **${a}** need updating? (check inferno/capabilities.json)`).join(`
81
- `);return`# Feature Scaffold \u2014 ${t} project
139
+ 3. Is it in ${naming}?
140
+ 4. Is it unique not already in contract.json?
141
+ `;
142
+ }
143
+
144
+ function buildFeatureScaffold(profile, contract) {
145
+ const clusters = profile.featureClusters.slice(0, 2);
146
+ const topCluster = clusters[0] || [];
147
+ const framework = profile.stack?.framework || "unknown";
148
+
149
+ const clusterChecks = topCluster.slice(0, 4).map(id =>
150
+ `- [ ] Does **${id}** need updating? (check inferno/capabilities.json)`
151
+ ).join("\n");
152
+
153
+ return `# Feature Scaffold — ${framework} project
82
154
  # Use this checklist whenever starting a new feature.
83
155
  # Regenerate with: infernoflow generate-skills
84
156
 
@@ -94,13 +166,13 @@ ${r}
94
166
  - [ ] Verify it works end-to-end
95
167
 
96
168
  ## Capability cluster check
97
- ${o||"- [ ] Review existing capabilities \u2014 do any need updating?"}
169
+ ${clusterChecks || "- [ ] Review existing capabilities do any need updating?"}
98
170
 
99
171
  ## Contract update (required before merge)
100
172
  - [ ] Run \`infernoflow_run\` with a description of what changed
101
173
  - [ ] Review the suggested JSON
102
174
  - [ ] Run \`infernoflow_apply\` with the JSON
103
- - [ ] Run \`infernoflow_check\` \u2014 must pass
175
+ - [ ] Run \`infernoflow_check\` must pass
104
176
 
105
177
  ## Commit message
106
178
  - Reference the capability: "feat: add SearchItems endpoint (#42)"
@@ -110,5 +182,58 @@ ${o||"- [ ] Review existing capabilities \u2014 do any need updating?"}
110
182
  - [ ] Feature works
111
183
  - [ ] \`infernoflow_check\` passes
112
184
  - [ ] PR description mentions which capabilities changed
113
- `}async function A(e){const n=process.cwd(),i=e.includes("--force")||e.includes("-f"),r=e.includes("--cursor");y("generate-skills");const t=s.join(n,"inferno");l.existsSync(t)||(console.error(` \u2718 inferno/ not found \u2014 run: infernoflow init
114
- `),process.exit(1));const o=k(t);let a=null;try{a=JSON.parse(l.readFileSync(s.join(t,"contract.json"),"utf8"))}catch{}const c=s.join(n,p);if(f(s.join(c,"cursor-rules.md"),S(o,a),i),f(s.join(c,"quick-restore.md"),R(o,a),i),f(s.join(c,"naming-guide.md"),_(o),i),f(s.join(c,"feature-scaffold.md"),I(o,a),i),r){const d=s.join(n,".cursor","rules");l.mkdirSync(d,{recursive:!0});const h=s.join(c,"cursor-rules.md"),g=s.join(d,"infernoflow.md");l.copyFileSync(h,g),w("Installed to: .cursor/rules/infernoflow.md")}const u=[o.namingStyle!=="unknown"?`naming: ${o.namingStyle}`:null,o.stack?.framework!=="unknown"?`stack: ${o.stack.framework}`:null,o.sessionCount>0?`sessions: ${o.sessionCount}`:null].filter(Boolean).join(" \xB7 ");$(`Skills generated${u?` (${u})`:""}`),C([`Review files in ${j(p+"/")}`,`Copy to Cursor: ${m("infernoflow generate-skills --cursor")}`,`Re-run any time to refresh after more sessions: ${m("infernoflow generate-skills --force")}`,o.sessionCount<5?`Run more commands to improve personalisation (${o.sessionCount} sessions so far)`:`Profile has ${o.sessionCount} sessions \u2014 personalisation is well-trained`])}export{A as generateSkillsCommand};
185
+ `;
186
+ }
187
+
188
+ export async function generateSkillsCommand(args) {
189
+ const cwd = process.cwd();
190
+ const force = args.includes("--force") || args.includes("-f");
191
+ const installCursor = args.includes("--cursor");
192
+
193
+ header("generate-skills");
194
+
195
+ const infernoDir = path.join(cwd, "inferno");
196
+ if (!fs.existsSync(infernoDir)) {
197
+ console.error(" ✘ inferno/ not found — run: infernoflow init\n");
198
+ process.exit(1);
199
+ }
200
+
201
+ const profile = readProfile(infernoDir);
202
+ let contract = null;
203
+ try { contract = JSON.parse(fs.readFileSync(path.join(infernoDir, "contract.json"), "utf8")); } catch {}
204
+
205
+ const skillsDir = path.join(cwd, SKILLS_DIR);
206
+
207
+ // Generate all four skill files
208
+ write(path.join(skillsDir, "cursor-rules.md"), buildCursorRules(profile, contract), force);
209
+ write(path.join(skillsDir, "quick-restore.md"), buildQuickRestore(profile, contract), force);
210
+ write(path.join(skillsDir, "naming-guide.md"), buildNamingGuide(profile), force);
211
+ write(path.join(skillsDir, "feature-scaffold.md"), buildFeatureScaffold(profile, contract), force);
212
+
213
+ // Optionally install cursor rules
214
+ if (installCursor) {
215
+ const rulesDir = path.join(cwd, ".cursor", "rules");
216
+ fs.mkdirSync(rulesDir, { recursive: true });
217
+ const src = path.join(skillsDir, "cursor-rules.md");
218
+ const dst = path.join(rulesDir, "infernoflow.md");
219
+ fs.copyFileSync(src, dst);
220
+ ok(`Installed to: .cursor/rules/infernoflow.md`);
221
+ }
222
+
223
+ const profileSummary = [
224
+ profile.namingStyle !== "unknown" ? `naming: ${profile.namingStyle}` : null,
225
+ profile.stack?.framework !== "unknown" ? `stack: ${profile.stack.framework}` : null,
226
+ profile.sessionCount > 0 ? `sessions: ${profile.sessionCount}` : null,
227
+ ].filter(Boolean).join(" · ");
228
+
229
+ done(`Skills generated${profileSummary ? ` (${profileSummary})` : ""}`);
230
+
231
+ nextSteps([
232
+ `Review files in ${yellow(SKILLS_DIR + "/")}`,
233
+ `Copy to Cursor: ${cyan("infernoflow generate-skills --cursor")}`,
234
+ `Re-run any time to refresh after more sessions: ${cyan("infernoflow generate-skills --force")}`,
235
+ profile.sessionCount < 5
236
+ ? `Run more commands to improve personalisation (${profile.sessionCount} sessions so far)`
237
+ : `Profile has ${profile.sessionCount} sessions — personalisation is well-trained`,
238
+ ]);
239
+ }