Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/agent-handoff-and-install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@docker-doctor/cli": patch
---

Add coding-agent integration: a `docker-doctor install` command that installs the bundled agent skill for any agent-install-supported coding agent (Claude Code, Cursor, Codex, OpenCode, and more), and a post-scan handoff that replaces the old "View rules list" prompt — when a scan finds issues, the CLI now offers to launch a detected agent (`claude`, `codex`, `cursor-agent`) with the issues as its prompt, or copy that prompt to the clipboard. Handoffs write the full report to `.docker-doctor/` (auto-gitignored) and install the skill for the chosen agent.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,6 @@ generated

# Next.js
next-env.d.ts

# Bundled agent skill (copied from skills/docker-doctor at build time)
packages/docker-doctor/skill/
50 changes: 50 additions & 0 deletions apps/web/content/docs/guides/coding-agents.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: Coding Agents
description: Install the Docker Doctor skill for your coding agent and hand scan results straight to it.
---

Docker Doctor is built to work with AI coding agents. There are two pieces: an installable **agent skill** that teaches your agent the `/docker-doctor` triage workflow, and a post-scan **handoff** that sends the issues Docker Doctor just found straight to an agent on your machine.

## Install the skill

```bash
npx @docker-doctor/cli@latest install
```

In a terminal this opens a multi-select of coding agents — agents already detected on your machine are pre-selected — and copies the skill into each agent's project-level skills directory (for example `.claude/skills/docker-doctor/` for Claude Code).

Works with Claude Code, Cursor, Codex, OpenCode, and every other agent supported by [agent-install](https://www.npmjs.com/package/agent-install).

### Non-interactive install

In scripts or CI, pass the agent ids explicitly:

```bash
npx @docker-doctor/cli@latest install --agent claude-code cursor
```

Any `agent-install` agent id is accepted. Unknown ids fail with the full list of valid ones.

## Hand issues to an agent after a scan

When an interactive scan finds issues, Docker Doctor asks what to do next:

```
✔ What would you like to do next?
❯ Claude Code
Copy prompt to clipboard
Skip
```

- **<agent name>** — shown for each agent CLI found on your `PATH` (`claude`, `codex`, `cursor-agent`). Docker Doctor installs the skill for that agent, then launches it in your terminal with a prompt describing every issue, its fix, and the affected files. The agent runs in its auto-approve mode so it can fix the issues end-to-end.
- **Copy prompt to clipboard** — copies the same prompt so you can paste it into any agent or chat.
- **Skip** — do nothing.

## The `.docker-doctor/` report directory

Before handing off, Docker Doctor writes the full scan results to `.docker-doctor/` in your project root:

- `diagnostics.json` — the same report as `--json`
- one `.txt` per rule with the fix recipe and every affected file

The prompt references this directory so the agent can read past the inline summary. Docker Doctor adds `.docker-doctor/` to your `.gitignore` automatically (only if the project is a git repository and the entry is missing).
2 changes: 1 addition & 1 deletion apps/web/content/docs/guides/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "Guides",
"pages": ["getting-started"]
"pages": ["getting-started", "coding-agents"]
}
18 changes: 18 additions & 0 deletions apps/web/content/docs/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ npx @docker-doctor/cli@latest . --json > report.json
| `--score` | Non-zero if the score is below 50 |
| `--json` | Non-zero if any `error`-severity diagnostic is present |

## Install for agents

Install the Docker Doctor agent skill for your coding agents (interactive multi-select in a TTY):

```bash
npx @docker-doctor/cli@latest install
```

Non-interactive runs must name the agents:

```bash
npx @docker-doctor/cli@latest install --agent claude-code cursor
```

See the [Coding Agents guide](/docs/guides/coding-agents) for details.

## Rules subcommands

List every built-in rule, its category, default severity, and description:
Expand All @@ -56,3 +72,5 @@ npx @docker-doctor/cli@latest rules explain docker-doctor/no-root-user
## Interactive mode

When run in a TTY without `--score` or `--json`, docker-doctor shows a spinner while scanning and, after printing results, offers to scaffold a `.github/workflows/docker-doctor.yml` CI workflow for you.

