codex-plugin-doctor 0.1.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.
@@ -0,0 +1,44 @@
1
+ export function buildMarkdownReport(result, options) {
2
+ const failCount = result.findings.filter((finding) => finding.severity === "fail").length;
3
+ const warnCount = result.findings.filter((finding) => finding.severity === "warn").length;
4
+ const lines = [
5
+ "# Codex Plugin Doctor Report",
6
+ "",
7
+ "| Field | Value |",
8
+ "| --- | --- |",
9
+ `| Target | \`${result.targetPath}\` |`,
10
+ `| Status | ${result.status.toUpperCase()} |`,
11
+ `| Exit Code | ${result.exitCode} |`,
12
+ `| Runtime Probe | ${options.runtimeProbeEnabled ? "enabled" : "disabled"} |`,
13
+ `| Fail Findings | ${failCount} |`,
14
+ `| Warn Findings | ${warnCount} |`,
15
+ `| Total Findings | ${result.findings.length} |`
16
+ ];
17
+ if (result.findings.length === 0) {
18
+ lines.push("", "No findings.");
19
+ return lines.join("\n");
20
+ }
21
+ if (result.runtimeScorecard) {
22
+ lines.push("", "## Runtime Scorecard", "");
23
+ lines.push("| Operation | Status |");
24
+ lines.push("| --- | --- |");
25
+ lines.push(`| initialize | ${result.runtimeScorecard.initialize.toUpperCase()} |`);
26
+ lines.push(`| tools/list | ${result.runtimeScorecard.toolsList.toUpperCase()} |`);
27
+ lines.push(`| tools/call | ${result.runtimeScorecard.toolsCall.toUpperCase()} |`);
28
+ lines.push(`| resources/list | ${result.runtimeScorecard.resourcesList.toUpperCase()} |`);
29
+ lines.push(`| resources/read | ${result.runtimeScorecard.resourceRead.toUpperCase()} |`);
30
+ lines.push(`| resources/templates/list | ${result.runtimeScorecard.resourceTemplatesList.toUpperCase()} |`);
31
+ lines.push(`| prompts/list | ${result.runtimeScorecard.promptsList.toUpperCase()} |`);
32
+ lines.push(`| prompts/get | ${result.runtimeScorecard.promptGet.toUpperCase()} |`);
33
+ }
34
+ lines.push("", "## Findings", "");
35
+ for (const finding of result.findings) {
36
+ lines.push(`### ${finding.severity.toUpperCase()} \`${finding.id}\``);
37
+ lines.push("");
38
+ lines.push(`- Message: ${finding.message}`);
39
+ lines.push(`- Impact: ${finding.impact}`);
40
+ lines.push(`- Suggested fix: ${finding.suggestedFix}`);
41
+ lines.push("");
42
+ }
43
+ return lines.join("\n");
44
+ }
@@ -0,0 +1,4 @@
1
+ import type { CheckResult } from "../domain/types.js";
2
+ export declare function renderTextReport(result: CheckResult, options?: {
3
+ ascii?: boolean;
4
+ }): string;
@@ -0,0 +1,77 @@
1
+ function getCounts(result) {
2
+ const failCount = result.findings.filter((finding) => finding.severity === "fail").length;
3
+ const warnCount = result.findings.filter((finding) => finding.severity === "warn").length;
4
+ return {
5
+ failCount,
6
+ warnCount,
7
+ totalCount: result.findings.length
8
+ };
9
+ }
10
+ function getGlyphs(ascii) {
11
+ return ascii
12
+ ? {
13
+ fail: "x",
14
+ warn: "!",
15
+ ok: "ok"
16
+ }
17
+ : {
18
+ fail: "✖",
19
+ warn: "!",
20
+ ok: "✔"
21
+ };
22
+ }
23
+ export function renderTextReport(result, options = {}) {
24
+ const ascii = options.ascii ?? false;
25
+ const glyphs = getGlyphs(ascii);
26
+ const { failCount, warnCount, totalCount } = getCounts(result);
27
+ const lines = [
28
+ "Codex Plugin Doctor",
29
+ "===================",
30
+ `Status: ${result.status.toUpperCase()}`,
31
+ `Target: ${result.targetPath}`,
32
+ `Summary: ${failCount} fail, ${warnCount} warn, ${totalCount} total`
33
+ ];
34
+ if (result.findings.length === 0) {
35
+ if (result.runtimeScorecard) {
36
+ lines.push("", "Runtime Scorecard", "----------------");
37
+ lines.push(`initialize: ${result.runtimeScorecard.initialize}`);
38
+ lines.push(`tools/list: ${result.runtimeScorecard.toolsList}`);
39
+ lines.push(`tools/call: ${result.runtimeScorecard.toolsCall}`);
40
+ lines.push(`resources/list: ${result.runtimeScorecard.resourcesList}`);
41
+ lines.push(`resources/read: ${result.runtimeScorecard.resourceRead}`);
42
+ lines.push(`resources/templates/list: ${result.runtimeScorecard.resourceTemplatesList}`);
43
+ lines.push(`prompts/list: ${result.runtimeScorecard.promptsList}`);
44
+ lines.push(`prompts/get: ${result.runtimeScorecard.promptGet}`);
45
+ }
46
+ lines.push("", "No findings.");
47
+ return lines.join("\n");
48
+ }
49
+ const failures = result.findings.filter((finding) => finding.severity === "fail");
50
+ const warnings = result.findings.filter((finding) => finding.severity === "warn");
51
+ const appendSection = (title, items, marker) => {
52
+ if (items.length === 0) {
53
+ return;
54
+ }
55
+ lines.push("", title, "--------");
56
+ for (const finding of items) {
57
+ lines.push(`${marker} ${finding.id}`);
58
+ lines.push(` Message: ${finding.message}`);
59
+ lines.push(` Impact: ${finding.impact}`);
60
+ lines.push(` Suggested fix: ${finding.suggestedFix}`);
61
+ }
62
+ };
63
+ appendSection("Failures", failures, glyphs.fail);
64
+ appendSection("Warnings", warnings, glyphs.warn);
65
+ if (result.runtimeScorecard) {
66
+ lines.push("", "Runtime Scorecard", "----------------");
67
+ lines.push(`initialize: ${result.runtimeScorecard.initialize}`);
68
+ lines.push(`tools/list: ${result.runtimeScorecard.toolsList}`);
69
+ lines.push(`tools/call: ${result.runtimeScorecard.toolsCall}`);
70
+ lines.push(`resources/list: ${result.runtimeScorecard.resourcesList}`);
71
+ lines.push(`resources/read: ${result.runtimeScorecard.resourceRead}`);
72
+ lines.push(`resources/templates/list: ${result.runtimeScorecard.resourceTemplatesList}`);
73
+ lines.push(`prompts/list: ${result.runtimeScorecard.promptsList}`);
74
+ lines.push(`prompts/get: ${result.runtimeScorecard.promptGet}`);
75
+ }
76
+ return lines.join("\n");
77
+ }
@@ -0,0 +1,15 @@
1
+ import { runCheck } from "./index.js";
2
+ export interface CliIo {
3
+ writeStdout(message: string): void;
4
+ writeStderr(message: string): void;
5
+ }
6
+ export interface CliTerminalContext {
7
+ stdoutIsTTY: boolean;
8
+ stderrIsTTY: boolean;
9
+ env: Record<string, string | undefined>;
10
+ }
11
+ export interface RunCliOptions {
12
+ terminalContext?: CliTerminalContext;
13
+ runCheckImpl?: typeof runCheck;
14
+ }
15
+ export declare function runCli(args: string[], io?: CliIo, options?: RunCliOptions): Promise<number>;
@@ -0,0 +1,87 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { runCheck } from "./index.js";
3
+ import { renderJsonReport } from "./reporting/render-json-report.js";
4
+ import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
5
+ import { renderTextReport } from "./reporting/render-text-report.js";
6
+ import { createLiveStatusRenderer } from "./terminal/live-status-renderer.js";
7
+ import { determineOutputPolicy } from "./terminal/output-policy.js";
8
+ import { getSpinner } from "./terminal/spinner-registry.js";
9
+ const defaultIo = {
10
+ writeStdout(message) {
11
+ process.stdout.write(`${message}\n`);
12
+ },
13
+ writeStderr(message) {
14
+ process.stderr.write(`${message}\n`);
15
+ }
16
+ };
17
+ function printUsage(io) {
18
+ io.writeStderr("Usage: codex-plugin-doctor check <path> [--json|--markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]");
19
+ }
20
+ export async function runCli(args, io = defaultIo, options = {}) {
21
+ const [command, maybePath, ...remainingArgs] = args;
22
+ if (command !== "check") {
23
+ printUsage(io);
24
+ return 2;
25
+ }
26
+ const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
27
+ const normalizedFlags = maybePath && maybePath.startsWith("--")
28
+ ? [maybePath, ...remainingArgs]
29
+ : remainingArgs;
30
+ const jsonOutput = normalizedFlags.includes("--json");
31
+ const markdownOutput = normalizedFlags.includes("--markdown");
32
+ const runtimeProbeEnabled = normalizedFlags.includes("--runtime");
33
+ const verboseRuntime = normalizedFlags.includes("--verbose-runtime");
34
+ const noAnimations = normalizedFlags.includes("--no-animations");
35
+ const asciiMode = normalizedFlags.includes("--ascii");
36
+ const outputIndex = normalizedFlags.indexOf("--output");
37
+ const outputPath = outputIndex === -1 ? null : normalizedFlags[outputIndex + 1];
38
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
39
+ io.writeStderr("Missing path after --output.");
40
+ return 2;
41
+ }
42
+ const terminalContext = options.terminalContext ?? {
43
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
44
+ stderrIsTTY: Boolean(process.stderr.isTTY),
45
+ env: process.env
46
+ };
47
+ const outputPolicy = determineOutputPolicy({
48
+ jsonOutput,
49
+ markdownOutput,
50
+ outputPath,
51
+ noAnimations,
52
+ asciiMode,
53
+ stdoutIsTTY: terminalContext.stdoutIsTTY,
54
+ stderrIsTTY: terminalContext.stderrIsTTY,
55
+ env: terminalContext.env
56
+ });
57
+ const runCheckImpl = options.runCheckImpl ?? runCheck;
58
+ const renderer = outputPolicy.interactive
59
+ && !verboseRuntime
60
+ ? createLiveStatusRenderer(io, getSpinner(outputPolicy.style === "ascii" ? "ascii" : "doctor"))
61
+ : null;
62
+ renderer?.start("Validating package");
63
+ const result = await runCheckImpl(targetPath, {
64
+ runtime: runtimeProbeEnabled,
65
+ runtimeTranscript: runtimeProbeEnabled && verboseRuntime
66
+ ? (line) => io.writeStderr(line)
67
+ : undefined
68
+ });
69
+ if (renderer) {
70
+ if (result.status === "fail") {
71
+ renderer.stopFailure("Validation failed");
72
+ }
73
+ else {
74
+ renderer.stopSuccess("Validation complete");
75
+ }
76
+ }
77
+ const report = markdownOutput
78
+ ? buildMarkdownReport(result, { runtimeProbeEnabled })
79
+ : jsonOutput
80
+ ? renderJsonReport(result, { runtimeProbeEnabled })
81
+ : renderTextReport(result, { ascii: outputPolicy.style === "ascii" });
82
+ if (outputPath) {
83
+ await writeFile(outputPath, report, "utf8");
84
+ }
85
+ io.writeStdout(report);
86
+ return result.exitCode;
87
+ }
@@ -0,0 +1,9 @@
1
+ import type { CliIo } from "../run-cli.js";
2
+ import type { SpinnerDefinition } from "./spinner-registry.js";
3
+ export interface LiveStatusRenderer {
4
+ start(label: string): void;
5
+ update(label: string): void;
6
+ stopSuccess(label: string): void;
7
+ stopFailure(label: string): void;
8
+ }
9
+ export declare function createLiveStatusRenderer(io: Pick<CliIo, "writeStderr">, spinner: SpinnerDefinition): LiveStatusRenderer;
@@ -0,0 +1,38 @@
1
+ function clearLine() {
2
+ return "\r\x1B[2K";
3
+ }
4
+ export function createLiveStatusRenderer(io, spinner) {
5
+ let frameIndex = 0;
6
+ let currentLabel = "";
7
+ let timer = null;
8
+ const renderFrame = () => {
9
+ const frame = spinner.frames[frameIndex % spinner.frames.length];
10
+ frameIndex += 1;
11
+ io.writeStderr(`${clearLine()}${frame} ${currentLabel}`);
12
+ };
13
+ const stopTimer = () => {
14
+ if (timer) {
15
+ clearInterval(timer);
16
+ timer = null;
17
+ }
18
+ };
19
+ return {
20
+ start(label) {
21
+ currentLabel = label;
22
+ renderFrame();
23
+ timer = setInterval(renderFrame, spinner.intervalMs);
24
+ },
25
+ update(label) {
26
+ currentLabel = label;
27
+ renderFrame();
28
+ },
29
+ stopSuccess(label) {
30
+ stopTimer();
31
+ io.writeStderr(`${clearLine()}✔ ${label}\n`);
32
+ },
33
+ stopFailure(label) {
34
+ stopTimer();
35
+ io.writeStderr(`${clearLine()}✖ ${label}\n`);
36
+ }
37
+ };
38
+ }
@@ -0,0 +1,16 @@
1
+ export interface OutputPolicyInput {
2
+ jsonOutput: boolean;
3
+ markdownOutput: boolean;
4
+ outputPath: string | null;
5
+ noAnimations: boolean;
6
+ asciiMode: boolean;
7
+ stdoutIsTTY: boolean;
8
+ stderrIsTTY: boolean;
9
+ env: Record<string, string | undefined>;
10
+ }
11
+ export interface OutputPolicy {
12
+ interactive: boolean;
13
+ style: "unicode" | "ascii";
14
+ reason: "tty" | "machine_output" | "redirected_output" | "ci" | "dumb_terminal" | "non_tty" | "disabled_by_flag";
15
+ }
16
+ export declare function determineOutputPolicy(input: OutputPolicyInput): OutputPolicy;
@@ -0,0 +1,50 @@
1
+ export function determineOutputPolicy(input) {
2
+ const style = input.asciiMode ? "ascii" : "unicode";
3
+ if (input.jsonOutput || input.markdownOutput) {
4
+ return {
5
+ interactive: false,
6
+ style,
7
+ reason: "machine_output"
8
+ };
9
+ }
10
+ if (input.outputPath) {
11
+ return {
12
+ interactive: false,
13
+ style,
14
+ reason: "redirected_output"
15
+ };
16
+ }
17
+ if (input.env.CI) {
18
+ return {
19
+ interactive: false,
20
+ style,
21
+ reason: "ci"
22
+ };
23
+ }
24
+ if (input.env.TERM === "dumb") {
25
+ return {
26
+ interactive: false,
27
+ style: "ascii",
28
+ reason: "dumb_terminal"
29
+ };
30
+ }
31
+ if (!input.stdoutIsTTY || !input.stderrIsTTY) {
32
+ return {
33
+ interactive: false,
34
+ style,
35
+ reason: "non_tty"
36
+ };
37
+ }
38
+ if (input.noAnimations) {
39
+ return {
40
+ interactive: false,
41
+ style,
42
+ reason: "disabled_by_flag"
43
+ };
44
+ }
45
+ return {
46
+ interactive: true,
47
+ style,
48
+ reason: "tty"
49
+ };
50
+ }
@@ -0,0 +1,8 @@
1
+ export interface SpinnerDefinition {
2
+ name: string;
3
+ frames: string[];
4
+ intervalMs: number;
5
+ }
6
+ declare const spinners: Record<string, SpinnerDefinition>;
7
+ export declare function getSpinner(name: keyof typeof spinners): SpinnerDefinition;
8
+ export {};
@@ -0,0 +1,30 @@
1
+ const spinners = {
2
+ braille: {
3
+ name: "braille",
4
+ frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
5
+ intervalMs: 80
6
+ },
7
+ doctor: {
8
+ name: "doctor",
9
+ frames: ["⠑", "⠒", "⠲", "⠴", "⠦", "⠖", "⠒", "⠐"],
10
+ intervalMs: 75
11
+ },
12
+ dots: {
13
+ name: "dots",
14
+ frames: ["⠁", "⠂", "⠄", "⠂"],
15
+ intervalMs: 90
16
+ },
17
+ scan: {
18
+ name: "scan",
19
+ frames: ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▂"],
20
+ intervalMs: 70
21
+ },
22
+ ascii: {
23
+ name: "ascii",
24
+ frames: ["-", "\\", "|", "/"],
25
+ intervalMs: 90
26
+ }
27
+ };
28
+ export function getSpinner(name) {
29
+ return spinners[name];
30
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "codex-plugin-doctor",
3
+ "version": "0.1.1",
4
+ "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "codex-plugin-doctor": "dist/cli.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
23
+ "build": "npm run clean && tsc -p tsconfig.json",
24
+ "dev": "tsx src/cli.ts",
25
+ "generate-validation-artifacts": "node scripts/generate-validation-artifacts.mjs",
26
+ "prepare-rc": "tsx scripts/prepare-release-candidate.ts",
27
+ "prepare-release": "npm test && npm run build && npm pack --dry-run",
28
+ "prepublishOnly": "npm test && npm run build",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest"
31
+ },
32
+ "packageManager": "npm@11.11.1",
33
+ "engines": {
34
+ "node": ">=22"
35
+ },
36
+ "keywords": [
37
+ "codex",
38
+ "mcp",
39
+ "plugin",
40
+ "skills",
41
+ "validation"
42
+ ],
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/Esquetta/CodexPluginDoctor.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/Esquetta/CodexPluginDoctor/issues"
50
+ },
51
+ "homepage": "https://github.com/Esquetta/CodexPluginDoctor#readme",
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^24.2.1",
57
+ "tsx": "^4.20.4",
58
+ "typescript": "^5.9.2",
59
+ "vitest": "^3.2.4"
60
+ }
61
+ }