@docker-doctor/cli 0.6.0 → 0.6.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/dist/cli.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_src = require('./src-BpAv-H0Z.cjs');
2
+ const require_src = require('./src-C7XK6Kp-.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");
@@ -593,6 +593,7 @@ const writeDiagnosticsDirectory = async (diagnostics, report, rootDir) => {
593
593
  message: sanitizeMessage(failure.message)
594
594
  }))
595
595
  };
596
+ await node_fs_promises.default.writeFile(node_path.default.join(dir, ".gitignore"), "*\n", "utf-8");
596
597
  await node_fs_promises.default.writeFile(node_path.default.join(dir, "diagnostics.json"), JSON.stringify({
597
598
  note: TRUST_BOUNDARY_NOTE,
598
599
  ...sanitizedReport
@@ -663,6 +664,7 @@ const buildHandoffPayload = (input) => {
663
664
  const ruleWord = groups.length === 1 ? "rule" : "rules";
664
665
  const lines = [
665
666
  `Fix the ${issueCount} Docker Doctor ${issueWord} (${groups.length} ${ruleWord}) in ${sanitizePath(input.projectName)}.`,
667
+ `Every path below is relative to ${sanitizePath(input.projectDir)}. Work from that directory.`,
666
668
  TRUST_BOUNDARY_NOTE,
667
669
  ""
668
670
  ];
@@ -727,8 +729,11 @@ const detectLaunchableAgents = () => {
727
729
 
728
730
  //#endregion
729
731
  //#region src/agents/launch-agent.ts
730
- const launchAgent = (agentId, prompt) => new Promise((resolve) => {
731
- const child = (0, node_child_process.spawn)(AGENT_BINARIES[agentId], [...AGENT_AUTO_FLAGS[agentId], prompt], { stdio: "inherit" });
732
+ const launchAgent = (agentId, prompt, cwd) => new Promise((resolve) => {
733
+ const child = (0, node_child_process.spawn)(AGENT_BINARIES[agentId], [...AGENT_AUTO_FLAGS[agentId], prompt], {
734
+ cwd,
735
+ stdio: "inherit"
736
+ });
732
737
  child.once("error", () => {
733
738
  resolve(false);
734
739
  });
@@ -782,9 +787,9 @@ const printDiscoveredFiles = (project) => {
782
787
  console.log(` Dockerfile(s): ${project.dockerfiles.length ? project.dockerfiles.map((f) => chalk.cyan(f)).join(", ") : chalk.dim("None")}`);
783
788
  console.log(` Compose file(s): ${project.composeFiles.length ? project.composeFiles.map((f) => chalk.cyan(f)).join(", ") : chalk.dim("None")}`);
784
789
  };
785
- const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCounts) => {
790
+ const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCounts, unanalyzedCount) => {
786
791
  if (diagnostics.length === 0) {
787
- console.log(`\n${chalk.green.bold("✔ No issues found! Your Docker setup looks healthy.")}`);
792
+ 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.")}`);
788
793
  return;
789
794
  }
790
795
  if (!verbose) {
@@ -873,7 +878,7 @@ const getWhaleMascot = (score, border) => {
873
878
  ];
874
879
  };
875
880
  const easeOutCubic = (x) => 1 - (1 - x) ** 3;
876
- const printScoreBox = async (score, label, warningsCount, errorsCount) => {
881
+ const printScoreBox = async (score, label, warningsCount, errorsCount, unanalyzedCount) => {
877
882
  const { isTTY } = process.stdout;
878
883
  const shouldAnimate = isTTY && !process.env.CI && !process.env.NO_ANIMATION && process.env.TERM !== "dumb" && process.env.NODE_ENV !== "test";
879
884
  let scoreColor = chalk.red.bold;
@@ -917,15 +922,21 @@ const printScoreBox = async (score, label, warningsCount, errorsCount) => {
917
922
  console.log(` ${whaleLines[3]}`);
918
923
  }
919
924
  console.log(`\n ${chalk.dim("────────────────────────────────────────────────────────────")}\n`);
920
- console.log(` Share: ${chalk.cyan(shareUrl)}`);
921
- console.log(` Tell others how you did on socials\n`);
925
+ if (unanalyzedCount === 0) {
926
+ console.log(` Share: ${chalk.cyan(shareUrl)}`);
927
+ console.log(` Tell others how you did on socials\n`);
928
+ } else {
929
+ const fileWord = unanalyzedCount === 1 ? "file" : "files";
930
+ console.log(` ${chalk.yellow(`⚠ Incomplete scan: ${unanalyzedCount} ${fileWord} could not be analyzed.`)}`);
931
+ console.log(` The score covers only the analyzed files.\n`);
932
+ }
922
933
  console.log(` Docs: ${chalk.cyan("https://docker-doctor.vercel.app")}`);
923
934
  console.log(` Learn more about fixing issues, setting up CI/CD, and`);
924
935
  console.log(` configuring rules with a config file\n`);
925
936
  console.log(` GitHub: ${chalk.cyan("https://github.com/PunGrumpy/docker-doctor")}`);
926
937
  console.log(` Report issues and star the repository!`);
927
938
  };
928
- const formatTerminal = async (diagnostics, score, label, project, verbose = false, fileContents = {}) => {
939
+ const formatTerminal = async (diagnostics, score, label, project, verbose = false, fileContents = {}, unanalyzedCount = 0) => {
929
940
  if (verbose) {
930
941
  console.log(`\n${chalk.bold("Docker Doctor Diagnostics")}`);
931
942
  console.log(`=================================`);
@@ -946,8 +957,8 @@ const formatTerminal = async (diagnostics, score, label, project, verbose = fals
946
957
  if (d.severity === "warning") warningsCount += 1;
947
958
  else if (d.severity === "error") errorsCount += 1;
948
959
  }
949
- printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts);
950
- await printScoreBox(score, label, warningsCount, errorsCount);
960
+ printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts, unanalyzedCount);
961
+ await printScoreBox(score, label, warningsCount, errorsCount, unanalyzedCount);
951
962
  };
952
963
 
953
964
  //#endregion
@@ -961,8 +972,25 @@ const fileExists = async (filePath) => {
961
972
  return false;
962
973
  }
963
974
  };
975
+ const selfAndAncestors = (dir) => {
976
+ const dirs = [dir];
977
+ let parent = node_path.default.dirname(dir);
978
+ while (parent !== dirs.at(-1)) {
979
+ dirs.push(parent);
980
+ parent = node_path.default.dirname(parent);
981
+ }
982
+ return dirs;
983
+ };
984
+ const findRepositoryRoot = async (dir) => {
985
+ const candidates = selfAndAncestors(dir);
986
+ const nearest = (await Promise.all(candidates.map((candidate) => fileExists(node_path.default.join(candidate, ".git"))))).indexOf(true);
987
+ return nearest === -1 ? null : candidates[nearest];
988
+ };
964
989
  const scaffoldActionWorkflow = async (options) => {
965
- const workflowDir = node_path.default.join(options.rootDir, ".github", "workflows");
990
+ const repositoryRoot = await findRepositoryRoot(options.rootDir) ?? options.rootDir;
991
+ const scanDirectory = node_path.default.relative(repositoryRoot, options.rootDir).split(node_path.default.sep).join("/");
992
+ const directoryInput = scanDirectory === "" ? "" : ` with:\n directory: ${JSON.stringify(scanDirectory)}\n`;
993
+ const workflowDir = node_path.default.join(repositoryRoot, ".github", "workflows");
966
994
  const workflowPath = node_path.default.join(workflowDir, "docker-doctor.yml");
967
995
  const existing = await fileExists(workflowPath);
968
996
  if (existing && !await options.confirmOverwrite()) return "kept";
@@ -980,7 +1008,7 @@ jobs:
980
1008
  steps:
981
1009
  - uses: actions/checkout@v7
982
1010
  - uses: ${options.actionRef}
983
- `;
1011
+ ${directoryInput}`;
984
1012
  await node_fs_promises.default.mkdir(workflowDir, { recursive: true });
985
1013
  await node_fs_promises.default.writeFile(workflowPath, workflowYaml, "utf-8");
986
1014
  return existing ? "updated" : "created";
@@ -1172,6 +1200,7 @@ const runAgentHandoff = async (context) => {
1172
1200
  await ensureGitignoreEntry(context.rootDir);
1173
1201
  const payload = buildHandoffPayload({
1174
1202
  diagnostics: context.diagnostics,
1203
+ projectDir: context.rootDir,
1175
1204
  projectName: node_path.default.basename(context.rootDir)
1176
1205
  });
1177
1206
  if (choice === clipboardIndex) {
@@ -1180,14 +1209,14 @@ const runAgentHandoff = async (context) => {
1180
1209
  return;
1181
1210
  }
1182
1211
  const agentId = launchable[choice];
1183
- if (!await askConfirm(`Launch ${AGENT_BINARIES[agentId]} with ${AGENT_AUTO_FLAGS[agentId].join(" ")}? It will edit files without asking for approval.`)) {
1212
+ if (!await askConfirm(`Launch ${AGENT_BINARIES[agentId]} with ${AGENT_AUTO_FLAGS[agentId].join(" ")} in ${context.rootDir}? It will edit files without asking for approval.`)) {
1184
1213
  printAgentPrompt(payload);
1185
1214
  return;
1186
1215
  }
1187
1216
  const installResult = await installSkillForAgents([agentId], { projectRoot: context.rootDir });
1188
1217
  if (installResult && installResult.installed.length > 0) console.log(`\n ${chalk.green("✔")} Installed the docker-doctor skill for ${agentDisplayName(agentId)}`);
1189
1218
  console.log(`\n Handing off to ${agentDisplayName(agentId)}...\n`);
1190
- if (!await launchAgent(agentId, payload)) {
1219
+ if (!await launchAgent(agentId, payload, context.rootDir)) {
1191
1220
  console.log(` ${chalk.yellow("⚠")} Couldn't launch ${AGENT_BINARIES[agentId]}. Here's the prompt instead:`);
1192
1221
  printAgentPrompt(payload);
1193
1222
  }
@@ -1283,6 +1312,13 @@ const reportScanFailures = (failures) => {
1283
1312
  console.error(`Docker Doctor could not analyze ${failures.length} ${fileWord}; the score below covers only the files that were analyzed.`);
1284
1313
  for (const failure of failures) console.error(` - ${failure.file}: ${flattenMessage(failure.message)}`);
1285
1314
  };
1315
+ const noFilesFoundMessage = (rootDir) => `No Dockerfiles or Compose files found in ${rootDir}`;
1316
+ const printNothingScanned = (rootDir) => {
1317
+ console.log(`\n${chalk.yellow("⚠")} ${noFilesFoundMessage(rootDir)}.`);
1318
+ console.log(` docker-doctor scans files named Dockerfile, Dockerfile.*, *.dockerfile,`);
1319
+ console.log(` compose.yaml, docker-compose.yaml and variants such as compose.prod.yaml`);
1320
+ console.log(` (.yaml or .yml). Check the directory and your ignore.files patterns.`);
1321
+ };
1286
1322
  const pathExists = async (filePath) => {
1287
1323
  try {
1288
1324
  await node_fs_promises.default.access(filePath);
@@ -1312,8 +1348,9 @@ const program = new commander.Command();
1312
1348
  program.name("docker-doctor").description("Static analysis for Dockerfile and Docker Compose files").version(require_src.version, "-V, --version", "display the version number");
1313
1349
  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) => {
1314
1350
  const isSilent = options.score || options.json;
1351
+ const showProgress = Boolean(process.stdout.isTTY) && !isSilent;
1315
1352
  const restoreCursor = () => {
1316
- if (process.stdout.isTTY && !isSilent) process.stdout.write("\x1B[?25h");
1353
+ if (showProgress) process.stdout.write("\x1B[?25h");
1317
1354
  };
1318
1355
  process.on("exit", restoreCursor);
1319
1356
  process.once("SIGINT", () => {
@@ -1351,7 +1388,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1351
1388
  const setStatus = (text) => {
1352
1389
  statusText = text;
1353
1390
  };
1354
- if (process.stdout.isTTY && !isSilent) {
1391
+ if (showProgress) {
1355
1392
  process.stdout.write("\x1B[?25l");
1356
1393
  process.stdout.write(`${chalk.cyan(spinnerFrames[0])} ${statusText}`);
1357
1394
  spinnerInterval = setInterval(() => {
@@ -1360,7 +1397,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1360
1397
  }, 80);
1361
1398
  }
1362
1399
  try {
1363
- if (process.stdout.isTTY && !isSilent) await (0, node_timers_promises.setTimeout)(150);
1400
+ if (showProgress) await (0, node_timers_promises.setTimeout)(150);
1364
1401
  setStatus("Loading configuration...");
1365
1402
  const configPath = await resolveConfigPath(options.config, rootDir);
1366
1403
  const config = await require_src.loadConfig(rootDir, configPath, (message) => {
@@ -1379,6 +1416,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1379
1416
  ...project.composeFiles,
1380
1417
  ...project.dockerignores || []
1381
1418
  ];
1419
+ const scannedFileCount = project.dockerfiles.length + project.composeFiles.length;
1382
1420
  const { diagnostics, failures } = await runRulesEngine(rootDir, project, config.rules, config.categories, projectFilesList, fileContents, options, setStatus);
1383
1421
  const { score, label } = require_src.calculateScore(diagnostics);
1384
1422
  const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
@@ -1388,9 +1426,17 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1388
1426
  spinnerInterval = null;
1389
1427
  process.stdout.write("\r\x1B[K\x1B[?25h");
1390
1428
  }
1391
- if (process.stdout.isTTY && !isSilent) console.log(`${chalk.green("✔")} Scanned ${projectFilesList.length} files in ${duration}s [~${concurrency} workers]`);
1429
+ if (showProgress) console.log(`${chalk.green("✔")} Scanned ${scannedFileCount} files in ${duration}s [~${concurrency} workers]`);
1392
1430
  reportScanFailures(failures);
1393
1431
  const scanIncomplete = failures.length > 0;
1432
+ if (scannedFileCount === 0) {
1433
+ if (isSilent) console.error(`Warning: ${noFilesFoundMessage(rootDir)}`);
1434
+ else {
1435
+ printNothingScanned(rootDir);
1436
+ process.exitCode = 0;
1437
+ return;
1438
+ }
1439
+ }
1394
1440
  const hasErrors = diagnostics.some((d) => d.severity === "error");
1395
1441
  if (options.score) {
1396
1442
  console.log(score);
@@ -1402,7 +1448,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
1402
1448
  process.exitCode = scanExitCode(scanIncomplete, hasErrors);
1403
1449
  return;
1404
1450
  }
1405
- await formatTerminal(diagnostics, score, label, project, options.verbose, fileContents);
1451
+ await formatTerminal(diagnostics, score, label, project, options.verbose, fileContents, failures.length);
1406
1452
  let wizardOk = true;
1407
1453
  if (process.stdout.isTTY && process.stdin.isTTY) wizardOk = await runInteractiveWizard({
1408
1454
  diagnostics,