@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.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_src = require('./src-
|
|
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], {
|
|
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
|
-
|
|
921
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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,
|