@docker-doctor/cli 0.1.0 → 0.2.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 +2 -0
- package/dist/cli.cjs +146 -30
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +145 -29
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -1
- package/dist/{src-CjKyZmlM.cjs → src-BUYNqQd8.cjs} +33 -20
- package/dist/src-BUYNqQd8.cjs.map +1 -0
- package/dist/{src-BhdU_CBh.mjs → src-Bt3AB-Xl.mjs} +33 -20
- package/dist/src-Bt3AB-Xl.mjs.map +1 -0
- package/package.json +3 -3
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/src-BhdU_CBh.mjs.map +0 -1
- package/dist/src-CjKyZmlM.cjs.map +0 -1
package/README.md
CHANGED
package/dist/cli.cjs
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_src = require('./src-
|
|
2
|
+
const require_src = require('./src-BUYNqQd8.cjs');
|
|
3
3
|
let node_fs_promises = require("node:fs/promises");
|
|
4
4
|
node_fs_promises = require_src.__toESM(node_fs_promises, 1);
|
|
5
5
|
let node_path = require("node:path");
|
|
6
6
|
node_path = require_src.__toESM(node_path, 1);
|
|
7
7
|
let node_os = require("node:os");
|
|
8
8
|
node_os = require_src.__toESM(node_os, 1);
|
|
9
|
-
let
|
|
10
|
-
|
|
11
|
-
node_readline_promises = require_src.__toESM(node_readline_promises, 1);
|
|
9
|
+
let node_readline = require("node:readline");
|
|
10
|
+
node_readline = require_src.__toESM(node_readline, 1);
|
|
12
11
|
let node_timers_promises = require("node:timers/promises");
|
|
13
12
|
let chalk = require("chalk");
|
|
14
13
|
chalk = require_src.__toESM(chalk, 1);
|
|
@@ -116,15 +115,16 @@ const getWhaleMascot = (score, border) => {
|
|
|
116
115
|
const easeOutCubic = (x) => 1 - (1 - x) ** 3;
|
|
117
116
|
const printScoreBox = async (score, label, categoryIssueCounts) => {
|
|
118
117
|
const { isTTY } = process.stdout;
|
|
119
|
-
const
|
|
120
|
-
const frameDelay = 50;
|
|
118
|
+
const shouldAnimate = isTTY && !process.env.CI && !process.env.NO_ANIMATION && process.env.TERM !== "dumb" && process.env.NODE_ENV !== "test";
|
|
121
119
|
let scoreColor = chalk.default.red.bold;
|
|
122
120
|
if (score >= 90) scoreColor = chalk.default.green.bold;
|
|
123
121
|
else if (score >= 75) scoreColor = chalk.default.yellow.bold;
|
|
124
122
|
else if (score >= 50) scoreColor = chalk.default.magenta.bold;
|
|
125
123
|
const whaleLines = getWhaleMascot(score, scoreColor);
|
|
126
124
|
const shareUrl = `https://github.com/PunGrumpy/docker-doctor/share?s=${score}&w=${Object.values(categoryIssueCounts).reduce((a, b) => a + b, 0)}`;
|
|
127
|
-
if (
|
|
125
|
+
if (shouldAnimate) {
|
|
126
|
+
const frameCount = 20;
|
|
127
|
+
const frameDelay = 15;
|
|
128
128
|
process.stdout.write("\x1B[?25l");
|
|
129
129
|
try {
|
|
130
130
|
for (let frame = 0; frame <= frameCount; frame += 1) {
|
|
@@ -182,13 +182,111 @@ const formatTerminal = async (diagnostics, score, label, project, verbose = fals
|
|
|
182
182
|
|
|
183
183
|
//#endregion
|
|
184
184
|
//#region src/cli.ts
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
const askConfirm = (question, defaultYes = false) => {
|
|
186
|
+
if (!process.stdin.isTTY) return Promise.resolve(defaultYes);
|
|
187
|
+
return new Promise((resolve) => {
|
|
188
|
+
let value = defaultYes;
|
|
189
|
+
node_readline.default.emitKeypressEvents(process.stdin);
|
|
190
|
+
process.stdin.setRawMode(true);
|
|
191
|
+
process.stdin.resume();
|
|
192
|
+
process.stdout.write("\x1B[?25l");
|
|
193
|
+
const render = (firstTime = false) => {
|
|
194
|
+
if (!firstTime) process.stdout.write("\x1B[3A\r");
|
|
195
|
+
process.stdout.write(`\r\u001B[K ${chalk.default.green("✔")} ${chalk.default.bold(question)}\n`);
|
|
196
|
+
const yesPrefix = value ? chalk.default.cyan("❯ ") : " ";
|
|
197
|
+
const yesText = value ? chalk.default.cyan.bold("Yes") : chalk.default.dim("Yes");
|
|
198
|
+
process.stdout.write(`\r\u001B[K${yesPrefix}${yesText}\n`);
|
|
199
|
+
const noPrefix = value ? " " : chalk.default.cyan("❯ ");
|
|
200
|
+
const noText = value ? chalk.default.dim("No") : chalk.default.cyan.bold("No");
|
|
201
|
+
process.stdout.write(`\r\u001B[K${noPrefix}${noText}\n`);
|
|
202
|
+
};
|
|
203
|
+
render(true);
|
|
204
|
+
const handleKeypress = (str, key) => {
|
|
205
|
+
const cleanup = () => {
|
|
206
|
+
process.stdin.removeListener("keypress", handleKeypress);
|
|
207
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
208
|
+
process.stdin.pause();
|
|
209
|
+
process.stdout.write("\x1B[?25h");
|
|
210
|
+
};
|
|
211
|
+
if (key.name === "left" || key.name === "right" || key.name === "up" || key.name === "down" || key.name === "h" || key.name === "l" || key.name === "j" || key.name === "k") {
|
|
212
|
+
value = !value;
|
|
213
|
+
render();
|
|
214
|
+
} else if (str === "y" || str === "Y") {
|
|
215
|
+
value = true;
|
|
216
|
+
render();
|
|
217
|
+
} else if (str === "n" || str === "N") {
|
|
218
|
+
value = false;
|
|
219
|
+
render();
|
|
220
|
+
} else if (key.name === "return" || key.name === "enter" || str === "\r" || str === "\n") {
|
|
221
|
+
cleanup();
|
|
222
|
+
process.stdout.write("\x1B[3A\r\x1B[K");
|
|
223
|
+
process.stdout.write(` ${chalk.default.green("✔")} ${chalk.default.bold(question)} › ${value ? chalk.default.cyan("Yes") : chalk.default.dim("No")}\n`);
|
|
224
|
+
process.stdout.write("\r\x1B[K\n");
|
|
225
|
+
process.stdout.write("\r\x1B[K\n");
|
|
226
|
+
process.stdout.write("\x1B[2A");
|
|
227
|
+
resolve(value);
|
|
228
|
+
} else if (key.ctrl && key.name === "c") {
|
|
229
|
+
cleanup();
|
|
230
|
+
process.stdout.write("\n");
|
|
231
|
+
process.exit(130);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
process.stdin.on("keypress", handleKeypress);
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
const askSelect = (question, options, defaultIndex = 0) => {
|
|
238
|
+
if (!process.stdin.isTTY) return Promise.resolve(defaultIndex);
|
|
239
|
+
return new Promise((resolve) => {
|
|
240
|
+
let index = defaultIndex;
|
|
241
|
+
node_readline.default.emitKeypressEvents(process.stdin);
|
|
242
|
+
process.stdin.setRawMode(true);
|
|
243
|
+
process.stdin.resume();
|
|
244
|
+
process.stdout.write("\x1B[?25l");
|
|
245
|
+
const render = (firstTime = false) => {
|
|
246
|
+
if (!firstTime) process.stdout.write(`\u001B[${options.length + 1}A\r`);
|
|
247
|
+
process.stdout.write(`\r\u001B[K ${chalk.default.green("✔")} ${chalk.default.bold(question)}\n`);
|
|
248
|
+
let i = 0;
|
|
249
|
+
for (const option of options) {
|
|
250
|
+
const isSelected = i === index;
|
|
251
|
+
const prefix = isSelected ? chalk.default.cyan("❯ ") : " ";
|
|
252
|
+
const text = isSelected ? chalk.default.cyan.bold(option) : chalk.default.dim(option);
|
|
253
|
+
process.stdout.write(`\r\u001B[K${prefix}${text}\n`);
|
|
254
|
+
i += 1;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
render(true);
|
|
258
|
+
const handleKeypress = (str, key) => {
|
|
259
|
+
const cleanup = () => {
|
|
260
|
+
process.stdin.removeListener("keypress", handleKeypress);
|
|
261
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
262
|
+
process.stdin.pause();
|
|
263
|
+
process.stdout.write("\x1B[?25h");
|
|
264
|
+
};
|
|
265
|
+
if (key.name === "up" || key.name === "k") {
|
|
266
|
+
index = (index - 1 + options.length) % options.length;
|
|
267
|
+
render();
|
|
268
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
269
|
+
index = (index + 1) % options.length;
|
|
270
|
+
render();
|
|
271
|
+
} else if (key.name === "return" || key.name === "enter" || str === "\r" || str === "\n") {
|
|
272
|
+
cleanup();
|
|
273
|
+
process.stdout.write(`\u001B[${options.length + 1}A\r\u001B[K`);
|
|
274
|
+
process.stdout.write(` ${chalk.default.green("✔")} ${chalk.default.bold(question)} › ${chalk.default.cyan(options[index])}\n`);
|
|
275
|
+
for (const _ of options) process.stdout.write("\r\x1B[K\n");
|
|
276
|
+
process.stdout.write(`\u001B[${options.length}A`);
|
|
277
|
+
resolve(index);
|
|
278
|
+
} else if (key.ctrl && key.name === "c") {
|
|
279
|
+
cleanup();
|
|
280
|
+
process.stdout.write("\n");
|
|
281
|
+
process.exit(130);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
process.stdin.on("keypress", handleKeypress);
|
|
189
285
|
});
|
|
286
|
+
};
|
|
287
|
+
const runInteractiveWizard = async () => {
|
|
190
288
|
try {
|
|
191
|
-
if (
|
|
289
|
+
if (await askConfirm("Add Docker Doctor to GitHub Actions?")) {
|
|
192
290
|
const workflowDir = node_path.default.resolve(".github/workflows");
|
|
193
291
|
await node_fs_promises.default.mkdir(workflowDir, { recursive: true });
|
|
194
292
|
const workflowPath = node_path.default.join(workflowDir, "docker-doctor.yml");
|
|
@@ -213,13 +311,11 @@ jobs:
|
|
|
213
311
|
console.log(`\n ${chalk.default.green("✨")} Created ${chalk.default.cyan(".github/workflows/docker-doctor.yml")}!`);
|
|
214
312
|
console.log(` Scan every pull request to prevent new Docker issues while you fix the backlog.`);
|
|
215
313
|
}
|
|
216
|
-
if (
|
|
314
|
+
if (await askSelect("What would you like to do next?", ["View rules list", "Skip"]) === 0) {
|
|
217
315
|
console.log(`\n ${chalk.default.bold("Available Rules:")}`);
|
|
218
316
|
for (const r of require_src.allRules) console.log(` - ${chalk.default.cyan(r.key)}: ${r.message} (${chalk.default.dim(r.category)})`);
|
|
219
317
|
}
|
|
220
|
-
} catch {}
|
|
221
|
-
rl.close();
|
|
222
|
-
}
|
|
318
|
+
} catch {}
|
|
223
319
|
};
|
|
224
320
|
const runRulesEngine = async (rootDir, project, rulesConfig, projectFilesList, fileContents, options, setStatus) => {
|
|
225
321
|
const diagnostics = [];
|
|
@@ -260,13 +356,35 @@ const runRulesEngine = async (rootDir, project, rulesConfig, projectFilesList, f
|
|
|
260
356
|
for (const diags of composeResults) diagnostics.push(...diags);
|
|
261
357
|
return diagnostics;
|
|
262
358
|
};
|
|
359
|
+
const filterDiagnostics = (diagnostics, categories) => {
|
|
360
|
+
if (!categories) return diagnostics;
|
|
361
|
+
return diagnostics.filter((d) => {
|
|
362
|
+
const ruleDef = require_src.findRule(d.rule);
|
|
363
|
+
if (ruleDef) {
|
|
364
|
+
if (categories[ruleDef.category] === "off") return false;
|
|
365
|
+
}
|
|
366
|
+
return true;
|
|
367
|
+
});
|
|
368
|
+
};
|
|
263
369
|
const program = new commander.Command();
|
|
264
370
|
program.name("docker-doctor").description("Static analysis for Dockerfile and Docker Compose files").version(require_src.package_default.version, "-V, --version", "display the version number");
|
|
265
371
|
program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "show verbose diagnostics description", false).option("-s, --score", "only output numeric health score", false).option("-j, --json", "output results as JSON report", false).option("-c, --config <path>", "custom config file path").action(async (dir, options) => {
|
|
372
|
+
const isSilent = options.score || options.json;
|
|
373
|
+
const restoreCursor = () => {
|
|
374
|
+
if (process.stdout.isTTY && !isSilent) process.stdout.write("\x1B[?25h");
|
|
375
|
+
};
|
|
376
|
+
process.on("exit", restoreCursor);
|
|
377
|
+
process.once("SIGINT", () => {
|
|
378
|
+
restoreCursor();
|
|
379
|
+
process.exit(130);
|
|
380
|
+
});
|
|
381
|
+
process.once("SIGTERM", () => {
|
|
382
|
+
restoreCursor();
|
|
383
|
+
process.exit(143);
|
|
384
|
+
});
|
|
266
385
|
try {
|
|
267
386
|
const rootDir = node_path.default.resolve(dir);
|
|
268
387
|
const startTime = Date.now();
|
|
269
|
-
const isSilent = options.score || options.json;
|
|
270
388
|
let statusText = "Discovering workspace...";
|
|
271
389
|
const spinnerFrames = [
|
|
272
390
|
"⠋",
|
|
@@ -286,9 +404,10 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
286
404
|
statusText = text;
|
|
287
405
|
};
|
|
288
406
|
if (process.stdout.isTTY && !isSilent) {
|
|
407
|
+
process.stdout.write("\x1B[?25l");
|
|
289
408
|
process.stdout.write(`${chalk.default.cyan(spinnerFrames[0])} ${statusText}`);
|
|
290
409
|
spinnerInterval = setInterval(() => {
|
|
291
|
-
process.stdout.write(`\r${chalk.default.cyan(spinnerFrames[frameIndex])} ${statusText}`);
|
|
410
|
+
process.stdout.write(`\r\u001B[K${chalk.default.cyan(spinnerFrames[frameIndex])} ${statusText}`);
|
|
292
411
|
frameIndex = (frameIndex + 1) % spinnerFrames.length;
|
|
293
412
|
}, 80);
|
|
294
413
|
}
|
|
@@ -299,23 +418,20 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
299
418
|
setStatus("Scanning workspace files...");
|
|
300
419
|
const project = await require_src.discoverProject(rootDir);
|
|
301
420
|
const fileContents = {};
|
|
302
|
-
const projectFilesList = [
|
|
421
|
+
const projectFilesList = [
|
|
422
|
+
...project.dockerfiles,
|
|
423
|
+
...project.composeFiles,
|
|
424
|
+
...project.dockerignores || []
|
|
425
|
+
];
|
|
303
426
|
const diagnostics = await runRulesEngine(rootDir, project, config.rules, projectFilesList, fileContents, options, setStatus);
|
|
304
|
-
|
|
305
|
-
if (config.categories) filteredDiagnostics = diagnostics.filter((d) => {
|
|
306
|
-
const ruleDef = require_src.findRule(d.rule);
|
|
307
|
-
if (ruleDef) {
|
|
308
|
-
if (config.categories?.[ruleDef.category] === "off") return false;
|
|
309
|
-
}
|
|
310
|
-
return true;
|
|
311
|
-
});
|
|
427
|
+
const filteredDiagnostics = filterDiagnostics(diagnostics, config.categories);
|
|
312
428
|
const { score, label } = require_src.calculateScore(filteredDiagnostics);
|
|
313
429
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
314
430
|
const concurrency = node_os.default.cpus().length;
|
|
315
431
|
if (spinnerInterval !== null) {
|
|
316
432
|
clearInterval(spinnerInterval);
|
|
317
433
|
spinnerInterval = null;
|
|
318
|
-
process.stdout.write("\r\x1B[K");
|
|
434
|
+
process.stdout.write("\r\x1B[K\x1B[?25h");
|
|
319
435
|
}
|
|
320
436
|
if (process.stdout.isTTY && !isSilent) console.log(`${chalk.default.green("✔")} Scanned ${projectFilesList.length} files in ${duration}s [~${concurrency} workers]`);
|
|
321
437
|
if (options.score) {
|
|
@@ -329,13 +445,13 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
329
445
|
} else {
|
|
330
446
|
await formatTerminal(filteredDiagnostics, score, label, project, options.verbose, fileContents);
|
|
331
447
|
const hasErrors = filteredDiagnostics.some((d) => d.severity === "error");
|
|
332
|
-
if (process.stdout.isTTY) await runInteractiveWizard();
|
|
448
|
+
if (process.stdout.isTTY && process.stdin.isTTY) await runInteractiveWizard();
|
|
333
449
|
process.exit(hasErrors ? 1 : 0);
|
|
334
450
|
}
|
|
335
451
|
} finally {
|
|
336
452
|
if (spinnerInterval !== null) {
|
|
337
453
|
clearInterval(spinnerInterval);
|
|
338
|
-
process.stdout.write("\r\x1B[K");
|
|
454
|
+
process.stdout.write("\r\x1B[K\x1B[?25h");
|
|
339
455
|
}
|
|
340
456
|
}
|
|
341
457
|
} catch (error) {
|
package/dist/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.cjs","names":["border","findRule","readline","path","fs","allRules","runDockerfileRules","parseDockerfile","runComposeRules","parseCompose","Command","packageJson","loadConfig","discoverProject","findRule","calculateScore","os","toJsonReport"],"sources":["../src/formatters/terminal.ts","../src/cli.ts"],"sourcesContent":["import { setTimeout } from \"node:timers/promises\";\n\nimport type { Diagnostic, ProjectInfo } from \"@docker-doctor/core\";\nimport { findRule } from \"@docker-doctor/core\";\nimport chalk from \"chalk\";\n\nconst printCodeFrame = (\n content: string | undefined,\n line: number | undefined,\n severityColor: (msg: string) => string\n): void => {\n if (!content || !line) {\n return;\n }\n const lines = content.split(/\\r?\\n/u);\n const start = Math.max(1, line - 1);\n const end = Math.min(lines.length, line + 1);\n\n for (let i = start; i <= end; i += 1) {\n const rawLine = lines[i - 1];\n const isTarget = i === line;\n const lineNumberStr = String(i).padStart(5, \" \");\n if (isTarget) {\n console.log(\n ` ${severityColor(\">\")} ${chalk.bold(lineNumberStr)} │ ${chalk.white(rawLine)}`\n );\n } else {\n console.log(` ${chalk.dim(lineNumberStr)} │ ${chalk.dim(rawLine)}`);\n }\n }\n console.log();\n};\n\nconst printDiscoveredFiles = (project: ProjectInfo): void => {\n console.log(`\\nDiscovered Files:`);\n console.log(\n ` Dockerfile(s): ${project.dockerfiles.length ? project.dockerfiles.map((f) => chalk.cyan(f)).join(\", \") : chalk.dim(\"None\")}`\n );\n console.log(\n ` Compose file(s): ${project.composeFiles.length ? project.composeFiles.map((f) => chalk.cyan(f)).join(\", \") : chalk.dim(\"None\")}`\n );\n};\n\nconst printDiagnostics = (\n diagnostics: Diagnostic[],\n verbose: boolean,\n fileContents: Record<string, string>,\n categoryIssueCounts: Record<string, number>\n): void => {\n if (diagnostics.length === 0) {\n console.log(\n `\\n${chalk.green.bold(\"✔ No issues found! Your Docker setup looks healthy.\")}`\n );\n return;\n }\n\n if (!verbose) {\n console.log(`\\n All ${chalk.bold(diagnostics.length)} issues\\n`);\n\n const categories = [\n \"Security\",\n \"Performance\",\n \"Best Practices\",\n \"Compose\",\n \"Image Size\",\n ];\n\n for (const cat of categories) {\n const count = categoryIssueCounts[cat];\n const issueLabel = count === 1 ? \"1 issue\" : `${count} issues`;\n console.log(` ${cat} › ${chalk.dim(issueLabel)}`);\n }\n\n console.log(\n `\\n Run ${chalk.cyan(\"docker-doctor --verbose\")} to list every error and warning`\n );\n\n // Migration-scale checks\n const ruleCounts: Record<string, number> = {};\n for (const d of diagnostics) {\n ruleCounts[d.rule] = (ruleCounts[d.rule] || 0) + 1;\n }\n const migrationRules = Object.entries(ruleCounts).filter(\n ([_, count]) => count >= 5\n );\n if (migrationRules.length > 0) {\n console.log();\n console.log(\n ` ${chalk.yellow(\"⚠ Migration-scale change: sample before you sweep\")}`\n );\n for (const [rule, count] of migrationRules) {\n console.log(` ${chalk.cyan(rule)} ×${count} across ${count} files`);\n }\n console.log(\n ` Fixing all of them at once is hard to review and prone to`\n );\n console.log(\n ` subtle mistakes across the whole repo. Fix a representative`\n );\n console.log(\n ` few first and confirm the recipe holds. Then get the code`\n );\n console.log(` owner's sign-off before changing the rest.`);\n console.log(` Scope it down one area at a time: docker-doctor <path>`);\n }\n return;\n }\n\n console.log(`\\nFound ${chalk.bold(diagnostics.length)} issue(s):`);\n\n // Group by file\n const filesGrouped: Record<string, Diagnostic[]> = {};\n for (const d of diagnostics) {\n if (!filesGrouped[d.file]) {\n filesGrouped[d.file] = [];\n }\n filesGrouped[d.file].push(d);\n }\n\n for (const [file, fileDiags] of Object.entries(filesGrouped)) {\n console.log(`\\n${chalk.underline.bold(file)}`);\n for (const d of fileDiags) {\n let sevColor = chalk.cyan;\n let prefix = \"ℹ INFO\";\n if (d.severity === \"error\") {\n sevColor = chalk.red.bold;\n prefix = \"✖ ERROR\";\n } else if (d.severity === \"warning\") {\n sevColor = chalk.yellow;\n prefix = \"⚠ WARN\";\n }\n\n const lineInfo = d.line ? `:${d.line}` : \"\";\n console.log(` ${sevColor(prefix)} [${chalk.dim(d.rule)}]${lineInfo}`);\n\n // Print Code Frame\n const content = fileContents[file];\n printCodeFrame(content, d.line, sevColor);\n\n console.log(` ${chalk.white(d.message)}`);\n console.log(` ${chalk.dim(\"Help:\")} ${d.help}`);\n console.log();\n }\n }\n};\n\nconst getWhaleMascot = (\n score: number,\n border: (text: string) => string\n): string[] => {\n let eyes = \"x x\";\n let spout = \" \";\n if (score >= 75) {\n eyes = \"◠ ◠\";\n spout = chalk.cyan(' \":\" ');\n } else if (score >= 50) {\n eyes = \"• •\";\n spout = chalk.cyan(\" . \");\n }\n\n return [\n spout,\n border(\" .---. \"),\n border(`( ${eyes} )>`),\n border(\" \\\\___/ \"),\n ];\n};\n\nconst easeOutCubic = (x: number): number => 1 - (1 - x) ** 3;\n\nconst printScoreBox = async (\n score: number,\n label: string,\n categoryIssueCounts: Record<string, number>\n): Promise<void> => {\n const { isTTY } = process.stdout;\n const frameCount = 40;\n const frameDelay = 50;\n\n let scoreColor = chalk.red.bold;\n if (score >= 90) {\n scoreColor = chalk.green.bold;\n } else if (score >= 75) {\n scoreColor = chalk.yellow.bold;\n } else if (score >= 50) {\n scoreColor = chalk.magenta.bold;\n }\n const border = scoreColor;\n const whaleLines = getWhaleMascot(score, border);\n\n const totalIssues = Object.values(categoryIssueCounts).reduce(\n (a, b) => a + b,\n 0\n );\n const shareUrl = `https://github.com/PunGrumpy/docker-doctor/share?s=${score}&w=${totalIssues}`;\n\n if (isTTY) {\n // Hide cursor\n process.stdout.write(\"\\u001B[?25l\");\n try {\n for (let frame = 0; frame <= frameCount; frame += 1) {\n const progress = easeOutCubic(frame / frameCount);\n const currentScore = Math.round(score * progress);\n const filledBlocks = Math.round(currentScore / 2);\n const emptyBlocks = 50 - filledBlocks;\n\n const bar =\n scoreColor(\"█\".repeat(filledBlocks)) +\n chalk.dim(\"░\".repeat(emptyBlocks));\n\n if (frame > 0) {\n // Move cursor up 4 lines and carriage return\n process.stdout.write(\"\\u001B[4A\\r\");\n } else {\n // print an extra newline first to start\n console.log();\n }\n\n process.stdout.write(\n ` ${whaleLines[0]} ${scoreColor(`${currentScore} / 100`)} ${scoreColor(label)}\\n` +\n ` ${whaleLines[1]} ${bar}\\n` +\n ` ${whaleLines[2]} ${chalk.dim(\"Docker Doctor (https://github.com/PunGrumpy/docker-doctor)\")}\\n` +\n ` ${whaleLines[3]}\\n`\n );\n\n if (frame < frameCount) {\n // eslint-disable-next-line no-await-in-loop\n await setTimeout(frameDelay);\n }\n }\n } finally {\n // Show cursor\n process.stdout.write(\"\\u001B[?25h\");\n }\n } else {\n // Non-TTY fall back to static print\n const filledBlocks = Math.round(score / 2);\n const emptyBlocks = 50 - filledBlocks;\n const bar =\n scoreColor(\"█\".repeat(filledBlocks)) + chalk.dim(\"░\".repeat(emptyBlocks));\n\n console.log(\n `\\n ${whaleLines[0]} ${scoreColor(`${score} / 100`)} ${scoreColor(label)}`\n );\n console.log(` ${whaleLines[1]} ${bar}`);\n console.log(\n ` ${whaleLines[2]} ${chalk.dim(\"Docker Doctor (https://github.com/PunGrumpy/docker-doctor)\")}`\n );\n console.log(` ${whaleLines[3]}`);\n }\n\n console.log(\n `\\n ${chalk.dim(\"────────────────────────────────────────────────────────────\")}\\n`\n );\n console.log(` Share: ${chalk.cyan(shareUrl)}`);\n console.log(` Tell others how you did on socials\\n`);\n console.log(\n ` Docs: ${chalk.cyan(\"https://github.com/PunGrumpy/docker-doctor/docs\")}`\n );\n console.log(` Learn more about fixing issues, setting up CI/CD, and`);\n console.log(` configuring rules with a config file\\n`);\n console.log(\n ` GitHub: ${chalk.cyan(\"https://github.com/PunGrumpy/docker-doctor\")}`\n );\n console.log(` Report issues and star the repository!`);\n};\n\nexport const formatTerminal = async (\n diagnostics: Diagnostic[],\n score: number,\n label: string,\n project: ProjectInfo,\n verbose = false,\n fileContents: Record<string, string> = {}\n): Promise<void> => {\n if (verbose) {\n console.log(`\\n${chalk.bold(\"Docker Doctor Diagnostics\")}`);\n console.log(`=================================`);\n printDiscoveredFiles(project);\n }\n\n // Calculate scores per category once\n const categoryIssueCounts: Record<string, number> = {\n \"Best Practices\": 0,\n Compose: 0,\n \"Image Size\": 0,\n Performance: 0,\n Security: 0,\n };\n\n for (const d of diagnostics) {\n const ruleDef = findRule(d.rule);\n const category = ruleDef?.category || \"Best Practices\";\n categoryIssueCounts[category] = (categoryIssueCounts[category] || 0) + 1;\n }\n\n printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts);\n\n await printScoreBox(score, label, categoryIssueCounts);\n};\n","import fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { stdin as input, stdout as output } from \"node:process\";\nimport readline from \"node:readline/promises\";\nimport { setTimeout } from \"node:timers/promises\";\n\nimport type { Diagnostic, RuleSeverity } from \"@docker-doctor/core\";\nimport {\n discoverProject,\n parseDockerfile,\n parseCompose,\n runDockerfileRules,\n runComposeRules,\n calculateScore,\n loadConfig,\n allRules,\n findRule,\n toJsonReport,\n} from \"@docker-doctor/core\";\nimport chalk from \"chalk\";\nimport { Command } from \"commander\";\n\nimport packageJson from \"../package.json\" with { type: \"json\" };\nimport { formatTerminal } from \"./formatters/terminal.js\";\n\nconst runInteractiveWizard = async (): Promise<void> => {\n const rl = readline.createInterface({ input, output });\n try {\n const ghAnswer = await rl.question(\n `\\n ${chalk.green(\"✔\")} ${chalk.bold(\"Add Docker Doctor to GitHub Actions?\")}\\n` +\n ` Scan every pull request to prevent new Docker issues while you fix the backlog.\\n` +\n ` › (y/N) `\n );\n if (ghAnswer.trim().toLowerCase() === \"y\") {\n const workflowDir = path.resolve(\".github/workflows\");\n await fs.mkdir(workflowDir, { recursive: true });\n const workflowPath = path.join(workflowDir, \"docker-doctor.yml\");\n const workflowYaml = `name: Docker Doctor Scan\non:\n push:\n branches: [ main, master ]\n pull_request:\n branches: [ main, master ]\njobs:\n docker-doctor:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - name: Setup Bun\n uses: oven-sh/setup-bun@v2\n - name: Install dependencies\n run: bun install\n - name: Run docker-doctor\n run: bunx docker-doctor .\n`;\n await fs.writeFile(workflowPath, workflowYaml, \"utf-8\");\n console.log(\n `\\n ${chalk.green(\"✨\")} Created ${chalk.cyan(\".github/workflows/docker-doctor.yml\")}!`\n );\n console.log(\n ` Scan every pull request to prevent new Docker issues while you fix the backlog.`\n );\n }\n\n const nextAnswer = await rl.question(\n `\\n ${chalk.green(\"✔\")} ${chalk.bold(\"What would you like to do next?\")}\\n` +\n ` 1: View rules list\\n` +\n ` 2: Skip\\n` +\n ` › `\n );\n if (nextAnswer.trim() === \"1\") {\n console.log(`\\n ${chalk.bold(\"Available Rules:\")}`);\n for (const r of allRules) {\n console.log(\n ` - ${chalk.cyan(r.key)}: ${r.message} (${chalk.dim(r.category)})`\n );\n }\n }\n } catch {\n // Ignore prompt errors\n } finally {\n rl.close();\n }\n};\n\nconst runRulesEngine = async (\n rootDir: string,\n project: { dockerfiles: string[]; composeFiles: string[] },\n rulesConfig: Record<string, RuleSeverity> | undefined,\n projectFilesList: string[],\n fileContents: Record<string, string>,\n options: { score?: boolean; json?: boolean },\n setStatus: (text: string) => void\n): Promise<Diagnostic[]> => {\n const diagnostics: Diagnostic[] = [];\n\n const isSilent = options.score || options.json;\n\n if (process.stdout.isTTY && !isSilent) {\n setStatus(`Analyzing ${project.dockerfiles.length} Dockerfile(s)...`);\n await setTimeout(100);\n }\n\n // 1. Scan Dockerfiles in parallel\n const dockerfileResults = await Promise.all(\n project.dockerfiles.map(async (df) => {\n const fullPath = path.join(rootDir, df);\n try {\n const content = await fs.readFile(fullPath, \"utf-8\");\n fileContents[df] = content;\n const instructions = parseDockerfile(content);\n return runDockerfileRules(\n instructions,\n df,\n projectFilesList,\n rulesConfig\n );\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n console.error(`Failed to analyze Dockerfile ${df}: ${msg}`);\n return [];\n }\n })\n );\n for (const diags of dockerfileResults) {\n diagnostics.push(...diags);\n }\n\n if (process.stdout.isTTY && !isSilent) {\n setStatus(`Analyzing ${project.composeFiles.length} Compose file(s)...`);\n await setTimeout(100);\n }\n\n // 2. Scan Compose files in parallel\n const composeResults = await Promise.all(\n project.composeFiles.map(async (cf) => {\n const fullPath = path.join(rootDir, cf);\n try {\n const content = await fs.readFile(fullPath, \"utf-8\");\n fileContents[cf] = content;\n const composeObj = parseCompose(content, cf);\n return runComposeRules(composeObj, cf, rulesConfig);\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n console.error(`Failed to analyze Compose file ${cf}: ${msg}`);\n return [];\n }\n })\n );\n for (const diags of composeResults) {\n diagnostics.push(...diags);\n }\n\n return diagnostics;\n};\n\nconst program = new Command();\n\nprogram\n .name(\"docker-doctor\")\n .description(\"Static analysis for Dockerfile and Docker Compose files\")\n .version(packageJson.version, \"-V, --version\", \"display the version number\");\n\n// Default scan command\nprogram\n .argument(\"[dir]\", \"directory to scan\", \".\")\n .option(\"-v, --verbose\", \"show verbose diagnostics description\", false)\n .option(\"-s, --score\", \"only output numeric health score\", false)\n .option(\"-j, --json\", \"output results as JSON report\", false)\n .option(\"-c, --config <path>\", \"custom config file path\")\n .action(async (dir, options) => {\n try {\n const rootDir = path.resolve(dir);\n const startTime = Date.now();\n\n const isSilent = options.score || options.json;\n\n let statusText = \"Discovering workspace...\";\n const spinnerFrames = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n let frameIndex = 0;\n let spinnerInterval: NodeJS.Timeout | null = null;\n\n const setStatus = (text: string) => {\n statusText = text;\n };\n\n if (process.stdout.isTTY && !isSilent) {\n process.stdout.write(`${chalk.cyan(spinnerFrames[0])} ${statusText}`);\n spinnerInterval = setInterval(() => {\n process.stdout.write(\n `\\r${chalk.cyan(spinnerFrames[frameIndex])} ${statusText}`\n );\n frameIndex = (frameIndex + 1) % spinnerFrames.length;\n }, 80);\n }\n\n try {\n if (process.stdout.isTTY && !isSilent) {\n await setTimeout(150);\n }\n\n setStatus(\"Loading configuration...\");\n // Load config first\n const config = await loadConfig(rootDir, options.config);\n\n setStatus(\"Scanning workspace files...\");\n // Project discovery\n const project = await discoverProject(rootDir);\n\n // Collect all diagnostics\n const fileContents: Record<string, string> = {};\n\n const projectFilesList = [\n ...project.dockerfiles,\n ...project.composeFiles,\n ];\n\n const diagnostics = await runRulesEngine(\n rootDir,\n project,\n config.rules,\n projectFilesList,\n fileContents,\n options,\n setStatus\n );\n\n // Filter by category config if needed\n let filteredDiagnostics = diagnostics;\n if (config.categories) {\n filteredDiagnostics = diagnostics.filter((d) => {\n const ruleDef = findRule(d.rule);\n if (ruleDef) {\n const catSeverity = config.categories?.[ruleDef.category];\n if (catSeverity === \"off\") {\n return false;\n }\n }\n return true;\n });\n }\n\n // Calculate score\n const { score, label } = calculateScore(filteredDiagnostics);\n\n const duration = ((Date.now() - startTime) / 1000).toFixed(1);\n const concurrency = os.cpus().length;\n\n if (spinnerInterval !== null) {\n clearInterval(spinnerInterval);\n spinnerInterval = null;\n // Clear the spinner line\n process.stdout.write(\"\\r\\u001B[K\");\n }\n\n if (process.stdout.isTTY && !isSilent) {\n console.log(\n `${chalk.green(\"✔\")} Scanned ${projectFilesList.length} files in ${duration}s [~${concurrency} workers]`\n );\n }\n\n if (options.score) {\n console.log(score);\n process.exit(score < 50 ? 1 : 0);\n } else if (options.json) {\n const report = toJsonReport(\n filteredDiagnostics,\n score,\n label,\n project\n );\n console.log(JSON.stringify(report, null, 2));\n const hasErrors = filteredDiagnostics.some(\n (d) => d.severity === \"error\"\n );\n process.exit(hasErrors ? 1 : 0);\n } else {\n await formatTerminal(\n filteredDiagnostics,\n score,\n label,\n project,\n options.verbose,\n fileContents\n );\n\n // Exit with non-zero code if there are any error severity diagnostics\n const hasErrors = filteredDiagnostics.some(\n (d) => d.severity === \"error\"\n );\n\n if (process.stdout.isTTY) {\n await runInteractiveWizard();\n }\n process.exit(hasErrors ? 1 : 0);\n }\n } finally {\n if (spinnerInterval !== null) {\n clearInterval(spinnerInterval);\n process.stdout.write(\"\\r\\u001B[K\");\n }\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n console.error(`Error: ${msg}`);\n process.exit(1);\n }\n });\n\n// Rules subcommand group\nconst rules = program\n .command(\"rules\")\n .description(\"manage and list configuration rules\");\n\nrules\n .command(\"list\")\n .description(\"list all available rules\")\n .action(() => {\n console.log(\"\\nAvailable Rules:\");\n console.log(\"================\\n\");\n for (const rule of allRules) {\n console.log(`- ${rule.key} (${rule.category})`);\n console.log(` Default Severity: ${rule.defaultSeverity}`);\n console.log(` Description: ${rule.message}\\n`);\n }\n });\n\nrules\n .command(\"explain <rule>\")\n .description(\"explain a specific rule in detail\")\n .action((ruleKey) => {\n const rule = findRule(ruleKey);\n if (!rule) {\n console.error(`Rule '${ruleKey}' not found.`);\n process.exit(1);\n }\n console.log(`\\nRule: ${rule.key}`);\n console.log(`Category: ${rule.category}`);\n console.log(`Default Severity: ${rule.defaultSeverity}`);\n console.log(`Description: ${rule.message}`);\n console.log(`Help / Fix: ${rule.help}\\n`);\n });\n\nprogram.parse(process.argv);\n"],"mappings":";;;;;;;;;;;;;;;;;AAMA,MAAM,kBACJ,SACA,MACA,kBACS;CACT,IAAI,CAAC,WAAW,CAAC,MACf;CAEF,MAAM,QAAQ,QAAQ,MAAM,QAAQ;CACpC,MAAM,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC;CAClC,MAAM,MAAM,KAAK,IAAI,MAAM,QAAQ,OAAO,CAAC;CAE3C,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,KAAK,GAAG;EACpC,MAAM,UAAU,MAAM,IAAI;EAC1B,MAAM,WAAW,MAAM;EACvB,MAAM,gBAAgB,OAAO,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;EAC/C,IAAI,UACF,QAAQ,IACN,KAAK,cAAc,GAAG,EAAE,GAAG,cAAM,KAAK,aAAa,EAAE,KAAK,cAAM,MAAM,OAAO,GAC/E;OAEA,QAAQ,IAAI,OAAO,cAAM,IAAI,aAAa,EAAE,KAAK,cAAM,IAAI,OAAO,GAAG;CAEzE;CACA,QAAQ,IAAI;AACd;AAEA,MAAM,wBAAwB,YAA+B;CAC3D,QAAQ,IAAI,qBAAqB;CACjC,QAAQ,IACN,oBAAoB,QAAQ,YAAY,SAAS,QAAQ,YAAY,KAAK,MAAM,cAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,cAAM,IAAI,MAAM,GAC9H;CACA,QAAQ,IACN,uBAAuB,QAAQ,aAAa,SAAS,QAAQ,aAAa,KAAK,MAAM,cAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,cAAM,IAAI,MAAM,GACnI;AACF;AAEA,MAAM,oBACJ,aACA,SACA,cACA,wBACS;CACT,IAAI,YAAY,WAAW,GAAG;EAC5B,QAAQ,IACN,KAAK,cAAM,MAAM,KAAK,qDAAqD,GAC7E;EACA;CACF;CAEA,IAAI,CAAC,SAAS;EACZ,QAAQ,IAAI,WAAW,cAAM,KAAK,YAAY,MAAM,EAAE,UAAU;EAUhE,KAAK,MAAM,OAAO;GAPhB;GACA;GACA;GACA;GACA;EAGyB,GAAG;GAC5B,MAAM,QAAQ,oBAAoB;GAClC,MAAM,aAAa,UAAU,IAAI,YAAY,GAAG,MAAM;GACtD,QAAQ,IAAI,KAAK,IAAI,KAAK,cAAM,IAAI,UAAU,GAAG;EACnD;EAEA,QAAQ,IACN,WAAW,cAAM,KAAK,yBAAyB,EAAE,iCACnD;EAGA,MAAM,aAAqC,CAAC;EAC5C,KAAK,MAAM,KAAK,aACd,WAAW,EAAE,SAAS,WAAW,EAAE,SAAS,KAAK;EAEnD,MAAM,iBAAiB,OAAO,QAAQ,UAAU,CAAC,CAAC,QAC/C,CAAC,GAAG,WAAW,SAAS,CAC3B;EACA,IAAI,eAAe,SAAS,GAAG;GAC7B,QAAQ,IAAI;GACZ,QAAQ,IACN,KAAK,cAAM,OAAO,mDAAmD,GACvE;GACA,KAAK,MAAM,CAAC,MAAM,UAAU,gBAC1B,QAAQ,IAAI,OAAO,cAAM,KAAK,IAAI,EAAE,IAAI,MAAM,UAAU,MAAM,OAAO;GAEvE,QAAQ,IACN,+DACF;GACA,QAAQ,IACN,iEACF;GACA,QAAQ,IACN,+DACF;GACA,QAAQ,IAAI,gDAAgD;GAC5D,QAAQ,IAAI,4DAA4D;EAC1E;EACA;CACF;CAEA,QAAQ,IAAI,WAAW,cAAM,KAAK,YAAY,MAAM,EAAE,WAAW;CAGjE,MAAM,eAA6C,CAAC;CACpD,KAAK,MAAM,KAAK,aAAa;EAC3B,IAAI,CAAC,aAAa,EAAE,OAClB,aAAa,EAAE,QAAQ,CAAC;EAE1B,aAAa,EAAE,KAAK,CAAC,KAAK,CAAC;CAC7B;CAEA,KAAK,MAAM,CAAC,MAAM,cAAc,OAAO,QAAQ,YAAY,GAAG;EAC5D,QAAQ,IAAI,KAAK,cAAM,UAAU,KAAK,IAAI,GAAG;EAC7C,KAAK,MAAM,KAAK,WAAW;GACzB,IAAI,WAAW,cAAM;GACrB,IAAI,SAAS;GACb,IAAI,EAAE,aAAa,SAAS;IAC1B,WAAW,cAAM,IAAI;IACrB,SAAS;GACX,OAAO,IAAI,EAAE,aAAa,WAAW;IACnC,WAAW,cAAM;IACjB,SAAS;GACX;GAEA,MAAM,WAAW,EAAE,OAAO,IAAI,EAAE,SAAS;GACzC,QAAQ,IAAI,KAAK,SAAS,MAAM,EAAE,IAAI,cAAM,IAAI,EAAE,IAAI,EAAE,GAAG,UAAU;GAGrE,MAAM,UAAU,aAAa;GAC7B,eAAe,SAAS,EAAE,MAAM,QAAQ;GAExC,QAAQ,IAAI,OAAO,cAAM,MAAM,EAAE,OAAO,GAAG;GAC3C,QAAQ,IAAI,OAAO,cAAM,IAAI,OAAO,EAAE,GAAG,EAAE,MAAM;GACjD,QAAQ,IAAI;EACd;CACF;AACF;AAEA,MAAM,kBACJ,OACA,WACa;CACb,IAAI,OAAO;CACX,IAAI,QAAQ;CACZ,IAAI,SAAS,IAAI;EACf,OAAO;EACP,QAAQ,cAAM,KAAK,YAAU;CAC/B,OAAO,IAAI,SAAS,IAAI;EACtB,OAAO;EACP,QAAQ,cAAM,KAAK,UAAU;CAC/B;CAEA,OAAO;EACL;EACA,OAAO,UAAU;EACjB,OAAO,KAAK,KAAK,IAAI;EACrB,OAAO,WAAW;CACpB;AACF;AAEA,MAAM,gBAAgB,MAAsB,KAAK,IAAI,MAAM;AAE3D,MAAM,gBAAgB,OACpB,OACA,OACA,wBACkB;CAClB,MAAM,EAAE,UAAU,QAAQ;CAC1B,MAAM,aAAa;CACnB,MAAM,aAAa;CAEnB,IAAI,aAAa,cAAM,IAAI;CAC3B,IAAI,SAAS,IACX,aAAa,cAAM,MAAM;MACpB,IAAI,SAAS,IAClB,aAAa,cAAM,OAAO;MACrB,IAAI,SAAS,IAClB,aAAa,cAAM,QAAQ;CAG7B,MAAM,aAAa,eAAe,OAAOA,UAAM;CAM/C,MAAM,WAAW,sDAAsD,MAAM,KAJzD,OAAO,OAAO,mBAAmB,CAAC,CAAC,QACpD,GAAG,MAAM,IAAI,GACd,CAE0F;CAE5F,IAAI,OAAO;EAET,QAAQ,OAAO,MAAM,WAAa;EAClC,IAAI;GACF,KAAK,IAAI,QAAQ,GAAG,SAAS,YAAY,SAAS,GAAG;IACnD,MAAM,WAAW,aAAa,QAAQ,UAAU;IAChD,MAAM,eAAe,KAAK,MAAM,QAAQ,QAAQ;IAChD,MAAM,eAAe,KAAK,MAAM,eAAe,CAAC;IAChD,MAAM,cAAc,KAAK;IAEzB,MAAM,MACJ,WAAW,IAAI,OAAO,YAAY,CAAC,IACnC,cAAM,IAAI,IAAI,OAAO,WAAW,CAAC;IAEnC,IAAI,QAAQ,GAEV,QAAQ,OAAO,MAAM,WAAa;SAGlC,QAAQ,IAAI;IAGd,QAAQ,OAAO,MACb,KAAK,WAAW,GAAG,IAAI,WAAW,GAAG,aAAa,OAAO,EAAE,GAAG,WAAW,KAAK,EAAE,MACzE,WAAW,GAAG,IAAI,IAAI,MACtB,WAAW,GAAG,IAAI,cAAM,IAAI,4DAA4D,EAAE,MAC1F,WAAW,GAAG,GACvB;IAEA,IAAI,QAAQ,YAEV,2CAAiB,UAAU;GAE/B;EACF,UAAU;GAER,QAAQ,OAAO,MAAM,WAAa;EACpC;CACF,OAAO;EAEL,MAAM,eAAe,KAAK,MAAM,QAAQ,CAAC;EACzC,MAAM,cAAc,KAAK;EACzB,MAAM,MACJ,WAAW,IAAI,OAAO,YAAY,CAAC,IAAI,cAAM,IAAI,IAAI,OAAO,WAAW,CAAC;EAE1E,QAAQ,IACN,OAAO,WAAW,GAAG,IAAI,WAAW,GAAG,MAAM,OAAO,EAAE,GAAG,WAAW,KAAK,GAC3E;EACA,QAAQ,IAAI,KAAK,WAAW,GAAG,IAAI,KAAK;EACxC,QAAQ,IACN,KAAK,WAAW,GAAG,IAAI,cAAM,IAAI,4DAA4D,GAC/F;EACA,QAAQ,IAAI,KAAK,WAAW,IAAI;CAClC;CAEA,QAAQ,IACN,OAAO,cAAM,IAAI,8DAA8D,EAAE,GACnF;CACA,QAAQ,IAAI,YAAY,cAAM,KAAK,QAAQ,GAAG;CAC9C,QAAQ,IAAI,wCAAwC;CACpD,QAAQ,IACN,WAAW,cAAM,KAAK,iDAAiD,GACzE;CACA,QAAQ,IAAI,yDAAyD;CACrE,QAAQ,IAAI,0CAA0C;CACtD,QAAQ,IACN,aAAa,cAAM,KAAK,4CAA4C,GACtE;CACA,QAAQ,IAAI,0CAA0C;AACxD;AAEA,MAAa,iBAAiB,OAC5B,aACA,OACA,OACA,SACA,UAAU,OACV,eAAuC,CAAC,MACtB;CAClB,IAAI,SAAS;EACX,QAAQ,IAAI,KAAK,cAAM,KAAK,2BAA2B,GAAG;EAC1D,QAAQ,IAAI,mCAAmC;EAC/C,qBAAqB,OAAO;CAC9B;CAGA,MAAM,sBAA8C;EAClD,kBAAkB;EAClB,SAAS;EACT,cAAc;EACd,aAAa;EACb,UAAU;CACZ;CAEA,KAAK,MAAM,KAAK,aAAa;EAE3B,MAAM,WADUC,qBAAS,EAAE,IACJ,CAAC,EAAE,YAAY;EACtC,oBAAoB,aAAa,oBAAoB,aAAa,KAAK;CACzE;CAEA,iBAAiB,aAAa,SAAS,cAAc,mBAAmB;CAExE,MAAM,cAAc,OAAO,OAAO,mBAAmB;AACvD;;;;ACjRA,MAAM,uBAAuB,YAA2B;CACtD,MAAM,KAAKC,+BAAS,gBAAgB;EAAE;EAAO;CAAO,CAAC;CACrD,IAAI;EAMF,KAAI,MALmB,GAAG,SACxB,OAAO,cAAM,MAAM,GAAG,EAAE,GAAG,cAAM,KAAK,sCAAsC,EAAE,oGAGhF,EACY,CAAC,KAAK,CAAC,CAAC,YAAY,MAAM,KAAK;GACzC,MAAM,cAAcC,kBAAK,QAAQ,mBAAmB;GACpD,MAAMC,yBAAG,MAAM,aAAa,EAAE,WAAW,KAAK,CAAC;GAC/C,MAAM,eAAeD,kBAAK,KAAK,aAAa,mBAAmB;GAmB/D,MAAMC,yBAAG,UAAU,cAAc;;;;;;;;;;;;;;;;;GAAc,OAAO;GACtD,QAAQ,IACN,OAAO,cAAM,MAAM,GAAG,EAAE,WAAW,cAAM,KAAK,qCAAqC,EAAE,EACvF;GACA,QAAQ,IACN,qFACF;EACF;EAQA,KAAI,MANqB,GAAG,SAC1B,OAAO,cAAM,MAAM,GAAG,EAAE,GAAG,cAAM,KAAK,iCAAiC,EAAE,8CAI3E,EACc,CAAC,KAAK,MAAM,KAAK;GAC7B,QAAQ,IAAI,OAAO,cAAM,KAAK,kBAAkB,GAAG;GACnD,KAAK,MAAM,KAAKC,sBACd,QAAQ,IACN,SAAS,cAAM,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,IAAI,cAAM,IAAI,EAAE,QAAQ,EAAE,EACrE;EAEJ;CACF,QAAQ,CAER,UAAU;EACR,GAAG,MAAM;CACX;AACF;AAEA,MAAM,iBAAiB,OACrB,SACA,SACA,aACA,kBACA,cACA,SACA,cAC0B;CAC1B,MAAM,cAA4B,CAAC;CAEnC,MAAM,WAAW,QAAQ,SAAS,QAAQ;CAE1C,IAAI,QAAQ,OAAO,SAAS,CAAC,UAAU;EACrC,UAAU,aAAa,QAAQ,YAAY,OAAO,kBAAkB;EACpE,2CAAiB,GAAG;CACtB;CAGA,MAAM,oBAAoB,MAAM,QAAQ,IACtC,QAAQ,YAAY,IAAI,OAAO,OAAO;EACpC,MAAM,WAAWF,kBAAK,KAAK,SAAS,EAAE;EACtC,IAAI;GACF,MAAM,UAAU,MAAMC,yBAAG,SAAS,UAAU,OAAO;GACnD,aAAa,MAAM;GAEnB,OAAOE,+BADcC,4BAAgB,OAExB,GACX,IACA,kBACA,WACF;EACF,SAAS,OAAgB;GACvB,MAAM,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GACjE,QAAQ,MAAM,gCAAgC,GAAG,IAAI,KAAK;GAC1D,OAAO,CAAC;EACV;CACF,CAAC,CACH;CACA,KAAK,MAAM,SAAS,mBAClB,YAAY,KAAK,GAAG,KAAK;CAG3B,IAAI,QAAQ,OAAO,SAAS,CAAC,UAAU;EACrC,UAAU,aAAa,QAAQ,aAAa,OAAO,oBAAoB;EACvE,2CAAiB,GAAG;CACtB;CAGA,MAAM,iBAAiB,MAAM,QAAQ,IACnC,QAAQ,aAAa,IAAI,OAAO,OAAO;EACrC,MAAM,WAAWJ,kBAAK,KAAK,SAAS,EAAE;EACtC,IAAI;GACF,MAAM,UAAU,MAAMC,yBAAG,SAAS,UAAU,OAAO;GACnD,aAAa,MAAM;GAEnB,OAAOI,4BADYC,yBAAa,SAAS,EACT,GAAG,IAAI,WAAW;EACpD,SAAS,OAAgB;GACvB,MAAM,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GACjE,QAAQ,MAAM,kCAAkC,GAAG,IAAI,KAAK;GAC5D,OAAO,CAAC;EACV;CACF,CAAC,CACH;CACA,KAAK,MAAM,SAAS,gBAClB,YAAY,KAAK,GAAG,KAAK;CAG3B,OAAO;AACT;AAEA,MAAM,UAAU,IAAIC,kBAAQ;AAE5B,QACG,KAAK,eAAe,CAAC,CACrB,YAAY,yDAAyD,CAAC,CACtE,QAAQC,4BAAY,SAAS,iBAAiB,4BAA4B;AAG7E,QACG,SAAS,SAAS,qBAAqB,GAAG,CAAC,CAC3C,OAAO,iBAAiB,wCAAwC,KAAK,CAAC,CACtE,OAAO,eAAe,oCAAoC,KAAK,CAAC,CAChE,OAAO,cAAc,iCAAiC,KAAK,CAAC,CAC5D,OAAO,uBAAuB,yBAAyB,CAAC,CACxD,OAAO,OAAO,KAAK,YAAY;CAC9B,IAAI;EACF,MAAM,UAAUR,kBAAK,QAAQ,GAAG;EAChC,MAAM,YAAY,KAAK,IAAI;EAE3B,MAAM,WAAW,QAAQ,SAAS,QAAQ;EAE1C,IAAI,aAAa;EACjB,MAAM,gBAAgB;GAAC;GAAK;GAAK;GAAK;GAAK;GAAK;GAAK;GAAK;GAAK;GAAK;EAAG;EACvE,IAAI,aAAa;EACjB,IAAI,kBAAyC;EAE7C,MAAM,aAAa,SAAiB;GAClC,aAAa;EACf;EAEA,IAAI,QAAQ,OAAO,SAAS,CAAC,UAAU;GACrC,QAAQ,OAAO,MAAM,GAAG,cAAM,KAAK,cAAc,EAAE,EAAE,GAAG,YAAY;GACpE,kBAAkB,kBAAkB;IAClC,QAAQ,OAAO,MACb,KAAK,cAAM,KAAK,cAAc,WAAW,EAAE,GAAG,YAChD;IACA,cAAc,aAAa,KAAK,cAAc;GAChD,GAAG,EAAE;EACP;EAEA,IAAI;GACF,IAAI,QAAQ,OAAO,SAAS,CAAC,UAC3B,2CAAiB,GAAG;GAGtB,UAAU,0BAA0B;GAEpC,MAAM,SAAS,MAAMS,uBAAW,SAAS,QAAQ,MAAM;GAEvD,UAAU,6BAA6B;GAEvC,MAAM,UAAU,MAAMC,4BAAgB,OAAO;GAG7C,MAAM,eAAuC,CAAC;GAE9C,MAAM,mBAAmB,CACvB,GAAG,QAAQ,aACX,GAAG,QAAQ,YACb;GAEA,MAAM,cAAc,MAAM,eACxB,SACA,SACA,OAAO,OACP,kBACA,cACA,SACA,SACF;GAGA,IAAI,sBAAsB;GAC1B,IAAI,OAAO,YACT,sBAAsB,YAAY,QAAQ,MAAM;IAC9C,MAAM,UAAUC,qBAAS,EAAE,IAAI;IAC/B,IAAI,SAEF;SADoB,OAAO,aAAa,QAAQ,cAC5B,OAClB,OAAO;IACT;IAEF,OAAO;GACT,CAAC;GAIH,MAAM,EAAE,OAAO,UAAUC,2BAAe,mBAAmB;GAE3D,MAAM,aAAa,KAAK,IAAI,IAAI,aAAa,IAAI,CAAE,QAAQ,CAAC;GAC5D,MAAM,cAAcC,gBAAG,KAAK,CAAC,CAAC;GAE9B,IAAI,oBAAoB,MAAM;IAC5B,cAAc,eAAe;IAC7B,kBAAkB;IAElB,QAAQ,OAAO,MAAM,UAAY;GACnC;GAEA,IAAI,QAAQ,OAAO,SAAS,CAAC,UAC3B,QAAQ,IACN,GAAG,cAAM,MAAM,GAAG,EAAE,WAAW,iBAAiB,OAAO,YAAY,SAAS,MAAM,YAAY,UAChG;GAGF,IAAI,QAAQ,OAAO;IACjB,QAAQ,IAAI,KAAK;IACjB,QAAQ,KAAK,QAAQ,KAAK,IAAI,CAAC;GACjC,OAAO,IAAI,QAAQ,MAAM;IACvB,MAAM,SAASC,yBACb,qBACA,OACA,OACA,OACF;IACA,QAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;IAC3C,MAAM,YAAY,oBAAoB,MACnC,MAAM,EAAE,aAAa,OACxB;IACA,QAAQ,KAAK,YAAY,IAAI,CAAC;GAChC,OAAO;IACL,MAAM,eACJ,qBACA,OACA,OACA,SACA,QAAQ,SACR,YACF;IAGA,MAAM,YAAY,oBAAoB,MACnC,MAAM,EAAE,aAAa,OACxB;IAEA,IAAI,QAAQ,OAAO,OACjB,MAAM,qBAAqB;IAE7B,QAAQ,KAAK,YAAY,IAAI,CAAC;GAChC;EACF,UAAU;GACR,IAAI,oBAAoB,MAAM;IAC5B,cAAc,eAAe;IAC7B,QAAQ,OAAO,MAAM,UAAY;GACnC;EACF;CACF,SAAS,OAAgB;EACvB,MAAM,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACjE,QAAQ,MAAM,UAAU,KAAK;EAC7B,QAAQ,KAAK,CAAC;CAChB;AACF,CAAC;AAGH,MAAM,QAAQ,QACX,QAAQ,OAAO,CAAC,CAChB,YAAY,qCAAqC;AAEpD,MACG,QAAQ,MAAM,CAAC,CACf,YAAY,0BAA0B,CAAC,CACvC,aAAa;CACZ,QAAQ,IAAI,oBAAoB;CAChC,QAAQ,IAAI,oBAAoB;CAChC,KAAK,MAAM,QAAQZ,sBAAU;EAC3B,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,SAAS,EAAE;EAC9C,QAAQ,IAAI,uBAAuB,KAAK,iBAAiB;EACzD,QAAQ,IAAI,uBAAuB,KAAK,QAAQ,GAAG;CACrD;AACF,CAAC;AAEH,MACG,QAAQ,gBAAgB,CAAC,CACzB,YAAY,mCAAmC,CAAC,CAChD,QAAQ,YAAY;CACnB,MAAM,OAAOS,qBAAS,OAAO;CAC7B,IAAI,CAAC,MAAM;EACT,QAAQ,MAAM,SAAS,QAAQ,aAAa;EAC5C,QAAQ,KAAK,CAAC;CAChB;CACA,QAAQ,IAAI,uBAAuB,KAAK,KAAK;CAC7C,QAAQ,IAAI,qBAAqB,KAAK,UAAU;CAChD,QAAQ,IAAI,qBAAqB,KAAK,iBAAiB;CACvD,QAAQ,IAAI,qBAAqB,KAAK,SAAS;CAC/C,QAAQ,IAAI,qBAAqB,KAAK,KAAK,GAAG;AAChD,CAAC;AAEH,QAAQ,MAAM,QAAQ,IAAI"}
|
|
1
|
+
{"version":3,"file":"cli.cjs","names":["border","findRule","path","fs","allRules","runDockerfileRules","parseDockerfile","runComposeRules","parseCompose","findRule","Command","packageJson","loadConfig","discoverProject","calculateScore","os","toJsonReport"],"sources":["../src/formatters/terminal.ts","../src/cli.ts"],"sourcesContent":["import { setTimeout } from \"node:timers/promises\";\n\nimport type { Diagnostic, ProjectInfo } from \"@docker-doctor/core\";\nimport { findRule } from \"@docker-doctor/core\";\nimport chalk from \"chalk\";\n\nconst printCodeFrame = (\n content: string | undefined,\n line: number | undefined,\n severityColor: (msg: string) => string\n): void => {\n if (!content || !line) {\n return;\n }\n const lines = content.split(/\\r?\\n/u);\n const start = Math.max(1, line - 1);\n const end = Math.min(lines.length, line + 1);\n\n for (let i = start; i <= end; i += 1) {\n const rawLine = lines[i - 1];\n const isTarget = i === line;\n const lineNumberStr = String(i).padStart(5, \" \");\n if (isTarget) {\n console.log(\n ` ${severityColor(\">\")} ${chalk.bold(lineNumberStr)} │ ${chalk.white(rawLine)}`\n );\n } else {\n console.log(` ${chalk.dim(lineNumberStr)} │ ${chalk.dim(rawLine)}`);\n }\n }\n console.log();\n};\n\nconst printDiscoveredFiles = (project: ProjectInfo): void => {\n console.log(`\\nDiscovered Files:`);\n console.log(\n ` Dockerfile(s): ${project.dockerfiles.length ? project.dockerfiles.map((f) => chalk.cyan(f)).join(\", \") : chalk.dim(\"None\")}`\n );\n console.log(\n ` Compose file(s): ${project.composeFiles.length ? project.composeFiles.map((f) => chalk.cyan(f)).join(\", \") : chalk.dim(\"None\")}`\n );\n};\n\nconst printDiagnostics = (\n diagnostics: Diagnostic[],\n verbose: boolean,\n fileContents: Record<string, string>,\n categoryIssueCounts: Record<string, number>\n): void => {\n if (diagnostics.length === 0) {\n console.log(\n `\\n${chalk.green.bold(\"✔ No issues found! Your Docker setup looks healthy.\")}`\n );\n return;\n }\n\n if (!verbose) {\n console.log(`\\n All ${chalk.bold(diagnostics.length)} issues\\n`);\n\n const categories = [\n \"Security\",\n \"Performance\",\n \"Best Practices\",\n \"Compose\",\n \"Image Size\",\n ];\n\n for (const cat of categories) {\n const count = categoryIssueCounts[cat];\n const issueLabel = count === 1 ? \"1 issue\" : `${count} issues`;\n console.log(` ${cat} › ${chalk.dim(issueLabel)}`);\n }\n\n console.log(\n `\\n Run ${chalk.cyan(\"docker-doctor --verbose\")} to list every error and warning`\n );\n\n // Migration-scale checks\n const ruleCounts: Record<string, number> = {};\n for (const d of diagnostics) {\n ruleCounts[d.rule] = (ruleCounts[d.rule] || 0) + 1;\n }\n const migrationRules = Object.entries(ruleCounts).filter(\n ([_, count]) => count >= 5\n );\n if (migrationRules.length > 0) {\n console.log();\n console.log(\n ` ${chalk.yellow(\"⚠ Migration-scale change: sample before you sweep\")}`\n );\n for (const [rule, count] of migrationRules) {\n console.log(` ${chalk.cyan(rule)} ×${count} across ${count} files`);\n }\n console.log(\n ` Fixing all of them at once is hard to review and prone to`\n );\n console.log(\n ` subtle mistakes across the whole repo. Fix a representative`\n );\n console.log(\n ` few first and confirm the recipe holds. Then get the code`\n );\n console.log(` owner's sign-off before changing the rest.`);\n console.log(` Scope it down one area at a time: docker-doctor <path>`);\n }\n return;\n }\n\n console.log(`\\nFound ${chalk.bold(diagnostics.length)} issue(s):`);\n\n // Group by file\n const filesGrouped: Record<string, Diagnostic[]> = {};\n for (const d of diagnostics) {\n if (!filesGrouped[d.file]) {\n filesGrouped[d.file] = [];\n }\n filesGrouped[d.file].push(d);\n }\n\n for (const [file, fileDiags] of Object.entries(filesGrouped)) {\n console.log(`\\n${chalk.underline.bold(file)}`);\n for (const d of fileDiags) {\n let sevColor = chalk.cyan;\n let prefix = \"ℹ INFO\";\n if (d.severity === \"error\") {\n sevColor = chalk.red.bold;\n prefix = \"✖ ERROR\";\n } else if (d.severity === \"warning\") {\n sevColor = chalk.yellow;\n prefix = \"⚠ WARN\";\n }\n\n const lineInfo = d.line ? `:${d.line}` : \"\";\n console.log(` ${sevColor(prefix)} [${chalk.dim(d.rule)}]${lineInfo}`);\n\n // Print Code Frame\n const content = fileContents[file];\n printCodeFrame(content, d.line, sevColor);\n\n console.log(` ${chalk.white(d.message)}`);\n console.log(` ${chalk.dim(\"Help:\")} ${d.help}`);\n console.log();\n }\n }\n};\n\nconst getWhaleMascot = (\n score: number,\n border: (text: string) => string\n): string[] => {\n let eyes = \"x x\";\n let spout = \" \";\n if (score >= 75) {\n eyes = \"◠ ◠\";\n spout = chalk.cyan(' \":\" ');\n } else if (score >= 50) {\n eyes = \"• •\";\n spout = chalk.cyan(\" . \");\n }\n\n return [\n spout,\n border(\" .---. \"),\n border(`( ${eyes} )>`),\n border(\" \\\\___/ \"),\n ];\n};\n\nconst easeOutCubic = (x: number): number => 1 - (1 - x) ** 3;\n\nconst printScoreBox = async (\n score: number,\n label: string,\n categoryIssueCounts: Record<string, number>\n): Promise<void> => {\n const { isTTY } = process.stdout;\n const shouldAnimate =\n isTTY &&\n !process.env.CI &&\n !process.env.NO_ANIMATION &&\n process.env.TERM !== \"dumb\" &&\n process.env.NODE_ENV !== \"test\";\n\n let scoreColor = chalk.red.bold;\n if (score >= 90) {\n scoreColor = chalk.green.bold;\n } else if (score >= 75) {\n scoreColor = chalk.yellow.bold;\n } else if (score >= 50) {\n scoreColor = chalk.magenta.bold;\n }\n const border = scoreColor;\n const whaleLines = getWhaleMascot(score, border);\n\n const totalIssues = Object.values(categoryIssueCounts).reduce(\n (a, b) => a + b,\n 0\n );\n const shareUrl = `https://github.com/PunGrumpy/docker-doctor/share?s=${score}&w=${totalIssues}`;\n\n if (shouldAnimate) {\n const frameCount = 20;\n const frameDelay = 15;\n\n // Hide cursor\n process.stdout.write(\"\\u001B[?25l\");\n try {\n for (let frame = 0; frame <= frameCount; frame += 1) {\n const progress = easeOutCubic(frame / frameCount);\n const currentScore = Math.round(score * progress);\n const filledBlocks = Math.round(currentScore / 2);\n const emptyBlocks = 50 - filledBlocks;\n\n const bar =\n scoreColor(\"█\".repeat(filledBlocks)) +\n chalk.dim(\"░\".repeat(emptyBlocks));\n\n if (frame > 0) {\n // Move cursor up 4 lines and carriage return\n process.stdout.write(\"\\u001B[4A\\r\");\n } else {\n // print an extra newline first to start\n console.log();\n }\n\n process.stdout.write(\n ` ${whaleLines[0]} ${scoreColor(`${currentScore} / 100`)} ${scoreColor(label)}\\n` +\n ` ${whaleLines[1]} ${bar}\\n` +\n ` ${whaleLines[2]} ${chalk.dim(\"Docker Doctor (https://github.com/PunGrumpy/docker-doctor)\")}\\n` +\n ` ${whaleLines[3]}\\n`\n );\n\n if (frame < frameCount) {\n // eslint-disable-next-line no-await-in-loop\n await setTimeout(frameDelay);\n }\n }\n } finally {\n // Show cursor\n process.stdout.write(\"\\u001B[?25h\");\n }\n } else {\n // Non-TTY fall back to static print\n const filledBlocks = Math.round(score / 2);\n const emptyBlocks = 50 - filledBlocks;\n const bar =\n scoreColor(\"█\".repeat(filledBlocks)) + chalk.dim(\"░\".repeat(emptyBlocks));\n\n console.log(\n `\\n ${whaleLines[0]} ${scoreColor(`${score} / 100`)} ${scoreColor(label)}`\n );\n console.log(` ${whaleLines[1]} ${bar}`);\n console.log(\n ` ${whaleLines[2]} ${chalk.dim(\"Docker Doctor (https://github.com/PunGrumpy/docker-doctor)\")}`\n );\n console.log(` ${whaleLines[3]}`);\n }\n\n console.log(\n `\\n ${chalk.dim(\"────────────────────────────────────────────────────────────\")}\\n`\n );\n console.log(` Share: ${chalk.cyan(shareUrl)}`);\n console.log(` Tell others how you did on socials\\n`);\n console.log(\n ` Docs: ${chalk.cyan(\"https://github.com/PunGrumpy/docker-doctor/docs\")}`\n );\n console.log(` Learn more about fixing issues, setting up CI/CD, and`);\n console.log(` configuring rules with a config file\\n`);\n console.log(\n ` GitHub: ${chalk.cyan(\"https://github.com/PunGrumpy/docker-doctor\")}`\n );\n console.log(` Report issues and star the repository!`);\n};\n\nexport const formatTerminal = async (\n diagnostics: Diagnostic[],\n score: number,\n label: string,\n project: ProjectInfo,\n verbose = false,\n fileContents: Record<string, string> = {}\n): Promise<void> => {\n if (verbose) {\n console.log(`\\n${chalk.bold(\"Docker Doctor Diagnostics\")}`);\n console.log(`=================================`);\n printDiscoveredFiles(project);\n }\n\n // Calculate scores per category once\n const categoryIssueCounts: Record<string, number> = {\n \"Best Practices\": 0,\n Compose: 0,\n \"Image Size\": 0,\n Performance: 0,\n Security: 0,\n };\n\n for (const d of diagnostics) {\n const ruleDef = findRule(d.rule);\n const category = ruleDef?.category || \"Best Practices\";\n categoryIssueCounts[category] = (categoryIssueCounts[category] || 0) + 1;\n }\n\n printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts);\n\n await printScoreBox(score, label, categoryIssueCounts);\n};\n","import fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport readline from \"node:readline\";\nimport { setTimeout } from \"node:timers/promises\";\n\nimport type { Diagnostic, RuleSeverity } from \"@docker-doctor/core\";\nimport {\n discoverProject,\n parseDockerfile,\n parseCompose,\n runDockerfileRules,\n runComposeRules,\n calculateScore,\n loadConfig,\n allRules,\n findRule,\n toJsonReport,\n} from \"@docker-doctor/core\";\nimport chalk from \"chalk\";\nimport { Command } from \"commander\";\n\nimport packageJson from \"../package.json\" with { type: \"json\" };\nimport { formatTerminal } from \"./formatters/terminal\";\n\ninterface KeypressKey {\n name?: string;\n ctrl?: boolean;\n meta?: boolean;\n shift?: boolean;\n}\n\nconst askConfirm = (question: string, defaultYes = false): Promise<boolean> => {\n const isRaw = process.stdin.isTTY;\n if (!isRaw) {\n return Promise.resolve(defaultYes);\n }\n\n /* eslint-disable promise/avoid-new */\n return new Promise((resolve) => {\n let value = defaultYes;\n\n readline.emitKeypressEvents(process.stdin);\n process.stdin.setRawMode(true);\n process.stdin.resume();\n\n // Hide cursor during prompt\n process.stdout.write(\"\\u001B[?25l\");\n\n const render = (firstTime = false) => {\n if (!firstTime) {\n process.stdout.write(\"\\u001B[3A\\r\");\n }\n\n process.stdout.write(\n `\\r\\u001B[K ${chalk.green(\"✔\")} ${chalk.bold(question)}\\n`\n );\n\n // Print options vertically\n const yesPrefix = value ? chalk.cyan(\"❯ \") : \" \";\n const yesText = value ? chalk.cyan.bold(\"Yes\") : chalk.dim(\"Yes\");\n process.stdout.write(`\\r\\u001B[K${yesPrefix}${yesText}\\n`);\n\n const noPrefix = value ? \" \" : chalk.cyan(\"❯ \");\n const noText = value ? chalk.dim(\"No\") : chalk.cyan.bold(\"No\");\n process.stdout.write(`\\r\\u001B[K${noPrefix}${noText}\\n`);\n };\n\n render(true);\n\n const handleKeypress = (str: string, key: KeypressKey) => {\n const cleanup = () => {\n process.stdin.removeListener(\"keypress\", handleKeypress);\n if (process.stdin.isTTY) {\n process.stdin.setRawMode(false);\n }\n process.stdin.pause();\n process.stdout.write(\"\\u001B[?25h\");\n };\n\n if (\n key.name === \"left\" ||\n key.name === \"right\" ||\n key.name === \"up\" ||\n key.name === \"down\" ||\n key.name === \"h\" ||\n key.name === \"l\" ||\n key.name === \"j\" ||\n key.name === \"k\"\n ) {\n value = !value;\n render();\n } else if (str === \"y\" || str === \"Y\") {\n value = true;\n render();\n } else if (str === \"n\" || str === \"N\") {\n value = false;\n render();\n } else if (\n key.name === \"return\" ||\n key.name === \"enter\" ||\n str === \"\\r\" ||\n str === \"\\n\"\n ) {\n cleanup();\n // Overwrite and resolve vertical layout cleanly\n process.stdout.write(\"\\u001B[3A\\r\\u001B[K\");\n process.stdout.write(\n ` ${chalk.green(\"✔\")} ${chalk.bold(question)} › ${value ? chalk.cyan(\"Yes\") : chalk.dim(\"No\")}\\n`\n );\n process.stdout.write(\"\\r\\u001B[K\\n\");\n process.stdout.write(\"\\r\\u001B[K\\n\");\n process.stdout.write(\"\\u001B[2A\");\n resolve(value);\n } else if (key.ctrl && key.name === \"c\") {\n cleanup();\n process.stdout.write(\"\\n\");\n process.exit(130);\n }\n };\n\n process.stdin.on(\"keypress\", handleKeypress);\n });\n /* eslint-enable promise/avoid-new */\n};\n\nconst askSelect = (\n question: string,\n options: string[],\n defaultIndex = 0\n): Promise<number> => {\n const isRaw = process.stdin.isTTY;\n if (!isRaw) {\n return Promise.resolve(defaultIndex);\n }\n\n /* eslint-disable promise/avoid-new */\n return new Promise((resolve) => {\n let index = defaultIndex;\n\n readline.emitKeypressEvents(process.stdin);\n process.stdin.setRawMode(true);\n process.stdin.resume();\n\n // Hide cursor during prompt\n process.stdout.write(\"\\u001B[?25l\");\n\n const render = (firstTime = false) => {\n if (!firstTime) {\n // Move back up to overwrite previous render\n process.stdout.write(`\\u001B[${options.length + 1}A\\r`);\n }\n\n process.stdout.write(\n `\\r\\u001B[K ${chalk.green(\"✔\")} ${chalk.bold(question)}\\n`\n );\n\n // Print options\n let i = 0;\n for (const option of options) {\n const isSelected = i === index;\n const prefix = isSelected ? chalk.cyan(\"❯ \") : \" \";\n const text = isSelected ? chalk.cyan.bold(option) : chalk.dim(option);\n process.stdout.write(`\\r\\u001B[K${prefix}${text}\\n`);\n i += 1;\n }\n };\n\n render(true);\n\n const handleKeypress = (str: string, key: KeypressKey) => {\n const cleanup = () => {\n process.stdin.removeListener(\"keypress\", handleKeypress);\n if (process.stdin.isTTY) {\n process.stdin.setRawMode(false);\n }\n process.stdin.pause();\n process.stdout.write(\"\\u001B[?25h\");\n };\n\n if (key.name === \"up\" || key.name === \"k\") {\n index = (index - 1 + options.length) % options.length;\n render();\n } else if (key.name === \"down\" || key.name === \"j\") {\n index = (index + 1) % options.length;\n render();\n } else if (\n key.name === \"return\" ||\n key.name === \"enter\" ||\n str === \"\\r\" ||\n str === \"\\n\"\n ) {\n cleanup();\n // Overwrite and resolve\n process.stdout.write(`\\u001B[${options.length + 1}A\\r\\u001B[K`);\n process.stdout.write(\n ` ${chalk.green(\"✔\")} ${chalk.bold(question)} › ${chalk.cyan(options[index])}\\n`\n );\n for (const _ of options) {\n process.stdout.write(\"\\r\\u001B[K\\n\");\n }\n process.stdout.write(`\\u001B[${options.length}A`);\n resolve(index);\n } else if (key.ctrl && key.name === \"c\") {\n cleanup();\n process.stdout.write(\"\\n\");\n process.exit(130);\n }\n };\n\n process.stdin.on(\"keypress\", handleKeypress);\n });\n /* eslint-enable promise/avoid-new */\n};\n\nconst runInteractiveWizard = async (): Promise<void> => {\n try {\n const addGhActions = await askConfirm(\n \"Add Docker Doctor to GitHub Actions?\"\n );\n if (addGhActions) {\n const workflowDir = path.resolve(\".github/workflows\");\n await fs.mkdir(workflowDir, { recursive: true });\n const workflowPath = path.join(workflowDir, \"docker-doctor.yml\");\n const workflowYaml = `name: Docker Doctor Scan\non:\n push:\n branches: [ main, master ]\n pull_request:\n branches: [ main, master ]\njobs:\n docker-doctor:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - name: Setup Bun\n uses: oven-sh/setup-bun@v2\n - name: Install dependencies\n run: bun install\n - name: Run docker-doctor\n run: bunx docker-doctor .\n`;\n await fs.writeFile(workflowPath, workflowYaml, \"utf-8\");\n console.log(\n `\\n ${chalk.green(\"✨\")} Created ${chalk.cyan(\".github/workflows/docker-doctor.yml\")}!`\n );\n console.log(\n ` Scan every pull request to prevent new Docker issues while you fix the backlog.`\n );\n }\n\n const nextChoice = await askSelect(\"What would you like to do next?\", [\n \"View rules list\",\n \"Skip\",\n ]);\n\n if (nextChoice === 0) {\n console.log(`\\n ${chalk.bold(\"Available Rules:\")}`);\n for (const r of allRules) {\n console.log(\n ` - ${chalk.cyan(r.key)}: ${r.message} (${chalk.dim(r.category)})`\n );\n }\n }\n } catch {\n // Ignore prompt errors\n }\n};\n\nconst runRulesEngine = async (\n rootDir: string,\n project: { dockerfiles: string[]; composeFiles: string[] },\n rulesConfig: Record<string, RuleSeverity> | undefined,\n projectFilesList: string[],\n fileContents: Record<string, string>,\n options: { score?: boolean; json?: boolean },\n setStatus: (text: string) => void\n): Promise<Diagnostic[]> => {\n const diagnostics: Diagnostic[] = [];\n\n const isSilent = options.score || options.json;\n\n if (process.stdout.isTTY && !isSilent) {\n setStatus(`Analyzing ${project.dockerfiles.length} Dockerfile(s)...`);\n await setTimeout(100);\n }\n\n // 1. Scan Dockerfiles in parallel\n const dockerfileResults = await Promise.all(\n project.dockerfiles.map(async (df) => {\n const fullPath = path.join(rootDir, df);\n try {\n const content = await fs.readFile(fullPath, \"utf-8\");\n fileContents[df] = content;\n const instructions = parseDockerfile(content);\n return runDockerfileRules(\n instructions,\n df,\n projectFilesList,\n rulesConfig\n );\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n console.error(`Failed to analyze Dockerfile ${df}: ${msg}`);\n return [];\n }\n })\n );\n for (const diags of dockerfileResults) {\n diagnostics.push(...diags);\n }\n\n if (process.stdout.isTTY && !isSilent) {\n setStatus(`Analyzing ${project.composeFiles.length} Compose file(s)...`);\n await setTimeout(100);\n }\n\n // 2. Scan Compose files in parallel\n const composeResults = await Promise.all(\n project.composeFiles.map(async (cf) => {\n const fullPath = path.join(rootDir, cf);\n try {\n const content = await fs.readFile(fullPath, \"utf-8\");\n fileContents[cf] = content;\n const composeObj = parseCompose(content, cf);\n return runComposeRules(composeObj, cf, rulesConfig);\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n console.error(`Failed to analyze Compose file ${cf}: ${msg}`);\n return [];\n }\n })\n );\n for (const diags of composeResults) {\n diagnostics.push(...diags);\n }\n\n return diagnostics;\n};\n\nconst filterDiagnostics = (\n diagnostics: Diagnostic[],\n categories?: Record<string, RuleSeverity>\n): Diagnostic[] => {\n if (!categories) {\n return diagnostics;\n }\n return diagnostics.filter((d) => {\n const ruleDef = findRule(d.rule);\n if (ruleDef) {\n const catSeverity = categories[ruleDef.category];\n if (catSeverity === \"off\") {\n return false;\n }\n }\n return true;\n });\n};\n\nconst program = new Command();\n\nprogram\n .name(\"docker-doctor\")\n .description(\"Static analysis for Dockerfile and Docker Compose files\")\n .version(packageJson.version, \"-V, --version\", \"display the version number\");\n\n// Default scan command\nprogram\n .argument(\"[dir]\", \"directory to scan\", \".\")\n .option(\"-v, --verbose\", \"show verbose diagnostics description\", false)\n .option(\"-s, --score\", \"only output numeric health score\", false)\n .option(\"-j, --json\", \"output results as JSON report\", false)\n .option(\"-c, --config <path>\", \"custom config file path\")\n .action(async (dir, options) => {\n const isSilent = options.score || options.json;\n\n // Helper to safely show terminal cursor\n const restoreCursor = (): void => {\n if (process.stdout.isTTY && !isSilent) {\n process.stdout.write(\"\\u001B[?25h\");\n }\n };\n\n // Set signal handlers to restore cursor on aborts\n process.on(\"exit\", restoreCursor);\n process.once(\"SIGINT\", () => {\n restoreCursor();\n process.exit(130);\n });\n process.once(\"SIGTERM\", () => {\n restoreCursor();\n process.exit(143);\n });\n\n try {\n const rootDir = path.resolve(dir);\n const startTime = Date.now();\n\n let statusText = \"Discovering workspace...\";\n const spinnerFrames = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n let frameIndex = 0;\n let spinnerInterval: NodeJS.Timeout | null = null;\n\n const setStatus = (text: string) => {\n statusText = text;\n };\n\n if (process.stdout.isTTY && !isSilent) {\n // Hide cursor during progress/spinner\n process.stdout.write(\"\\u001B[?25l\");\n process.stdout.write(`${chalk.cyan(spinnerFrames[0])} ${statusText}`);\n spinnerInterval = setInterval(() => {\n process.stdout.write(\n `\\r\\u001B[K${chalk.cyan(spinnerFrames[frameIndex])} ${statusText}`\n );\n frameIndex = (frameIndex + 1) % spinnerFrames.length;\n }, 80);\n }\n\n try {\n if (process.stdout.isTTY && !isSilent) {\n await setTimeout(150);\n }\n\n setStatus(\"Loading configuration...\");\n // Load config first\n const config = await loadConfig(rootDir, options.config);\n\n setStatus(\"Scanning workspace files...\");\n // Project discovery\n const project = await discoverProject(rootDir);\n\n // Collect all diagnostics\n const fileContents: Record<string, string> = {};\n\n const projectFilesList = [\n ...project.dockerfiles,\n ...project.composeFiles,\n ...(project.dockerignores || []),\n ];\n\n const diagnostics = await runRulesEngine(\n rootDir,\n project,\n config.rules,\n projectFilesList,\n fileContents,\n options,\n setStatus\n );\n\n // Filter by category config if needed\n const filteredDiagnostics = filterDiagnostics(\n diagnostics,\n config.categories\n );\n\n // Calculate score\n const { score, label } = calculateScore(filteredDiagnostics);\n\n const duration = ((Date.now() - startTime) / 1000).toFixed(1);\n const concurrency = os.cpus().length;\n\n if (spinnerInterval !== null) {\n clearInterval(spinnerInterval);\n spinnerInterval = null;\n // Clear spinner line and restore cursor\n process.stdout.write(\"\\r\\u001B[K\\u001B[?25h\");\n }\n\n if (process.stdout.isTTY && !isSilent) {\n console.log(\n `${chalk.green(\"✔\")} Scanned ${projectFilesList.length} files in ${duration}s [~${concurrency} workers]`\n );\n }\n\n if (options.score) {\n console.log(score);\n process.exit(score < 50 ? 1 : 0);\n } else if (options.json) {\n const report = toJsonReport(\n filteredDiagnostics,\n score,\n label,\n project\n );\n console.log(JSON.stringify(report, null, 2));\n const hasErrors = filteredDiagnostics.some(\n (d) => d.severity === \"error\"\n );\n process.exit(hasErrors ? 1 : 0);\n } else {\n await formatTerminal(\n filteredDiagnostics,\n score,\n label,\n project,\n options.verbose,\n fileContents\n );\n\n // Exit with non-zero code if there are any error severity diagnostics\n const hasErrors = filteredDiagnostics.some(\n (d) => d.severity === \"error\"\n );\n\n if (process.stdout.isTTY && process.stdin.isTTY) {\n await runInteractiveWizard();\n }\n process.exit(hasErrors ? 1 : 0);\n }\n } finally {\n if (spinnerInterval !== null) {\n clearInterval(spinnerInterval);\n process.stdout.write(\"\\r\\u001B[K\\u001B[?25h\");\n }\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n console.error(`Error: ${msg}`);\n process.exit(1);\n }\n });\n\n// Rules subcommand group\nconst rules = program\n .command(\"rules\")\n .description(\"manage and list configuration rules\");\n\nrules\n .command(\"list\")\n .description(\"list all available rules\")\n .action(() => {\n console.log(\"\\nAvailable Rules:\");\n console.log(\"================\\n\");\n for (const rule of allRules) {\n console.log(`- ${rule.key} (${rule.category})`);\n console.log(` Default Severity: ${rule.defaultSeverity}`);\n console.log(` Description: ${rule.message}\\n`);\n }\n });\n\nrules\n .command(\"explain <rule>\")\n .description(\"explain a specific rule in detail\")\n .action((ruleKey) => {\n const rule = findRule(ruleKey);\n if (!rule) {\n console.error(`Rule '${ruleKey}' not found.`);\n process.exit(1);\n }\n console.log(`\\nRule: ${rule.key}`);\n console.log(`Category: ${rule.category}`);\n console.log(`Default Severity: ${rule.defaultSeverity}`);\n console.log(`Description: ${rule.message}`);\n console.log(`Help / Fix: ${rule.help}\\n`);\n });\n\nprogram.parse(process.argv);\n"],"mappings":"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"}
|