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,3 +1,321 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow doctor
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive setup diagnostic — like `brew doctor`.
|
|
5
|
+
* Checks every component of the infernoflow setup and tells you
|
|
6
|
+
* exactly what's wrong and how to fix it.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* infernoflow doctor Print full diagnostic report
|
|
10
|
+
* infernoflow doctor --fix Auto-fix common issues
|
|
11
|
+
* infernoflow doctor --json Machine-readable output
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import * as os from "node:os";
|
|
17
|
+
import * as http from "node:http";
|
|
18
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
19
|
+
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
20
|
+
import { detectAvailableProviders } from "../ai/providerRouter.mjs";
|
|
21
|
+
|
|
22
|
+
// ── Check runners ─────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function check(label, fn) {
|
|
25
|
+
try {
|
|
26
|
+
const result = fn();
|
|
27
|
+
return { label, ...result };
|
|
28
|
+
} catch (err) {
|
|
29
|
+
return { label, status: "error", message: err.message, fix: null };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pass(message, detail) { return { status: "pass", message, detail: detail || null, fix: null }; }
|
|
34
|
+
function warn(message, fix) { return { status: "warn", message, detail: null, fix: fix || null }; }
|
|
35
|
+
function fail(message, fix) { return { status: "fail", message, detail: null, fix: fix || null }; }
|
|
36
|
+
|
|
37
|
+
// ── Individual checks ─────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function checkNodeVersion() {
|
|
40
|
+
const v = process.version;
|
|
41
|
+
const major = parseInt(v.slice(1).split(".")[0], 10);
|
|
42
|
+
if (major >= 20) return pass(`Node.js ${v}`, "Node 20+ recommended");
|
|
43
|
+
if (major >= 18) return pass(`Node.js ${v}`);
|
|
44
|
+
return fail(`Node.js ${v} — infernoflow requires Node 18+`, "Install Node 20 from nodejs.org");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkCli() {
|
|
48
|
+
// Use shell: true so Windows .cmd wrappers resolve correctly.
|
|
49
|
+
// If the spawn fails entirely, we're still clearly running — downgrade to warn.
|
|
50
|
+
try {
|
|
51
|
+
const r = spawnSync("infernoflow", ["--version"], { encoding: "utf8", timeout: 5000, shell: true });
|
|
52
|
+
if (r.status === 0 && r.stdout.trim()) {
|
|
53
|
+
return pass(`infernoflow v${r.stdout.trim()} on PATH`);
|
|
54
|
+
}
|
|
55
|
+
// Running but PATH lookup failed for subprocesses (common on Windows)
|
|
56
|
+
return warn("infernoflow not resolvable in subprocesses", "Ensure npm global bin folder is in your PATH");
|
|
57
|
+
} catch {
|
|
58
|
+
return warn("infernoflow not resolvable in subprocesses", "Ensure npm global bin folder is in your PATH");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function checkGitRepo(cwd) {
|
|
63
|
+
try {
|
|
64
|
+
execSync("git rev-parse --git-dir", { cwd, stdio: "ignore" });
|
|
65
|
+
return pass("Git repository detected");
|
|
66
|
+
} catch {
|
|
67
|
+
return fail("Not a git repository", "git init && git add . && git commit -m 'init'");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function checkInfernoDir(cwd) {
|
|
72
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
73
|
+
if (!fs.existsSync(infernoDir)) return fail("inferno/ not found", "infernoflow init");
|
|
74
|
+
return pass("inferno/ directory exists");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function checkContract(cwd) {
|
|
78
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
79
|
+
for (const f of ["contract.json", "capabilities.json"]) {
|
|
80
|
+
const p = path.join(infernoDir, f);
|
|
81
|
+
if (!fs.existsSync(p)) continue;
|
|
82
|
+
try {
|
|
83
|
+
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
84
|
+
const caps = (data.capabilities || []).length;
|
|
85
|
+
return pass(`${f} valid — ${caps} capabilities`);
|
|
86
|
+
} catch {
|
|
87
|
+
return fail(`${f} contains invalid JSON`, `Fix the JSON syntax in inferno/${f}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return fail("No contract.json or capabilities.json", "infernoflow init");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function checkScenarios(cwd) {
|
|
94
|
+
const scenDir = path.join(cwd, "inferno", "scenarios");
|
|
95
|
+
if (!fs.existsSync(scenDir)) return warn("No scenarios/ directory", "infernoflow init");
|
|
96
|
+
const files = fs.readdirSync(scenDir).filter(f => f.endsWith(".json"));
|
|
97
|
+
if (!files.length) return warn("scenarios/ is empty", "Add scenario files or run infernoflow suggest");
|
|
98
|
+
return pass(`${files.length} scenario file${files.length !== 1 ? "s" : ""} found`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function checkChangelog(cwd) {
|
|
102
|
+
const p = path.join(cwd, "inferno", "CHANGELOG.md");
|
|
103
|
+
if (!fs.existsSync(p)) return warn("No inferno/CHANGELOG.md", "infernoflow init");
|
|
104
|
+
return pass("inferno/CHANGELOG.md exists");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function checkContextMd(cwd) {
|
|
108
|
+
const p = path.join(cwd, "inferno", "CONTEXT.md");
|
|
109
|
+
if (!fs.existsSync(p)) return warn("No CONTEXT.md generated", "infernoflow context");
|
|
110
|
+
const age = (Date.now() - fs.statSync(p).mtimeMs) / (1000 * 60 * 60 * 24);
|
|
111
|
+
if (age > 7) return warn(`CONTEXT.md is ${Math.round(age)} days old — may be stale`, "infernoflow context");
|
|
112
|
+
return pass(`CONTEXT.md present (${Math.round(age)}d old)`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function checkGitHooks(cwd) {
|
|
116
|
+
const hooksDir = path.join(cwd, ".git", "hooks");
|
|
117
|
+
const postCommit = path.join(hooksDir, "post-commit");
|
|
118
|
+
const prePush = path.join(hooksDir, "pre-push");
|
|
119
|
+
const hasPost = fs.existsSync(postCommit) && fs.readFileSync(postCommit, "utf8").includes("infernoflow");
|
|
120
|
+
const hasPre = fs.existsSync(prePush) && fs.readFileSync(prePush, "utf8").includes("infernoflow");
|
|
121
|
+
if (hasPost && hasPre) return pass("Git hooks installed (post-commit + pre-push)");
|
|
122
|
+
if (hasPost || hasPre) return warn("Partial git hooks installed", "infernoflow setup --yes");
|
|
123
|
+
return warn("Git hooks not installed", "infernoflow setup --yes");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function checkMcp(cwd) {
|
|
127
|
+
// Check for MCP server in cursor config or .mcp.json
|
|
128
|
+
const checks = [
|
|
129
|
+
path.join(cwd, ".cursor", "mcp.json"),
|
|
130
|
+
path.join(cwd, ".mcp.json"),
|
|
131
|
+
path.join(os.homedir(), ".cursor", "mcp.json"),
|
|
132
|
+
path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
133
|
+
path.join(os.homedir(), "AppData", "Roaming", "Claude", "claude_desktop_config.json"),
|
|
134
|
+
];
|
|
135
|
+
for (const p of checks) {
|
|
136
|
+
if (!fs.existsSync(p)) continue;
|
|
137
|
+
try {
|
|
138
|
+
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
139
|
+
const servers = data.mcpServers || data.mcp_servers || {};
|
|
140
|
+
if (Object.keys(servers).some(k => k.toLowerCase().includes("inferno"))) {
|
|
141
|
+
return pass(`MCP server configured in ${path.basename(p)}`);
|
|
142
|
+
}
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
return warn("MCP server not configured", "infernoflow setup --yes (adds to Cursor/Claude config)");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function checkAiProviders(cwd) {
|
|
149
|
+
const providers = detectAvailableProviders(cwd);
|
|
150
|
+
const available = Object.entries(providers).filter(([, v]) => v).map(([k]) => k);
|
|
151
|
+
|
|
152
|
+
if (available.length) return pass(`AI provider${available.length !== 1 ? "s" : ""}: ${available.join(", ")}`);
|
|
153
|
+
|
|
154
|
+
return warn(
|
|
155
|
+
"No AI provider configured",
|
|
156
|
+
"Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY, or OPENROUTER_API_KEY\n" +
|
|
157
|
+
" Or install Ollama (ollama.com) for free local AI\n" +
|
|
158
|
+
" Or use VS Code with GitHub Copilot (zero config)"
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function checkOllama() {
|
|
163
|
+
return new Promise(resolve => {
|
|
164
|
+
const req = http.get({ hostname: "localhost", port: 11434, path: "/api/tags", timeout: 1500 }, res => {
|
|
165
|
+
resolve(pass("Ollama running on localhost:11434"));
|
|
166
|
+
});
|
|
167
|
+
req.on("error", () => resolve({ status: "info", message: "Ollama not running (optional)", fix: "ollama serve", detail: null }));
|
|
168
|
+
req.on("timeout", () => { req.destroy(); resolve({ status: "info", message: "Ollama not running (optional)", fix: null, detail: null }); });
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function checkCloudToken(cwd) {
|
|
173
|
+
const p = path.join(cwd, "inferno", "integrations.json");
|
|
174
|
+
if (!fs.existsSync(p)) return { status: "info", message: "Cloud sync not configured (optional)", fix: "infernoflow cloud init", detail: null };
|
|
175
|
+
try {
|
|
176
|
+
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
177
|
+
if (data.cloud?.token) return pass("Cloud sync configured");
|
|
178
|
+
return { status: "info", message: "Cloud sync not configured (optional)", fix: "infernoflow cloud init", detail: null };
|
|
179
|
+
} catch {
|
|
180
|
+
return { status: "info", message: "Cloud sync not configured (optional)", fix: null, detail: null };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Auto-fix ──────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
function autoFix(results, cwd) {
|
|
187
|
+
const fixable = results.filter(r => r.status === "warn" && r.fix);
|
|
188
|
+
const fixed = [];
|
|
189
|
+
|
|
190
|
+
for (const r of fixable) {
|
|
191
|
+
const fix = r.fix;
|
|
192
|
+
if (fix.startsWith("infernoflow ")) {
|
|
193
|
+
const args = fix.slice("infernoflow ".length).split(" ");
|
|
194
|
+
const res = spawnSync("infernoflow", args, { cwd, encoding: "utf8", timeout: 30_000 });
|
|
195
|
+
if (res.status === 0) fixed.push(r.label);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return fixed;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Renderer ──────────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
function icon(status) {
|
|
204
|
+
if (status === "pass") return green("✔");
|
|
205
|
+
if (status === "warn") return yellow("⚠");
|
|
206
|
+
if (status === "fail") return red("✗");
|
|
207
|
+
return gray("·");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function printReport(results, elapsed) {
|
|
211
|
+
const counts = { pass: 0, warn: 0, fail: 0, info: 0, error: 0 };
|
|
212
|
+
for (const r of results) counts[r.status] = (counts[r.status] || 0) + 1;
|
|
213
|
+
|
|
214
|
+
console.log();
|
|
215
|
+
console.log(` ${bold("🔥 infernoflow doctor")}`);
|
|
216
|
+
console.log();
|
|
217
|
+
|
|
218
|
+
const w = Math.max(...results.map(r => r.label.length)) + 2;
|
|
219
|
+
for (const r of results) {
|
|
220
|
+
console.log(` ${icon(r.status)} ${bold(r.label.padEnd(w))} ${r.message}`);
|
|
221
|
+
if (r.detail) console.log(` ${" ".repeat(w)} ${gray(r.detail)}`);
|
|
222
|
+
if (r.fix && (r.status === "warn" || r.status === "fail")) {
|
|
223
|
+
console.log(` ${" ".repeat(w)} ${cyan("fix:")} ${gray(r.fix)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log();
|
|
228
|
+
const overall = counts.fail > 0 ? red("issues found") : counts.warn > 0 ? yellow("warnings") : green("all good");
|
|
229
|
+
console.log(` ${overall} — ${green(String(counts.pass))} pass · ${yellow(String(counts.warn))} warn · ${red(String(counts.fail))} fail (${elapsed}ms)`);
|
|
230
|
+
console.log();
|
|
231
|
+
|
|
232
|
+
// Prioritized action list — show concrete next steps, not just status flags
|
|
233
|
+
const actions = [];
|
|
234
|
+
|
|
235
|
+
// Failures first
|
|
236
|
+
const fails = results.filter(r => r.status === "fail");
|
|
237
|
+
for (const f of fails) {
|
|
238
|
+
if (f.fix) actions.push({ priority: "🔴", text: f.fix });
|
|
239
|
+
else actions.push({ priority: "🔴", text: `Fix: ${f.label} — ${f.message}` });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// AI provider — very common gap, elevate it
|
|
243
|
+
const aiCheck = results.find(r => r.label === "AI providers");
|
|
244
|
+
if (aiCheck && aiCheck.status !== "pass") {
|
|
245
|
+
actions.push({ priority: "💡", text: `Connect an AI provider: ${cyan("infernoflow ai setup")} (unlocks explain, why, review, changelog)` });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Warnings
|
|
249
|
+
const warns = results.filter(r => r.status === "warn" && r.label !== "AI providers");
|
|
250
|
+
for (const w of warns) {
|
|
251
|
+
if (w.fix) actions.push({ priority: "⚠️ ", text: w.fix });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (actions.length > 0) {
|
|
255
|
+
console.log(` ${bold("Next steps:")}`);
|
|
256
|
+
for (const a of actions) {
|
|
257
|
+
console.log(` ${a.priority} ${a.text}`);
|
|
258
|
+
}
|
|
259
|
+
console.log();
|
|
260
|
+
if (warns.length > 0) {
|
|
261
|
+
console.log(` ${gray("Auto-fix warnings:")} ${cyan("infernoflow doctor --fix")}`);
|
|
262
|
+
}
|
|
263
|
+
console.log();
|
|
264
|
+
} else {
|
|
265
|
+
console.log(` ${green("✓")} You're all set. Run ${cyan("infernoflow demo")} to see the full capability chain.`);
|
|
266
|
+
console.log();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Entry ─────────────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
export async function doctorCommand(rawArgs) {
|
|
273
|
+
const args = rawArgs.slice(1);
|
|
274
|
+
const jsonMode = args.includes("--json");
|
|
275
|
+
const fixMode = args.includes("--fix");
|
|
276
|
+
const cwd = process.cwd();
|
|
277
|
+
const start = Date.now();
|
|
278
|
+
|
|
279
|
+
const results = [
|
|
280
|
+
check("Node.js version", () => checkNodeVersion()),
|
|
281
|
+
check("infernoflow CLI", () => checkCli()),
|
|
282
|
+
check("Git repository", () => checkGitRepo(cwd)),
|
|
283
|
+
check("inferno/ directory",() => checkInfernoDir(cwd)),
|
|
284
|
+
check("Contract file", () => checkContract(cwd)),
|
|
285
|
+
check("Scenarios", () => checkScenarios(cwd)),
|
|
286
|
+
check("Changelog", () => checkChangelog(cwd)),
|
|
287
|
+
check("CONTEXT.md", () => checkContextMd(cwd)),
|
|
288
|
+
check("Git hooks", () => checkGitHooks(cwd)),
|
|
289
|
+
check("MCP server", () => checkMcp(cwd)),
|
|
290
|
+
check("AI providers", () => checkAiProviders(cwd)),
|
|
291
|
+
check("Cloud sync", () => checkCloudToken(cwd)),
|
|
292
|
+
await checkOllama().then(r => ({ label: "Ollama (local AI)", ...r })),
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
const elapsed = Date.now() - start;
|
|
296
|
+
|
|
297
|
+
if (fixMode) {
|
|
298
|
+
const fixed = autoFix(results, cwd);
|
|
299
|
+
if (fixed.length) {
|
|
300
|
+
if (!jsonMode) {
|
|
301
|
+
console.log();
|
|
302
|
+
fixed.forEach(f => console.log(` ${green("✔")} Fixed: ${f}`));
|
|
303
|
+
console.log();
|
|
304
|
+
}
|
|
305
|
+
// Re-run checks after fixing
|
|
306
|
+
return doctorCommand(["doctor", "--json"]);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (jsonMode) {
|
|
311
|
+
const counts = { pass: 0, warn: 0, fail: 0, info: 0 };
|
|
312
|
+
results.forEach(r => counts[r.status] = (counts[r.status] || 0) + 1);
|
|
313
|
+
console.log(JSON.stringify({ ok: counts.fail === 0, counts, results, elapsed }));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
printReport(results, elapsed);
|
|
318
|
+
|
|
319
|
+
const hasFail = results.some(r => r.status === "fail");
|
|
320
|
+
if (hasFail) process.exit(1);
|
|
321
|
+
}
|