@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/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
Your Dockerfiles are probably wrong. Docker Doctor finds out why.
|
|
16
16
|
|
|
17
|
-
Docker Doctor is an opinionated static analysis tool for Dockerfile and Docker Compose files. It scans your project, runs
|
|
17
|
+
Docker Doctor is an opinionated static analysis tool for Dockerfile and Docker Compose files. It scans your project, runs 33 rules across security, performance, best practices, Compose, and image size — then gives you a health score and fix guidance.
|
|
18
18
|
|
|
19
19
|
Works with any project that uses Docker.
|
|
20
20
|
|
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");
|
|
@@ -587,8 +587,13 @@ const writeDiagnosticsDirectory = async (diagnostics, report, rootDir) => {
|
|
|
587
587
|
...d,
|
|
588
588
|
file: sanitizePath(d.file),
|
|
589
589
|
message: sanitizeMessage(d.message)
|
|
590
|
+
})),
|
|
591
|
+
failures: report.failures.map((failure) => ({
|
|
592
|
+
file: sanitizePath(failure.file),
|
|
593
|
+
message: sanitizeMessage(failure.message)
|
|
590
594
|
}))
|
|
591
595
|
};
|
|
596
|
+
await node_fs_promises.default.writeFile(node_path.default.join(dir, ".gitignore"), "*\n", "utf-8");
|
|
592
597
|
await node_fs_promises.default.writeFile(node_path.default.join(dir, "diagnostics.json"), JSON.stringify({
|
|
593
598
|
note: TRUST_BOUNDARY_NOTE,
|
|
594
599
|
...sanitizedReport
|
|
@@ -659,6 +664,7 @@ const buildHandoffPayload = (input) => {
|
|
|
659
664
|
const ruleWord = groups.length === 1 ? "rule" : "rules";
|
|
660
665
|
const lines = [
|
|
661
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.`,
|
|
662
668
|
TRUST_BOUNDARY_NOTE,
|
|
663
669
|
""
|
|
664
670
|
];
|
|
@@ -723,8 +729,11 @@ const detectLaunchableAgents = () => {
|
|
|
723
729
|
|
|
724
730
|
//#endregion
|
|
725
731
|
//#region src/agents/launch-agent.ts
|
|
726
|
-
const launchAgent = (agentId, prompt) => new Promise((resolve) => {
|
|
727
|
-
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
|
+
});
|
|
728
737
|
child.once("error", () => {
|
|
729
738
|
resolve(false);
|
|
730
739
|
});
|
|
@@ -778,9 +787,9 @@ const printDiscoveredFiles = (project) => {
|
|
|
778
787
|
console.log(` Dockerfile(s): ${project.dockerfiles.length ? project.dockerfiles.map((f) => chalk.cyan(f)).join(", ") : chalk.dim("None")}`);
|
|
779
788
|
console.log(` Compose file(s): ${project.composeFiles.length ? project.composeFiles.map((f) => chalk.cyan(f)).join(", ") : chalk.dim("None")}`);
|
|
780
789
|
};
|
|
781
|
-
const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCounts) => {
|
|
790
|
+
const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCounts, unanalyzedCount) => {
|
|
782
791
|
if (diagnostics.length === 0) {
|
|
783
|
-
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.")}`);
|
|
784
793
|
return;
|
|
785
794
|
}
|
|
786
795
|
if (!verbose) {
|
|
@@ -797,13 +806,24 @@ const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCount
|
|
|
797
806
|
console.log(` ${cat} › ${chalk.dim(issueLabel)}`);
|
|
798
807
|
}
|
|
799
808
|
console.log(`\n Run ${chalk.cyan("docker-doctor --verbose")} to list every error and warning`);
|
|
800
|
-
const
|
|
801
|
-
for (const d of diagnostics)
|
|
802
|
-
|
|
809
|
+
const findingsByRule = /* @__PURE__ */ new Map();
|
|
810
|
+
for (const d of diagnostics) {
|
|
811
|
+
const findings = findingsByRule.get(d.rule) ?? {
|
|
812
|
+
count: 0,
|
|
813
|
+
files: /* @__PURE__ */ new Set()
|
|
814
|
+
};
|
|
815
|
+
findings.count += 1;
|
|
816
|
+
findings.files.add(d.file);
|
|
817
|
+
findingsByRule.set(d.rule, findings);
|
|
818
|
+
}
|
|
819
|
+
const migrationRules = [...findingsByRule].filter(([, { count }]) => count >= 5);
|
|
803
820
|
if (migrationRules.length > 0) {
|
|
804
821
|
console.log();
|
|
805
822
|
console.log(` ${chalk.yellow("⚠ Migration-scale change: sample before you sweep")}`);
|
|
806
|
-
for (const [rule, count] of migrationRules)
|
|
823
|
+
for (const [rule, { count, files }] of migrationRules) {
|
|
824
|
+
const fileWord = files.size === 1 ? "file" : "files";
|
|
825
|
+
console.log(` ${chalk.cyan(rule)} ×${count} across ${files.size} ${fileWord}`);
|
|
826
|
+
}
|
|
807
827
|
console.log(` Fixing all of them at once is hard to review and prone to`);
|
|
808
828
|
console.log(` subtle mistakes across the whole repo. Fix a representative`);
|
|
809
829
|
console.log(` few first and confirm the recipe holds. Then get the code`);
|
|
@@ -858,7 +878,7 @@ const getWhaleMascot = (score, border) => {
|
|
|
858
878
|
];
|
|
859
879
|
};
|
|
860
880
|
const easeOutCubic = (x) => 1 - (1 - x) ** 3;
|
|
861
|
-
const printScoreBox = async (score, label,
|
|
881
|
+
const printScoreBox = async (score, label, warningsCount, errorsCount, unanalyzedCount) => {
|
|
862
882
|
const { isTTY } = process.stdout;
|
|
863
883
|
const shouldAnimate = isTTY && !process.env.CI && !process.env.NO_ANIMATION && process.env.TERM !== "dumb" && process.env.NODE_ENV !== "test";
|
|
864
884
|
let scoreColor = chalk.red.bold;
|
|
@@ -902,15 +922,21 @@ const printScoreBox = async (score, label, categoryIssueCounts, warningsCount, e
|
|
|
902
922
|
console.log(` ${whaleLines[3]}`);
|
|
903
923
|
}
|
|
904
924
|
console.log(`\n ${chalk.dim("────────────────────────────────────────────────────────────")}\n`);
|
|
905
|
-
|
|
906
|
-
|
|
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
|
+
}
|
|
907
933
|
console.log(` Docs: ${chalk.cyan("https://docker-doctor.vercel.app")}`);
|
|
908
934
|
console.log(` Learn more about fixing issues, setting up CI/CD, and`);
|
|
909
935
|
console.log(` configuring rules with a config file\n`);
|
|
910
936
|
console.log(` GitHub: ${chalk.cyan("https://github.com/PunGrumpy/docker-doctor")}`);
|
|
911
937
|
console.log(` Report issues and star the repository!`);
|
|
912
938
|
};
|
|
913
|
-
const formatTerminal = async (diagnostics, score, label, project, verbose = false, fileContents = {}) => {
|
|
939
|
+
const formatTerminal = async (diagnostics, score, label, project, verbose = false, fileContents = {}, unanalyzedCount = 0) => {
|
|
914
940
|
if (verbose) {
|
|
915
941
|
console.log(`\n${chalk.bold("Docker Doctor Diagnostics")}`);
|
|
916
942
|
console.log(`=================================`);
|
|
@@ -931,8 +957,8 @@ const formatTerminal = async (diagnostics, score, label, project, verbose = fals
|
|
|
931
957
|
if (d.severity === "warning") warningsCount += 1;
|
|
932
958
|
else if (d.severity === "error") errorsCount += 1;
|
|
933
959
|
}
|
|
934
|
-
printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts);
|
|
935
|
-
await printScoreBox(score, label,
|
|
960
|
+
printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts, unanalyzedCount);
|
|
961
|
+
await printScoreBox(score, label, warningsCount, errorsCount, unanalyzedCount);
|
|
936
962
|
};
|
|
937
963
|
|
|
938
964
|
//#endregion
|
|
@@ -946,8 +972,25 @@ const fileExists = async (filePath) => {
|
|
|
946
972
|
return false;
|
|
947
973
|
}
|
|
948
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
|
+
};
|
|
949
989
|
const scaffoldActionWorkflow = async (options) => {
|
|
950
|
-
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");
|
|
951
994
|
const workflowPath = node_path.default.join(workflowDir, "docker-doctor.yml");
|
|
952
995
|
const existing = await fileExists(workflowPath);
|
|
953
996
|
if (existing && !await options.confirmOverwrite()) return "kept";
|
|
@@ -958,13 +1001,14 @@ permissions:
|
|
|
958
1001
|
contents: read
|
|
959
1002
|
pull-requests: write
|
|
960
1003
|
issues: write
|
|
1004
|
+
statuses: write
|
|
961
1005
|
jobs:
|
|
962
1006
|
docker-doctor:
|
|
963
1007
|
runs-on: ubuntu-latest
|
|
964
1008
|
steps:
|
|
965
|
-
- uses: actions/checkout@
|
|
1009
|
+
- uses: actions/checkout@v7
|
|
966
1010
|
- uses: ${options.actionRef}
|
|
967
|
-
`;
|
|
1011
|
+
${directoryInput}`;
|
|
968
1012
|
await node_fs_promises.default.mkdir(workflowDir, { recursive: true });
|
|
969
1013
|
await node_fs_promises.default.writeFile(workflowPath, workflowYaml, "utf-8");
|
|
970
1014
|
return existing ? "updated" : "created";
|
|
@@ -1156,6 +1200,7 @@ const runAgentHandoff = async (context) => {
|
|
|
1156
1200
|
await ensureGitignoreEntry(context.rootDir);
|
|
1157
1201
|
const payload = buildHandoffPayload({
|
|
1158
1202
|
diagnostics: context.diagnostics,
|
|
1203
|
+
projectDir: context.rootDir,
|
|
1159
1204
|
projectName: node_path.default.basename(context.rootDir)
|
|
1160
1205
|
});
|
|
1161
1206
|
if (choice === clipboardIndex) {
|
|
@@ -1164,14 +1209,14 @@ const runAgentHandoff = async (context) => {
|
|
|
1164
1209
|
return;
|
|
1165
1210
|
}
|
|
1166
1211
|
const agentId = launchable[choice];
|
|
1167
|
-
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.`)) {
|
|
1168
1213
|
printAgentPrompt(payload);
|
|
1169
1214
|
return;
|
|
1170
1215
|
}
|
|
1171
1216
|
const installResult = await installSkillForAgents([agentId], { projectRoot: context.rootDir });
|
|
1172
1217
|
if (installResult && installResult.installed.length > 0) console.log(`\n ${chalk.green("✔")} Installed the docker-doctor skill for ${agentDisplayName(agentId)}`);
|
|
1173
1218
|
console.log(`\n Handing off to ${agentDisplayName(agentId)}...\n`);
|
|
1174
|
-
if (!await launchAgent(agentId, payload)) {
|
|
1219
|
+
if (!await launchAgent(agentId, payload, context.rootDir)) {
|
|
1175
1220
|
console.log(` ${chalk.yellow("⚠")} Couldn't launch ${AGENT_BINARIES[agentId]}. Here's the prompt instead:`);
|
|
1176
1221
|
printAgentPrompt(payload);
|
|
1177
1222
|
}
|
|
@@ -1267,12 +1312,45 @@ const reportScanFailures = (failures) => {
|
|
|
1267
1312
|
console.error(`Docker Doctor could not analyze ${failures.length} ${fileWord}; the score below covers only the files that were analyzed.`);
|
|
1268
1313
|
for (const failure of failures) console.error(` - ${failure.file}: ${flattenMessage(failure.message)}`);
|
|
1269
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
|
+
};
|
|
1322
|
+
const pathExists = async (filePath) => {
|
|
1323
|
+
try {
|
|
1324
|
+
await node_fs_promises.default.access(filePath);
|
|
1325
|
+
return true;
|
|
1326
|
+
} catch {
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
const resolveConfigPath = async (configOption, rootDir) => {
|
|
1331
|
+
if (!configOption) return;
|
|
1332
|
+
const fromCwd = node_path.default.resolve(process.cwd(), configOption);
|
|
1333
|
+
if (await pathExists(fromCwd)) return fromCwd;
|
|
1334
|
+
const fromRoot = node_path.default.resolve(rootDir, configOption);
|
|
1335
|
+
if (fromRoot !== fromCwd && await pathExists(fromRoot)) {
|
|
1336
|
+
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.`);
|
|
1337
|
+
return fromRoot;
|
|
1338
|
+
}
|
|
1339
|
+
return fromCwd;
|
|
1340
|
+
};
|
|
1341
|
+
const findScanTargetProblem = async (rootDir) => {
|
|
1342
|
+
const rootStat = await node_fs_promises.default.stat(rootDir).catch(() => null);
|
|
1343
|
+
if (rootStat === null) return `Directory not found: ${rootDir}`;
|
|
1344
|
+
if (!rootStat.isDirectory()) return `${rootDir} is not a directory. Pass the directory that contains your Dockerfile or Compose file.`;
|
|
1345
|
+
return null;
|
|
1346
|
+
};
|
|
1270
1347
|
const program = new commander.Command();
|
|
1271
1348
|
program.name("docker-doctor").description("Static analysis for Dockerfile and Docker Compose files").version(require_src.version, "-V, --version", "display the version number");
|
|
1272
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) => {
|
|
1273
1350
|
const isSilent = options.score || options.json;
|
|
1351
|
+
const showProgress = Boolean(process.stdout.isTTY) && !isSilent;
|
|
1274
1352
|
const restoreCursor = () => {
|
|
1275
|
-
if (
|
|
1353
|
+
if (showProgress) process.stdout.write("\x1B[?25h");
|
|
1276
1354
|
};
|
|
1277
1355
|
process.on("exit", restoreCursor);
|
|
1278
1356
|
process.once("SIGINT", () => {
|
|
@@ -1285,6 +1363,12 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
1285
1363
|
});
|
|
1286
1364
|
try {
|
|
1287
1365
|
const rootDir = node_path.default.resolve(dir);
|
|
1366
|
+
const scanTargetProblem = await findScanTargetProblem(rootDir);
|
|
1367
|
+
if (scanTargetProblem !== null) {
|
|
1368
|
+
console.error(`Error: ${scanTargetProblem}`);
|
|
1369
|
+
process.exitCode = 1;
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1288
1372
|
const startTime = Date.now();
|
|
1289
1373
|
let statusText = "Discovering workspace...";
|
|
1290
1374
|
const spinnerFrames = [
|
|
@@ -1304,7 +1388,7 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
1304
1388
|
const setStatus = (text) => {
|
|
1305
1389
|
statusText = text;
|
|
1306
1390
|
};
|
|
1307
|
-
if (
|
|
1391
|
+
if (showProgress) {
|
|
1308
1392
|
process.stdout.write("\x1B[?25l");
|
|
1309
1393
|
process.stdout.write(`${chalk.cyan(spinnerFrames[0])} ${statusText}`);
|
|
1310
1394
|
spinnerInterval = setInterval(() => {
|
|
@@ -1313,19 +1397,26 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
1313
1397
|
}, 80);
|
|
1314
1398
|
}
|
|
1315
1399
|
try {
|
|
1316
|
-
if (
|
|
1400
|
+
if (showProgress) await (0, node_timers_promises.setTimeout)(150);
|
|
1317
1401
|
setStatus("Loading configuration...");
|
|
1318
|
-
const
|
|
1402
|
+
const configPath = await resolveConfigPath(options.config, rootDir);
|
|
1403
|
+
const config = await require_src.loadConfig(rootDir, configPath, (message) => {
|
|
1319
1404
|
console.error(`Warning: ${message}`);
|
|
1320
1405
|
});
|
|
1321
1406
|
setStatus("Scanning workspace files...");
|
|
1322
|
-
const project = await require_src.discoverProject(rootDir, {
|
|
1407
|
+
const project = await require_src.discoverProject(rootDir, {
|
|
1408
|
+
ignoreFiles: config.ignore?.files,
|
|
1409
|
+
onSkippedDirectory: (skippedDir) => {
|
|
1410
|
+
console.error(`Warning: skipped unreadable directory ${skippedDir} (permission denied)`);
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1323
1413
|
const fileContents = {};
|
|
1324
1414
|
const projectFilesList = [
|
|
1325
1415
|
...project.dockerfiles,
|
|
1326
1416
|
...project.composeFiles,
|
|
1327
1417
|
...project.dockerignores || []
|
|
1328
1418
|
];
|
|
1419
|
+
const scannedFileCount = project.dockerfiles.length + project.composeFiles.length;
|
|
1329
1420
|
const { diagnostics, failures } = await runRulesEngine(rootDir, project, config.rules, config.categories, projectFilesList, fileContents, options, setStatus);
|
|
1330
1421
|
const { score, label } = require_src.calculateScore(diagnostics);
|
|
1331
1422
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
@@ -1335,25 +1426,33 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
1335
1426
|
spinnerInterval = null;
|
|
1336
1427
|
process.stdout.write("\r\x1B[K\x1B[?25h");
|
|
1337
1428
|
}
|
|
1338
|
-
if (
|
|
1429
|
+
if (showProgress) console.log(`${chalk.green("✔")} Scanned ${scannedFileCount} files in ${duration}s [~${concurrency} workers]`);
|
|
1339
1430
|
reportScanFailures(failures);
|
|
1340
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
|
+
}
|
|
1341
1440
|
const hasErrors = diagnostics.some((d) => d.severity === "error");
|
|
1342
1441
|
if (options.score) {
|
|
1343
1442
|
console.log(score);
|
|
1344
1443
|
process.exitCode = scanExitCode(scanIncomplete, hasErrors);
|
|
1345
1444
|
return;
|
|
1346
1445
|
} else if (options.json) {
|
|
1347
|
-
const report = require_src.toJsonReport(diagnostics, score, label, project);
|
|
1446
|
+
const report = require_src.toJsonReport(diagnostics, score, label, project, failures);
|
|
1348
1447
|
console.log(JSON.stringify(report, null, 2));
|
|
1349
1448
|
process.exitCode = scanExitCode(scanIncomplete, hasErrors);
|
|
1350
1449
|
return;
|
|
1351
1450
|
}
|
|
1352
|
-
await formatTerminal(diagnostics, score, label, project, options.verbose, fileContents);
|
|
1451
|
+
await formatTerminal(diagnostics, score, label, project, options.verbose, fileContents, failures.length);
|
|
1353
1452
|
let wizardOk = true;
|
|
1354
1453
|
if (process.stdout.isTTY && process.stdin.isTTY) wizardOk = await runInteractiveWizard({
|
|
1355
1454
|
diagnostics,
|
|
1356
|
-
report: require_src.toJsonReport(diagnostics, score, label, project),
|
|
1455
|
+
report: require_src.toJsonReport(diagnostics, score, label, project, failures),
|
|
1357
1456
|
rootDir
|
|
1358
1457
|
});
|
|
1359
1458
|
const exitCode = scanExitCode(scanIncomplete, hasErrors);
|
|
@@ -1441,7 +1540,7 @@ rules.command("list").description("list all available rules").action(() => {
|
|
|
1441
1540
|
}
|
|
1442
1541
|
});
|
|
1443
1542
|
rules.command("explain <rule>").description("explain a specific rule in detail").action((ruleKey) => {
|
|
1444
|
-
const rule = require_src.findRule(ruleKey);
|
|
1543
|
+
const rule = require_src.findRule(ruleKey) ?? require_src.findRule(`docker-doctor/${ruleKey}`);
|
|
1445
1544
|
if (!rule) {
|
|
1446
1545
|
console.error(`Rule '${ruleKey}' not found.`);
|
|
1447
1546
|
process.exitCode = 1;
|