@docker-doctor/cli 0.3.2 → 0.3.4

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.
@@ -4,74 +4,7 @@ import path from "node:path";
4
4
  import { parse } from "yaml";
5
5
 
6
6
  //#region package.json
7
- var package_default = {
8
- name: "@docker-doctor/cli",
9
- version: "0.3.2",
10
- description: "Static analysis for Dockerfile and Docker Compose files",
11
- keywords: [
12
- "best-practices",
13
- "diagnostics",
14
- "docker",
15
- "docker-compose",
16
- "dockerfile",
17
- "linter",
18
- "performance",
19
- "security"
20
- ],
21
- homepage: "https://docker-doctor.vercel.app",
22
- bugs: { "url": "https://github.com/PunGrumpy/docker-doctor/issues" },
23
- license: "MIT",
24
- author: {
25
- "name": "Noppakorn Kaewsalabnil",
26
- "url": "https://www.pungrumpy.com"
27
- },
28
- repository: {
29
- "type": "git",
30
- "url": "git+https://github.com/PunGrumpy/docker-doctor.git",
31
- "directory": "packages/docker-doctor"
32
- },
33
- bin: { "docker-doctor": "dist/cli.mjs" },
34
- files: [
35
- "dist",
36
- "skill",
37
- "LICENSE"
38
- ],
39
- type: "module",
40
- sideEffects: false,
41
- exports: { ".": {
42
- "import": {
43
- "types": "./dist/index.d.mts",
44
- "default": "./dist/index.mjs"
45
- },
46
- "require": {
47
- "types": "./dist/index.d.cts",
48
- "default": "./dist/index.cjs"
49
- }
50
- } },
51
- publishConfig: {
52
- "access": "public",
53
- "registry": "https://registry.npmjs.org/"
54
- },
55
- scripts: {
56
- "build": "NODE_OPTIONS='--max-old-space-size=4096' tsdown",
57
- "dev": "NODE_OPTIONS='--max-old-space-size=4096' tsdown --watch",
58
- "test": "bun test",
59
- "typecheck": "tsc --noEmit",
60
- "clean": "git clean -xdf .turbo node_modules dist skill"
61
- },
62
- dependencies: {
63
- "agent-install": "0.0.8",
64
- "chalk": "^5.4.1",
65
- "commander": "^15.0.0",
66
- "yaml": "^2.7.0"
67
- },
68
- devDependencies: {
69
- "@docker-doctor/core": "workspace:*",
70
- "@types/node": "^26",
71
- "tsdown": "^0.22.14",
72
- "typescript": "^6"
73
- }
74
- };
7
+ var version = "0.3.4";
75
8
 
76
9
  //#endregion
77
10
  //#region ../core/src/project-info/discover.ts
@@ -1066,5 +999,5 @@ const toJsonReport = (diagnostics, score, label, project) => ({
1066
999
  });
1067
1000
 
1068
1001
  //#endregion
