codex-plugin-doctor 1.28.0 → 1.30.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
@@ -400,9 +400,9 @@ jobs:
400
400
  runs-on: ubuntu-latest
401
401
  steps:
402
402
  - uses: actions/checkout@v5
403
- - uses: Esquetta/CodexPluginDoctor@v1.28.0
403
+ - uses: Esquetta/CodexPluginDoctor@v1.30.0
404
404
  with:
405
- version: "1.28.0"
405
+ version: "1.30.0"
406
406
  path: .
407
407
  runtime: "true"
408
408
  policy: codex-publish
@@ -73,6 +73,12 @@ const publicSchemaDefinitions = [
73
73
  outputKind: "doctor.suppress.remove",
74
74
  required: ["schemaVersion", "kind", "command", "configPath", "index", "suppression"]
75
75
  },
76
+ {
77
+ id: "doctor.suppress.prune.json",
78
+ command: "codex-plugin-doctor suppress prune <path> --apply --json",
79
+ outputKind: "doctor.suppress.prune",
80
+ required: ["schemaVersion", "kind", "command", "configPath", "applied", "removedCount", "removed"]
81
+ },
76
82
  {
77
83
  id: "doctor.history.json",
78
84
  command: "codex-plugin-doctor history <history.jsonl> --json",
@@ -13,6 +13,10 @@ export interface SuppressionMutationResult {
13
13
  index: number;
14
14
  suppression: unknown;
15
15
  }
16
+ export interface SuppressionPruneResult {
17
+ config: RawDoctorConfig;
18
+ removed: ManagedSuppressionRecord[];
19
+ }
16
20
  export type SuppressionManagementErrorCode = "suppression_non_array" | "suppression_invalid_record" | "suppression_duplicate_fingerprint" | "suppression_invalid_index" | "suppression_invalid_fingerprint" | "suppression_fingerprint_not_found" | "suppression_fingerprint_ambiguous";
17
21
  type SuppressionManagementErrorDetails = {
18
22
  code: SuppressionManagementErrorCode;
@@ -31,4 +35,5 @@ export declare function listSuppressions(config: RawDoctorConfig, now?: Date): M
31
35
  export declare function addSuppression(config: RawDoctorConfig, record: unknown): SuppressionMutationResult;
32
36
  export declare function removeSuppressionByIndex(config: RawDoctorConfig, index: number): SuppressionMutationResult;
33
37
  export declare function removeSuppressionByFingerprint(config: RawDoctorConfig, fingerprint: string): SuppressionMutationResult;
38
+ export declare function pruneSuppressions(config: RawDoctorConfig, now?: Date): SuppressionPruneResult;
34
39
  export {};
@@ -168,3 +168,14 @@ export function removeSuppressionByFingerprint(config, fingerprint) {
168
168
  }
169
169
  return removeSuppressionByIndex(config, matches[0]);
170
170
  }
171
+ export function pruneSuppressions(config, now = new Date()) {
172
+ const { suppressions } = readSuppressions(config);
173
+ const managedSuppressions = listSuppressions(config, now);
174
+ const removed = managedSuppressions.filter((suppression) => suppression.status !== "active");
175
+ const removedIndexes = new Set(removed.map((suppression) => suppression.index));
176
+ const nextSuppressions = suppressions.filter((_suppression, index) => !removedIndexes.has(index));
177
+ return {
178
+ config: copyConfigWithSuppressions(config, nextSuppressions),
179
+ removed
180
+ };
181
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { CheckOptions, CheckResult } from "./domain/types.js";
2
2
  export { applyDoctorConfig, loadDoctorConfig, type DoctorConfig } from "./core/doctor-config.js";
3
3
  export { readRawDoctorConfig, resolveDoctorConfigPath, writeRawDoctorConfig, type LoadedDoctorConfig, type RawDoctorConfig } from "./core/doctor-config-store.js";
4
4
  export { classifySuppressionRecord, isValidSuppressionFingerprint, isSuppressionExpired, validateSuppressionRecord, type ClassifiedSuppressionRecord, type InvalidSuppressionRecord, type SuppressionRecordField, type ValidSuppressionRecord } from "./core/suppression-record.js";
5
- export { addSuppression, listSuppressions, removeSuppressionByFingerprint, removeSuppressionByIndex, SuppressionManagementError, type ManagedSuppressionRecord, type SuppressionManagementErrorCode, type SuppressionMutationResult } from "./core/suppression-management.js";
5
+ export { addSuppression, listSuppressions, pruneSuppressions, removeSuppressionByFingerprint, removeSuppressionByIndex, SuppressionManagementError, type ManagedSuppressionRecord, type SuppressionManagementErrorCode, type SuppressionMutationResult, type SuppressionPruneResult } from "./core/suppression-management.js";
6
6
  export { buildFindingFingerprint, formatFindingFingerprintLine, withFindingFingerprint, withFindingFingerprints } from "./reporting/finding-fingerprint.js";
7
7
  export { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard, type SecurityAudit } from "./security/security-audit.js";
8
8
  export { buildTrustScore, renderTrustScore, renderTrustScoreJson, type BuildTrustScoreOptions, type TrustScoreReport } from "./security/trust-score.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { validatePlugin } from "./core/validate-plugin.js";
2
2
  export { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
3
3
  export { readRawDoctorConfig, resolveDoctorConfigPath, writeRawDoctorConfig } from "./core/doctor-config-store.js";
4
4
  export { classifySuppressionRecord, isValidSuppressionFingerprint, isSuppressionExpired, validateSuppressionRecord } from "./core/suppression-record.js";
5
- export { addSuppression, listSuppressions, removeSuppressionByFingerprint, removeSuppressionByIndex, SuppressionManagementError } from "./core/suppression-management.js";
5
+ export { addSuppression, listSuppressions, pruneSuppressions, removeSuppressionByFingerprint, removeSuppressionByIndex, SuppressionManagementError } from "./core/suppression-management.js";
6
6
  export { buildFindingFingerprint, formatFindingFingerprintLine, withFindingFingerprint, withFindingFingerprints } from "./reporting/finding-fingerprint.js";
7
7
  export { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard } from "./security/security-audit.js";
8
8
  export { buildTrustScore, renderTrustScore, renderTrustScoreJson } from "./security/trust-score.js";
@@ -1,7 +1,13 @@
1
- import type { ManagedSuppressionRecord, SuppressionMutationResult } from "../core/suppression-management.js";
1
+ import type { ManagedSuppressionRecord, SuppressionMutationResult, SuppressionPruneResult } from "../core/suppression-management.js";
2
2
  type SuppressionMutationCommand = "suppress.add" | "suppress.remove";
3
3
  export declare function renderSuppressionList(configPath: string, suppressions: ManagedSuppressionRecord[]): string;
4
4
  export declare function renderSuppressionListJson(configPath: string, suppressions: ManagedSuppressionRecord[]): string;
5
5
  export declare function renderSuppressionMutation(command: SuppressionMutationCommand, configPath: string, result: SuppressionMutationResult): string;
6
6
  export declare function renderSuppressionMutationJson(command: SuppressionMutationCommand, configPath: string, result: SuppressionMutationResult): string;
7
+ export declare function renderSuppressionPrune(configPath: string, result: SuppressionPruneResult, options: {
8
+ applied: boolean;
9
+ }): string;
10
+ export declare function renderSuppressionPruneJson(configPath: string, result: SuppressionPruneResult, options: {
11
+ applied: boolean;
12
+ }): string;
7
13
  export {};
@@ -115,3 +115,38 @@ export function renderSuppressionMutationJson(command, configPath, result) {
115
115
  suppression: buildMutationSummary(result.suppression)
116
116
  }, null, 2);
117
117
  }
118
+ export function renderSuppressionPrune(configPath, result, options) {
119
+ const lines = [
120
+ "Action: Prune",
121
+ `Mode: ${options.applied ? "apply" : "dry-run"}`,
122
+ `Config: ${configPath}`,
123
+ `Removed suppressions: ${result.removed.length}`
124
+ ];
125
+ if (result.removed.length === 0) {
126
+ lines.push("", "No expired or invalid suppressions to prune.");
127
+ return lines.join("\n");
128
+ }
129
+ lines.push("", "Removed", "-------");
130
+ for (const suppression of result.removed) {
131
+ const entry = buildListEntry(suppression);
132
+ if (entry.status === "invalid") {
133
+ lines.push(`[${entry.index}] INVALID ${entry.invalidField}`);
134
+ continue;
135
+ }
136
+ lines.push(`[${entry.index}] ${entry.status.toUpperCase()} ${entry.fingerprint}`);
137
+ lines.push(` Reason: ${entry.reason}`);
138
+ lines.push(` Expires: ${entry.expiresAt}`);
139
+ }
140
+ return lines.join("\n");
141
+ }
142
+ export function renderSuppressionPruneJson(configPath, result, options) {
143
+ return JSON.stringify({
144
+ schemaVersion: "1.0.0",
145
+ kind: "doctor.suppress.prune",
146
+ command: "suppress.prune",
147
+ configPath,
148
+ applied: options.applied,
149
+ removedCount: result.removed.length,
150
+ removed: result.removed.map(buildListEntry)
151
+ }, null, 2);
152
+ }
package/dist/run-cli.js CHANGED
@@ -44,7 +44,7 @@ import { renderCompatibilityReport } from "./reporting/render-compatibility-repo
44
44
  import { renderHistorySummary } from "./reporting/render-history-summary.js";
45
45
  import { renderJsonReport } from "./reporting/render-json-report.js";
46
46
  import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
47
- import { renderSuppressionList, renderSuppressionListJson, renderSuppressionMutation, renderSuppressionMutationJson } from "./reporting/render-suppression-management.js";
47
+ import { renderSuppressionList, renderSuppressionListJson, renderSuppressionMutation, renderSuppressionMutationJson, renderSuppressionPrune, renderSuppressionPruneJson } from "./reporting/render-suppression-management.js";
48
48
  import { renderRuleExplanation } from "./reporting/render-rule-explanation.js";
49
49
  import { renderSarifReport } from "./reporting/render-sarif-report.js";
50
50
  import { renderTextReport } from "./reporting/render-text-report.js";
@@ -56,7 +56,7 @@ import { createLiveStatusRenderer } from "./terminal/live-status-renderer.js";
56
56
  import { determineOutputPolicy } from "./terminal/output-policy.js";
57
57
  import { getSpinner } from "./terminal/spinner-registry.js";
58
58
  import { packageVersion } from "./version.js";
59
- import { addSuppression, listSuppressions, readRawDoctorConfig, removeSuppressionByFingerprint, removeSuppressionByIndex, SuppressionManagementError, writeRawDoctorConfig } from "./index.js";
59
+ import { addSuppression, listSuppressions, pruneSuppressions, readRawDoctorConfig, removeSuppressionByFingerprint, removeSuppressionByIndex, SuppressionManagementError, writeRawDoctorConfig } from "./index.js";
60
60
  const defaultIo = {
61
61
  writeStdout(message) {
62
62
  process.stdout.write(`${message}\n`);
@@ -79,12 +79,14 @@ const defaultIo = {
79
79
  };
80
80
  function printUsage(io) {
81
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]\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> [--json] [--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>]\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 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
+ 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]");
82
83
  }
83
84
  const suppressUsageText = [
84
85
  "Usage:",
85
86
  " codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]",
86
- " codex-plugin-doctor suppress list <path> [--config <path>] [--json]",
87
- " codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]"
87
+ " codex-plugin-doctor suppress list <path> [--config <path>] [--json] [--fail-on-expired] [--fail-on-invalid] [--warn-expiring-within-days <days>]",
88
+ " codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]",
89
+ " codex-plugin-doctor suppress prune <path> [--config <path>] [--apply] [--json]"
88
90
  ].join("\n");
89
91
  function printSuppressUsage(io) {
90
92
  io.writeStderr(suppressUsageText);
@@ -113,7 +115,10 @@ function parseSuppressCommand(args) {
113
115
  showUsage: true
114
116
  };
115
117
  }
116
- if (action !== "add" && action !== "list" && action !== "remove") {
118
+ if (action !== "add" &&
119
+ action !== "list" &&
120
+ action !== "remove" &&
121
+ action !== "prune") {
117
122
  return {
118
123
  message: `Unknown suppress action: ${action}.`,
119
124
  showUsage: true
@@ -131,6 +136,10 @@ function parseSuppressCommand(args) {
131
136
  let reason = null;
132
137
  let expiresAt = null;
133
138
  let indexValue = null;
139
+ let failOnExpired = false;
140
+ let failOnInvalid = false;
141
+ let apply = false;
142
+ let warnExpiringWithinDays = null;
134
143
  const seenFlags = new Set();
135
144
  const markSeenFlag = (flag) => {
136
145
  if (seenFlags.has(flag)) {
@@ -168,6 +177,59 @@ function parseSuppressCommand(args) {
168
177
  index += 1;
169
178
  continue;
170
179
  }
180
+ if (action === "list" && flag === "--fail-on-expired") {
181
+ const duplicateError = markSeenFlag(flag);
182
+ if (duplicateError) {
183
+ return duplicateError;
184
+ }
185
+ failOnExpired = true;
186
+ continue;
187
+ }
188
+ if (action === "list" && flag === "--fail-on-invalid") {
189
+ const duplicateError = markSeenFlag(flag);
190
+ if (duplicateError) {
191
+ return duplicateError;
192
+ }
193
+ failOnInvalid = true;
194
+ continue;
195
+ }
196
+ if (action === "list" && flag === "--warn-expiring-within-days") {
197
+ const duplicateError = markSeenFlag(flag);
198
+ if (duplicateError) {
199
+ return duplicateError;
200
+ }
201
+ const value = flags[index + 1];
202
+ if (!value || value.startsWith("--")) {
203
+ return {
204
+ message: "Missing days after --warn-expiring-within-days.",
205
+ showUsage: false
206
+ };
207
+ }
208
+ if (!/^\d+$/.test(value)) {
209
+ return {
210
+ message: "--warn-expiring-within-days must be a non-negative integer.",
211
+ showUsage: false
212
+ };
213
+ }
214
+ const parsedDays = Number(value);
215
+ if (!Number.isSafeInteger(parsedDays)) {
216
+ return {
217
+ message: "--warn-expiring-within-days must be a non-negative integer.",
218
+ showUsage: false
219
+ };
220
+ }
221
+ warnExpiringWithinDays = parsedDays;
222
+ index += 1;
223
+ continue;
224
+ }
225
+ if (action === "prune" && flag === "--apply") {
226
+ const duplicateError = markSeenFlag(flag);
227
+ if (duplicateError) {
228
+ return duplicateError;
229
+ }
230
+ apply = true;
231
+ continue;
232
+ }
171
233
  if (action === "add" && flag === "--fingerprint") {
172
234
  const duplicateError = markSeenFlag(flag);
173
235
  if (duplicateError) {
@@ -258,7 +320,19 @@ function parseSuppressCommand(args) {
258
320
  action,
259
321
  targetPath,
260
322
  configPath,
261
- jsonOutput
323
+ jsonOutput,
324
+ failOnExpired,
325
+ failOnInvalid,
326
+ warnExpiringWithinDays
327
+ };
328
+ }
329
+ if (action === "prune") {
330
+ return {
331
+ action,
332
+ targetPath,
333
+ configPath,
334
+ jsonOutput,
335
+ apply
262
336
  };
263
337
  }
264
338
  if (action === "add") {
@@ -353,6 +427,38 @@ function defaultSuppressionExpiry(now) {
353
427
  expiry.setDate(expiry.getDate() + 30);
354
428
  return formatLocalDate(expiry);
355
429
  }
430
+ function parseSuppressionDateUtcStart(value) {
431
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
432
+ if (!match) {
433
+ return null;
434
+ }
435
+ const year = Number(match[1]);
436
+ const month = Number(match[2]);
437
+ const day = Number(match[3]);
438
+ const timestamp = Date.UTC(year, month - 1, day);
439
+ const date = new Date(timestamp);
440
+ return date.getUTCFullYear() === year &&
441
+ date.getUTCMonth() === month - 1 &&
442
+ date.getUTCDate() === day
443
+ ? timestamp
444
+ : null;
445
+ }
446
+ function listExpiringSuppressionIndexes(suppressions, now, withinDays) {
447
+ const dayMs = 24 * 60 * 60 * 1000;
448
+ const todayUtc = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
449
+ const cutoffUtc = todayUtc + withinDays * dayMs;
450
+ return suppressions.flatMap((suppression) => {
451
+ if (suppression.status !== "active" || !suppression.expiresAt) {
452
+ return [];
453
+ }
454
+ const expiresAtUtc = parseSuppressionDateUtcStart(suppression.expiresAt);
455
+ return expiresAtUtc !== null &&
456
+ expiresAtUtc >= todayUtc &&
457
+ expiresAtUtc <= cutoffUtc
458
+ ? [suppression.index]
459
+ : [];
460
+ });
461
+ }
356
462
  function parseInteractiveSelection(answer, count) {
357
463
  const trimmed = answer.trim();
358
464
  if (!/^\d+$/.test(trimmed)) {
@@ -380,7 +486,8 @@ function renderInteractiveSuppressionCandidates(suppressions) {
380
486
  }
381
487
  async function executeSuppressCommand(command, io, options = {}) {
382
488
  const writeConfig = options.writeRawDoctorConfigImpl ?? writeRawDoctorConfig;
383
- if (command.action !== "list" && command.interactive) {
489
+ if ((command.action === "add" || command.action === "remove") &&
490
+ command.interactive) {
384
491
  if (command.jsonOutput) {
385
492
  io.writeStderr(`Interactive suppress ${command.action} does not support --json.`);
386
493
  return 2;
@@ -395,9 +502,34 @@ async function executeSuppressCommand(command, io, options = {}) {
395
502
  const now = options.now?.() ?? new Date();
396
503
  if (command.action === "list") {
397
504
  const suppressions = listSuppressions(rawConfig.value, now);
505
+ const expiredCount = suppressions.filter((suppression) => suppression.status === "expired").length;
506
+ const invalidCount = suppressions.filter((suppression) => suppression.status === "invalid").length;
398
507
  io.writeStdout(command.jsonOutput
399
508
  ? renderSuppressionListJson(rawConfig.configPath, suppressions)
400
509
  : renderSuppressionList(rawConfig.configPath, suppressions));
510
+ if (command.warnExpiringWithinDays !== null) {
511
+ const expiringIndexes = listExpiringSuppressionIndexes(suppressions, now, command.warnExpiringWithinDays);
512
+ if (expiringIndexes.length > 0) {
513
+ io.writeStderr(`Warning: ${expiringIndexes.length} suppression(s) expire within ${command.warnExpiringWithinDays} day(s): indexes ${expiringIndexes.join(", ")}.`);
514
+ }
515
+ }
516
+ return (command.failOnExpired && expiredCount > 0) ||
517
+ (command.failOnInvalid && invalidCount > 0)
518
+ ? 1
519
+ : 0;
520
+ }
521
+ if (command.action === "prune") {
522
+ const result = pruneSuppressions(rawConfig.value, now);
523
+ if (command.apply && result.removed.length > 0) {
524
+ await writeConfig(rawConfig.configPath, result.config);
525
+ }
526
+ io.writeStdout(command.jsonOutput
527
+ ? renderSuppressionPruneJson(rawConfig.configPath, result, {
528
+ applied: command.apply
529
+ })
530
+ : renderSuppressionPrune(rawConfig.configPath, result, {
531
+ applied: command.apply
532
+ }));
401
533
  return 0;
402
534
  }
403
535
  if (command.action === "add") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.28.0",
3
+ "version": "1.30.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",