codex-plugin-doctor 1.43.0 → 1.45.0

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 CHANGED
@@ -287,8 +287,10 @@ codex-plugin-doctor check . --ascii
287
287
  codex-plugin-doctor check . --no-animations
288
288
  codex-plugin-doctor check . --runtime
289
289
  codex-plugin-doctor check . --runtime --require-runtime-approval --runtime-approval-digest sha256:<approved-plan-digest>
290
- codex-plugin-doctor check . --config .codex-doctor.json
291
- codex-plugin-doctor check . --history validation-history.jsonl
290
+ codex-plugin-doctor check . --config .codex-doctor.json
291
+ codex-plugin-doctor baseline create . --output .codex-doctor-baseline.json
292
+ codex-plugin-doctor check . --baseline .codex-doctor-baseline.json
293
+ codex-plugin-doctor check . --history validation-history.jsonl
292
294
  codex-plugin-doctor history validation-history.jsonl
293
295
  codex-plugin-doctor history validation-history.jsonl --json
294
296
  codex-plugin-doctor history validation-history.jsonl --fail-on-regression
@@ -334,9 +336,11 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
334
336
 
335
337
  `check --badge-json` emits Shields endpoint-compatible JSON such as `{"schemaVersion":1,"label":"doctor","message":"PASS","color":"brightgreen"}`. `check --badge-markdown` emits a static shields.io Markdown badge for README or release notes. Badge output is intentionally limited to single package checks, not `check --installed`.
336
338
 
337
- `check --history <path>` appends a compact JSONL validation snapshot after a single package check. `history <path>` reads the JSONL file and compares the latest run to the previous run, including status, finding-count deltas, and whether the latest run regressed. Add `history --json` for automation output or `history --fail-on-regression` when CI should fail after a worse latest run.
338
-
339
- `fix --dry-run` renders safe automatic fix plans without changing files. `fix --interactive --backup` shows the same numbered plan, then applies everything after `yes` or only selected action numbers such as `1,3`. `fix --apply --backup` applies supported safe fixes, such as manifest defaults and missing skills directories, after creating backups.
339
+ `check --history <path>` appends a compact JSONL validation snapshot after a single package check. `history <path>` reads the JSONL file and compares the latest run to the previous run, including status, finding-count deltas, and whether the latest run regressed. Add `history --json` for automation output or `history --fail-on-regression` when CI should fail after a worse latest run.
340
+
341
+ `baseline create <path> --output <path>` records the current fingerprinted findings for gradual CI adoption. A later `check <path> --baseline <path>` keeps matched findings visible in baseline metadata but gates only new active findings; reports summarize new, matched, and resolved counts. Commit the baseline for review and regenerate it deliberately after accepted findings change.
342
+
343
+ `fix --dry-run` renders safe automatic fix plans without changing files. `fix --interactive --backup` shows the same numbered plan, then applies everything after `yes` or only selected action numbers such as `1,3`. `fix --apply --backup` applies supported safe fixes, such as manifest defaults and missing skills directories, after creating backups.
340
344
 
341
345
  Optional local policy file:
342
346
 
@@ -402,9 +406,9 @@ jobs:
402
406
  runs-on: ubuntu-latest
403
407
  steps:
404
408
  - uses: actions/checkout@v5
405
- - uses: Esquetta/CodexPluginDoctor@v1.43.0
409
+ - uses: Esquetta/CodexPluginDoctor@v1.45.0
406
410
  with:
407
- version: "1.43.0"
411
+ version: "1.45.0"
408
412
  path: .
409
413
  runtime: "true"
410
414
  policy: codex-publish
