infernoflow 0.32.8 → 0.32.9

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 (78) hide show
  1. package/dist/bin/infernoflow.mjs +84 -255
  2. package/dist/lib/adopters/angular.mjs +1 -128
  3. package/dist/lib/adopters/css.mjs +1 -111
  4. package/dist/lib/adopters/react.mjs +1 -104
  5. package/dist/lib/ai/ideDetection.mjs +1 -31
  6. package/dist/lib/ai/localProvider.mjs +1 -88
  7. package/dist/lib/ai/providerRouter.mjs +2 -295
  8. package/dist/lib/commands/adopt.mjs +20 -869
  9. package/dist/lib/commands/adoptWizard.mjs +9 -320
  10. package/dist/lib/commands/agent.mjs +5 -191
  11. package/dist/lib/commands/ai.mjs +2 -407
  12. package/dist/lib/commands/audit.mjs +13 -300
  13. package/dist/lib/commands/changelog.mjs +26 -594
  14. package/dist/lib/commands/check.mjs +3 -184
  15. package/dist/lib/commands/ci.mjs +3 -208
  16. package/dist/lib/commands/claudeMd.mjs +25 -130
  17. package/dist/lib/commands/cloud.mjs +5 -521
  18. package/dist/lib/commands/context.mjs +31 -287
  19. package/dist/lib/commands/coverage.mjs +2 -282
  20. package/dist/lib/commands/dashboard.mjs +123 -635
  21. package/dist/lib/commands/demo.mjs +8 -465
  22. package/dist/lib/commands/diff.mjs +5 -274
  23. package/dist/lib/commands/docGate.mjs +2 -81
  24. package/dist/lib/commands/doctor.mjs +3 -321
  25. package/dist/lib/commands/explain.mjs +8 -438
  26. package/dist/lib/commands/export.mjs +10 -239
  27. package/dist/lib/commands/generateSkills.mjs +38 -163
  28. package/dist/lib/commands/graph.mjs +203 -321
  29. package/dist/lib/commands/health.mjs +2 -309
  30. package/dist/lib/commands/impact.mjs +2 -325
  31. package/dist/lib/commands/implement.mjs +7 -103
  32. package/dist/lib/commands/init.mjs +23 -475
  33. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  34. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  35. package/dist/lib/commands/link.mjs +2 -342
  36. package/dist/lib/commands/monorepo.mjs +4 -428
  37. package/dist/lib/commands/notify.mjs +4 -258
  38. package/dist/lib/commands/onboard.mjs +4 -296
  39. package/dist/lib/commands/prComment.mjs +2 -361
  40. package/dist/lib/commands/prImpact.mjs +2 -157
  41. package/dist/lib/commands/publish.mjs +15 -316
  42. package/dist/lib/commands/report.mjs +28 -272
  43. package/dist/lib/commands/review.mjs +9 -223
  44. package/dist/lib/commands/run.mjs +8 -336
  45. package/dist/lib/commands/scaffold.mjs +54 -419
  46. package/dist/lib/commands/scan.mjs +5 -558
  47. package/dist/lib/commands/scout.mjs +2 -291
  48. package/dist/lib/commands/setup.mjs +5 -310
  49. package/dist/lib/commands/share.mjs +13 -196
  50. package/dist/lib/commands/snapshot.mjs +3 -383
  51. package/dist/lib/commands/stability.mjs +2 -293
  52. package/dist/lib/commands/status.mjs +4 -172
  53. package/dist/lib/commands/suggest.mjs +21 -563
  54. package/dist/lib/commands/syncAuto.mjs +1 -96
  55. package/dist/lib/commands/synthesize.mjs +10 -228
  56. package/dist/lib/commands/teamSync.mjs +2 -388
  57. package/dist/lib/commands/test.mjs +6 -363
  58. package/dist/lib/commands/version.mjs +2 -282
  59. package/dist/lib/commands/vibe.mjs +7 -357
  60. package/dist/lib/commands/watch.mjs +4 -203
  61. package/dist/lib/commands/why.mjs +4 -358
  62. package/dist/lib/cursorHooksInstall.mjs +1 -60
  63. package/dist/lib/draftToolingInstall.mjs +7 -68
  64. package/dist/lib/git/detect-drift.mjs +4 -208
  65. package/dist/lib/learning/adapt.mjs +6 -101
  66. package/dist/lib/learning/observe.mjs +1 -119
  67. package/dist/lib/learning/patternDetector.mjs +1 -298
  68. package/dist/lib/learning/profile.mjs +2 -279
  69. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  70. package/dist/lib/templates/index.mjs +1 -131
  71. package/dist/lib/ui/errors.mjs +1 -142
  72. package/dist/lib/ui/output.mjs +6 -72
  73. package/dist/lib/ui/prompts.mjs +6 -147
  74. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  75. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  76. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  77. package/dist/templates/github-app/app-manifest.json +20 -0
  78. package/package.json +1 -1