If the scan found issues, it then offers to hand them to a coding agent detected on your machine — launching the agent with the issues as its prompt — or to copy that prompt to your clipboard. See the [Coding Agents guide](/docs/guides/coding-agents).
7 changes: 6 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions packages/docker-doctor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,18 @@ Works with any project that uses Docker.
npx @docker-doctor/cli@latest
```

### 2. Browse rules
### 2. Install for agents

Once you have an audit, install the skill so your coding agent learns the `/docker-doctor` triage workflow and can fix the issues for you:

```bash
npx @docker-doctor/cli@latest rules list
npx @docker-doctor/cli@latest rules explain docker-doctor/no-root-user
npx @docker-doctor/cli@latest install
```

Works with Claude Code, Cursor, Codex, OpenCode, and many more. After an interactive scan finds issues, Docker Doctor also offers to hand them straight to an agent detected on your machine.

[Rules reference →](https://docker-doctor.vercel.app/docs/reference/rules)

### 3. Run in CI

Docker Doctor walks you through setting up a GitHub Actions workflow after your first scan:
Expand Down
4 changes: 3 additions & 1 deletion packages/docker-doctor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"files": [
"dist",
"skill",
"LICENSE"
],
"type": "module",
Expand All @@ -56,9 +57,10 @@
"dev": "NODE_OPTIONS='--max-old-space-size=4096' tsdown --watch",
"test": "bun test",
"typecheck": "tsc --noEmit",
"clean": "git clean -xdf .turbo node_modules dist"
"clean": "git clean -xdf .turbo node_modules dist skill"
},
"dependencies": {
"agent-install": "0.0.8",
"chalk": "^5.4.1",
"commander": "^15.0.0",
"yaml": "^2.7.0"
Expand Down
56 changes: 56 additions & 0 deletions packages/docker-doctor/src/agents/clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { spawn } from "node:child_process";

interface ClipboardCommand {
command: string;
args: string[];
}

const getClipboardCommands = (): ClipboardCommand[] => {
if (process.platform === "darwin") {
return [{ args: [], command: "pbcopy" }];
}
if (process.platform === "win32") {
return [{ args: [], command: "clip" }];
}
return [
{ args: [], command: "wl-copy" },
{ args: ["-selection", "clipboard"], command: "xclip" },
{ args: ["--clipboard", "--input"], command: "xsel" },
];
};

const tryCopy = ({ command, args }: ClipboardCommand, text: string) =>
/* eslint-disable promise/avoid-new */
new Promise<boolean>((resolve) => {
const child = spawn(command, args, {
stdio: ["pipe", "ignore", "ignore"],
});
child.once("error", () => {
resolve(false);
});
child.once("exit", (code) => {
resolve(code === 0);
});
child.stdin.end(text);
});
/* eslint-enable promise/avoid-new */

const tryCommands = async (
commands: ClipboardCommand[],
text: string
): Promise<boolean> => {
const [first, ...rest] = commands;
if (!first) {
return false;
}
if (await tryCopy(first, text)) {
return true;
}
return tryCommands(rest, text);
};

// Best-effort: tries each platform clipboard tool in order (they're fallbacks,
// so the attempts are sequential). Returns false when none worked — the
// caller prints the payload instead.
export const copyToClipboard = (text: string): Promise<boolean> =>
tryCommands(getClipboardCommands(), text);
114 changes: 114 additions & 0 deletions packages/docker-doctor/src/agents/diagnostics-dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import fs from "node:fs/promises";
import path from "node:path";

import type { Diagnostic, JsonReport } from "@docker-doctor/core";

export const DIAGNOSTICS_DIR_NAME = ".docker-doctor";

const UNSAFE_FILE_CHARS = /[^a-z0-9-]+/giu;

const ruleFileName = (rule: string): string => {
const shortKey = rule.split("/").at(-1) ?? rule;
return `${shortKey.replace(UNSAFE_FILE_CHARS, "-")}.txt`;
};

export const groupDiagnosticsByRule = (
diagnostics: Diagnostic[]
): Map<string, Diagnostic[]> => {
const groups = new Map<string, Diagnostic[]>();
for (const diagnostic of diagnostics) {
const group = groups.get(diagnostic.rule);
if (group) {
group.push(diagnostic);
} else {
groups.set(diagnostic.rule, [diagnostic]);
}
}
return groups;
};

// Writes the full scan results into <root>/.docker-doctor/ — diagnostics.json
// (the same shape as `--json`) plus one .txt per rule — so the handed-off
// agent can read past the inline prompt. Recreated fresh on every handoff.
export const writeDiagnosticsDirectory = async (
diagnostics: Diagnostic[],
report: JsonReport,
rootDir: string
): Promise<void> => {
const dir = path.join(rootDir, DIAGNOSTICS_DIR_NAME);
await fs.rm(dir, { force: true, recursive: true });
await fs.mkdir(dir, { recursive: true });

await fs.writeFile(
path.join(dir, "diagnostics.json"),
JSON.stringify(report, null, 2),
"utf-8"
);

const writes: Promise<void>[] = [];
for (const [rule, ruleDiagnostics] of groupDiagnosticsByRule(diagnostics)) {
const [first] = ruleDiagnostics;
const lines = [
`${rule} (${first.severity})`,
first.message,
`Fix: ${first.help}`,
"",
...ruleDiagnostics.map(
(d) => `${d.file}${d.line === undefined ? "" : `:${d.line}`}`
),
"",
];
writes.push(
fs.writeFile(
path.join(dir, ruleFileName(rule)),
lines.join("\n"),
"utf-8"
)
);
}
await Promise.all(writes);
};

// Keeps the scan output out of version control: appends `.docker-doctor/` to
// the project's .gitignore when it isn't covered yet. Only creates a new
// .gitignore when the project is actually a git repository.
export const ensureGitignoreEntry = async (rootDir: string): Promise<void> => {
const gitignorePath = path.join(rootDir, ".gitignore");

let existing: string | null = null;
try {
existing = await fs.readFile(gitignorePath, "utf-8");
} catch {
existing = null;
}

if (existing !== null) {
const isIgnored = existing
.split(/\r?\n/u)
.some((line) =>
[
DIAGNOSTICS_DIR_NAME,
`${DIAGNOSTICS_DIR_NAME}/`,
`/${DIAGNOSTICS_DIR_NAME}`,
`/${DIAGNOSTICS_DIR_NAME}/`,
].includes(line.trim())
);
if (isIgnored) {
return;
}
const separator = existing.endsWith("\n") || existing === "" ? "" : "\n";
await fs.writeFile(
gitignorePath,
`${existing}${separator}${DIAGNOSTICS_DIR_NAME}/\n`,
"utf-8"
);
return;
}

try {
await fs.access(path.join(rootDir, ".git"));
} catch {
return;
}
await fs.writeFile(gitignorePath, `${DIAGNOSTICS_DIR_NAME}/\n`, "utf-8");
};
Loading