@@ -0,0 +1,13 @@
1
+ export interface ConfigValidationFinding {
2
+ field: string;
3
+ message: string;
4
+ severity: "fail" | "warn";
5
+ }
6
+ export interface ConfigValidationReport {
7
+ configPath: string;
8
+ status: "pass" | "fail";
9
+ findings: ConfigValidationFinding[];
10
+ }
11
+ export declare function validateConfigFile(configPath: string): Promise<ConfigValidationReport>;
12
+ export declare function renderConfigValidation(report: ConfigValidationReport): string;
13
+ export declare function renderConfigValidationJson(report: ConfigValidationReport): string;
@@ -0,0 +1,133 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const knownFields = ["ignoreRules", "failOnWarnings", "suppressions"];
4
+ export async function validateConfigFile(configPath) {
5
+ const resolvedPath = path.resolve(configPath);
6
+ const findings = [];
7
+ try {
8
+ await stat(resolvedPath);
9
+ }
10
+ catch {
11
+ return {
12
+ configPath: resolvedPath,
13
+ status: "fail",
14
+ findings: [
15
+ { field: "file", message: `Config file not found: ${resolvedPath}`, severity: "fail" }
16
+ ]
17
+ };
18
+ }
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(await readFile(resolvedPath, "utf8"));
22
+ }
23
+ catch (error) {
24
+ return {
25
+ configPath: resolvedPath,
26
+ status: "fail",
27
+ findings: [
28
+ { field: "json", message: `Invalid JSON: ${error.message}`, severity: "fail" }
29
+ ]
30
+ };
31
+ }
32
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
33
+ return {
34
+ configPath: resolvedPath,
35
+ status: "fail",
36
+ findings: [
37
+ { field: "root", message: "Config must be a JSON object.", severity: "fail" }
38
+ ]
39
+ };
40
+ }
41
+ const config = parsed;
42
+ for (const key of Object.keys(config)) {
43
+ if (!knownFields.includes(key)) {
44
+ findings.push({
45
+ field: key,
46
+ message: `Unknown field: "${key}". Known fields: ${knownFields.join(", ")}`,
47
+ severity: "warn"
48
+ });
49
+ }
50
+ }
51
+ if ("ignoreRules" in config) {
52
+ const ignoreRules = config.ignoreRules;
53
+ if (!Array.isArray(ignoreRules)) {
54
+ findings.push({ field: "ignoreRules", message: "Must be an array of strings.", severity: "fail" });
55
+ }
56
+ else if (!ignoreRules.every((r) => typeof r === "string")) {
57
+ findings.push({ field: "ignoreRules", message: "All entries must be strings.", severity: "fail" });
58
+ }
59
+ }
60
+ if ("failOnWarnings" in config) {
61
+ if (typeof config.failOnWarnings !== "boolean") {
62
+ findings.push({ field: "failOnWarnings", message: "Must be a boolean.", severity: "fail" });
63
+ }
64
+ }
65
+ if ("suppressions" in config) {
66
+ const suppressions = config.suppressions;
67
+ if (!Array.isArray(suppressions)) {
68
+ findings.push({ field: "suppressions", message: "Must be an array.", severity: "fail" });
69
+ }
70
+ else {
71
+ for (let i = 0; i < suppressions.length; i += 1) {
72
+ const entry = suppressions[i];
73
+ if (!entry || typeof entry !== "object") {
74
+ findings.push({ field: `suppressions[${i}]`, message: "Must be an object.", severity: "fail" });
75
+ continue;
76
+ }
77
+ if (typeof entry.fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(entry.fingerprint)) {
78
+ findings.push({
79
+ field: `suppressions[${i}].fingerprint`,
80
+ message: "Must be a 64-character lowercase hex string.",
81
+ severity: "fail"
82
+ });
83
+ }
84
+ if (typeof entry.reason !== "string" || entry.reason.trim().length === 0) {
85
+ findings.push({
86
+ field: `suppressions[${i}].reason`,
87
+ message: "Must be a non-empty string.",
88
+ severity: "fail"
89
+ });
90
+ }
91
+ if (typeof entry.expiresAt !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(entry.expiresAt)) {
92
+ findings.push({
93
+ field: `suppressions[${i}].expiresAt`,
94
+ message: "Must be a YYYY-MM-DD date string.",
95
+ severity: "fail"
96
+ });
97
+ }
98
+ }
99
+ }
100
+ }
101
+ const hasFail = findings.some((f) => f.severity === "fail");
102
+ return {
103
+ configPath: resolvedPath,
104
+ status: hasFail ? "fail" : findings.length === 0 ? "pass" : "warn",
105
+ findings
106
+ };
107
+ }
108
+ export function renderConfigValidation(report) {
109
+ const lines = [
110
+ "Codex Plugin Doctor Config Validation",
111
+ "=====================================",
112
+ `Path: ${report.configPath}`,
113
+ `Status: ${report.status.toUpperCase()}`,
114
+ ""
115
+ ];
116
+ if (report.findings.length === 0) {
117
+ lines.push("Config is valid.");
118
+ return lines.join("\n");
119
+ }
120
+ for (const finding of report.findings) {
121
+ const tag = finding.severity === "fail" ? "FAIL" : "WARN";
122
+ lines.push(`${tag} ${finding.field}: ${finding.message}`);
123
+ }
124
+ return lines.join("\n");
125
+ }
126
+ export function renderConfigValidationJson(report) {
127
+ return JSON.stringify({
128
+ schemaVersion: "1.0.0",
129
+ configPath: report.configPath,
130
+ status: report.status,
131
+ findings: report.findings
132
+ }, null, 2);
133
+ }
@@ -0,0 +1,2 @@
1
+ export type CompletionShell = "bash" | "zsh" | "fish";
2
+ export declare function generateCompletion(shell: CompletionShell): string;
@@ -0,0 +1,97 @@
1
+ const topLevelCommands = [
2
+ "check",
3
+ "audit",
4
+ "mcp",
5
+ "security",
6
+ "compat",
7
+ "suppress",
8
+ "fix",
9
+ "history",
10
+ "watch",
11
+ "doctor",
12
+ "init",
13
+ "init-ci",
14
+ "init-git-hooks",
15
+ "self-test",
16
+ "list",
17
+ "explain",
18
+ "config"
19
+ ];
20
+ function bashCompletion() {
21
+ return [
22
+ "_codex_plugin_doctor() {",
23
+ " local cur prev",
24
+ " COMPREPLY=()",
25
+ " cur=\"${COMP_WORDS[COMP_CWORD]}\"",
26
+ " prev=\"${COMP_WORDS[COMP_CWORD-1]}\"",
27
+ "",
28
+ ` local commands="${topLevelCommands.join(" ")}"`,
29
+ "",
30
+ " case \"${prev}\" in",
31
+ " codex-plugin-doctor)",
32
+ " COMPREPLY=( $(compgen -W \"${commands}\" -- \"${cur}\") )",
33
+ " return 0",
34
+ " ;;",
35
+ " esac",
36
+ "",
37
+ " case \"${cur}\" in",
38
+ " --*)",
39
+ " local flags=\"--json --output --runtime --policy --help\"",
40
+ " COMPREPLY=( $(compgen -W \"${flags}\" -- \"${cur}\") )",
41
+ " return 0",
42
+ " ;;",
43
+ " *)",
44
+ " COMPREPLY=( $(compgen -W \"${commands}\" -- \"${cur}\") )",
45
+ " return 0",
46
+ " ;;",
47
+ " esac",
48
+ "}",
49
+ "",
50
+ "complete -F _codex_plugin_doctor codex-plugin-doctor",
51
+ ""
52
+ ].join("\n");
53
+ }
54
+ function zshCompletion() {
55
+ return [
56
+ "#compdef codex-plugin-doctor",
57
+ "",
58
+ "_codex_plugin_doctor() {",
59
+ " local context state line",
60
+ " typeset -A opt_args",
61
+ "",
62
+ ` local commands=(${topLevelCommands.join(" ")})`,
63
+ "",
64
+ " _arguments -C \\",
65
+ " '1:command:(${commands})' \\",
66
+ " '*--json[Output as JSON]' \\",
67
+ " '*--output[Write to file]:file:_files' \\",
68
+ " '*--runtime[Enable runtime probes]' \\",
69
+ " '*--policy[Apply policy]:policy:(codex-publish mcp-strict security)'",
70
+ "}",
71
+ "",
72
+ "_codex_plugin_doctor \"$@\"",
73
+ ""
74
+ ].join("\n");
75
+ }
76
+ function fishCompletion() {
77
+ return [
78
+ "complete -c codex-plugin-doctor -f",
79
+ `complete -c codex-plugin-doctor -n "not __fish_seen_subcommand_from ${topLevelCommands.join(" ")}" -a "${topLevelCommands.join(" ")}"`,
80
+ "complete -c codex-plugin-doctor -s h -l help -d 'Show help'",
81
+ "complete -c codex-plugin-doctor -l json -d 'Output as JSON'",
82
+ "complete -c codex-plugin-doctor -l output -d 'Write to file' -r",
83
+ "complete -c codex-plugin-doctor -l runtime -d 'Enable runtime probes'",
84
+ "complete -c codex-plugin-doctor -l policy -d 'Apply policy' -x -a 'codex-publish mcp-strict security'",
85
+ ""
86
+ ].join("\n");
87
+ }
88
+ export function generateCompletion(shell) {
89
+ switch (shell) {
90
+ case "bash":
91
+ return bashCompletion();
92
+ case "zsh":
93
+ return zshCompletion();
94
+ case "fish":
95
+ return fishCompletion();
96
+ }
97
+ }
@@ -0,0 +1,17 @@
1
+ import type { CheckResult, FindingSeverity } from "../domain/types.js";
2
+ export interface ValidationBaselineFinding {
3
+ id: string;
4
+ severity: FindingSeverity;
5
+ fingerprint: string;
6
+ }
7
+ export interface ValidationBaseline {
8
+ schemaVersion: "1.0.0";
9
+ generatedAt: string;
10
+ findings: ValidationBaselineFinding[];
11
+ }
12
+ export declare function buildValidationBaseline(result: CheckResult): ValidationBaseline;
13
+ export declare function writeValidationBaseline(baselinePath: string, baseline: ValidationBaseline): Promise<void>;
14
+ export declare function readValidationBaseline(baselinePath: string): Promise<ValidationBaseline>;
15
+ export declare function applyValidationBaseline(result: CheckResult, baseline: ValidationBaseline, options: {
16
+ failOnWarnings: boolean;
17
+ }): CheckResult;
@@ -0,0 +1,83 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const fingerprintPattern = /^[a-f0-9]{64}$/;
4
+ export function buildValidationBaseline(result) {
5
+ const findings = new Map();
6
+ for (const finding of result.findings) {
7
+ if (finding.fingerprint && !findings.has(finding.fingerprint)) {
8
+ findings.set(finding.fingerprint, {
9
+ id: finding.id,
10
+ severity: finding.severity,
11
+ fingerprint: finding.fingerprint
12
+ });
13
+ }
14
+ }
15
+ return {
16
+ schemaVersion: "1.0.0",
17
+ generatedAt: new Date().toISOString(),
18
+ findings: [...findings.values()]
19
+ };
20
+ }
21
+ export async function writeValidationBaseline(baselinePath, baseline) {
22
+ const absolutePath = path.resolve(baselinePath);
23
+ await mkdir(path.dirname(absolutePath), { recursive: true });
24
+ await writeFile(absolutePath, JSON.stringify(baseline, null, 2), "utf8");
25
+ }
26
+ export async function readValidationBaseline(baselinePath) {
27
+ const value = JSON.parse(await readFile(path.resolve(baselinePath), "utf8"));
28
+ if (!isValidationBaseline(value)) {
29
+ throw new Error("expected schemaVersion 1.0.0 and valid fingerprinted findings");
30
+ }
31
+ return value;
32
+ }
33
+ export function applyValidationBaseline(result, baseline, options) {
34
+ const baselineFingerprints = new Set(baseline.findings.map((finding) => finding.fingerprint));
35
+ const currentFingerprints = new Set(result.findings.flatMap((finding) => finding.fingerprint ? [finding.fingerprint] : []));
36
+ const baselinedFindings = [];
37
+ const findings = [];
38
+ for (const finding of result.findings) {
39
+ if (finding.fingerprint && baselineFingerprints.has(finding.fingerprint)) {
40
+ baselinedFindings.push(finding);
41
+ }
42
+ else {
43
+ findings.push(finding);
44
+ }
45
+ }
46
+ const hasFailures = findings.some((finding) => finding.severity === "fail");
47
+ const hasWarnings = findings.some((finding) => finding.severity === "warn");
48
+ const status = hasFailures || (options.failOnWarnings && hasWarnings)
49
+ ? "fail"
50
+ : hasWarnings
51
+ ? "warn"
52
+ : "pass";
53
+ return {
54
+ ...result,
55
+ status,
56
+ exitCode: status === "fail" ? 1 : 0,
57
+ findings,
58
+ baselinedFindings,
59
+ baselineSummary: {
60
+ matched: baselinedFindings.length,
61
+ new: findings.length,
62
+ resolved: baseline.findings.filter((finding) => !currentFingerprints.has(finding.fingerprint)).length
63
+ }
64
+ };
65
+ }
66
+ function isValidationBaseline(value) {
67
+ if (!value || typeof value !== "object") {
68
+ return false;
69
+ }
70
+ const candidate = value;
71
+ return candidate.schemaVersion === "1.0.0"
72
+ && typeof candidate.generatedAt === "string"
73
+ && !Number.isNaN(Date.parse(candidate.generatedAt))
74
+ && Array.isArray(candidate.findings)
75
+ && candidate.findings.every((finding) => finding
76
+ && typeof finding.id === "string"
77
+ && finding.id.length > 0
78
+ && (finding.severity === "warn" || finding.severity === "fail")
79
+ && typeof finding.fingerprint === "string"
80
+ && fingerprintPattern.test(finding.fingerprint))
81
+ && new Set(candidate.findings.map((finding) => finding.fingerprint)).size
82
+ === candidate.findings.length;
83
+ }
@@ -30,6 +30,13 @@ export interface CheckResult {
30
30
  suppressedFindings?: SuppressedFinding[];
31
31
  suppressionSummary?: SuppressionSummary;
32
32
  runtimeScorecard?: RuntimeScorecard;
33
+ baselinedFindings?: Finding[];
34
+ baselineSummary?: BaselineSummary;
35
+ }
36
+ export interface BaselineSummary {
37
+ matched: number;
38
+ new: number;
39
+ resolved: number;
33
40
  }