@@ -1,291 +1,2 @@
1
- /**
2
- * infernoflow scout
3
- *
4
- * Scans your source files for undocumented capabilities — functions, routes,
5
- * exports, controllers — that exist in code but are missing from the contract.
6
- *
7
- * Smarter than --adopt: it reads your existing contract, diffs against what
8
- * it finds in source, and only surfaces genuine gaps.
9
- *
10
- * Usage:
11
- * infernoflow scout Scan src/, lib/, app/ (auto-detected)
12
- * infernoflow scout --dir src,api Custom scan directories
13
- * infernoflow scout --apply Write discovered caps to capabilities.json
14
- * infernoflow scout --json Machine-readable output
15
- * infernoflow scout --min-confidence 0.7 Only show high-confidence hits
16
- *
17
- * Output:
18
- * Lists undocumented capabilities with confidence score + source location.
19
- * With --apply, merges them into the contract file.
20
- */
21
-
22
- import * as fs from "node:fs";
23
- import * as path from "node:path";
24
- import { done, warn, info, bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
25
-
26
- // ── Pattern library ───────────────────────────────────────────────────────────
27
-
28
- const PATTERNS = [
29
- // Express / Fastify / Hono routes
30
- {
31
- name: "http-route",
32
- regex: /(?:app|router|server)\s*\.\s*(get|post|put|patch|delete|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
33
- extract: (m) => ({ id: routeToId(m[2], m[1]), hint: `${m[1].toUpperCase()} ${m[2]}`, confidence: 0.85 }),
34
- },
35
- // Next.js / Nuxt API route files (path from filename)
36
- {
37
- name: "api-file",
38
- filePattern: /[/\\](pages[/\\]api|app[/\\]api)[/\\](.+)\.(js|ts|mjs)$/,
39
- extract: (filePath) => {
40
- const m = filePath.match(/[/\\](pages[/\\]api|app[/\\]api)[/\\](.+)\.(js|ts|mjs)$/);
41
- if (!m) return null;
42
- const route = m[2].replace(/[/\\]index$/, "").replace(/\[([^\]]+)\]/g, ":$1");
43
- return { id: routeToId("/" + route, "api"), hint: `Next.js API: /${route}`, confidence: 0.9 };
44
- },
45
- },
46
- // Named exports (functions, classes, consts)
47
- {
48
- name: "export",
49
- regex: /export\s+(?:async\s+)?(?:function|class|const|let)\s+([A-Z][A-Za-z0-9]+)/g,
50
- extract: (m) => ({ id: camelToId(m[1]), hint: `export ${m[1]}`, confidence: 0.65 }),
51
- },
52
- // Default export object keys (controller-style)
53
- {
54
- name: "controller-method",
55
- regex: /^\s{2,}(?:async\s+)?([a-z][A-Za-z0-9]+)\s*[:(]/gm,
56
- extract: (m) => ({ id: camelToId(m[1]), hint: `method ${m[1]}`, confidence: 0.5 }),
57
- minLen: 5,
58
- },
59
- // GraphQL resolver fields
60
- {
61
- name: "graphql-resolver",
62
- regex: /['"`]?([A-Za-z][A-Za-z0-9]+)['"`]?\s*:\s*(?:async\s+)?\([^)]*\)\s*(?:=>|{)/g,
63
- extract: (m) => ({ id: camelToId(m[1]), hint: `resolver ${m[1]}`, confidence: 0.6 }),
64
- },
65
- // Django / Flask URL patterns
66
- {
67
- name: "python-route",
68
- regex: /(?:path|url|re_path)\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*([A-Za-z0-9_.]+)/g,
69
- extract: (m) => ({ id: routeToId(m[1], "view"), hint: `view ${m[2]} @ ${m[1]}`, confidence: 0.8 }),
70
- },
71
- // Rails routes
72
- {
73
- name: "rails-route",
74
- regex: /(?:get|post|put|patch|delete)\s+['"`]([^'"`]+)['"`]\s*,\s*to:\s*['"`]([^'"`]+)['"`]/g,
75
- extract: (m) => ({ id: routeToId(m[1], "rails"), hint: `Rails: ${m[2]}`, confidence: 0.8 }),
76
- },
77
- ];
78
-
79
- // ── Helpers ───────────────────────────────────────────────────────────────────
80
-
81
- function routeToId(route, method) {
82
- return route
83
- .replace(/^\//, "")
84
- .replace(/\/:?[^/]+/g, "") // strip params
85
- .replace(/\//g, "-")
86
- .replace(/[^a-zA-Z0-9-]/g, "")
87
- .replace(/-+/g, "-")
88
- .replace(/^-|-$/g, "")
89
- .toLowerCase() || method.toLowerCase() + "-root";
90
- }
91
-
92
- function camelToId(name) {
93
- return name
94
- .replace(/([A-Z])/g, "-$1")
95
- .toLowerCase()
96
- .replace(/^-/, "")
97
- .replace(/-+/g, "-");
98
- }
99
-
100
- // ── File scanner ──────────────────────────────────────────────────────────────
101
-
102
- const SOURCE_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".py", ".rb"]);
103
- const IGNORE_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "__pycache__", "vendor", "coverage"]);
104
-
105
- function* walkFiles(dir) {
106
- if (!fs.existsSync(dir)) return;
107
- const entries = fs.readdirSync(dir, { withFileTypes: true });
108
- for (const e of entries) {
109
- if (IGNORE_DIRS.has(e.name)) continue;
110
- const full = path.join(dir, e.name);
111
- if (e.isDirectory()) {
112
- yield* walkFiles(full);
113
- } else if (e.isFile() && SOURCE_EXTENSIONS.has(path.extname(e.name))) {
114
- yield full;
115
- }
116
- }
117
- }
118
-
119
- function scanFile(filePath) {
120
- const hits = [];
121
-
122
- // File-pattern rules (e.g. Next.js API files)
123
- for (const pat of PATTERNS) {
124
- if (pat.filePattern && pat.filePattern.test(filePath)) {
125
- const result = pat.extract(filePath);
126
- if (result) hits.push({ ...result, source: filePath, pattern: pat.name });
127
- }
128
- }
129
-
130
- let content;
131
- try { content = fs.readFileSync(filePath, "utf8"); } catch { return hits; }
132
-
133
- for (const pat of PATTERNS) {
134
- if (!pat.regex) continue;
135
- pat.regex.lastIndex = 0;
136
- let m;
137
- while ((m = pat.regex.exec(content)) !== null) {
138
- const result = pat.extract(m);
139
- if (!result) continue;
140
- if (pat.minLen && result.id.length < pat.minLen) continue;
141
- hits.push({ ...result, source: filePath, pattern: pat.name });
142
- }
143
- }
144
-
145
- return hits;
146
- }
147
-
148
- // ── Gap detection ─────────────────────────────────────────────────────────────
149
-
150
- function readContract(infernoDir) {
151
- for (const f of ["capabilities.json", "contract.json"]) {
152
- const p = path.join(infernoDir, f);
153
- if (!fs.existsSync(p)) continue;
154
- try { return { file: f, data: JSON.parse(fs.readFileSync(p, "utf8")) }; } catch {}
155
- }
156
- return null;
157
- }
158
-
159
- function getKnownIds(contract) {
160
- const caps = contract?.data?.capabilities || [];
161
- return new Set(caps.map(c => (typeof c === "string" ? c : c.id || c.name || "").toLowerCase()));
162
- }
163
-
164
- function dedupeHits(hits) {
165
- const seen = new Map();
166
- for (const h of hits) {
167
- const key = h.id;
168
- const existing = seen.get(key);
169
- if (!existing || h.confidence > existing.confidence) {
170
- seen.set(key, h);
171
- }
172
- }
173
- return [...seen.values()].sort((a, b) => b.confidence - a.confidence);
174
- }
175
-
176
- // ── Apply ─────────────────────────────────────────────────────────────────────
177
-
178
- function applyToContract(infernoDir, contract, newCaps) {
179
- const caps = contract.data.capabilities || [];
180
- const added = [];
181
-
182
- for (const cap of newCaps) {
183
- const entry = {
184
- id: cap.id,
185
- description: cap.hint,
186
- since: new Date().toISOString().slice(0, 10),
187
- source: "scout",
188
- };
189
- caps.push(entry);
190
- added.push(cap.id);
191
- }
192
-
193
- contract.data.capabilities = caps;
194
- fs.writeFileSync(
195
- path.join(infernoDir, contract.file),
196
- JSON.stringify(contract.data, null, 2) + "\n"
197
- );
198
- return added;
199
- }
200
-
201
- // ── Entry ─────────────────────────────────────────────────────────────────────
202
-
203
- export async function scoutCommand(rawArgs) {
204
- const args = rawArgs.slice(1);
205
- const jsonMode = args.includes("--json");
206
- const apply = args.includes("--apply");
207
- const cwd = process.cwd();
208
- const infernoDir = path.join(cwd, "inferno");
209
-
210
- if (!fs.existsSync(infernoDir)) {
211
- const msg = "inferno/ not found. Run: infernoflow init";
212
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
213
- else warn(msg);
214
- process.exit(1);
215
- }
216
-
217
- // Parse --dir flag
218
- const dirIdx = args.indexOf("--dir");
219
- const scanDirs = dirIdx !== -1
220
- ? args[dirIdx + 1].split(",").map(d => path.resolve(cwd, d.trim()))
221
- : ["src", "lib", "app", "api", "pages", "routes", "controllers", "handlers"]
222
- .map(d => path.join(cwd, d))
223
- .filter(fs.existsSync);
224
-
225
- // Parse --min-confidence
226
- const confIdx = args.indexOf("--min-confidence");
227
- const minConfidence = confIdx !== -1 ? parseFloat(args[confIdx + 1]) : 0.6;
228
-
229
- if (!scanDirs.length) {
230
- const msg = "No source directories found. Use --dir src,lib,api";
231
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
232
- else warn(msg);
233
- process.exit(1);
234
- }
235
-
236
- const contract = readContract(infernoDir);
237
- const knownIds = getKnownIds(contract);
238
-
239
- if (!jsonMode) {
240
- info(`Scanning ${scanDirs.map(d => path.relative(cwd, d)).join(", ")} …`);
241
- console.log();
242
- }
243
-
244
- // Collect hits from all source dirs
245
- const allHits = [];
246
- for (const dir of scanDirs) {
247
- for (const file of walkFiles(dir)) {
248
- allHits.push(...scanFile(file));
249
- }
250
- }
251
-
252
- // Filter: unknown, above confidence threshold, non-trivial id
253
- const candidates = dedupeHits(allHits).filter(h =>
254
- !knownIds.has(h.id.toLowerCase()) &&
255
- h.confidence >= minConfidence &&
256
- h.id.length >= 3 &&
257
- !["index", "app", "main", "root", "default", "handler"].includes(h.id)
258
- );
259
-
260
- if (jsonMode) {
261
- console.log(JSON.stringify({ ok: true, found: candidates.length, candidates }));
262
- return;
263
- }
264
-
265
- if (!candidates.length) {
266
- done("No undocumented capabilities found — contract looks complete.");
267
- return;
268
- }
269
-
270
- console.log(` ${bold(`${candidates.length} undocumented capability candidate${candidates.length !== 1 ? "s" : ""} found`)}`);
271
- console.log();
272
-
273
- const w = Math.max(...candidates.map(c => c.id.length), 10) + 2;
274
- for (const c of candidates) {
275
- const confStr = `${Math.round(c.confidence * 100)}%`;
276
- const confColor = c.confidence >= 0.8 ? green : c.confidence >= 0.65 ? yellow : gray;
277
- const rel = path.relative(cwd, c.source);
278
- console.log(` ${bold(c.id.padEnd(w))} ${confColor(confStr.padEnd(5))} ${gray(c.hint)}`);
279
- console.log(` ${" ".repeat(w + 7)}${gray(rel)}`);
280
- }
281
- console.log();
282
-
283
- if (apply) {
284
- const added = applyToContract(infernoDir, contract, candidates);
285
- done(`Added ${bold(String(added.length))} capabilities to ${contract.file}`);
286
- console.log();
287
- } else {
288
- info(`Run with ${cyan("--apply")} to add these to your contract.`);
289
- console.log();
290
- }
291
- }
1
+ import*as l from"node:fs";import*as r from"node:path";import{done as w,warn as j,info as b,bold as h,cyan as _,gray as x,green as F,yellow as L}from"../ui/output.mjs";const C=[{name:"http-route",regex:/(?:app|router|server)\s*\.\s*(get|post|put|patch|delete|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/gi,extract:e=>({id:p(e[2],e[1]),hint:`${e[1].toUpperCase()} ${e[2]}`,confidence:.85})},{name:"api-file",filePattern:/[/\\](pages[/\\]api|app[/\\]api)[/\\](.+)\.(js|ts|mjs)$/,extract:e=>{const n=e.match(/[/\\](pages[/\\]api|app[/\\]api)[/\\](.+)\.(js|ts|mjs)$/);if(!n)return null;const t=n[2].replace(/[/\\]index$/,"").replace(/\[([^\]]+)\]/g,":$1");return{id:p("/"+t,"api"),hint:`Next.js API: /${t}`,confidence:.9}}},{name:"export",regex:/export\s+(?:async\s+)?(?:function|class|const|let)\s+([A-Z][A-Za-z0-9]+)/g,extract:e=>({id:m(e[1]),hint:`export ${e[1]}`,confidence:.65})},{name:"controller-method",regex:/^\s{2,}(?:async\s+)?([a-z][A-Za-z0-9]+)\s*[:(]/gm,extract:e=>({id:m(e[1]),hint:`method ${e[1]}`,confidence:.5}),minLen:5},{name:"graphql-resolver",regex:/['"`]?([A-Za-z][A-Za-z0-9]+)['"`]?\s*:\s*(?:async\s+)?\([^)]*\)\s*(?:=>|{)/g,extract:e=>({id:m(e[1]),hint:`resolver ${e[1]}`,confidence:.6})},{name:"python-route",regex:/(?:path|url|re_path)\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*([A-Za-z0-9_.]+)/g,extract:e=>({id:p(e[1],"view"),hint:`view ${e[2]} @ ${e[1]}`,confidence:.8})},{name:"rails-route",regex:/(?:get|post|put|patch|delete)\s+['"`]([^'"`]+)['"`]\s*,\s*to:\s*['"`]([^'"`]+)['"`]/g,extract:e=>({id:p(e[1],"rails"),hint:`Rails: ${e[2]}`,confidence:.8})}];function p(e,n){return e.replace(/^\//,"").replace(/\/:?[^/]+/g,"").replace(/\//g,"-").replace(/[^a-zA-Z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase()||n.toLowerCase()+"-root"}function m(e){return e.replace(/([A-Z])/g,"-$1").toLowerCase().replace(/^-/,"").replace(/-+/g,"-")}const Z=new Set([".js",".mjs",".cjs",".ts",".tsx",".jsx",".py",".rb"]),k=new Set(["node_modules",".git","dist","build",".next","__pycache__","vendor","coverage"]);function*I(e){if(!l.existsSync(e))return;const n=l.readdirSync(e,{withFileTypes:!0});for(const t of n){if(k.has(t.name))continue;const s=r.join(e,t.name);t.isDirectory()?yield*I(s):t.isFile()&&Z.has(r.extname(t.name))&&(yield s)}}function z(e){const n=[];for(const s of C)if(s.filePattern&&s.filePattern.test(e)){const i=s.extract(e);i&&n.push({...i,source:e,pattern:s.name})}let t;try{t=l.readFileSync(e,"utf8")}catch{return n}for(const s of C){if(!s.regex)continue;s.regex.lastIndex=0;let i;for(;(i=s.regex.exec(t))!==null;){const c=s.extract(i);c&&(s.minLen&&c.id.length<s.minLen||n.push({...c,source:e,pattern:s.name}))}}return n}function E(e){for(const n of["capabilities.json","contract.json"]){const t=r.join(e,n);if(l.existsSync(t))try{return{file:n,data:JSON.parse(l.readFileSync(t,"utf8"))}}catch{}}return null}function R(e){const n=e?.data?.capabilities||[];return new Set(n.map(t=>(typeof t=="string"?t:t.id||t.name||"").toLowerCase()))}function T(e){const n=new Map;for(const t of e){const s=t.id,i=n.get(s);(!i||t.confidence>i.confidence)&&n.set(s,t)}return[...n.values()].sort((t,s)=>s.confidence-t.confidence)}function D(e,n,t){const s=n.data.capabilities||[],i=[];for(const c of t){const d={id:c.id,description:c.hint,since:new Date().toISOString().slice(0,10),source:"scout"};s.push(d),i.push(c.id)}return n.data.capabilities=s,l.writeFileSync(r.join(e,n.file),JSON.stringify(n.data,null,2)+`
2
+ `),i}async function U(e){const n=e.slice(1),t=n.includes("--json"),s=n.includes("--apply"),i=process.cwd(),c=r.join(i,"inferno");if(!l.existsSync(c)){const o="inferno/ not found. Run: infernoflow init";t?console.log(JSON.stringify({ok:!1,error:o})):j(o),process.exit(1)}const d=n.indexOf("--dir"),f=d!==-1?n[d+1].split(",").map(o=>r.resolve(i,o.trim())):["src","lib","app","api","pages","routes","controllers","handlers"].map(o=>r.join(i,o)).filter(l.existsSync),y=n.indexOf("--min-confidence"),N=y!==-1?parseFloat(n[y+1]):.6;if(!f.length){const o="No source directories found. Use --dir src,lib,api";t?console.log(JSON.stringify({ok:!1,error:o})):j(o),process.exit(1)}const u=E(c),v=R(u);t||(b(`Scanning ${f.map(o=>r.relative(i,o)).join(", ")} \u2026`),console.log());const $=[];for(const o of f)for(const g of I(o))$.push(...z(g));const a=T($).filter(o=>!v.has(o.id.toLowerCase())&&o.confidence>=N&&o.id.length>=3&&!["index","app","main","root","default","handler"].includes(o.id));if(t){console.log(JSON.stringify({ok:!0,found:a.length,candidates:a}));return}if(!a.length){w("No undocumented capabilities found \u2014 contract looks complete.");return}console.log(` ${h(`${a.length} undocumented capability candidate${a.length!==1?"s":""} found`)}`),console.log();const S=Math.max(...a.map(o=>o.id.length),10)+2;for(const o of a){const g=`${Math.round(o.confidence*100)}%`,A=o.confidence>=.8?F:o.confidence>=.65?L:x,O=r.relative(i,o.source);console.log(` ${h(o.id.padEnd(S))} ${A(g.padEnd(5))} ${x(o.hint)}`),console.log(` ${" ".repeat(S+7)}${x(O)}`)}if(console.log(),s){const o=D(c,u,a);w(`Added ${h(String(o.length))} capabilities to ${u.file}`),console.log()}else b(`Run with ${_("--apply")} to add these to your contract.`),console.log()}export{U as scoutCommand};
@@ -1,310 +1,5 @@
1
- /**
2
- * infernoflow setup
3
- * One command that gets a project fully operational:
4
- * 1. Detects IDE (Cursor / VS Code / other)
5
- * 2. Runs `infernoflow init --adopt` if inferno/ doesn't exist yet
6
- * 3. Copies MCP server + hooks
7
- * 4. Auto-updates ~/.claude.json for Claude Code (VS Code extension):
8
- * - Registers MCP server under mcpServers.infernoflow
9
- * - Pre-approves all tools under projects[cwd].allowedTools ← kills prompts
10
- * 5. Writes .claude/settings.json with pre-approved tools (CLI fallback)
11
- */
12
-
13
- import * as fs from "node:fs";
14
- import * as path from "node:path";
15
- import * as os from "node:os";
16
- import { fileURLToPath } from "node:url";
17
- import { execSync } from "node:child_process";
18
- import { detectIdeContext } from "../ai/ideDetection.mjs";
19
- import { header, ok, warn, info, done, cyan, yellow, bold, green } from "../ui/output.mjs";
20
- import { installCursorHooksArtifacts } from "../cursorHooksInstall.mjs";
21
- import { installVsCodeCopilotHooksArtifacts } from "../vsCodeCopilotHooksInstall.mjs";
22
- import { writeClaudeMd } from "./claudeMd.mjs";
23
-
24
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
-
26
- function getTemplatesRoot() {
27
- return path.resolve(__dirname, "../../templates");
28
- }
29
-
30
- function runInferno(args) {
31
- try {
32
- return execSync(`npx infernoflow ${args}`, {
33
- encoding: "utf8",
34
- cwd: process.cwd(),
35
- timeout: 60_000,
36
- stdio: ["inherit", "pipe", "pipe"],
37
- });
38
- } catch (err) {
39
- return err.stdout || err.stderr || err.message;
40
- }
41
- }
42
-
43
- // ── MCP tool names (must match inferno-mcp-server.mjs) ───────────────────────
44
- const MCP_TOOLS = [
45
- "infernoflow_status",
46
- "infernoflow_run",
47
- "infernoflow_suggest",
48
- "infernoflow_check",
49
- "infernoflow_context",
50
- "infernoflow_implement",
51
- "infernoflow_git_drift",
52
- "infernoflow_scan_ui",
53
- "infernoflow_review",
54
- "infernoflow_synthesize",
55
- "infernoflow_agent_list",
56
- "infernoflow_agent_run",
57
- "infernoflow_version",
58
- ];
59
-
60
- // ── Git hooks installer ───────────────────────────────────────────────────────
61
- function installGitHooks(cwd, templatesRoot, force) {
62
- const gitDir = path.join(cwd, ".git");
63
- const hooksDir = path.join(gitDir, "hooks");
64
-
65
- if (!fs.existsSync(gitDir)) {
66
- return { skipped: true, reason: "no .git directory" };
67
- }
68
-
69
- fs.mkdirSync(hooksDir, { recursive: true });
70
-
71
- const hooks = ["post-commit", "pre-push"];
72
- const installed = [];
73
-
74
- for (const hookName of hooks) {
75
- const src = path.join(templatesRoot, "git-hooks", hookName);
76
- const dst = path.join(hooksDir, hookName);
77
-
78
- if (!fs.existsSync(src)) continue;
79
-
80
- if (fs.existsSync(dst) && !force) {
81
- // Append our hook to existing one rather than overwriting
82
- const existing = fs.readFileSync(dst, "utf8");
83
- const marker = "# infernoflow";
84
- if (!existing.includes(marker)) {
85
- const hookContent = fs.readFileSync(src, "utf8");
86
- // Append the infernoflow block after the existing content
87
- fs.appendFileSync(dst, `\n${hookContent}`);
88
- installed.push(`${hookName} (appended)`);
89
- }
90
- // Already has infernoflow — skip silently
91
- } else {
92
- fs.copyFileSync(src, dst);
93
- // Make executable
94
- try { fs.chmodSync(dst, 0o755); } catch {}
95
- installed.push(hookName);
96
- }
97
- }
98
-
99
- return { skipped: false, installed };
100
- }
101
-
102
- // ── ~/.claude.json updater (Claude Code for VS Code) ─────────────────────────
103
- //
104
- // Claude Code VS Code stores two kinds of data in ~/.claude.json:
105
- // mcpServers → global MCP server registry
106
- // projects[path] → per-project settings incl. allowedTools
107
- //
108
- // Writing allowedTools under projects[cwd] is the same thing the extension
109
- // does when you click "Yes, allow for this project" — so setup pre-fills it.
110
- //
111
- function updateClaudeJson(mcpServerAbsPath, projectPath, allowedToolNames) {
112
- const claudeJsonPath = path.join(os.homedir(), ".claude.json");
113
-
114
- let config = {};
115
- if (fs.existsSync(claudeJsonPath)) {
116
- try {
117
- const raw = fs.readFileSync(claudeJsonPath, "utf8").replace(/\u0000+/g, "");
118
- config = JSON.parse(raw);
119
- } catch { config = {}; }
120
- }
121
-
122
- let changed = false;
123
-
124
- // ── 1. MCP server registration ────────────────────────────────────────────
125
- if (!config.mcpServers) config.mcpServers = {};
126
- const existingSrv = config.mcpServers.infernoflow;
127
- if (!existingSrv || !existingSrv.args || existingSrv.args[0] !== mcpServerAbsPath) {
128
- config.mcpServers.infernoflow = { command: "node", args: [mcpServerAbsPath] };
129
- changed = true;
130
- }
131
-
132
- // ── 2. Per-project tool approvals ─────────────────────────────────────────
133
- // This mirrors exactly what Claude Code writes when user clicks
134
- // "Yes, allow <tool> for this project (just you)".
135
- if (!config.projects) config.projects = {};
136
- if (!config.projects[projectPath]) config.projects[projectPath] = {};
137
-
138
- const proj = config.projects[projectPath];
139
- const existingAllowed = new Set(proj.allowedTools || []);
140
- const sizeBefore = existingAllowed.size;
141
- for (const name of allowedToolNames) existingAllowed.add(name);
142
-
143
- if (existingAllowed.size !== sizeBefore || proj.allowedTools === undefined) {
144
- proj.allowedTools = [...existingAllowed];
145
- changed = true;
146
- }
147
-
148
- // Strip null bytes before writing (Windows artifact)
149
- const content = JSON.stringify(config, null, 2).replace(/\u0000+/g, "");
150
- fs.writeFileSync(claudeJsonPath, content, "utf8");
151
- return { changed, mcpUpdated: !existingSrv || existingSrv.args[0] !== mcpServerAbsPath };
152
- }
153
-
154
- // ── .claude/settings.json (auto-approve tools) ───────────────────────────────
155
- function writeClaudeSettings(cwd, force) {
156
- const settingsDir = path.join(cwd, ".claude");
157
- const settingsPath = path.join(settingsDir, "settings.json");
158
-
159
- let existing = {};
160
- if (fs.existsSync(settingsPath)) {
161
- try { existing = JSON.parse(fs.readFileSync(settingsPath, "utf8")); }
162
- catch { existing = {}; }
163
- }
164
-
165
- // Build allowedTools — add infernoflow tools, keep any existing entries
166
- const existingAllowed = new Set(existing.allowedTools || []);
167
- for (const tool of MCP_TOOLS) {
168
- existingAllowed.add(`mcp__infernoflow__${tool}`);
169
- }
170
-
171
- const updated = { ...existing, allowedTools: [...existingAllowed] };
172
-
173
- // Also keep mcpServers if it was in the project settings
174
- fs.mkdirSync(settingsDir, { recursive: true });
175
- fs.writeFileSync(settingsPath, JSON.stringify(updated, null, 2), "utf8");
176
- return settingsPath;
177
- }
178
-
179
- // ── main ─────────────────────────────────────────────────────────────────────
180
-
181
- export async function setupCommand(args) {
182
- const cwd = process.cwd();
183
- const force = args.includes("--force") || args.includes("-f");
184
- const yes = args.includes("--yes") || args.includes("-y");
185
- const templatesRoot = getTemplatesRoot();
186
-
187
- header("infernoflow setup");
188
-
189
- // ── 1. Detect IDE ─────────────────────────────────────────────────────────
190
- const { ideDetected } = detectIdeContext("auto");
191
- const ideLabel = ideDetected === "cursor" ? "Cursor"
192
- : ideDetected === "vscode" ? "VS Code"
193
- : ideDetected === "windsurf" ? "Windsurf"
194
- : "unknown";
195
-
196
- info(`IDE detected: ${bold(ideLabel)}`);
197
-
198
- // ── 2. Init if needed ─────────────────────────────────────────────────────
199
- const infernoDir = path.join(cwd, "inferno");
200
- const contractPath = path.join(infernoDir, "contract.json");
201
-
202
- if (!fs.existsSync(contractPath)) {
203
- console.log(`\n ${yellow("inferno/")} not found — running init --adopt ...\n`);
204
- const initArgs = ["--adopt", yes ? "--yes" : ""].filter(Boolean).join(" ");
205
- runInferno(`init ${initArgs}`);
206
- } else {
207
- ok("inferno/contract.json already exists — skipping init");
208
- }
209
-
210
- // ── 3. Install hooks + MCP server file ───────────────────────────────────
211
- const logOk = (msg) => ok(msg);
212
- const logWarn = (msg) => warn(msg);
213
-
214
- // Always install Cursor artifacts (MCP server works for both Cursor + VS Code)
215
- installCursorHooksArtifacts({ cwd, templatesRoot, force, silent: false, logOk, logWarn });
216
-
217
- // Also copy MCP server into .cursor/ so it's findable by absolute path
218
- const srcMcp = path.join(templatesRoot, "cursor", "inferno-mcp-server.mjs");
219
- const dstMcp = path.join(cwd, ".cursor", "inferno-mcp-server.mjs");
220
- if (!fs.existsSync(dstMcp) || force) {
221
- fs.mkdirSync(path.dirname(dstMcp), { recursive: true });
222
- fs.copyFileSync(srcMcp, dstMcp);
223
- ok("Copied MCP server → .cursor/inferno-mcp-server.mjs");
224
- }
225
-
226
- if (ideDetected === "vscode") {
227
- installVsCodeCopilotHooksArtifacts({ cwd, templatesRoot, force, silent: false, logOk, logWarn });
228
- }
229
-
230
- // ── 4. Auto-update ~/.claude.json for Claude Code VS Code ────────────────
231
- console.log();
232
- info("Configuring Claude Code (VS Code extension)...");
233
-
234
- const mcpAbsPath = dstMcp; // absolute path to .cursor/inferno-mcp-server.mjs
235
- const allowedTools = MCP_TOOLS.map(t => `mcp__infernoflow__${t}`);
236
-
237
- try {
238
- const result = updateClaudeJson(mcpAbsPath, cwd, allowedTools);
239
- if (result.mcpUpdated) {
240
- ok(`~/.claude.json → MCP server registered`);
241
- } else {
242
- ok(`~/.claude.json → MCP server already registered`);
243
- }
244
- ok(`~/.claude.json → ${allowedTools.length} infernoflow tools pre-approved for this project`);
245
- } catch (err) {
246
- warn(`Could not update ~/.claude.json: ${err.message}`);
247
- warn(`Add manually: ${cyan('"mcpServers": { "infernoflow": { "command": "node", "args": ["' + mcpAbsPath + '"] } }')}`);
248
- }
249
-
250
- // ── 5. Write .claude/settings.json (CLI / non-interactive fallback) ───────
251
- try {
252
- writeClaudeSettings(cwd, force);
253
- ok(`.claude/settings.json → tools pre-approved (CLI fallback)`);
254
- } catch (err) {
255
- warn(`Could not write .claude/settings.json: ${err.message}`);
256
- }
257
-
258
- // ── 6. Generate CLAUDE.md (invisible instruction layer) ──────────────────
259
- console.log();
260
- info("Installing invisible AI behavior layer...");
261
- try {
262
- const claudeMdResult = writeClaudeMd(cwd, infernoDir, { force });
263
- ok(`CLAUDE.md → ${claudeMdResult.action} (auto-behavior instructions for Claude)`);
264
- } catch (err) {
265
- warn(`Could not write CLAUDE.md: ${err.message}`);
266
- }
267
-
268
- // ── 7. Install git hooks ──────────────────────────────────────────────────
269
- try {
270
- const hooksResult = installGitHooks(cwd, templatesRoot, force);
271
- if (hooksResult.skipped) {
272
- warn(`Git hooks skipped: ${hooksResult.reason}`);
273
- } else if (hooksResult.installed.length > 0) {
274
- ok(`Git hooks → ${hooksResult.installed.join(", ")} installed`);
275
- } else {
276
- ok(`Git hooks → already installed (use --force to overwrite)`);
277
- }
278
- } catch (err) {
279
- warn(`Could not install git hooks: ${err.message}`);
280
- }
281
-
282
- // ── 8. Verify contract ────────────────────────────────────────────────────
283
- let capCount = 0;
284
- try {
285
- const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
286
- capCount = (contract.capabilities || []).length;
287
- } catch {}
288
-
289
- // ── 9. Summary ────────────────────────────────────────────────────────────
290
- console.log();
291
- done(
292
- capCount > 0
293
- ? `infernoflow ready — ${capCount} capabilities tracked`
294
- : `infernoflow ready`
295
- );
296
-
297
- console.log(`\n ${bold("What was set up:")}`);
298
- console.log(` ${green("✔")} MCP server → .cursor/inferno-mcp-server.mjs`);
299
- console.log(` ${green("✔")} ~/.claude.json → MCP registered + ${allowedTools.length} tools pre-approved (no prompts)`);
300
- console.log(` ${green("✔")} .claude/settings.json → CLI fallback approvals`);
301
- console.log(` ${green("✔")} CLAUDE.md → Claude auto-calls infernoflow silently every session`);
302
- console.log(` ${green("✔")} Git hooks → post-commit (changelog) + pre-push (drift check)`);
303
- console.log();
304
- console.log(` ${bold("You're done.")} Just write code — infernoflow handles itself.`);
305
- console.log(` Claude automatically tracks capabilities, updates the contract,`);
306
- console.log(` and synthesizes agents from your workflow patterns.`);
307
- console.log();
308
- console.log(` ${bold("Restart VS Code")} to activate the MCP server.`);
309
- console.log();
310
- }
1
+ import*as n from"node:fs";import*as r from"node:path";import*as $ from"node:os";import{fileURLToPath as x}from"node:url";import{execSync as M}from"node:child_process";import{detectIdeContext as b}from"../ai/ideDetection.mjs";import{header as F,ok as p,warn as m,info as j,done as A,cyan as D,yellow as L,bold as S,green as h}from"../ui/output.mjs";import{installCursorHooksArtifacts as T}from"../cursorHooksInstall.mjs";import{installVsCodeCopilotHooksArtifacts as J}from"../vsCodeCopilotHooksInstall.mjs";import{writeClaudeMd as O}from"./claudeMd.mjs";const P=r.dirname(x(import.meta.url));function I(){return r.resolve(P,"../../templates")}function N(s){try{return M(`npx infernoflow ${s}`,{encoding:"utf8",cwd:process.cwd(),timeout:6e4,stdio:["inherit","pipe","pipe"]})}catch(t){return t.stdout||t.stderr||t.message}}const _=["infernoflow_status","infernoflow_run","infernoflow_suggest","infernoflow_check","infernoflow_context","infernoflow_implement","infernoflow_git_drift","infernoflow_scan_ui","infernoflow_review","infernoflow_synthesize","infernoflow_agent_list","infernoflow_agent_run","infernoflow_version"];function R(s,t,i){const c=r.join(s,".git"),e=r.join(c,"hooks");if(!n.existsSync(c))return{skipped:!0,reason:"no .git directory"};n.mkdirSync(e,{recursive:!0});const l=["post-commit","pre-push"],d=[];for(const a of l){const f=r.join(t,"git-hooks",a),u=r.join(e,a);if(n.existsSync(f))if(n.existsSync(u)&&!i){if(!n.readFileSync(u,"utf8").includes("# infernoflow")){const w=n.readFileSync(f,"utf8");n.appendFileSync(u,`
2
+ ${w}`),d.push(`${a} (appended)`)}}else{n.copyFileSync(f,u);try{n.chmodSync(u,493)}catch{}d.push(a)}}return{skipped:!1,installed:d}}function U(s,t,i){const c=r.join($.homedir(),".claude.json");let e={};if(n.existsSync(c))try{const g=n.readFileSync(c,"utf8").replace(/\u0000+/g,"");e=JSON.parse(g)}catch{e={}}let l=!1;e.mcpServers||(e.mcpServers={});const d=e.mcpServers.infernoflow;(!d||!d.args||d.args[0]!==s)&&(e.mcpServers.infernoflow={command:"node",args:[s]},l=!0),e.projects||(e.projects={}),e.projects[t]||(e.projects[t]={});const a=e.projects[t],f=new Set(a.allowedTools||[]),u=f.size;for(const g of i)f.add(g);(f.size!==u||a.allowedTools===void 0)&&(a.allowedTools=[...f],l=!0);const y=JSON.stringify(e,null,2).replace(/\u0000+/g,"");return n.writeFileSync(c,y,"utf8"),{changed:l,mcpUpdated:!d||d.args[0]!==s}}function z(s,t){const i=r.join(s,".claude"),c=r.join(i,"settings.json");let e={};if(n.existsSync(c))try{e=JSON.parse(n.readFileSync(c,"utf8"))}catch{e={}}const l=new Set(e.allowedTools||[]);for(const a of _)l.add(`mcp__infernoflow__${a}`);const d={...e,allowedTools:[...l]};return n.mkdirSync(i,{recursive:!0}),n.writeFileSync(c,JSON.stringify(d,null,2),"utf8"),c}async function q(s){const t=process.cwd(),i=s.includes("--force")||s.includes("-f"),c=s.includes("--yes")||s.includes("-y"),e=I();F("infernoflow setup");const{ideDetected:l}=b("auto");j(`IDE detected: ${S(l==="cursor"?"Cursor":l==="vscode"?"VS Code":l==="windsurf"?"Windsurf":"unknown")}`);const a=r.join(t,"inferno"),f=r.join(a,"contract.json");if(n.existsSync(f))p("inferno/contract.json already exists \u2014 skipping init");else{console.log(`
3
+ ${L("inferno/")} not found \u2014 running init --adopt ...
4
+ `);const o=["--adopt",c?"--yes":""].filter(Boolean).join(" ");N(`init ${o}`)}const u=o=>p(o),y=o=>m(o);T({cwd:t,templatesRoot:e,force:i,silent:!1,logOk:u,logWarn:y});const g=r.join(e,"cursor","inferno-mcp-server.mjs"),w=r.join(t,".cursor","inferno-mcp-server.mjs");(!n.existsSync(w)||i)&&(n.mkdirSync(r.dirname(w),{recursive:!0}),n.copyFileSync(g,w),p("Copied MCP server \u2192 .cursor/inferno-mcp-server.mjs")),l==="vscode"&&J({cwd:t,templatesRoot:e,force:i,silent:!1,logOk:u,logWarn:y}),console.log(),j("Configuring Claude Code (VS Code extension)...");const v=w,C=_.map(o=>`mcp__infernoflow__${o}`);try{U(v,t,C).mcpUpdated?p("~/.claude.json \u2192 MCP server registered"):p("~/.claude.json \u2192 MCP server already registered"),p(`~/.claude.json \u2192 ${C.length} infernoflow tools pre-approved for this project`)}catch(o){m(`Could not update ~/.claude.json: ${o.message}`),m(`Add manually: ${D('"mcpServers": { "infernoflow": { "command": "node", "args": ["'+v+'"] } }')}`)}try{z(t,i),p(".claude/settings.json \u2192 tools pre-approved (CLI fallback)")}catch(o){m(`Could not write .claude/settings.json: ${o.message}`)}console.log(),j("Installing invisible AI behavior layer...");try{const o=O(t,a,{force:i});p(`CLAUDE.md \u2192 ${o.action} (auto-behavior instructions for Claude)`)}catch(o){m(`Could not write CLAUDE.md: ${o.message}`)}try{const o=R(t,e,i);o.skipped?m(`Git hooks skipped: ${o.reason}`):o.installed.length>0?p(`Git hooks \u2192 ${o.installed.join(", ")} installed`):p("Git hooks \u2192 already installed (use --force to overwrite)")}catch(o){m(`Could not install git hooks: ${o.message}`)}let k=0;try{k=(JSON.parse(n.readFileSync(f,"utf8")).capabilities||[]).length}catch{}console.log(),A(k>0?`infernoflow ready \u2014 ${k} capabilities tracked`:"infernoflow ready"),console.log(`
5
+ ${S("What was set up:")}`),console.log(` ${h("\u2714")} MCP server \u2192 .cursor/inferno-mcp-server.mjs`),console.log(` ${h("\u2714")} ~/.claude.json \u2192 MCP registered + ${C.length} tools pre-approved (no prompts)`),console.log(` ${h("\u2714")} .claude/settings.json \u2192 CLI fallback approvals`),console.log(` ${h("\u2714")} CLAUDE.md \u2192 Claude auto-calls infernoflow silently every session`),console.log(` ${h("\u2714")} Git hooks \u2192 post-commit (changelog) + pre-push (drift check)`),console.log(),console.log(` ${S("You're done.")} Just write code \u2014 infernoflow handles itself.`),console.log(" Claude automatically tracks capabilities, updates the contract,"),console.log(" and synthesizes agents from your workflow patterns."),console.log(),console.log(` ${S("Restart VS Code")} to activate the MCP server.`),console.log()}export{q as setupCommand};