@docker-doctor/cli 0.1.0 → 0.2.1
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 +12 -4
- package/dist/cli.cjs +155 -35
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +154 -34
- 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-BhdU_CBh.mjs → src-DvNFg6Nz.mjs} +33 -20
- package/dist/src-DvNFg6Nz.mjs.map +1 -0
- package/dist/{src-CjKyZmlM.cjs → src-O8FXMq8O.cjs} +33 -20
- package/dist/src-O8FXMq8O.cjs.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/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as runDockerfileRules, c as parseCompose, d as package_default, i as runComposeRules, l as parseDockerfile, n as calculateScore, o as allRules, r as loadConfig, s as findRule, t as toJsonReport, u as discoverProject } from "./src-
|
|
2
|
+
import { a as runDockerfileRules, c as parseCompose, d as package_default, i as runComposeRules, l as parseDockerfile, n as calculateScore, o as allRules, r as loadConfig, s as findRule, t as toJsonReport, u as discoverProject } from "./src-DvNFg6Nz.mjs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
6
|
-
import
|
|
7
|
-
import readline from "node:readline/promises";
|
|
6
|
+
import readline from "node:readline";
|
|
8
7
|
import { setTimeout } from "node:timers/promises";
|
|
9
8
|
import chalk from "chalk";
|
|
10
9
|
import { Command } from "commander";
|
|
@@ -109,17 +108,18 @@ const getWhaleMascot = (score, border) => {
|
|
|
109
108
|
];
|
|
110
109
|
};
|
|
111
110
|
const easeOutCubic = (x) => 1 - (1 - x) ** 3;
|
|
112
|
-
const printScoreBox = async (score, label, categoryIssueCounts) => {
|
|
111
|
+
const printScoreBox = async (score, label, categoryIssueCounts, warningsCount, errorsCount) => {
|
|
113
112
|
const { isTTY } = process.stdout;
|
|
114
|
-
const
|
|
115
|
-
const frameDelay = 50;
|
|
113
|
+
const shouldAnimate = isTTY && !process.env.CI && !process.env.NO_ANIMATION && process.env.TERM !== "dumb" && process.env.NODE_ENV !== "test";
|
|
116
114
|
let scoreColor = chalk.red.bold;
|
|
117
115
|
if (score >= 90) scoreColor = chalk.green.bold;
|
|
118
116
|
else if (score >= 75) scoreColor = chalk.yellow.bold;
|
|
119
117
|
else if (score >= 50) scoreColor = chalk.magenta.bold;
|
|
120
118
|
const whaleLines = getWhaleMascot(score, scoreColor);
|
|
121
|
-
const shareUrl = `https://
|
|
122
|
-
if (
|
|
119
|
+
const shareUrl = `https://docker-doctor.vercel.app/share?s=${score}&w=${warningsCount}&e=${errorsCount}`;
|
|
120
|
+
if (shouldAnimate) {
|
|
121
|
+
const frameCount = 20;
|
|
122
|
+
const frameDelay = 15;
|
|
123
123
|
process.stdout.write("\x1B[?25l");
|
|
124
124
|
try {
|
|
125
125
|
for (let frame = 0; frame <= frameCount; frame += 1) {
|
|
@@ -130,7 +130,7 @@ const printScoreBox = async (score, label, categoryIssueCounts) => {
|
|
|
130
130
|
const bar = scoreColor("█".repeat(filledBlocks)) + chalk.dim("░".repeat(emptyBlocks));
|
|
131
131
|
if (frame > 0) process.stdout.write("\x1B[4A\r");
|
|
132
132
|
else console.log();
|
|
133
|
-
process.stdout.write(` ${whaleLines[0]} ${scoreColor(`${currentScore} / 100`)} ${scoreColor(label)}\n ${whaleLines[1]} ${bar}\n ${whaleLines[2]} ${chalk.dim("Docker Doctor (https://
|
|
133
|
+
process.stdout.write(` ${whaleLines[0]} ${scoreColor(`${currentScore} / 100`)} ${scoreColor(label)}\n ${whaleLines[1]} ${bar}\n ${whaleLines[2]} ${chalk.dim("Docker Doctor (https://docker-doctor.vercel.app)")}\n ${whaleLines[3]}\n`);
|
|
134
134
|
if (frame < frameCount) await setTimeout(frameDelay);
|
|
135
135
|
}
|
|
136
136
|
} finally {
|
|
@@ -142,7 +142,7 @@ const printScoreBox = async (score, label, categoryIssueCounts) => {
|
|
|
142
142
|
const bar = scoreColor("█".repeat(filledBlocks)) + chalk.dim("░".repeat(emptyBlocks));
|
|
143
143
|
console.log(`\n ${whaleLines[0]} ${scoreColor(`${score} / 100`)} ${scoreColor(label)}`);
|
|
144
144
|
console.log(` ${whaleLines[1]} ${bar}`);
|
|
145
|
-
console.log(` ${whaleLines[2]} ${chalk.dim("Docker Doctor (https://
|
|
145
|
+
console.log(` ${whaleLines[2]} ${chalk.dim("Docker Doctor (https://docker-doctor.vercel.app)")}`);
|
|
146
146
|
console.log(` ${whaleLines[3]}`);
|
|
147
147
|
}
|
|
148
148
|
console.log(`\n ${chalk.dim("────────────────────────────────────────────────────────────")}\n`);
|
|
@@ -167,23 +167,125 @@ const formatTerminal = async (diagnostics, score, label, project, verbose = fals
|
|
|
167
167
|
Performance: 0,
|
|
168
168
|
Security: 0
|
|
169
169
|
};
|
|
170
|
+
let warningsCount = 0;
|
|
171
|
+
let errorsCount = 0;
|
|
170
172
|
for (const d of diagnostics) {
|
|
171
173
|
const category = findRule(d.rule)?.category || "Best Practices";
|
|
172
174
|
categoryIssueCounts[category] = (categoryIssueCounts[category] || 0) + 1;
|
|
175
|
+
if (d.severity === "warning") warningsCount += 1;
|
|
176
|
+
else if (d.severity === "error") errorsCount += 1;
|
|
173
177
|
}
|
|
174
178
|
printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts);
|
|
175
|
-
await printScoreBox(score, label, categoryIssueCounts);
|
|
179
|
+
await printScoreBox(score, label, categoryIssueCounts, warningsCount, errorsCount);
|
|
176
180
|
};
|
|
177
181
|
|
|
178
182
|
//#endregion
|
|
179
183
|
//#region src/cli.ts
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
const askConfirm = (question, defaultYes = false) => {
|
|
185
|
+
if (!process.stdin.isTTY) return Promise.resolve(defaultYes);
|
|
186
|
+
return new Promise((resolve) => {
|
|
187
|
+
let value = defaultYes;
|
|
188
|
+
readline.emitKeypressEvents(process.stdin);
|
|
189
|
+
process.stdin.setRawMode(true);
|
|
190
|
+
process.stdin.resume();
|
|
191
|
+
process.stdout.write("\x1B[?25l");
|
|
192
|
+
const render = (firstTime = false) => {
|
|
193
|
+
if (!firstTime) process.stdout.write("\x1B[3A\r");
|
|
194
|
+
process.stdout.write(`\r\u001B[K ${chalk.green("✔")} ${chalk.bold(question)}\n`);
|
|
195
|
+
const yesPrefix = value ? chalk.cyan("❯ ") : " ";
|
|
196
|
+
const yesText = value ? chalk.cyan.bold("Yes") : chalk.dim("Yes");
|
|
197
|
+
process.stdout.write(`\r\u001B[K${yesPrefix}${yesText}\n`);
|
|
198
|
+
const noPrefix = value ? " " : chalk.cyan("❯ ");
|
|
199
|
+
const noText = value ? chalk.dim("No") : chalk.cyan.bold("No");
|
|
200
|
+
process.stdout.write(`\r\u001B[K${noPrefix}${noText}\n`);
|
|
201
|
+
};
|
|
202
|
+
render(true);
|
|
203
|
+
const handleKeypress = (str, key) => {
|
|
204
|
+
const cleanup = () => {
|
|
205
|
+
process.stdin.removeListener("keypress", handleKeypress);
|
|
206
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
207
|
+
process.stdin.pause();
|
|
208
|
+
process.stdout.write("\x1B[?25h");
|
|
209
|
+
};
|
|
210
|
+
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") {
|
|
211
|
+
value = !value;
|
|
212
|
+
render();
|
|
213
|
+
} else if (str === "y" || str === "Y") {
|
|
214
|
+
value = true;
|
|
215
|
+
render();
|
|
216
|
+
} else if (str === "n" || str === "N") {
|
|
217
|
+
value = false;
|
|
218
|
+
render();
|
|
219
|
+
} else if (key.name === "return" || key.name === "enter" || str === "\r" || str === "\n") {
|
|
220
|
+
cleanup();
|
|
221
|
+
process.stdout.write("\x1B[3A\r\x1B[K");
|
|
222
|
+
process.stdout.write(` ${chalk.green("✔")} ${chalk.bold(question)} › ${value ? chalk.cyan("Yes") : chalk.dim("No")}\n`);
|
|
223
|
+
process.stdout.write("\r\x1B[K\n");
|
|
224
|
+
process.stdout.write("\r\x1B[K\n");
|
|
225
|
+
process.stdout.write("\x1B[2A");
|
|
226
|
+
resolve(value);
|
|
227
|
+
} else if (key.ctrl && key.name === "c") {
|
|
228
|
+
cleanup();
|
|
229
|
+
process.stdout.write("\n");
|
|
230
|
+
process.exit(130);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
process.stdin.on("keypress", handleKeypress);
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
const askSelect = (question, options, defaultIndex = 0) => {
|
|
237
|
+
if (!process.stdin.isTTY) return Promise.resolve(defaultIndex);
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
let index = defaultIndex;
|
|
240
|
+
readline.emitKeypressEvents(process.stdin);
|
|
241
|
+
process.stdin.setRawMode(true);
|
|
242
|
+
process.stdin.resume();
|
|
243
|
+
process.stdout.write("\x1B[?25l");
|
|
244
|
+
const render = (firstTime = false) => {
|
|
245
|
+
if (!firstTime) process.stdout.write(`\u001B[${options.length + 1}A\r`);
|
|
246
|
+
process.stdout.write(`\r\u001B[K ${chalk.green("✔")} ${chalk.bold(question)}\n`);
|
|
247
|
+
let i = 0;
|
|
248
|
+
for (const option of options) {
|
|
249
|
+
const isSelected = i === index;
|
|
250
|
+
const prefix = isSelected ? chalk.cyan("❯ ") : " ";
|
|
251
|
+
const text = isSelected ? chalk.cyan.bold(option) : chalk.dim(option);
|
|
252
|
+
process.stdout.write(`\r\u001B[K${prefix}${text}\n`);
|
|
253
|
+
i += 1;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
render(true);
|
|
257
|
+
const handleKeypress = (str, key) => {
|
|
258
|
+
const cleanup = () => {
|
|
259
|
+
process.stdin.removeListener("keypress", handleKeypress);
|
|
260
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
261
|
+
process.stdin.pause();
|
|
262
|
+
process.stdout.write("\x1B[?25h");
|
|
263
|
+
};
|
|
264
|
+
if (key.name === "up" || key.name === "k") {
|
|
265
|
+
index = (index - 1 + options.length) % options.length;
|
|
266
|
+
render();
|
|
267
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
268
|
+
index = (index + 1) % options.length;
|
|
269
|
+
render();
|
|
270
|
+
} else if (key.name === "return" || key.name === "enter" || str === "\r" || str === "\n") {
|
|
271
|
+
cleanup();
|
|
272
|
+
process.stdout.write(`\u001B[${options.length + 1}A\r\u001B[K`);
|
|
273
|
+
process.stdout.write(` ${chalk.green("✔")} ${chalk.bold(question)} › ${chalk.cyan(options[index])}\n`);
|
|
274
|
+
for (const _ of options) process.stdout.write("\r\x1B[K\n");
|
|
275
|
+
process.stdout.write(`\u001B[${options.length}A`);
|
|
276
|
+
resolve(index);
|
|
277
|
+
} else if (key.ctrl && key.name === "c") {
|
|
278
|
+
cleanup();
|
|
279
|
+
process.stdout.write("\n");
|
|
280
|
+
process.exit(130);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
process.stdin.on("keypress", handleKeypress);
|
|
184
284
|
});
|
|
285
|
+
};
|
|
286
|
+
const runInteractiveWizard = async () => {
|
|
185
287
|
try {
|
|
186
|
-
if (
|
|
288
|
+
if (await askConfirm("Add Docker Doctor to GitHub Actions?")) {
|
|
187
289
|
const workflowDir = path.resolve(".github/workflows");
|
|
188
290
|
await fs.mkdir(workflowDir, { recursive: true });
|
|
189
291
|
const workflowPath = path.join(workflowDir, "docker-doctor.yml");
|
|
@@ -208,13 +310,11 @@ jobs:
|
|
|
208
310
|
console.log(`\n ${chalk.green("✨")} Created ${chalk.cyan(".github/workflows/docker-doctor.yml")}!`);
|
|
209
311
|
console.log(` Scan every pull request to prevent new Docker issues while you fix the backlog.`);
|
|
210
312
|
}
|
|
211
|
-
if (
|
|
313
|
+
if (await askSelect("What would you like to do next?", ["View rules list", "Skip"]) === 0) {
|
|
212
314
|
console.log(`\n ${chalk.bold("Available Rules:")}`);
|
|
213
315
|
for (const r of allRules) console.log(` - ${chalk.cyan(r.key)}: ${r.message} (${chalk.dim(r.category)})`);
|
|
214
316
|
}
|
|
215
|
-
} catch {}
|
|
216
|
-
rl.close();
|
|
217
|
-
}
|
|
317
|
+
} catch {}
|
|
218
318
|
};
|
|
219
319
|
const runRulesEngine = async (rootDir, project, rulesConfig, projectFilesList, fileContents, options, setStatus) => {
|
|
220
320
|
const diagnostics = [];
|
|
@@ -255,13 +355,35 @@ const runRulesEngine = async (rootDir, project, rulesConfig, projectFilesList, f
|
|
|
255
355
|
for (const diags of composeResults) diagnostics.push(...diags);
|
|
256
356
|
return diagnostics;
|
|
257
357
|
};
|
|
358
|
+
const filterDiagnostics = (diagnostics, categories) => {
|
|
359
|
+
if (!categories) return diagnostics;
|
|
360
|
+
return diagnostics.filter((d) => {
|
|
361
|
+
const ruleDef = findRule(d.rule);
|
|
362
|
+
if (ruleDef) {
|
|
363
|
+
if (categories[ruleDef.category] === "off") return false;
|
|
364
|
+
}
|
|
365
|
+
return true;
|
|
366
|
+
});
|
|
367
|
+
};
|
|
258
368
|
const program = new Command();
|
|
259
369
|
program.name("docker-doctor").description("Static analysis for Dockerfile and Docker Compose files").version(package_default.version, "-V, --version", "display the version number");
|
|
260
370
|
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) => {
|
|
371
|
+
const isSilent = options.score || options.json;
|
|
372
|
+
const restoreCursor = () => {
|
|
373
|
+
if (process.stdout.isTTY && !isSilent) process.stdout.write("\x1B[?25h");
|
|
374
|
+
};
|
|
375
|
+
process.on("exit", restoreCursor);
|
|
376
|
+
process.once("SIGINT", () => {
|
|
377
|
+
restoreCursor();
|
|
378
|
+
process.exit(130);
|
|
379
|
+
});
|
|
380
|
+
process.once("SIGTERM", () => {
|
|
381
|
+
restoreCursor();
|
|
382
|
+
process.exit(143);
|
|
383
|
+
});
|
|
261
384
|
try {
|
|
262
385
|
const rootDir = path.resolve(dir);
|
|
263
386
|
const startTime = Date.now();
|
|
264
|
-
const isSilent = options.score || options.json;
|
|
265
387
|
let statusText = "Discovering workspace...";
|
|
266
388
|
const spinnerFrames = [
|
|
267
389
|
"⠋",
|
|
@@ -281,9 +403,10 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
281
403
|
statusText = text;
|
|
282
404
|
};
|
|
283
405
|
if (process.stdout.isTTY && !isSilent) {
|
|
406
|
+
process.stdout.write("\x1B[?25l");
|
|
284
407
|
process.stdout.write(`${chalk.cyan(spinnerFrames[0])} ${statusText}`);
|
|
285
408
|
spinnerInterval = setInterval(() => {
|
|
286
|
-
process.stdout.write(`\r${chalk.cyan(spinnerFrames[frameIndex])} ${statusText}`);
|
|
409
|
+
process.stdout.write(`\r\u001B[K${chalk.cyan(spinnerFrames[frameIndex])} ${statusText}`);
|
|
287
410
|
frameIndex = (frameIndex + 1) % spinnerFrames.length;
|
|
288
411
|
}, 80);
|
|
289
412
|
}
|
|
@@ -294,23 +417,20 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
294
417
|
setStatus("Scanning workspace files...");
|
|
295
418
|
const project = await discoverProject(rootDir);
|
|
296
419
|
const fileContents = {};
|
|
297
|
-
const projectFilesList = [
|
|
420
|
+
const projectFilesList = [
|
|
421
|
+
...project.dockerfiles,
|
|
422
|
+
...project.composeFiles,
|
|
423
|
+
...project.dockerignores || []
|
|
424
|
+
];
|
|
298
425
|
const diagnostics = await runRulesEngine(rootDir, project, config.rules, projectFilesList, fileContents, options, setStatus);
|
|
299
|
-
|
|
300
|
-
if (config.categories) filteredDiagnostics = diagnostics.filter((d) => {
|
|
301
|
-
const ruleDef = findRule(d.rule);
|
|
302
|
-
if (ruleDef) {
|
|
303
|
-
if (config.categories?.[ruleDef.category] === "off") return false;
|
|
304
|
-
}
|
|
305
|
-
return true;
|
|
306
|
-
});
|
|
426
|
+
const filteredDiagnostics = filterDiagnostics(diagnostics, config.categories);
|
|
307
427
|
const { score, label } = calculateScore(filteredDiagnostics);
|
|
308
428
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
309
429
|
const concurrency = os.cpus().length;
|
|
310
430
|
if (spinnerInterval !== null) {
|
|
311
431
|
clearInterval(spinnerInterval);
|
|
312
432
|
spinnerInterval = null;
|
|
313
|
-
process.stdout.write("\r\x1B[K");
|
|
433
|
+
process.stdout.write("\r\x1B[K\x1B[?25h");
|
|
314
434
|
}
|
|
315
435
|
if (process.stdout.isTTY && !isSilent) console.log(`${chalk.green("✔")} Scanned ${projectFilesList.length} files in ${duration}s [~${concurrency} workers]`);
|
|
316
436
|
if (options.score) {
|
|
@@ -324,13 +444,13 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
324
444
|
} else {
|
|
325
445
|
await formatTerminal(filteredDiagnostics, score, label, project, options.verbose, fileContents);
|
|
326
446
|
const hasErrors = filteredDiagnostics.some((d) => d.severity === "error");
|
|
327
|
-
if (process.stdout.isTTY) await runInteractiveWizard();
|
|
447
|
+
if (process.stdout.isTTY && process.stdin.isTTY) await runInteractiveWizard();
|
|
328
448
|
process.exit(hasErrors ? 1 : 0);
|
|
329
449
|
}
|
|
330
450
|
} finally {
|
|
331
451
|
if (spinnerInterval !== null) {
|
|
332
452
|
clearInterval(spinnerInterval);
|
|
333
|
-
process.stdout.write("\r\x1B[K");
|
|
453
|
+
process.stdout.write("\r\x1B[K\x1B[?25h");
|
|
334
454
|
}
|
|
335
455
|
}
|
|
336
456
|
} catch (error) {
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":["border","packageJson"],"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,MAAM,KAAK,aAAa,EAAE,KAAK,MAAM,MAAM,OAAO,GAC/E;OAEA,QAAQ,IAAI,OAAO,MAAM,IAAI,aAAa,EAAE,KAAK,MAAM,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,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,MAAM,IAAI,MAAM,GAC9H;CACA,QAAQ,IACN,uBAAuB,QAAQ,aAAa,SAAS,QAAQ,aAAa,KAAK,MAAM,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,MAAM,IAAI,MAAM,GACnI;AACF;AAEA,MAAM,oBACJ,aACA,SACA,cACA,wBACS;CACT,IAAI,YAAY,WAAW,GAAG;EAC5B,QAAQ,IACN,KAAK,MAAM,MAAM,KAAK,qDAAqD,GAC7E;EACA;CACF;CAEA,IAAI,CAAC,SAAS;EACZ,QAAQ,IAAI,WAAW,MAAM,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,MAAM,IAAI,UAAU,GAAG;EACnD;EAEA,QAAQ,IACN,WAAW,MAAM,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,MAAM,OAAO,mDAAmD,GACvE;GACA,KAAK,MAAM,CAAC,MAAM,UAAU,gBAC1B,QAAQ,IAAI,OAAO,MAAM,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,MAAM,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,MAAM,UAAU,KAAK,IAAI,GAAG;EAC7C,KAAK,MAAM,KAAK,WAAW;GACzB,IAAI,WAAW,MAAM;GACrB,IAAI,SAAS;GACb,IAAI,EAAE,aAAa,SAAS;IAC1B,WAAW,MAAM,IAAI;IACrB,SAAS;GACX,OAAO,IAAI,EAAE,aAAa,WAAW;IACnC,WAAW,MAAM;IACjB,SAAS;GACX;GAEA,MAAM,WAAW,EAAE,OAAO,IAAI,EAAE,SAAS;GACzC,QAAQ,IAAI,KAAK,SAAS,MAAM,EAAE,IAAI,MAAM,IAAI,EAAE,IAAI,EAAE,GAAG,UAAU;GAGrE,MAAM,UAAU,aAAa;GAC7B,eAAe,SAAS,EAAE,MAAM,QAAQ;GAExC,QAAQ,IAAI,OAAO,MAAM,MAAM,EAAE,OAAO,GAAG;GAC3C,QAAQ,IAAI,OAAO,MAAM,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,MAAM,KAAK,YAAU;CAC/B,OAAO,IAAI,SAAS,IAAI;EACtB,OAAO;EACP,QAAQ,MAAM,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,MAAM,IAAI;CAC3B,IAAI,SAAS,IACX,aAAa,MAAM,MAAM;MACpB,IAAI,SAAS,IAClB,aAAa,MAAM,OAAO;MACrB,IAAI,SAAS,IAClB,aAAa,MAAM,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,MAAM,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,MAAM,IAAI,4DAA4D,EAAE,MAC1F,WAAW,GAAG,GACvB;IAEA,IAAI,QAAQ,YAEV,MAAM,WAAW,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,MAAM,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,MAAM,IAAI,4DAA4D,GAC/F;EACA,QAAQ,IAAI,KAAK,WAAW,IAAI;CAClC;CAEA,QAAQ,IACN,OAAO,MAAM,IAAI,8DAA8D,EAAE,GACnF;CACA,QAAQ,IAAI,YAAY,MAAM,KAAK,QAAQ,GAAG;CAC9C,QAAQ,IAAI,wCAAwC;CACpD,QAAQ,IACN,WAAW,MAAM,KAAK,iDAAiD,GACzE;CACA,QAAQ,IAAI,yDAAyD;CACrE,QAAQ,IAAI,0CAA0C;CACtD,QAAQ,IACN,aAAa,MAAM,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,MAAM,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,WADU,SAAS,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,KAAK,SAAS,gBAAgB;EAAE;EAAO;CAAO,CAAC;CACrD,IAAI;EAMF,KAAI,MALmB,GAAG,SACxB,OAAO,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,KAAK,sCAAsC,EAAE,oGAGhF,EACY,CAAC,KAAK,CAAC,CAAC,YAAY,MAAM,KAAK;GACzC,MAAM,cAAc,KAAK,QAAQ,mBAAmB;GACpD,MAAM,GAAG,MAAM,aAAa,EAAE,WAAW,KAAK,CAAC;GAC/C,MAAM,eAAe,KAAK,KAAK,aAAa,mBAAmB;GAmB/D,MAAM,GAAG,UAAU,cAAc;;;;;;;;;;;;;;;;;GAAc,OAAO;GACtD,QAAQ,IACN,OAAO,MAAM,MAAM,GAAG,EAAE,WAAW,MAAM,KAAK,qCAAqC,EAAE,EACvF;GACA,QAAQ,IACN,qFACF;EACF;EAQA,KAAI,MANqB,GAAG,SAC1B,OAAO,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,KAAK,iCAAiC,EAAE,8CAI3E,EACc,CAAC,KAAK,MAAM,KAAK;GAC7B,QAAQ,IAAI,OAAO,MAAM,KAAK,kBAAkB,GAAG;GACnD,KAAK,MAAM,KAAK,UACd,QAAQ,IACN,SAAS,MAAM,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,IAAI,MAAM,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,MAAM,WAAW,GAAG;CACtB;CAGA,MAAM,oBAAoB,MAAM,QAAQ,IACtC,QAAQ,YAAY,IAAI,OAAO,OAAO;EACpC,MAAM,WAAW,KAAK,KAAK,SAAS,EAAE;EACtC,IAAI;GACF,MAAM,UAAU,MAAM,GAAG,SAAS,UAAU,OAAO;GACnD,aAAa,MAAM;GAEnB,OAAO,mBADc,gBAAgB,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,MAAM,WAAW,GAAG;CACtB;CAGA,MAAM,iBAAiB,MAAM,QAAQ,IACnC,QAAQ,aAAa,IAAI,OAAO,OAAO;EACrC,MAAM,WAAW,KAAK,KAAK,SAAS,EAAE;EACtC,IAAI;GACF,MAAM,UAAU,MAAM,GAAG,SAAS,UAAU,OAAO;GACnD,aAAa,MAAM;GAEnB,OAAO,gBADY,aAAa,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,IAAI,QAAQ;AAE5B,QACG,KAAK,eAAe,CAAC,CACrB,YAAY,yDAAyD,CAAC,CACtE,QAAQC,gBAAY,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,UAAU,KAAK,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,MAAM,KAAK,cAAc,EAAE,EAAE,GAAG,YAAY;GACpE,kBAAkB,kBAAkB;IAClC,QAAQ,OAAO,MACb,KAAK,MAAM,KAAK,cAAc,WAAW,EAAE,GAAG,YAChD;IACA,cAAc,aAAa,KAAK,cAAc;GAChD,GAAG,EAAE;EACP;EAEA,IAAI;GACF,IAAI,QAAQ,OAAO,SAAS,CAAC,UAC3B,MAAM,WAAW,GAAG;GAGtB,UAAU,0BAA0B;GAEpC,MAAM,SAAS,MAAM,WAAW,SAAS,QAAQ,MAAM;GAEvD,UAAU,6BAA6B;GAEvC,MAAM,UAAU,MAAM,gBAAgB,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,UAAU,SAAS,EAAE,IAAI;IAC/B,IAAI,SAEF;SADoB,OAAO,aAAa,QAAQ,cAC5B,OAClB,OAAO;IACT;IAEF,OAAO;GACT,CAAC;GAIH,MAAM,EAAE,OAAO,UAAU,eAAe,mBAAmB;GAE3D,MAAM,aAAa,KAAK,IAAI,IAAI,aAAa,IAAI,CAAE,QAAQ,CAAC;GAC5D,MAAM,cAAc,GAAG,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,MAAM,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,SAAS,aACb,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,QAAQ,UAAU;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,OAAO,SAAS,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.mjs","names":["border","packageJson"],"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 warningsCount: number,\n errorsCount: 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 shareUrl = `https://docker-doctor.vercel.app/share?s=${score}&w=${warningsCount}&e=${errorsCount}`;\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://docker-doctor.vercel.app)\")}\\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://docker-doctor.vercel.app)\")}`\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 let warningsCount = 0;\n let errorsCount = 0;\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 if (d.severity === \"warning\") {\n warningsCount += 1;\n } else if (d.severity === \"error\") {\n errorsCount += 1;\n }\n }\n\n printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts);\n\n await printScoreBox(\n score,\n label,\n categoryIssueCounts,\n warningsCount,\n errorsCount\n );\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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"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
3
|
-
const require_src = require('./src-
|
|
3
|
+
const require_src = require('./src-O8FXMq8O.cjs');
|
|
4
4
|
|
|
5
5
|
//#region src/index.ts
|
|
6
6
|
const { version } = require_src.package_default;
|
package/dist/index.d.cts
CHANGED
|
@@ -19,6 +19,7 @@ interface Diagnostic {
|
|
|
19
19
|
interface ProjectInfo {
|
|
20
20
|
dockerfiles: string[];
|
|
21
21
|
composeFiles: string[];
|
|
22
|
+
dockerignores?: string[];
|
|
22
23
|
}
|
|
23
24
|
type RuleCategory = "Security" | "Performance" | "Best Practices" | "Compose" | "Image Size";
|
|
24
25
|
type RuleSeverity = "error" | "warning" | "info" | "off";
|
package/dist/index.d.mts
CHANGED
|
@@ -19,6 +19,7 @@ interface Diagnostic {
|
|
|
19
19
|
interface ProjectInfo {
|
|
20
20
|
dockerfiles: string[];
|
|
21
21
|
composeFiles: string[];
|
|
22
|
+
dockerignores?: string[];
|
|
22
23
|
}
|
|
23
24
|
type RuleCategory = "Security" | "Performance" | "Best Practices" | "Compose" | "Image Size";
|
|
24
25
|
type RuleSeverity = "error" | "warning" | "info" | "off";
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as runDockerfileRules, c as parseCompose, d as package_default, i as runComposeRules, l as parseDockerfile, n as calculateScore, o as allRules, r as loadConfig, s as findRule, t as toJsonReport, u as discoverProject } from "./src-
|
|
2
|
+
import { a as runDockerfileRules, c as parseCompose, d as package_default, i as runComposeRules, l as parseDockerfile, n as calculateScore, o as allRules, r as loadConfig, s as findRule, t as toJsonReport, u as discoverProject } from "./src-DvNFg6Nz.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/index.ts
|
|
5
5
|
const { version } = package_default;
|
|
@@ -8,7 +8,7 @@ import * as Schema from "effect/Schema";
|
|
|
8
8
|
//#region package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "@docker-doctor/cli",
|
|
11
|
-
version: "0.1
|
|
11
|
+
version: "0.2.1",
|
|
12
12
|
description: "Static analysis for Dockerfile and Docker Compose files",
|
|
13
13
|
keywords: [
|
|
14
14
|
"best-practices",
|
|
@@ -29,10 +29,10 @@ var package_default = {
|
|
|
29
29
|
},
|
|
30
30
|
repository: {
|
|
31
31
|
"type": "git",
|
|
32
|
-
"url": "https://github.com/PunGrumpy/docker-doctor.git",
|
|
32
|
+
"url": "git+https://github.com/PunGrumpy/docker-doctor.git",
|
|
33
33
|
"directory": "packages/docker-doctor"
|
|
34
34
|
},
|
|
35
|
-
bin: { "docker-doctor": "
|
|
35
|
+
bin: { "docker-doctor": "dist/cli.mjs" },
|
|
36
36
|
files: ["dist", "LICENSE"],
|
|
37
37
|
type: "module",
|
|
38
38
|
sideEffects: false,
|
|
@@ -87,14 +87,17 @@ const discoverProject = async (rootDir) => {
|
|
|
87
87
|
const allFiles = await walk(rootDir);
|
|
88
88
|
const dockerfiles = [];
|
|
89
89
|
const composeFiles = [];
|
|
90
|
+
const dockerignores = [];
|
|
90
91
|
for (const file of allFiles) {
|
|
91
92
|
const base = path.basename(file).toLowerCase();
|
|
93
|
+
if (base === ".dockerignore") dockerignores.push(path.relative(rootDir, file));
|
|
92
94
|
if (base === "dockerfile" || base.startsWith("dockerfile.") || base.endsWith(".dockerfile")) dockerfiles.push(path.relative(rootDir, file));
|
|
93
95
|
if (base === "docker-compose.yml" || base === "docker-compose.yaml" || base === "compose.yml" || base === "compose.yaml" || (base.startsWith("docker-compose.") || base.startsWith("compose.")) && (base.endsWith(".yml") || base.endsWith(".yaml"))) composeFiles.push(path.relative(rootDir, file));
|
|
94
96
|
}
|
|
95
97
|
return {
|
|
96
98
|
composeFiles,
|
|
97
|
-
dockerfiles
|
|
99
|
+
dockerfiles,
|
|
100
|
+
dockerignores
|
|
98
101
|
};
|
|
99
102
|
};
|
|
100
103
|
|
|
@@ -460,8 +463,8 @@ const preferSlimBase = {
|
|
|
460
463
|
check(instructions, file) {
|
|
461
464
|
const diagnostics = [];
|
|
462
465
|
for (const inst of instructions) if (inst.instruction === "FROM") {
|
|
463
|
-
const
|
|
464
|
-
if (imagePart === "scratch") continue;
|
|
466
|
+
const imagePart = inst.args.split(/\s+/u).find((p) => !p.startsWith("--"));
|
|
467
|
+
if (!imagePart || imagePart === "scratch") continue;
|
|
465
468
|
const colonIndex = imagePart.indexOf(":");
|
|
466
469
|
if (colonIndex !== -1) {
|
|
467
470
|
const tag = imagePart.slice(colonIndex + 1).toLowerCase();
|
|
@@ -555,7 +558,8 @@ const orderLayers = {
|
|
|
555
558
|
let copyAllLine = -1;
|
|
556
559
|
for (const inst of instructions) {
|
|
557
560
|
if (inst.instruction === "COPY" || inst.instruction === "ADD") {
|
|
558
|
-
const
|
|
561
|
+
const src = inst.args.split(/\s+/u).find((p) => !p.startsWith("--"));
|
|
562
|
+
if (!src) continue;
|
|
559
563
|
if ((src === "." || src === "./" || src === "*" || src.includes("src")) && copyAllLine === -1) copyAllLine = inst.line;
|
|
560
564
|
}
|
|
561
565
|
if (inst.instruction === "RUN" && copyAllLine !== -1) {
|
|
@@ -657,16 +661,25 @@ const noSecretsInEnv = {
|
|
|
657
661
|
/auth/iu
|
|
658
662
|
];
|
|
659
663
|
for (const inst of instructions) if (inst.instruction === "ENV" || inst.instruction === "ARG") {
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
664
|
+
const args = inst.args.trim();
|
|
665
|
+
if (inst.instruction === "ENV" && !args.includes("=")) {
|
|
666
|
+
const match = args.match(/^(?<key>[^\s]+)\s+(?<value>.*)$/u);
|
|
667
|
+
if (match?.groups) {
|
|
668
|
+
const { key, value } = match.groups;
|
|
669
|
+
if (secretKeywords.some((regex) => regex.test(key)) && value && !value.startsWith("$") && !value.startsWith("{")) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`, this.help, inst.line));
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
const parts = args.split(/\s+/u);
|
|
673
|
+
for (const part of parts) {
|
|
674
|
+
const eqIndex = part.indexOf("=");
|
|
675
|
+
let key = "";
|
|
676
|
+
let value = "";
|
|
677
|
+
if (eqIndex > 0) {
|
|
678
|
+
key = part.slice(0, eqIndex);
|
|
679
|
+
value = part.slice(eqIndex + 1);
|
|
680
|
+
} else key = part;
|
|
681
|
+
if (secretKeywords.some((regex) => regex.test(key)) && value && !value.startsWith("$") && !value.startsWith("{")) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`, this.help, inst.line));
|
|
682
|
+
}
|
|
670
683
|
}
|
|
671
684
|
}
|
|
672
685
|
return diagnostics;
|
|
@@ -681,8 +694,8 @@ const pinImageVersion = {
|
|
|
681
694
|
check(instructions, file) {
|
|
682
695
|
const diagnostics = [];
|
|
683
696
|
for (const inst of instructions) if (inst.instruction === "FROM") {
|
|
684
|
-
const
|
|
685
|
-
if (imagePart === "scratch") continue;
|
|
697
|
+
const imagePart = inst.args.split(/\s+/u).find((p) => !p.startsWith("--"));
|
|
698
|
+
if (!imagePart || imagePart === "scratch") continue;
|
|
686
699
|
const colonIndex = imagePart.indexOf(":");
|
|
687
700
|
const atIndex = imagePart.indexOf("@");
|
|
688
701
|
if (colonIndex === -1 && atIndex === -1) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Base image '${imagePart}' does not specify a tag. This makes builds non-deterministic.`, this.help, inst.line));
|
|
@@ -878,4 +891,4 @@ const toJsonReport = (diagnostics, score, label, project) => ({
|
|
|
878
891
|
|
|
879
892
|
//#endregion
|
|
880
893
|
export { runDockerfileRules as a, parseCompose as c, package_default as d, runComposeRules as i, parseDockerfile as l, calculateScore as n, allRules as o, loadConfig as r, findRule as s, toJsonReport as t, discoverProject as u };
|
|
881
|
-
//# sourceMappingURL=src-
|
|
894
|
+
//# sourceMappingURL=src-DvNFg6Nz.mjs.map
|