@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/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as runDockerfileRules, c as createComposeLocator, d as discoverProject, f as version, i as runComposeRules, l as parseCompose, n as calculateScore, o as allRules, r as loadConfig, s as findRule, t as toJsonReport, u as parseDockerfile } from "./src-CklEIXTX.mjs";
2
+ import { a as runDockerfileRules, c as createComposeLocator, d as discoverProject, f as version, i as runComposeRules, l as parseCompose, n as calculateScore, o as allRules, r as loadConfig, s as findRule, t as toJsonReport, u as parseDockerfile } from "./src-BLeXr_hR.mjs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import os from "node:os";
@@ -580,6 +580,10 @@ const writeDiagnosticsDirectory = async (diagnostics, report, rootDir) => {
580
580
  ...d,
581
581
  file: sanitizePath(d.file),
582
582
  message: sanitizeMessage(d.message)
583
+ })),
584
+ failures: report.failures.map((failure) => ({
585
+ file: sanitizePath(failure.file),
586
+ message: sanitizeMessage(failure.message)
583
587
  }))
584
588
  };
585
589
  await fs.writeFile(path.join(dir, "diagnostics.json"), JSON.stringify({
@@ -790,13 +794,24 @@ const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCount
790
794
  console.log(` ${cat} › ${chalk.dim(issueLabel)}`);
791
795
  }
792
796
  console.log(`\n Run ${chalk.cyan("docker-doctor --verbose")} to list every error and warning`);
793
- const ruleCounts = {};
794
- for (const d of diagnostics) ruleCounts[d.rule] = (ruleCounts[d.rule] || 0) + 1;
795
- const migrationRules = Object.entries(ruleCounts).filter(([, count]) => count >= 5);
797
+ const findingsByRule = /* @__PURE__ */ new Map();
798
+ for (const d of diagnostics) {
799
+ const findings = findingsByRule.get(d.rule) ?? {
800
+ count: 0,
801
+ files: /* @__PURE__ */ new Set()
802
+ };
803
+ findings.count += 1;
804
+ findings.files.add(d.file);
805
+ findingsByRule.set(d.rule, findings);
806
+ }
807
+ const migrationRules = [...findingsByRule].filter(([, { count }]) => count >= 5);
796
808
  if (migrationRules.length > 0) {
797
809
  console.log();
798
810
  console.log(` ${chalk.yellow("⚠ Migration-scale change: sample before you sweep")}`);
799
- for (const [rule, count] of migrationRules) console.log(` ${chalk.cyan(rule)} ×${count} across ${count} files`);
811
+ for (const [rule, { count, files }] of migrationRules) {
812
+ const fileWord = files.size === 1 ? "file" : "files";
813
+ console.log(` ${chalk.cyan(rule)} ×${count} across ${files.size} ${fileWord}`);
814
+ }
800
815
  console.log(` Fixing all of them at once is hard to review and prone to`);
801
816
  console.log(` subtle mistakes across the whole repo. Fix a representative`);
802
817
  console.log(` few first and confirm the recipe holds. Then get the code`);
@@ -851,7 +866,7 @@ const getWhaleMascot = (score, border) => {
851
866
  ];
852
867
  };
853
868
  const easeOutCubic = (x) => 1 - (1 - x) ** 3;
854
- const printScoreBox = async (score, label, categoryIssueCounts, warningsCount, errorsCount) => {
869
+ const printScoreBox = async (score, label, warningsCount, errorsCount) => {
855
870
  const { isTTY } = process.stdout;
856
871
  const shouldAnimate = isTTY && !process.env.CI && !process.env.NO_ANIMATION && process.env.TERM !== "dumb" && process.env.NODE_ENV !== "test";
857
872
  let scoreColor = chalk.red.bold;
@@ -925,7 +940,7 @@ const formatTerminal = async (diagnostics, score, label, project, verbose = fals
925
940
  else if (d.severity === "error") errorsCount += 1;
926
941
  }
927
942
  printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts);
928
- await printScoreBox(score, label, categoryIssueCounts, warningsCount, errorsCount);
943
+ await printScoreBox(score, label, warningsCount, errorsCount);
929
944
  };
930
945
 
931
946
  //#endregion
@@ -951,11 +966,12 @@ permissions:
951
966
  contents: read
952
967
  pull-requests: write
953
968
  issues: write
969
+ statuses: write
954
970
  jobs:
955
971
  docker-doctor:
956
972
  runs-on: ubuntu-latest
957
973
  steps:
958
- - uses: actions/checkout@v5
974
+ - uses: actions/checkout@v7
959
975
  - uses: ${options.actionRef}
960
976
  `;
961
977
  await fs.mkdir(workflowDir, { recursive: true });
@@ -1260,6 +1276,31 @@ const reportScanFailures = (failures) => {
1260
1276
  console.error(`Docker Doctor could not analyze ${failures.length} ${fileWord}; the score below covers only the files that were analyzed.`);
1261
1277
  for (const failure of failures) console.error(` - ${failure.file}: ${flattenMessage(failure.message)}`);
1262
1278
  };
1279
+ const pathExists = async (filePath) => {
1280
+ try {
1281
+ await fs.access(filePath);
1282
+ return true;
1283
+ } catch {
1284
+ return false;
1285
+ }
1286
+ };
1287
+ const resolveConfigPath = async (configOption, rootDir) => {
1288
+ if (!configOption) return;
1289
+ const fromCwd = path.resolve(process.cwd(), configOption);
1290
+ if (await pathExists(fromCwd)) return fromCwd;
1291
+ const fromRoot = path.resolve(rootDir, configOption);
1292
+ if (fromRoot !== fromCwd && await pathExists(fromRoot)) {
1293
+ 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.`);
1294
+ return fromRoot;
1295
+ }
1296
+ return fromCwd;
1297
+ };
1298
+ const findScanTargetProblem = async (rootDir) => {
1299
+ const rootStat = await fs.stat(rootDir).catch(() => null);
1300
+ if (rootStat === null) return `Directory not found: ${rootDir}`;
1301
+ if (!rootStat.isDirectory()) return `${rootDir} is not a directory. Pass the directory that contains your Dockerfile or Compose file.`;
1302
+ return null;
1303
+ };
1263
1304
  const program = new Command();
