codex-plugin-doctor 0.5.0 → 0.6.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
@@ -65,8 +65,9 @@ Output formats:
65
65
  - human text output
66
66
  - JSON reports
67
67
  - Markdown reports
68
- - Shields-compatible badge JSON and static badge Markdown
69
- - `--output` file writing
68
+ - Shields-compatible badge JSON and static badge Markdown
69
+ - validation history JSONL and trend summaries
70
+ - `--output` file writing
70
71
  - CI summary and artifact generation
71
72
 
72
73
  ## Quick Start
@@ -184,10 +185,14 @@ codex-plugin-doctor check . --badge-markdown
184
185
  codex-plugin-doctor check . --sarif --output results.sarif
185
186
  codex-plugin-doctor check . --ascii
186
187
  codex-plugin-doctor check . --no-animations
187
- codex-plugin-doctor check . --runtime
188
- codex-plugin-doctor check . --config .codex-doctor.json
189
- codex-plugin-doctor check . --json --runtime --verbose-runtime
190
- ```
188
+ codex-plugin-doctor check . --runtime
189
+ codex-plugin-doctor check . --config .codex-doctor.json
190
+ codex-plugin-doctor check . --history validation-history.jsonl
191
+ codex-plugin-doctor history validation-history.jsonl
192
+ codex-plugin-doctor history validation-history.jsonl --json
193
+ codex-plugin-doctor history validation-history.jsonl --fail-on-regression
194
+ codex-plugin-doctor check . --json --runtime --verbose-runtime
195
+ ```
191
196
 
192
197
  `self-test` runs the bundled runtime-complete sample through static validation, runtime MCP probes, and the compatibility scorecard. It is the fastest post-install check after `npm install -g codex-plugin-doctor`.
193
198
 
@@ -197,7 +202,9 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
197
202
 
198
203
  `compat --scorecard` turns the compatibility matrix into a compact score summary. `PASS` maps to `100`, `WARN` maps to `70`, and `FAIL` or `SKIPPED` maps to `0`.
199
204
 
200
- `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`.
205
+ `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`.
206
+
207
+ `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.
201
208
 
202
209
  Optional local policy file:
203
210
 
@@ -232,11 +239,11 @@ jobs:
232
239
  runs-on: ubuntu-latest
233
240
  steps:
234
241
  - uses: actions/checkout@v4
235
- - uses: Esquetta/CodexPluginDoctor@v0.5.0
236
- with:
237
- version: "0.5.0"
238
- path: .
239
- runtime: "false"
242
+ - uses: Esquetta/CodexPluginDoctor@v0.6.0
243
+ with:
244
+ version: "0.6.0"
245
+ path: .
246
+ runtime: "false"
240
247
  ```
241
248
 
242
249
  For runtime probing, SARIF output, installed plugin cache checks, and pinned release examples, see [GitHub Action Usage](./docs/engineering/github-action-usage.md).
