@docker-doctor/cli 0.5.3 → 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/README.md +1 -1
- package/dist/cli.cjs +129 -30
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +129 -30
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.mts +7 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-D4rVJg36.cjs → src-C7XK6Kp-.cjs} +280 -76
- package/dist/src-C7XK6Kp-.cjs.map +1 -0
- package/dist/{src-CklEIXTX.mjs → src-DiEWLJ6g.mjs} +280 -76
- package/dist/src-DiEWLJ6g.mjs.map +1 -0
- package/package.json +2 -1
- package/skill/docker-doctor/SKILL.md +7 -5
- package/skill/docker-doctor/references/explain.md +2 -2
- package/dist/src-CklEIXTX.mjs.map +0 -1
- package/dist/src-D4rVJg36.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";
|
|
@@ -580,8 +580,13 @@ 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
|
};
|
|
589
|
+
await fs.writeFile(path.join(dir, ".gitignore"), "*\n", "utf-8");
|
|
585
590
|
await fs.writeFile(path.join(dir, "diagnostics.json"), JSON.stringify({
|
|
586
591
|
note: TRUST_BOUNDARY_NOTE,
|
|
587
592
|
...sanitizedReport
|
|
@@ -652,6 +657,7 @@ const buildHandoffPayload = (input) => {
|
|
|
652
657
|
const ruleWord = groups.length === 1 ? "rule" : "rules";
|
|
653
658
|
const lines = [
|
|
654
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.`,
|
|
655
661
|
TRUST_BOUNDARY_NOTE,
|
|
656
662
|
""
|
|
657
663
|
];
|
|
@@ -716,8 +722,11 @@ const detectLaunchableAgents = () => {
|
|
|
716
722
|
|
|
717
723
|
//#endregion
|
|
718
724
|
//#region src/agents/launch-agent.ts
|
|
719
|
-
const launchAgent = (agentId, prompt) => new Promise((resolve) => {
|
|
720
|
-
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
|
+
});
|
|
721
730
|
child.once("error", () => {
|
|
722
731
|
resolve(false);
|
|
723
732
|
});
|
|
@@ -771,9 +780,9 @@ const printDiscoveredFiles = (project) => {
|
|
|
771
780
|
console.log(` Dockerfile(s): ${project.dockerfiles.length ? project.dockerfiles.map((f) => chalk.cyan(f)).join(", ") : chalk.dim("None")}`);
|
|
772
781
|
console.log(` Compose file(s): ${project.composeFiles.length ? project.composeFiles.map((f) => chalk.cyan(f)).join(", ") : chalk.dim("None")}`);
|
|
773
782
|
};
|
|
774
|
-
const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCounts) => {
|
|
783
|
+
const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCounts, unanalyzedCount) => {
|
|
775
784
|
if (diagnostics.length === 0) {
|
|
776
|
-
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.")}`);
|
|
777
786
|
return;
|
|
778
787
|
}
|
|
779
788
|
if (!verbose) {
|
|
@@ -790,13 +799,24 @@ const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCount
|
|
|
790
799
|
console.log(` ${cat} › ${chalk.dim(issueLabel)}`);
|
|
791
800
|
}
|
|
792
801
|
console.log(`\n Run ${chalk.cyan("docker-doctor --verbose")} to list every error and warning`);
|
|
793
|
-
const
|
|
794
|
-
for (const d of diagnostics)
|
|
795
|
-
|
|
802
|
+
const findingsByRule = /* @__PURE__ */ new Map();
|
|
803
|
+
for (const d of diagnostics) {
|
|
804
|
+
const findings = findingsByRule.get(d.rule) ?? {
|
|
805
|
+
count: 0,
|
|
806
|
+
files: /* @__PURE__ */ new Set()
|
|
807
|
+
};
|
|
808
|
+
findings.count += 1;
|
|
809
|
+
findings.files.add(d.file);
|
|
810
|
+
findingsByRule.set(d.rule, findings);
|
|
811
|
+
}
|
|
812
|
+
const migrationRules = [...findingsByRule].filter(([, { count }]) => count >= 5);
|
|
796
813
|
if (migrationRules.length > 0) {
|
|
797
814
|
console.log();
|
|
798
815
|
console.log(` ${chalk.yellow("⚠ Migration-scale change: sample before you sweep")}`);
|
|
799
|
-
for (const [rule, count] of migrationRules)
|
|
816
|
+
for (const [rule, { count, files }] of migrationRules) {
|
|
817
|
+
const fileWord = files.size === 1 ? "file" : "files";
|
|
818
|
+
console.log(` ${chalk.cyan(rule)} ×${count} across ${files.size} ${fileWord}`);
|
|
819
|
+
}
|
|
800
820
|
console.log(` Fixing all of them at once is hard to review and prone to`);
|
|
801
821
|
console.log(` subtle mistakes across the whole repo. Fix a representative`);
|
|
802
822
|
console.log(` few first and confirm the recipe holds. Then get the code`);
|
|
@@ -851,7 +871,7 @@ const getWhaleMascot = (score, border) => {
|
|
|
851
871
|
];
|
|
852
872
|
};
|
|
853
873
|
const easeOutCubic = (x) => 1 - (1 - x) ** 3;
|
|
854
|
-
const printScoreBox = async (score, label,
|
|
874
|
+
const printScoreBox = async (score, label, warningsCount, errorsCount, unanalyzedCount) => {
|
|
855
875
|
const { isTTY } = process.stdout;
|
|
856
876
|
const shouldAnimate = isTTY && !process.env.CI && !process.env.NO_ANIMATION && process.env.TERM !== "dumb" && process.env.NODE_ENV !== "test";
|
|
857
877
|
let scoreColor = chalk.red.bold;
|
|
@@ -895,15 +915,21 @@ const printScoreBox = async (score, label, categoryIssueCounts, warningsCount, e
|
|
|
895
915
|
console.log(` ${whaleLines[3]}`);
|
|
896
916
|
}
|
|
897
917
|
console.log(`\n ${chalk.dim("────────────────────────────────────────────────────────────")}\n`);
|
|
898
|
-
|
|
899
|
-
|
|
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
|
+
}
|
|
900
926
|
console.log(` Docs: ${chalk.cyan("https://docker-doctor.vercel.app")}`);
|
|
901
927
|
console.log(` Learn more about fixing issues, setting up CI/CD, and`);
|
|
902
928
|
console.log(` configuring rules with a config file\n`);
|
|
903
929
|
console.log(` GitHub: ${chalk.cyan("https://github.com/PunGrumpy/docker-doctor")}`);
|
|
904
930
|
console.log(` Report issues and star the repository!`);
|
|
905
931
|
};
|
|
906
|
-
const formatTerminal = async (diagnostics, score, label, project, verbose = false, fileContents = {}) => {
|
|
932
|
+
const formatTerminal = async (diagnostics, score, label, project, verbose = false, fileContents = {}, unanalyzedCount = 0) => {
|
|
907
933
|
if (verbose) {
|
|
908
934
|
console.log(`\n${chalk.bold("Docker Doctor Diagnostics")}`);
|
|
909
935
|
console.log(`=================================`);
|
|
@@ -924,8 +950,8 @@ const formatTerminal = async (diagnostics, score, label, project, verbose = fals
|
|
|
924
950
|
if (d.severity === "warning") warningsCount += 1;
|
|
925
951
|
else if (d.severity === "error") errorsCount += 1;
|
|
926
952
|
}
|
|
927
|
-
printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts);
|
|
928
|
-
await printScoreBox(score, label,
|
|
953
|
+
printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts, unanalyzedCount);
|
|
954
|
+
await printScoreBox(score, label, warningsCount, errorsCount, unanalyzedCount);
|
|
929
955
|
};
|
|
930
956
|
|
|
931
957
|
//#endregion
|
|
@@ -939,8 +965,25 @@ const fileExists = async (filePath) => {
|
|
|
939
965
|
return false;
|
|
940
966
|
}
|
|
941
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
|
+
};
|
|
942
982
|
const scaffoldActionWorkflow = async (options) => {
|
|
943
|
-
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");
|
|
944
987
|
const workflowPath = path.join(workflowDir, "docker-doctor.yml");
|
|
945
988
|
const existing = await fileExists(workflowPath);
|
|
946
989
|
if (existing && !await options.confirmOverwrite()) return "kept";
|
|
@@ -951,13 +994,14 @@ permissions:
|
|
|
951
994
|
contents: read
|
|
952
995
|
pull-requests: write
|
|
953
996
|
issues: write
|
|
997
|
+
statuses: write
|
|
954
998
|
jobs:
|
|
955
999
|
docker-doctor:
|
|
956
1000
|
runs-on: ubuntu-latest
|
|
957
1001
|
steps:
|
|
958
|
-
- uses: actions/checkout@
|
|
1002
|
+
- uses: actions/checkout@v7
|
|
959
1003
|
- uses: ${options.actionRef}
|
|
960
|
-
`;
|
|
1004
|
+
${directoryInput}`;
|
|
961
1005
|
await fs.mkdir(workflowDir, { recursive: true });
|
|
962
1006
|
await fs.writeFile(workflowPath, workflowYaml, "utf-8");
|
|
963
1007
|
return existing ? "updated" : "created";
|
|
@@ -1149,6 +1193,7 @@ const runAgentHandoff = async (context) => {
|
|
|
1149
1193
|
await ensureGitignoreEntry(context.rootDir);
|
|
1150
1194
|
const payload = buildHandoffPayload({
|
|
1151
1195
|
diagnostics: context.diagnostics,
|
|
1196
|
+
projectDir: context.rootDir,
|
|
1152
1197
|
projectName: path.basename(context.rootDir)
|
|
1153
1198
|
});
|
|
1154
1199
|
if (choice === clipboardIndex) {
|
|
@@ -1157,14 +1202,14 @@ const runAgentHandoff = async (context) => {
|
|
|
1157
1202
|
return;
|
|
1158
1203
|
}
|
|
1159
1204
|
const agentId = launchable[choice];
|
|
1160
|
-
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.`)) {
|
|
1161
1206
|
printAgentPrompt(payload);
|
|
1162
1207
|
return;
|
|
1163
1208
|
}
|
|
1164
1209
|
const installResult = await installSkillForAgents([agentId], { projectRoot: context.rootDir });
|
|
1165
1210
|
if (installResult && installResult.installed.length > 0) console.log(`\n ${chalk.green("✔")} Installed the docker-doctor skill for ${agentDisplayName(agentId)}`);
|
|
1166
1211
|
console.log(`\n Handing off to ${agentDisplayName(agentId)}...\n`);
|
|
1167
|
-
if (!await launchAgent(agentId, payload)) {
|
|
1212
|
+
if (!await launchAgent(agentId, payload, context.rootDir)) {
|
|
1168
1213
|
console.log(` ${chalk.yellow("⚠")} Couldn't launch ${AGENT_BINARIES[agentId]}. Here's the prompt instead:`);
|
|
1169
1214
|
printAgentPrompt(payload);
|
|
1170
1215
|
}
|
|
@@ -1260,12 +1305,45 @@ const reportScanFailures = (failures) => {
|
|
|
1260
1305
|
console.error(`Docker Doctor could not analyze ${failures.length} ${fileWord}; the score below covers only the files that were analyzed.`);
|
|
1261
1306
|
for (const failure of failures) console.error(` - ${failure.file}: ${flattenMessage(failure.message)}`);
|
|
1262
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
|
+
};
|
|
1315
|
+
const pathExists = async (filePath) => {
|
|
1316
|
+
try {
|
|
1317
|
+
await fs.access(filePath);
|
|
1318
|
+
return true;
|
|
1319
|
+
} catch {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
const resolveConfigPath = async (configOption, rootDir) => {
|
|
1324
|
+
if (!configOption) return;
|
|
1325
|
+
const fromCwd = path.resolve(process.cwd(), configOption);
|
|
1326
|
+
if (await pathExists(fromCwd)) return fromCwd;
|
|
1327
|
+
const fromRoot = path.resolve(rootDir, configOption);
|
|
1328
|
+
if (fromRoot !== fromCwd && await pathExists(fromRoot)) {
|
|
1329
|
+
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.`);
|
|
1330
|
+
return fromRoot;
|
|
1331
|
+
}
|
|
1332
|
+
return fromCwd;
|
|
1333
|
+
};
|
|
1334
|
+
const findScanTargetProblem = async (rootDir) => {
|
|
1335
|
+
const rootStat = await fs.stat(rootDir).catch(() => null);
|
|
1336
|
+
if (rootStat === null) return `Directory not found: ${rootDir}`;
|
|
1337
|
+
if (!rootStat.isDirectory()) return `${rootDir} is not a directory. Pass the directory that contains your Dockerfile or Compose file.`;
|
|
1338
|
+
return null;
|
|
1339
|
+
};
|
|
1263
1340
|
const program = new Command();
|
|
1264
1341
|
program.name("docker-doctor").description("Static analysis for Dockerfile and Docker Compose files").version(version, "-V, --version", "display the version number");
|
|
1265
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) => {
|
|
1266
1343
|
const isSilent = options.score || options.json;
|
|
1344
|
+
const showProgress = Boolean(process.stdout.isTTY) && !isSilent;
|
|
1267
1345
|
const restoreCursor = () => {
|
|
1268
|
-
if (
|
|
1346
|
+
if (showProgress) process.stdout.write("\x1B[?25h");
|
|
1269
1347
|
};
|
|
1270
1348
|
process.on("exit", restoreCursor);
|
|
1271
1349
|
process.once("SIGINT", () => {
|
|
@@ -1278,6 +1356,12 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
1278
1356
|
});
|
|
1279
1357
|
try {
|
|
1280
1358
|
const rootDir = path.resolve(dir);
|
|
1359
|
+
const scanTargetProblem = await findScanTargetProblem(rootDir);
|
|
1360
|
+
if (scanTargetProblem !== null) {
|
|
1361
|
+
console.error(`Error: ${scanTargetProblem}`);
|
|
1362
|
+
process.exitCode = 1;
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1281
1365
|
const startTime = Date.now();
|
|
1282
1366
|
let statusText = "Discovering workspace...";
|
|
1283
1367
|
const spinnerFrames = [
|
|
@@ -1297,7 +1381,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
1297
1381
|
const setStatus = (text) => {
|
|
1298
1382
|
statusText = text;
|
|
1299
1383
|
};
|
|
1300
|
-
if (
|
|
1384
|
+
if (showProgress) {
|
|
1301
1385
|
process.stdout.write("\x1B[?25l");
|
|
1302
1386
|
process.stdout.write(`${chalk.cyan(spinnerFrames[0])} ${statusText}`);
|
|
1303
1387
|
spinnerInterval = setInterval(() => {
|
|
@@ -1306,19 +1390,26 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
1306
1390
|
}, 80);
|
|
1307
1391
|
}
|
|
1308
1392
|
try {
|
|
1309
|
-
if (
|
|
1393
|
+
if (showProgress) await setTimeout(150);
|
|
1310
1394
|
setStatus("Loading configuration...");
|
|
1311
|
-
const
|
|
1395
|
+
const configPath = await resolveConfigPath(options.config, rootDir);
|
|
1396
|
+
const config = await loadConfig(rootDir, configPath, (message) => {
|
|
1312
1397
|
console.error(`Warning: ${message}`);
|
|
1313
1398
|
});
|
|
1314
1399
|
setStatus("Scanning workspace files...");
|
|
1315
|
-
const project = await discoverProject(rootDir, {
|
|
1400
|
+
const project = await discoverProject(rootDir, {
|
|
1401
|
+
ignoreFiles: config.ignore?.files,
|
|
1402
|
+
onSkippedDirectory: (skippedDir) => {
|
|
1403
|
+
console.error(`Warning: skipped unreadable directory ${skippedDir} (permission denied)`);
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1316
1406
|
const fileContents = {};
|
|
1317
1407
|
const projectFilesList = [
|
|
1318
1408
|
...project.dockerfiles,
|
|
1319
1409
|
...project.composeFiles,
|
|
1320
1410
|
...project.dockerignores || []
|
|
1321
1411
|
];
|
|
1412
|
+
const scannedFileCount = project.dockerfiles.length + project.composeFiles.length;
|
|
1322
1413
|
const { diagnostics, failures } = await runRulesEngine(rootDir, project, config.rules, config.categories, projectFilesList, fileContents, options, setStatus);
|
|
1323
1414
|
const { score, label } = calculateScore(diagnostics);
|
|
1324
1415
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
@@ -1328,25 +1419,33 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
1328
1419
|
spinnerInterval = null;
|
|
1329
1420
|
process.stdout.write("\r\x1B[K\x1B[?25h");
|
|
1330
1421
|
}
|
|
1331
|
-
if (
|
|
1422
|
+
if (showProgress) console.log(`${chalk.green("✔")} Scanned ${scannedFileCount} files in ${duration}s [~${concurrency} workers]`);
|
|
1332
1423
|
reportScanFailures(failures);
|
|
1333
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
|
+
}
|
|
1334
1433
|
const hasErrors = diagnostics.some((d) => d.severity === "error");
|
|
1335
1434
|
if (options.score) {
|
|
1336
1435
|
console.log(score);
|
|
1337
1436
|
process.exitCode = scanExitCode(scanIncomplete, hasErrors);
|
|
1338
1437
|
return;
|
|
1339
1438
|
} else if (options.json) {
|
|
1340
|
-
const report = toJsonReport(diagnostics, score, label, project);
|
|
1439
|
+
const report = toJsonReport(diagnostics, score, label, project, failures);
|
|
1341
1440
|
console.log(JSON.stringify(report, null, 2));
|
|
1342
1441
|
process.exitCode = scanExitCode(scanIncomplete, hasErrors);
|
|
1343
1442
|
return;
|
|
1344
1443
|
}
|
|
1345
|
-
await formatTerminal(diagnostics, score, label, project, options.verbose, fileContents);
|
|
1444
|
+
await formatTerminal(diagnostics, score, label, project, options.verbose, fileContents, failures.length);
|
|
1346
1445
|
let wizardOk = true;
|
|
1347
1446
|
if (process.stdout.isTTY && process.stdin.isTTY) wizardOk = await runInteractiveWizard({
|
|
1348
1447
|
diagnostics,
|
|
1349
|
-
report: toJsonReport(diagnostics, score, label, project),
|
|
1448
|
+
report: toJsonReport(diagnostics, score, label, project, failures),
|
|
1350
1449
|
rootDir
|
|
1351
1450
|
});
|
|
1352
1451
|
const exitCode = scanExitCode(scanIncomplete, hasErrors);
|
|
@@ -1434,7 +1533,7 @@ rules.command("list").description("list all available rules").action(() => {
|
|
|
1434
1533
|
}
|
|
1435
1534
|
});
|
|
1436
1535
|
rules.command("explain <rule>").description("explain a specific rule in detail").action((ruleKey) => {
|
|
1437
|
-
const rule = findRule(ruleKey);
|
|
1536
|
+
const rule = findRule(ruleKey) ?? findRule(`docker-doctor/${ruleKey}`);
|
|
1438
1537
|
if (!rule) {
|
|
1439
1538
|
console.error(`Rule '${ruleKey}' not found.`);
|
|
1440
1539
|
process.exitCode = 1;
|