1069
- export { runDockerfileRules as a, parseCompose as c, package_default as d, runComposeRules as i, parseDockerfile as l, calculateScore as n, allRules as o, loadConfig as r, findRule as s, toJsonReport as t, discoverProject as u };
1070
- //# sourceMappingURL=src-M5fLUTju.mjs.map
1002
+ export { runDockerfileRules as a, parseCompose as c, version as d, runComposeRules as i, parseDockerfile as l, calculateScore as n, allRules as o, loadConfig as r, findRule as s, toJsonReport as t, discoverProject as u };
1003
+ //# sourceMappingURL=src-BuADwzRj.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"src-M5fLUTju.mjs","names":["createDiagnostic","createDiagnostic","createDiagnostic","createDiagnostic","parseYaml"],"sources":["../package.json","../../core/src/project-info/discover.ts","../../core/src/parsers/dockerfile-parser.ts","../../core/src/errors/config-error.ts","../../core/src/errors/parse-error.ts","../../core/src/parsers/compose-parser.ts","../../core/src/rules/best-practices.ts","../../core/src/rules/compose.ts","../../core/src/parsers/image-ref.ts","../../core/src/rules/image-size.ts","../../core/src/rules/performance.ts","../../core/src/rules/security.ts","../../core/src/rules/index.ts","../../core/src/runners/dockerfile-runner.ts","../../core/src/runners/compose-runner.ts","../../core/src/schemas/config.ts","../../core/src/config/loader.ts","../../core/src/scoring.ts","../../core/src/report.ts"],"sourcesContent":["","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { ProjectInfo } from \"../types/index\";\n\nconst walk = async (\n dir: string,\n fileList: string[] = []\n): Promise<string[]> => {\n const files = await fs.readdir(dir, { withFileTypes: true });\n await Promise.all(\n files.map(async (file) => {\n const filePath = path.join(dir, file.name);\n if (file.isDirectory()) {\n if (\n file.name === \"node_modules\" ||\n file.name === \".git\" ||\n file.name === \".next\" ||\n file.name === \"dist\" ||\n file.name === \".turbo\"\n ) {\n return;\n }\n await walk(filePath, fileList);\n } else {\n fileList.push(filePath);\n }\n })\n );\n return fileList;\n};\n\nexport const discoverProject = async (\n rootDir: string\n): Promise<ProjectInfo> => {\n const allFiles = await walk(rootDir);\n const dockerfiles: string[] = [];\n const composeFiles: string[] = [];\n const dockerignores: string[] = [];\n\n for (const file of allFiles) {\n const base = path.basename(file).toLowerCase();\n\n if (base === \".dockerignore\") {\n dockerignores.push(path.relative(rootDir, file));\n }\n\n // Match Dockerfile, Dockerfile.*, *.dockerfile\n if (\n base === \"dockerfile\" ||\n base.startsWith(\"dockerfile.\") ||\n base.endsWith(\".dockerfile\")\n ) {\n dockerfiles.push(path.relative(rootDir, file));\n }\n\n // Match docker-compose.yml, docker-compose.*.yml, compose.yml, compose.*.yml, and yaml extensions\n if (\n base === \"docker-compose.yml\" ||\n base === \"docker-compose.yaml\" ||\n base === \"compose.yml\" ||\n base === \"compose.yaml\" ||\n ((base.startsWith(\"docker-compose.\") || base.startsWith(\"compose.\")) &&\n (base.endsWith(\".yml\") || base.endsWith(\".yaml\")))\n ) {\n composeFiles.push(path.relative(rootDir, file));\n }\n }\n\n return {\n composeFiles,\n dockerfiles,\n dockerignores,\n };\n};\n","import type { DockerfileInstruction } from \"../types/index\";\n\nconst DOCKERFILE_KEYWORDS = new Set([\n \"ADD\",\n \"ARG\",\n \"CMD\",\n \"COPY\",\n \"ENTRYPOINT\",\n \"ENV\",\n \"EXPOSE\",\n \"FROM\",\n \"HEALTHCHECK\",\n \"LABEL\",\n \"MAINTAINER\",\n \"ONBUILD\",\n \"RUN\",\n \"SHELL\",\n \"STOPSIGNAL\",\n \"USER\",\n \"VOLUME\",\n \"WORKDIR\",\n]);\n\nconst INSTRUCTION_LINE_RE = /^(?<inst>[A-Za-z]+)\\s+(?<args>.*)$/u;\n\n// Matches a heredoc opener like <<EOF, <<-EOF, <<'EOF', <<\"EOF\". Global so a\n// single line (e.g. `COPY <<FILE1 <<FILE2 /dest/`) can open more than one.\nconst HEREDOC_OPENER_RE = /<<-?\\s*(?<quote>['\"]?)(?<delim>\\w+)\\k<quote>/gu;\n\ninterface ParserState {\n instructions: DockerfileInstruction[];\n currentInstruction: string;\n currentArgs: string;\n startLine: number;\n rawAccumulator: string[];\n // FIFO of heredoc delimiters still open for the current instruction, in\n // the order they were opened (closed in the same order).\n heredocQueue: string[];\n}\n\nconst createParserState = (): ParserState => ({\n currentArgs: \"\",\n currentInstruction: \"\",\n heredocQueue: [],\n instructions: [],\n rawAccumulator: [],\n startLine: 0,\n});\n\nconst closeInstruction = (state: ParserState): void => {\n if (state.currentInstruction) {\n state.instructions.push({\n args: state.currentArgs,\n instruction: state.currentInstruction,\n line: state.startLine,\n raw: state.rawAccumulator.join(\"\\n\"),\n });\n }\n state.currentInstruction = \"\";\n state.currentArgs = \"\";\n state.rawAccumulator = [];\n};\n\nconst matchInstructionKeyword = (\n lineContent: string\n): { instruction: string; args: string } | null => {\n const match = lineContent.match(INSTRUCTION_LINE_RE);\n const matchedWord = match?.groups?.inst.toUpperCase();\n if (matchedWord && DOCKERFILE_KEYWORDS.has(matchedWord)) {\n return { args: match?.groups?.args ?? \"\", instruction: matchedWord };\n }\n\n const word = lineContent.trim().toUpperCase();\n if (DOCKERFILE_KEYWORDS.has(word)) {\n return { args: \"\", instruction: word };\n }\n\n return null;\n};\n\nconst findHeredocDelimiters = (lineContent: string): string[] =>\n [...lineContent.matchAll(HEREDOC_OPENER_RE)].map(\n (m) => m.groups?.delim ?? \"\"\n );\n\n// Heredoc body lines are never treated as instructions (not even comment\n// lines, which are shell-comment content here) — they are folded verbatim\n// into the owning instruction's args until the delimiter line closes the\n// (possibly multiple) open heredoc(s), in the order they were opened.\nconst processHeredocLine = (state: ParserState, trimmed: string): void => {\n if (trimmed === state.heredocQueue[0]) {\n state.heredocQueue.shift();\n } else {\n state.currentArgs += (state.currentArgs ? \" \" : \"\") + trimmed;\n }\n\n if (state.heredocQueue.length === 0) {\n // Last open heredoc just closed: the instruction ends here.\n closeInstruction(state);\n }\n};\n\nconst processInstructionLine = (\n state: ParserState,\n trimmed: string,\n lineNum: number\n): void => {\n let lineContent = trimmed;\n\n // Inside a multi-line run, comment lines are ignored by docker parser\n if (lineContent.startsWith(\"#\")) {\n return;\n }\n\n const hasContinuation = lineContent.endsWith(\"\\\\\");\n if (hasContinuation) {\n lineContent = lineContent.slice(0, -1).trim();\n }\n\n if (state.currentInstruction) {\n state.currentArgs += (state.currentArgs ? \" \" : \"\") + lineContent;\n } else {\n state.startLine = lineNum;\n // Match the first instruction word (e.g. FROM, RUN, COPY)\n const matched = matchInstructionKeyword(lineContent);\n if (matched) {\n state.currentInstruction = matched.instruction;\n state.currentArgs = matched.args;\n }\n }\n\n if (state.currentInstruction) {\n state.heredocQueue.push(...findHeredocDelimiters(lineContent));\n }\n\n if (state.heredocQueue.length > 0) {\n // A heredoc opener implies continuation even without a trailing\n // backslash — keep accumulating until every opened delimiter closes.\n return;\n }\n\n if (!hasContinuation) {\n closeInstruction(state);\n }\n};\n\nexport const parseDockerfile = (content: string): DockerfileInstruction[] => {\n const state = createParserState();\n const lines = content.split(/\\r?\\n/u);\n\n for (const [i, rawLine] of lines.entries()) {\n const trimmed = rawLine.trim();\n const lineNum = i + 1;\n const insideHeredoc = state.heredocQueue.length > 0;\n\n // Skip empty lines or comment lines if not in multi-line block\n if (\n !state.currentInstruction &&\n !insideHeredoc &&\n (trimmed === \"\" || trimmed.startsWith(\"#\"))\n ) {\n continue;\n }\n\n state.rawAccumulator.push(rawLine);\n\n if (insideHeredoc) {\n processHeredocLine(state, trimmed);\n } else {\n processInstructionLine(state, trimmed, lineNum);\n }\n }\n\n // EOF: an unterminated heredoc (or a trailing backslash continuation)\n // still emits whatever was accumulated so far, rather than dropping it.\n closeInstruction(state);\n\n return state.instructions;\n};\n","export class ConfigError extends Error {\n readonly _tag = \"ConfigError\" as const;\n\n constructor(options: { readonly message: string }) {\n super(options.message);\n this.name = \"ConfigError\";\n }\n}\n","export class ParseError extends Error {\n readonly _tag = \"ParseError\" as const;\n readonly file: string;\n\n constructor(options: { readonly file: string; readonly message: string }) {\n super(options.message);\n this.name = \"ParseError\";\n this.file = options.file;\n }\n}\n","import { parse } from \"yaml\";\n\nimport { ParseError } from \"../errors\";\n\nexport const parseCompose = (content: string, filepath: string): unknown => {\n try {\n return parse(content);\n } catch (error: unknown) {\n throw new ParseError({\n file: filepath,\n message: error instanceof Error ? error.message : String(error),\n });\n }\n};\n","import type { Diagnostic, DockerfileRule } from \"../types/index\";\n\nconst createDiagnostic = (\n file: string,\n ruleKey: string,\n severity: \"error\" | \"warning\" | \"info\",\n message: string,\n help: string,\n line?: number\n): Diagnostic => ({ file, help, line, message, rule: ruleKey, severity });\n\nexport const requireHealthcheck: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const hasHealthcheck = instructions.some(\n (inst) => inst.instruction === \"HEALTHCHECK\"\n );\n\n // Only suggest healthcheck if it has exposed ports or command lines indicating it runs an app\n const hasExposedPortsOrEntry = instructions.some(\n (inst) =>\n inst.instruction === \"EXPOSE\" ||\n inst.instruction === \"CMD\" ||\n inst.instruction === \"ENTRYPOINT\"\n );\n\n if (!hasHealthcheck && hasExposedPortsOrEntry) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"No HEALTHCHECK instruction found. Containers running services should expose healthchecks to enable auto-healing.\",\n this.help,\n 1\n ),\n ];\n }\n return [];\n },\n defaultSeverity: \"info\",\n help: \"Use HEALTHCHECK (e.g., 'HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/ || exit 1') so Docker can monitor the container's live status.\",\n key: \"docker-doctor/require-healthcheck\",\n message: \"Add a HEALTHCHECK instruction\",\n};\n\nexport const preferCopyOverAdd: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n if (inst.instruction === \"ADD\") {\n const parts = inst.args.split(/\\s+/u);\n const src = parts.find((p) => !p.startsWith(\"--\"));\n\n if (!src) {\n continue;\n }\n\n // If it's not a remote url (handled by security/no-add-remote) and not a compressed file\n const isRemote =\n src.startsWith(\"http://\") || src.startsWith(\"https://\");\n const isArchive =\n src.endsWith(\".tar\") ||\n src.endsWith(\".tar.gz\") ||\n src.endsWith(\".tgz\") ||\n src.endsWith(\".zip\");\n\n if (!isRemote && !isArchive) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `ADD instruction used for regular files: '${inst.args}'. COPY is simpler and less prone to magic side effects.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Use COPY instead of ADD unless you explicitly need auto-extraction of local compressed archives (tar, zip, etc.).\",\n key: \"docker-doctor/prefer-copy-over-add\",\n message: \"Prefer COPY over ADD\",\n};\n\nexport const useExecForm: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n if (inst.instruction === \"CMD\" || inst.instruction === \"ENTRYPOINT\") {\n const args = inst.args.trim();\n // If it does not start with [ and end with ]\n if (!args.startsWith(\"[\") || !args.endsWith(\"]\")) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `${inst.instruction} instruction uses shell form instead of exec form. In shell form, the command runs under '/bin/sh -c', which does not pass signals to child processes.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: 'Write CMD/ENTRYPOINT instructions as JSON arrays (e.g. ENTRYPOINT [\"node\", \"index.js\"]) so OS signals (like SIGTERM) are forwarded correctly.',\n key: \"docker-doctor/use-exec-form\",\n message: \"Use exec form for CMD and ENTRYPOINT\",\n};\n\nexport const requireLabels: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const hasLabel = instructions.some((inst) => inst.instruction === \"LABEL\");\n if (!hasLabel) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"No LABEL metadata was found in this Dockerfile. Adding labels helps identify build information, maintainers, and descriptions.\",\n this.help,\n 1\n ),\n ];\n }\n return [];\n },\n defaultSeverity: \"info\",\n help: 'Use LABEL instructions (e.g. LABEL org.opencontainers.image.authors=\"...\") to document ownership, license, version, and build info.',\n key: \"docker-doctor/require-labels\",\n message: \"Add LABEL metadata to images\",\n};\n\nexport const combineAptUpdateInstall: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n const hasUpdate = inst.args.includes(\"apt-get update\");\n const hasInstall = inst.args.includes(\"apt-get install\");\n\n if (hasUpdate && !hasInstall) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"RUN apt-get update used without apt-get install in the same instruction. This can cause caching issues and build failures.\",\n this.help,\n inst.line\n )\n );\n } else if (hasInstall && !hasUpdate) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"RUN apt-get install used without apt-get update in the same instruction. Always combine them to ensure up-to-date package installation.\",\n this.help,\n inst.line\n )\n );\n }\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Combine 'apt-get update' and 'apt-get install' in the same RUN instruction (e.g. 'RUN apt-get update && apt-get install -y --no-install-recommends <package> && rm -rf /var/lib/apt/lists/*').\",\n key: \"docker-doctor/combine-apt-update-install\",\n message: \"Combine apt-get update and apt-get install\",\n};\n\nexport const usePipefail: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n const { raw } = inst;\n const hasPipe = /(?<!\\|)\\|(?!\\|)/u.test(raw);\n if (hasPipe && !raw.includes(\"pipefail\")) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"RUN instruction uses a pipe (|) but does not configure 'pipefail'. If a command in the pipe fails, the step may still succeed silently.\",\n this.help,\n inst.line\n )\n );\n }\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Prepend 'set -o pipefail &&' to pipe commands, or use exec form with a shell that supports it (e.g., RUN ['/bin/bash', '-c', 'set -o pipefail && ...']).\",\n key: \"docker-doctor/use-pipefail\",\n message: \"Use pipefail to catch pipeline command failures\",\n};\n\nexport const absoluteWorkdir: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"WORKDIR\") {\n const path = inst.args.trim();\n const isAbsolute = /^(?:\\/|\\\\|\\$|[a-zA-Z]:)/u.test(path);\n\n if (!isAbsolute) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `WORKDIR specifies a relative path '${path}'. For clarity and reliability, always use absolute paths.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Always specify absolute paths for WORKDIR instructions (e.g. WORKDIR /app).\",\n key: \"docker-doctor/absolute-workdir\",\n message: \"Use absolute paths for WORKDIR\",\n};\n\nexport const avoidRunCd: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\" && /\\bcd\\b/u.test(inst.args)) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"Avoid using 'cd' in RUN instructions. Use WORKDIR instead to change the working directory stably across layers.\",\n this.help,\n inst.line\n )\n );\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Use the WORKDIR instruction instead of 'cd' inside RUN to establish directory context.\",\n key: \"docker-doctor/avoid-run-cd\",\n message: \"Avoid changing directories with cd in RUN\",\n};\n\nexport const sortMultilineArgs: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n const { raw } = inst;\n const isPackageInstall =\n raw.includes(\"apt-get install\") ||\n raw.includes(\"apk add\") ||\n raw.includes(\"yum install\") ||\n raw.includes(\"dnf install\");\n\n const hasContinuation = raw.includes(\"\\\\\\n\") || raw.includes(\"\\\\\\r\\n\");\n\n if (isPackageInstall && hasContinuation) {\n const lines = raw.split(/\\r?\\n/u);\n const packages = lines\n .slice(1)\n .map((line) => line.trim())\n .filter(\n (line) =>\n line !== \"\" &&\n !line.startsWith(\"&&\") &&\n !line.startsWith(\"-\") &&\n !line.includes(\"rm -rf\")\n )\n .map((line) =>\n line.endsWith(\"\\\\\") ? line.slice(0, -1).trim() : line\n )\n .filter(Boolean);\n\n if (packages.length > 1) {\n const sorted = packages.toSorted((a, b) => a.localeCompare(b));\n const isSorted = packages.every((val, idx) => val === sorted[idx]);\n if (!isSorted) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"Multi-line package arguments are not sorted alphanumerically. Keeping them sorted makes maintenance easier and prevents duplicates.\",\n this.help,\n inst.line\n )\n );\n }\n }\n }\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Sort multi-line package installation lists (e.g. apk/apt package lists) alphabetically.\",\n key: \"docker-doctor/sort-multiline-args\",\n message: \"Sort multi-line arguments alphanumerically\",\n};\n\nexport const useraddNoLogInit: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (\n inst.instruction === \"RUN\" &&\n /\\buseradd\\b/u.test(inst.args) &&\n !inst.args.includes(\"--no-log-init\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"RUN instruction runs 'useradd' without '--no-log-init'. This can cause excessive disk space usage / exhaustion under Go's sparse tar archive bug when large UIDs are used.\",\n this.help,\n inst.line\n )\n );\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Pass '--no-log-init' flag to useradd (e.g., 'RUN useradd --no-log-init -r -g mygroup myuser').\",\n key: \"docker-doctor/useradd-no-log-init\",\n message: \"Use --no-log-init with useradd\",\n};\n\nexport const bestPracticesRules = [\n requireHealthcheck,\n preferCopyOverAdd,\n useExecForm,\n requireLabels,\n combineAptUpdateInstall,\n usePipefail,\n absoluteWorkdir,\n avoidRunCd,\n sortMultilineArgs,\n useraddNoLogInit,\n];\n","import type { Diagnostic, ComposeRule } from \"../types/index\";\n\nconst createDiagnostic = (\n file: string,\n ruleKey: string,\n severity: \"error\" | \"warning\" | \"info\",\n message: string,\n help: string\n): Diagnostic => ({ file, help, message, rule: ruleKey, severity });\n\nexport const noVersionKey: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file) {\n if (\n composeContent &&\n typeof composeContent === \"object\" &&\n \"version\" in composeContent\n ) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"The 'version' property is deprecated. Remove it to use standard Compose spec behavior.\",\n this.help\n ),\n ];\n }\n return [];\n },\n defaultSeverity: \"warning\",\n help: \"The 'version' key is deprecated by the Compose specification. Omitting it defaults to the latest specification.\",\n key: \"docker-doctor/no-version-key\",\n message: \"Remove the 'version' key from Compose file\",\n};\n\nexport const requireResourceLimits: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file) {\n const diagnostics: Diagnostic[] = [];\n\n if (\n composeContent &&\n typeof composeContent === \"object\" &&\n \"services\" in composeContent\n ) {\n const { services } = composeContent;\n if (services && typeof services === \"object\") {\n for (const [name, config] of Object.entries(services)) {\n if (config && typeof config === \"object\") {\n const deploy = (config as Record<string, unknown>).deploy as\n | Record<string, unknown>\n | undefined;\n const resources = deploy?.resources as\n | Record<string, unknown>\n | undefined;\n const limits = resources?.limits as\n | Record<string, unknown>\n | undefined;\n\n if (!limits || (!limits.cpus && !limits.memory)) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Service '${name}' does not have CPU or memory limits defined. A resource leak in this service could crash the host.`,\n this.help\n )\n );\n }\n }\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Add resource limits (e.g. deploy.resources.limits) to prevent a single service from starving host resources in production.\",\n key: \"docker-doctor/require-resource-limits\",\n message: \"Define resource limits for services\",\n};\n\nexport const requireRestartPolicy: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file) {\n const diagnostics: Diagnostic[] = [];\n\n if (\n composeContent &&\n typeof composeContent === \"object\" &&\n \"services\" in composeContent\n ) {\n const { services } = composeContent;\n if (services && typeof services === \"object\") {\n for (const [name, config] of Object.entries(services)) {\n if (config && typeof config === \"object\") {\n const hasRestart = \"restart\" in config;\n const deploy = (config as Record<string, unknown>).deploy as\n | Record<string, unknown>\n | undefined;\n const hasDeployRestart = deploy?.restart_policy !== undefined;\n\n if (!hasRestart && !hasDeployRestart) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Service '${name}' has no restart policy configured. It will not restart if it crashes or if the host reboots.`,\n this.help\n )\n );\n }\n }\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Define 'restart: always' or 'restart: unless-stopped' (or deploy.restart_policy) so services restart on crashes or host reboot.\",\n key: \"docker-doctor/require-restart-policy\",\n message: \"Set restart policy for services\",\n};\n\nexport const useDependsOnCondition: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file) {\n const diagnostics: Diagnostic[] = [];\n\n if (\n composeContent &&\n typeof composeContent === \"object\" &&\n \"services\" in composeContent\n ) {\n const { services } = composeContent;\n if (services && typeof services === \"object\") {\n for (const [name, config] of Object.entries(services)) {\n if (config && typeof config === \"object\") {\n const dependsOn = (config as Record<string, unknown>).depends_on;\n if (dependsOn && Array.isArray(dependsOn)) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Service '${name}' uses shorthand depends_on list. This only checks if containers are started, not if they are ready/healthy.`,\n this.help\n )\n );\n }\n }\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Instead of a simple service list, use 'depends_on: { dependency: { condition: service_healthy } }' to ensure dependencies are fully ready before starting.\",\n key: \"docker-doctor/use-depends-on-condition\",\n message: \"Use long-form depends_on with healthcheck conditions\",\n};\n\nexport const composeRules = [\n noVersionKey,\n requireResourceLimits,\n requireRestartPolicy,\n useDependsOnCondition,\n];\n","import type { DockerfileInstruction } from \"../types/index\";\n\nexport interface ImageRef {\n registry?: string;\n name: string;\n tag?: string;\n digest?: string;\n isVariable: boolean;\n}\n\nexport const parseImageRef = (ref: string): ImageRef => {\n if (ref.includes(\"${\") || ref.startsWith(\"$\")) {\n return { isVariable: true, name: ref };\n }\n\n let remainder = ref;\n let digest: string | undefined;\n\n const atIndex = remainder.indexOf(\"@\");\n if (atIndex !== -1) {\n digest = remainder.slice(atIndex + 1);\n remainder = remainder.slice(0, atIndex);\n }\n\n let tag: string | undefined;\n const lastColonIndex = remainder.lastIndexOf(\":\");\n const lastSlashIndex = remainder.lastIndexOf(\"/\");\n\n if (lastColonIndex !== -1 && lastColonIndex > lastSlashIndex) {\n tag = remainder.slice(lastColonIndex + 1);\n remainder = remainder.slice(0, lastColonIndex);\n }\n\n let registry: string | undefined;\n const firstSlashIndex = remainder.indexOf(\"/\");\n if (firstSlashIndex !== -1) {\n const firstSegment = remainder.slice(0, firstSlashIndex);\n if (\n firstSegment.includes(\".\") ||\n firstSegment.includes(\":\") ||\n firstSegment === \"localhost\"\n ) {\n registry = firstSegment;\n remainder = remainder.slice(firstSlashIndex + 1);\n }\n }\n\n return {\n digest,\n isVariable: false,\n name: remainder,\n registry,\n tag,\n };\n};\n\nexport const collectStageAliases = (\n instructions: DockerfileInstruction[]\n): Set<string> => {\n const aliases = new Set<string>();\n\n for (const inst of instructions) {\n if (inst.instruction !== \"FROM\") {\n continue;\n }\n\n const match = /\\sas\\s+(?<alias>\\S+)/iu.exec(inst.args);\n if (match?.groups?.alias) {\n aliases.add(match.groups.alias.toLowerCase());\n }\n }\n\n return aliases;\n};\n","import { collectStageAliases, parseImageRef } from \"../parsers/image-ref\";\nimport type { Diagnostic, DockerfileRule } from \"../types/index\";\n\nconst createDiagnostic = (\n file: string,\n ruleKey: string,\n severity: \"error\" | \"warning\" | \"info\",\n message: string,\n help: string,\n line?: number\n): Diagnostic => ({ file, help, line, message, rule: ruleKey, severity });\n\nexport const preferSlimBase: DockerfileRule = {\n category: \"Image Size\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n const stageAliases = collectStageAliases(instructions);\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n const parts = inst.args.split(/\\s+/u);\n const imagePart = parts.find((p) => !p.startsWith(\"--\"));\n if (!imagePart || imagePart === \"scratch\") {\n continue;\n }\n\n const ref = parseImageRef(imagePart);\n\n if (ref.isVariable || stageAliases.has(imagePart.toLowerCase())) {\n continue;\n }\n\n // Digest pins are already fully deterministic; not our concern here.\n if (ref.digest) {\n continue;\n }\n\n // No tag: pin-image-version owns the untagged case, don't double-report.\n if (!ref.tag) {\n continue;\n }\n\n const tag = ref.tag.toLowerCase();\n const isSlim =\n tag.includes(\"alpine\") ||\n tag.includes(\"slim\") ||\n tag.includes(\"distroless\");\n\n if (!isSlim) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Base image '${imagePart}' may be a full-OS distribution. Consider using a slim or alpine alternative.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Prefer tags with '-slim', '-alpine', or use distroless base images to minimize the default operating system footprint.\",\n key: \"docker-doctor/prefer-slim-base\",\n message: \"Use slim, alpine, or distroless base images\",\n};\n\nexport const cleanPackageCache: DockerfileRule = {\n category: \"Image Size\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n const { args } = inst;\n\n // check apt-get install without cleanup\n if (\n args.includes(\"apt-get install\") &&\n !args.includes(\"rm -rf /var/lib/apt/lists\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Running 'apt-get install' without removing package lists afterwards. This keeps metadata caches inside the image layer.`,\n this.help,\n inst.line\n )\n );\n }\n\n // check apk add without --no-cache\n if (\n args.includes(\"apk add\") &&\n !args.includes(\"--no-cache\") &&\n !args.includes(\"rm -rf /var/cache/apk\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Running 'apk add' without '--no-cache' or cleaning the apk cache. This increases layer size.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"For apt-get, append '&& rm -rf /var/lib/apt/lists/*'. For apk, use 'apk add --no-cache'. For dnf/yum, run 'yum clean all'.\",\n key: \"docker-doctor/clean-package-cache\",\n message: \"Clean up package manager cache in the same RUN layer\",\n};\n\nexport const avoidDevDependencies: DockerfileRule = {\n category: \"Image Size\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n let isLastStage = false;\n let fromCount = 0;\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n fromCount += 1;\n }\n }\n\n let currentStage = 0;\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n currentStage += 1;\n isLastStage = currentStage === fromCount;\n }\n\n if (isLastStage && inst.instruction === \"RUN\") {\n const { args } = inst;\n if (\n (args.includes(\"npm install\") ||\n args.includes(\"npm ci\") ||\n args.includes(\"yarn install\")) &&\n !args.includes(\"--production\") &&\n !args.includes(\"--omit=dev\") &&\n !args.includes(\"prune\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Running package install '${inst.args}' in the final stage without omitting devDependencies.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"For Node.js, run 'npm prune --production' or install only production dependencies ('npm ci --omit=dev') in the runtime stage.\",\n key: \"docker-doctor/avoid-dev-dependencies\",\n message:\n \"Avoid installing development dependencies in final production stage\",\n};\n\nexport const imageSizeRules = [\n preferSlimBase,\n cleanPackageCache,\n avoidDevDependencies,\n];\n","import type { Diagnostic, DockerfileRule } from \"../types/index\";\n\nconst createDiagnostic = (\n file: string,\n ruleKey: string,\n severity: \"error\" | \"warning\" | \"info\",\n message: string,\n help: string,\n line?: number\n): Diagnostic => ({ file, help, line, message, rule: ruleKey, severity });\n\nexport const useMultiStage: DockerfileRule = {\n category: \"Performance\",\n check(instructions, file) {\n const fromCount = instructions.filter(\n (inst) => inst.instruction === \"FROM\"\n ).length;\n if (fromCount === 1) {\n // Check if it's not a trivial/short Dockerfile (e.g., has some build steps)\n const hasBuildSteps = instructions.some(\n (inst) =>\n inst.instruction === \"RUN\" &&\n (inst.args.includes(\"npm run build\") ||\n inst.args.includes(\"yarn build\") ||\n inst.args.includes(\"bun run build\") ||\n inst.args.includes(\"cargo build\") ||\n inst.args.includes(\"make\"))\n );\n\n if (hasBuildSteps) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"Only one build stage (FROM) was detected, but build instructions were found. Multi-stage builds can significantly reduce final image size.\",\n this.help,\n instructions.find((inst) => inst.instruction === \"FROM\")?.line || 1\n ),\n ];\n }\n }\n return [];\n },\n defaultSeverity: \"info\",\n help: \"Use multi-stage builds (multiple FROM statements) to separate build dependencies from the runtime image and reduce size.\",\n key: \"docker-doctor/use-multi-stage\",\n message: \"Consider using multi-stage builds\",\n};\n\nexport const orderLayers: DockerfileRule = {\n category: \"Performance\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n let copyAllLine = -1;\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n copyAllLine = -1;\n }\n\n if (inst.instruction === \"COPY\" || inst.instruction === \"ADD\") {\n const parts = inst.args.split(/\\s+/u);\n const src = parts.find((p) => !p.startsWith(\"--\"));\n\n if (!src) {\n continue;\n }\n\n const normalized = src.replace(/^\\.\\//u, \"\");\n const isCopyAll =\n normalized === \".\" ||\n normalized === \"\" ||\n normalized === \"*\" ||\n normalized === \"src\" ||\n normalized.startsWith(\"src/\");\n\n if (isCopyAll && copyAllLine === -1) {\n copyAllLine = inst.line;\n }\n }\n\n if (inst.instruction === \"RUN\" && copyAllLine !== -1) {\n const args = inst.args.toLowerCase();\n if (\n args.includes(\"npm install\") ||\n args.includes(\"npm ci\") ||\n args.includes(\"yarn install\") ||\n args.includes(\"bun install\") ||\n args.includes(\"pip install\") ||\n args.includes(\"cargo fetch\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Running package installation command '${inst.args}' after copying application files (at line ${copyAllLine}). This invalidates the cache on any code changes.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Copy dependency definition files (like package.json, lockfiles) and run install commands BEFORE copying the rest of the application source code.\",\n key: \"docker-doctor/order-layers\",\n message: \"Order layers to maximize build cache utility\",\n};\n\nexport const minimizeLayers: DockerfileRule = {\n category: \"Performance\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n let consecutiveRunCount = 0;\n let firstRunLine = -1;\n\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n if (consecutiveRunCount === 0) {\n firstRunLine = inst.line;\n }\n consecutiveRunCount += 1;\n } else {\n if (consecutiveRunCount > 2) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`,\n this.help,\n firstRunLine\n )\n );\n }\n consecutiveRunCount = 0;\n }\n }\n\n if (consecutiveRunCount > 2) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`,\n this.help,\n firstRunLine\n )\n );\n }\n\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Combine consecutive RUN instructions using '&&' and '\\\\' to reduce the total layer count and image size.\",\n key: \"docker-doctor/minimize-layers\",\n message: \"Minimize the number of image layers\",\n};\n\nexport const useDockerignore: DockerfileRule = {\n category: \"Performance\",\n check(instructions, file, context) {\n // If copying everything, we definitely need .dockerignore\n const hasCopyAll = instructions.some((inst) => {\n if (inst.instruction === \"COPY\" || inst.instruction === \"ADD\") {\n // COPY --from=<stage> reads from a previous build stage, not the\n // build context, so .dockerignore is irrelevant to it.\n const isStageCopy = /(?:^|\\s)--from=/u.test(inst.args);\n if (isStageCopy) {\n return false;\n }\n\n const parts = inst.args.split(/\\s+/u);\n const src = parts.find((p) => !p.startsWith(\"--\"));\n if (!src) {\n return false;\n }\n return src === \".\" || src === \"./\" || src === \"*\";\n }\n return false;\n });\n\n if (hasCopyAll && context?.projectFiles) {\n const hasDockerignore = context.projectFiles.some((f) =>\n f.endsWith(\".dockerignore\")\n );\n if (!hasDockerignore) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"Using COPY/ADD with wildcard/directory, but no .dockerignore file was found in the workspace. This can copy local build folders and secrets.\",\n this.help,\n 1\n ),\n ];\n }\n }\n return [];\n },\n defaultSeverity: \"warning\",\n help: \"Create a .dockerignore file in the same directory as the Dockerfile to prevent copying unnecessary files (like node_modules, logs, build artifacts).\",\n key: \"docker-doctor/use-dockerignore\",\n message: \"Ensure .dockerignore is used\",\n};\n\nexport const performanceRules = [\n useMultiStage,\n orderLayers,\n minimizeLayers,\n useDockerignore,\n];\n","import { collectStageAliases, parseImageRef } from \"../parsers/image-ref\";\nimport type { Diagnostic, DockerfileRule } from \"../types/index\";\n\nconst createDiagnostic = (\n file: string,\n ruleKey: string,\n severity: \"error\" | \"warning\" | \"info\",\n message: string,\n help: string,\n line?: number\n): Diagnostic => ({ file, help, line, message, rule: ruleKey, severity });\n\nexport const noRootUser: DockerfileRule = {\n category: \"Security\",\n check(instructions, file) {\n let lastUser = \"root\";\n let lastUserLine = 1;\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n lastUser = \"root\";\n lastUserLine = inst.line;\n } else if (inst.instruction === \"USER\") {\n lastUser = inst.args.trim().toLowerCase();\n lastUserLine = inst.line;\n }\n }\n\n if (lastUser === \"root\" || lastUser === \"0\" || lastUser === \"0:0\") {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"The container runs as root. Running as root allows potential container breakout vulnerabilities.\",\n this.help,\n lastUserLine\n ),\n ];\n }\n\n return [];\n },\n defaultSeverity: \"warning\",\n help: \"Add a non-root user (e.g., 'USER node' or 'USER 1000') to improve security.\",\n key: \"docker-doctor/no-root-user\",\n message: \"Container should not run as root user\",\n};\n\nexport const noSecretsInEnv: DockerfileRule = {\n category: \"Security\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n const secretKeywords = [\n /(?:^|[_-])password(?:[_-]|$)/iu,\n /(?:^|[_-])secret(?:[_-]|$)/iu,\n /(?:^|[_-])token(?:[_-]|$)/iu,\n /(?:^|[_-])api_key(?:[_-]|$)/iu,\n /(?:^|[_-])private_key(?:[_-]|$)/iu,\n /(?:^|[_-])auth(?:[_-]|$)/iu,\n ];\n\n for (const inst of instructions) {\n if (inst.instruction === \"ENV\" || inst.instruction === \"ARG\") {\n const args = inst.args.trim();\n if (inst.instruction === \"ENV\" && !args.includes(\"=\")) {\n // KEY VALUE format\n const match = args.match(/^(?<key>[^\\s]+)\\s+(?<value>.*)$/u);\n if (match?.groups) {\n const { key, value } = match.groups;\n const isSecretKey = secretKeywords.some((regex) => regex.test(key));\n if (\n isSecretKey &&\n value &&\n !value.startsWith(\"$\") &&\n !value.startsWith(\"{\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`,\n this.help,\n inst.line\n )\n );\n }\n }\n } else {\n // Existing KEY=VALUE logic\n const parts = args.split(/\\s+/u);\n for (const part of parts) {\n const eqIndex = part.indexOf(\"=\");\n let key = \"\";\n let value = \"\";\n\n if (eqIndex > 0) {\n key = part.slice(0, eqIndex);\n value = part.slice(eqIndex + 1);\n } else {\n key = part;\n }\n\n const isSecretKey = secretKeywords.some((regex) => regex.test(key));\n if (\n isSecretKey &&\n value &&\n !value.startsWith(\"$\") &&\n !value.startsWith(\"{\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"error\",\n help: \"Use Docker Secrets, build arguments passed at runtime, or environment variables at runtime instead of baking them into the image.\",\n key: \"docker-doctor/no-secrets-in-env\",\n message: \"Do not store secrets in ENV or ARG instructions\",\n};\n\nexport const pinImageVersion: DockerfileRule = {\n category: \"Security\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n const stageAliases = collectStageAliases(instructions);\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n // FROM image or FROM image:tag or FROM image@sha256:hash\n // Also respect multi-stage builds (AS stageName)\n const parts = inst.args.split(/\\s+/u);\n const imagePart = parts.find((p) => !p.startsWith(\"--\"));\n\n if (!imagePart || imagePart === \"scratch\") {\n continue;\n }\n\n const ref = parseImageRef(imagePart);\n\n if (ref.isVariable || stageAliases.has(imagePart.toLowerCase())) {\n continue;\n }\n\n if (!(ref.tag || ref.digest)) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Base image '${imagePart}' does not specify a tag. This makes builds non-deterministic.`,\n this.help,\n inst.line\n )\n );\n } else if (ref.tag === \"latest\" && !ref.digest) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Base image '${imagePart}' uses the mutable 'latest' tag. This makes builds non-deterministic.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Specify a concrete tag instead of 'latest' or no tag (e.g., 'node:22.2.0-alpine' instead of 'node').\",\n key: \"docker-doctor/pin-image-version\",\n message: \"Always pin base image versions to specific tags\",\n};\n\nexport const noAddRemote: DockerfileRule = {\n category: \"Security\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n if (inst.instruction === \"ADD\") {\n const parts = inst.args.split(/\\s+/u);\n const src = parts.find((p) => !p.startsWith(\"--\"));\n\n if (!src) {\n continue;\n }\n\n if (src.startsWith(\"http://\") || src.startsWith(\"https://\")) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `ADD instruction uses a remote URL '${src}'. Remote files added via ADD cannot be cleaned up in later layers, increasing image size.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Use 'RUN curl' or 'RUN wget' instead of ADD for remote URLs, and delete the downloaded archive in the same layer to minimize size.\",\n key: \"docker-doctor/no-add-remote\",\n message: \"Avoid using ADD with remote URLs\",\n};\n\nexport const securityRules = [\n noRootUser,\n noSecretsInEnv,\n pinImageVersion,\n noAddRemote,\n];\n","import type {\n DockerfileRule,\n ComposeRule,\n RuleDefinition,\n} from \"../types/index\";\nimport { bestPracticesRules } from \"./best-practices\";\nimport { composeRules } from \"./compose\";\nimport { imageSizeRules } from \"./image-size\";\nimport { performanceRules } from \"./performance\";\nimport { securityRules } from \"./security\";\n\nexport const allDockerfileRules: DockerfileRule[] = [\n ...securityRules,\n ...performanceRules,\n ...bestPracticesRules,\n ...imageSizeRules,\n];\n\nexport const allComposeRules: ComposeRule[] = [...composeRules];\n\nexport const allRules: RuleDefinition[] = [\n ...allDockerfileRules,\n ...allComposeRules,\n];\n\nexport const findRule = (key: string): RuleDefinition | undefined =>\n allRules.find((rule) => rule.key === key);\n","import { allDockerfileRules } from \"../rules/index\";\nimport type {\n DockerfileInstruction,\n Diagnostic,\n RuleSeverity,\n} from \"../types/index\";\n\nexport const runDockerfileRules = (\n instructions: DockerfileInstruction[],\n file: string,\n projectFiles: string[],\n rulesConfig?: Record<string, RuleSeverity>\n): Diagnostic[] => {\n const diagnostics: Diagnostic[] = [];\n\n for (const rule of allDockerfileRules) {\n const configSeverity = rulesConfig?.[rule.key];\n if (configSeverity === \"off\") {\n continue;\n }\n\n const ruleDiagnostics = rule.check(instructions, file, { projectFiles });\n\n // Override severity if config specifies it\n if (configSeverity) {\n for (const diag of ruleDiagnostics) {\n diag.severity = configSeverity as \"error\" | \"warning\" | \"info\";\n }\n }\n\n diagnostics.push(...ruleDiagnostics);\n }\n\n return diagnostics;\n};\n","import { allComposeRules } from \"../rules/index\";\nimport type { Diagnostic, RuleSeverity } from \"../types/index\";\n\nexport const runComposeRules = (\n composeContent: unknown,\n file: string,\n rulesConfig?: Record<string, RuleSeverity>\n): Diagnostic[] => {\n const diagnostics: Diagnostic[] = [];\n\n for (const rule of allComposeRules) {\n const configSeverity = rulesConfig?.[rule.key];\n if (configSeverity === \"off\") {\n continue;\n }\n\n const ruleDiagnostics = rule.check(composeContent, file);\n\n // Override severity if config specifies it\n if (configSeverity) {\n for (const diag of ruleDiagnostics) {\n diag.severity = configSeverity as \"error\" | \"warning\" | \"info\";\n }\n }\n\n diagnostics.push(...ruleDiagnostics);\n }\n\n return diagnostics;\n};\n","import type {\n DockerDoctorConfig,\n RuleCategory,\n RuleSeverity,\n} from \"../types/index\";\n\nexport type { DockerDoctorConfig } from \"../types/index\";\n\nconst RULE_SEVERITIES: readonly RuleSeverity[] = [\n \"error\",\n \"warning\",\n \"info\",\n \"off\",\n];\n\nconst RULE_CATEGORIES: readonly RuleCategory[] = [\n \"Best Practices\",\n \"Compose\",\n \"Image Size\",\n \"Performance\",\n \"Security\",\n];\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isRuleSeverity = (value: unknown): value is RuleSeverity =>\n typeof value === \"string\" &&\n (RULE_SEVERITIES as readonly string[]).includes(value);\n\nconst validateRules = (value: unknown): Record<string, RuleSeverity> => {\n if (!isPlainObject(value)) {\n throw new Error(\n `Invalid config: \"rules\" must be an object, got ${typeof value}`\n );\n }\n\n for (const [key, severity] of Object.entries(value)) {\n if (!isRuleSeverity(severity)) {\n throw new Error(\n `Invalid severity ${JSON.stringify(severity)} for rule \"${key}\"`\n );\n }\n }\n\n return value as Record<string, RuleSeverity>;\n};\n\nconst validateCategories = (\n value: unknown\n): Partial<Record<RuleCategory, RuleSeverity>> => {\n if (!isPlainObject(value)) {\n throw new Error(\n `Invalid config: \"categories\" must be an object, got ${typeof value}`\n );\n }\n\n const result: Partial<Record<RuleCategory, RuleSeverity>> = {};\n for (const [key, severity] of Object.entries(value)) {\n if (!(RULE_CATEGORIES as readonly string[]).includes(key)) {\n // Unknown category keys are silently dropped (matches the\n // legacy schema's excess-property behavior).\n continue;\n }\n if (!isRuleSeverity(severity)) {\n throw new Error(\n `Invalid severity ${JSON.stringify(severity)} for category \"${key}\"`\n );\n }\n result[key as RuleCategory] = severity;\n }\n\n return result;\n};\n\nconst validateIgnore = (value: unknown): { files?: string[] } => {\n if (!isPlainObject(value)) {\n throw new Error(\n `Invalid config: \"ignore\" must be an object, got ${typeof value}`\n );\n }\n\n const result: { files?: string[] } = {};\n if (\"files\" in value && value.files !== undefined) {\n if (\n !Array.isArray(value.files) ||\n !value.files.every((item) => typeof item === \"string\")\n ) {\n throw new Error(\n 'Invalid config: \"ignore.files\" must be an array of strings'\n );\n }\n result.files = value.files;\n }\n\n return result;\n};\n\nconst describeInvalidTopLevel = (input: unknown): string => {\n if (input === null) {\n return \"null\";\n }\n if (Array.isArray(input)) {\n return \"array\";\n }\n return typeof input;\n};\n\n/**\n * Validates and normalizes a raw config object, throwing on invalid input.\n *\n * Mirrors the legacy schema's behavior exactly, including silently\n * dropping unknown top-level (and nested) keys rather than throwing\n * or preserving them.\n */\nexport const validateConfig = (input: unknown): DockerDoctorConfig => {\n if (!isPlainObject(input)) {\n throw new Error(\n `Invalid config: expected an object, got ${describeInvalidTopLevel(input)}`\n );\n }\n\n const result: DockerDoctorConfig = {};\n\n if (\"rules\" in input && input.rules !== undefined) {\n result.rules = validateRules(input.rules);\n }\n\n if (\"categories\" in input && input.categories !== undefined) {\n result.categories = validateCategories(input.categories);\n }\n\n if (\"ignore\" in input && input.ignore !== undefined) {\n result.ignore = validateIgnore(input.ignore);\n }\n\n return result;\n};\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { parse as parseYaml } from \"yaml\";\n\nimport { ConfigError } from \"../errors\";\nimport type { DockerDoctorConfig } from \"../schemas/config\";\nimport { validateConfig } from \"../schemas/config\";\n\nconst fileExists = async (filePath: string): Promise<boolean> => {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n};\n\nconst parseConfigFile = async (\n filePath: string,\n format: \"JSON\" | \"YAML\",\n parse: (content: string) => unknown\n): Promise<unknown> => {\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n return parse(content);\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n throw new ConfigError({\n message: `Failed to parse config ${format}: ${msg}`,\n });\n }\n};\n\nconst importConfig = async (filePath: string): Promise<unknown> => {\n if (filePath.endsWith(\".json\")) {\n return parseConfigFile(filePath, \"JSON\", JSON.parse);\n }\n\n if (filePath.endsWith(\".yaml\") || filePath.endsWith(\".yml\")) {\n return parseConfigFile(filePath, \"YAML\", parseYaml);\n }\n\n try {\n const configModule = await import(filePath);\n return configModule.default || configModule;\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n throw new ConfigError({\n message: `Failed to load config file ${filePath}: ${msg}`,\n });\n }\n};\n\nexport const loadConfig = async (\n rootDir: string,\n customPath?: string\n): Promise<DockerDoctorConfig> => {\n let configObject: unknown = null;\n\n if (customPath) {\n const fullPath = path.resolve(rootDir, customPath);\n if (!(await fileExists(fullPath))) {\n throw new ConfigError({\n message: `Specified config file not found at ${fullPath}`,\n });\n }\n configObject = await importConfig(fullPath);\n } else {\n const candidates = [\n \"docker-doctor.config.ts\",\n \"docker-doctor.config.js\",\n \"docker-doctor.config.mjs\",\n \"docker-doctor.config.cjs\",\n \"docker-doctor.config.json\",\n \"docker-doctor.config.yaml\",\n \"docker-doctor.config.yml\",\n ];\n\n /* eslint-disable no-await-in-loop */\n for (const cand of candidates) {\n const fullPath = path.join(rootDir, cand);\n if (await fileExists(fullPath)) {\n configObject = await importConfig(fullPath);\n break;\n }\n }\n /* eslint-enable no-await-in-loop */\n\n if (!configObject) {\n const pkgPath = path.join(rootDir, \"package.json\");\n if (await fileExists(pkgPath)) {\n try {\n const pkgContent = await fs.readFile(pkgPath, \"utf-8\");\n const pkgJson = JSON.parse(pkgContent);\n if (pkgJson.dockerDoctor) {\n configObject = pkgJson.dockerDoctor;\n }\n } catch {\n // ignore package.json read/parse failures\n }\n }\n }\n }\n\n if (!configObject) {\n return {};\n }\n\n try {\n return validateConfig(configObject);\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n throw new ConfigError({\n message: `Invalid configuration format: ${msg}`,\n });\n }\n};\n","import type { Diagnostic } from \"./types/index\";\n\nexport const SCORE_BUCKETS = [\n { emoji: \"🏆\", label: \"Excellent\", min: 90 },\n { emoji: \"✅\", label: \"Good\", min: 75 },\n { emoji: \"⚠️\", label: \"Needs Work\", min: 50 },\n { emoji: \"🚨\", label: \"Critical\", min: 0 },\n] as const;\n\nexport const getScoreBucket = (\n score: number\n): (typeof SCORE_BUCKETS)[number] => {\n for (const bucket of SCORE_BUCKETS) {\n if (score >= bucket.min) {\n return bucket;\n }\n }\n return SCORE_BUCKETS.at(-1) as (typeof SCORE_BUCKETS)[number];\n};\n\nexport const calculateScore = (\n diagnostics: Diagnostic[]\n): {\n score: number;\n label: string;\n} => {\n let penalty = 0;\n\n for (const diag of diagnostics) {\n if (diag.severity === \"error\") {\n penalty += 10;\n } else if (diag.severity === \"warning\") {\n penalty += 4;\n } else if (diag.severity === \"info\") {\n penalty += 1;\n }\n }\n\n // Asymptotic decay curve: score = round(100 * e^(-penalty / K)).\n //\n // The old `max(0, 100 - penalty)` formula saturates at 0 once penalty\n // reaches 100 (e.g. ~10 errors), so a messy project and a catastrophic\n // one are indistinguishable and the score can never register a fix.\n // This curve approaches (but never reaches) 0, so it stays monotonic\n // and responsive across the whole range instead of going inert.\n //\n // K=70 was chosen so a single warning (penalty 4) still scores ~94,\n // comfortably inside the \"Excellent\" (>=90) bucket, while errors and\n // repeated warnings still meaningfully erode the score. K=40 (the\n // naive \"half-life at penalty ~28\" choice) was tried first and pushed\n // a single warning down to ~90 - right on the Excellent/Good boundary,\n // which is too harsh a penalty for one warning.\n const K = 70;\n const score = Math.round(100 * Math.exp(-penalty / K));\n const bucket = getScoreBucket(score);\n const label = `${bucket.label} ${bucket.emoji}`;\n\n return { label, score };\n};\n","import type { Diagnostic, ProjectInfo } from \"./types/index\";\n\n// Bump whenever the JSON report shape or the score formula/weights change.\n// The unversioned shape shipped before this field existed is implicitly 1.\nexport const REPORT_SCHEMA_VERSION = 2;\n\nexport interface JsonReport {\n diagnostics: {\n column?: number;\n file: string;\n help: string;\n line?: number;\n message: string;\n rule: string;\n severity: \"error\" | \"warning\" | \"info\";\n }[];\n label: string;\n project: ProjectInfo;\n schemaVersion: number;\n score: number;\n timestamp: string;\n}\n\nexport const toJsonReport = (\n diagnostics: Diagnostic[],\n score: number,\n label: string,\n project: ProjectInfo\n): JsonReport => ({\n diagnostics: diagnostics.map((d) => ({\n column: d.column,\n file: d.file,\n help: d.help,\n line: d.line,\n message: d.message,\n rule: d.rule,\n severity: d.severity,\n })),\n label,\n project,\n schemaVersion: REPORT_SCHEMA_VERSION,\n score,\n timestamp: new Date().toISOString(),\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"src-BuADwzRj.mjs","names":["createDiagnostic","createDiagnostic","createDiagnostic","createDiagnostic","parseYaml"],"sources":["../package.json","../../core/src/project-info/discover.ts","../../core/src/parsers/dockerfile-parser.ts","../../core/src/errors/config-error.ts","../../core/src/errors/parse-error.ts","../../core/src/parsers/compose-parser.ts","../../core/src/rules/best-practices.ts","../../core/src/rules/compose.ts","../../core/src/parsers/image-ref.ts","../../core/src/rules/image-size.ts","../../core/src/rules/performance.ts","../../core/src/rules/security.ts","../../core/src/rules/index.ts","../../core/src/runners/dockerfile-runner.ts","../../core/src/runners/compose-runner.ts","../../core/src/schemas/config.ts","../../core/src/config/loader.ts","../../core/src/scoring.ts","../../core/src/report.ts"],"sourcesContent":["","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { ProjectInfo } from \"../types/index\";\n\nconst walk = async (\n dir: string,\n fileList: string[] = []\n): Promise<string[]> => {\n const files = await fs.readdir(dir, { withFileTypes: true });\n await Promise.all(\n files.map(async (file) => {\n const filePath = path.join(dir, file.name);\n if (file.isDirectory()) {\n if (\n file.name === \"node_modules\" ||\n file.name === \".git\" ||\n file.name === \".next\" ||\n file.name === \"dist\" ||\n file.name === \".turbo\"\n ) {\n return;\n }\n await walk(filePath, fileList);\n } else {\n fileList.push(filePath);\n }\n })\n );\n return fileList;\n};\n\nexport const discoverProject = async (\n rootDir: string\n): Promise<ProjectInfo> => {\n const allFiles = await walk(rootDir);\n const dockerfiles: string[] = [];\n const composeFiles: string[] = [];\n const dockerignores: string[] = [];\n\n for (const file of allFiles) {\n const base = path.basename(file).toLowerCase();\n\n if (base === \".dockerignore\") {\n dockerignores.push(path.relative(rootDir, file));\n }\n\n // Match Dockerfile, Dockerfile.*, *.dockerfile\n if (\n base === \"dockerfile\" ||\n base.startsWith(\"dockerfile.\") ||\n base.endsWith(\".dockerfile\")\n ) {\n dockerfiles.push(path.relative(rootDir, file));\n }\n\n // Match docker-compose.yml, docker-compose.*.yml, compose.yml, compose.*.yml, and yaml extensions\n if (\n base === \"docker-compose.yml\" ||\n base === \"docker-compose.yaml\" ||\n base === \"compose.yml\" ||\n base === \"compose.yaml\" ||\n ((base.startsWith(\"docker-compose.\") || base.startsWith(\"compose.\")) &&\n (base.endsWith(\".yml\") || base.endsWith(\".yaml\")))\n ) {\n composeFiles.push(path.relative(rootDir, file));\n }\n }\n\n return {\n composeFiles,\n dockerfiles,\n dockerignores,\n };\n};\n","import type { DockerfileInstruction } from \"../types/index\";\n\nconst DOCKERFILE_KEYWORDS = new Set([\n \"ADD\",\n \"ARG\",\n \"CMD\",\n \"COPY\",\n \"ENTRYPOINT\",\n \"ENV\",\n \"EXPOSE\",\n \"FROM\",\n \"HEALTHCHECK\",\n \"LABEL\",\n \"MAINTAINER\",\n \"ONBUILD\",\n \"RUN\",\n \"SHELL\",\n \"STOPSIGNAL\",\n \"USER\",\n \"VOLUME\",\n \"WORKDIR\",\n]);\n\nconst INSTRUCTION_LINE_RE = /^(?<inst>[A-Za-z]+)\\s+(?<args>.*)$/u;\n\n// Matches a heredoc opener like <<EOF, <<-EOF, <<'EOF', <<\"EOF\". Global so a\n// single line (e.g. `COPY <<FILE1 <<FILE2 /dest/`) can open more than one.\nconst HEREDOC_OPENER_RE = /<<-?\\s*(?<quote>['\"]?)(?<delim>\\w+)\\k<quote>/gu;\n\ninterface ParserState {\n instructions: DockerfileInstruction[];\n currentInstruction: string;\n currentArgs: string;\n startLine: number;\n rawAccumulator: string[];\n // FIFO of heredoc delimiters still open for the current instruction, in\n // the order they were opened (closed in the same order).\n heredocQueue: string[];\n}\n\nconst createParserState = (): ParserState => ({\n currentArgs: \"\",\n currentInstruction: \"\",\n heredocQueue: [],\n instructions: [],\n rawAccumulator: [],\n startLine: 0,\n});\n\nconst closeInstruction = (state: ParserState): void => {\n if (state.currentInstruction) {\n state.instructions.push({\n args: state.currentArgs,\n instruction: state.currentInstruction,\n line: state.startLine,\n raw: state.rawAccumulator.join(\"\\n\"),\n });\n }\n state.currentInstruction = \"\";\n state.currentArgs = \"\";\n state.rawAccumulator = [];\n};\n\nconst matchInstructionKeyword = (\n lineContent: string\n): { instruction: string; args: string } | null => {\n const match = lineContent.match(INSTRUCTION_LINE_RE);\n const matchedWord = match?.groups?.inst.toUpperCase();\n if (matchedWord && DOCKERFILE_KEYWORDS.has(matchedWord)) {\n return { args: match?.groups?.args ?? \"\", instruction: matchedWord };\n }\n\n const word = lineContent.trim().toUpperCase();\n if (DOCKERFILE_KEYWORDS.has(word)) {\n return { args: \"\", instruction: word };\n }\n\n return null;\n};\n\nconst findHeredocDelimiters = (lineContent: string): string[] =>\n [...lineContent.matchAll(HEREDOC_OPENER_RE)].map(\n (m) => m.groups?.delim ?? \"\"\n );\n\n// Heredoc body lines are never treated as instructions (not even comment\n// lines, which are shell-comment content here) — they are folded verbatim\n// into the owning instruction's args until the delimiter line closes the\n// (possibly multiple) open heredoc(s), in the order they were opened.\nconst processHeredocLine = (state: ParserState, trimmed: string): void => {\n if (trimmed === state.heredocQueue[0]) {\n state.heredocQueue.shift();\n } else {\n state.currentArgs += (state.currentArgs ? \" \" : \"\") + trimmed;\n }\n\n if (state.heredocQueue.length === 0) {\n // Last open heredoc just closed: the instruction ends here.\n closeInstruction(state);\n }\n};\n\nconst processInstructionLine = (\n state: ParserState,\n trimmed: string,\n lineNum: number\n): void => {\n let lineContent = trimmed;\n\n // Inside a multi-line run, comment lines are ignored by docker parser\n if (lineContent.startsWith(\"#\")) {\n return;\n }\n\n const hasContinuation = lineContent.endsWith(\"\\\\\");\n if (hasContinuation) {\n lineContent = lineContent.slice(0, -1).trim();\n }\n\n if (state.currentInstruction) {\n state.currentArgs += (state.currentArgs ? \" \" : \"\") + lineContent;\n } else {\n state.startLine = lineNum;\n // Match the first instruction word (e.g. FROM, RUN, COPY)\n const matched = matchInstructionKeyword(lineContent);\n if (matched) {\n state.currentInstruction = matched.instruction;\n state.currentArgs = matched.args;\n }\n }\n\n if (state.currentInstruction) {\n state.heredocQueue.push(...findHeredocDelimiters(lineContent));\n }\n\n if (state.heredocQueue.length > 0) {\n // A heredoc opener implies continuation even without a trailing\n // backslash — keep accumulating until every opened delimiter closes.\n return;\n }\n\n if (!hasContinuation) {\n closeInstruction(state);\n }\n};\n\nexport const parseDockerfile = (content: string): DockerfileInstruction[] => {\n const state = createParserState();\n const lines = content.split(/\\r?\\n/u);\n\n for (const [i, rawLine] of lines.entries()) {\n const trimmed = rawLine.trim();\n const lineNum = i + 1;\n const insideHeredoc = state.heredocQueue.length > 0;\n\n // Skip empty lines or comment lines if not in multi-line block\n if (\n !state.currentInstruction &&\n !insideHeredoc &&\n (trimmed === \"\" || trimmed.startsWith(\"#\"))\n ) {\n continue;\n }\n\n state.rawAccumulator.push(rawLine);\n\n if (insideHeredoc) {\n processHeredocLine(state, trimmed);\n } else {\n processInstructionLine(state, trimmed, lineNum);\n }\n }\n\n // EOF: an unterminated heredoc (or a trailing backslash continuation)\n // still emits whatever was accumulated so far, rather than dropping it.\n closeInstruction(state);\n\n return state.instructions;\n};\n","export class ConfigError extends Error {\n readonly _tag = \"ConfigError\" as const;\n\n constructor(options: { readonly message: string }) {\n super(options.message);\n this.name = \"ConfigError\";\n }\n}\n","export class ParseError extends Error {\n readonly _tag = \"ParseError\" as const;\n readonly file: string;\n\n constructor(options: { readonly file: string; readonly message: string }) {\n super(options.message);\n this.name = \"ParseError\";\n this.file = options.file;\n }\n}\n","import { parse } from \"yaml\";\n\nimport { ParseError } from \"../errors\";\n\nexport const parseCompose = (content: string, filepath: string): unknown => {\n try {\n return parse(content);\n } catch (error: unknown) {\n throw new ParseError({\n file: filepath,\n message: error instanceof Error ? error.message : String(error),\n });\n }\n};\n","import type { Diagnostic, DockerfileRule } from \"../types/index\";\n\nconst createDiagnostic = (\n file: string,\n ruleKey: string,\n severity: \"error\" | \"warning\" | \"info\",\n message: string,\n help: string,\n line?: number\n): Diagnostic => ({ file, help, line, message, rule: ruleKey, severity });\n\nexport const requireHealthcheck: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const hasHealthcheck = instructions.some(\n (inst) => inst.instruction === \"HEALTHCHECK\"\n );\n\n // Only suggest healthcheck if it has exposed ports or command lines indicating it runs an app\n const hasExposedPortsOrEntry = instructions.some(\n (inst) =>\n inst.instruction === \"EXPOSE\" ||\n inst.instruction === \"CMD\" ||\n inst.instruction === \"ENTRYPOINT\"\n );\n\n if (!hasHealthcheck && hasExposedPortsOrEntry) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"No HEALTHCHECK instruction found. Containers running services should expose healthchecks to enable auto-healing.\",\n this.help,\n 1\n ),\n ];\n }\n return [];\n },\n defaultSeverity: \"info\",\n help: \"Use HEALTHCHECK (e.g., 'HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/ || exit 1') so Docker can monitor the container's live status.\",\n key: \"docker-doctor/require-healthcheck\",\n message: \"Add a HEALTHCHECK instruction\",\n};\n\nexport const preferCopyOverAdd: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n if (inst.instruction === \"ADD\") {\n const parts = inst.args.split(/\\s+/u);\n const src = parts.find((p) => !p.startsWith(\"--\"));\n\n if (!src) {\n continue;\n }\n\n // If it's not a remote url (handled by security/no-add-remote) and not a compressed file\n const isRemote =\n src.startsWith(\"http://\") || src.startsWith(\"https://\");\n const isArchive =\n src.endsWith(\".tar\") ||\n src.endsWith(\".tar.gz\") ||\n src.endsWith(\".tgz\") ||\n src.endsWith(\".zip\");\n\n if (!isRemote && !isArchive) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `ADD instruction used for regular files: '${inst.args}'. COPY is simpler and less prone to magic side effects.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Use COPY instead of ADD unless you explicitly need auto-extraction of local compressed archives (tar, zip, etc.).\",\n key: \"docker-doctor/prefer-copy-over-add\",\n message: \"Prefer COPY over ADD\",\n};\n\nexport const useExecForm: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n if (inst.instruction === \"CMD\" || inst.instruction === \"ENTRYPOINT\") {\n const args = inst.args.trim();\n // If it does not start with [ and end with ]\n if (!args.startsWith(\"[\") || !args.endsWith(\"]\")) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `${inst.instruction} instruction uses shell form instead of exec form. In shell form, the command runs under '/bin/sh -c', which does not pass signals to child processes.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: 'Write CMD/ENTRYPOINT instructions as JSON arrays (e.g. ENTRYPOINT [\"node\", \"index.js\"]) so OS signals (like SIGTERM) are forwarded correctly.',\n key: \"docker-doctor/use-exec-form\",\n message: \"Use exec form for CMD and ENTRYPOINT\",\n};\n\nexport const requireLabels: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const hasLabel = instructions.some((inst) => inst.instruction === \"LABEL\");\n if (!hasLabel) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"No LABEL metadata was found in this Dockerfile. Adding labels helps identify build information, maintainers, and descriptions.\",\n this.help,\n 1\n ),\n ];\n }\n return [];\n },\n defaultSeverity: \"info\",\n help: 'Use LABEL instructions (e.g. LABEL org.opencontainers.image.authors=\"...\") to document ownership, license, version, and build info.',\n key: \"docker-doctor/require-labels\",\n message: \"Add LABEL metadata to images\",\n};\n\nexport const combineAptUpdateInstall: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n const hasUpdate = inst.args.includes(\"apt-get update\");\n const hasInstall = inst.args.includes(\"apt-get install\");\n\n if (hasUpdate && !hasInstall) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"RUN apt-get update used without apt-get install in the same instruction. This can cause caching issues and build failures.\",\n this.help,\n inst.line\n )\n );\n } else if (hasInstall && !hasUpdate) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"RUN apt-get install used without apt-get update in the same instruction. Always combine them to ensure up-to-date package installation.\",\n this.help,\n inst.line\n )\n );\n }\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Combine 'apt-get update' and 'apt-get install' in the same RUN instruction (e.g. 'RUN apt-get update && apt-get install -y --no-install-recommends <package> && rm -rf /var/lib/apt/lists/*').\",\n key: \"docker-doctor/combine-apt-update-install\",\n message: \"Combine apt-get update and apt-get install\",\n};\n\nexport const usePipefail: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n const { raw } = inst;\n const hasPipe = /(?<!\\|)\\|(?!\\|)/u.test(raw);\n if (hasPipe && !raw.includes(\"pipefail\")) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"RUN instruction uses a pipe (|) but does not configure 'pipefail'. If a command in the pipe fails, the step may still succeed silently.\",\n this.help,\n inst.line\n )\n );\n }\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Prepend 'set -o pipefail &&' to pipe commands, or use exec form with a shell that supports it (e.g., RUN ['/bin/bash', '-c', 'set -o pipefail && ...']).\",\n key: \"docker-doctor/use-pipefail\",\n message: \"Use pipefail to catch pipeline command failures\",\n};\n\nexport const absoluteWorkdir: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"WORKDIR\") {\n const path = inst.args.trim();\n const isAbsolute = /^(?:\\/|\\\\|\\$|[a-zA-Z]:)/u.test(path);\n\n if (!isAbsolute) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `WORKDIR specifies a relative path '${path}'. For clarity and reliability, always use absolute paths.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Always specify absolute paths for WORKDIR instructions (e.g. WORKDIR /app).\",\n key: \"docker-doctor/absolute-workdir\",\n message: \"Use absolute paths for WORKDIR\",\n};\n\nexport const avoidRunCd: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\" && /\\bcd\\b/u.test(inst.args)) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"Avoid using 'cd' in RUN instructions. Use WORKDIR instead to change the working directory stably across layers.\",\n this.help,\n inst.line\n )\n );\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Use the WORKDIR instruction instead of 'cd' inside RUN to establish directory context.\",\n key: \"docker-doctor/avoid-run-cd\",\n message: \"Avoid changing directories with cd in RUN\",\n};\n\nexport const sortMultilineArgs: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n const { raw } = inst;\n const isPackageInstall =\n raw.includes(\"apt-get install\") ||\n raw.includes(\"apk add\") ||\n raw.includes(\"yum install\") ||\n raw.includes(\"dnf install\");\n\n const hasContinuation = raw.includes(\"\\\\\\n\") || raw.includes(\"\\\\\\r\\n\");\n\n if (isPackageInstall && hasContinuation) {\n const lines = raw.split(/\\r?\\n/u);\n const packages = lines\n .slice(1)\n .map((line) => line.trim())\n .filter(\n (line) =>\n line !== \"\" &&\n !line.startsWith(\"&&\") &&\n !line.startsWith(\"-\") &&\n !line.includes(\"rm -rf\")\n )\n .map((line) =>\n line.endsWith(\"\\\\\") ? line.slice(0, -1).trim() : line\n )\n .filter(Boolean);\n\n if (packages.length > 1) {\n const sorted = packages.toSorted((a, b) => a.localeCompare(b));\n const isSorted = packages.every((val, idx) => val === sorted[idx]);\n if (!isSorted) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"Multi-line package arguments are not sorted alphanumerically. Keeping them sorted makes maintenance easier and prevents duplicates.\",\n this.help,\n inst.line\n )\n );\n }\n }\n }\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Sort multi-line package installation lists (e.g. apk/apt package lists) alphabetically.\",\n key: \"docker-doctor/sort-multiline-args\",\n message: \"Sort multi-line arguments alphanumerically\",\n};\n\nexport const useraddNoLogInit: DockerfileRule = {\n category: \"Best Practices\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n for (const inst of instructions) {\n if (\n inst.instruction === \"RUN\" &&\n /\\buseradd\\b/u.test(inst.args) &&\n !inst.args.includes(\"--no-log-init\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"RUN instruction runs 'useradd' without '--no-log-init'. This can cause excessive disk space usage / exhaustion under Go's sparse tar archive bug when large UIDs are used.\",\n this.help,\n inst.line\n )\n );\n }\n }\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Pass '--no-log-init' flag to useradd (e.g., 'RUN useradd --no-log-init -r -g mygroup myuser').\",\n key: \"docker-doctor/useradd-no-log-init\",\n message: \"Use --no-log-init with useradd\",\n};\n\nexport const bestPracticesRules = [\n requireHealthcheck,\n preferCopyOverAdd,\n useExecForm,\n requireLabels,\n combineAptUpdateInstall,\n usePipefail,\n absoluteWorkdir,\n avoidRunCd,\n sortMultilineArgs,\n useraddNoLogInit,\n];\n","import type { Diagnostic, ComposeRule } from \"../types/index\";\n\nconst createDiagnostic = (\n file: string,\n ruleKey: string,\n severity: \"error\" | \"warning\" | \"info\",\n message: string,\n help: string\n): Diagnostic => ({ file, help, message, rule: ruleKey, severity });\n\nexport const noVersionKey: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file) {\n if (\n composeContent &&\n typeof composeContent === \"object\" &&\n \"version\" in composeContent\n ) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"The 'version' property is deprecated. Remove it to use standard Compose spec behavior.\",\n this.help\n ),\n ];\n }\n return [];\n },\n defaultSeverity: \"warning\",\n help: \"The 'version' key is deprecated by the Compose specification. Omitting it defaults to the latest specification.\",\n key: \"docker-doctor/no-version-key\",\n message: \"Remove the 'version' key from Compose file\",\n};\n\nexport const requireResourceLimits: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file) {\n const diagnostics: Diagnostic[] = [];\n\n if (\n composeContent &&\n typeof composeContent === \"object\" &&\n \"services\" in composeContent\n ) {\n const { services } = composeContent;\n if (services && typeof services === \"object\") {\n for (const [name, config] of Object.entries(services)) {\n if (config && typeof config === \"object\") {\n const deploy = (config as Record<string, unknown>).deploy as\n | Record<string, unknown>\n | undefined;\n const resources = deploy?.resources as\n | Record<string, unknown>\n | undefined;\n const limits = resources?.limits as\n | Record<string, unknown>\n | undefined;\n\n if (!limits || (!limits.cpus && !limits.memory)) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Service '${name}' does not have CPU or memory limits defined. A resource leak in this service could crash the host.`,\n this.help\n )\n );\n }\n }\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Add resource limits (e.g. deploy.resources.limits) to prevent a single service from starving host resources in production.\",\n key: \"docker-doctor/require-resource-limits\",\n message: \"Define resource limits for services\",\n};\n\nexport const requireRestartPolicy: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file) {\n const diagnostics: Diagnostic[] = [];\n\n if (\n composeContent &&\n typeof composeContent === \"object\" &&\n \"services\" in composeContent\n ) {\n const { services } = composeContent;\n if (services && typeof services === \"object\") {\n for (const [name, config] of Object.entries(services)) {\n if (config && typeof config === \"object\") {\n const hasRestart = \"restart\" in config;\n const deploy = (config as Record<string, unknown>).deploy as\n | Record<string, unknown>\n | undefined;\n const hasDeployRestart = deploy?.restart_policy !== undefined;\n\n if (!hasRestart && !hasDeployRestart) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Service '${name}' has no restart policy configured. It will not restart if it crashes or if the host reboots.`,\n this.help\n )\n );\n }\n }\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Define 'restart: always' or 'restart: unless-stopped' (or deploy.restart_policy) so services restart on crashes or host reboot.\",\n key: \"docker-doctor/require-restart-policy\",\n message: \"Set restart policy for services\",\n};\n\nexport const useDependsOnCondition: ComposeRule = {\n category: \"Compose\",\n check(composeContent, file) {\n const diagnostics: Diagnostic[] = [];\n\n if (\n composeContent &&\n typeof composeContent === \"object\" &&\n \"services\" in composeContent\n ) {\n const { services } = composeContent;\n if (services && typeof services === \"object\") {\n for (const [name, config] of Object.entries(services)) {\n if (config && typeof config === \"object\") {\n const dependsOn = (config as Record<string, unknown>).depends_on;\n if (dependsOn && Array.isArray(dependsOn)) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Service '${name}' uses shorthand depends_on list. This only checks if containers are started, not if they are ready/healthy.`,\n this.help\n )\n );\n }\n }\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Instead of a simple service list, use 'depends_on: { dependency: { condition: service_healthy } }' to ensure dependencies are fully ready before starting.\",\n key: \"docker-doctor/use-depends-on-condition\",\n message: \"Use long-form depends_on with healthcheck conditions\",\n};\n\nexport const composeRules = [\n noVersionKey,\n requireResourceLimits,\n requireRestartPolicy,\n useDependsOnCondition,\n];\n","import type { DockerfileInstruction } from \"../types/index\";\n\nexport interface ImageRef {\n registry?: string;\n name: string;\n tag?: string;\n digest?: string;\n isVariable: boolean;\n}\n\nexport const parseImageRef = (ref: string): ImageRef => {\n if (ref.includes(\"${\") || ref.startsWith(\"$\")) {\n return { isVariable: true, name: ref };\n }\n\n let remainder = ref;\n let digest: string | undefined;\n\n const atIndex = remainder.indexOf(\"@\");\n if (atIndex !== -1) {\n digest = remainder.slice(atIndex + 1);\n remainder = remainder.slice(0, atIndex);\n }\n\n let tag: string | undefined;\n const lastColonIndex = remainder.lastIndexOf(\":\");\n const lastSlashIndex = remainder.lastIndexOf(\"/\");\n\n if (lastColonIndex !== -1 && lastColonIndex > lastSlashIndex) {\n tag = remainder.slice(lastColonIndex + 1);\n remainder = remainder.slice(0, lastColonIndex);\n }\n\n let registry: string | undefined;\n const firstSlashIndex = remainder.indexOf(\"/\");\n if (firstSlashIndex !== -1) {\n const firstSegment = remainder.slice(0, firstSlashIndex);\n if (\n firstSegment.includes(\".\") ||\n firstSegment.includes(\":\") ||\n firstSegment === \"localhost\"\n ) {\n registry = firstSegment;\n remainder = remainder.slice(firstSlashIndex + 1);\n }\n }\n\n return {\n digest,\n isVariable: false,\n name: remainder,\n registry,\n tag,\n };\n};\n\nexport const collectStageAliases = (\n instructions: DockerfileInstruction[]\n): Set<string> => {\n const aliases = new Set<string>();\n\n for (const inst of instructions) {\n if (inst.instruction !== \"FROM\") {\n continue;\n }\n\n const match = /\\sas\\s+(?<alias>\\S+)/iu.exec(inst.args);\n if (match?.groups?.alias) {\n aliases.add(match.groups.alias.toLowerCase());\n }\n }\n\n return aliases;\n};\n","import { collectStageAliases, parseImageRef } from \"../parsers/image-ref\";\nimport type { Diagnostic, DockerfileRule } from \"../types/index\";\n\nconst createDiagnostic = (\n file: string,\n ruleKey: string,\n severity: \"error\" | \"warning\" | \"info\",\n message: string,\n help: string,\n line?: number\n): Diagnostic => ({ file, help, line, message, rule: ruleKey, severity });\n\nexport const preferSlimBase: DockerfileRule = {\n category: \"Image Size\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n const stageAliases = collectStageAliases(instructions);\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n const parts = inst.args.split(/\\s+/u);\n const imagePart = parts.find((p) => !p.startsWith(\"--\"));\n if (!imagePart || imagePart === \"scratch\") {\n continue;\n }\n\n const ref = parseImageRef(imagePart);\n\n if (ref.isVariable || stageAliases.has(imagePart.toLowerCase())) {\n continue;\n }\n\n // Digest pins are already fully deterministic; not our concern here.\n if (ref.digest) {\n continue;\n }\n\n // No tag: pin-image-version owns the untagged case, don't double-report.\n if (!ref.tag) {\n continue;\n }\n\n const tag = ref.tag.toLowerCase();\n const isSlim =\n tag.includes(\"alpine\") ||\n tag.includes(\"slim\") ||\n tag.includes(\"distroless\");\n\n if (!isSlim) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Base image '${imagePart}' may be a full-OS distribution. Consider using a slim or alpine alternative.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Prefer tags with '-slim', '-alpine', or use distroless base images to minimize the default operating system footprint.\",\n key: \"docker-doctor/prefer-slim-base\",\n message: \"Use slim, alpine, or distroless base images\",\n};\n\nexport const cleanPackageCache: DockerfileRule = {\n category: \"Image Size\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n const { args } = inst;\n\n // check apt-get install without cleanup\n if (\n args.includes(\"apt-get install\") &&\n !args.includes(\"rm -rf /var/lib/apt/lists\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Running 'apt-get install' without removing package lists afterwards. This keeps metadata caches inside the image layer.`,\n this.help,\n inst.line\n )\n );\n }\n\n // check apk add without --no-cache\n if (\n args.includes(\"apk add\") &&\n !args.includes(\"--no-cache\") &&\n !args.includes(\"rm -rf /var/cache/apk\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Running 'apk add' without '--no-cache' or cleaning the apk cache. This increases layer size.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"For apt-get, append '&& rm -rf /var/lib/apt/lists/*'. For apk, use 'apk add --no-cache'. For dnf/yum, run 'yum clean all'.\",\n key: \"docker-doctor/clean-package-cache\",\n message: \"Clean up package manager cache in the same RUN layer\",\n};\n\nexport const avoidDevDependencies: DockerfileRule = {\n category: \"Image Size\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n let isLastStage = false;\n let fromCount = 0;\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n fromCount += 1;\n }\n }\n\n let currentStage = 0;\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n currentStage += 1;\n isLastStage = currentStage === fromCount;\n }\n\n if (isLastStage && inst.instruction === \"RUN\") {\n const { args } = inst;\n if (\n (args.includes(\"npm install\") ||\n args.includes(\"npm ci\") ||\n args.includes(\"yarn install\")) &&\n !args.includes(\"--production\") &&\n !args.includes(\"--omit=dev\") &&\n !args.includes(\"prune\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Running package install '${inst.args}' in the final stage without omitting devDependencies.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"For Node.js, run 'npm prune --production' or install only production dependencies ('npm ci --omit=dev') in the runtime stage.\",\n key: \"docker-doctor/avoid-dev-dependencies\",\n message:\n \"Avoid installing development dependencies in final production stage\",\n};\n\nexport const imageSizeRules = [\n preferSlimBase,\n cleanPackageCache,\n avoidDevDependencies,\n];\n","import type { Diagnostic, DockerfileRule } from \"../types/index\";\n\nconst createDiagnostic = (\n file: string,\n ruleKey: string,\n severity: \"error\" | \"warning\" | \"info\",\n message: string,\n help: string,\n line?: number\n): Diagnostic => ({ file, help, line, message, rule: ruleKey, severity });\n\nexport const useMultiStage: DockerfileRule = {\n category: \"Performance\",\n check(instructions, file) {\n const fromCount = instructions.filter(\n (inst) => inst.instruction === \"FROM\"\n ).length;\n if (fromCount === 1) {\n // Check if it's not a trivial/short Dockerfile (e.g., has some build steps)\n const hasBuildSteps = instructions.some(\n (inst) =>\n inst.instruction === \"RUN\" &&\n (inst.args.includes(\"npm run build\") ||\n inst.args.includes(\"yarn build\") ||\n inst.args.includes(\"bun run build\") ||\n inst.args.includes(\"cargo build\") ||\n inst.args.includes(\"make\"))\n );\n\n if (hasBuildSteps) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"Only one build stage (FROM) was detected, but build instructions were found. Multi-stage builds can significantly reduce final image size.\",\n this.help,\n instructions.find((inst) => inst.instruction === \"FROM\")?.line || 1\n ),\n ];\n }\n }\n return [];\n },\n defaultSeverity: \"info\",\n help: \"Use multi-stage builds (multiple FROM statements) to separate build dependencies from the runtime image and reduce size.\",\n key: \"docker-doctor/use-multi-stage\",\n message: \"Consider using multi-stage builds\",\n};\n\nexport const orderLayers: DockerfileRule = {\n category: \"Performance\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n let copyAllLine = -1;\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n copyAllLine = -1;\n }\n\n if (inst.instruction === \"COPY\" || inst.instruction === \"ADD\") {\n const parts = inst.args.split(/\\s+/u);\n const src = parts.find((p) => !p.startsWith(\"--\"));\n\n if (!src) {\n continue;\n }\n\n const normalized = src.replace(/^\\.\\//u, \"\");\n const isCopyAll =\n normalized === \".\" ||\n normalized === \"\" ||\n normalized === \"*\" ||\n normalized === \"src\" ||\n normalized.startsWith(\"src/\");\n\n if (isCopyAll && copyAllLine === -1) {\n copyAllLine = inst.line;\n }\n }\n\n if (inst.instruction === \"RUN\" && copyAllLine !== -1) {\n const args = inst.args.toLowerCase();\n if (\n args.includes(\"npm install\") ||\n args.includes(\"npm ci\") ||\n args.includes(\"yarn install\") ||\n args.includes(\"bun install\") ||\n args.includes(\"pip install\") ||\n args.includes(\"cargo fetch\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Running package installation command '${inst.args}' after copying application files (at line ${copyAllLine}). This invalidates the cache on any code changes.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Copy dependency definition files (like package.json, lockfiles) and run install commands BEFORE copying the rest of the application source code.\",\n key: \"docker-doctor/order-layers\",\n message: \"Order layers to maximize build cache utility\",\n};\n\nexport const minimizeLayers: DockerfileRule = {\n category: \"Performance\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n let consecutiveRunCount = 0;\n let firstRunLine = -1;\n\n for (const inst of instructions) {\n if (inst.instruction === \"RUN\") {\n if (consecutiveRunCount === 0) {\n firstRunLine = inst.line;\n }\n consecutiveRunCount += 1;\n } else {\n if (consecutiveRunCount > 2) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`,\n this.help,\n firstRunLine\n )\n );\n }\n consecutiveRunCount = 0;\n }\n }\n\n if (consecutiveRunCount > 2) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`,\n this.help,\n firstRunLine\n )\n );\n }\n\n return diagnostics;\n },\n defaultSeverity: \"info\",\n help: \"Combine consecutive RUN instructions using '&&' and '\\\\' to reduce the total layer count and image size.\",\n key: \"docker-doctor/minimize-layers\",\n message: \"Minimize the number of image layers\",\n};\n\nexport const useDockerignore: DockerfileRule = {\n category: \"Performance\",\n check(instructions, file, context) {\n // If copying everything, we definitely need .dockerignore\n const hasCopyAll = instructions.some((inst) => {\n if (inst.instruction === \"COPY\" || inst.instruction === \"ADD\") {\n // COPY --from=<stage> reads from a previous build stage, not the\n // build context, so .dockerignore is irrelevant to it.\n const isStageCopy = /(?:^|\\s)--from=/u.test(inst.args);\n if (isStageCopy) {\n return false;\n }\n\n const parts = inst.args.split(/\\s+/u);\n const src = parts.find((p) => !p.startsWith(\"--\"));\n if (!src) {\n return false;\n }\n return src === \".\" || src === \"./\" || src === \"*\";\n }\n return false;\n });\n\n if (hasCopyAll && context?.projectFiles) {\n const hasDockerignore = context.projectFiles.some((f) =>\n f.endsWith(\".dockerignore\")\n );\n if (!hasDockerignore) {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"Using COPY/ADD with wildcard/directory, but no .dockerignore file was found in the workspace. This can copy local build folders and secrets.\",\n this.help,\n 1\n ),\n ];\n }\n }\n return [];\n },\n defaultSeverity: \"warning\",\n help: \"Create a .dockerignore file in the same directory as the Dockerfile to prevent copying unnecessary files (like node_modules, logs, build artifacts).\",\n key: \"docker-doctor/use-dockerignore\",\n message: \"Ensure .dockerignore is used\",\n};\n\nexport const performanceRules = [\n useMultiStage,\n orderLayers,\n minimizeLayers,\n useDockerignore,\n];\n","import { collectStageAliases, parseImageRef } from \"../parsers/image-ref\";\nimport type { Diagnostic, DockerfileRule } from \"../types/index\";\n\nconst createDiagnostic = (\n file: string,\n ruleKey: string,\n severity: \"error\" | \"warning\" | \"info\",\n message: string,\n help: string,\n line?: number\n): Diagnostic => ({ file, help, line, message, rule: ruleKey, severity });\n\nexport const noRootUser: DockerfileRule = {\n category: \"Security\",\n check(instructions, file) {\n let lastUser = \"root\";\n let lastUserLine = 1;\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n lastUser = \"root\";\n lastUserLine = inst.line;\n } else if (inst.instruction === \"USER\") {\n lastUser = inst.args.trim().toLowerCase();\n lastUserLine = inst.line;\n }\n }\n\n if (lastUser === \"root\" || lastUser === \"0\" || lastUser === \"0:0\") {\n return [\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n \"The container runs as root. Running as root allows potential container breakout vulnerabilities.\",\n this.help,\n lastUserLine\n ),\n ];\n }\n\n return [];\n },\n defaultSeverity: \"warning\",\n help: \"Add a non-root user (e.g., 'USER node' or 'USER 1000') to improve security.\",\n key: \"docker-doctor/no-root-user\",\n message: \"Container should not run as root user\",\n};\n\nexport const noSecretsInEnv: DockerfileRule = {\n category: \"Security\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n const secretKeywords = [\n /(?:^|[_-])password(?:[_-]|$)/iu,\n /(?:^|[_-])secret(?:[_-]|$)/iu,\n /(?:^|[_-])token(?:[_-]|$)/iu,\n /(?:^|[_-])api_key(?:[_-]|$)/iu,\n /(?:^|[_-])private_key(?:[_-]|$)/iu,\n /(?:^|[_-])auth(?:[_-]|$)/iu,\n ];\n\n for (const inst of instructions) {\n if (inst.instruction === \"ENV\" || inst.instruction === \"ARG\") {\n const args = inst.args.trim();\n if (inst.instruction === \"ENV\" && !args.includes(\"=\")) {\n // KEY VALUE format\n const match = args.match(/^(?<key>[^\\s]+)\\s+(?<value>.*)$/u);\n if (match?.groups) {\n const { key, value } = match.groups;\n const isSecretKey = secretKeywords.some((regex) => regex.test(key));\n if (\n isSecretKey &&\n value &&\n !value.startsWith(\"$\") &&\n !value.startsWith(\"{\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`,\n this.help,\n inst.line\n )\n );\n }\n }\n } else {\n // Existing KEY=VALUE logic\n const parts = args.split(/\\s+/u);\n for (const part of parts) {\n const eqIndex = part.indexOf(\"=\");\n let key = \"\";\n let value = \"\";\n\n if (eqIndex > 0) {\n key = part.slice(0, eqIndex);\n value = part.slice(eqIndex + 1);\n } else {\n key = part;\n }\n\n const isSecretKey = secretKeywords.some((regex) => regex.test(key));\n if (\n isSecretKey &&\n value &&\n !value.startsWith(\"$\") &&\n !value.startsWith(\"{\")\n ) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"error\",\n help: \"Use Docker Secrets, build arguments passed at runtime, or environment variables at runtime instead of baking them into the image.\",\n key: \"docker-doctor/no-secrets-in-env\",\n message: \"Do not store secrets in ENV or ARG instructions\",\n};\n\nexport const pinImageVersion: DockerfileRule = {\n category: \"Security\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n const stageAliases = collectStageAliases(instructions);\n\n for (const inst of instructions) {\n if (inst.instruction === \"FROM\") {\n // FROM image or FROM image:tag or FROM image@sha256:hash\n // Also respect multi-stage builds (AS stageName)\n const parts = inst.args.split(/\\s+/u);\n const imagePart = parts.find((p) => !p.startsWith(\"--\"));\n\n if (!imagePart || imagePart === \"scratch\") {\n continue;\n }\n\n const ref = parseImageRef(imagePart);\n\n if (ref.isVariable || stageAliases.has(imagePart.toLowerCase())) {\n continue;\n }\n\n if (!(ref.tag || ref.digest)) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Base image '${imagePart}' does not specify a tag. This makes builds non-deterministic.`,\n this.help,\n inst.line\n )\n );\n } else if (ref.tag === \"latest\" && !ref.digest) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `Base image '${imagePart}' uses the mutable 'latest' tag. This makes builds non-deterministic.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Specify a concrete tag instead of 'latest' or no tag (e.g., 'node:22.2.0-alpine' instead of 'node').\",\n key: \"docker-doctor/pin-image-version\",\n message: \"Always pin base image versions to specific tags\",\n};\n\nexport const noAddRemote: DockerfileRule = {\n category: \"Security\",\n check(instructions, file) {\n const diagnostics: Diagnostic[] = [];\n\n for (const inst of instructions) {\n if (inst.instruction === \"ADD\") {\n const parts = inst.args.split(/\\s+/u);\n const src = parts.find((p) => !p.startsWith(\"--\"));\n\n if (!src) {\n continue;\n }\n\n if (src.startsWith(\"http://\") || src.startsWith(\"https://\")) {\n diagnostics.push(\n createDiagnostic(\n file,\n this.key,\n this.defaultSeverity as \"error\" | \"warning\" | \"info\",\n `ADD instruction uses a remote URL '${src}'. Remote files added via ADD cannot be cleaned up in later layers, increasing image size.`,\n this.help,\n inst.line\n )\n );\n }\n }\n }\n\n return diagnostics;\n },\n defaultSeverity: \"warning\",\n help: \"Use 'RUN curl' or 'RUN wget' instead of ADD for remote URLs, and delete the downloaded archive in the same layer to minimize size.\",\n key: \"docker-doctor/no-add-remote\",\n message: \"Avoid using ADD with remote URLs\",\n};\n\nexport const securityRules = [\n noRootUser,\n noSecretsInEnv,\n pinImageVersion,\n noAddRemote,\n];\n","import type {\n DockerfileRule,\n ComposeRule,\n RuleDefinition,\n} from \"../types/index\";\nimport { bestPracticesRules } from \"./best-practices\";\nimport { composeRules } from \"./compose\";\nimport { imageSizeRules } from \"./image-size\";\nimport { performanceRules } from \"./performance\";\nimport { securityRules } from \"./security\";\n\nexport const allDockerfileRules: DockerfileRule[] = [\n ...securityRules,\n ...performanceRules,\n ...bestPracticesRules,\n ...imageSizeRules,\n];\n\nexport const allComposeRules: ComposeRule[] = [...composeRules];\n\nexport const allRules: RuleDefinition[] = [\n ...allDockerfileRules,\n ...allComposeRules,\n];\n\nexport const findRule = (key: string): RuleDefinition | undefined =>\n allRules.find((rule) => rule.key === key);\n","import { allDockerfileRules } from \"../rules/index\";\nimport type {\n DockerfileInstruction,\n Diagnostic,\n RuleSeverity,\n} from \"../types/index\";\n\nexport const runDockerfileRules = (\n instructions: DockerfileInstruction[],\n file: string,\n projectFiles: string[],\n rulesConfig?: Record<string, RuleSeverity>\n): Diagnostic[] => {\n const diagnostics: Diagnostic[] = [];\n\n for (const rule of allDockerfileRules) {\n const configSeverity = rulesConfig?.[rule.key];\n if (configSeverity === \"off\") {\n continue;\n }\n\n const ruleDiagnostics = rule.check(instructions, file, { projectFiles });\n\n // Override severity if config specifies it\n if (configSeverity) {\n for (const diag of ruleDiagnostics) {\n diag.severity = configSeverity as \"error\" | \"warning\" | \"info\";\n }\n }\n\n diagnostics.push(...ruleDiagnostics);\n }\n\n return diagnostics;\n};\n","import { allComposeRules } from \"../rules/index\";\nimport type { Diagnostic, RuleSeverity } from \"../types/index\";\n\nexport const runComposeRules = (\n composeContent: unknown,\n file: string,\n rulesConfig?: Record<string, RuleSeverity>\n): Diagnostic[] => {\n const diagnostics: Diagnostic[] = [];\n\n for (const rule of allComposeRules) {\n const configSeverity = rulesConfig?.[rule.key];\n if (configSeverity === \"off\") {\n continue;\n }\n\n const ruleDiagnostics = rule.check(composeContent, file);\n\n // Override severity if config specifies it\n if (configSeverity) {\n for (const diag of ruleDiagnostics) {\n diag.severity = configSeverity as \"error\" | \"warning\" | \"info\";\n }\n }\n\n diagnostics.push(...ruleDiagnostics);\n }\n\n return diagnostics;\n};\n","import type {\n DockerDoctorConfig,\n RuleCategory,\n RuleSeverity,\n} from \"../types/index\";\n\nexport type { DockerDoctorConfig } from \"../types/index\";\n\nconst RULE_SEVERITIES: readonly RuleSeverity[] = [\n \"error\",\n \"warning\",\n \"info\",\n \"off\",\n];\n\nconst RULE_CATEGORIES: readonly RuleCategory[] = [\n \"Best Practices\",\n \"Compose\",\n \"Image Size\",\n \"Performance\",\n \"Security\",\n];\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isRuleSeverity = (value: unknown): value is RuleSeverity =>\n typeof value === \"string\" &&\n (RULE_SEVERITIES as readonly string[]).includes(value);\n\nconst validateRules = (value: unknown): Record<string, RuleSeverity> => {\n if (!isPlainObject(value)) {\n throw new Error(\n `Invalid config: \"rules\" must be an object, got ${typeof value}`\n );\n }\n\n for (const [key, severity] of Object.entries(value)) {\n if (!isRuleSeverity(severity)) {\n throw new Error(\n `Invalid severity ${JSON.stringify(severity)} for rule \"${key}\"`\n );\n }\n }\n\n return value as Record<string, RuleSeverity>;\n};\n\nconst validateCategories = (\n value: unknown\n): Partial<Record<RuleCategory, RuleSeverity>> => {\n if (!isPlainObject(value)) {\n throw new Error(\n `Invalid config: \"categories\" must be an object, got ${typeof value}`\n );\n }\n\n const result: Partial<Record<RuleCategory, RuleSeverity>> = {};\n for (const [key, severity] of Object.entries(value)) {\n if (!(RULE_CATEGORIES as readonly string[]).includes(key)) {\n // Unknown category keys are silently dropped (matches the\n // legacy schema's excess-property behavior).\n continue;\n }\n if (!isRuleSeverity(severity)) {\n throw new Error(\n `Invalid severity ${JSON.stringify(severity)} for category \"${key}\"`\n );\n }\n result[key as RuleCategory] = severity;\n }\n\n return result;\n};\n\nconst validateIgnore = (value: unknown): { files?: string[] } => {\n if (!isPlainObject(value)) {\n throw new Error(\n `Invalid config: \"ignore\" must be an object, got ${typeof value}`\n );\n }\n\n const result: { files?: string[] } = {};\n if (\"files\" in value && value.files !== undefined) {\n if (\n !Array.isArray(value.files) ||\n !value.files.every((item) => typeof item === \"string\")\n ) {\n throw new Error(\n 'Invalid config: \"ignore.files\" must be an array of strings'\n );\n }\n result.files = value.files;\n }\n\n return result;\n};\n\nconst describeInvalidTopLevel = (input: unknown): string => {\n if (input === null) {\n return \"null\";\n }\n if (Array.isArray(input)) {\n return \"array\";\n }\n return typeof input;\n};\n\n/**\n * Validates and normalizes a raw config object, throwing on invalid input.\n *\n * Mirrors the legacy schema's behavior exactly, including silently\n * dropping unknown top-level (and nested) keys rather than throwing\n * or preserving them.\n */\nexport const validateConfig = (input: unknown): DockerDoctorConfig => {\n if (!isPlainObject(input)) {\n throw new Error(\n `Invalid config: expected an object, got ${describeInvalidTopLevel(input)}`\n );\n }\n\n const result: DockerDoctorConfig = {};\n\n if (\"rules\" in input && input.rules !== undefined) {\n result.rules = validateRules(input.rules);\n }\n\n if (\"categories\" in input && input.categories !== undefined) {\n result.categories = validateCategories(input.categories);\n }\n\n if (\"ignore\" in input && input.ignore !== undefined) {\n result.ignore = validateIgnore(input.ignore);\n }\n\n return result;\n};\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { parse as parseYaml } from \"yaml\";\n\nimport { ConfigError } from \"../errors\";\nimport type { DockerDoctorConfig } from \"../schemas/config\";\nimport { validateConfig } from \"../schemas/config\";\n\nconst fileExists = async (filePath: string): Promise<boolean> => {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n};\n\nconst parseConfigFile = async (\n filePath: string,\n format: \"JSON\" | \"YAML\",\n parse: (content: string) => unknown\n): Promise<unknown> => {\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n return parse(content);\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n throw new ConfigError({\n message: `Failed to parse config ${format}: ${msg}`,\n });\n }\n};\n\nconst importConfig = async (filePath: string): Promise<unknown> => {\n if (filePath.endsWith(\".json\")) {\n return parseConfigFile(filePath, \"JSON\", JSON.parse);\n }\n\n if (filePath.endsWith(\".yaml\") || filePath.endsWith(\".yml\")) {\n return parseConfigFile(filePath, \"YAML\", parseYaml);\n }\n\n try {\n const configModule = await import(filePath);\n return configModule.default || configModule;\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n throw new ConfigError({\n message: `Failed to load config file ${filePath}: ${msg}`,\n });\n }\n};\n\nexport const loadConfig = async (\n rootDir: string,\n customPath?: string\n): Promise<DockerDoctorConfig> => {\n let configObject: unknown = null;\n\n if (customPath) {\n const fullPath = path.resolve(rootDir, customPath);\n if (!(await fileExists(fullPath))) {\n throw new ConfigError({\n message: `Specified config file not found at ${fullPath}`,\n });\n }\n configObject = await importConfig(fullPath);\n } else {\n const candidates = [\n \"docker-doctor.config.ts\",\n \"docker-doctor.config.js\",\n \"docker-doctor.config.mjs\",\n \"docker-doctor.config.cjs\",\n \"docker-doctor.config.json\",\n \"docker-doctor.config.yaml\",\n \"docker-doctor.config.yml\",\n ];\n\n /* eslint-disable no-await-in-loop */\n for (const cand of candidates) {\n const fullPath = path.join(rootDir, cand);\n if (await fileExists(fullPath)) {\n configObject = await importConfig(fullPath);\n break;\n }\n }\n /* eslint-enable no-await-in-loop */\n\n if (!configObject) {\n const pkgPath = path.join(rootDir, \"package.json\");\n if (await fileExists(pkgPath)) {\n try {\n const pkgContent = await fs.readFile(pkgPath, \"utf-8\");\n const pkgJson = JSON.parse(pkgContent);\n if (pkgJson.dockerDoctor) {\n configObject = pkgJson.dockerDoctor;\n }\n } catch {\n // ignore package.json read/parse failures\n }\n }\n }\n }\n\n if (!configObject) {\n return {};\n }\n\n try {\n return validateConfig(configObject);\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n throw new ConfigError({\n message: `Invalid configuration format: ${msg}`,\n });\n }\n};\n","import type { Diagnostic } from \"./types/index\";\n\nexport const SCORE_BUCKETS = [\n { emoji: \"🏆\", label: \"Excellent\", min: 90 },\n { emoji: \"✅\", label: \"Good\", min: 75 },\n { emoji: \"⚠️\", label: \"Needs Work\", min: 50 },\n { emoji: \"🚨\", label: \"Critical\", min: 0 },\n] as const;\n\nexport const getScoreBucket = (\n score: number\n): (typeof SCORE_BUCKETS)[number] => {\n for (const bucket of SCORE_BUCKETS) {\n if (score >= bucket.min) {\n return bucket;\n }\n }\n return SCORE_BUCKETS.at(-1) as (typeof SCORE_BUCKETS)[number];\n};\n\nexport const calculateScore = (\n diagnostics: Diagnostic[]\n): {\n score: number;\n label: string;\n} => {\n let penalty = 0;\n\n for (const diag of diagnostics) {\n if (diag.severity === \"error\") {\n penalty += 10;\n } else if (diag.severity === \"warning\") {\n penalty += 4;\n } else if (diag.severity === \"info\") {\n penalty += 1;\n }\n }\n\n // Asymptotic decay curve: score = round(100 * e^(-penalty / K)).\n //\n // The old `max(0, 100 - penalty)` formula saturates at 0 once penalty\n // reaches 100 (e.g. ~10 errors), so a messy project and a catastrophic\n // one are indistinguishable and the score can never register a fix.\n // This curve approaches (but never reaches) 0, so it stays monotonic\n // and responsive across the whole range instead of going inert.\n //\n // K=70 was chosen so a single warning (penalty 4) still scores ~94,\n // comfortably inside the \"Excellent\" (>=90) bucket, while errors and\n // repeated warnings still meaningfully erode the score. K=40 (the\n // naive \"half-life at penalty ~28\" choice) was tried first and pushed\n // a single warning down to ~90 - right on the Excellent/Good boundary,\n // which is too harsh a penalty for one warning.\n const K = 70;\n const score = Math.round(100 * Math.exp(-penalty / K));\n const bucket = getScoreBucket(score);\n const label = `${bucket.label} ${bucket.emoji}`;\n\n return { label, score };\n};\n","import type { Diagnostic, ProjectInfo } from \"./types/index\";\n\n// Bump whenever the JSON report shape or the score formula/weights change.\n// The unversioned shape shipped before this field existed is implicitly 1.\nexport const REPORT_SCHEMA_VERSION = 2;\n\nexport interface JsonReport {\n diagnostics: {\n column?: number;\n file: string;\n help: string;\n line?: number;\n message: string;\n rule: string;\n severity: \"error\" | \"warning\" | \"info\";\n }[];\n label: string;\n project: ProjectInfo;\n schemaVersion: number;\n score: number;\n timestamp: string;\n}\n\nexport const toJsonReport = (\n diagnostics: Diagnostic[],\n score: number,\n label: string,\n project: ProjectInfo\n): JsonReport => ({\n diagnostics: diagnostics.map((d) => ({\n column: d.column,\n file: d.file,\n help: d.help,\n line: d.line,\n message: d.message,\n rule: d.rule,\n severity: d.severity,\n })),\n label,\n project,\n schemaVersion: REPORT_SCHEMA_VERSION,\n score,\n timestamp: new Date().toISOString(),\n});\n"],"mappings":"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"}