@docker-doctor/cli 0.6.0 → 0.7.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-BLeXr_hR.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-D2DWrv5B.mjs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import os from "node:os";
@@ -586,6 +586,7 @@ const writeDiagnosticsDirectory = async (diagnostics, report, rootDir) => {
586
586
  message: sanitizeMessage(failure.message)
587
587
  }))
588
588
  };
589
+ await fs.writeFile(path.join(dir, ".gitignore"), "*\n", "utf-8");
589
590
  await fs.writeFile(path.join(dir, "diagnostics.json"), JSON.stringify({
590
591
  note: TRUST_BOUNDARY_NOTE,
591
592
  ...sanitizedReport
@@ -656,6 +657,7 @@ const buildHandoffPayload = (input) => {
656
657
  const ruleWord = groups.length === 1 ? "rule" : "rules";
657
658
  const lines = [
658
659
  `Fix the ${issueCount} Docker Doctor ${issueWord} (${groups.length} ${ruleWord}) in ${sanitizePath(input.projectName)}.`,
660
+ `Every path below is relative to ${sanitizePath(input.projectDir)}. Work from that directory.`,
659
661
  TRUST_BOUNDARY_NOTE,
660
662
  ""
661
663
  ];
@@ -720,8 +722,11 @@ const detectLaunchableAgents = () => {
720
722
 
721
723
  //#endregion
722
724
  //#region src/agents/launch-agent.ts
723
- const launchAgent = (agentId, prompt) => new Promise((resolve) => {
724
- const child = spawn(AGENT_BINARIES[agentId], [...AGENT_AUTO_FLAGS[agentId], prompt], { stdio: "inherit" });
725
+ const launchAgent = (agentId, prompt, cwd) => new Promise((resolve) => {
726
+ const child = spawn(AGENT_BINARIES[agentId], [...AGENT_AUTO_FLAGS[agentId], prompt], {
727
+ cwd,
728
+ stdio: "inherit"
729
+ });
725
730
  child.once("error", () => {
726
731
  resolve(false);
727
732
  });
@@ -775,9 +780,9 @@ const printDiscoveredFiles = (project) => {
775
780
  console.log(` Dockerfile(s): ${project.dockerfiles.length ? project.dockerfiles.map((f) => chalk.cyan(f)).join(", ") : chalk.dim("None")}`);
776
781
  console.log(` Compose file(s): ${project.composeFiles.length ? project.composeFiles.map((f) => chalk.cyan(f)).join(", ") : chalk.dim("None")}`);
777
782
  };
778
- const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCounts) => {
783
+ const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCounts, unanalyzedCount) => {
779
784
  if (diagnostics.length === 0) {
780
- console.log(`\n${chalk.green.bold("✔ No issues found! Your Docker setup looks healthy.")}`);
785
+ console.log(unanalyzedCount === 0 ? `\n${chalk.green.bold("✔ No issues found! Your Docker setup looks healthy.")}` : `\n${chalk.yellow.bold("⚠ No issues found in the files that could be analyzed.")}`);
781
786
  return;
782
787
  }
783
788
  if (!verbose) {
@@ -866,7 +871,7 @@ const getWhaleMascot = (score, border) => {
866
871
  ];
867
872
  };
868
873
  const easeOutCubic = (x) => 1 - (1 - x) ** 3;
869
- const printScoreBox = async (score, label, warningsCount, errorsCount) => {
874
+ const printScoreBox = async (score, label, warningsCount, errorsCount, unanalyzedCount) => {
870
875
  const { isTTY } = process.stdout;
871
876
  const shouldAnimate = isTTY && !process.env.CI && !process.env.NO_ANIMATION && process.env.TERM !== "dumb" && process.env.NODE_ENV !== "test";
872
877
  let scoreColor = chalk.red.bold;
@@ -910,15 +915,21 @@ const printScoreBox = async (score, label, warningsCount, errorsCount) => {
910
915
  console.log(` ${whaleLines[3]}`);
911
916
  }
912
917
  console.log(`\n ${chalk.dim("────────────────────────────────────────────────────────────")}\n`);
913
- console.log(` Share: ${chalk.cyan(shareUrl)}`);
914
- console.log(` Tell others how you did on socials\n`);
918
+ if (unanalyzedCount === 0) {
919
+ console.log(` Share: ${chalk.cyan(shareUrl)}`);
920
+ console.log(` Tell others how you did on socials\n`);
921
+ } else {
922
+ const fileWord = unanalyzedCount === 1 ? "file" : "files";
923
+ console.log(` ${chalk.yellow(`⚠ Incomplete scan: ${unanalyzedCount} ${fileWord} could not be analyzed.`)}`);
924
+ console.log(` The score covers only the analyzed files.\n`);
925
+ }
915
926
  console.log(` Docs: ${chalk.cyan("https://docker-doctor.vercel.app")}`);
916
927
  console.log(` Learn more about fixing issues, setting up CI/CD, and`);
917
928
  console.log(` configuring rules with a config file\n`);
918
929
  console.log(` GitHub: ${chalk.cyan("https://github.com/PunGrumpy/docker-doctor")}`);
919
930
  console.log(` Report issues and star the repository!`);
920
931
  };
921
- const formatTerminal = async (diagnostics, score, label, project, verbose = false, fileContents = {}) => {
932
+ const formatTerminal = async (diagnostics, score, label, project, verbose = false, fileContents = {}, unanalyzedCount = 0) => {
922
933
  if (verbose) {
923
934
  console.log(`\n${chalk.bold("Docker Doctor Diagnostics")}`);
924
935
  console.log(`=================================`);
@@ -939,8 +950,8 @@ const formatTerminal = async (diagnostics, score, label, project, verbose = fals
939
950
  if (d.severity === "warning") warningsCount += 1;
940
951
  else if (d.severity === "error") errorsCount += 1;
941
952
  }
942
- printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts);
943
- await printScoreBox(score, label, warningsCount, errorsCount);
953
+ printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts, unanalyzedCount);
954
+ await printScoreBox(score, label, warningsCount, errorsCount, unanalyzedCount);
944
955
  };
945
956
 
946
957
  //#endregion
@@ -954,8 +965,25 @@ const fileExists = async (filePath) => {
954
965
  return false;
955
966
  }
956
967
  };
968
+ const selfAndAncestors = (dir) => {
969
+ const dirs = [dir];
970
+ let parent = path.dirname(dir);
971
+ while (parent !== dirs.at(-1)) {
972
+ dirs.push(parent);
973
+ parent = path.dirname(parent);
974
+ }
975
+ return dirs;
976
+ };
977
+ const findRepositoryRoot = async (dir) => {
978
+ const candidates = selfAndAncestors(dir);
979
+ const nearest = (await Promise.all(candidates.map((candidate) => fileExists(path.join(candidate, ".git"))))).indexOf(true);
980
+ return nearest === -1 ? null : candidates[nearest];
981
+ };
957
982
  const scaffoldActionWorkflow = async (options) => {
958
- const workflowDir = path.join(options.rootDir, ".github", "workflows");
983
+ const repositoryRoot = await findRepositoryRoot(options.rootDir) ?? options.rootDir;
984
+ const scanDirectory = path.relative(repositoryRoot, options.rootDir).split(path.sep).join("/");
985
+ const directoryInput = scanDirectory === "" ? "" : ` with:\n directory: ${JSON.stringify(scanDirectory)}\n`;
986
+ const workflowDir = path.join(repositoryRoot, ".github", "workflows");
959
987
  const workflowPath = path.join(workflowDir, "docker-doctor.yml");
960
988
  const existing = await fileExists(workflowPath);
961
989
  if (existing && !await options.confirmOverwrite()) return "kept";
@@ -973,7 +1001,7 @@ jobs:
973
1001
  steps:
974
1002
  - uses: actions/checkout@v7
975
1003
  - uses: ${options.actionRef}
976
- `;
1004
+ ${directoryInput}`;
977
1005
  await fs.mkdir(workflowDir, { recursive: true });
978
1006
  await fs.writeFile(workflowPath, workflowYaml, "utf-8");
979
1007
  return existing ? "updated" : "created";
@@ -1165,6 +1193,7 @@ const runAgentHandoff = async (context) => {
1165
1193
  await ensureGitignoreEntry(context.rootDir);
1166
1194
  const payload = buildHandoffPayload({
1167
1195
  diagnostics: context.diagnostics,
1196
+ projectDir: context.rootDir,
1168
1197
  projectName: path.basename(context.rootDir)
1169
1198
  });
1170
1199
  if (choice === clipboardIndex) {
@@ -1173,14 +1202,14 @@ const runAgentHandoff = async (context) => {
1173
1202
  return;
1174
1203
  }
1175
1204
  const agentId = launchable[choice];
1176
- if (!await askConfirm(`Launch ${AGENT_BINARIES[agentId]} with ${AGENT_AUTO_FLAGS[agentId].join(" ")}? It will edit files without asking for approval.`)) {
1205
+ if (!await askConfirm(`Launch ${AGENT_BINARIES[agentId]} with ${AGENT_AUTO_FLAGS[agentId].join(" ")} in ${context.rootDir}? It will edit files without asking for approval.`)) {
1177
1206
  printAgentPrompt(payload);
1178
1207
  return;
1179
1208
  }
1180
1209
  const installResult = await installSkillForAgents([agentId], { projectRoot: context.rootDir });
1181
1210
  if (installResult && installResult.installed.length > 0) console.log(`\n ${chalk.green("✔")} Installed the docker-doctor skill for ${agentDisplayName(agentId)}`);
1182
1211
  console.log(`\n Handing off to ${agentDisplayName(agentId)}...\n`);
1183
- if (!await launchAgent(agentId, payload)) {
1212
+ if (!await launchAgent(agentId, payload, context.rootDir)) {
1184
1213
  console.log(` ${chalk.yellow("⚠")} Couldn't launch ${AGENT_BINARIES[agentId]}. Here's the prompt instead:`);
1185
1214
  printAgentPrompt(payload);
1186
1215
  }
@@ -1276,6 +1305,13 @@ const reportScanFailures = (failures) => {
1276
1305
  console.error(`Docker Doctor could not analyze ${failures.length} ${fileWord}; the score below covers only the files that were analyzed.`);
1277
1306
  for (const failure of failures) console.error(` - ${failure.file}: ${flattenMessage(failure.message)}`);
1278
1307
  };
1308
+ const noFilesFoundMessage = (rootDir) => `No Dockerfiles or Compose files found in ${rootDir}`;
1309
+ const printNothingScanned = (rootDir) => {
1310
+ console.log(`\n${chalk.yellow("⚠")} ${noFilesFoundMessage(rootDir)}.`);
1311
+ console.log(` docker-doctor scans files named Dockerfile, Dockerfile.*, *.dockerfile,`);
1312
+ console.log(` compose.yaml, docker-compose.yaml and variants such as compose.prod.yaml`);
1313
+ console.log(` (.yaml or .yml). Check the directory and your ignore.files patterns.`);
1314
+ };
1279
1315
  const pathExists = async (filePath) => {
1280
1316
  try {
1281
1317
  await fs.access(filePath);
@@ -1305,8 +1341,9 @@ const program = new Command();
1305
1341
  program.name("docker-doctor").description("Static analysis for Dockerfile and Docker Compose files").version(version, "-V, --version", "display the version number");
1306
1342
  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) => {
1307
1343
  const isSilent = options.score || options.json;
1344
+ const showProgress = Boolean(process.stdout.isTTY) && !isSilent;
1308
1345
  const restoreCursor = () => {
1309
- if (process.stdout.isTTY && !isSilent) process.stdout.write("\x1B[?25h");
1346
+ if (showProgress) process.stdout.write("\x1B[?25h");
1310
1347
  };
1311
1348
  process.on("exit", restoreCursor);
1312
1349
  process.once("SIGINT", () => {
@@ -1344,7 +1381,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1344
1381
  const setStatus = (text) => {
1345
1382
  statusText = text;
1346
1383
  };
1347
- if (process.stdout.isTTY && !isSilent) {
1384
+ if (showProgress) {
1348
1385
  process.stdout.write("\x1B[?25l");
1349
1386
  process.stdout.write(`${chalk.cyan(spinnerFrames[0])} ${statusText}`);
1350
1387
  spinnerInterval = setInterval(() => {
@@ -1353,7 +1390,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1353
1390
  }, 80);
1354
1391
  }
1355
1392
  try {
1356
- if (process.stdout.isTTY && !isSilent) await setTimeout(150);
1393
+ if (showProgress) await setTimeout(150);
1357
1394
  setStatus("Loading configuration...");
1358
1395
  const configPath = await resolveConfigPath(options.config, rootDir);
1359
1396
  const config = await loadConfig(rootDir, configPath, (message) => {
@@ -1372,8 +1409,9 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1372
1409
  ...project.composeFiles,
1373
1410
  ...project.dockerignores || []
1374
1411
  ];
1412
+ const scannedFileCount = project.dockerfiles.length + project.composeFiles.length;
1375
1413
  const { diagnostics, failures } = await runRulesEngine(rootDir, project, config.rules, config.categories, projectFilesList, fileContents, options, setStatus);
1376
- const { score, label } = calculateScore(diagnostics);
1414
+ const { score, label } = calculateScore(diagnostics, scannedFileCount - failures.length);
1377
1415
  const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
1378
1416
  const concurrency = os.cpus().length;
1379
1417
  if (spinnerInterval !== null) {
@@ -1381,9 +1419,17 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1381
1419
  spinnerInterval = null;
1382
1420
  process.stdout.write("\r\x1B[K\x1B[?25h");
1383
1421
  }
1384
- if (process.stdout.isTTY && !isSilent) console.log(`${chalk.green("✔")} Scanned ${projectFilesList.length} files in ${duration}s [~${concurrency} workers]`);
1422
+ if (showProgress) console.log(`${chalk.green("✔")} Scanned ${scannedFileCount} files in ${duration}s [~${concurrency} workers]`);
1385
1423
  reportScanFailures(failures);
1386
1424
  const scanIncomplete = failures.length > 0;
1425
+ if (scannedFileCount === 0) {
1426
+ if (isSilent) console.error(`Warning: ${noFilesFoundMessage(rootDir)}`);
1427
+ else {
1428
+ printNothingScanned(rootDir);
1429
+ process.exitCode = 0;
1430
+ return;
1431
+ }
1432
+ }
1387
1433
  const hasErrors = diagnostics.some((d) => d.severity === "error");
1388
1434
  if (options.score) {
1389
1435
  console.log(score);
@@ -1395,7 +1441,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1395
1441
  process.exitCode = scanExitCode(scanIncomplete, hasErrors);
1396
1442
  return;
1397
1443
  }
1398
- await formatTerminal(diagnostics, score, label, project, options.verbose, fileContents);
1444
+ await formatTerminal(diagnostics, score, label, project, options.verbose, fileContents, failures.length);
1399
1445
  let wizardOk = true;
1400
1446
  if (process.stdout.isTTY && process.stdin.isTTY) wizardOk = await runInteractiveWizard({
1401
1447
  diagnostics,