34
41
  export interface CheckOptions {
35
42
  runtime?: boolean;
@@ -59,6 +66,7 @@ export interface JsonReportSummary {
59
66
  warn: number;
60
67
  total: number;
61
68
  };
69
+ baseline?: BaselineSummary;
62
70
  }
63
71
  export interface JsonReport {
64
72
  schemaVersion: "1.0.0";
@@ -67,6 +75,7 @@ export interface JsonReport {
67
75
  findings: Finding[];
68
76
  suppressedFindings?: SuppressedFinding[];
69
77
  suppressionSummary?: SuppressionSummary;
78
+ baselinedFindings?: Finding[];
70
79
  }
71
80
  export type RuntimeCapabilityStatus = "pass" | "fail" | "warn" | "skipped" | "unsupported";
72
81
  export interface RuntimeScorecard {
package/dist/index.d.ts CHANGED
@@ -27,4 +27,6 @@ export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJs
27
27
  export { watchPlugin, type WatchPluginOptions, type WatchPluginResult } from "./core/watch-plugin.js";
28
28
  export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepAuditJson, renderDepAuditSarif, type DepAuditRecommendation, type DepAuditReport, type DepAuditVulnerability } from "./core/dep-audit.js";
29
29
  export { initGitHooks, removeGitHooks, type InitGitHooksResult, type RemoveGitHooksResult } from "./core/init-git-hooks.js";
30
+ export { generateCompletion, type CompletionShell } from "./core/shell-completion.js";
31
+ export { validateConfigFile, renderConfigValidation, renderConfigValidationJson, type ConfigValidationFinding, type ConfigValidationReport } from "./core/config-validate.js";
30
32
  export declare function runCheck(targetPath: string, options?: CheckOptions): Promise<CheckResult>;
package/dist/index.js CHANGED
@@ -27,6 +27,8 @@ export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJs
27
27
  export { watchPlugin } from "./core/watch-plugin.js";
28
28
  export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepAuditJson, renderDepAuditSarif } from "./core/dep-audit.js";
