@docker-doctor/cli 0.0.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.
package/dist/cli.mjs ADDED
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env node
2
+ import { a as runDockerfileRules, c as parseCompose, d as package_default, i as runComposeRules, l as parseDockerfile, n as calculateScore, o as allRules, r as loadConfig, s as findRule, t as toJsonReport, u as discoverProject } from "./src-IXht-l0J.mjs";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+ import { stdin, stdout } from "node:process";
7
+ import readline from "node:readline/promises";
8
+ import { setTimeout } from "node:timers/promises";
9
+ import chalk from "chalk";
10
+ import { Command } from "commander";
11
+
12
+ //#region src/formatters/terminal.ts
13
+ const printCodeFrame = (content, line, severityColor) => {
14
+ if (!content || !line) return;
15
+ const lines = content.split(/\r?\n/u);
16
+ const start = Math.max(1, line - 1);
17
+ const end = Math.min(lines.length, line + 1);
18
+ for (let i = start; i <= end; i += 1) {
19
+ const rawLine = lines[i - 1];
20
+ const isTarget = i === line;
21
+ const lineNumberStr = String(i).padStart(5, " ");
22
+ if (isTarget) console.log(` ${severityColor(">")} ${chalk.bold(lineNumberStr)} │ ${chalk.white(rawLine)}`);
23
+ else console.log(` ${chalk.dim(lineNumberStr)} │ ${chalk.dim(rawLine)}`);
24
+ }
25
+ console.log();
26
+ };
27
+ const printDiscoveredFiles = (project) => {
28
+ console.log(`\nDiscovered Files:`);
29
+ console.log(` Dockerfile(s): ${project.dockerfiles.length ? project.dockerfiles.map((f) => chalk.cyan(f)).join(", ") : chalk.dim("None")}`);
30
+ console.log(` Compose file(s): ${project.composeFiles.length ? project.composeFiles.map((f) => chalk.cyan(f)).join(", ") : chalk.dim("None")}`);
31
+ };
32
+ const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCounts) => {
33
+ if (diagnostics.length === 0) {
34
+ console.log(`\n${chalk.green.bold("✔ No issues found! Your Docker setup looks healthy.")}`);
35
+ return;
36
+ }
37
+ if (!verbose) {
38
+ console.log(`\n All ${chalk.bold(diagnostics.length)} issues\n`);
39
+ for (const cat of [
40
+ "Security",
41
+ "Performance",
42
+ "Best Practices",
43
+ "Compose",
44
+ "Image Size"
45
+ ]) {
46
+ const count = categoryIssueCounts[cat];
47
+ const issueLabel = count === 1 ? "1 issue" : `${count} issues`;
48
+ console.log(` ${cat} › ${chalk.dim(issueLabel)}`);
49
+ }
50
+ console.log(`\n Run ${chalk.cyan("docker-doctor --verbose")} to list every error and warning`);
51
+ const ruleCounts = {};
52
+ for (const d of diagnostics) ruleCounts[d.rule] = (ruleCounts[d.rule] || 0) + 1;
53
+ const migrationRules = Object.entries(ruleCounts).filter(([_, count]) => count >= 5);
54
+ if (migrationRules.length > 0) {
55
+ console.log();
56
+ console.log(` ${chalk.yellow("⚠ Migration-scale change: sample before you sweep")}`);
57
+ for (const [rule, count] of migrationRules) console.log(` ${chalk.cyan(rule)} ×${count} across ${count} files`);
58
+ console.log(` Fixing all of them at once is hard to review and prone to`);
59
+ console.log(` subtle mistakes across the whole repo. Fix a representative`);
60
+ console.log(` few first and confirm the recipe holds. Then get the code`);
61
+ console.log(` owner's sign-off before changing the rest.`);
62
+ console.log(` Scope it down one area at a time: docker-doctor <path>`);
63
+ }
64
+ return;
65
+ }
66
+ console.log(`\nFound ${chalk.bold(diagnostics.length)} issue(s):`);
67
+ const filesGrouped = {};
68
+ for (const d of diagnostics) {
69
+ if (!filesGrouped[d.file]) filesGrouped[d.file] = [];
70
+ filesGrouped[d.file].push(d);
71
+ }
72
+ for (const [file, fileDiags] of Object.entries(filesGrouped)) {
73
+ console.log(`\n${chalk.underline.bold(file)}`);
74
+ for (const d of fileDiags) {
75
+ let sevColor = chalk.cyan;
76
+ let prefix = "ℹ INFO";
77
+ if (d.severity === "error") {
78
+ sevColor = chalk.red.bold;
79
+ prefix = "✖ ERROR";
80
+ } else if (d.severity === "warning") {
81
+ sevColor = chalk.yellow;
82
+ prefix = "⚠ WARN";
83
+ }
84
+ const lineInfo = d.line ? `:${d.line}` : "";
85
+ console.log(` ${sevColor(prefix)} [${chalk.dim(d.rule)}]${lineInfo}`);
86
+ const content = fileContents[file];
87
+ printCodeFrame(content, d.line, sevColor);
88
+ console.log(` ${chalk.white(d.message)}`);
89
+ console.log(` ${chalk.dim("Help:")} ${d.help}`);
90
+ console.log();
91
+ }
92
+ }
93
+ };
94
+ const getWhaleMascot = (score, border) => {
95
+ let eyes = "x x";
96
+ let spout = " ";
97
+ if (score >= 75) {
98
+ eyes = "◠ ◠";
99
+ spout = chalk.cyan(" \":\" ");
100
+ } else if (score >= 50) {
101
+ eyes = "• •";
102
+ spout = chalk.cyan(" . ");
103
+ }
104
+ return [
105
+ spout,
106
+ border(" .---. "),
107
+ border(`( ${eyes} )>`),
108
+ border(" \\___/ ")
109
+ ];
110
+ };
111
+ const easeOutCubic = (x) => 1 - (1 - x) ** 3;
112
+ const printScoreBox = async (score, label, categoryIssueCounts) => {
113
+ const { isTTY } = process.stdout;
114
+ const frameCount = 40;
115
+ const frameDelay = 50;
116
+ let scoreColor = chalk.red.bold;
117
+ if (score >= 90) scoreColor = chalk.green.bold;
118
+ else if (score >= 75) scoreColor = chalk.yellow.bold;
119
+ else if (score >= 50) scoreColor = chalk.magenta.bold;
120
+ const whaleLines = getWhaleMascot(score, scoreColor);
121
+ const shareUrl = `https://github.com/PunGrumpy/docker-doctor/share?s=${score}&w=${Object.values(categoryIssueCounts).reduce((a, b) => a + b, 0)}`;
122
+ if (isTTY) {
123
+ process.stdout.write("\x1B[?25l");
124
+ try {
125
+ for (let frame = 0; frame <= frameCount; frame += 1) {
126
+ const progress = easeOutCubic(frame / frameCount);
127
+ const currentScore = Math.round(score * progress);
128
+ const filledBlocks = Math.round(currentScore / 2);
129
+ const emptyBlocks = 50 - filledBlocks;
130
+ const bar = scoreColor("█".repeat(filledBlocks)) + chalk.dim("░".repeat(emptyBlocks));
131
+ if (frame > 0) process.stdout.write("\x1B[4A\r");
132
+ else console.log();
133
+ process.stdout.write(` ${whaleLines[0]} ${scoreColor(`${currentScore} / 100`)} ${scoreColor(label)}\n ${whaleLines[1]} ${bar}\n ${whaleLines[2]} ${chalk.dim("Docker Doctor (https://github.com/PunGrumpy/docker-doctor)")}\n ${whaleLines[3]}\n`);
134
+ if (frame < frameCount) await setTimeout(frameDelay);
135
+ }
136
+ } finally {
137
+ process.stdout.write("\x1B[?25h");
138
+ }
139
+ } else {
140
+ const filledBlocks = Math.round(score / 2);
141
+ const emptyBlocks = 50 - filledBlocks;
142
+ const bar = scoreColor("█".repeat(filledBlocks)) + chalk.dim("░".repeat(emptyBlocks));
143
+ console.log(`\n ${whaleLines[0]} ${scoreColor(`${score} / 100`)} ${scoreColor(label)}`);
144
+ console.log(` ${whaleLines[1]} ${bar}`);
145
+ console.log(` ${whaleLines[2]} ${chalk.dim("Docker Doctor (https://github.com/PunGrumpy/docker-doctor)")}`);
146
+ console.log(` ${whaleLines[3]}`);
147
+ }
148
+ console.log(`\n ${chalk.dim("────────────────────────────────────────────────────────────")}\n`);
149
+ console.log(` Share: ${chalk.cyan(shareUrl)}`);
150
+ console.log(` Tell others how you did on socials\n`);
151
+ console.log(` Docs: ${chalk.cyan("https://github.com/PunGrumpy/docker-doctor/docs")}`);
152
+ console.log(` Learn more about fixing issues, setting up CI/CD, and`);
153
+ console.log(` configuring rules with a config file\n`);
154
+ console.log(` GitHub: ${chalk.cyan("https://github.com/PunGrumpy/docker-doctor")}`);
155
+ console.log(` Report issues and star the repository!`);
156
+ };
157
+ const formatTerminal = async (diagnostics, score, label, project, verbose = false, fileContents = {}) => {
158
+ if (verbose) {
159
+ console.log(`\n${chalk.bold("Docker Doctor Diagnostics")}`);
160
+ console.log(`=================================`);
161
+ printDiscoveredFiles(project);
162
+ }
163
+ const categoryIssueCounts = {
164
+ "Best Practices": 0,
165
+ Compose: 0,
166
+ "Image Size": 0,
167
+ Performance: 0,
168
+ Security: 0
169
+ };
170
+ for (const d of diagnostics) {
171
+ const category = findRule(d.rule)?.category || "Best Practices";
172
+ categoryIssueCounts[category] = (categoryIssueCounts[category] || 0) + 1;
173
+ }
174
+ printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts);
175
+ await printScoreBox(score, label, categoryIssueCounts);
176
+ };
177
+
178
+ //#endregion
179
+ //#region src/cli.ts
180
+ const runInteractiveWizard = async () => {
181
+ const rl = readline.createInterface({
182
+ input: stdin,
183
+ output: stdout
184
+ });
185
+ try {
186
+ if ((await rl.question(`\n ${chalk.green("✔")} ${chalk.bold("Add Docker Doctor to GitHub Actions?")}\n Scan every pull request to prevent new Docker issues while you fix the backlog.\n › (y/N) `)).trim().toLowerCase() === "y") {
187
+ const workflowDir = path.resolve(".github/workflows");
188
+ await fs.mkdir(workflowDir, { recursive: true });
189
+ const workflowPath = path.join(workflowDir, "docker-doctor.yml");
190
+ await fs.writeFile(workflowPath, `name: Docker Doctor Scan
191
+ on:
192
+ push:
193
+ branches: [ main, master ]
194
+ pull_request:
195
+ branches: [ main, master ]
196
+ jobs:
197
+ docker-doctor:
198
+ runs-on: ubuntu-latest
199
+ steps:
200
+ - uses: actions/checkout@v4
201
+ - name: Setup Bun
202
+ uses: oven-sh/setup-bun@v2
203
+ - name: Install dependencies
204
+ run: bun install
205
+ - name: Run docker-doctor
206
+ run: bunx docker-doctor .
207
+ `, "utf-8");
208
+ console.log(`\n ${chalk.green("✨")} Created ${chalk.cyan(".github/workflows/docker-doctor.yml")}!`);
209
+ console.log(` Scan every pull request to prevent new Docker issues while you fix the backlog.`);
210
+ }
211
+ if ((await rl.question(`\n ${chalk.green("✔")} ${chalk.bold("What would you like to do next?")}\n 1: View rules list\n 2: Skip\n › `)).trim() === "1") {
212
+ console.log(`\n ${chalk.bold("Available Rules:")}`);
213
+ for (const r of allRules) console.log(` - ${chalk.cyan(r.key)}: ${r.message} (${chalk.dim(r.category)})`);
214
+ }
215
+ } catch {} finally {
216
+ rl.close();
217
+ }
218
+ };
219
+ const runRulesEngine = async (rootDir, project, rulesConfig, projectFilesList, fileContents, options, setStatus) => {
220
+ const diagnostics = [];
221
+ const isSilent = options.score || options.json;
222
+ if (process.stdout.isTTY && !isSilent) {
223
+ setStatus(`Analyzing ${project.dockerfiles.length} Dockerfile(s)...`);
224
+ await setTimeout(100);
225
+ }
226
+ const dockerfileResults = await Promise.all(project.dockerfiles.map(async (df) => {
227
+ const fullPath = path.join(rootDir, df);
228
+ try {
229
+ const content = await fs.readFile(fullPath, "utf-8");
230
+ fileContents[df] = content;
231
+ return runDockerfileRules(parseDockerfile(content), df, projectFilesList, rulesConfig);
232
+ } catch (error) {
233
+ const msg = error instanceof Error ? error.message : String(error);
234
+ console.error(`Failed to analyze Dockerfile ${df}: ${msg}`);
235
+ return [];
236
+ }
237
+ }));
238
+ for (const diags of dockerfileResults) diagnostics.push(...diags);
239
+ if (process.stdout.isTTY && !isSilent) {
240
+ setStatus(`Analyzing ${project.composeFiles.length} Compose file(s)...`);
241
+ await setTimeout(100);
242
+ }
243
+ const composeResults = await Promise.all(project.composeFiles.map(async (cf) => {
244
+ const fullPath = path.join(rootDir, cf);
245
+ try {
246
+ const content = await fs.readFile(fullPath, "utf-8");
247
+ fileContents[cf] = content;
248
+ return runComposeRules(parseCompose(content, cf), cf, rulesConfig);
249
+ } catch (error) {
250
+ const msg = error instanceof Error ? error.message : String(error);
251
+ console.error(`Failed to analyze Compose file ${cf}: ${msg}`);
252
+ return [];
253
+ }
254
+ }));
255
+ for (const diags of composeResults) diagnostics.push(...diags);
256
+ return diagnostics;
257
+ };
258
+ const program = new Command();
259
+ program.name("docker-doctor").description("Static analysis for Dockerfile and Docker Compose files").version(package_default.version, "-V, --version", "display the version number");
260
+ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "show verbose diagnostics description", false).option("-s, --score", "only output numeric health score", false).option("-j, --json", "output results as JSON report", false).option("-c, --config <path>", "custom config file path").action(async (dir, options) => {
261
+ try {
262
+ const rootDir = path.resolve(dir);
263
+ const startTime = Date.now();
264
+ const isSilent = options.score || options.json;
265
+ let statusText = "Discovering workspace...";
266
+ const spinnerFrames = [
267
+ "⠋",
268
+ "⠙",
269
+ "⠹",
270
+ "⠸",
271
+ "⠼",
272
+ "⠴",
273
+ "⠦",
274
+ "⠧",
275
+ "⠇",
276
+ "⠏"
277
+ ];
278
+ let frameIndex = 0;
279
+ let spinnerInterval = null;
280
+ const setStatus = (text) => {
281
+ statusText = text;
282
+ };
283
+ if (process.stdout.isTTY && !isSilent) {
284
+ process.stdout.write(`${chalk.cyan(spinnerFrames[0])} ${statusText}`);
285
+ spinnerInterval = setInterval(() => {
286
+ process.stdout.write(`\r${chalk.cyan(spinnerFrames[frameIndex])} ${statusText}`);
287
+ frameIndex = (frameIndex + 1) % spinnerFrames.length;
288
+ }, 80);
289
+ }
290
+ try {
291
+ if (process.stdout.isTTY && !isSilent) await setTimeout(150);
292
+ setStatus("Loading configuration...");
293
+ const config = await loadConfig(rootDir, options.config);
294
+ setStatus("Scanning workspace files...");
295
+ const project = await discoverProject(rootDir);
296
+ const fileContents = {};
297
+ const projectFilesList = [...project.dockerfiles, ...project.composeFiles];
298
+ const diagnostics = await runRulesEngine(rootDir, project, config.rules, projectFilesList, fileContents, options, setStatus);
299
+ let filteredDiagnostics = diagnostics;
300
+ if (config.categories) filteredDiagnostics = diagnostics.filter((d) => {
301
+ const ruleDef = findRule(d.rule);
302
+ if (ruleDef) {
303
+ if (config.categories?.[ruleDef.category] === "off") return false;
304
+ }
305
+ return true;
306
+ });
307
+ const { score, label } = calculateScore(filteredDiagnostics);
308
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
309
+ const concurrency = os.cpus().length;
310
+ if (spinnerInterval !== null) {
311
+ clearInterval(spinnerInterval);
312
+ spinnerInterval = null;
313
+ process.stdout.write("\r\x1B[K");
314
+ }
315
+ if (process.stdout.isTTY && !isSilent) console.log(`${chalk.green("✔")} Scanned ${projectFilesList.length} files in ${duration}s [~${concurrency} workers]`);
316
+ if (options.score) {
317
+ console.log(score);
318
+ process.exit(score < 50 ? 1 : 0);
319
+ } else if (options.json) {
320
+ const report = toJsonReport(filteredDiagnostics, score, label, project);
321
+ console.log(JSON.stringify(report, null, 2));
322
+ const hasErrors = filteredDiagnostics.some((d) => d.severity === "error");
323
+ process.exit(hasErrors ? 1 : 0);
324
+ } else {
325
+ await formatTerminal(filteredDiagnostics, score, label, project, options.verbose, fileContents);
326
+ const hasErrors = filteredDiagnostics.some((d) => d.severity === "error");
327
+ if (process.stdout.isTTY) await runInteractiveWizard();
328
+ process.exit(hasErrors ? 1 : 0);
329
+ }
330
+ } finally {
331
+ if (spinnerInterval !== null) {
332
+ clearInterval(spinnerInterval);
333
+ process.stdout.write("\r\x1B[K");
334
+ }
335
+ }
336
+ } catch (error) {
337
+ const msg = error instanceof Error ? error.message : String(error);
338
+ console.error(`Error: ${msg}`);
339
+ process.exit(1);
340
+ }
341
+ });
342
+ const rules = program.command("rules").description("manage and list configuration rules");
343
+ rules.command("list").description("list all available rules").action(() => {
344
+ console.log("\nAvailable Rules:");
345
+ console.log("================\n");
346
+ for (const rule of allRules) {
347
+ console.log(`- ${rule.key} (${rule.category})`);
348
+ console.log(` Default Severity: ${rule.defaultSeverity}`);
349
+ console.log(` Description: ${rule.message}\n`);
350
+ }
351
+ });
352
+ rules.command("explain <rule>").description("explain a specific rule in detail").action((ruleKey) => {
353
+ const rule = findRule(ruleKey);
354
+ if (!rule) {
355
+ console.error(`Rule '${ruleKey}' not found.`);
356
+ process.exit(1);
357
+ }
358
+ console.log(`\nRule: ${rule.key}`);
359
+ console.log(`Category: ${rule.category}`);
360
+ console.log(`Default Severity: ${rule.defaultSeverity}`);
361
+ console.log(`Description: ${rule.message}`);
362
+ console.log(`Help / Fix: ${rule.help}\n`);
363
+ });
364
+ program.parse(process.argv);
365
+
366
+ //#endregion
367
+ export { };
368
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":["border","packageJson"],"sources":["../src/formatters/terminal.ts","../src/cli.ts"],"sourcesContent":["import { setTimeout } from \"node:timers/promises\";\n\nimport type { Diagnostic, ProjectInfo } from \"@docker-doctor/core\";\nimport { findRule } from \"@docker-doctor/core\";\nimport chalk from \"chalk\";\n\nconst printCodeFrame = (\n content: string | undefined,\n line: number | undefined,\n severityColor: (msg: string) => string\n): void => {\n if (!content || !line) {\n return;\n }\n const lines = content.split(/\\r?\\n/u);\n const start = Math.max(1, line - 1);\n const end = Math.min(lines.length, line + 1);\n\n for (let i = start; i <= end; i += 1) {\n const rawLine = lines[i - 1];\n const isTarget = i === line;\n const lineNumberStr = String(i).padStart(5, \" \");\n if (isTarget) {\n console.log(\n ` ${severityColor(\">\")} ${chalk.bold(lineNumberStr)} │ ${chalk.white(rawLine)}`\n );\n } else {\n console.log(` ${chalk.dim(lineNumberStr)} │ ${chalk.dim(rawLine)}`);\n }\n }\n console.log();\n};\n\nconst printDiscoveredFiles = (project: ProjectInfo): void => {\n console.log(`\\nDiscovered Files:`);\n console.log(\n ` Dockerfile(s): ${project.dockerfiles.length ? project.dockerfiles.map((f) => chalk.cyan(f)).join(\", \") : chalk.dim(\"None\")}`\n );\n console.log(\n ` Compose file(s): ${project.composeFiles.length ? project.composeFiles.map((f) => chalk.cyan(f)).join(\", \") : chalk.dim(\"None\")}`\n );\n};\n\nconst printDiagnostics = (\n diagnostics: Diagnostic[],\n verbose: boolean,\n fileContents: Record<string, string>,\n categoryIssueCounts: Record<string, number>\n): void => {\n if (diagnostics.length === 0) {\n console.log(\n `\\n${chalk.green.bold(\"✔ No issues found! Your Docker setup looks healthy.\")}`\n );\n return;\n }\n\n if (!verbose) {\n console.log(`\\n All ${chalk.bold(diagnostics.length)} issues\\n`);\n\n const categories = [\n \"Security\",\n \"Performance\",\n \"Best Practices\",\n \"Compose\",\n \"Image Size\",\n ];\n\n for (const cat of categories) {\n const count = categoryIssueCounts[cat];\n const issueLabel = count === 1 ? \"1 issue\" : `${count} issues`;\n console.log(` ${cat} › ${chalk.dim(issueLabel)}`);\n }\n\n console.log(\n `\\n Run ${chalk.cyan(\"docker-doctor --verbose\")} to list every error and warning`\n );\n\n // Migration-scale checks\n const ruleCounts: Record<string, number> = {};\n for (const d of diagnostics) {\n ruleCounts[d.rule] = (ruleCounts[d.rule] || 0) + 1;\n }\n const migrationRules = Object.entries(ruleCounts).filter(\n ([_, count]) => count >= 5\n );\n if (migrationRules.length > 0) {\n console.log();\n console.log(\n ` ${chalk.yellow(\"⚠ Migration-scale change: sample before you sweep\")}`\n );\n for (const [rule, count] of migrationRules) {\n console.log(` ${chalk.cyan(rule)} ×${count} across ${count} files`);\n }\n console.log(\n ` Fixing all of them at once is hard to review and prone to`\n );\n console.log(\n ` subtle mistakes across the whole repo. Fix a representative`\n );\n console.log(\n ` few first and confirm the recipe holds. Then get the code`\n );\n console.log(` owner's sign-off before changing the rest.`);\n console.log(` Scope it down one area at a time: docker-doctor <path>`);\n }\n return;\n }\n\n console.log(`\\nFound ${chalk.bold(diagnostics.length)} issue(s):`);\n\n // Group by file\n const filesGrouped: Record<string, Diagnostic[]> = {};\n for (const d of diagnostics) {\n if (!filesGrouped[d.file]) {\n filesGrouped[d.file] = [];\n }\n filesGrouped[d.file].push(d);\n }\n\n for (const [file, fileDiags] of Object.entries(filesGrouped)) {\n console.log(`\\n${chalk.underline.bold(file)}`);\n for (const d of fileDiags) {\n let sevColor = chalk.cyan;\n let prefix = \"ℹ INFO\";\n if (d.severity === \"error\") {\n sevColor = chalk.red.bold;\n prefix = \"✖ ERROR\";\n } else if (d.severity === \"warning\") {\n sevColor = chalk.yellow;\n prefix = \"⚠ WARN\";\n }\n\n const lineInfo = d.line ? `:${d.line}` : \"\";\n console.log(` ${sevColor(prefix)} [${chalk.dim(d.rule)}]${lineInfo}`);\n\n // Print Code Frame\n const content = fileContents[file];\n printCodeFrame(content, d.line, sevColor);\n\n console.log(` ${chalk.white(d.message)}`);\n console.log(` ${chalk.dim(\"Help:\")} ${d.help}`);\n console.log();\n }\n }\n};\n\nconst getWhaleMascot = (\n score: number,\n border: (text: string) => string\n): string[] => {\n let eyes = \"x x\";\n let spout = \" \";\n if (score >= 75) {\n eyes = \"◠ ◠\";\n spout = chalk.cyan(' \":\" ');\n } else if (score >= 50) {\n eyes = \"• •\";\n spout = chalk.cyan(\" . \");\n }\n\n return [\n spout,\n border(\" .---. \"),\n border(`( ${eyes} )>`),\n border(\" \\\\___/ \"),\n ];\n};\n\nconst easeOutCubic = (x: number): number => 1 - (1 - x) ** 3;\n\nconst printScoreBox = async (\n score: number,\n label: string,\n categoryIssueCounts: Record<string, number>\n): Promise<void> => {\n const { isTTY } = process.stdout;\n const frameCount = 40;\n const frameDelay = 50;\n\n let scoreColor = chalk.red.bold;\n if (score >= 90) {\n scoreColor = chalk.green.bold;\n } else if (score >= 75) {\n scoreColor = chalk.yellow.bold;\n } else if (score >= 50) {\n scoreColor = chalk.magenta.bold;\n }\n const border = scoreColor;\n const whaleLines = getWhaleMascot(score, border);\n\n const totalIssues = Object.values(categoryIssueCounts).reduce(\n (a, b) => a + b,\n 0\n );\n const shareUrl = `https://github.com/PunGrumpy/docker-doctor/share?s=${score}&w=${totalIssues}`;\n\n if (isTTY) {\n // Hide cursor\n process.stdout.write(\"\\u001B[?25l\");\n try {\n for (let frame = 0; frame <= frameCount; frame += 1) {\n const progress = easeOutCubic(frame / frameCount);\n const currentScore = Math.round(score * progress);\n const filledBlocks = Math.round(currentScore / 2);\n const emptyBlocks = 50 - filledBlocks;\n\n const bar =\n scoreColor(\"█\".repeat(filledBlocks)) +\n chalk.dim(\"░\".repeat(emptyBlocks));\n\n if (frame > 0) {\n // Move cursor up 4 lines and carriage return\n process.stdout.write(\"\\u001B[4A\\r\");\n } else {\n // print an extra newline first to start\n console.log();\n }\n\n process.stdout.write(\n ` ${whaleLines[0]} ${scoreColor(`${currentScore} / 100`)} ${scoreColor(label)}\\n` +\n ` ${whaleLines[1]} ${bar}\\n` +\n ` ${whaleLines[2]} ${chalk.dim(\"Docker Doctor (https://github.com/PunGrumpy/docker-doctor)\")}\\n` +\n ` ${whaleLines[3]}\\n`\n );\n\n if (frame < frameCount) {\n // eslint-disable-next-line no-await-in-loop\n await setTimeout(frameDelay);\n }\n }\n } finally {\n // Show cursor\n process.stdout.write(\"\\u001B[?25h\");\n }\n } else {\n // Non-TTY fall back to static print\n const filledBlocks = Math.round(score / 2);\n const emptyBlocks = 50 - filledBlocks;\n const bar =\n scoreColor(\"█\".repeat(filledBlocks)) + chalk.dim(\"░\".repeat(emptyBlocks));\n\n console.log(\n `\\n ${whaleLines[0]} ${scoreColor(`${score} / 100`)} ${scoreColor(label)}`\n );\n console.log(` ${whaleLines[1]} ${bar}`);\n console.log(\n ` ${whaleLines[2]} ${chalk.dim(\"Docker Doctor (https://github.com/PunGrumpy/docker-doctor)\")}`\n );\n console.log(` ${whaleLines[3]}`);\n }\n\n console.log(\n `\\n ${chalk.dim(\"────────────────────────────────────────────────────────────\")}\\n`\n );\n console.log(` Share: ${chalk.cyan(shareUrl)}`);\n console.log(` Tell others how you did on socials\\n`);\n console.log(\n ` Docs: ${chalk.cyan(\"https://github.com/PunGrumpy/docker-doctor/docs\")}`\n );\n console.log(` Learn more about fixing issues, setting up CI/CD, and`);\n console.log(` configuring rules with a config file\\n`);\n console.log(\n ` GitHub: ${chalk.cyan(\"https://github.com/PunGrumpy/docker-doctor\")}`\n );\n console.log(` Report issues and star the repository!`);\n};\n\nexport const formatTerminal = async (\n diagnostics: Diagnostic[],\n score: number,\n label: string,\n project: ProjectInfo,\n verbose = false,\n fileContents: Record<string, string> = {}\n): Promise<void> => {\n if (verbose) {\n console.log(`\\n${chalk.bold(\"Docker Doctor Diagnostics\")}`);\n console.log(`=================================`);\n printDiscoveredFiles(project);\n }\n\n // Calculate scores per category once\n const categoryIssueCounts: Record<string, number> = {\n \"Best Practices\": 0,\n Compose: 0,\n \"Image Size\": 0,\n Performance: 0,\n Security: 0,\n };\n\n for (const d of diagnostics) {\n const ruleDef = findRule(d.rule);\n const category = ruleDef?.category || \"Best Practices\";\n categoryIssueCounts[category] = (categoryIssueCounts[category] || 0) + 1;\n }\n\n printDiagnostics(diagnostics, verbose, fileContents, categoryIssueCounts);\n\n await printScoreBox(score, label, categoryIssueCounts);\n};\n","import fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { stdin as input, stdout as output } from \"node:process\";\nimport readline from \"node:readline/promises\";\nimport { setTimeout } from \"node:timers/promises\";\n\nimport type { Diagnostic, RuleSeverity } from \"@docker-doctor/core\";\nimport {\n discoverProject,\n parseDockerfile,\n parseCompose,\n runDockerfileRules,\n runComposeRules,\n calculateScore,\n loadConfig,\n allRules,\n findRule,\n toJsonReport,\n} from \"@docker-doctor/core\";\nimport chalk from \"chalk\";\nimport { Command } from \"commander\";\n\nimport packageJson from \"../package.json\" with { type: \"json\" };\nimport { formatTerminal } from \"./formatters/terminal.js\";\n\nconst runInteractiveWizard = async (): Promise<void> => {\n const rl = readline.createInterface({ input, output });\n try {\n const ghAnswer = await rl.question(\n `\\n ${chalk.green(\"✔\")} ${chalk.bold(\"Add Docker Doctor to GitHub Actions?\")}\\n` +\n ` Scan every pull request to prevent new Docker issues while you fix the backlog.\\n` +\n ` › (y/N) `\n );\n if (ghAnswer.trim().toLowerCase() === \"y\") {\n const workflowDir = path.resolve(\".github/workflows\");\n await fs.mkdir(workflowDir, { recursive: true });\n const workflowPath = path.join(workflowDir, \"docker-doctor.yml\");\n const workflowYaml = `name: Docker Doctor Scan\non:\n push:\n branches: [ main, master ]\n pull_request:\n branches: [ main, master ]\njobs:\n docker-doctor:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - name: Setup Bun\n uses: oven-sh/setup-bun@v2\n - name: Install dependencies\n run: bun install\n - name: Run docker-doctor\n run: bunx docker-doctor .\n`;\n await fs.writeFile(workflowPath, workflowYaml, \"utf-8\");\n console.log(\n `\\n ${chalk.green(\"✨\")} Created ${chalk.cyan(\".github/workflows/docker-doctor.yml\")}!`\n );\n console.log(\n ` Scan every pull request to prevent new Docker issues while you fix the backlog.`\n );\n }\n\n const nextAnswer = await rl.question(\n `\\n ${chalk.green(\"✔\")} ${chalk.bold(\"What would you like to do next?\")}\\n` +\n ` 1: View rules list\\n` +\n ` 2: Skip\\n` +\n ` › `\n );\n if (nextAnswer.trim() === \"1\") {\n console.log(`\\n ${chalk.bold(\"Available Rules:\")}`);\n for (const r of allRules) {\n console.log(\n ` - ${chalk.cyan(r.key)}: ${r.message} (${chalk.dim(r.category)})`\n );\n }\n }\n } catch {\n // Ignore prompt errors\n } finally {\n rl.close();\n }\n};\n\nconst runRulesEngine = async (\n rootDir: string,\n project: { dockerfiles: string[]; composeFiles: string[] },\n rulesConfig: Record<string, RuleSeverity> | undefined,\n projectFilesList: string[],\n fileContents: Record<string, string>,\n options: { score?: boolean; json?: boolean },\n setStatus: (text: string) => void\n): Promise<Diagnostic[]> => {\n const diagnostics: Diagnostic[] = [];\n\n const isSilent = options.score || options.json;\n\n if (process.stdout.isTTY && !isSilent) {\n setStatus(`Analyzing ${project.dockerfiles.length} Dockerfile(s)...`);\n await setTimeout(100);\n }\n\n // 1. Scan Dockerfiles in parallel\n const dockerfileResults = await Promise.all(\n project.dockerfiles.map(async (df) => {\n const fullPath = path.join(rootDir, df);\n try {\n const content = await fs.readFile(fullPath, \"utf-8\");\n fileContents[df] = content;\n const instructions = parseDockerfile(content);\n return runDockerfileRules(\n instructions,\n df,\n projectFilesList,\n rulesConfig\n );\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n console.error(`Failed to analyze Dockerfile ${df}: ${msg}`);\n return [];\n }\n })\n );\n for (const diags of dockerfileResults) {\n diagnostics.push(...diags);\n }\n\n if (process.stdout.isTTY && !isSilent) {\n setStatus(`Analyzing ${project.composeFiles.length} Compose file(s)...`);\n await setTimeout(100);\n }\n\n // 2. Scan Compose files in parallel\n const composeResults = await Promise.all(\n project.composeFiles.map(async (cf) => {\n const fullPath = path.join(rootDir, cf);\n try {\n const content = await fs.readFile(fullPath, \"utf-8\");\n fileContents[cf] = content;\n const composeObj = parseCompose(content, cf);\n return runComposeRules(composeObj, cf, rulesConfig);\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n console.error(`Failed to analyze Compose file ${cf}: ${msg}`);\n return [];\n }\n })\n );\n for (const diags of composeResults) {\n diagnostics.push(...diags);\n }\n\n return diagnostics;\n};\n\nconst program = new Command();\n\nprogram\n .name(\"docker-doctor\")\n .description(\"Static analysis for Dockerfile and Docker Compose files\")\n .version(packageJson.version, \"-V, --version\", \"display the version number\");\n\n// Default scan command\nprogram\n .argument(\"[dir]\", \"directory to scan\", \".\")\n .option(\"-v, --verbose\", \"show verbose diagnostics description\", false)\n .option(\"-s, --score\", \"only output numeric health score\", false)\n .option(\"-j, --json\", \"output results as JSON report\", false)\n .option(\"-c, --config <path>\", \"custom config file path\")\n .action(async (dir, options) => {\n try {\n const rootDir = path.resolve(dir);\n const startTime = Date.now();\n\n const isSilent = options.score || options.json;\n\n let statusText = \"Discovering workspace...\";\n const spinnerFrames = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n let frameIndex = 0;\n let spinnerInterval: NodeJS.Timeout | null = null;\n\n const setStatus = (text: string) => {\n statusText = text;\n };\n\n if (process.stdout.isTTY && !isSilent) {\n process.stdout.write(`${chalk.cyan(spinnerFrames[0])} ${statusText}`);\n spinnerInterval = setInterval(() => {\n process.stdout.write(\n `\\r${chalk.cyan(spinnerFrames[frameIndex])} ${statusText}`\n );\n frameIndex = (frameIndex + 1) % spinnerFrames.length;\n }, 80);\n }\n\n try {\n if (process.stdout.isTTY && !isSilent) {\n await setTimeout(150);\n }\n\n setStatus(\"Loading configuration...\");\n // Load config first\n const config = await loadConfig(rootDir, options.config);\n\n setStatus(\"Scanning workspace files...\");\n // Project discovery\n const project = await discoverProject(rootDir);\n\n // Collect all diagnostics\n const fileContents: Record<string, string> = {};\n\n const projectFilesList = [\n ...project.dockerfiles,\n ...project.composeFiles,\n ];\n\n const diagnostics = await runRulesEngine(\n rootDir,\n project,\n config.rules,\n projectFilesList,\n fileContents,\n options,\n setStatus\n );\n\n // Filter by category config if needed\n let filteredDiagnostics = diagnostics;\n if (config.categories) {\n filteredDiagnostics = diagnostics.filter((d) => {\n const ruleDef = findRule(d.rule);\n if (ruleDef) {\n const catSeverity = config.categories?.[ruleDef.category];\n if (catSeverity === \"off\") {\n return false;\n }\n }\n return true;\n });\n }\n\n // Calculate score\n const { score, label } = calculateScore(filteredDiagnostics);\n\n const duration = ((Date.now() - startTime) / 1000).toFixed(1);\n const concurrency = os.cpus().length;\n\n if (spinnerInterval !== null) {\n clearInterval(spinnerInterval);\n spinnerInterval = null;\n // Clear the spinner line\n process.stdout.write(\"\\r\\u001B[K\");\n }\n\n if (process.stdout.isTTY && !isSilent) {\n console.log(\n `${chalk.green(\"✔\")} Scanned ${projectFilesList.length} files in ${duration}s [~${concurrency} workers]`\n );\n }\n\n if (options.score) {\n console.log(score);\n process.exit(score < 50 ? 1 : 0);\n } else if (options.json) {\n const report = toJsonReport(\n filteredDiagnostics,\n score,\n label,\n project\n );\n console.log(JSON.stringify(report, null, 2));\n const hasErrors = filteredDiagnostics.some(\n (d) => d.severity === \"error\"\n );\n process.exit(hasErrors ? 1 : 0);\n } else {\n await formatTerminal(\n filteredDiagnostics,\n score,\n label,\n project,\n options.verbose,\n fileContents\n );\n\n // Exit with non-zero code if there are any error severity diagnostics\n const hasErrors = filteredDiagnostics.some(\n (d) => d.severity === \"error\"\n );\n\n if (process.stdout.isTTY) {\n await runInteractiveWizard();\n }\n process.exit(hasErrors ? 1 : 0);\n }\n } finally {\n if (spinnerInterval !== null) {\n clearInterval(spinnerInterval);\n process.stdout.write(\"\\r\\u001B[K\");\n }\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n console.error(`Error: ${msg}`);\n process.exit(1);\n }\n });\n\n// Rules subcommand group\nconst rules = program\n .command(\"rules\")\n .description(\"manage and list configuration rules\");\n\nrules\n .command(\"list\")\n .description(\"list all available rules\")\n .action(() => {\n console.log(\"\\nAvailable Rules:\");\n console.log(\"================\\n\");\n for (const rule of allRules) {\n console.log(`- ${rule.key} (${rule.category})`);\n console.log(` Default Severity: ${rule.defaultSeverity}`);\n console.log(` Description: ${rule.message}\\n`);\n }\n });\n\nrules\n .command(\"explain <rule>\")\n .description(\"explain a specific rule in detail\")\n .action((ruleKey) => {\n const rule = findRule(ruleKey);\n if (!rule) {\n console.error(`Rule '${ruleKey}' not found.`);\n process.exit(1);\n }\n console.log(`\\nRule: ${rule.key}`);\n console.log(`Category: ${rule.category}`);\n console.log(`Default Severity: ${rule.defaultSeverity}`);\n console.log(`Description: ${rule.message}`);\n console.log(`Help / Fix: ${rule.help}\\n`);\n });\n\nprogram.parse(process.argv);\n"],"mappings":"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"}
package/dist/index.cjs ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
3
+ const require_src = require('./src-D3U-LUsr.cjs');
4
+
5
+ //#region src/index.ts
6
+ const { version } = require_src.package_default;
7
+
8
+ //#endregion
9
+ exports.allRules = require_src.allRules;
10
+ exports.calculateScore = require_src.calculateScore;
11
+ exports.discoverProject = require_src.discoverProject;
12
+ exports.findRule = require_src.findRule;
13
+ exports.loadConfig = require_src.loadConfig;
14
+ exports.parseCompose = require_src.parseCompose;
15
+ exports.parseDockerfile = require_src.parseDockerfile;
16
+ exports.runComposeRules = require_src.runComposeRules;
17
+ exports.runDockerfileRules = require_src.runDockerfileRules;
18
+ exports.toJsonReport = require_src.toJsonReport;
19
+ exports.version = version;
20
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["packageJson"],"sources":["../src/index.ts"],"sourcesContent":["import packageJson from \"../package.json\" with { type: \"json\" };\n\n// Re-export core programmatic API\nexport {\n discoverProject,\n parseDockerfile,\n parseCompose,\n runDockerfileRules,\n runComposeRules,\n calculateScore,\n loadConfig,\n allRules,\n findRule,\n toJsonReport,\n} from \"@docker-doctor/core\";\nexport type { Diagnostic, RuleSeverity } from \"@docker-doctor/core\";\nexport const { version } = packageJson;\n"],"mappings":";;;;;AAgBA,MAAa,EAAE,YAAYA"}
@@ -0,0 +1,99 @@
1
+
2
+ import * as Schema from "effect/Schema";
3
+ //#region ../core/src/types/index.d.ts
4
+ interface DockerfileInstruction {
5
+ instruction: string;
6
+ args: string;
7
+ line: number;
8
+ raw: string;
9
+ }
10
+ interface Diagnostic {
11
+ file: string;
12
+ rule: string;
13
+ severity: "error" | "warning" | "info";
14
+ message: string;
15
+ help: string;
16
+ line?: number;
17
+ column?: number;
18
+ }
19
+ interface ProjectInfo {
20
+ dockerfiles: string[];
21
+ composeFiles: string[];
22
+ }
23
+ type RuleCategory = "Security" | "Performance" | "Best Practices" | "Compose" | "Image Size";
24
+ type RuleSeverity = "error" | "warning" | "info" | "off";
25
+ interface RuleDefinition {
26
+ key: string;
27
+ category: RuleCategory;
28
+ defaultSeverity: RuleSeverity;
29
+ message: string;
30
+ help: string;
31
+ }
32
+ //#endregion
33
+ //#region ../core/src/project-info/discover.d.ts
34
+ declare const discoverProject: (rootDir: string) => Promise<ProjectInfo>;
35
+ //#endregion
36
+ //#region ../core/src/parsers/dockerfile-parser.d.ts
37
+ declare const parseDockerfile: (content: string) => DockerfileInstruction[];
38
+ //#endregion
39
+ //#region ../core/src/parsers/compose-parser.d.ts
40
+ declare const parseCompose: (content: string, filepath: string) => unknown;
41
+ //#endregion
42
+ //#region ../core/src/runners/dockerfile-runner.d.ts
43
+ declare const runDockerfileRules: (instructions: DockerfileInstruction[], file: string, projectFiles: string[], rulesConfig?: Record<string, RuleSeverity>) => Diagnostic[];
44
+ //#endregion
45
+ //#region ../core/src/runners/compose-runner.d.ts
46
+ declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>) => Diagnostic[];
47
+ //#endregion
48
+ //#region ../core/src/rules/index.d.ts
49
+ declare const allRules: RuleDefinition[];
50
+ declare const findRule: (key: string) => RuleDefinition | undefined;
51
+ //#endregion
52
+ //#region ../core/src/schemas/config.d.ts
53
+ declare const DockerDoctorConfigSchema: Schema.Struct<{
54
+ readonly categories: Schema.optional<Schema.Struct<{
55
+ readonly "Best Practices": Schema.optional<Schema.Union<readonly [Schema.Literal<"error">, Schema.Literal<"warning">, Schema.Literal<"info">, Schema.Literal<"off">]>>;
56
+ readonly Compose: Schema.optional<Schema.Union<readonly [Schema.Literal<"error">, Schema.Literal<"warning">, Schema.Literal<"info">, Schema.Literal<"off">]>>;
57
+ readonly "Image Size": Schema.optional<Schema.Union<readonly [Schema.Literal<"error">, Schema.Literal<"warning">, Schema.Literal<"info">, Schema.Literal<"off">]>>;
58
+ readonly Performance: Schema.optional<Schema.Union<readonly [Schema.Literal<"error">, Schema.Literal<"warning">, Schema.Literal<"info">, Schema.Literal<"off">]>>;
59
+ readonly Security: Schema.optional<Schema.Union<readonly [Schema.Literal<"error">, Schema.Literal<"warning">, Schema.Literal<"info">, Schema.Literal<"off">]>>;
60
+ }>>;
61
+ readonly ignore: Schema.optional<Schema.Struct<{
62
+ readonly files: Schema.optional<Schema.$Array<Schema.String>>;
63
+ }>>;
64
+ readonly rules: Schema.optional<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.Literal<"error">, Schema.Literal<"warning">, Schema.Literal<"info">, Schema.Literal<"off">]>>>;
65
+ }>;
66
+ type DockerDoctorConfig = Schema.Schema.Type<typeof DockerDoctorConfigSchema>;
67
+ //#endregion
68
+ //#region ../core/src/config/loader.d.ts
69
+ declare const loadConfig: (rootDir: string, customPath?: string) => Promise<DockerDoctorConfig>;
70
+ //#endregion
71
+ //#region ../core/src/scoring.d.ts
72
+ declare const calculateScore: (diagnostics: Diagnostic[]) => {
73
+ score: number;
74
+ label: string;
75
+ };
76
+ //#endregion
77
+ //#region ../core/src/report.d.ts
78
+ interface JsonReport {
79
+ diagnostics: {
80
+ column?: number;
81
+ file: string;
82
+ help: string;
83
+ line?: number;
84
+ message: string;
85
+ rule: string;
86
+ severity: "error" | "warning" | "info";
87
+ }[];
88
+ label: string;
89
+ project: ProjectInfo;
90
+ score: number;
91
+ timestamp: string;
92
+ }
93
+ declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo) => JsonReport;
94
+ //#endregion
95
+ //#region src/index.d.ts
96
+ declare const version: string;
97
+ //#endregion
98
+ export { type Diagnostic, type RuleSeverity, allRules, calculateScore, discoverProject, findRule, loadConfig, parseCompose, parseDockerfile, runComposeRules, runDockerfileRules, toJsonReport, version };
99
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../core/src/types/index.ts","../../core/src/project-info/discover.ts","../../core/src/parsers/dockerfile-parser.ts","../../core/src/parsers/compose-parser.ts","../../core/src/runners/dockerfile-runner.ts","../../core/src/runners/compose-runner.ts","../../core/src/rules/index.ts","../../core/src/schemas/config.ts","../../core/src/config/loader.ts","../../core/src/scoring.ts","../../core/src/report.ts","../src/index.ts"],"mappings":";;;UAAiB;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;;KAUU;KAMA;UAEK;EACf;EACA,UAAU;EACV,iBAAiB;EACjB;EACA;;;;cCVW,kBACX,oBACC,QAAQ;;;cChCE,kBAAmB,oBAAkB;;;cCErC,eAAgB,iBAAiB;;;cCGjC,qBACX,cAAc,yBACd,cACA,wBACA,cAAc,eAAe,kBAC5B;;;cCTU,kBACX,yBACA,cACA,cAAc,eAAe,kBAC5B;;;cCaU,UAAU;cAKV,WAAY,gBAAc;;;cChB1B,0BAAwB,OAAA;;;;;;;;;;;;;KAkBzB,qBAAqB,OAAO,OAAO,YACtC;;;cCcI,aACX,iBACA,wBACC,QAAQ;;;cC3CE,iBACX,aAAa;EAEb;EACA;;;;UCJe;EACf;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;EAEF;EACA,SAAS;EACT;EACA;;cAGW,eACX,aAAa,cACb,eACA,eACA,SAAS,gBACR;;;cCPY"}