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,9 +1,320 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow adopt
|
|
3
|
+
*
|
|
4
|
+
* Interactive adoption wizard for existing projects.
|
|
5
|
+
* Takes a project from zero to fully wired in one guided flow.
|
|
6
|
+
*
|
|
7
|
+
* Steps:
|
|
8
|
+
* 1. Detect project type, framework, language
|
|
9
|
+
* 2. Scout capabilities in source files
|
|
10
|
+
* 3. Interactive approve / rename / skip each candidate
|
|
11
|
+
* 4. Write capabilities.json + contract.json
|
|
12
|
+
* 5. Generate starter scenarios
|
|
13
|
+
* 6. Install git hooks
|
|
14
|
+
* 7. Regenerate CONTEXT.md
|
|
15
|
+
* 8. Show health score and next steps
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* infernoflow adopt Full interactive wizard
|
|
19
|
+
* infernoflow adopt --yes Auto-approve all (non-interactive)
|
|
20
|
+
* infernoflow adopt --dir src Custom source directory
|
|
21
|
+
* infernoflow adopt --json Machine-readable, implies --yes
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as fs from "node:fs";
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
import * as readline from "node:readline";
|
|
27
|
+
import { spawnSync } from "node:child_process";
|
|
28
|
+
import { done, warn, info, bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
29
|
+
|
|
30
|
+
// ── Detection ─────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function detectProject(cwd) {
|
|
33
|
+
const has = (...files) => files.some(f => fs.existsSync(path.join(cwd, f)));
|
|
34
|
+
const read = (f) => { try { return JSON.parse(fs.readFileSync(path.join(cwd, f), "utf8")); } catch { return {}; } };
|
|
35
|
+
|
|
36
|
+
const pkg = has("package.json") ? read("package.json") : {};
|
|
37
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
38
|
+
|
|
39
|
+
let framework = "unknown", lang = "javascript", type = "backend";
|
|
40
|
+
|
|
41
|
+
if (has("tsconfig.json")) lang = "typescript";
|
|
42
|
+
if (has("Pipfile","pyproject.toml","requirements.txt")) lang = "python";
|
|
43
|
+
if (has("Gemfile")) lang = "ruby";
|
|
44
|
+
if (has("go.mod")) lang = "go";
|
|
45
|
+
if (has("Cargo.toml")) lang = "rust";
|
|
46
|
+
|
|
47
|
+
if (deps["next"]) { framework = "nextjs"; type = "fullstack"; }
|
|
48
|
+
else if (deps["nuxt"]) { framework = "nuxt"; type = "fullstack"; }
|
|
49
|
+
else if (deps["react"]) { framework = "react"; type = "frontend"; }
|
|
50
|
+
else if (deps["vue"]) { framework = "vue"; type = "frontend"; }
|
|
51
|
+
else if (deps["@angular/core"]) { framework = "angular"; type = "frontend"; }
|
|
52
|
+
else if (deps["express"]) { framework = "express"; type = "backend"; }
|
|
53
|
+
else if (deps["fastify"]) { framework = "fastify"; type = "backend"; }
|
|
54
|
+
else if (deps["hono"]) { framework = "hono"; type = "backend"; }
|
|
55
|
+
else if (deps["@nestjs/core"]) { framework = "nestjs"; type = "backend"; }
|
|
56
|
+
else if (deps["graphql"]) { framework = "graphql"; type = "backend"; }
|
|
57
|
+
else if (has("config/routes.rb")) { framework = "rails"; type = "fullstack"; }
|
|
58
|
+
else if (has("manage.py")) { framework = "django"; type = "backend"; }
|
|
59
|
+
|
|
60
|
+
if (pkg.bin || has("bin/")) type = "cli";
|
|
61
|
+
if (has("nx.json","turbo.json","pnpm-workspace.yaml")) type = "monorepo";
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
name: pkg.name || path.basename(cwd),
|
|
65
|
+
version: pkg.version || "0.1.0",
|
|
66
|
+
description: pkg.description || "",
|
|
67
|
+
lang, framework, type,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Scout ─────────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
const SRC_EXTS = new Set([".js",".mjs",".cjs",".ts",".tsx",".jsx",".py",".rb"]);
|
|
74
|
+
const SKIP_DIRS = new Set(["node_modules",".git","dist","build",".next","__pycache__","vendor","coverage","inferno"]);
|
|
75
|
+
|
|
76
|
+
const PATTERNS = [
|
|
77
|
+
{
|
|
78
|
+
regex: /(?:app|router|server)\s*\.\s*(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
79
|
+
extract: m => ({ id: routeId(m[2], m[1]), hint: `${m[1].toUpperCase()} ${m[2]}`, confidence: 0.85 }),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
filePattern: /[/\\](pages[/\\]api|app[/\\]api)[/\\](.+)\.(js|ts|mjs)$/,
|
|
83
|
+
extract: fp => {
|
|
84
|
+
const m = fp.match(/[/\\](pages[/\\]api|app[/\\]api)[/\\](.+)\.(js|ts|mjs)$/);
|
|
85
|
+
if (!m) return null;
|
|
86
|
+
const r = m[2].replace(/[/\\]index$/, "").replace(/\[([^\]]+)\]/g, ":$1");
|
|
87
|
+
return { id: routeId("/" + r, "api"), hint: `Next.js API: /${r}`, confidence: 0.9 };
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
regex: /export\s+(?:async\s+)?(?:function|class|const)\s+([A-Z][A-Za-z0-9]+)/g,
|
|
92
|
+
extract: m => ({ id: camelId(m[1]), hint: `export ${m[1]}`, confidence: 0.65 }),
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
function routeId(r, method) {
|
|
97
|
+
const id = r.replace(/^\//, "").replace(/\/:?[^/]+/g, "").replace(/\//g, "-")
|
|
98
|
+
.replace(/[^a-zA-Z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
99
|
+
return id || method.toLowerCase() + "-root";
|
|
100
|
+
}
|
|
101
|
+
function camelId(name) {
|
|
102
|
+
return name.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "").replace(/-+/g, "-");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function* walk(dir) {
|
|
106
|
+
if (!fs.existsSync(dir)) return;
|
|
107
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
108
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
109
|
+
const full = path.join(dir, e.name);
|
|
110
|
+
if (e.isDirectory()) yield* walk(full);
|
|
111
|
+
else if (e.isFile() && SRC_EXTS.has(path.extname(e.name))) yield full;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function scout(dirs, cwd) {
|
|
116
|
+
const seen = new Map();
|
|
117
|
+
const add = (id, r, file) => {
|
|
118
|
+
if (!seen.has(id) || r.confidence > seen.get(id).confidence)
|
|
119
|
+
seen.set(id, { ...r, source: path.relative(cwd, file) });
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
for (const dir of dirs) {
|
|
123
|
+
for (const file of walk(dir)) {
|
|
124
|
+
for (const pat of PATTERNS) {
|
|
125
|
+
if (pat.filePattern && pat.filePattern.test(file)) {
|
|
126
|
+
const r = pat.extract(file);
|
|
127
|
+
if (r && r.id.length >= 3) add(r.id, r, file);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
let content;
|
|
131
|
+
try { content = fs.readFileSync(file, "utf8"); } catch { continue; }
|
|
132
|
+
for (const pat of PATTERNS) {
|
|
133
|
+
if (!pat.regex) continue;
|
|
134
|
+
pat.regex.lastIndex = 0;
|
|
135
|
+
let m;
|
|
136
|
+
while ((m = pat.regex.exec(content)) !== null) {
|
|
137
|
+
const r = pat.extract(m);
|
|
138
|
+
if (r && r.id.length >= 3 && !["index","app","main","root","default","handler"].includes(r.id))
|
|
139
|
+
add(r.id, r, file);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return [...seen.values()].filter(c => c.confidence >= 0.5).sort((a, b) => b.confidence - a.confidence);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Interactive review ────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
function ask(rl, q) { return new Promise(r => rl.question(q, r)); }
|
|
150
|
+
|
|
151
|
+
async function review(candidates, autoYes) {
|
|
152
|
+
if (autoYes) return candidates.filter(c => c.confidence >= 0.6);
|
|
153
|
+
|
|
154
|
+
const approved = [];
|
|
155
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
156
|
+
|
|
157
|
+
console.log();
|
|
158
|
+
console.log(` ${bold("Review candidates")} ${cyan("y")} approve · ${yellow("e")} edit · ${red("n")} skip · ${gray("a")} approve all · ${gray("q")} stop`);
|
|
159
|
+
console.log();
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
162
|
+
const c = candidates[i];
|
|
163
|
+
const conf = `${Math.round(c.confidence * 100)}%`;
|
|
164
|
+
const col = c.confidence >= 0.8 ? green : c.confidence >= 0.65 ? yellow : gray;
|
|
165
|
+
process.stdout.write(
|
|
166
|
+
` [${i + 1}/${candidates.length}] ${bold(c.id.padEnd(30))} ${col(conf)} ${gray(c.hint)}\n` +
|
|
167
|
+
` ${gray(c.source)}\n > `
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const ans = (await ask(rl, "")).trim().toLowerCase();
|
|
171
|
+
if (ans === "q") break;
|
|
172
|
+
if (ans === "a") {
|
|
173
|
+
for (let j = i; j < candidates.length; j++) approved.push(candidates[j]);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
if (ans === "n") { console.log(` ${gray("skipped")}\n`); continue; }
|
|
177
|
+
|
|
178
|
+
let id = c.id;
|
|
179
|
+
if (ans === "e") {
|
|
180
|
+
const newId = (await ask(rl, ` New id (${c.id}): `)).trim();
|
|
181
|
+
if (newId && /^[a-z0-9-_]+$/.test(newId)) id = newId;
|
|
182
|
+
}
|
|
183
|
+
approved.push({ ...c, id });
|
|
184
|
+
console.log(` ${green("✔")} ${bold(id)}\n`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
rl.close();
|
|
188
|
+
return approved;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Writers ───────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
function writeFiles(infernoDir, approved, project) {
|
|
194
|
+
// contract.json
|
|
195
|
+
const cp = path.join(infernoDir, "contract.json");
|
|
196
|
+
const existing = fs.existsSync(cp) ? JSON.parse(fs.readFileSync(cp, "utf8")) : {};
|
|
197
|
+
fs.writeFileSync(cp, JSON.stringify({
|
|
198
|
+
...existing,
|
|
199
|
+
policyId: existing.policyId || project.name.replace(/[^a-zA-Z0-9-]/g, "-"),
|
|
200
|
+
policyVersion: existing.policyVersion || 1,
|
|
201
|
+
capabilities: approved.map(c => c.id),
|
|
202
|
+
}, null, 2) + "\n");
|
|
203
|
+
|
|
204
|
+
// capabilities.json
|
|
205
|
+
fs.writeFileSync(path.join(infernoDir, "capabilities.json"), JSON.stringify({
|
|
206
|
+
capabilities: approved.map(c => ({
|
|
207
|
+
id: c.id, description: c.hint,
|
|
208
|
+
since: new Date().toISOString().slice(0, 10), source: "adopt",
|
|
209
|
+
})),
|
|
210
|
+
}, null, 2) + "\n");
|
|
211
|
+
|
|
212
|
+
// Starter scenarios
|
|
213
|
+
const scenDir = path.join(infernoDir, "scenarios");
|
|
214
|
+
if (!fs.existsSync(scenDir)) fs.mkdirSync(scenDir, { recursive: true });
|
|
215
|
+
for (const c of approved) {
|
|
216
|
+
const sp = path.join(scenDir, `${c.id}.json`);
|
|
217
|
+
if (fs.existsSync(sp)) continue;
|
|
218
|
+
fs.writeFileSync(sp, JSON.stringify({
|
|
219
|
+
id: `${c.id}-happy-path`, capability: c.id,
|
|
220
|
+
description: `Happy path for ${c.description || c.id}`,
|
|
221
|
+
steps: [{ action: "invoke", target: c.id, input: {} }, { action: "assert", field: "status", value: "success" }],
|
|
222
|
+
capabilitiesCovered: [c.id],
|
|
223
|
+
}, null, 2) + "\n");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function runCli(args, cwd) {
|
|
228
|
+
return spawnSync("infernoflow", args, {
|
|
229
|
+
cwd, encoding: "utf8", timeout: 30_000,
|
|
230
|
+
stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, NO_COLOR: "1" },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Entry ─────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
export async function adoptWizardCommand(rawArgs) {
|
|
237
|
+
const args = rawArgs.slice(1);
|
|
238
|
+
const autoYes = args.includes("--yes") || args.includes("-y") || args.includes("--json");
|
|
239
|
+
const jsonMode = args.includes("--json");
|
|
240
|
+
const cwd = process.cwd();
|
|
241
|
+
|
|
242
|
+
const dirIdx = args.indexOf("--dir");
|
|
243
|
+
const dirs = dirIdx !== -1
|
|
244
|
+
? args[dirIdx + 1].split(",").map(d => path.resolve(cwd, d.trim()))
|
|
245
|
+
: ["src","lib","app","api","pages","routes","controllers","services","handlers"]
|
|
246
|
+
.map(d => path.join(cwd, d)).filter(d => fs.existsSync(d));
|
|
247
|
+
if (!dirs.length) dirs.push(cwd);
|
|
248
|
+
|
|
249
|
+
const project = detectProject(cwd);
|
|
250
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
251
|
+
if (!fs.existsSync(infernoDir)) {
|
|
252
|
+
fs.mkdirSync(infernoDir, { recursive: true });
|
|
253
|
+
fs.mkdirSync(path.join(infernoDir, "scenarios"), { recursive: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!jsonMode) {
|
|
257
|
+
console.log();
|
|
258
|
+
console.log(` ${bold("🔥 infernoflow adopt")} ${gray("— existing project wizard")}`);
|
|
259
|
+
console.log();
|
|
260
|
+
console.log(` ${bold("Project:")} ${cyan(project.name)} v${project.version}`);
|
|
261
|
+
console.log(` ${bold("Language:")} ${project.lang} ${bold("Framework:")} ${project.framework} ${bold("Type:")} ${project.type}`);
|
|
262
|
+
console.log();
|
|
263
|
+
info(`Scanning ${dirs.map(d => path.relative(cwd, d) || ".").join(", ")} …`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const candidates = scout(dirs, cwd);
|
|
267
|
+
|
|
268
|
+
if (!candidates.length) {
|
|
269
|
+
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: "No capabilities detected." })); return; }
|
|
270
|
+
warn("No capabilities detected. Use --dir src,lib,api");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!jsonMode) {
|
|
275
|
+
console.log();
|
|
276
|
+
console.log(` ${bold(`${candidates.length} capability candidates detected`)}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const approved = await review(candidates, autoYes);
|
|
280
|
+
|
|
281
|
+
if (!approved.length) {
|
|
282
|
+
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: "No capabilities approved." })); return; }
|
|
283
|
+
warn("Nothing approved — exiting.");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
writeFiles(infernoDir, approved, project);
|
|
288
|
+
runCli(["context"], cwd);
|
|
289
|
+
|
|
290
|
+
// Offer hooks in interactive mode
|
|
291
|
+
let hooksInstalled = false;
|
|
292
|
+
if (!autoYes) {
|
|
293
|
+
const rl3 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
294
|
+
const ans = (await ask(rl3, `\n Install git hooks for auto-sync? ${gray("[Y/n]")} `)).trim().toLowerCase();
|
|
295
|
+
rl3.close();
|
|
296
|
+
if (ans !== "n") { runCli(["setup", "--yes"], cwd); hooksInstalled = true; }
|
|
297
|
+
} else {
|
|
298
|
+
runCli(["setup", "--yes"], cwd); hooksInstalled = true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (jsonMode) {
|
|
302
|
+
console.log(JSON.stringify({ ok: true, project, detected: candidates.length, approved: approved.length, capabilities: approved.map(c => c.id), hooksInstalled }));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
console.log();
|
|
307
|
+
done(`${bold(String(approved.length))} capabilities adopted`);
|
|
308
|
+
console.log();
|
|
309
|
+
console.log(` ${green("✔")} ${bold("inferno/contract.json")} written`);
|
|
310
|
+
console.log(` ${green("✔")} ${bold("inferno/capabilities.json")} written`);
|
|
311
|
+
console.log(` ${green("✔")} ${bold("inferno/scenarios/")} scaffolded`);
|
|
312
|
+
console.log(` ${green("✔")} ${bold("inferno/CONTEXT.md")} generated`);
|
|
313
|
+
if (hooksInstalled) console.log(` ${green("✔")} Git hooks installed`);
|
|
314
|
+
console.log();
|
|
315
|
+
console.log(` ${bold("Next steps:")}`);
|
|
316
|
+
console.log(` ${cyan("1.")} Paste ${bold("inferno/CONTEXT.md")} into Claude or Cursor`);
|
|
317
|
+
console.log(` ${cyan("2.")} Run ${bold("infernoflow vibe")} to keep everything in sync automatically`);
|
|
318
|
+
console.log(` ${cyan("3.")} Run ${bold("infernoflow health")} to see your starting score`);
|
|
319
|
+
console.log();
|
|
320
|
+
}
|
|
@@ -1,5 +1,191 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow agent
|
|
3
|
+
*
|
|
4
|
+
* Manage and run auto-synthesized agents.
|
|
5
|
+
*
|
|
6
|
+
* Sub-commands:
|
|
7
|
+
* infernoflow agent list — list all saved agents
|
|
8
|
+
* infernoflow agent run <name> — execute an agent's command sequence
|
|
9
|
+
* infernoflow agent show <name> — print agent definition
|
|
10
|
+
* infernoflow agent delete <name> — remove an agent
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { execSync } from "node:child_process";
|
|
16
|
+
import { header, ok, warn, info, done, bold, cyan, green, gray, red } from "../ui/output.mjs";
|
|
17
|
+
import { observeCommandStart } from "../learning/observe.mjs";
|
|
18
|
+
|
|
19
|
+
function findInfernoDir(cwd) {
|
|
20
|
+
const d = path.join(cwd, "inferno");
|
|
21
|
+
return fs.existsSync(d) ? d : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function agentsDir(infernoDir) {
|
|
25
|
+
return path.join(infernoDir, "agents");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadAgents(infernoDir) {
|
|
29
|
+
const dir = agentsDir(infernoDir);
|
|
30
|
+
if (!fs.existsSync(dir)) return [];
|
|
31
|
+
return fs.readdirSync(dir)
|
|
32
|
+
.filter(f => f.endsWith(".json"))
|
|
33
|
+
.map(f => {
|
|
34
|
+
try { return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")); }
|
|
35
|
+
catch { return null; }
|
|
36
|
+
})
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findAgent(infernoDir, name) {
|
|
41
|
+
const agents = loadAgents(infernoDir);
|
|
42
|
+
return agents.find(a => a.name === name || a.id === name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runStep(cmd, args, cwd) {
|
|
46
|
+
const fullCmd = `npx infernoflow ${cmd} ${args.join(" ")}`.trim();
|
|
47
|
+
try {
|
|
48
|
+
const out = execSync(fullCmd, {
|
|
49
|
+
cwd,
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
timeout: 60_000,
|
|
52
|
+
stdio: ["inherit", "pipe", "inherit"],
|
|
53
|
+
});
|
|
54
|
+
return { ok: true, output: out };
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return { ok: false, output: err.stdout || err.message };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Sub-commands ──────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function listAgents(infernoDir) {
|
|
63
|
+
const agents = loadAgents(infernoDir);
|
|
64
|
+
if (agents.length === 0) {
|
|
65
|
+
info("No agents saved yet.");
|
|
66
|
+
info(`Run ${cyan("infernoflow synthesize")} to auto-generate agents from your workflow patterns.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
header("infernoflow agents");
|
|
71
|
+
console.log(`\n ${bold(String(agents.length))} saved agent${agents.length !== 1 ? "s" : ""}:\n`);
|
|
72
|
+
for (const agent of agents) {
|
|
73
|
+
const steps = (agent.steps || []).map(s => s.command).join(" → ");
|
|
74
|
+
const conf = agent.confidence ? gray(`${Math.round(agent.confidence * 100)}% confidence`) : "";
|
|
75
|
+
console.log(` ${green("▶")} ${bold(agent.name)} ${conf}`);
|
|
76
|
+
console.log(` ${gray(agent.description || steps)}`);
|
|
77
|
+
console.log(` Steps: ${cyan(steps)}`);
|
|
78
|
+
console.log();
|
|
79
|
+
}
|
|
80
|
+
console.log(` Run: ${cyan("infernoflow agent run <name>")}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function runAgent(infernoDir, cwd, name) {
|
|
84
|
+
const agent = findAgent(infernoDir, name);
|
|
85
|
+
if (!agent) {
|
|
86
|
+
warn(`Agent "${name}" not found.`);
|
|
87
|
+
warn(`Run ${cyan("infernoflow agent list")} to see available agents.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
header(`infernoflow agent run: ${agent.name}`);
|
|
92
|
+
info(agent.description || agent.name);
|
|
93
|
+
console.log();
|
|
94
|
+
|
|
95
|
+
const steps = agent.steps || [];
|
|
96
|
+
let allOk = true;
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < steps.length; i++) {
|
|
99
|
+
const step = steps[i];
|
|
100
|
+
const cmd = typeof step === "string" ? step : step.command;
|
|
101
|
+
const args = typeof step === "string" ? [] : (step.args || []);
|
|
102
|
+
|
|
103
|
+
console.log(` ${bold(`[${i + 1}/${steps.length}]`)} ${cyan(`infernoflow ${cmd} ${args.join(" ")}`.trim())}`);
|
|
104
|
+
|
|
105
|
+
const result = runStep(cmd, args, cwd);
|
|
106
|
+
if (result.output) {
|
|
107
|
+
const lines = result.output.trim().split("\n");
|
|
108
|
+
for (const line of lines) console.log(` ${gray(line)}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
warn(`Step "${cmd}" exited with an error — stopping agent.`);
|
|
113
|
+
allOk = false;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
ok(`Step ${i + 1} complete`);
|
|
118
|
+
console.log();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (allOk) {
|
|
122
|
+
done(`Agent "${agent.name}" completed all ${steps.length} steps`);
|
|
123
|
+
} else {
|
|
124
|
+
console.log(`\n ${red("✘")} Agent stopped early. Fix the error above and re-run.`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function showAgent(infernoDir, name) {
|
|
130
|
+
const agent = findAgent(infernoDir, name);
|
|
131
|
+
if (!agent) {
|
|
132
|
+
warn(`Agent "${name}" not found.`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
console.log(JSON.stringify(agent, null, 2));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function deleteAgent(infernoDir, name) {
|
|
139
|
+
const agent = findAgent(infernoDir, name);
|
|
140
|
+
if (!agent) {
|
|
141
|
+
warn(`Agent "${name}" not found.`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
const filePath = path.join(agentsDir(infernoDir), `${agent.name}.json`);
|
|
145
|
+
fs.unlinkSync(filePath);
|
|
146
|
+
ok(`Deleted agent: ${agent.name}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
export async function agentCommand(args) {
|
|
152
|
+
const cwd = process.cwd();
|
|
153
|
+
const infernoDir = findInfernoDir(cwd);
|
|
154
|
+
|
|
155
|
+
if (!infernoDir) {
|
|
156
|
+
warn("inferno/ not found — run infernoflow init first");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
observeCommandStart(infernoDir, "agent");
|
|
161
|
+
|
|
162
|
+
const sub = args[0];
|
|
163
|
+
const name = args[1];
|
|
164
|
+
|
|
165
|
+
switch (sub) {
|
|
166
|
+
case "list":
|
|
167
|
+
case undefined:
|
|
168
|
+
listAgents(infernoDir);
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case "run":
|
|
172
|
+
if (!name) { warn("Usage: infernoflow agent run <name>"); process.exit(1); }
|
|
173
|
+
await runAgent(infernoDir, cwd, name);
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case "show":
|
|
177
|
+
if (!name) { warn("Usage: infernoflow agent show <name>"); process.exit(1); }
|
|
178
|
+
showAgent(infernoDir, name);
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case "delete":
|
|
182
|
+
case "remove":
|
|
183
|
+
if (!name) { warn("Usage: infernoflow agent delete <name>"); process.exit(1); }
|
|
184
|
+
deleteAgent(infernoDir, name);
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
default:
|
|
188
|
+
// Treat unknown arg as agent name to run (convenience shortcut)
|
|
189
|
+
await runAgent(infernoDir, cwd, sub);
|
|
190
|
+
}
|
|
191
|
+
}
|