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