@docker-doctor/cli 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,13 +1,651 @@
1
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-DvNFg6Nz.mjs";
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-BOzcDwm-.mjs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import os from "node:os";
6
6
  import readline from "node:readline";
7
7
  import { setTimeout } from "node:timers/promises";
8
- import chalk from "chalk";
8
+ import { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource, isSkillAgentType } from "agent-install";
9
+ import process$1 from "node:process";
10
+ import tty from "node:tty";
9
11
  import { Command } from "commander";
12
+ import { spawn } from "node:child_process";
13
+ import fs$1 from "node:fs";
10
14
 
15
+ //#region ../../node_modules/.bun/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
16
+ const ANSI_BACKGROUND_OFFSET = 10;
17
+ const wrapAnsi16 = (offset = 0) => (code) => `\u001B[${code + offset}m`;
18
+ const wrapAnsi256 = (offset = 0) => (code) => `\u001B[${38 + offset};5;${code}m`;
19
+ const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`;
20
+ const styles$1 = {
21
+ modifier: {
22
+ reset: [0, 0],
23
+ bold: [1, 22],
24
+ dim: [2, 22],
25
+ italic: [3, 23],
26
+ underline: [4, 24],
27
+ overline: [53, 55],
28
+ inverse: [7, 27],
29
+ hidden: [8, 28],
30
+ strikethrough: [9, 29]
31
+ },
32
+ color: {
33
+ black: [30, 39],
34
+ red: [31, 39],
35
+ green: [32, 39],
36
+ yellow: [33, 39],
37
+ blue: [34, 39],
38
+ magenta: [35, 39],
39
+ cyan: [36, 39],
40
+ white: [37, 39],
41
+ blackBright: [90, 39],
42
+ gray: [90, 39],
43
+ grey: [90, 39],
44
+ redBright: [91, 39],
45
+ greenBright: [92, 39],
46
+ yellowBright: [93, 39],
47
+ blueBright: [94, 39],
48
+ magentaBright: [95, 39],
49
+ cyanBright: [96, 39],
50
+ whiteBright: [97, 39]
51
+ },
52
+ bgColor: {
53
+ bgBlack: [40, 49],
54
+ bgRed: [41, 49],
55
+ bgGreen: [42, 49],
56
+ bgYellow: [43, 49],
57
+ bgBlue: [44, 49],
58
+ bgMagenta: [45, 49],
59
+ bgCyan: [46, 49],
60
+ bgWhite: [47, 49],
61
+ bgBlackBright: [100, 49],
62
+ bgGray: [100, 49],
63
+ bgGrey: [100, 49],
64
+ bgRedBright: [101, 49],
65
+ bgGreenBright: [102, 49],
66
+ bgYellowBright: [103, 49],
67
+ bgBlueBright: [104, 49],
68
+ bgMagentaBright: [105, 49],
69
+ bgCyanBright: [106, 49],
70
+ bgWhiteBright: [107, 49]
71
+ }
72
+ };
73
+ const modifierNames = Object.keys(styles$1.modifier);
74
+ const foregroundColorNames = Object.keys(styles$1.color);
75
+ const backgroundColorNames = Object.keys(styles$1.bgColor);
76
+ const colorNames = [...foregroundColorNames, ...backgroundColorNames];
77
+ function assembleStyles() {
78
+ const codes = /* @__PURE__ */ new Map();
79
+ for (const [groupName, group] of Object.entries(styles$1)) {
80
+ for (const [styleName, style] of Object.entries(group)) {
81
+ styles$1[styleName] = {
82
+ open: `\u001B[${style[0]}m`,
83
+ close: `\u001B[${style[1]}m`
84
+ };
85
+ group[styleName] = styles$1[styleName];
86
+ codes.set(style[0], style[1]);
87
+ }
88
+ Object.defineProperty(styles$1, groupName, {
89
+ value: group,
90
+ enumerable: false
91
+ });
92
+ }
93
+ Object.defineProperty(styles$1, "codes", {
94
+ value: codes,
95
+ enumerable: false
96
+ });
97
+ styles$1.color.close = "\x1B[39m";
98
+ styles$1.bgColor.close = "\x1B[49m";
99
+ styles$1.color.ansi = wrapAnsi16();
100
+ styles$1.color.ansi256 = wrapAnsi256();
101
+ styles$1.color.ansi16m = wrapAnsi16m();
102
+ styles$1.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
103
+ styles$1.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
104
+ styles$1.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
105
+ Object.defineProperties(styles$1, {
106
+ rgbToAnsi256: {
107
+ value(red, green, blue) {
108
+ if (red === green && green === blue) {
109
+ if (red < 8) return 16;
110
+ if (red > 248) return 231;
111
+ return Math.round((red - 8) / 247 * 24) + 232;
112
+ }
113
+ return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
114
+ },
115
+ enumerable: false
116
+ },
117
+ hexToRgb: {
118
+ value(hex) {
119
+ const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
120
+ if (!matches) return [
121
+ 0,
122
+ 0,
123
+ 0
124
+ ];
125
+ let [colorString] = matches;
126
+ if (colorString.length === 3) colorString = [...colorString].map((character) => character + character).join("");
127
+ const integer = Number.parseInt(colorString, 16);
128
+ return [
129
+ integer >> 16 & 255,
130
+ integer >> 8 & 255,
131
+ integer & 255
132
+ ];
133
+ },
134
+ enumerable: false
135
+ },
136
+ hexToAnsi256: {
137
+ value: (hex) => styles$1.rgbToAnsi256(...styles$1.hexToRgb(hex)),
138
+ enumerable: false
139
+ },
140
+ ansi256ToAnsi: {
141
+ value(code) {
142
+ if (code < 8) return 30 + code;
143
+ if (code < 16) return 90 + (code - 8);
144
+ let red;
145
+ let green;
146
+ let blue;
147
+ if (code >= 232) {
148
+ red = ((code - 232) * 10 + 8) / 255;
149
+ green = red;
150
+ blue = red;
151
+ } else {
152
+ code -= 16;
153
+ const remainder = code % 36;
154
+ red = Math.floor(code / 36) / 5;
155
+ green = Math.floor(remainder / 6) / 5;
156
+ blue = remainder % 6 / 5;
157
+ }
158
+ const value = Math.max(red, green, blue) * 2;
159
+ if (value === 0) return 30;
160
+ let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
161
+ if (value === 2) result += 60;
162
+ return result;
163
+ },
164
+ enumerable: false
165
+ },
166
+ rgbToAnsi: {
167
+ value: (red, green, blue) => styles$1.ansi256ToAnsi(styles$1.rgbToAnsi256(red, green, blue)),
168
+ enumerable: false
169
+ },
170
+ hexToAnsi: {
171
+ value: (hex) => styles$1.ansi256ToAnsi(styles$1.hexToAnsi256(hex)),
172
+ enumerable: false
173
+ }
174
+ });
175
+ return styles$1;
176
+ }
177
+ const ansiStyles = assembleStyles();
178
+
179
+ //#endregion
180
+ //#region ../../node_modules/.bun/chalk@5.6.2/node_modules/chalk/source/vendor/supports-color/index.js
181
+ function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process$1.argv) {
182
+ const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
183
+ const position = argv.indexOf(prefix + flag);
184
+ const terminatorPosition = argv.indexOf("--");
185
+ return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
186
+ }
187
+ const { env } = process$1;
188
+ let flagForceColor;
189
+ if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) flagForceColor = 0;
190
+ else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) flagForceColor = 1;
191
+ function envForceColor() {
192
+ if ("FORCE_COLOR" in env) {
193
+ if (env.FORCE_COLOR === "true") return 1;
194
+ if (env.FORCE_COLOR === "false") return 0;
195
+ return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
196
+ }
197
+ }
198
+ function translateLevel(level) {
199
+ if (level === 0) return false;
200
+ return {
201
+ level,
202
+ hasBasic: true,
203
+ has256: level >= 2,
204
+ has16m: level >= 3
205
+ };
206
+ }
207
+ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
208
+ const noFlagForceColor = envForceColor();
209
+ if (noFlagForceColor !== void 0) flagForceColor = noFlagForceColor;
210
+ const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
211
+ if (forceColor === 0) return 0;
212
+ if (sniffFlags) {
213
+ if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) return 3;
214
+ if (hasFlag("color=256")) return 2;
215
+ }
216
+ if ("TF_BUILD" in env && "AGENT_NAME" in env) return 1;
217
+ if (haveStream && !streamIsTTY && forceColor === void 0) return 0;
218
+ const min = forceColor || 0;
219
+ if (env.TERM === "dumb") return min;
220
+ if (process$1.platform === "win32") {
221
+ const osRelease = os.release().split(".");
222
+ if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
223
+ return 1;
224
+ }
225
+ if ("CI" in env) {
226
+ if ([
227
+ "GITHUB_ACTIONS",
228
+ "GITEA_ACTIONS",
229
+ "CIRCLECI"
230
+ ].some((key) => key in env)) return 3;
231
+ if ([
232
+ "TRAVIS",
233
+ "APPVEYOR",
234
+ "GITLAB_CI",
235
+ "BUILDKITE",
236
+ "DRONE"
237
+ ].some((sign) => sign in env) || env.CI_NAME === "codeship") return 1;
238
+ return min;
239
+ }
240
+ if ("TEAMCITY_VERSION" in env) return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
241
+ if (env.COLORTERM === "truecolor") return 3;
242
+ if (env.TERM === "xterm-kitty") return 3;
243
+ if (env.TERM === "xterm-ghostty") return 3;
244
+ if (env.TERM === "wezterm") return 3;
245
+ if ("TERM_PROGRAM" in env) {
246
+ const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
247
+ switch (env.TERM_PROGRAM) {
248
+ case "iTerm.app": return version >= 3 ? 3 : 2;
249
+ case "Apple_Terminal": return 2;
250
+ }
251
+ }
252
+ if (/-256(color)?$/i.test(env.TERM)) return 2;
253
+ if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) return 1;
254
+ if ("COLORTERM" in env) return 1;
255
+ return min;
256
+ }
257
+ function createSupportsColor(stream, options = {}) {
258
+ return translateLevel(_supportsColor(stream, {
259
+ streamIsTTY: stream && stream.isTTY,
260
+ ...options
261
+ }));
262
+ }
263
+ const supportsColor = {
264
+ stdout: createSupportsColor({ isTTY: tty.isatty(1) }),
265
+ stderr: createSupportsColor({ isTTY: tty.isatty(2) })
266
+ };
267
+
268
+ //#endregion
269
+ //#region ../../node_modules/.bun/chalk@5.6.2/node_modules/chalk/source/utilities.js
270
+ function stringReplaceAll(string, substring, replacer) {
271
+ let index = string.indexOf(substring);
272
+ if (index === -1) return string;
273
+ const substringLength = substring.length;
274
+ let endIndex = 0;
275
+ let returnValue = "";
276
+ do {
277
+ returnValue += string.slice(endIndex, index) + substring + replacer;
278
+ endIndex = index + substringLength;
279
+ index = string.indexOf(substring, endIndex);
280
+ } while (index !== -1);
281
+ returnValue += string.slice(endIndex);
282
+ return returnValue;
283
+ }
284
+ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
285
+ let endIndex = 0;
286
+ let returnValue = "";
287
+ do {
288
+ const gotCR = string[index - 1] === "\r";
289
+ returnValue += string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? "\r\n" : "\n") + postfix;
290
+ endIndex = index + 1;
291
+ index = string.indexOf("\n", endIndex);
292
+ } while (index !== -1);
293
+ returnValue += string.slice(endIndex);
294
+ return returnValue;
295
+ }
296
+
297
+ //#endregion
298
+ //#region ../../node_modules/.bun/chalk@5.6.2/node_modules/chalk/source/index.js
299
+ const { stdout: stdoutColor, stderr: stderrColor } = supportsColor;
300
+ const GENERATOR = Symbol("GENERATOR");
301
+ const STYLER = Symbol("STYLER");
302
+ const IS_EMPTY = Symbol("IS_EMPTY");
303
+ const levelMapping = [
304
+ "ansi",
305
+ "ansi",
306
+ "ansi256",
307
+ "ansi16m"
308
+ ];
309
+ const styles = Object.create(null);
310
+ const applyOptions = (object, options = {}) => {
311
+ if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) throw new Error("The `level` option should be an integer from 0 to 3");
312
+ const colorLevel = stdoutColor ? stdoutColor.level : 0;
313
+ object.level = options.level === void 0 ? colorLevel : options.level;
314
+ };
315
+ const chalkFactory = (options) => {
316
+ const chalk = (...strings) => strings.join(" ");
317
+ applyOptions(chalk, options);
318
+ Object.setPrototypeOf(chalk, createChalk.prototype);
319
+ return chalk;
320
+ };
321
+ function createChalk(options) {
322
+ return chalkFactory(options);
323
+ }
324
+ Object.setPrototypeOf(createChalk.prototype, Function.prototype);
325
+ for (const [styleName, style] of Object.entries(ansiStyles)) styles[styleName] = { get() {
326
+ const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
327
+ Object.defineProperty(this, styleName, { value: builder });
328
+ return builder;
329
+ } };
330
+ styles.visible = { get() {
331
+ const builder = createBuilder(this, this[STYLER], true);
332
+ Object.defineProperty(this, "visible", { value: builder });
333
+ return builder;
334
+ } };
335
+ const getModelAnsi = (model, level, type, ...arguments_) => {
336
+ if (model === "rgb") {
337
+ if (level === "ansi16m") return ansiStyles[type].ansi16m(...arguments_);
338
+ if (level === "ansi256") return ansiStyles[type].ansi256(ansiStyles.rgbToAnsi256(...arguments_));
339
+ return ansiStyles[type].ansi(ansiStyles.rgbToAnsi(...arguments_));
340
+ }
341
+ if (model === "hex") return getModelAnsi("rgb", level, type, ...ansiStyles.hexToRgb(...arguments_));
342
+ return ansiStyles[type][model](...arguments_);
343
+ };
344
+ for (const model of [
345
+ "rgb",
346
+ "hex",
347
+ "ansi256"
348
+ ]) {
349
+ styles[model] = { get() {
350
+ const { level } = this;
351
+ return function(...arguments_) {
352
+ const styler = createStyler(getModelAnsi(model, levelMapping[level], "color", ...arguments_), ansiStyles.color.close, this[STYLER]);
353
+ return createBuilder(this, styler, this[IS_EMPTY]);
354
+ };
355
+ } };
356
+ const bgModel = "bg" + model[0].toUpperCase() + model.slice(1);
357
+ styles[bgModel] = { get() {
358
+ const { level } = this;
359
+ return function(...arguments_) {
360
+ const styler = createStyler(getModelAnsi(model, levelMapping[level], "bgColor", ...arguments_), ansiStyles.bgColor.close, this[STYLER]);
361
+ return createBuilder(this, styler, this[IS_EMPTY]);
362
+ };
363
+ } };
364
+ }
365
+ const proto = Object.defineProperties(() => {}, {
366
+ ...styles,
367
+ level: {
368
+ enumerable: true,
369
+ get() {
370
+ return this[GENERATOR].level;
371
+ },
372
+ set(level) {
373
+ this[GENERATOR].level = level;
374
+ }
375
+ }
376
+ });
377
+ const createStyler = (open, close, parent) => {
378
+ let openAll;
379
+ let closeAll;
380
+ if (parent === void 0) {
381
+ openAll = open;
382
+ closeAll = close;
383
+ } else {
384
+ openAll = parent.openAll + open;
385
+ closeAll = close + parent.closeAll;
386
+ }
387
+ return {
388
+ open,
389
+ close,
390
+ openAll,
391
+ closeAll,
392
+ parent
393
+ };
394
+ };
395
+ const createBuilder = (self, _styler, _isEmpty) => {
396
+ const builder = (...arguments_) => applyStyle(builder, arguments_.length === 1 ? "" + arguments_[0] : arguments_.join(" "));
397
+ Object.setPrototypeOf(builder, proto);
398
+ builder[GENERATOR] = self;
399
+ builder[STYLER] = _styler;
400
+ builder[IS_EMPTY] = _isEmpty;
401
+ return builder;
402
+ };
403
+ const applyStyle = (self, string) => {
404
+ if (self.level <= 0 || !string) return self[IS_EMPTY] ? "" : string;
405
+ let styler = self[STYLER];
406
+ if (styler === void 0) return string;
407
+ const { openAll, closeAll } = styler;
408
+ if (string.includes("\x1B")) while (styler !== void 0) {
409
+ string = stringReplaceAll(string, styler.close, styler.open);
410
+ styler = styler.parent;
411
+ }
412
+ const lfIndex = string.indexOf("\n");
413
+ if (lfIndex !== -1) string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
414
+ return openAll + string + closeAll;
415
+ };
416
+ Object.defineProperties(createChalk.prototype, styles);
417
+ const chalk = createChalk();
418
+ const chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
419
+
420
+ //#endregion
421
+ //#region src/agents/clipboard.ts
422
+ const getClipboardCommands = () => {
423
+ if (process.platform === "darwin") return [{
424
+ args: [],
425
+ command: "pbcopy"
426
+ }];
427
+ if (process.platform === "win32") return [{
428
+ args: [],
429
+ command: "clip"
430
+ }];
431
+ return [
432
+ {
433
+ args: [],
434
+ command: "wl-copy"
435
+ },
436
+ {
437
+ args: ["-selection", "clipboard"],
438
+ command: "xclip"
439
+ },
440
+ {
441
+ args: ["--clipboard", "--input"],
442
+ command: "xsel"
443
+ }
444
+ ];
445
+ };
446
+ const tryCopy = ({ command, args }, text) => new Promise((resolve) => {
447
+ const child = spawn(command, args, { stdio: [
448
+ "pipe",
449
+ "ignore",
450
+ "ignore"
451
+ ] });
452
+ child.once("error", () => {
453
+ resolve(false);
454
+ });
455
+ child.once("exit", (code) => {
456
+ resolve(code === 0);
457
+ });
458
+ child.stdin.end(text);
459
+ });
460
+ const tryCommands = async (commands, text) => {
461
+ const [first, ...rest] = commands;
462
+ if (!first) return false;
463
+ if (await tryCopy(first, text)) return true;
464
+ return tryCommands(rest, text);
465
+ };
466
+ const copyToClipboard = (text) => tryCommands(getClipboardCommands(), text);
467
+
468
+ //#endregion
469
+ //#region src/agents/diagnostics-dir.ts
470
+ const DIAGNOSTICS_DIR_NAME = ".docker-doctor";
471
+ const UNSAFE_FILE_CHARS = /[^a-z0-9-]+/giu;
472
+ const ruleFileName = (rule) => {
473
+ return `${(rule.split("/").at(-1) ?? rule).replace(UNSAFE_FILE_CHARS, "-")}.txt`;
474
+ };
475
+ const groupDiagnosticsByRule = (diagnostics) => {
476
+ const groups = /* @__PURE__ */ new Map();
477
+ for (const diagnostic of diagnostics) {
478
+ const group = groups.get(diagnostic.rule);
479
+ if (group) group.push(diagnostic);
480
+ else groups.set(diagnostic.rule, [diagnostic]);
481
+ }
482
+ return groups;
483
+ };
484
+ const writeDiagnosticsDirectory = async (diagnostics, report, rootDir) => {
485
+ const dir = path.join(rootDir, DIAGNOSTICS_DIR_NAME);
486
+ await fs.rm(dir, {
487
+ force: true,
488
+ recursive: true
489
+ });
490
+ await fs.mkdir(dir, { recursive: true });
491
+ await fs.writeFile(path.join(dir, "diagnostics.json"), JSON.stringify(report, null, 2), "utf-8");
492
+ const writes = [];
493
+ for (const [rule, ruleDiagnostics] of groupDiagnosticsByRule(diagnostics)) {
494
+ const [first] = ruleDiagnostics;
495
+ const lines = [
496
+ `${rule} (${first.severity})`,
497
+ first.message,
498
+ `Fix: ${first.help}`,
499
+ "",
500
+ ...ruleDiagnostics.map((d) => `${d.file}${d.line === void 0 ? "" : `:${d.line}`}`),
501
+ ""
502
+ ];
503
+ writes.push(fs.writeFile(path.join(dir, ruleFileName(rule)), lines.join("\n"), "utf-8"));
504
+ }
505
+ await Promise.all(writes);
506
+ };
507
+ const ensureGitignoreEntry = async (rootDir) => {
508
+ const gitignorePath = path.join(rootDir, ".gitignore");
509
+ let existing = null;
510
+ try {
511
+ existing = await fs.readFile(gitignorePath, "utf-8");
512
+ } catch {
513
+ existing = null;
514
+ }
515
+ if (existing !== null) {
516
+ if (existing.split(/\r?\n/u).some((line) => [
517
+ ".docker-doctor",
518
+ `${".docker-doctor"}/`,
519
+ `/${".docker-doctor"}`,
520
+ `/${".docker-doctor"}/`
521
+ ].includes(line.trim()))) return;
522
+ const separator = existing.endsWith("\n") || existing === "" ? "" : "\n";
523
+ await fs.writeFile(gitignorePath, `${existing}${separator}${DIAGNOSTICS_DIR_NAME}/\n`, "utf-8");
524
+ return;
525
+ }
526
+ try {
527
+ await fs.access(path.join(rootDir, ".git"));
528
+ } catch {
529
+ return;
530
+ }
531
+ await fs.writeFile(gitignorePath, `${DIAGNOSTICS_DIR_NAME}/\n`, "utf-8");
532
+ };
533
+
534
+ //#endregion
535
+ //#region src/agents/handoff-payload.ts
536
+ const MAX_FILES_PER_RULE = 5;
537
+ const SEVERITY_RANK = {
538
+ error: 0,
539
+ info: 2,
540
+ warning: 1
541
+ };
542
+ const SEVERITY_LABEL = {
543
+ error: "ERROR",
544
+ info: "INFO",
545
+ warning: "WARN"
546
+ };
547
+ const buildHandoffPayload = (input) => {
548
+ const groups = [...groupDiagnosticsByRule(input.diagnostics).entries()].toSorted(([, a], [, b]) => {
549
+ const rankDelta = SEVERITY_RANK[a[0].severity] - SEVERITY_RANK[b[0].severity];
550
+ return rankDelta === 0 ? b.length - a.length : rankDelta;
551
+ });
552
+ const issueWord = groups.length === 1 ? "issue" : "issues";
553
+ const lines = [`Fix the ${groups.length} Docker Doctor ${issueWord} in ${input.projectName}.`, ""];
554
+ for (const [index, [rule, ruleDiagnostics]] of groups.entries()) {
555
+ const [first] = ruleDiagnostics;
556
+ const category = findRule(rule)?.category ?? "General";
557
+ const countBadge = ruleDiagnostics.length > 1 ? ` (×${ruleDiagnostics.length})` : "";
558
+ lines.push(`${index + 1}. ${SEVERITY_LABEL[first.severity]} ${category}: ${first.message} [${rule}]${countBadge}`, ` Fix: ${first.help}`);
559
+ const files = [...new Set(ruleDiagnostics.map((d) => d.file))];
560
+ for (const file of files.slice(0, MAX_FILES_PER_RULE)) {
561
+ const firstSite = ruleDiagnostics.find((d) => d.file === file && d.line !== void 0);
562
+ lines.push(` - ${file}${firstSite ? `:${firstSite.line}` : ""}`);
563
+ }
564
+ const remaining = files.length - MAX_FILES_PER_RULE;
565
+ if (remaining > 0) lines.push(` - +${remaining} more files`);
566
+ }
567
+ lines.push("", `Full report (diagnostics.json + a .txt per rule): ${DIAGNOSTICS_DIR_NAME}/`, "", "Read each file and fix the root cause — don't suppress or silence the rule.", "When you're done, re-run `npx @docker-doctor/cli@latest .` and confirm the score improved and no errors remain.");
568
+ return lines.join("\n");
569
+ };
570
+
571
+ //#endregion
572
+ //#region src/agents/is-command-available.ts
573
+ const WINDOWS_EXTENSIONS = [
574
+ ".exe",
575
+ ".cmd",
576
+ ".bat"
577
+ ];
578
+ const isCommandAvailable = (command) => {
579
+ const pathValue = process.env.PATH ?? "";
580
+ const extensions = process.platform === "win32" ? WINDOWS_EXTENSIONS : [""];
581
+ for (const dir of pathValue.split(path.delimiter)) {
582
+ if (dir === "") continue;
583
+ for (const extension of extensions) try {
584
+ fs$1.accessSync(path.join(dir, command + extension), fs$1.constants.X_OK);
585
+ return true;
586
+ } catch {}
587
+ }
588
+ return false;
589
+ };
590
+
591
+ //#endregion
592
+ //#region src/agents/launchable-agents.ts
593
+ const LAUNCHABLE_AGENT_IDS = [
594
+ "claude-code",
595
+ "codex",
596
+ "cursor"
597
+ ];
598
+ const AGENT_BINARIES = {
599
+ "claude-code": "claude",
600
+ codex: "codex",
601
+ cursor: "cursor-agent"
602
+ };
603
+ const AGENT_AUTO_FLAGS = {
604
+ "claude-code": ["--dangerously-skip-permissions"],
605
+ codex: ["--yolo"],
606
+ cursor: ["--force"]
607
+ };
608
+ const detectLaunchableAgents = () => {
609
+ if (process.platform === "win32") return [];
610
+ return LAUNCHABLE_AGENT_IDS.filter((agentId) => isCommandAvailable(AGENT_BINARIES[agentId]));
611
+ };
612
+
613
+ //#endregion
614
+ //#region src/agents/launch-agent.ts
615
+ const launchAgent = (agentId, prompt) => new Promise((resolve) => {
616
+ const child = spawn(AGENT_BINARIES[agentId], [...AGENT_AUTO_FLAGS[agentId], prompt], { stdio: "inherit" });
617
+ child.once("error", () => {
618
+ resolve(false);
619
+ });
620
+ child.once("exit", () => {
621
+ resolve(true);
622
+ });
623
+ });
624
+
625
+ //#endregion
626
+ //#region src/agents/skill-install.ts
627
+ const moduleDir = import.meta.dirname;
628
+ const getSkillSourceDirectory = () => {
629
+ const candidates = [path.resolve(moduleDir, "../skill/docker-doctor"), path.resolve(moduleDir, "../../../../skills/docker-doctor")];
630
+ for (const candidate of candidates) if (fs$1.existsSync(path.join(candidate, SKILL_MANIFEST_FILE))) return candidate;
631
+ return null;
632
+ };
633
+ const installSkillForAgents = async (agents, projectRoot) => {
634
+ const source = getSkillSourceDirectory();
635
+ if (!source) return null;
636
+ try {
637
+ return await installSkillsFromSource({
638
+ agents,
639
+ cwd: projectRoot,
640
+ mode: "copy",
641
+ source
642
+ });
643
+ } catch {
644
+ return null;
645
+ }
646
+ };
647
+
648
+ //#endregion
11
649
  //#region src/formatters/terminal.ts
12
650
  const printCodeFrame = (content, line, severityColor) => {
13
651
  if (!content || !line) return;
@@ -148,7 +786,7 @@ const printScoreBox = async (score, label, categoryIssueCounts, warningsCount, e
148
786
  console.log(`\n ${chalk.dim("────────────────────────────────────────────────────────────")}\n`);
149
787
  console.log(` Share: ${chalk.cyan(shareUrl)}`);
150
788
  console.log(` Tell others how you did on socials\n`);
151
- console.log(` Docs: ${chalk.cyan("https://github.com/PunGrumpy/docker-doctor/docs")}`);
789
+ console.log(` Docs: ${chalk.cyan("https://docker-doctor.vercel.app")}`);
152
790
  console.log(` Learn more about fixing issues, setting up CI/CD, and`);
153
791
  console.log(` configuring rules with a config file\n`);
154
792
  console.log(` GitHub: ${chalk.cyan("https://github.com/PunGrumpy/docker-doctor")}`);
@@ -283,7 +921,104 @@ const askSelect = (question, options, defaultIndex = 0) => {
283
921
  process.stdin.on("keypress", handleKeypress);
284
922
  });
285
923
  };
286
- const runInteractiveWizard = async () => {
924
+ const askMultiSelect = (question, options) => {
925
+ if (!process.stdin.isTTY) return Promise.resolve(options.flatMap((option, i) => option.selected ? [i] : []));
926
+ return new Promise((resolve) => {
927
+ let index = 0;
928
+ const selected = options.map((option) => option.selected);
929
+ const lineCount = options.length + 2;
930
+ readline.emitKeypressEvents(process.stdin);
931
+ process.stdin.setRawMode(true);
932
+ process.stdin.resume();
933
+ process.stdout.write("\x1B[?25l");
934
+ const render = (firstTime = false) => {
935
+ if (!firstTime) process.stdout.write(`\u001B[${lineCount}A\r`);
936
+ process.stdout.write(`\r\u001B[K ${chalk.green("✔")} ${chalk.bold(question)}\n`);
937
+ let i = 0;
938
+ for (const option of options) {
939
+ const isCursor = i === index;
940
+ const cursor = isCursor ? chalk.cyan("❯ ") : " ";
941
+ const box = selected[i] ? chalk.cyan("[x]") : chalk.dim("[ ]");
942
+ let text = chalk.dim(option.label);
943
+ if (isCursor) text = chalk.cyan.bold(option.label);
944
+ else if (selected[i]) text = option.label;
945
+ process.stdout.write(`\r\u001B[K${cursor}${box} ${text}\n`);
946
+ i += 1;
947
+ }
948
+ process.stdout.write(`\r\u001B[K ${chalk.dim("space to toggle · enter to confirm")}\n`);
949
+ };
950
+ render(true);
951
+ const handleKeypress = (str, key) => {
952
+ const cleanup = () => {
953
+ process.stdin.removeListener("keypress", handleKeypress);
954
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
955
+ process.stdin.pause();
956
+ process.stdout.write("\x1B[?25h");
957
+ };
958
+ if (key.name === "up" || key.name === "k") {
959
+ index = (index - 1 + options.length) % options.length;
960
+ render();
961
+ } else if (key.name === "down" || key.name === "j") {
962
+ index = (index + 1) % options.length;
963
+ render();
964
+ } else if (key.name === "space" || str === " ") {
965
+ selected[index] = !selected[index];
966
+ render();
967
+ } else if (key.name === "return" || key.name === "enter" || str === "\r" || str === "\n") {
968
+ cleanup();
969
+ const chosen = options.flatMap((option, i) => selected[i] ? [option.label] : []);
970
+ process.stdout.write(`\u001B[${lineCount}A\r\u001B[K`);
971
+ process.stdout.write(` ${chalk.green("✔")} ${chalk.bold(question)} › ${chosen.length > 0 ? chalk.cyan(chosen.join(", ")) : chalk.dim("none")}\n`);
972
+ for (let i = 0; i < lineCount - 1; i += 1) process.stdout.write("\r\x1B[K\n");
973
+ process.stdout.write(`\u001B[${lineCount - 1}A`);
974
+ resolve(options.flatMap((_, i) => selected[i] ? [i] : []));
975
+ } else if (key.ctrl && key.name === "c") {
976
+ cleanup();
977
+ process.stdout.write("\n");
978
+ process.exit(130);
979
+ }
980
+ };
981
+ process.stdin.on("keypress", handleKeypress);
982
+ });
983
+ };
984
+ const printAgentPrompt = (payload) => {
985
+ console.log(`\n${chalk.dim("──── Agent prompt ────")}`);
986
+ console.log(payload);
987
+ console.log(chalk.dim("──────────────────────"));
988
+ };
989
+ const agentDisplayName = (agent) => agent === "universal" ? "Universal" : getSkillAgentConfig(agent).displayName;
990
+ const runAgentHandoff = async (context) => {
991
+ const launchable = detectLaunchableAgents();
992
+ const options = [
993
+ ...launchable.map((agentId) => agentDisplayName(agentId)),
994
+ "Copy prompt to clipboard",
995
+ "Skip"
996
+ ];
997
+ const skipIndex = options.length - 1;
998
+ const clipboardIndex = options.length - 2;
999
+ const choice = await askSelect("What would you like to do next?", options);
1000
+ if (choice === skipIndex) return;
1001
+ await writeDiagnosticsDirectory(context.diagnostics, context.report, context.rootDir);
1002
+ await ensureGitignoreEntry(context.rootDir);
1003
+ const payload = buildHandoffPayload({
1004
+ diagnostics: context.diagnostics,
1005
+ projectName: path.basename(context.rootDir)
1006
+ });
1007
+ if (choice === clipboardIndex) {
1008
+ if (await copyToClipboard(payload)) console.log(`\n ${chalk.green("✔")} Prompt copied — paste it into any agent or chat.`);
1009
+ else printAgentPrompt(payload);
1010
+ return;
1011
+ }
1012
+ const agentId = launchable[choice];
1013
+ const installResult = await installSkillForAgents([agentId], context.rootDir);
1014
+ if (installResult && installResult.installed.length > 0) console.log(`\n ${chalk.green("✔")} Installed the docker-doctor skill for ${agentDisplayName(agentId)}`);
1015
+ console.log(`\n Handing off to ${agentDisplayName(agentId)}...\n`);
1016
+ if (!await launchAgent(agentId, payload)) {
1017
+ console.log(` ${chalk.yellow("⚠")} Couldn't launch ${AGENT_BINARIES[agentId]}. Here's the prompt instead:`);
1018
+ printAgentPrompt(payload);
1019
+ }
1020
+ };
1021
+ const runInteractiveWizard = async (context) => {
287
1022
  try {
288
1023
  if (await askConfirm("Add Docker Doctor to GitHub Actions?")) {
289
1024
  const workflowDir = path.resolve(".github/workflows");
@@ -310,10 +1045,8 @@ jobs:
310
1045
  console.log(`\n ${chalk.green("✨")} Created ${chalk.cyan(".github/workflows/docker-doctor.yml")}!`);
311
1046
  console.log(` Scan every pull request to prevent new Docker issues while you fix the backlog.`);
312
1047
  }
313
- if (await askSelect("What would you like to do next?", ["View rules list", "Skip"]) === 0) {
314
- console.log(`\n ${chalk.bold("Available Rules:")}`);
315
- for (const r of allRules) console.log(` - ${chalk.cyan(r.key)}: ${r.message} (${chalk.dim(r.category)})`);
316
- }
1048
+ if (context.diagnostics.length === 0) return;
1049
+ await runAgentHandoff(context);
317
1050
  } catch {}
318
1051
  };
319
1052
  const runRulesEngine = async (rootDir, project, rulesConfig, projectFilesList, fileContents, options, setStatus) => {
@@ -435,18 +1168,23 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
435
1168
  if (process.stdout.isTTY && !isSilent) console.log(`${chalk.green("✔")} Scanned ${projectFilesList.length} files in ${duration}s [~${concurrency} workers]`);
436
1169
  if (options.score) {
437
1170
  console.log(score);
438
- process.exit(score < 50 ? 1 : 0);
1171
+ process.exitCode = score < 50 ? 1 : 0;
1172
+ return;
439
1173
  } else if (options.json) {
440
1174
  const report = toJsonReport(filteredDiagnostics, score, label, project);
441
1175
  console.log(JSON.stringify(report, null, 2));
442
1176
  const hasErrors = filteredDiagnostics.some((d) => d.severity === "error");
443
- process.exit(hasErrors ? 1 : 0);
444
- } else {
445
- await formatTerminal(filteredDiagnostics, score, label, project, options.verbose, fileContents);
446
- const hasErrors = filteredDiagnostics.some((d) => d.severity === "error");
447
- if (process.stdout.isTTY && process.stdin.isTTY) await runInteractiveWizard();
448
- process.exit(hasErrors ? 1 : 0);
1177
+ process.exitCode = hasErrors ? 1 : 0;
1178
+ return;
449
1179
  }
1180
+ await formatTerminal(filteredDiagnostics, score, label, project, options.verbose, fileContents);
1181
+ const hasErrors = filteredDiagnostics.some((d) => d.severity === "error");
1182
+ if (process.stdout.isTTY && process.stdin.isTTY) await runInteractiveWizard({
1183
+ diagnostics: filteredDiagnostics,
1184
+ report: toJsonReport(filteredDiagnostics, score, label, project),
1185
+ rootDir
1186
+ });
1187
+ process.exitCode = hasErrors ? 1 : 0;
450
1188
  } finally {
451
1189
  if (spinnerInterval !== null) {
452
1190
  clearInterval(spinnerInterval);
@@ -459,6 +1197,55 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
459
1197
  process.exit(1);
460
1198
  }
461
1199
  });
1200
+ const CURATED_INSTALL_AGENTS = [
1201
+ "claude-code",
1202
+ "codex",
1203
+ "cursor",
1204
+ "opencode"
1205
+ ];
1206
+ const resolveInstallAgents = async (requested) => {
1207
+ if (requested && requested.length > 0) {
1208
+ const invalid = requested.filter((agent) => !isSkillAgentType(agent));
1209
+ if (invalid.length > 0) {
1210
+ console.error(`Unknown agent id(s): ${invalid.join(", ")}`);
1211
+ console.error(`Valid ids: ${getSkillAgentTypes().filter((agent) => agent !== "universal").join(", ")}`);
1212
+ return null;
1213
+ }
1214
+ return requested.filter((agent) => isSkillAgentType(agent));
1215
+ }
1216
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
1217
+ console.error("Non-interactive run: pass --agent <id...> (e.g. --agent claude-code cursor).");
1218
+ return null;
1219
+ }
1220
+ const detected = (await detectInstalledSkillAgents()).filter((agent) => agent !== "universal");
1221
+ const choices = [.../* @__PURE__ */ new Set([...detected, ...CURATED_INSTALL_AGENTS])];
1222
+ const detectedSet = new Set(detected);
1223
+ return (await askMultiSelect("Which coding agents should get the docker-doctor skill?", choices.map((agent) => ({
1224
+ label: agentDisplayName(agent),
1225
+ selected: detectedSet.has(agent)
1226
+ })))).map((i) => choices[i]);
1227
+ };
1228
+ program.command("install").description("install the Docker Doctor agent skill for your coding agents").option("-a, --agent <agents...>", "agent id(s) to install for (e.g. claude-code codex cursor)").action(async (options) => {
1229
+ if (!getSkillSourceDirectory()) {
1230
+ console.error("Bundled skill not found — this looks like a broken installation.");
1231
+ process.exit(1);
1232
+ }
1233
+ const agents = await resolveInstallAgents(options.agent);
1234
+ if (agents === null) process.exit(1);
1235
+ if (agents.length === 0) {
1236
+ console.log("Nothing selected — skipped.");
1237
+ return;
1238
+ }
1239
+ const result = await installSkillForAgents(agents, process.cwd());
1240
+ if (!result) {
1241
+ console.error("Failed to install the skill.");
1242
+ process.exit(1);
1243
+ }
1244
+ for (const installed of result.installed) console.log(` ${chalk.green("✔")} ${agentDisplayName(installed.agent)} → ${installed.path}`);
1245
+ for (const failed of result.failed) console.log(` ${chalk.red("✖")} ${agentDisplayName(failed.agent)}: ${failed.error}`);
1246
+ if (result.installed.length > 0) console.log(`\n The agent can now run ${chalk.cyan("/docker-doctor")} to scan and triage this project.`);
1247
+ process.exitCode = result.failed.length > 0 ? 1 : 0;
1248
+ });
462
1249
  const rules = program.command("rules").description("manage and list configuration rules");
463
1250
  rules.command("list").description("list all available rules").action(() => {
464
1251
  console.log("\nAvailable Rules:");