@@ -0,0 +1,33 @@
1
+ import type { CheckResult } from "../domain/types.js";
2
+ export interface ValidationHistoryEntry {
3
+ schemaVersion: "1.0.0";
4
+ generatedAt: string;
5
+ targetPath: string;
6
+ status: CheckResult["status"];
7
+ runtimeProbeEnabled: boolean;
8
+ findingCounts: {
9
+ fail: number;
10
+ warn: number;
11
+ total: number;
12
+ };
13
+ }
14
+ export interface ValidationHistorySummary {
15
+ schemaVersion: "1.0.0";
16
+ runs: number;
17
+ latest: ValidationHistoryEntry;
18
+ previous: ValidationHistoryEntry | null;
19
+ delta: {
20
+ fail: number;
21
+ warn: number;
22
+ total: number;
23
+ };
24
+ regression: boolean;
25
+ }
26
+ export declare function buildValidationHistoryEntry(result: CheckResult, options: {
27
+ runtimeProbeEnabled: boolean;
28
+ }): ValidationHistoryEntry;
29
+ export declare function appendValidationHistoryEntry(historyPath: string, result: CheckResult, options: {
30
+ runtimeProbeEnabled: boolean;
31
+ }): Promise<void>;
32
+ export declare function readValidationHistory(historyPath: string): Promise<ValidationHistoryEntry[]>;
33
+ export declare function summarizeValidationHistory(entries: ValidationHistoryEntry[]): ValidationHistorySummary;
@@ -0,0 +1,64 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const statusRank = {
4
+ pass: 0,
5
+ warn: 1,
6
+ fail: 2
7
+ };
8
+ function countFindings(result) {
9
+ return {
10
+ fail: result.findings.filter((finding) => finding.severity === "fail").length,
11
+ warn: result.findings.filter((finding) => finding.severity === "warn").length,
12
+ total: result.findings.length
13
+ };
14
+ }
15
+ export function buildValidationHistoryEntry(result, options) {
16
+ return {
17
+ schemaVersion: "1.0.0",
18
+ generatedAt: new Date().toISOString(),
19
+ targetPath: result.targetPath,
20
+ status: result.status,
21
+ runtimeProbeEnabled: options.runtimeProbeEnabled,
22
+ findingCounts: countFindings(result)
23
+ };
24
+ }
25
+ export async function appendValidationHistoryEntry(historyPath, result, options) {
26
+ const absoluteHistoryPath = path.resolve(historyPath);
27
+ await mkdir(path.dirname(absoluteHistoryPath), { recursive: true });
28
+ await writeFile(absoluteHistoryPath, `${JSON.stringify(buildValidationHistoryEntry(result, options))}\n`, { encoding: "utf8", flag: "a" });
29
+ }
30
+ export async function readValidationHistory(historyPath) {
31
+ const content = await readFile(path.resolve(historyPath), "utf8");
32
+ return content
33
+ .split(/\r?\n/)
34
+ .map((line) => line.trim())
35
+ .filter(Boolean)
36
+ .map((line) => JSON.parse(line));
37
+ }
38
+ export function summarizeValidationHistory(entries) {
39
+ if (entries.length === 0) {
40
+ throw new Error("No validation history entries found.");
41
+ }
42
+ const latest = entries[entries.length - 1];
43
+ const previous = entries.length > 1 ? entries[entries.length - 2] : null;
44
+ const delta = previous
45
+ ? {
46
+ fail: latest.findingCounts.fail - previous.findingCounts.fail,
47
+ warn: latest.findingCounts.warn - previous.findingCounts.warn,
48
+ total: latest.findingCounts.total - previous.findingCounts.total
49
+ }
50
+ : { fail: 0, warn: 0, total: 0 };
51
+ const regression = previous
52
+ ? statusRank[latest.status] > statusRank[previous.status]
53
+ || delta.fail > 0
54
+ || delta.warn > 0
55
+ : false;
56
+ return {
57
+ schemaVersion: "1.0.0",
58
+ runs: entries.length,
59
+ latest,
60
+ previous,
61
+ delta,
62
+ regression
63
+ };
64
+ }
@@ -0,0 +1,2 @@
1
+ import { type ValidationHistoryEntry } from "../core/validation-history.js";
2
+ export declare function renderHistorySummary(entries: ValidationHistoryEntry[]): string;
@@ -0,0 +1,23 @@
1
+ import { summarizeValidationHistory } from "../core/validation-history.js";
2
+ function formatDelta(value) {
3
+ return value > 0 ? `+${value}` : String(value);
4
+ }
5
+ export function renderHistorySummary(entries) {
6
+ const summary = summarizeValidationHistory(entries);
7
+ const { latest, previous } = summary;
8
+ const lines = [
9
+ "Validation History",
10
+ "==================",
11
+ `Runs: ${summary.runs}`,
12
+ `Latest: ${latest.status.toUpperCase()}`,
13
+ `Target: ${latest.targetPath}`,
14
+ `Generated: ${latest.generatedAt}`,
15
+ `Fail findings: ${latest.findingCounts.fail}`,
16
+ `Warn findings: ${latest.findingCounts.warn}`,
17
+ `Regression: ${summary.regression ? "YES" : "NO"}`
18
+ ];
19
+ if (previous) {
20
+ lines.push("", `Previous: ${previous.status.toUpperCase()}`, `Fail findings: ${formatDelta(summary.delta.fail)}`, `Warn findings: ${formatDelta(summary.delta.warn)}`);
21
+ }
22
+ return lines.join("\n");
23
+ }
package/dist/run-cli.js CHANGED
@@ -2,6 +2,7 @@ import { writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
5
+ import { appendValidationHistoryEntry, readValidationHistory, summarizeValidationHistory } from "./core/validation-history.js";
5
6
  import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compatibility-matrix.js";
6
7
  import { applyInstallPreview, renderApplyInstallResult } from "./compatibility/apply-install-preview.js";
7
8
  import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
@@ -13,6 +14,7 @@ import { renderInstalledSummary } from "./reporting/render-installed-summary.js"
13
14
  import { renderBadgeJson, renderBadgeMarkdown } from "./reporting/render-badge-report.js";
14
15
  import { renderCompatibilityScorecard } from "./reporting/render-compatibility-scorecard.js";
15
16
  import { renderCompatibilityReport } from "./reporting/render-compatibility-report.js";
17
+ import { renderHistorySummary } from "./reporting/render-history-summary.js";
16
18
  import { renderJsonReport } from "./reporting/render-json-report.js";
17
19
  import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
18
20
  import { renderRuleExplanation } from "./reporting/render-rule-explanation.js";
@@ -32,7 +34,7 @@ const defaultIo = {
32
34
  }
33
35
  };
