@docker-doctor/cli 0.5.3 → 0.6.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 CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  Your Dockerfiles are probably wrong. Docker Doctor finds out why.
16
16
 
17
- Docker Doctor is an opinionated static analysis tool for Dockerfile and Docker Compose files. It scans your project, runs 31 rules across security, performance, best practices, Compose, and image size — then gives you a health score and fix guidance.
17
+ Docker Doctor is an opinionated static analysis tool for Dockerfile and Docker Compose files. It scans your project, runs 33 rules across security, performance, best practices, Compose, and image size — then gives you a health score and fix guidance.
18
18
 
19
19
  Works with any project that uses Docker.
20
20
 
package/dist/cli.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_src = require('./src-D4rVJg36.cjs');
2
+ const require_src = require('./src-BpAv-H0Z.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");
@@ -587,6 +587,10 @@ const writeDiagnosticsDirectory = async (diagnostics, report, rootDir) => {
587
587
  ...d,
588
588
  file: sanitizePath(d.file),
589
589
  message: sanitizeMessage(d.message)
590
+ })),
591
+ failures: report.failures.map((failure) => ({
592
+ file: sanitizePath(failure.file),
593
+ message: sanitizeMessage(failure.message)
590
594
  }))
591
595
  };
592
596
  await node_fs_promises.default.writeFile(node_path.default.join(dir, "diagnostics.json"), JSON.stringify({
@@ -797,13 +801,24 @@ const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCount
797
801
  console.log(` ${cat} › ${chalk.dim(issueLabel)}`);
798
802
  }
799
803
  console.log(`\n Run ${chalk.cyan("docker-doctor --verbose")} to list every error and warning`);
800
- const ruleCounts = {};
801
- for (const d of diagnostics) ruleCounts[d.rule] = (ruleCounts[d.rule] || 0) + 1;
802
- const migrationRules = Object.entries(ruleCounts).filter(([, count]) => count >= 5);
804
+ const findingsByRule = /* @__PURE__ */ new Map();
805
+ for (const d of diagnostics) {
806
+ const findings = findingsByRule.get(d.rule) ?? {
807
+ count: 0,
808
+ files: /* @__PURE__ */ new Set()
809
+ };
810
+ findings.count += 1;
811
+ findings.files.add(d.file);
812
+ findingsByRule.set(d.rule, findings);
813
+ }
814
+ const migrationRules = [...findingsByRule].filter(([, { count }]) => count >= 5);
803
815
  if (migrationRules.length > 0) {
804
816
  console.log();
805
817
  console.log(` ${chalk.yellow("⚠ Migration-scale change: sample before you sweep")}`);
806
- for (const [rule, count] of migrationRules) console.log(` ${chalk.cyan(rule)} ×${count} across ${count} files`);
818
+ for (const [rule, { count, files }] of migrationRules) {
819
+ const fileWord = files.size === 1 ? "file" : "files";
820
+ console.log(` ${chalk.cyan(rule)} ×${count} across ${files.size} ${fileWord}`);
821
+ }
807
822
  console.log(` Fixing all of them at once is hard to review and prone to`);
808
823
  console.log(` subtle mistakes across the whole repo. Fix a representative`);
809
824
  console.log(` few first and confirm the recipe holds. Then get the code`);
@@ -858,7 +873,7 @@ const getWhaleMascot = (score, border) => {
858
873
  ];
859
874
  };
860
875
  const easeOutCubic = (x) => 1 - (1 - x) ** 3;
861
- const printScoreBox = async (score, label, categoryIssueCounts, warningsCount, errorsCount) => {
876
+ const printScoreBox = async (score, label, warningsCount, errorsCount) => {
862
877
  const { isTTY } = process.stdout;
863
878
  const shouldAnimate = isTTY && !process.env.CI && !process.env.NO_ANIMATION && process.env.TERM !== "dumb" && process.env.NODE_ENV !== "test";
864
879
  let scoreColor = chalk.red.bold;
@@ -932,7 +947,7 @@ const formatTerminal = async (diagnostics, score, label, project, verbose = fals
932
947
  else if (d.severity === "error") errorsCount += 1;
933
948
  }
934
949
  printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts);
935
- await printScoreBox(score, label, categoryIssueCounts, warningsCount, errorsCount);
950
+ await printScoreBox(score, label, warningsCount, errorsCount);
936
951
  };
937
952
 
938
953
  //#endregion
@@ -958,11 +973,12 @@ permissions:
958
973
  contents: read
959
974
  pull-requests: write
960
975
  issues: write
976
+ statuses: write
961
977
  jobs:
962
978
  docker-doctor:
963
979
  runs-on: ubuntu-latest
964
980
  steps:
965
- - uses: actions/checkout@v5
981
+ - uses: actions/checkout@v7
966
982
  - uses: ${options.actionRef}
967
983
  `;
968
984
  await node_fs_promises.default.mkdir(workflowDir, { recursive: true });
@@ -1267,6 +1283,31 @@ const reportScanFailures = (failures) => {
1267
1283
  console.error(`Docker Doctor could not analyze ${failures.length} ${fileWord}; the score below covers only the files that were analyzed.`);
1268
1284
  for (const failure of failures) console.error(` - ${failure.file}: ${flattenMessage(failure.message)}`);
1269
1285
  };
1286
+ const pathExists = async (filePath) => {
1287
+ try {
1288
+ await node_fs_promises.default.access(filePath);
1289
+ return true;
1290
+ } catch {
1291
+ return false;
1292
+ }
1293
+ };
1294
+ const resolveConfigPath = async (configOption, rootDir) => {
1295
+ if (!configOption) return;
1296
+ const fromCwd = node_path.default.resolve(process.cwd(), configOption);
1297
+ if (await pathExists(fromCwd)) return fromCwd;
1298
+ const fromRoot = node_path.default.resolve(rootDir, configOption);
1299
+ if (fromRoot !== fromCwd && await pathExists(fromRoot)) {
1300
+ console.error(`Warning: --config ${configOption} was found relative to the scanned directory, not the current directory. Pass a path relative to the current directory or an absolute path.`);
1301
+ return fromRoot;
1302
+ }
1303
+ return fromCwd;
1304
+ };
1305
+ const findScanTargetProblem = async (rootDir) => {
1306
+ const rootStat = await node_fs_promises.default.stat(rootDir).catch(() => null);
1307
+ if (rootStat === null) return `Directory not found: ${rootDir}`;
1308
+ if (!rootStat.isDirectory()) return `${rootDir} is not a directory. Pass the directory that contains your Dockerfile or Compose file.`;
1309
+ return null;
1310
+ };
1270
1311
  const program = new commander.Command();
1271
1312
  program.name("docker-doctor").description("Static analysis for Dockerfile and Docker Compose files").version(require_src.version, "-V, --version", "display the version number");
1272
1313
  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) => {
@@ -1285,6 +1326,12 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1285
1326
  });
1286
1327
  try {
1287
1328
  const rootDir = node_path.default.resolve(dir);
1329
+ const scanTargetProblem = await findScanTargetProblem(rootDir);
1330
+ if (scanTargetProblem !== null) {
1331
+ console.error(`Error: ${scanTargetProblem}`);
1332
+ process.exitCode = 1;
1333
+ return;
1334
+ }
1288
1335
  const startTime = Date.now();
1289
1336
  let statusText = "Discovering workspace...";
1290
1337
  const spinnerFrames = [
@@ -1315,11 +1362,17 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1315
1362
  try {
1316
1363
  if (process.stdout.isTTY && !isSilent) await (0, node_timers_promises.setTimeout)(150);
1317
1364
  setStatus("Loading configuration...");
1318
- const config = await require_src.loadConfig(rootDir, options.config, (message) => {
1365
+ const configPath = await resolveConfigPath(options.config, rootDir);
1366
+ const config = await require_src.loadConfig(rootDir, configPath, (message) => {
1319
1367
  console.error(`Warning: ${message}`);
1320
1368
  });
1321
1369
  setStatus("Scanning workspace files...");
1322
- const project = await require_src.discoverProject(rootDir, { ignoreFiles: config.ignore?.files });
1370
+ const project = await require_src.discoverProject(rootDir, {
1371
+ ignoreFiles: config.ignore?.files,
1372
+ onSkippedDirectory: (skippedDir) => {
1373
+ console.error(`Warning: skipped unreadable directory ${skippedDir} (permission denied)`);
1374
+ }
1375
+ });
1323
1376
  const fileContents = {};
1324
1377
  const projectFilesList = [
1325
1378
  ...project.dockerfiles,
@@ -1344,7 +1397,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1344
1397
  process.exitCode = scanExitCode(scanIncomplete, hasErrors);
1345
1398
  return;
1346
1399
  } else if (options.json) {
1347
- const report = require_src.toJsonReport(diagnostics, score, label, project);
1400
+ const report = require_src.toJsonReport(diagnostics, score, label, project, failures);
1348
1401
  console.log(JSON.stringify(report, null, 2));
1349
1402
  process.exitCode = scanExitCode(scanIncomplete, hasErrors);
1350
1403
  return;
@@ -1353,7 +1406,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1353
1406
  let wizardOk = true;
1354
1407
  if (process.stdout.isTTY && process.stdin.isTTY) wizardOk = await runInteractiveWizard({
1355
1408
  diagnostics,
1356
- report: require_src.toJsonReport(diagnostics, score, label, project),
1409
+ report: require_src.toJsonReport(diagnostics, score, label, project, failures),
1357
1410
  rootDir
1358
1411
  });
1359
1412
  const exitCode = scanExitCode(scanIncomplete, hasErrors);
@@ -1441,7 +1494,7 @@ rules.command("list").description("list all available rules").action(() => {
1441
1494
  }
1442
1495
  });
1443
1496
  rules.command("explain <rule>").description("explain a specific rule in detail").action((ruleKey) => {
1444
- const rule = require_src.findRule(ruleKey);
1497
+ const rule = require_src.findRule(ruleKey) ?? require_src.findRule(`docker-doctor/${ruleKey}`);
1445
1498
  if (!rule) {
1446
1499
  console.error(`Rule '${ruleKey}' not found.`);
1447
1500
  process.exitCode = 1;