@docker-doctor/cli 0.3.0 → 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/README.md +14 -5
- package/dist/cli.cjs +387 -7
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +386 -7
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +6 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +6 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{src-TEhFWhpk.cjs → src-38C8Tk2b.cjs} +20 -10
- package/dist/src-38C8Tk2b.cjs.map +1 -0
- package/dist/{src-EqtziccA.mjs → src-BOzcDwm-.mjs} +20 -10
- package/dist/src-BOzcDwm-.mjs.map +1 -0
- package/package.json +4 -2
- package/skill/docker-doctor/SKILL.md +60 -0
- package/skill/docker-doctor/references/explain.md +72 -0
- package/dist/src-EqtziccA.mjs.map +0 -1
- package/dist/src-TEhFWhpk.cjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
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-
|
|
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 { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource, isSkillAgentType } from "agent-install";
|
|
8
9
|
import process$1 from "node:process";
|
|
9
10
|
import tty from "node:tty";
|
|
10
11
|
import { Command } from "commander";
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import fs$1 from "node:fs";
|
|
11
14
|
|
|
12
15
|
//#region ../../node_modules/.bun/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
13
16
|
const ANSI_BACKGROUND_OFFSET = 10;
|
|
@@ -414,6 +417,234 @@ Object.defineProperties(createChalk.prototype, styles);
|
|
|
414
417
|
const chalk = createChalk();
|
|
415
418
|
const chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
416
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
|
+
|
|
417
648
|
//#endregion
|
|
418
649
|
//#region src/formatters/terminal.ts
|
|
419
650
|
const printCodeFrame = (content, line, severityColor) => {
|
|
@@ -690,7 +921,104 @@ const askSelect = (question, options, defaultIndex = 0) => {
|
|
|
690
921
|
process.stdin.on("keypress", handleKeypress);
|
|
691
922
|
});
|
|
692
923
|
};
|
|
693
|
-
const
|
|
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) => {
|
|
694
1022
|
try {
|
|
695
1023
|
if (await askConfirm("Add Docker Doctor to GitHub Actions?")) {
|
|
696
1024
|
const workflowDir = path.resolve(".github/workflows");
|
|
@@ -717,10 +1045,8 @@ jobs:
|
|
|
717
1045
|
console.log(`\n ${chalk.green("✨")} Created ${chalk.cyan(".github/workflows/docker-doctor.yml")}!`);
|
|
718
1046
|
console.log(` Scan every pull request to prevent new Docker issues while you fix the backlog.`);
|
|
719
1047
|
}
|
|
720
|
-
if (
|
|
721
|
-
|
|
722
|
-
for (const r of allRules) console.log(` - ${chalk.cyan(r.key)}: ${r.message} (${chalk.dim(r.category)})`);
|
|
723
|
-
}
|
|
1048
|
+
if (context.diagnostics.length === 0) return;
|
|
1049
|
+
await runAgentHandoff(context);
|
|
724
1050
|
} catch {}
|
|
725
1051
|
};
|
|
726
1052
|
const runRulesEngine = async (rootDir, project, rulesConfig, projectFilesList, fileContents, options, setStatus) => {
|
|
@@ -853,7 +1179,11 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
853
1179
|
}
|
|
854
1180
|
await formatTerminal(filteredDiagnostics, score, label, project, options.verbose, fileContents);
|
|
855
1181
|
const hasErrors = filteredDiagnostics.some((d) => d.severity === "error");
|
|
856
|
-
if (process.stdout.isTTY && process.stdin.isTTY) await runInteractiveWizard(
|
|
1182
|
+
if (process.stdout.isTTY && process.stdin.isTTY) await runInteractiveWizard({
|
|
1183
|
+
diagnostics: filteredDiagnostics,
|
|
1184
|
+
report: toJsonReport(filteredDiagnostics, score, label, project),
|
|
1185
|
+
rootDir
|
|
1186
|
+
});
|
|
857
1187
|
process.exitCode = hasErrors ? 1 : 0;
|
|
858
1188
|
} finally {
|
|
859
1189
|
if (spinnerInterval !== null) {
|
|
@@ -867,6 +1197,55 @@ program.argument("[dir]", "directory to scan", ".").option("-v, --verbose", "sho
|
|
|
867
1197
|
process.exit(1);
|
|
868
1198
|
}
|
|
869
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
|
+
});
|
|
870
1249
|
const rules = program.command("rules").description("manage and list configuration rules");
|
|
871
1250
|
rules.command("list").description("list all available rules").action(() => {
|
|
872
1251
|
console.log("\nAvailable Rules:");
|