@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.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_src = require('./src-O8FXMq8O.cjs');
2
+ const require_src = require('./src-38C8Tk2b.cjs');
3
3
  let node_fs_promises = require("node:fs/promises");
4
4
  node_fs_promises = require_src.__toESM(node_fs_promises, 1);
5
5
  let node_path = require("node:path");
@@ -9,10 +9,650 @@ node_os = require_src.__toESM(node_os, 1);
9
9
  let node_readline = require("node:readline");
10
10
  node_readline = require_src.__toESM(node_readline, 1);
11
11
  let node_timers_promises = require("node:timers/promises");
12
- let chalk = require("chalk");
13
- chalk = require_src.__toESM(chalk, 1);
12
+ let agent_install = require("agent-install");
13
+ let node_process = require("node:process");
14
+ node_process = require_src.__toESM(node_process, 1);
15
+ let node_tty = require("node:tty");
16
+ node_tty = require_src.__toESM(node_tty, 1);
14
17
  let commander = require("commander");
18
+ let node_child_process = require("node:child_process");
19
+ let node_fs = require("node:fs");
20
+ node_fs = require_src.__toESM(node_fs, 1);
15
21
 
22
+ //#region ../../node_modules/.bun/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
23
+ const ANSI_BACKGROUND_OFFSET = 10;
24
+ const wrapAnsi16 = (offset = 0) => (code) => `\u001B[${code + offset}m`;
25
+ const wrapAnsi256 = (offset = 0) => (code) => `\u001B[${38 + offset};5;${code}m`;
26
+ const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`;
27
+ const styles$1 = {
28
+ modifier: {
29
+ reset: [0, 0],
30
+ bold: [1, 22],
31
+ dim: [2, 22],
32
+ italic: [3, 23],
33
+ underline: [4, 24],
34
+ overline: [53, 55],
35
+ inverse: [7, 27],
36
+ hidden: [8, 28],
37
+ strikethrough: [9, 29]
38
+ },
39
+ color: {
40
+ black: [30, 39],
41
+ red: [31, 39],
42
+ green: [32, 39],
43
+ yellow: [33, 39],
44
+ blue: [34, 39],
45
+ magenta: [35, 39],
46
+ cyan: [36, 39],
47
+ white: [37, 39],
48
+ blackBright: [90, 39],
49
+ gray: [90, 39],
50
+ grey: [90, 39],
51
+ redBright: [91, 39],
52
+ greenBright: [92, 39],
53
+ yellowBright: [93, 39],
54
+ blueBright: [94, 39],
55
+ magentaBright: [95, 39],
56
+ cyanBright: [96, 39],
57
+ whiteBright: [97, 39]
58
+ },
59
+ bgColor: {
60
+ bgBlack: [40, 49],
61
+ bgRed: [41, 49],
62
+ bgGreen: [42, 49],
63
+ bgYellow: [43, 49],
64
+ bgBlue: [44, 49],
65
+ bgMagenta: [45, 49],
66
+ bgCyan: [46, 49],
67
+ bgWhite: [47, 49],
68
+ bgBlackBright: [100, 49],
69
+ bgGray: [100, 49],
70
+ bgGrey: [100, 49],
71
+ bgRedBright: [101, 49],
72
+ bgGreenBright: [102, 49],
73
+ bgYellowBright: [103, 49],
74
+ bgBlueBright: [104, 49],
75
+ bgMagentaBright: [105, 49],
76
+ bgCyanBright: [106, 49],
77
+ bgWhiteBright: [107, 49]
78
+ }
79
+ };
80
+ const modifierNames = Object.keys(styles$1.modifier);
81
+ const foregroundColorNames = Object.keys(styles$1.color);
82
+ const backgroundColorNames = Object.keys(styles$1.bgColor);
83
+ const colorNames = [...foregroundColorNames, ...backgroundColorNames];
84
+ function assembleStyles() {
85
+ const codes = /* @__PURE__ */ new Map();
86
+ for (const [groupName, group] of Object.entries(styles$1)) {
87
+ for (const [styleName, style] of Object.entries(group)) {
88
+ styles$1[styleName] = {
89
+ open: `\u001B[${style[0]}m`,
90
+ close: `\u001B[${style[1]}m`
91
+ };
92
+ group[styleName] = styles$1[styleName];
93
+ codes.set(style[0], style[1]);
94
+ }
95
+ Object.defineProperty(styles$1, groupName, {
96
+ value: group,
97
+ enumerable: false
98
+ });
99
+ }
100
+ Object.defineProperty(styles$1, "codes", {
101
+ value: codes,
102
+ enumerable: false
103
+ });
104
+ styles$1.color.close = "\x1B[39m";
105
+ styles$1.bgColor.close = "\x1B[49m";
106
+ styles$1.color.ansi = wrapAnsi16();
107
+ styles$1.color.ansi256 = wrapAnsi256();
108
+ styles$1.color.ansi16m = wrapAnsi16m();
109
+ styles$1.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
110
+ styles$1.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
111
+ styles$1.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
112
+ Object.defineProperties(styles$1, {
113
+ rgbToAnsi256: {
114
+ value(red, green, blue) {
115
+ if (red === green && green === blue) {
116
+ if (red < 8) return 16;
117
+ if (red > 248) return 231;
118
+ return Math.round((red - 8) / 247 * 24) + 232;
119
+ }
120
+ return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
121
+ },
122
+ enumerable: false
123
+ },
124
+ hexToRgb: {
125
+ value(hex) {
126
+ const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
127
+ if (!matches) return [
128
+ 0,
129
+ 0,
130
+ 0
131
+ ];
132
+ let [colorString] = matches;
133
+ if (colorString.length === 3) colorString = [...colorString].map((character) => character + character).join("");
134
+ const integer = Number.parseInt(colorString, 16);
135
+ return [
136
+ integer >> 16 & 255,
137
+ integer >> 8 & 255,
138
+ integer & 255
139
+ ];
140
+ },
141
+ enumerable: false
142
+ },
143
+ hexToAnsi256: {
144
+ value: (hex) => styles$1.rgbToAnsi256(...styles$1.hexToRgb(hex)),
145
+ enumerable: false
146
+ },
147
+ ansi256ToAnsi: {
148
+ value(code) {
149
+ if (code < 8) return 30 + code;
150
+ if (code < 16) return 90 + (code - 8);
151
+ let red;
152
+ let green;
153
+ let blue;
154
+ if (code >= 232) {
155
+ red = ((code - 232) * 10 + 8) / 255;
156
+ green = red;
157
+ blue = red;
158
+ } else {
159
+ code -= 16;
160
+ const remainder = code % 36;
161
+ red = Math.floor(code / 36) / 5;
162
+ green = Math.floor(remainder / 6) / 5;
163
+ blue = remainder % 6 / 5;
164
+ }
165
+ const value = Math.max(red, green, blue) * 2;
166
+ if (value === 0) return 30;
167
+ let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
168
+ if (value === 2) result += 60;
169
+ return result;
170
+ },
171
+ enumerable: false
172
+ },
173
+ rgbToAnsi: {
174
+ value: (red, green, blue) => styles$1.ansi256ToAnsi(styles$1.rgbToAnsi256(red, green, blue)),
175
+ enumerable: false
176
+ },
177
+ hexToAnsi: {
178
+ value: (hex) => styles$1.ansi256ToAnsi(styles$1.hexToAnsi256(hex)),
179
+ enumerable: false
180
+ }
181
+ });
182
+ return styles$1;
183
+ }
184
+ const ansiStyles = assembleStyles();
185
+
186
+ //#endregion
187
+ //#region ../../node_modules/.bun/chalk@5.6.2/node_modules/chalk/source/vendor/supports-color/index.js
188
+ function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : node_process.default.argv) {
189
+ const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
190
+ const position = argv.indexOf(prefix + flag);
191
+ const terminatorPosition = argv.indexOf("--");
192
+ return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
193
+ }
194
+ const { env } = node_process.default;
195
+ let flagForceColor;
196
+ if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) flagForceColor = 0;
197
+ else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) flagForceColor = 1;
198
+ function envForceColor() {
199
+ if ("FORCE_COLOR" in env) {
200
+ if (env.FORCE_COLOR === "true") return 1;
201
+ if (env.FORCE_COLOR === "false") return 0;
202
+ return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
203
+ }
204
+ }
205
+ function translateLevel(level) {
206
+ if (level === 0) return false;
207
+ return {
208
+ level,
209
+ hasBasic: true,
210
+ has256: level >= 2,
211
+ has16m: level >= 3
212
+ };
213
+ }
214
+ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
215
+ const noFlagForceColor = envForceColor();
216
+ if (noFlagForceColor !== void 0) flagForceColor = noFlagForceColor;
217
+ const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
218
+ if (forceColor === 0) return 0;
219
+ if (sniffFlags) {
220
+ if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) return 3;
221
+ if (hasFlag("color=256")) return 2;
222
+ }
223
+ if ("TF_BUILD" in env && "AGENT_NAME" in env) return 1;
224
+ if (haveStream && !streamIsTTY && forceColor === void 0) return 0;
225
+ const min = forceColor || 0;
226
+ if (env.TERM === "dumb") return min;
227
+ if (node_process.default.platform === "win32") {
228
+ const osRelease = node_os.default.release().split(".");
229
+ if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
230
+ return 1;
231
+ }
232
+ if ("CI" in env) {
233
+ if ([
234
+ "GITHUB_ACTIONS",
235
+ "GITEA_ACTIONS",
236
+ "CIRCLECI"
237
+ ].some((key) => key in env)) return 3;
238
+ if ([
239
+ "TRAVIS",
240
+ "APPVEYOR",
241
+ "GITLAB_CI",
242
+ "BUILDKITE",
243
+ "DRONE"
244
+ ].some((sign) => sign in env) || env.CI_NAME === "codeship") return 1;
245
+ return min;
246
+ }
247
+ if ("TEAMCITY_VERSION" in env) return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
248
+ if (env.COLORTERM === "truecolor") return 3;
249
+ if (env.TERM === "xterm-kitty") return 3;
250
+ if (env.TERM === "xterm-ghostty") return 3;
251
+ if (env.TERM === "wezterm") return 3;
252
+ if ("TERM_PROGRAM" in env) {
253
+ const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
254
+ switch (env.TERM_PROGRAM) {
255
+ case "iTerm.app": return version >= 3 ? 3 : 2;
256
+ case "Apple_Terminal": return 2;
257
+ }
258
+ }
259
+ if (/-256(color)?$/i.test(env.TERM)) return 2;
260
+ if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) return 1;
261
+ if ("COLORTERM" in env) return 1;
262
+ return min;
263
+ }
264
+ function createSupportsColor(stream, options = {}) {
265
+ return translateLevel(_supportsColor(stream, {
266
+ streamIsTTY: stream && stream.isTTY,
267
+ ...options
268
+ }));
269
+ }
270
+ const supportsColor = {
271
+ stdout: createSupportsColor({ isTTY: node_tty.default.isatty(1) }),
272
+ stderr: createSupportsColor({ isTTY: node_tty.default.isatty(2) })
273
+ };
274
+
275
+ //#endregion
276
+ //#region ../../node_modules/.bun/chalk@5.6.2/node_modules/chalk/source/utilities.js
277
+ function stringReplaceAll(string, substring, replacer) {
278
+ let index = string.indexOf(substring);
279
+ if (index === -1) return string;
280
+ const substringLength = substring.length;
281
+ let endIndex = 0;
282
+ let returnValue = "";
283
+ do {
284
+ returnValue += string.slice(endIndex, index) + substring + replacer;
285
+ endIndex = index + substringLength;
286
+ index = string.indexOf(substring, endIndex);
287
+ } while (index !== -1);
288
+ returnValue += string.slice(endIndex);
289
+ return returnValue;
290
+ }
291
+ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
292
+ let endIndex = 0;
293
+ let returnValue = "";
294
+ do {
295
+ const gotCR = string[index - 1] === "\r";
296
+ returnValue += string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? "\r\n" : "\n") + postfix;
297
+ endIndex = index + 1;
298
+ index = string.indexOf("\n", endIndex);
299
+ } while (index !== -1);
300
+ returnValue += string.slice(endIndex);
301
+ return returnValue;
302
+ }
303
+
304
+ //#endregion
305
+ //#region ../../node_modules/.bun/chalk@5.6.2/node_modules/chalk/source/index.js
306
+ const { stdout: stdoutColor, stderr: stderrColor } = supportsColor;
307
+ const GENERATOR = Symbol("GENERATOR");
308
+ const STYLER = Symbol("STYLER");
309
+ const IS_EMPTY = Symbol("IS_EMPTY");
310
+ const levelMapping = [
311
+ "ansi",
312
+ "ansi",
313
+ "ansi256",
314
+ "ansi16m"
315
+ ];
316
+ const styles = Object.create(null);
317
+ const applyOptions = (object, options = {}) => {
318
+ 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");
319
+ const colorLevel = stdoutColor ? stdoutColor.level : 0;
320
+ object.level = options.level === void 0 ? colorLevel : options.level;
321
+ };
322
+ const chalkFactory = (options) => {
323
+ const chalk = (...strings) => strings.join(" ");
324
+ applyOptions(chalk, options);
325
+ Object.setPrototypeOf(chalk, createChalk.prototype);
326
+ return chalk;
327
+ };
328
+ function createChalk(options) {
329
+ return chalkFactory(options);
330
+ }
331
+ Object.setPrototypeOf(createChalk.prototype, Function.prototype);
332
+ for (const [styleName, style] of Object.entries(ansiStyles)) styles[styleName] = { get() {
333
+ const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
334
+ Object.defineProperty(this, styleName, { value: builder });
335
+ return builder;
336
+ } };
337
+ styles.visible = { get() {
338
+ const builder = createBuilder(this, this[STYLER], true);
339
+ Object.defineProperty(this, "visible", { value: builder });
340
+ return builder;
341
+ } };
342
+ const getModelAnsi = (model, level, type, ...arguments_) => {
343
+ if (model === "rgb") {
344
+ if (level === "ansi16m") return ansiStyles[type].ansi16m(...arguments_);
345
+ if (level === "ansi256") return ansiStyles[type].ansi256(ansiStyles.rgbToAnsi256(...arguments_));
346
+ return ansiStyles[type].ansi(ansiStyles.rgbToAnsi(...arguments_));
347
+ }
348
+ if (model === "hex") return getModelAnsi("rgb", level, type, ...ansiStyles.hexToRgb(...arguments_));
349
+ return ansiStyles[type][model](...arguments_);
350
+ };
351
+ for (const model of [
352
+ "rgb",
353
+ "hex",
354
+ "ansi256"
355
+ ]) {
356
+ styles[model] = { get() {
357
+ const { level } = this;
358
+ return function(...arguments_) {
359
+ const styler = createStyler(getModelAnsi(model, levelMapping[level], "color", ...arguments_), ansiStyles.color.close, this[STYLER]);
360
+ return createBuilder(this, styler, this[IS_EMPTY]);
361
+ };
362
+ } };
363
+ const bgModel = "bg" + model[0].toUpperCase() + model.slice(1);
364
+ styles[bgModel] = { get() {
365
+ const { level } = this;
366
+ return function(...arguments_) {
367
+ const styler = createStyler(getModelAnsi(model, levelMapping[level], "bgColor", ...arguments_), ansiStyles.bgColor.close, this[STYLER]);
368
+ return createBuilder(this, styler, this[IS_EMPTY]);
369
+ };
370
+ } };
371
+ }
372
+ const proto = Object.defineProperties(() => {}, {
373
+ ...styles,
374
+ level: {
375
+ enumerable: true,
376
+ get() {
377
+ return this[GENERATOR].level;
378
+ },
379
+ set(level) {
380
+ this[GENERATOR].level = level;
381
+ }
382
+ }
383
+ });
384
+ const createStyler = (open, close, parent) => {
385
+ let openAll;
386
+ let closeAll;
387
+ if (parent === void 0) {
388
+ openAll = open;
389
+ closeAll = close;
390
+ } else {
391
+ openAll = parent.openAll + open;
392
+ closeAll = close + parent.closeAll;
393
+ }
394
+ return {
395
+ open,
396
+ close,
397
+ openAll,
398
+ closeAll,
399
+ parent
400
+ };
401
+ };
402
+ const createBuilder = (self, _styler, _isEmpty) => {
403
+ const builder = (...arguments_) => applyStyle(builder, arguments_.length === 1 ? "" + arguments_[0] : arguments_.join(" "));
404
+ Object.setPrototypeOf(builder, proto);
405
+ builder[GENERATOR] = self;
406
+ builder[STYLER] = _styler;
407
+ builder[IS_EMPTY] = _isEmpty;
408
+ return builder;
409
+ };
410
+ const applyStyle = (self, string) => {
411
+ if (self.level <= 0 || !string) return self[IS_EMPTY] ? "" : string;
412
+ let styler = self[STYLER];
413
+ if (styler === void 0) return string;
414
+ const { openAll, closeAll } = styler;
415
+ if (string.includes("\x1B")) while (styler !== void 0) {
416
+ string = stringReplaceAll(string, styler.close, styler.open);
417
+ styler = styler.parent;
418
+ }
419
+ const lfIndex = string.indexOf("\n");
420
+ if (lfIndex !== -1) string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
421
+ return openAll + string + closeAll;
422
+ };
423
+ Object.defineProperties(createChalk.prototype, styles);
424
+ const chalk = createChalk();
425
+ const chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
426
+
427
+ //#endregion
428
+ //#region src/agents/clipboard.ts
429
+ const getClipboardCommands = () => {
430
+ if (process.platform === "darwin") return [{
431
+ args: [],
432
+ command: "pbcopy"
433
+ }];
434
+ if (process.platform === "win32") return [{
435
+ args: [],
436
+ command: "clip"
437
+ }];
438
+ return [
439
+ {
440
+ args: [],
441
+ command: "wl-copy"
442
+ },
443
+ {
444
+ args: ["-selection", "clipboard"],
445
+ command: "xclip"
446
+ },
447
+ {
448
+ args: ["--clipboard", "--input"],
449
+ command: "xsel"
450
+ }
451
+ ];
452
+ };
453
+ const tryCopy = ({ command, args }, text) => new Promise((resolve) => {
454
+ const child = (0, node_child_process.spawn)(command, args, { stdio: [
455
+ "pipe",
456
+ "ignore",
457
+ "ignore"
458
+ ] });
459
+ child.once("error", () => {
460
+ resolve(false);
461
+ });
462
+ child.once("exit", (code) => {
463
+ resolve(code === 0);
464
+ });
465
+ child.stdin.end(text);
466
+ });
467
+ const tryCommands = async (commands, text) => {
468
+ const [first, ...rest] = commands;
469
+ if (!first) return false;
470
+ if (await tryCopy(first, text)) return true;
471
+ return tryCommands(rest, text);
472
+ };
473
+ const copyToClipboard = (text) => tryCommands(getClipboardCommands(), text);
474
+
475
+ //#endregion
476
+ //#region src/agents/diagnostics-dir.ts
477
+ const DIAGNOSTICS_DIR_NAME = ".docker-doctor";
478
+ const UNSAFE_FILE_CHARS = /[^a-z0-9-]+/giu;
479
+ const ruleFileName = (rule) => {
480
+ return `${(rule.split("/").at(-1) ?? rule).replace(UNSAFE_FILE_CHARS, "-")}.txt`;
481
+ };
482
+ const groupDiagnosticsByRule = (diagnostics) => {
483
+ const groups = /* @__PURE__ */ new Map();
484
+ for (const diagnostic of diagnostics) {
485
+ const group = groups.get(diagnostic.rule);
486
+ if (group) group.push(diagnostic);
487
+ else groups.set(diagnostic.rule, [diagnostic]);
488
+ }
489
+ return groups;
490
+ };
491
+ const writeDiagnosticsDirectory = async (diagnostics, report, rootDir) => {
492
+ const dir = node_path.default.join(rootDir, DIAGNOSTICS_DIR_NAME);
493
+ await node_fs_promises.default.rm(dir, {
494
+ force: true,
495
+ recursive: true
496
+ });
497
+ await node_fs_promises.default.mkdir(dir, { recursive: true });
498
+ await node_fs_promises.default.writeFile(node_path.default.join(dir, "diagnostics.json"), JSON.stringify(report, null, 2), "utf-8");
499
+ const writes = [];
500
+ for (const [rule, ruleDiagnostics] of groupDiagnosticsByRule(diagnostics)) {
501
+ const [first] = ruleDiagnostics;
502
+ const lines = [
503
+ `${rule} (${first.severity})`,
504
+ first.message,
505
+ `Fix: ${first.help}`,
506
+ "",
507
+ ...ruleDiagnostics.map((d) => `${d.file}${d.line === void 0 ? "" : `:${d.line}`}`),
508
+ ""
509
+ ];
510
+ writes.push(node_fs_promises.default.writeFile(node_path.default.join(dir, ruleFileName(rule)), lines.join("\n"), "utf-8"));
511
+ }
512
+ await Promise.all(writes);
513
+ };
514
+ const ensureGitignoreEntry = async (rootDir) => {
515
+ const gitignorePath = node_path.default.join(rootDir, ".gitignore");
516
+ let existing = null;
517
+ try {
518
+ existing = await node_fs_promises.default.readFile(gitignorePath, "utf-8");
519
+ } catch {
520
+ existing = null;
521
+ }
522
+ if (existing !== null) {
523
+ if (existing.split(/\r?\n/u).some((line) => [
524
+ ".docker-doctor",
525
+ `${".docker-doctor"}/`,
526
+ `/${".docker-doctor"}`,
527
+ `/${".docker-doctor"}/`
528
+ ].includes(line.trim()))) return;
529
+ const separator = existing.endsWith("\n") || existing === "" ? "" : "\n";
530
+ await node_fs_promises.default.writeFile(gitignorePath, `${existing}${separator}${DIAGNOSTICS_DIR_NAME}/\n`, "utf-8");
531
+ return;
532
+ }
533
+ try {
534
+ await node_fs_promises.default.access(node_path.default.join(rootDir, ".git"));
535
+ } catch {
536
+ return;
537
+ }
538
+ await node_fs_promises.default.writeFile(gitignorePath, `${DIAGNOSTICS_DIR_NAME}/\n`, "utf-8");
539
+ };
540
+
541
+ //#endregion
542
+ //#region src/agents/handoff-payload.ts
543
+ const MAX_FILES_PER_RULE = 5;
544
+ const SEVERITY_RANK = {
545
+ error: 0,
546
+ info: 2,
547
+ warning: 1
548
+ };
549
+ const SEVERITY_LABEL = {
550
+ error: "ERROR",
551
+ info: "INFO",
552
+ warning: "WARN"
553
+ };
554
+ const buildHandoffPayload = (input) => {
555
+ const groups = [...groupDiagnosticsByRule(input.diagnostics).entries()].toSorted(([, a], [, b]) => {
556
+ const rankDelta = SEVERITY_RANK[a[0].severity] - SEVERITY_RANK[b[0].severity];
557
+ return rankDelta === 0 ? b.length - a.length : rankDelta;
558
+ });
559
+ const issueWord = groups.length === 1 ? "issue" : "issues";
560
+ const lines = [`Fix the ${groups.length} Docker Doctor ${issueWord} in ${input.projectName}.`, ""];
561
+ for (const [index, [rule, ruleDiagnostics]] of groups.entries()) {
562
+ const [first] = ruleDiagnostics;
563
+ const category = require_src.findRule(rule)?.category ?? "General";
564
+ const countBadge = ruleDiagnostics.length > 1 ? ` (×${ruleDiagnostics.length})` : "";
565
+ lines.push(`${index + 1}. ${SEVERITY_LABEL[first.severity]} ${category}: ${first.message} [${rule}]${countBadge}`, ` Fix: ${first.help}`);
566
+ const files = [...new Set(ruleDiagnostics.map((d) => d.file))];
567
+ for (const file of files.slice(0, MAX_FILES_PER_RULE)) {
568
+ const firstSite = ruleDiagnostics.find((d) => d.file === file && d.line !== void 0);
569
+ lines.push(` - ${file}${firstSite ? `:${firstSite.line}` : ""}`);
570
+ }
571
+ const remaining = files.length - MAX_FILES_PER_RULE;
572
+ if (remaining > 0) lines.push(` - +${remaining} more files`);
573
+ }
574
+ 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.");
575
+ return lines.join("\n");
576
+ };
577
+
578
+ //#endregion
579
+ //#region src/agents/is-command-available.ts
580
+ const WINDOWS_EXTENSIONS = [
581
+ ".exe",
582
+ ".cmd",
583
+ ".bat"
584
+ ];
585
+ const isCommandAvailable = (command) => {
586
+ const pathValue = process.env.PATH ?? "";
587
+ const extensions = process.platform === "win32" ? WINDOWS_EXTENSIONS : [""];
588
+ for (const dir of pathValue.split(node_path.default.delimiter)) {
589
+ if (dir === "") continue;
590
+ for (const extension of extensions) try {
591
+ node_fs.default.accessSync(node_path.default.join(dir, command + extension), node_fs.default.constants.X_OK);
592
+ return true;
593
+ } catch {}
594
+ }
595
+ return false;
596
+ };
597
+
598
+ //#endregion
599
+ //#region src/agents/launchable-agents.ts
600
+ const LAUNCHABLE_AGENT_IDS = [
601
+ "claude-code",
602
+ "codex",
603
+ "cursor"
604
+ ];
605
+ const AGENT_BINARIES = {
606
+ "claude-code": "claude",
607
+ codex: "codex",
608
+ cursor: "cursor-agent"
609
+ };
610
+ const AGENT_AUTO_FLAGS = {
611
+ "claude-code": ["--dangerously-skip-permissions"],
612
+ codex: ["--yolo"],
613
+ cursor: ["--force"]
614
+ };
615
+ const detectLaunchableAgents = () => {
616
+ if (process.platform === "win32") return [];
617
+ return LAUNCHABLE_AGENT_IDS.filter((agentId) => isCommandAvailable(AGENT_BINARIES[agentId]));
618
+ };
619
+
620
+ //#endregion
621
+ //#region src/agents/launch-agent.ts
622
+ const launchAgent = (agentId, prompt) => new Promise((resolve) => {
623
+ const child = (0, node_child_process.spawn)(AGENT_BINARIES[agentId], [...AGENT_AUTO_FLAGS[agentId], prompt], { stdio: "inherit" });
624
+ child.once("error", () => {
625
+ resolve(false);
626
+ });
627
+ child.once("exit", () => {
628
+ resolve(true);
629
+ });
630
+ });
631
+
632
+ //#endregion
633
+ //#region src/agents/skill-install.ts
634
+ const moduleDir = __dirname;
635
+ const getSkillSourceDirectory = () => {
636
+ const candidates = [node_path.default.resolve(moduleDir, "../skill/docker-doctor"), node_path.default.resolve(moduleDir, "../../../../skills/docker-doctor")];
637
+ for (const candidate of candidates) if (node_fs.default.existsSync(node_path.default.join(candidate, agent_install.SKILL_MANIFEST_FILE))) return candidate;
638
+ return null;
639
+ };
640
+ const installSkillForAgents = async (agents, projectRoot) => {
641
+ const source = getSkillSourceDirectory();
642
+ if (!source) return null;
643
+ try {
644
+ return await (0, agent_install.installSkillsFromSource)({
645
+ agents,
646
+ cwd: projectRoot,
647
+ mode: "copy",
648
+ source
649
+ });
650
+ } catch {
651
+ return null;
652
+ }
653
+ };
654
+
655
+ //#endregion
16
656
  //#region src/formatters/terminal.ts
17
657
  const printCodeFrame = (content, line, severityColor) => {
18
658
  if (!content || !line) return;
@@ -23,23 +663,23 @@ const printCodeFrame = (content, line, severityColor) => {
23
663
  const rawLine = lines[i - 1];
24
664
  const isTarget = i === line;
25
665
  const lineNumberStr = String(i).padStart(5, " ");
26
- if (isTarget) console.log(` ${severityColor(">")} ${chalk.default.bold(lineNumberStr)} │ ${chalk.default.white(rawLine)}`);
27
- else console.log(` ${chalk.default.dim(lineNumberStr)} │ ${chalk.default.dim(rawLine)}`);
666
+ if (isTarget) console.log(` ${severityColor(">")} ${chalk.bold(lineNumberStr)} │ ${chalk.white(rawLine)}`);
667
+ else console.log(` ${chalk.dim(lineNumberStr)} │ ${chalk.dim(rawLine)}`);
28
668
  }
29
669
  console.log();
30
670
  };
31
671
  const printDiscoveredFiles = (project) => {
32
672
  console.log(`\nDiscovered Files:`);
33
- console.log(` Dockerfile(s): ${project.dockerfiles.length ? project.dockerfiles.map((f) => chalk.default.cyan(f)).join(", ") : chalk.default.dim("None")}`);
34
- console.log(` Compose file(s): ${project.composeFiles.length ? project.composeFiles.map((f) => chalk.default.cyan(f)).join(", ") : chalk.default.dim("None")}`);
673
+ console.log(` Dockerfile(s): ${project.dockerfiles.length ? project.dockerfiles.map((f) => chalk.cyan(f)).join(", ") : chalk.dim("None")}`);
674
+ console.log(` Compose file(s): ${project.composeFiles.length ? project.composeFiles.map((f) => chalk.cyan(f)).join(", ") : chalk.dim("None")}`);
35
675
  };
36
676
  const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCounts) => {
37
677
  if (diagnostics.length === 0) {
38
- console.log(`\n${chalk.default.green.bold("✔ No issues found! Your Docker setup looks healthy.")}`);
678
+ console.log(`\n${chalk.green.bold("✔ No issues found! Your Docker setup looks healthy.")}`);
39
679
  return;
40
680
  }
41
681
  if (!verbose) {
42
- console.log(`\n All ${chalk.default.bold(diagnostics.length)} issues\n`);
682
+ console.log(`\n All ${chalk.bold(diagnostics.length)} issues\n`);
43
683
  for (const cat of [
44
684
  "Security",
45
685
  "Performance",
@@ -49,16 +689,16 @@ const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCount
49
689
  ]) {
50
690
  const count = categoryIssueCounts[cat];
51
691
  const issueLabel = count === 1 ? "1 issue" : `${count} issues`;
52
- console.log(` ${cat} › ${chalk.default.dim(issueLabel)}`);
692
+ console.log(` ${cat} › ${chalk.dim(issueLabel)}`);
53
693
  }
54
- console.log(`\n Run ${chalk.default.cyan("docker-doctor --verbose")} to list every error and warning`);
694
+ console.log(`\n Run ${chalk.cyan("docker-doctor --verbose")} to list every error and warning`);
55
695
  const ruleCounts = {};
56
696
  for (const d of diagnostics) ruleCounts[d.rule] = (ruleCounts[d.rule] || 0) + 1;
57
697
  const migrationRules = Object.entries(ruleCounts).filter(([_, count]) => count >= 5);
58
698
  if (migrationRules.length > 0) {
59
699
  console.log();
60
- console.log(` ${chalk.default.yellow("⚠ Migration-scale change: sample before you sweep")}`);
61
- for (const [rule, count] of migrationRules) console.log(` ${chalk.default.cyan(rule)} ×${count} across ${count} files`);
700
+ console.log(` ${chalk.yellow("⚠ Migration-scale change: sample before you sweep")}`);
701
+ for (const [rule, count] of migrationRules) console.log(` ${chalk.cyan(rule)} ×${count} across ${count} files`);
62
702
  console.log(` Fixing all of them at once is hard to review and prone to`);
63
703
  console.log(` subtle mistakes across the whole repo. Fix a representative`);
64
704
  console.log(` few first and confirm the recipe holds. Then get the code`);
@@ -67,30 +707,30 @@ const printDiagnostics = (diagnostics, verbose, fileContents, categoryIssueCount
67
707
  }
68
708
  return;
69
709
  }
70
- console.log(`\nFound ${chalk.default.bold(diagnostics.length)} issue(s):`);
710
+ console.log(`\nFound ${chalk.bold(diagnostics.length)} issue(s):`);
71
711
  const filesGrouped = {};
72
712
  for (const d of diagnostics) {
73
713
  if (!filesGrouped[d.file]) filesGrouped[d.file] = [];
74
714
  filesGrouped[d.file].push(d);
75
715
  }
76
716
  for (const [file, fileDiags] of Object.entries(filesGrouped)) {
77
- console.log(`\n${chalk.default.underline.bold(file)}`);
717
+ console.log(`\n${chalk.underline.bold(file)}`);
78
718
  for (const d of fileDiags) {
79
- let sevColor = chalk.default.cyan;
719
+ let sevColor = chalk.cyan;
80
720
  let prefix = "ℹ INFO";
81
721
  if (d.severity === "error") {
82
- sevColor = chalk.default.red.bold;
722
+ sevColor = chalk.red.bold;
83
723
  prefix = "✖ ERROR";
84
724
  } else if (d.severity === "warning") {
85
- sevColor = chalk.default.yellow;
725
+ sevColor = chalk.yellow;
86
726
  prefix = "⚠ WARN";
87
727
  }
88
728
  const lineInfo = d.line ? `:${d.line}` : "";
89
- console.log(` ${sevColor(prefix)} [${chalk.default.dim(d.rule)}]${lineInfo}`);
729
+ console.log(` ${sevColor(prefix)} [${chalk.dim(d.rule)}]${lineInfo}`);
90
730
  const content = fileContents[file];
91
731
  printCodeFrame(content, d.line, sevColor);
92
- console.log(` ${chalk.default.white(d.message)}`);
93
- console.log(` ${chalk.default.dim("Help:")} ${d.help}`);
732
+ console.log(` ${chalk.white(d.message)}`);
733
+ console.log(` ${chalk.dim("Help:")} ${d.help}`);
94
734
  console.log();
95
735
  }
96
736
  }
@@ -100,10 +740,10 @@ const getWhaleMascot = (score, border) => {
100
740
  let spout = " ";
101
741
  if (score >= 75) {
102
742
  eyes = "◠ ◠";
103
- spout = chalk.default.cyan(" \":\" ");
743
+ spout = chalk.cyan(" \":\" ");
104
744
  } else if (score >= 50) {
105
745
  eyes = "• •";
106
- spout = chalk.default.cyan(" . ");
746
+ spout = chalk.cyan(" . ");
107
747
  }
108
748
  return [
109
749
  spout,
@@ -116,10 +756,10 @@ const easeOutCubic = (x) => 1 - (1 - x) ** 3;
116
756
  const printScoreBox = async (score, label, categoryIssueCounts, warningsCount, errorsCount) => {
117
757
  const { isTTY } = process.stdout;
118
758
  const shouldAnimate = isTTY && !process.env.CI && !process.env.NO_ANIMATION && process.env.TERM !== "dumb" && process.env.NODE_ENV !== "test";
119
- let scoreColor = chalk.default.red.bold;
120
- if (score >= 90) scoreColor = chalk.default.green.bold;
121
- else if (score >= 75) scoreColor = chalk.default.yellow.bold;
122
- else if (score >= 50) scoreColor = chalk.default.magenta.bold;
759
+ let scoreColor = chalk.red.bold;
760
+ if (score >= 90) scoreColor = chalk.green.bold;
761
+ else if (score >= 75) scoreColor = chalk.yellow.bold;
762
+ else if (score >= 50) scoreColor = chalk.magenta.bold;
123
763
  const whaleLines = getWhaleMascot(score, scoreColor);
124
764
  const shareUrl = `https://docker-doctor.vercel.app/share?s=${score}&w=${warningsCount}&e=${errorsCount}`;
125
765
  if (shouldAnimate) {
@@ -132,10 +772,10 @@ const printScoreBox = async (score, label, categoryIssueCounts, warningsCount, e
132
772
  const currentScore = Math.round(score * progress);
133
773
  const filledBlocks = Math.round(currentScore / 2);
134
774
  const emptyBlocks = 50 - filledBlocks;
135
- const bar = scoreColor("█".repeat(filledBlocks)) + chalk.default.dim("░".repeat(emptyBlocks));
775
+ const bar = scoreColor("█".repeat(filledBlocks)) + chalk.dim("░".repeat(emptyBlocks));
136
776
  if (frame > 0) process.stdout.write("\x1B[4A\r");
137
777
  else console.log();
138
- process.stdout.write(` ${whaleLines[0]} ${scoreColor(`${currentScore} / 100`)} ${scoreColor(label)}\n ${whaleLines[1]} ${bar}\n ${whaleLines[2]} ${chalk.default.dim("Docker Doctor (https://docker-doctor.vercel.app)")}\n ${whaleLines[3]}\n`);
778
+ process.stdout.write(` ${whaleLines[0]} ${scoreColor(`${currentScore} / 100`)} ${scoreColor(label)}\n ${whaleLines[1]} ${bar}\n ${whaleLines[2]} ${chalk.dim("Docker Doctor (https://docker-doctor.vercel.app)")}\n ${whaleLines[3]}\n`);
139
779
  if (frame < frameCount) await (0, node_timers_promises.setTimeout)(frameDelay);
140
780
  }
141
781
  } finally {
@@ -144,24 +784,24 @@ const printScoreBox = async (score, label, categoryIssueCounts, warningsCount, e
144
784
  } else {
145
785
  const filledBlocks = Math.round(score / 2);
146
786
  const emptyBlocks = 50 - filledBlocks;
147
- const bar = scoreColor("█".repeat(filledBlocks)) + chalk.default.dim("░".repeat(emptyBlocks));
787
+ const bar = scoreColor("█".repeat(filledBlocks)) + chalk.dim("░".repeat(emptyBlocks));
148
788
  console.log(`\n ${whaleLines[0]} ${scoreColor(`${score} / 100`)} ${scoreColor(label)}`);
149
789
  console.log(` ${whaleLines[1]} ${bar}`);
150
- console.log(` ${whaleLines[2]} ${chalk.default.dim("Docker Doctor (https://docker-doctor.vercel.app)")}`);
790
+ console.log(` ${whaleLines[2]} ${chalk.dim("Docker Doctor (https://docker-doctor.vercel.app)")}`);
151
791
  console.log(` ${whaleLines[3]}`);
152
792
  }
153
- console.log(`\n ${chalk.default.dim("────────────────────────────────────────────────────────────")}\n`);
154
- console.log(` Share: ${chalk.default.cyan(shareUrl)}`);
793
+ console.log(`\n ${chalk.dim("────────────────────────────────────────────────────────────")}\n`);
794
+ console.log(` Share: ${chalk.cyan(shareUrl)}`);
155
795
  console.log(` Tell others how you did on socials\n`);
156
- console.log(` Docs: ${chalk.default.cyan("https://github.com/PunGrumpy/docker-doctor/docs")}`);
796
+ console.log(` Docs: ${chalk.cyan("https://docker-doctor.vercel.app")}`);
157
797
  console.log(` Learn more about fixing issues, setting up CI/CD, and`);
158
798
  console.log(` configuring rules with a config file\n`);
159
- console.log(` GitHub: ${chalk.default.cyan("https://github.com/PunGrumpy/docker-doctor")}`);
799
+ console.log(` GitHub: ${chalk.cyan("https://github.com/PunGrumpy/docker-doctor")}`);
160
800
  console.log(` Report issues and star the repository!`);
161
801
  };
162
802
  const formatTerminal = async (diagnostics, score, label, project, verbose = false, fileContents = {}) => {
163
803
  if (verbose) {
164
- console.log(`\n${chalk.default.bold("Docker Doctor Diagnostics")}`);
804
+ console.log(`\n${chalk.bold("Docker Doctor Diagnostics")}`);
165
805
  console.log(`=================================`);
166
806
  printDiscoveredFiles(project);
167
807
  }
@@ -196,12 +836,12 @@ const askConfirm = (question, defaultYes = false) => {
196
836
  process.stdout.write("\x1B[?25l");
197
837
  const render = (firstTime = false) => {
198
838
  if (!firstTime) process.stdout.write("\x1B[3A\r");
199
- process.stdout.write(`\r\u001B[K ${chalk.default.green("✔")} ${chalk.default.bold(question)}\n`);
200
- const yesPrefix = value ? chalk.default.cyan("❯ ") : " ";
201
- const yesText = value ? chalk.default.cyan.bold("Yes") : chalk.default.dim("Yes");
839
+ process.stdout.write(`\r\u001B[K ${chalk.green("✔")} ${chalk.bold(question)}\n`);
840
+ const yesPrefix = value ? chalk.cyan("❯ ") : " ";
841
+ const yesText = value ? chalk.cyan.bold("Yes") : chalk.dim("Yes");
202
842
  process.stdout.write(`\r\u001B[K${yesPrefix}${yesText}\n`);
203
- const noPrefix = value ? " " : chalk.default.cyan("❯ ");
204
- const noText = value ? chalk.default.dim("No") : chalk.default.cyan.bold("No");
843
+ const noPrefix = value ? " " : chalk.cyan("❯ ");
844
+ const noText = value ? chalk.dim("No") : chalk.cyan.bold("No");
205
845
  process.stdout.write(`\r\u001B[K${noPrefix}${noText}\n`);
206
846
  };
207
847
  render(true);
@@ -224,7 +864,7 @@ const askConfirm = (question, defaultYes = false) => {
224
864
  } else if (key.name === "return" || key.name === "enter" || str === "\r" || str === "\n") {
225
865
  cleanup();
226
866
  process.stdout.write("\x1B[3A\r\x1B[K");
227
- process.stdout.write(` ${chalk.default.green("✔")} ${chalk.default.bold(question)} › ${value ? chalk.default.cyan("Yes") : chalk.default.dim("No")}\n`);
867
+ process.stdout.write(` ${chalk.green("✔")} ${chalk.bold(question)} › ${value ? chalk.cyan("Yes") : chalk.dim("No")}\n`);
228
868
  process.stdout.write("\r\x1B[K\n");
229
869
  process.stdout.write("\r\x1B[K\n");
230
870
  process.stdout.write("\x1B[2A");
@@ -248,12 +888,12 @@ const askSelect = (question, options, defaultIndex = 0) => {
248
888
  process.stdout.write("\x1B[?25l");
249
889
  const render = (firstTime = false) => {
250
890
  if (!firstTime) process.stdout.write(`\u001B[${options.length + 1}A\r`);
251
- process.stdout.write(`\r\u001B[K ${chalk.default.green("✔")} ${chalk.default.bold(question)}\n`);
891
+ process.stdout.write(`\r\u001B[K ${chalk.green("✔")} ${chalk.bold(question)}\n`);
252
892
  let i = 0;
253
893
  for (const option of options) {
254
894
  const isSelected = i === index;
255
- const prefix = isSelected ? chalk.default.cyan("❯ ") : " ";
256
- const text = isSelected ? chalk.default.cyan.bold(option) : chalk.default.dim(option);
895
+ const prefix = isSelected ? chalk.cyan("❯ ") : " ";
896
+ const text = isSelected ? chalk.cyan.bold(option) : chalk.dim(option);
257
897
  process.stdout.write(`\r\u001B[K${prefix}${text}\n`);
258
898
  i += 1;
259
899
  }
@@ -275,7 +915,7 @@ const askSelect = (question, options, defaultIndex = 0) => {
275
915
  } else if (key.name === "return" || key.name === "enter" || str === "\r" || str === "\n") {
276
916
  cleanup();
277
917
  process.stdout.write(`\u001B[${options.length + 1}A\r\u001B[K`);
278
- process.stdout.write(` ${chalk.default.green("✔")} ${chalk.default.bold(question)} › ${chalk.default.cyan(options[index])}\n`);
918
+ process.stdout.write(` ${chalk.green("✔")} ${chalk.bold(question)} › ${chalk.cyan(options[index])}\n`);
279
919
  for (const _ of options) process.stdout.write("\r\x1B[K\n");
280
920
  process.stdout.write(`\u001B[${options.length}A`);
281
921
  resolve(index);
@@ -288,7 +928,104 @@ const askSelect = (question, options, defaultIndex = 0) => {
288
928
  process.stdin.on("keypress", handleKeypress);
289
929
  });
290
930
  };
291
- const runInteractiveWizard = async () => {
931
+ const askMultiSelect = (question, options) => {
932
+ if (!process.stdin.isTTY) return Promise.resolve(options.flatMap((option, i) => option.selected ? [i] : []));
933
+ return new Promise((resolve) => {
934
+ let index = 0;
935
+ const selected = options.map((option) => option.selected);
936
+ const lineCount = options.length + 2;
937
+ node_readline.default.emitKeypressEvents(process.stdin);
938
+ process.stdin.setRawMode(true);
939
+ process.stdin.resume();
940
+ process.stdout.write("\x1B[?25l");
941
+ const render = (firstTime = false) => {
942
+ if (!firstTime) process.stdout.write(`\u001B[${lineCount}A\r`);
943
+ process.stdout.write(`\r\u001B[K ${chalk.green("✔")} ${chalk.bold(question)}\n`);
944
+ let i = 0;
945
+ for (const option of options) {
946
+ const isCursor = i === index;
947
+ const cursor = isCursor ? chalk.cyan("❯ ") : " ";
948
+ const box = selected[i] ? chalk.cyan("[x]") : chalk.dim("[ ]");
949
+ let text = chalk.dim(option.label);
950
+ if (isCursor) text = chalk.cyan.bold(option.label);
951
+ else if (selected[i]) text = option.label;
952
+ process.stdout.write(`\r\u001B[K${cursor}${box} ${text}\n`);
953
+ i += 1;
954
+ }
955
+ process.stdout.write(`\r\u001B[K ${chalk.dim("space to toggle · enter to confirm")}\n`);
956
+ };
957
+ render(true);
958
+ const handleKeypress = (str, key) => {
959
+ const cleanup = () => {
960
+ process.stdin.removeListener("keypress", handleKeypress);
961
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
962
+ process.stdin.pause();
963
+ process.stdout.write("\x1B[?25h");
964
+ };
965
+ if (key.name === "up" || key.name === "k") {
966
+ index = (index - 1 + options.length) % options.length;
967
+ render();
968
+ } else if (key.name === "down" || key.name === "j") {
969
+ index = (index + 1) % options.length;
970
+ render();
971
+ } else if (key.name === "space" || str === " ") {
972
+ selected[index] = !selected[index];
973
+ render();
974
+ } else if (key.name === "return" || key.name === "enter" || str === "\r" || str === "\n") {
975
+ cleanup();
976
+ const chosen = options.flatMap((option, i) => selected[i] ? [option.label] : []);
977
+ process.stdout.write(`\u001B[${lineCount}A\r\u001B[K`);
978
+ process.stdout.write(` ${chalk.green("✔")} ${chalk.bold(question)} › ${chosen.length > 0 ? chalk.cyan(chosen.join(", ")) : chalk.dim("none")}\n`);
979
+ for (let i = 0; i < lineCount - 1; i += 1) process.stdout.write("\r\x1B[K\n");
980
+ process.stdout.write(`\u001B[${lineCount - 1}A`);
981
+ resolve(options.flatMap((_, i) => selected[i] ? [i] : []));
982
+ } else if (key.ctrl && key.name === "c") {
983
+ cleanup();
984
+ process.stdout.write("\n");
985
+ process.exit(130);
986
+ }
987
+ };
988
+ process.stdin.on("keypress", handleKeypress);
989
+ });
990
+ };
991
+ const printAgentPrompt = (payload) => {
992
+ console.log(`\n${chalk.dim("──── Agent prompt ────")}`);
993
+ console.log(payload);
994
+ console.log(chalk.dim("──────────────────────"));
995
+ };
996
+ const agentDisplayName = (agent) => agent === "universal" ? "Universal" : (0, agent_install.getSkillAgentConfig)(agent).displayName;
997
+ const runAgentHandoff = async (context) => {
998
+ const launchable = detectLaunchableAgents();
999
+ const options = [
1000
+ ...launchable.map((agentId) => agentDisplayName(agentId)),
1001
+ "Copy prompt to clipboard",
1002
+ "Skip"
1003
+ ];
1004
+ const skipIndex = options.length - 1;
1005
+ const clipboardIndex = options.length - 2;
1006
+ const choice = await askSelect("What would you like to do next?", options);
1007
+ if (choice === skipIndex) return;
1008
+ await writeDiagnosticsDirectory(context.diagnostics, context.report, context.rootDir);
1009
+ await ensureGitignoreEntry(context.rootDir);
1010
+ const payload = buildHandoffPayload({
1011
+ diagnostics: context.diagnostics,
1012
+ projectName: node_path.default.basename(context.rootDir)
1013
+ });
1014
+ if (choice === clipboardIndex) {
1015
+ if (await copyToClipboard(payload)) console.log(`\n ${chalk.green("✔")} Prompt copied — paste it into any agent or chat.`);
1016
+ else printAgentPrompt(payload);
1017
+ return;
1018
+ }
1019
+ const agentId = launchable[choice];
1020
+ const installResult = await installSkillForAgents([agentId], context.rootDir);
1021
+ if (installResult && installResult.installed.length > 0) console.log(`\n ${chalk.green("✔")} Installed the docker-doctor skill for ${agentDisplayName(agentId)}`);
1022
+ console.log(`\n Handing off to ${agentDisplayName(agentId)}...\n`);
1023
+ if (!await launchAgent(agentId, payload)) {
1024
+ console.log(` ${chalk.yellow("⚠")} Couldn't launch ${AGENT_BINARIES[agentId]}. Here's the prompt instead:`);
1025
+ printAgentPrompt(payload);
1026
+ }
1027
+ };
1028
+ const runInteractiveWizard = async (context) => {
292
1029
  try {
293
1030
  if (await askConfirm("Add Docker Doctor to GitHub Actions?")) {
294
1031
  const workflowDir = node_path.default.resolve(".github/workflows");
@@ -312,13 +1049,11 @@ jobs:
312
1049
  - name: Run docker-doctor
313
1050
  run: bunx docker-doctor .
314
1051
  `, "utf-8");
315
- console.log(`\n ${chalk.default.green("✨")} Created ${chalk.default.cyan(".github/workflows/docker-doctor.yml")}!`);
1052
+ console.log(`\n ${chalk.green("✨")} Created ${chalk.cyan(".github/workflows/docker-doctor.yml")}!`);
316
1053
  console.log(` Scan every pull request to prevent new Docker issues while you fix the backlog.`);
317
1054
  }
318
- if (await askSelect("What would you like to do next?", ["View rules list", "Skip"]) === 0) {
319
- console.log(`\n ${chalk.default.bold("Available Rules:")}`);
320
- for (const r of require_src.allRules) console.log(` - ${chalk.default.cyan(r.key)}: ${r.message} (${chalk.default.dim(r.category)})`);
321
- }
1055
+ if (context.diagnostics.length === 0) return;
1056
+ await runAgentHandoff(context);
322
1057
  } catch {}
323
1058
  };
324
1059
  const runRulesEngine = async (rootDir, project, rulesConfig, projectFilesList, fileContents, options, setStatus) => {
@@ -409,9 +1144,9 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
409
1144
  };
410
1145
  if (process.stdout.isTTY && !isSilent) {
411
1146
  process.stdout.write("\x1B[?25l");
412
- process.stdout.write(`${chalk.default.cyan(spinnerFrames[0])} ${statusText}`);
1147
+ process.stdout.write(`${chalk.cyan(spinnerFrames[0])} ${statusText}`);
413
1148
  spinnerInterval = setInterval(() => {
414
- process.stdout.write(`\r\u001B[K${chalk.default.cyan(spinnerFrames[frameIndex])} ${statusText}`);
1149
+ process.stdout.write(`\r\u001B[K${chalk.cyan(spinnerFrames[frameIndex])} ${statusText}`);
415
1150
  frameIndex = (frameIndex + 1) % spinnerFrames.length;
416
1151
  }, 80);
417
1152
  }
@@ -437,21 +1172,26 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
437
1172
  spinnerInterval = null;
438
1173
  process.stdout.write("\r\x1B[K\x1B[?25h");
439
1174
  }
440
- if (process.stdout.isTTY && !isSilent) console.log(`${chalk.default.green("✔")} Scanned ${projectFilesList.length} files in ${duration}s [~${concurrency} workers]`);
1175
+ if (process.stdout.isTTY && !isSilent) console.log(`${chalk.green("✔")} Scanned ${projectFilesList.length} files in ${duration}s [~${concurrency} workers]`);
441
1176
  if (options.score) {
442
1177
  console.log(score);
443
- process.exit(score < 50 ? 1 : 0);
1178
+ process.exitCode = score < 50 ? 1 : 0;
1179
+ return;
444
1180
  } else if (options.json) {
445
1181
  const report = require_src.toJsonReport(filteredDiagnostics, score, label, project);
446
1182
  console.log(JSON.stringify(report, null, 2));
447
1183
  const hasErrors = filteredDiagnostics.some((d) => d.severity === "error");
448
- process.exit(hasErrors ? 1 : 0);
449
- } else {
450
- await formatTerminal(filteredDiagnostics, score, label, project, options.verbose, fileContents);
451
- const hasErrors = filteredDiagnostics.some((d) => d.severity === "error");
452
- if (process.stdout.isTTY && process.stdin.isTTY) await runInteractiveWizard();
453
- process.exit(hasErrors ? 1 : 0);
1184
+ process.exitCode = hasErrors ? 1 : 0;
1185
+ return;
454
1186
  }
1187
+ await formatTerminal(filteredDiagnostics, score, label, project, options.verbose, fileContents);
1188
+ const hasErrors = filteredDiagnostics.some((d) => d.severity === "error");
1189
+ if (process.stdout.isTTY && process.stdin.isTTY) await runInteractiveWizard({
1190
+ diagnostics: filteredDiagnostics,
1191
+ report: require_src.toJsonReport(filteredDiagnostics, score, label, project),
1192
+ rootDir
1193
+ });
1194
+ process.exitCode = hasErrors ? 1 : 0;
455
1195
  } finally {
456
1196
  if (spinnerInterval !== null) {
457
1197
  clearInterval(spinnerInterval);
@@ -464,6 +1204,55 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
464
1204
  process.exit(1);
465
1205
  }
466
1206
  });
1207
+ const CURATED_INSTALL_AGENTS = [
1208
+ "claude-code",
1209
+ "codex",
1210
+ "cursor",
1211
+ "opencode"
1212
+ ];
1213
+ const resolveInstallAgents = async (requested) => {
1214
+ if (requested && requested.length > 0) {
1215
+ const invalid = requested.filter((agent) => !(0, agent_install.isSkillAgentType)(agent));
1216
+ if (invalid.length > 0) {
1217
+ console.error(`Unknown agent id(s): ${invalid.join(", ")}`);
1218
+ console.error(`Valid ids: ${(0, agent_install.getSkillAgentTypes)().filter((agent) => agent !== "universal").join(", ")}`);
1219
+ return null;
1220
+ }
1221
+ return requested.filter((agent) => (0, agent_install.isSkillAgentType)(agent));
1222
+ }
1223
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
1224
+ console.error("Non-interactive run: pass --agent <id...> (e.g. --agent claude-code cursor).");
1225
+ return null;
1226
+ }
1227
+ const detected = (await (0, agent_install.detectInstalledSkillAgents)()).filter((agent) => agent !== "universal");
1228
+ const choices = [.../* @__PURE__ */ new Set([...detected, ...CURATED_INSTALL_AGENTS])];
1229
+ const detectedSet = new Set(detected);
1230
+ return (await askMultiSelect("Which coding agents should get the docker-doctor skill?", choices.map((agent) => ({
1231
+ label: agentDisplayName(agent),
1232
+ selected: detectedSet.has(agent)
1233
+ })))).map((i) => choices[i]);
1234
+ };
1235
+ 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) => {
1236
+ if (!getSkillSourceDirectory()) {
1237
+ console.error("Bundled skill not found — this looks like a broken installation.");
1238
+ process.exit(1);
1239
+ }
1240
+ const agents = await resolveInstallAgents(options.agent);
1241
+ if (agents === null) process.exit(1);
1242
+ if (agents.length === 0) {
1243
+ console.log("Nothing selected — skipped.");
1244
+ return;
1245
+ }
1246
+ const result = await installSkillForAgents(agents, process.cwd());
1247
+ if (!result) {
1248
+ console.error("Failed to install the skill.");
1249
+ process.exit(1);
1250
+ }
1251
+ for (const installed of result.installed) console.log(` ${chalk.green("✔")} ${agentDisplayName(installed.agent)} → ${installed.path}`);
1252
+ for (const failed of result.failed) console.log(` ${chalk.red("✖")} ${agentDisplayName(failed.agent)}: ${failed.error}`);
1253
+ if (result.installed.length > 0) console.log(`\n The agent can now run ${chalk.cyan("/docker-doctor")} to scan and triage this project.`);
1254
+ process.exitCode = result.failed.length > 0 ? 1 : 0;
1255
+ });
467
1256
  const rules = program.command("rules").description("manage and list configuration rules");
468
1257
  rules.command("list").description("list all available rules").action(() => {
469
1258
  console.log("\nAvailable Rules:");