@infograb/docker-slim-advisor 0.1.0

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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/build/base-image-db-loader.d.ts +16 -0
  4. package/build/base-image-db-loader.js +72 -0
  5. package/build/base-image-db-loader.js.map +1 -0
  6. package/build/cli.d.ts +47 -0
  7. package/build/cli.js +142 -0
  8. package/build/cli.js.map +1 -0
  9. package/build/data/base-image-db-schema.d.ts +117 -0
  10. package/build/data/base-image-db-schema.js +108 -0
  11. package/build/data/base-image-db-schema.js.map +1 -0
  12. package/build/data/base-image-db.d.ts +21 -0
  13. package/build/data/base-image-db.js +190 -0
  14. package/build/data/base-image-db.js.map +1 -0
  15. package/build/exit-codes.d.ts +25 -0
  16. package/build/exit-codes.js +31 -0
  17. package/build/exit-codes.js.map +1 -0
  18. package/build/formatters/formatter-dispatcher.d.ts +15 -0
  19. package/build/formatters/formatter-dispatcher.js +27 -0
  20. package/build/formatters/formatter-dispatcher.js.map +1 -0
  21. package/build/formatters/json-formatter.d.ts +10 -0
  22. package/build/formatters/json-formatter.js +52 -0
  23. package/build/formatters/json-formatter.js.map +1 -0
  24. package/build/formatters/json-schema.d.ts +57 -0
  25. package/build/formatters/json-schema.js +13 -0
  26. package/build/formatters/json-schema.js.map +1 -0
  27. package/build/formatters/markdown-formatter.d.ts +12 -0
  28. package/build/formatters/markdown-formatter.js +97 -0
  29. package/build/formatters/markdown-formatter.js.map +1 -0
  30. package/build/formatters/terminal-formatter.d.ts +12 -0
  31. package/build/formatters/terminal-formatter.js +142 -0
  32. package/build/formatters/terminal-formatter.js.map +1 -0
  33. package/build/image-size-lookup.d.ts +35 -0
  34. package/build/image-size-lookup.js +187 -0
  35. package/build/image-size-lookup.js.map +1 -0
  36. package/build/multi-stage-detector.d.ts +29 -0
  37. package/build/multi-stage-detector.js +29 -0
  38. package/build/multi-stage-detector.js.map +1 -0
  39. package/build/output.d.ts +46 -0
  40. package/build/output.js +62 -0
  41. package/build/output.js.map +1 -0
  42. package/build/parser.d.ts +7 -0
  43. package/build/parser.js +123 -0
  44. package/build/parser.js.map +1 -0
  45. package/build/rules/alpine-swap.d.ts +10 -0
  46. package/build/rules/alpine-swap.js +81 -0
  47. package/build/rules/alpine-swap.js.map +1 -0
  48. package/build/rules/dockerignore-missing.d.ts +19 -0
  49. package/build/rules/dockerignore-missing.js +107 -0
  50. package/build/rules/dockerignore-missing.js.map +1 -0
  51. package/build/rules/index.d.ts +11 -0
  52. package/build/rules/index.js +22 -0
  53. package/build/rules/index.js.map +1 -0
  54. package/build/rules/package-cache-cleanup.d.ts +15 -0
  55. package/build/rules/package-cache-cleanup.js +89 -0
  56. package/build/rules/package-cache-cleanup.js.map +1 -0
  57. package/build/rules/run-merge.d.ts +12 -0
  58. package/build/rules/run-merge.js +67 -0
  59. package/build/rules/run-merge.js.map +1 -0
  60. package/build/rules/unnecessary-packages.d.ts +23 -0
  61. package/build/rules/unnecessary-packages.js +184 -0
  62. package/build/rules/unnecessary-packages.js.map +1 -0
  63. package/build/severity-filter.d.ts +22 -0
  64. package/build/severity-filter.js +31 -0
  65. package/build/severity-filter.js.map +1 -0
  66. package/build/size-estimator.d.ts +35 -0
  67. package/build/size-estimator.js +238 -0
  68. package/build/size-estimator.js.map +1 -0
  69. package/build/tty-detection.d.ts +20 -0
  70. package/build/tty-detection.js +33 -0
  71. package/build/tty-detection.js.map +1 -0
  72. package/build/types.d.ts +82 -0
  73. package/build/types.js +6 -0
  74. package/build/types.js.map +1 -0
  75. package/package.json +52 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 docker-slim-advisor contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,274 @@
