@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 +66 -20
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +66 -20
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-BpAv-H0Z.cjs → src-C7XK6Kp-.cjs} +123 -52
- package/dist/src-C7XK6Kp-.cjs.map +1 -0
- package/dist/{src-BLeXr_hR.mjs → src-DiEWLJ6g.mjs} +123 -52
- package/dist/src-DiEWLJ6g.mjs.map +1 -0
- package/package.json +1 -1
- package/skill/docker-doctor/SKILL.md +7 -5
- package/skill/docker-doctor/references/explain.md +2 -2
- package/dist/src-BLeXr_hR.mjs.map +0 -1
- package/dist/src-BpAv-H0Z.cjs.map +0 -1
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-
|
|
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-DiEWLJ6g.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], {
|
|
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
|
-
|
|
914
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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,6 +1409,7 @@ 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
1414
|
const { score, label } = calculateScore(diagnostics);
|
|
1377
1415
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
@@ -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 (
|
|
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,
|