34
36
  function printUsage(io) {
35
- io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
37
+ io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
36
38
  }
37
39
  function renderInstalledPlugins(plugins) {
38
40
  const lines = [
@@ -118,6 +120,31 @@ export async function runCli(args, io = defaultIo, options = {}) {
118
120
  io.writeStdout(renderRuleExplanation(rule));
119
121
  return 0;
120
122
  }
123
+ if (command === "history") {
124
+ if (!maybePath || maybePath.startsWith("--")) {
125
+ io.writeStderr("Missing history path. Usage: codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]");
126
+ return 2;
127
+ }
128
+ try {
129
+ const entries = await readValidationHistory(maybePath);
130
+ const summary = summarizeValidationHistory(entries);
131
+ const jsonOutput = remainingArgs.includes("--json");
132
+ const failOnRegression = remainingArgs.includes("--fail-on-regression");
133
+ io.writeStdout(jsonOutput
134
+ ? JSON.stringify(summary, null, 2)
135
+ : renderHistorySummary(entries));
136
+ if (failOnRegression && summary.regression) {
137
+ io.writeStderr("Validation history regression detected.");
138
+ return 1;
139
+ }
140
+ return 0;
141
+ }
142
+ catch (error) {
143
+ const message = error instanceof Error ? error.message : "Unable to read validation history.";
144
+ io.writeStderr(message);
145
+ return 1;
146
+ }
147
+ }
121
148
  if (command === "self-test" || command === "demo") {
122
149
  const targetPath = resolveBundledSelfTestTarget();
123
150
  const runCheckImpl = options.runCheckImpl ?? runCheck;
@@ -260,6 +287,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
260
287
  const outputPath = outputIndex === -1 ? null : normalizedFlags[outputIndex + 1];
261
288
  const configIndex = normalizedFlags.indexOf("--config");
262
289
  const configPath = configIndex === -1 ? null : normalizedFlags[configIndex + 1];
290
+ const historyIndex = normalizedFlags.indexOf("--history");
291
+ const historyPath = historyIndex === -1 ? null : normalizedFlags[historyIndex + 1];
263
292
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
264
293
  io.writeStderr("Missing path after --output.");
265
294
  return 2;
@@ -268,10 +297,18 @@ export async function runCli(args, io = defaultIo, options = {}) {
268
297
  io.writeStderr("Missing path after --config.");
269
298
  return 2;
270
299
  }
300
+ if (historyIndex !== -1 && (!historyPath || historyPath.startsWith("--"))) {
301
+ io.writeStderr("Missing path after --history.");
302
+ return 2;
303
+ }
271
304
  if (checkInstalled && (badgeJsonOutput || badgeMarkdownOutput)) {
272
305
  io.writeStderr("Badge output requires a single package target.");
273
306
  return 2;
274
307
  }
308
+ if (checkInstalled && historyPath) {
309
+ io.writeStderr("History output requires a single package target.");
310
+ return 2;
311
+ }
275
312
  const outputPolicy = determineOutputPolicy({
276
313
  jsonOutput: jsonOutput || badgeJsonOutput,
277
314
  markdownOutput: markdownOutput || badgeMarkdownOutput,
@@ -354,6 +391,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
354
391
  if (outputPath) {
355
392
  await writeFile(outputPath, report, "utf8");
356
393
  }
394
+ if (historyPath) {
395
+ await appendValidationHistoryEntry(historyPath, result, { runtimeProbeEnabled });
396
+ }
357
397
  io.writeStdout(report);
358
398
  return result.exitCode;
359
399
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",