1264
1305
  program.name("docker-doctor").description("Static analysis for Dockerfile and Docker Compose files").version(version, "-V, --version", "display the version number");
1265
1306
  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) => {
@@ -1278,6 +1319,12 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1278
1319
  });
1279
1320
  try {
1280
1321
  const rootDir = path.resolve(dir);
1322
+ const scanTargetProblem = await findScanTargetProblem(rootDir);
1323
+ if (scanTargetProblem !== null) {
1324
+ console.error(`Error: ${scanTargetProblem}`);
1325
+ process.exitCode = 1;
1326
+ return;
1327
+ }
1281
1328
  const startTime = Date.now();
1282
1329
  let statusText = "Discovering workspace...";
1283
1330
  const spinnerFrames = [
@@ -1308,11 +1355,17 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1308
1355
  try {
1309
1356
  if (process.stdout.isTTY && !isSilent) await setTimeout(150);
1310
1357
  setStatus("Loading configuration...");
1311
- const config = await loadConfig(rootDir, options.config, (message) => {
1358
+ const configPath = await resolveConfigPath(options.config, rootDir);
1359
+ const config = await loadConfig(rootDir, configPath, (message) => {
1312
1360
  console.error(`Warning: ${message}`);
1313
1361
  });
1314
1362
  setStatus("Scanning workspace files...");
1315
- const project = await discoverProject(rootDir, { ignoreFiles: config.ignore?.files });
1363
+ const project = await discoverProject(rootDir, {
1364
+ ignoreFiles: config.ignore?.files,
1365
+ onSkippedDirectory: (skippedDir) => {
1366
+ console.error(`Warning: skipped unreadable directory ${skippedDir} (permission denied)`);
1367
+ }
1368
+ });
1316
1369
  const fileContents = {};
1317
1370
  const projectFilesList = [
1318
1371
  ...project.dockerfiles,
@@ -1337,7 +1390,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1337
1390
  process.exitCode = scanExitCode(scanIncomplete, hasErrors);
1338
1391
  return;
1339
1392
  } else if (options.json) {
1340
- const report = toJsonReport(diagnostics, score, label, project);
1393
+ const report = toJsonReport(diagnostics, score, label, project, failures);
1341
1394
  console.log(JSON.stringify(report, null, 2));
1342
1395
  process.exitCode = scanExitCode(scanIncomplete, hasErrors);
1343
1396
  return;
@@ -1346,7 +1399,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1346
1399
  let wizardOk = true;
1347
1400
  if (process.stdout.isTTY && process.stdin.isTTY) wizardOk = await runInteractiveWizard({
1348
1401
  diagnostics,
1349
- report: toJsonReport(diagnostics, score, label, project),
1402
+ report: toJsonReport(diagnostics, score, label, project, failures),
1350
1403
  rootDir
1351
1404
  });
1352
1405
  const exitCode = scanExitCode(scanIncomplete, hasErrors);
@@ -1434,7 +1487,7 @@ rules.command("list").description("list all available rules").action(() => {
1434
1487
  }
1435
1488
  });
1436
1489
  rules.command("explain <rule>").description("explain a specific rule in detail").action((ruleKey) => {
1437
- const rule = findRule(ruleKey);
1490
+ const rule = findRule(ruleKey) ?? findRule(`docker-doctor/${ruleKey}`);
1438
1491
  if (!rule) {
1439
1492
  console.error(`Rule '${ruleKey}' not found.`);
1440
1493
  process.exitCode = 1;