29
29
  export { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";
30
+ export { generateCompletion } from "./core/shell-completion.js";
31
+ export { validateConfigFile, renderConfigValidation, renderConfigValidationJson } from "./core/config-validate.js";
30
32
  export async function runCheck(targetPath, options = {}) {
31
33
  return validatePlugin(targetPath, options);
32
34
  }
@@ -16,7 +16,8 @@ export function buildJsonReport(result, options) {
16
16
  fail: failCount,
17
17
  warn: warnCount,
18
18
  total: result.findings.length
19
- }
19
+ },
20
+ ...(result.baselineSummary ? { baseline: result.baselineSummary } : {})
20
21
  },
21
22
  findings: result.findings,
22
23
  ...(result.suppressedFindings
@@ -24,6 +25,9 @@ export function buildJsonReport(result, options) {
24
25
  : {}),
25
26
  ...(result.suppressionSummary
26
27
  ? { suppressionSummary: result.suppressionSummary }
28
+ : {}),
29
+ ...(result.baselinedFindings
30
+ ? { baselinedFindings: result.baselinedFindings }
27
31
  : {})
28
32
  };
29
33
  }
@@ -20,8 +20,11 @@ export function buildMarkdownReport(result, options) {
20
20
  if (result.suppressionSummary) {
21
21
  lines.push(`| Suppressions Applied | ${result.suppressionSummary.applied} |`, `| Suppressions Expired | ${result.suppressionSummary.expired} |`, `| Suppressions Invalid | ${result.suppressionSummary.invalid} |`);
22
22
  }
23
+ if (result.baselineSummary) {
24
+ lines.push(`| Baseline New | ${result.baselineSummary.new} |`, `| Baseline Matched | ${result.baselineSummary.matched} |`, `| Baseline Resolved | ${result.baselineSummary.resolved} |`);
25
+ }
23
26
  if (result.findings.length === 0 && !result.suppressedFindings?.length) {
24
- lines.push("", "No findings.");
27
+ lines.push("", result.baselineSummary ? "No new findings." : "No findings.");
25
28
  return lines.join("\n");
26
29
  }
27
30
  if (result.runtimeScorecard) {
@@ -39,6 +39,9 @@ export function renderTextReport(result, options = {}) {
39
39
  if (result.suppressionSummary) {
40
40
  lines.push(`Suppressions: ${result.suppressionSummary.applied} applied, ${result.suppressionSummary.expired} expired, ${result.suppressionSummary.invalid} invalid`);
41
41
  }
42
+ if (result.baselineSummary) {
43
+ lines.push(`Baseline: ${result.baselineSummary.new} new, ${result.baselineSummary.matched} matched, ${result.baselineSummary.resolved} resolved`);
44
+ }
42
45
  if (result.findings.length === 0 && !result.suppressedFindings?.length) {
43
46
  if (result.runtimeScorecard) {
44
47
  lines.push("", "Runtime Scorecard", "----------------");
@@ -51,7 +54,7 @@ export function renderTextReport(result, options = {}) {
51
54
  lines.push(`prompts/list: ${result.runtimeScorecard.promptsList}`);
52
55
  lines.push(`prompts/get: ${result.runtimeScorecard.promptGet}`);
53
56
  }
54
- lines.push("", "No findings.");
57
+ lines.push("", result.baselineSummary ? "No new findings." : "No findings.");
55
58
  return lines.join("\n");
56
59
  }
57
60
  const failures = result.findings.filter((finding) => finding.severity === "fail");
package/dist/run-cli.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
7
7
  import { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } from "./audit/ecosystem-audit.js";
8
8
  import { appendValidationHistoryEntry, readValidationHistory, summarizeValidationHistory } from "./core/validation-history.js";
9
+ import { applyValidationBaseline, buildValidationBaseline, readValidationBaseline, writeValidationBaseline } from "./core/validation-baseline.js";
9
10
  import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compatibility-matrix.js";
10
11
  import { applyInstallPreview, renderApplyInstallResult } from "./compatibility/apply-install-preview.js";
11
12
  import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
@@ -33,6 +34,8 @@ import { initCiWorkflow } from "./core/init-ci.js";
33
34
  import { watchPlugin } from "./core/watch-plugin.js";
34
35
  import { buildDepAudit, renderDepAudit, renderDepAuditJson, renderDepAuditSarif } from "./core/dep-audit.js";
35
36
  import { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";
37
+ import { generateCompletion } from "./core/shell-completion.js";
38
+ import { renderConfigValidation, renderConfigValidationJson, validateConfigFile } from "./core/config-validate.js";
36
39
  import { initPluginPackage, initPluginTemplates, isInitPluginTemplate } from "./core/init-plugin.js";
37
40
  import { runCheck } from "./index.js";
38
41
  import { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
@@ -78,8 +81,9 @@ const defaultIo = {
78
81
  }
79
82
  };
80
83
  function printUsage(io) {
81
- io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
84
+ io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor completion bash|zsh|fish\n codex-plugin-doctor config validate <path> [--json]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
82
85
  io.writeStderr("Suppression governance: codex-plugin-doctor suppress list <path> [--fail-on-expired] [--fail-on-invalid] [--warn-expiring-within-days <days>]\n codex-plugin-doctor suppress prune <path> [--apply] [--json]");
86
+ io.writeStderr("Baseline gating: codex-plugin-doctor baseline create <path> --output <path> [--runtime]\n codex-plugin-doctor check <path> --baseline <path>");
83
87
  }
84
88
  const suppressUsageText = [
85
89
  "Usage:",
@@ -817,6 +821,31 @@ export async function runCli(args, io = defaultIo, options = {}) {
817
821
  io.writeStdout(renderInstalledPlugins(installedPlugins));
818
822
  return 0;
819
823
  }
824
+ if (command === "baseline") {
825
+ if (maybePath !== "create") {
826
+ io.writeStderr("Usage: codex-plugin-doctor baseline create <path> --output <path> [--runtime]");
827
+ return 2;
828
+ }
829
+ const targetPath = remainingArgs[0];
830
+ const baselineFlags = remainingArgs.slice(1);
831
+ const outputIndex = baselineFlags.indexOf("--output");
832
+ const outputPath = outputIndex === -1 ? null : baselineFlags[outputIndex + 1];
833
+ if (!targetPath || targetPath.startsWith("--")) {
834
+ io.writeStderr("Missing target path for baseline create.");
835
+ return 2;
836
+ }
837
+ if (!outputPath || outputPath.startsWith("--")) {
838
+ io.writeStderr("baseline create requires --output <path>.");
839
+ return 2;
840
+ }
841
+ const result = await (options.runCheckImpl ?? runCheck)(targetPath, {
842
+ runtime: baselineFlags.includes("--runtime")
843
+ });
844
+ const baseline = buildValidationBaseline(result);
845
+ await writeValidationBaseline(outputPath, baseline);
846
+ io.writeStdout(`Baseline created: ${path.resolve(outputPath)}\nFindings: ${baseline.findings.length}`);
847
+ return 0;
848
+ }
820
849
  if (command === "doctor") {
821
850
  const doctorFlags = maybePath?.startsWith("--")
822
851
  ? [maybePath, ...remainingArgs]
@@ -2193,6 +2222,33 @@ export async function runCli(args, io = defaultIo, options = {}) {
2193
2222
  io.writeStdout(report);
2194
2223
  return matrixExitCode(matrix);
2195
2224
  }
2225
+ if (command === "completion") {
2226
+ const shell = maybePath;
2227
+ if (!shell || !["bash", "zsh", "fish"].includes(shell)) {
2228
+ io.writeStderr("Usage: codex-plugin-doctor completion bash|zsh|fish");
2229
+ return 2;
2230
+ }
2231
+ io.writeStdout(generateCompletion(shell));
2232
+ return 0;
2233
+ }
2234
+ if (command === "config") {
2235
+ const subCommand = maybePath;
2236
+ if (subCommand !== "validate") {
2237
+ io.writeStderr("Usage: codex-plugin-doctor config validate <path> [--json]");
2238
+ return 2;
2239
+ }
2240
+ const configPath = remainingArgs[0] && !remainingArgs[0].startsWith("--") ? remainingArgs[0] : ".codex-doctor.json";
2241
+ const configFlags = remainingArgs[0] && remainingArgs[0].startsWith("--")
2242
+ ? remainingArgs
2243
+ : remainingArgs.slice(1);
2244
+ const jsonOutput = configFlags.includes("--json");
2245
+ const report = await validateConfigFile(configPath);
2246
+ const output = jsonOutput
2247
+ ? renderConfigValidationJson(report)
2248
+ : renderConfigValidation(report);
2249
+ io.writeStdout(output);
2250
+ return report.status === "fail" ? 1 : 0;
2251
+ }
2196
2252
  if (command === "suppress") {
2197
2253
  const parsedSuppressCommand = parseSuppressCommand([maybePath ?? "", ...remainingArgs]);
2198
2254
  if ("message" in parsedSuppressCommand) {
@@ -2256,6 +2312,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
2256
2312
  : normalizedFlags[runtimeApprovalDigestIndex + 1];
2257
2313
  const changedSinceIndex = normalizedFlags.indexOf("--changed-since");
2258
2314
  const changedSinceRef = changedSinceIndex === -1 ? null : normalizedFlags[changedSinceIndex + 1];
2315
+ const baselineIndex = normalizedFlags.indexOf("--baseline");
2316
+ const baselinePath = baselineIndex === -1 ? null : normalizedFlags[baselineIndex + 1];
2259
2317
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
2260
2318
  io.writeStderr("Missing path after --output.");
2261
2319
  return 2;
@@ -2292,6 +2350,18 @@ export async function runCli(args, io = defaultIo, options = {}) {
2292
2350
  io.writeStderr("--changed-since requires a single package target.");
2293
2351
  return 2;
2294
2352
  }
2353
+ if (baselineIndex !== -1 && (!baselinePath || baselinePath.startsWith("--"))) {
2354
+ io.writeStderr("Missing path after --baseline.");
2355
+ return 2;
2356
+ }
2357
+ if (baselinePath && checkInstalled) {
2358
+ io.writeStderr("Baseline gating requires a single package target.");
2359
+ return 2;
2360
+ }
2361
+ if (baselinePath && changedSinceRef) {
2362
+ io.writeStderr("Use either --baseline or --changed-since, not both.");
2363
+ return 2;
2364
+ }
2295
2365
  if (runtimeApprovalDigestIndex !== -1 &&
2296
2366
  (!runtimeApprovalDigest || runtimeApprovalDigest.startsWith("--"))) {
2297
2367
  io.writeStderr("Missing digest after --runtime-approval-digest.");
@@ -2316,6 +2386,16 @@ export async function runCli(args, io = defaultIo, options = {}) {
2316
2386
  io.writeStderr("Runtime approval gating requires a single package target, not --installed.");
2317
2387
  return 2;
2318
2388
  }
2389
+ let baseline = null;
2390
+ if (baselinePath) {
2391
+ try {
2392
+ baseline = await readValidationBaseline(baselinePath);
2393
+ }
2394
+ catch (error) {
2395
+ io.writeStderr(`Invalid baseline file: ${error.message}`);
2396
+ return 2;
2397
+ }
2398
+ }
2319
2399
  const outputPolicy = determineOutputPolicy({
2320
2400
  jsonOutput: jsonOutput || badgeJsonOutput,
2321
2401
  markdownOutput: markdownOutput || badgeMarkdownOutput,
@@ -2447,12 +2527,18 @@ export async function runCli(args, io = defaultIo, options = {}) {
2447
2527
  return 2;
2448
2528
  }
2449
2529
  }
2450
- const result = applyDoctorConfig(await runCheckImpl(targetPath, {
2530
+ const doctorConfig = applyPolicyToDoctorConfig(applyCheckProfile(await loadDoctorConfig(targetPath, configPath), checkProfile), policy);
2531
+ const configuredResult = applyDoctorConfig(await runCheckImpl(targetPath, {
2451
2532
  runtime: effectiveRuntimeProbeEnabled,
2452
2533
  runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
2453
2534
  ? (line) => io.writeStderr(line)
2454
2535
  : undefined
2455
- }), applyPolicyToDoctorConfig(applyCheckProfile(await loadDoctorConfig(targetPath, configPath), checkProfile), policy));
2536
+ }), doctorConfig);
2537
+ const result = baseline
2538
+ ? applyValidationBaseline(configuredResult, baseline, {
2539
+ failOnWarnings: doctorConfig.failOnWarnings
2540
+ })
2541
+ : configuredResult;
2456
2542
  if (renderer) {
2457
2543
  if (result.status === "fail") {
2458
2544
  renderer.stopFailure("Validation failed");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.43.0",
3
+ "version": "1.45.0",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",