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.
- package/dist/bin/infernoflow.mjs +84 -255
- package/dist/lib/adopters/angular.mjs +1 -128
- package/dist/lib/adopters/css.mjs +1 -111
- package/dist/lib/adopters/react.mjs +1 -104
- package/dist/lib/ai/ideDetection.mjs +1 -31
- package/dist/lib/ai/localProvider.mjs +1 -88
- package/dist/lib/ai/providerRouter.mjs +2 -295
- package/dist/lib/commands/adopt.mjs +20 -869
- package/dist/lib/commands/adoptWizard.mjs +9 -320
- package/dist/lib/commands/agent.mjs +5 -191
- package/dist/lib/commands/ai.mjs +2 -407
- package/dist/lib/commands/audit.mjs +13 -300
- package/dist/lib/commands/changelog.mjs +26 -594
- package/dist/lib/commands/check.mjs +3 -184
- package/dist/lib/commands/ci.mjs +3 -208
- package/dist/lib/commands/claudeMd.mjs +25 -130
- package/dist/lib/commands/cloud.mjs +5 -521
- package/dist/lib/commands/context.mjs +31 -287
- package/dist/lib/commands/coverage.mjs +2 -282
- package/dist/lib/commands/dashboard.mjs +123 -635
- package/dist/lib/commands/demo.mjs +8 -465
- package/dist/lib/commands/diff.mjs +5 -274
- package/dist/lib/commands/docGate.mjs +2 -81
- package/dist/lib/commands/doctor.mjs +3 -321
- package/dist/lib/commands/explain.mjs +8 -438
- package/dist/lib/commands/export.mjs +10 -239
- package/dist/lib/commands/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +203 -321
- package/dist/lib/commands/health.mjs +2 -309
- package/dist/lib/commands/impact.mjs +2 -325
- package/dist/lib/commands/implement.mjs +7 -103
- package/dist/lib/commands/init.mjs +23 -475
- package/dist/lib/commands/installCursorHooks.mjs +1 -36
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
- package/dist/lib/commands/link.mjs +2 -342
- package/dist/lib/commands/monorepo.mjs +4 -428
- package/dist/lib/commands/notify.mjs +4 -258
- package/dist/lib/commands/onboard.mjs +4 -296
- package/dist/lib/commands/prComment.mjs +2 -361
- package/dist/lib/commands/prImpact.mjs +2 -157
- package/dist/lib/commands/publish.mjs +15 -316
- package/dist/lib/commands/report.mjs +28 -272
- package/dist/lib/commands/review.mjs +9 -223
- package/dist/lib/commands/run.mjs +8 -336
- package/dist/lib/commands/scaffold.mjs +54 -419
- package/dist/lib/commands/scan.mjs +5 -558
- package/dist/lib/commands/scout.mjs +2 -291
- package/dist/lib/commands/setup.mjs +5 -310
- package/dist/lib/commands/share.mjs +13 -196
- package/dist/lib/commands/snapshot.mjs +3 -383
- package/dist/lib/commands/stability.mjs +2 -293
- package/dist/lib/commands/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- package/dist/lib/commands/syncAuto.mjs +1 -96
- package/dist/lib/commands/synthesize.mjs +10 -228
- package/dist/lib/commands/teamSync.mjs +2 -388
- package/dist/lib/commands/test.mjs +6 -363
- package/dist/lib/commands/version.mjs +2 -282
- package/dist/lib/commands/vibe.mjs +7 -357
- package/dist/lib/commands/watch.mjs +4 -203
- package/dist/lib/commands/why.mjs +4 -358
- package/dist/lib/cursorHooksInstall.mjs +1 -60
- package/dist/lib/draftToolingInstall.mjs +7 -68
- package/dist/lib/git/detect-drift.mjs +4 -208
- package/dist/lib/learning/adapt.mjs +6 -101
- package/dist/lib/learning/observe.mjs +1 -119
- package/dist/lib/learning/patternDetector.mjs +1 -298
- package/dist/lib/learning/profile.mjs +2 -279
- package/dist/lib/learning/skillSynthesizer.mjs +24 -145
- package/dist/lib/templates/index.mjs +1 -131
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -72
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
- package/dist/templates/github-app/GITHUB_APP.md +67 -0
- package/dist/templates/github-app/app-manifest.json +20 -0
- package/package.json +1 -1
|
@@ -1,291 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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};
|