1
+ # docker-slim-advisor
2
+
3
+ Static Dockerfile analyzer that recommends image size optimizations — no Docker build required.
4
+
5
+ ```bash
6
+ npx @infograb/docker-slim-advisor analyze ./Dockerfile
7
+ ```
8
+
9
+ ---
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ npx @infograb/docker-slim-advisor ./Dockerfile
15
+ ```
16
+
17
+ **Example output** (against a typical Node.js app):
18
+
19
+ ```
20
+ Docker Slim Advisor
21
+ --------------------------------------------------
22
+
23
+ File: Dockerfile
24
+ Base Image: node:18
25
+
26
+ Size Prediction
27
+
28
+ Before: 1.1GB
29
+ After: 226.6MB
30
+
31
+ [██████░░░░░░░░░░░░░░░░░░░░░░░░]
32
+
33
+ Estimated savings: 904.9MB (80% reduction)
34
+
35
+ Findings (2)
36
+
37
+ [HIGH] Use node:18-slim instead of node:18 (DSA001) Line 1
38
+ Switching from node:18 (1.0GB) to node:18-slim (200MB) saves ~800MB (80% reduction).
39
+ Fix: FROM node:18-slim
40
+ Saves ~800.0MB
41
+
42
+ [HIGH] Broad COPY/ADD without .dockerignore optimization (DSA004) Line 5
43
+ `COPY .` copies the entire build context. Without a comprehensive .dockerignore,
44
+ this includes node_modules, .git, build artifacts, and other unnecessary files.
45
+ Fix: Add node_modules, .git, dist, build, .env to .dockerignore
46
+ Saves ~104.9MB
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Features
52
+
53
+ - **5 optimization rules** — alpine/slim base swap, RUN layer merge, apt/apk cache cleanup, `.dockerignore` detection, unnecessary package removal
54
+ - **3 output formats** — terminal (TTY-aware), JSON (versioned schema), Markdown
55
+ - **Size prediction** — before/after estimates with ±30% accuracy, no `docker build` needed
56
+ - **CI/CD ready** — structured exit codes, `NO_COLOR` support, stderr/stdout separation
57
+ - **Fast** — analysis completes in under 1 second for typical Dockerfiles
58
+
59
+ ---
60
+
61
+ ## Installation
62
+
63
+ **Global install:**
64
+
65
+ ```bash
66
+ npm install -g @infograb/docker-slim-advisor
67
+ docker-slim-advisor ./Dockerfile
68
+ ```
69
+
70
+ **One-off with npx (no install):**
71
+
72
+ ```bash
73
+ npx @infograb/docker-slim-advisor ./Dockerfile
74
+ ```
75
+
76
+ **Requirements:** Node.js >= 18
77
+
78
+ ---
79
+
80
+ ## Usage
81
+
82
+ ```
83
+ docker-slim-advisor [dockerfile] [options]
84
+
85
+ Arguments:
86
+ dockerfile Path to Dockerfile (default: "Dockerfile")
87
+
88
+ Options:
89
+ -f, --format <format> Output format: terminal, json, markdown (default: "terminal")
90
+ -s, --severity <level> Minimum severity to report: LOW, MEDIUM, HIGH
91
+ -V, --version Print version
92
+ -h, --help Display help
93
+ ```
94
+
95
+ **Examples:**
96
+
97
+ ```bash
98
+ # Analyze Dockerfile in current directory
99
+ docker-slim-advisor
100
+
101
+ # Analyze a specific file
102
+ docker-slim-advisor path/to/Dockerfile
103
+
104
+ # JSON output (machine-readable)
105
+ docker-slim-advisor --format json ./Dockerfile
106
+
107
+ # Only report HIGH severity findings
108
+ docker-slim-advisor --severity HIGH ./Dockerfile
109
+
110
+ # Markdown report for documentation
111
+ docker-slim-advisor --format markdown ./Dockerfile > report.md
112
+ ```
113
+
114
+ ### Exit Codes
115
+
116
+ | Code | Meaning |
117
+ |------|---------|
118
+ | `0` | No HIGH severity findings |
119
+ | `1` | One or more HIGH findings present |
120
+ | `2` | Error (file not found, parse failure, etc.) |
121
+
122
+ ---
123
+
124
+ ## Output Formats
125
+
126
+ ### Terminal (default)
127
+
128
+ Color and emoji output when connected to a TTY. Plain text in pipes. Respects `NO_COLOR` environment variable.
129
+
130
+ ```bash
131
+ docker-slim-advisor ./Dockerfile
132
+ ```
133
+
134
+ ### JSON
135
+
136
+ Versioned schema suitable for downstream tooling.
137
+
138
+ ```bash
139
+ docker-slim-advisor --format json ./Dockerfile
140
+ ```
141
+
142
+ ```json
143
+ {
144
+ "schemaVersion": 1,
145
+ "dockerfilePath": "Dockerfile",
146
+ "isMultiStage": false,
147
+ "baseImage": "node:18",
148
+ "estimatedBeforeSize": {
149
+ "totalBytes": 1131500000,
150
+ "humanReadable": "1.1GB"
151
+ },
152
+ "estimatedAfterSize": {
153
+ "totalBytes": 226642400,
154
+ "humanReadable": "227MB"
155
+ },
156
+ "sizeReductionPercent": 80,
157
+ "totalFindings": 2,
158
+ "findings": [
159
+ {
160
+ "ruleId": "DSA001",
161
+ "severity": "HIGH",
162
+ "line": 1,
163
+ "title": "Use node:18-slim instead of node:18",
164
+ "description": "Switching from node:18 (1.0GB) to node:18-slim (200MB) saves ~800MB.",
165
+ "fix": "FROM node:18-slim",
166
+ "estimatedSavingsBytes": 800000000
167
+ }
168
+ ]
169
+ }
170
+ ```
171
+
172
+ ### Markdown
173
+
174
+ ```bash
175
+ docker-slim-advisor --format markdown ./Dockerfile
176
+ ```
177
+
178
+ Produces a GitHub-compatible report with a findings table and per-rule detail sections. Suitable for PR comments or documentation.
179
+
180
+ ---
181
+
182
+ ## CI/CD Integration
183
+
184
+ Use exit codes to fail pipelines on HIGH findings:
185
+
186
+ ```bash
187
+ # Fail CI if any HIGH findings are found
188
+ docker-slim-advisor ./Dockerfile
189
+ if [ $? -eq 1 ]; then
190
+ echo "Image optimization issues detected. Review the findings above."
191
+ exit 1
192
+ fi
193
+ ```
194
+
195
+ **GitHub Actions example:**
196
+
197
+ ```yaml
198
+ - name: Analyze Dockerfile
199
+ run: npx @infograb/docker-slim-advisor ./Dockerfile --severity HIGH
200
+ ```
201
+
202
+ **Pipe-friendly (no color/emoji):**
203
+
204
+ ```bash
205
+ # NO_COLOR disables ANSI codes; plain text is always pipe-safe
206
+ NO_COLOR=1 docker-slim-advisor ./Dockerfile | tee advisor-report.txt
207
+ ```
208
+
209
+ **JSON in scripts:**
210
+
211
+ ```bash
212
+ FINDINGS=$(docker-slim-advisor --format json ./Dockerfile | jq '.totalFindings')
213
+ echo "Found $FINDINGS optimization opportunities"
214
+ ```
215
+
216
+ ---
217
+
218
+ ## How It Works
219
+
220
+ `docker-slim-advisor` performs **static analysis** — it reads and parses your Dockerfile as text, with no Docker daemon or image pull required.
221
+
222
+ 1. **Parse** — Dockerfile is tokenized into a typed instruction AST (`FROM`, `RUN`, `COPY`, `ADD`, etc.). Multi-line `RUN` with backslash continuations are handled correctly.
223
+ 2. **Detect** — Multi-stage builds (`multiple FROM` instructions) are detected and reported as already optimized.
224
+ 3. **Evaluate rules** — Each of the 5 optimization rules inspects the AST and emits findings with line numbers, rule IDs, and estimated savings.
225
+ 4. **Estimate sizes** — A bundled JSON database of 170+ image tags provides base image sizes. Layer size heuristics compute before/after predictions (±30% accuracy).
226
+ 5. **Format output** — The result is rendered in the requested format with TTY detection and `NO_COLOR` support.
227
+
228
+ ---
229
+
230
+ ## Optimization Rules
231
+
232
+ | Rule ID | Severity | Description |
233
+ |---------|----------|-------------|
234
+ | `DSA001` | HIGH | Switch to a slimmer base image (e.g. `node:18` → `node:18-slim`, `node:18-alpine`) |
235
+ | `DSA002` | MEDIUM | Merge multiple `RUN` instructions into one to reduce image layers |
236
+ | `DSA003` | MEDIUM | Clean apt/apk cache in the same `RUN` layer (`--no-install-recommends`, `rm -rf /var/lib/apt/lists/*`) |
237
+ | `DSA004` | HIGH | Add or improve `.dockerignore` to avoid copying unnecessary files via `COPY .` |
238
+ | `DSA005` | LOW | Remove unnecessary packages installed for build-time only (e.g. `curl`, `wget`, `git`) |
239
+
240
+ ---
241
+
242
+ ## Supported Base Images
243
+
244
+ The bundled database covers **170+ image tags** across **53 base images**, including:
245
+
246
+ | Image | Image | Image |
247
+ |-------|-------|-------|
248
+ | `alpine` | `node` | `python` |
249
+ | `ubuntu` | `debian` | `golang` |
250
+ | `nginx` | `postgres` | `redis` |
251
+ | `rust` | `ruby` | `php` |
252
+ | `maven` | `gradle` | `eclipse-temurin` |
253
+ | `mongo` | `mysql` | `elasticsearch` |
254
+ | `gcr.io/distroless/*` | `mcr.microsoft.com/dotnet/*` | `pytorch/pytorch` |
255
+
256
+ Tag variants (e.g. `latest`, `slim`, `alpine`, `bookworm`, version-pinned) are included where available.
257
+
258
+ ---
259
+
260
+ ## Contributing
261
+
262
+ 1. Fork the repository and create a feature branch.
263
+ 2. Install dependencies: `npm install`
264
+ 3. Run tests: `npm test`
265
+ 4. Check types: `npm run lint`
266
+ 5. Submit a pull request with a clear description of the change.
267
+
268
+ To add a new optimization rule, create `src/rules/your-rule.ts` implementing the `Rule` interface, then register it in `src/rules/index.ts`.
269
+
270
+ ---
271
+
272
+ ## License
273
+
274
+ MIT
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Base image database loader — reads static JSON bundle, provides
3
+ * size lookup and smaller-alternative search for the size estimator.
4
+ */
5
+ /** A smaller alternative image found in the DB */
6
+ export interface AlternativeImage {
7
+ image: string;
8
+ tag: string;
9
+ sizeBytes: number;
10
+ }
11
+ /** Load the static image→size map from bundled JSON */
12
+ export declare function loadImageSizeMap(): Record<string, number>;
13
+ /** Lookup image size by name and tag. Returns undefined if not in DB. */
14
+ export declare function getImageSize(image: string, tag: string, images?: Record<string, number>): number | undefined;
15
+ /** Find a smaller alpine/slim alternative for the given image:tag */
16
+ export declare function findSmallerAlternative(image: string, tag: string, images?: Record<string, number>): AlternativeImage | undefined;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Base image database loader — reads static JSON bundle, provides
3
+ * size lookup and smaller-alternative search for the size estimator.
4
+ */
5
+ import { readFileSync } from 'node:fs';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { dirname, join } from 'node:path';
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ /** Suffixes to try when searching for slim alternatives */
11
+ const SLIM_SUFFIXES = ['-alpine', '-slim'];
12
+ let cachedImages = null;
13
+ /** Load the static image→size map from bundled JSON */
14
+ export function loadImageSizeMap() {
15
+ if (cachedImages)
16
+ return cachedImages;
17
+ const dbPath = join(__dirname, 'data', 'base-image-db.json');
18
+ const raw = JSON.parse(readFileSync(dbPath, 'utf-8'));
19
+ // Normalize: entries may be plain numbers or {compressedBytes} objects
20
+ const normalized = {};
21
+ for (const [key, value] of Object.entries(raw.images)) {
22
+ normalized[key] = typeof value === 'number' ? value : value.compressedBytes;
23
+ }
24
+ cachedImages = normalized;
25
+ return cachedImages;
26
+ }
27
+ /** Lookup image size by name and tag. Returns undefined if not in DB. */
28
+ export function getImageSize(image, tag, images) {
29
+ const db = images ?? loadImageSizeMap();
30
+ const key = `${image}:${tag}`;
31
+ if (db[key] !== undefined)
32
+ return db[key];
33
+ if (key === 'scratch:latest' || image === 'scratch')
34
+ return 0;
35
+ if (tag === 'latest' && db[image] !== undefined)
36
+ return db[image];
37
+ return undefined;
38
+ }
39
+ /** Find a smaller alpine/slim alternative for the given image:tag */
40
+ export function findSmallerAlternative(image, tag, images) {
41
+ const db = images ?? loadImageSizeMap();
42
+ const currentSize = getImageSize(image, tag, db);
43
+ if (currentSize === undefined)
44
+ return undefined;
45
+ // Already alpine/slim — no further suggestion
46
+ if (tag.includes('alpine') || tag.includes('slim'))
47
+ return undefined;
48
+ const versionPrefix = tag.match(/^[\d.]+/)?.[0] || '';
49
+ let best;
50
+ for (const suffix of SLIM_SUFFIXES) {
51
+ const candidates = [
52
+ `${image}:${tag}${suffix}`,
53
+ versionPrefix ? `${image}:${versionPrefix}${suffix}` : null,
54
+ `${image}:${suffix.slice(1)}`, // e.g. "alpine"
55
+ ].filter(Boolean);
56
+ for (const candidate of candidates) {
57
+ const size = db[candidate];
58
+ if (size !== undefined && size < currentSize) {
59
+ if (!best || size < best.sizeBytes) {
60
+ const colonIdx = candidate.indexOf(':');
61
+ best = {
62
+ image: candidate.slice(0, colonIdx),
63
+ tag: candidate.slice(colonIdx + 1),
64
+ sizeBytes: size,
65
+ };
66
+ }
67
+ }
68
+ }
69
+ }
70
+ return best;
71
+ }
72
+ //# sourceMappingURL=base-image-db-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-image-db-loader.js","sourceRoot":"","sources":["../src/base-image-db-loader.ts"],"names":[],"mappings":"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"}
package/build/cli.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for docker-slim-advisor.
4
+ *
5
+ * Parses command-line arguments using commander and routes
6
+ * to the analysis pipeline. Handles --format flag with
7
+ * 'terminal' (default), 'json', and 'markdown' values.
8
+ */
9
+ import { Command } from 'commander';
10
+ import type { Severity, AnalysisResult } from './types.js';
11
+ /** Supported output format values */
12
+ export declare const OUTPUT_FORMATS: readonly ["terminal", "json", "markdown"];
13
+ export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
14
+ /** Parsed CLI options after argument processing */
15
+ export interface CliOptions {
16
+ format: OutputFormat;
17
+ severity?: Severity;
18
+ }
19
+ /**
20
+ * Validate that a format string is one of the accepted values.
21
+ * Used as commander's argParser callback for --format flag.
22
+ */
23
+ export declare function parseFormat(value: string): OutputFormat;
24
+ /**
25
+ * Validate that a severity string is one of the accepted values.
26
+ * Accepts case-insensitive input (e.g. "high", "HIGH", "High").
27
+ * Used as commander's argParser callback for --severity flag.
28
+ */
29
+ export declare function parseSeverity(value: string): Severity;
30
+ /**
31
+ * Build and return the configured Commander program.
32
+ * Separated from execution for testability.
33
+ */
34
+ export declare function createProgram(): Command;
35
+ /**
36
+ * Parse argv and return structured CLI options + positional args.
37
+ * Exits with code 2 on invalid arguments (commander default behavior).
38
+ */
39
+ export declare function parseArgs(argv?: string[]): {
40
+ options: CliOptions;
41
+ dockerfilePath: string;
42
+ };
43
+ /**
44
+ * Run the full analysis pipeline: parse → detect → evaluate rules →
45
+ * estimate sizes → format output → determine exit code.
46
+ */
47
+ export declare function analyze(dockerfilePath: string, options: CliOptions): AnalysisResult;
package/build/cli.js ADDED
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for docker-slim-advisor.
4
+ *
5
+ * Parses command-line arguments using commander and routes
6
+ * to the analysis pipeline. Handles --format flag with
7
+ * 'terminal' (default), 'json', and 'markdown' values.
8
+ */
9
+ import { Command, InvalidArgumentError } from 'commander';
10
+ import { readFileSync } from 'node:fs';
11
+ import { EXIT_CODE } from './exit-codes.js';
12
+ import { determineExitCode } from './exit-codes.js';
13
+ import { SEVERITY_LEVELS, isValidSeverity, filterBySeverity } from './severity-filter.js';
14
+ import { writeError, writeOutput } from './output.js';
15
+ import { parseDockerfile } from './parser.js';
16
+ import { detectMultiStage } from './multi-stage-detector.js';
17
+ import { rules } from './rules/index.js';
18
+ import { estimateBeforeSize, estimateAfterSize, calcSizeReductionPercent } from './size-estimator.js';
19
+ import { getImageSize } from './data/base-image-db.js';
20
+ import { getFormatter } from './formatters/formatter-dispatcher.js';
21
+ /** Supported output format values */
22
+ export const OUTPUT_FORMATS = ['terminal', 'json', 'markdown'];
23
+ /**
24
+ * Validate that a format string is one of the accepted values.
25
+ * Used as commander's argParser callback for --format flag.
26
+ */
27
+ export function parseFormat(value) {
28
+ const lower = value.toLowerCase();
29
+ if (!OUTPUT_FORMATS.includes(lower)) {
30
+ throw new InvalidArgumentError(`Invalid format '${value}'. Must be one of: ${OUTPUT_FORMATS.join(', ')}`);
31
+ }
32
+ return lower;
33
+ }
34
+ /**
35
+ * Validate that a severity string is one of the accepted values.
36
+ * Accepts case-insensitive input (e.g. "high", "HIGH", "High").
37
+ * Used as commander's argParser callback for --severity flag.
38
+ */
39
+ export function parseSeverity(value) {
40
+ if (!isValidSeverity(value)) {
41
+ throw new InvalidArgumentError(`Invalid severity '${value}'. Must be one of: ${SEVERITY_LEVELS.join(', ')}`);
42
+ }
43
+ return value.toUpperCase();
44
+ }
45
+ /**
46
+ * Build and return the configured Commander program.
47
+ * Separated from execution for testability.
48
+ */
49
+ export function createProgram() {
50
+ const program = new Command();
51
+ program
52
+ .name('docker-slim-advisor')
53
+ .description('Statically analyze Dockerfiles to recommend image size optimizations')
54
+ .version('0.1.0')
55
+ .argument('[dockerfile]', 'Path to Dockerfile to analyze', 'Dockerfile')
56
+ .option('-f, --format <format>', `Output format: ${OUTPUT_FORMATS.join(', ')}`, parseFormat, 'terminal')
57
+ .option('-s, --severity <level>', `Minimum severity to report: ${SEVERITY_LEVELS.join(', ')}`, parseSeverity);
58
+ return program;
59
+ }
60
+ /**
61
+ * Parse argv and return structured CLI options + positional args.
62
+ * Exits with code 2 on invalid arguments (commander default behavior).
63
+ */
64
+ export function parseArgs(argv) {
65
+ const program = createProgram();
66
+ // commander.parse() exits on --help/--version;
67
+ // commander.parseAsync() not needed since we have no async opts
68
+ program.parse(argv);
69
+ const opts = program.opts();
70
+ const dockerfilePath = program.args[0] ?? 'Dockerfile';
71
+ return { options: opts, dockerfilePath };
72
+ }
73
+ /**
74
+ * Run the full analysis pipeline: parse → detect → evaluate rules →
75
+ * estimate sizes → format output → determine exit code.
76
+ */
77
+ export function analyze(dockerfilePath, options) {
78
+ const content = readFileSync(dockerfilePath, 'utf-8');
79
+ const instructions = parseDockerfile(content);
80
+ const { isMultiStage } = detectMultiStage(instructions);
81
+ // Determine base image from the first FROM instruction
82
+ const fromInstr = instructions.find((i) => i.type === 'FROM');
83
+ const baseImage = fromInstr
84
+ ? `${fromInstr.image}:${fromInstr.tag}`
85
+ : 'unknown';
86
+ // Build rule context with image size lookup
87
+ const ruleContext = { getImageSize };
88
+ // Evaluate all rules against parsed instructions
89
+ const recommendations = isMultiStage
90
+ ? []
91
+ : rules.flatMap((rule) => rule.evaluate(instructions, ruleContext));
92
+ // Size estimation
93
+ const beforeSize = estimateBeforeSize(instructions, getImageSize);
94
+ // Calculate new base image size if alpine-swap is recommended
95
+ const alpineSwapRec = recommendations.find((r) => r.ruleId === 'DSA001');
96
+ const newBaseImageBytes = alpineSwapRec
97
+ ? beforeSize.baseImageBytes - alpineSwapRec.estimatedSavingsBytes
98
+ : undefined;
99
+ const afterSize = estimateAfterSize(beforeSize, recommendations, newBaseImageBytes);
100
+ const sizeReductionPercent = calcSizeReductionPercent(beforeSize.totalBytes, afterSize.totalBytes);
101
+ return {
102
+ dockerfilePath,
103
+ isMultiStage,
104
+ baseImage,
105
+ instructions,
106
+ recommendations,
107
+ estimatedBeforeSizeBytes: beforeSize.totalBytes,
108
+ estimatedAfterSizeBytes: afterSize.totalBytes,
109
+ sizeReductionPercent,
110
+ schemaVersion: 1,
111
+ };
112
+ }
113
+ /* Main execution - only runs when invoked directly, not when imported */
114
+ async function main() {
115
+ try {
116
+ const { options, dockerfilePath } = parseArgs();
117
+ // Run analysis pipeline
118
+ const result = analyze(dockerfilePath, options);
119
+ // Filter recommendations by severity threshold
120
+ const filtered = options.severity
121
+ ? filterBySeverity(result.recommendations, options.severity)
122
+ : result.recommendations;
123
+ // Dispatch to the correct formatter based on --format value
124
+ const formatter = getFormatter(options.format);
125
+ const output = formatter(result, filtered);
126
+ writeOutput(output);
127
+ // Set exit code based on findings
128
+ process.exitCode = determineExitCode(result.recommendations, options.severity);
129
+ }
130
+ catch (err) {
131
+ writeError(err instanceof Error ? err.message : String(err));
132
+ process.exitCode = EXIT_CODE.ERROR;
133
+ }
134
+ }
135
+ // ESM doesn't have require.main === module; detect via argv
136
+ const isDirectRun = process.argv[1] &&
137
+ (process.argv[1].endsWith('/cli.js') ||
138
+ process.argv[1].endsWith('/cli.ts'));
139
+ if (isDirectRun) {
140
+ main();
141
+ }
142
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}