codex-plugin-doctor 1.27.0 → 1.29.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/dist/run-cli.js CHANGED
@@ -44,6 +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, renderSuppressionPrune, renderSuppressionPruneJson } from "./reporting/render-suppression-management.js";
47
48
  import { renderRuleExplanation } from "./reporting/render-rule-explanation.js";
48
49
  import { renderSarifReport } from "./reporting/render-sarif-report.js";
49
50
  import { renderTextReport } from "./reporting/render-text-report.js";
@@ -55,6 +56,7 @@ import { createLiveStatusRenderer } from "./terminal/live-status-renderer.js";
55
56
  import { determineOutputPolicy } from "./terminal/output-policy.js";
56
57
  import { getSpinner } from "./terminal/spinner-registry.js";
57
58
  import { packageVersion } from "./version.js";
59
+ import { addSuppression, listSuppressions, pruneSuppressions, readRawDoctorConfig, removeSuppressionByFingerprint, removeSuppressionByIndex, SuppressionManagementError, writeRawDoctorConfig } from "./index.js";
58
60
  const defaultIo = {
59
61
  writeStdout(message) {
60
62
  process.stdout.write(`${message}\n`);
@@ -76,7 +78,18 @@ const defaultIo = {
76
78
  }
77
79
  };
78
80
  function printUsage(io) {
79
- 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 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");
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]");
83
+ }
84
+ const suppressUsageText = [
85
+ "Usage:",
86
+ " codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--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]"
90
+ ].join("\n");
91
+ function printSuppressUsage(io) {
92
+ io.writeStderr(suppressUsageText);
80
93
  }
81
94
  const performanceStageNames = new Set([
82
95
  "validation",
@@ -94,6 +107,520 @@ function parseNonNegativeNumber(value) {
94
107
  const parsed = Number(value);
95
108
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
96
109
  }
110
+ function parseSuppressCommand(args) {
111
+ const [action, targetPath, ...flags] = args;
112
+ if (!action) {
113
+ return {
114
+ message: "Missing suppress action.",
115
+ showUsage: true
116
+ };
117
+ }
118
+ if (action !== "add" &&
119
+ action !== "list" &&
120
+ action !== "remove" &&
121
+ action !== "prune") {
122
+ return {
123
+ message: `Unknown suppress action: ${action}.`,
124
+ showUsage: true
125
+ };
126
+ }
127
+ if (!targetPath || targetPath.startsWith("--")) {
128
+ return {
129
+ message: "Missing suppression target path.",
130
+ showUsage: true
131
+ };
132
+ }
133
+ let configPath = null;
134
+ let jsonOutput = false;
135
+ let fingerprint = null;
136
+ let reason = null;
137
+ let expiresAt = null;
138
+ let indexValue = null;
139
+ let failOnExpired = false;
140
+ let failOnInvalid = false;
141
+ let apply = false;
142
+ let warnExpiringWithinDays = null;
143
+ const seenFlags = new Set();
144
+ const markSeenFlag = (flag) => {
145
+ if (seenFlags.has(flag)) {
146
+ return {
147
+ message: `Duplicate suppress flag: ${flag}.`,
148
+ showUsage: false
149
+ };
150
+ }
151
+ seenFlags.add(flag);
152
+ return null;
153
+ };
154
+ for (let index = 0; index < flags.length; index += 1) {
155
+ const flag = flags[index];
156
+ if (flag === "--json") {
157
+ const duplicateError = markSeenFlag(flag);
158
+ if (duplicateError) {
159
+ return duplicateError;
160
+ }
161
+ jsonOutput = true;
162
+ continue;
163
+ }
164
+ if (flag === "--config") {
165
+ const duplicateError = markSeenFlag(flag);
166
+ if (duplicateError) {
167
+ return duplicateError;
168
+ }
169
+ const value = flags[index + 1];
170
+ if (!value || value.startsWith("--")) {
171
+ return {
172
+ message: "Missing path after --config.",
173
+ showUsage: false
174
+ };
175
+ }
176
+ configPath = value;
177
+ index += 1;
178
+ continue;
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
+ }
233
+ if (action === "add" && flag === "--fingerprint") {
234
+ const duplicateError = markSeenFlag(flag);
235
+ if (duplicateError) {
236
+ return duplicateError;
237
+ }
238
+ const value = flags[index + 1];
239
+ if (!value || value.startsWith("--")) {
240
+ return {
241
+ message: "Missing fingerprint after --fingerprint.",
242
+ showUsage: false
243
+ };
244
+ }
245
+ fingerprint = value;
246
+ index += 1;
247
+ continue;
248
+ }
249
+ if (action === "add" && flag === "--reason") {
250
+ const duplicateError = markSeenFlag(flag);
251
+ if (duplicateError) {
252
+ return duplicateError;
253
+ }
254
+ const value = flags[index + 1];
255
+ if (!value || value.startsWith("--")) {
256
+ return {
257
+ message: "Missing reason after --reason.",
258
+ showUsage: false
259
+ };
260
+ }
261
+ reason = value;
262
+ index += 1;
263
+ continue;
264
+ }
265
+ if (action === "add" && flag === "--expires-at") {
266
+ const duplicateError = markSeenFlag(flag);
267
+ if (duplicateError) {
268
+ return duplicateError;
269
+ }
270
+ const value = flags[index + 1];
271
+ if (!value || value.startsWith("--")) {
272
+ return {
273
+ message: "Missing date after --expires-at.",
274
+ showUsage: false
275
+ };
276
+ }
277
+ expiresAt = value;
278
+ index += 1;
279
+ continue;
280
+ }
281
+ if (action === "remove" && flag === "--fingerprint") {
282
+ const duplicateError = markSeenFlag(flag);
283
+ if (duplicateError) {
284
+ return duplicateError;
285
+ }
286
+ const value = flags[index + 1];
287
+ if (!value || value.startsWith("--")) {
288
+ return {
289
+ message: "Missing fingerprint after --fingerprint.",
290
+ showUsage: false
291
+ };
292
+ }
293
+ fingerprint = value;
294
+ index += 1;
295
+ continue;
296
+ }
297
+ if (action === "remove" && flag === "--index") {
298
+ const duplicateError = markSeenFlag(flag);
299
+ if (duplicateError) {
300
+ return duplicateError;
301
+ }
302
+ const value = flags[index + 1];
303
+ if (!value || value.startsWith("--")) {
304
+ return {
305
+ message: "Missing value after --index.",
306
+ showUsage: false
307
+ };
308
+ }
309
+ indexValue = value;
310
+ index += 1;
311
+ continue;
312
+ }
313
+ return {
314
+ message: `Unknown suppress flag: ${flag}.`,
315
+ showUsage: false
316
+ };
317
+ }
318
+ if (action === "list") {
319
+ return {
320
+ action,
321
+ targetPath,
322
+ configPath,
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
336
+ };
337
+ }
338
+ if (action === "add") {
339
+ const suppliedFlagCount = [fingerprint, reason, expiresAt].filter((value) => value !== null).length;
340
+ if (suppliedFlagCount === 0) {
341
+ return {
342
+ action,
343
+ targetPath,
344
+ configPath,
345
+ jsonOutput,
346
+ interactive: true
347
+ };
348
+ }
349
+ if (suppliedFlagCount !== 3 || !fingerprint || !reason || !expiresAt) {
350
+ return {
351
+ message: "suppress add requires --fingerprint, --reason, and --expires-at together.",
352
+ showUsage: false
353
+ };
354
+ }
355
+ return {
356
+ action,
357
+ targetPath,
358
+ configPath,
359
+ jsonOutput,
360
+ interactive: false,
361
+ fingerprint,
362
+ reason,
363
+ expiresAt
364
+ };
365
+ }
366
+ if (fingerprint && indexValue !== null) {
367
+ return {
368
+ message: "Use exactly one of --index or --fingerprint for suppress remove.",
369
+ showUsage: false
370
+ };
371
+ }
372
+ if (!fingerprint && indexValue === null) {
373
+ return {
374
+ action,
375
+ targetPath,
376
+ configPath,
377
+ jsonOutput,
378
+ interactive: true
379
+ };
380
+ }
381
+ if (indexValue !== null) {
382
+ if (!/^\d+$/.test(indexValue)) {
383
+ return {
384
+ message: "--index must be a non-negative integer.",
385
+ showUsage: false
386
+ };
387
+ }
388
+ const parsedIndex = Number(indexValue);
389
+ if (!Number.isSafeInteger(parsedIndex)) {
390
+ return {
391
+ message: "--index must be a non-negative integer.",
392
+ showUsage: false
393
+ };
394
+ }
395
+ return {
396
+ action,
397
+ targetPath,
398
+ configPath,
399
+ jsonOutput,
400
+ interactive: false,
401
+ selector: {
402
+ type: "index",
403
+ index: parsedIndex
404
+ }
405
+ };
406
+ }
407
+ return {
408
+ action,
409
+ targetPath,
410
+ configPath,
411
+ jsonOutput,
412
+ interactive: false,
413
+ selector: {
414
+ type: "fingerprint",
415
+ fingerprint: fingerprint
416
+ }
417
+ };
418
+ }
419
+ function formatLocalDate(date) {
420
+ const year = date.getFullYear();
421
+ const month = String(date.getMonth() + 1).padStart(2, "0");
422
+ const day = String(date.getDate()).padStart(2, "0");
423
+ return `${year}-${month}-${day}`;
424
+ }
425
+ function defaultSuppressionExpiry(now) {
426
+ const expiry = new Date(now.getFullYear(), now.getMonth(), now.getDate());
427
+ expiry.setDate(expiry.getDate() + 30);
428
+ return formatLocalDate(expiry);
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
+ }
462
+ function parseInteractiveSelection(answer, count) {
463
+ const trimmed = answer.trim();
464
+ if (!/^\d+$/.test(trimmed)) {
465
+ return null;
466
+ }
467
+ const selection = Number(trimmed);
468
+ return Number.isSafeInteger(selection) && selection >= 1 && selection <= count
469
+ ? selection - 1
470
+ : null;
471
+ }
472
+ function renderInteractiveFindingCandidates(findings) {
473
+ return findings
474
+ .map((finding, index) => `[${index + 1}] ${finding.severity.toUpperCase()} ${finding.id} - ${finding.message}\n ${finding.fingerprint}`)
475
+ .join("\n");
476
+ }
477
+ function renderInteractiveSuppressionCandidates(suppressions) {
478
+ return suppressions
479
+ .map((suppression, selectionIndex) => {
480
+ const prefix = `[${selectionIndex + 1}] ${suppression.status.toUpperCase()} config index ${suppression.index}`;
481
+ return suppression.status === "invalid"
482
+ ? `${prefix} ${suppression.invalidField}`
483
+ : `${prefix} ${suppression.fingerprint}`;
484
+ })
485
+ .join("\n");
486
+ }
487
+ async function executeSuppressCommand(command, io, options = {}) {
488
+ const writeConfig = options.writeRawDoctorConfigImpl ?? writeRawDoctorConfig;
489
+ if ((command.action === "add" || command.action === "remove") &&
490
+ command.interactive) {
491
+ if (command.jsonOutput) {
492
+ io.writeStderr(`Interactive suppress ${command.action} does not support --json.`);
493
+ return 2;
494
+ }
495
+ if (!io.readStdin) {
496
+ io.writeStderr(`Interactive suppress ${command.action} requires stdin input.`);
497
+ return 2;
498
+ }
499
+ }
500
+ try {
501
+ const rawConfig = await readRawDoctorConfig(command.targetPath, command.configPath);
502
+ const now = options.now?.() ?? new Date();
503
+ if (command.action === "list") {
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;
507
+ io.writeStdout(command.jsonOutput
508
+ ? renderSuppressionListJson(rawConfig.configPath, suppressions)
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
+ }));
533
+ return 0;
534
+ }
535
+ if (command.action === "add") {
536
+ if (command.interactive) {
537
+ listSuppressions(rawConfig.value, now);
538
+ const configuredResult = applyDoctorConfig(await (options.runCheckImpl ?? runCheck)(command.targetPath), await loadDoctorConfig(command.targetPath, command.configPath), { now });
539
+ const candidates = configuredResult.findings.filter((finding) => Boolean(finding.fingerprint) &&
540
+ !finding.id.startsWith("suppression."));
541
+ if (candidates.length === 0) {
542
+ io.writeStderr("No active fingerprinted findings are available to suppress.");
543
+ return 1;
544
+ }
545
+ io.writeStdout(renderInteractiveFindingCandidates(candidates));
546
+ const selection = parseInteractiveSelection(await io.readStdin("Select finding to suppress: "), candidates.length);
547
+ if (selection === null) {
548
+ io.writeStderr(`Selection must be a number from 1 to ${candidates.length}.`);
549
+ return 2;
550
+ }
551
+ const reason = (await io.readStdin("Reason: ")).trim();
552
+ if (!reason) {
553
+ io.writeStderr("Suppression reason must not be blank.");
554
+ return 2;
555
+ }
556
+ const defaultExpiry = defaultSuppressionExpiry(now);
557
+ const expiryAnswer = (await io.readStdin(`Expiration date [${defaultExpiry}]: `)).trim();
558
+ const result = addSuppression(rawConfig.value, {
559
+ fingerprint: candidates[selection].fingerprint,
560
+ reason,
561
+ expiresAt: expiryAnswer || defaultExpiry
562
+ });
563
+ const confirmation = await io.readStdin("Type yes to confirm: ");
564
+ if (confirmation.trim().toLowerCase() !== "yes") {
565
+ io.writeStdout("Suppression add cancelled.");
566
+ return 0;
567
+ }
568
+ await writeConfig(rawConfig.configPath, result.config);
569
+ io.writeStdout(renderSuppressionMutation("suppress.add", rawConfig.configPath, result));
570
+ return 0;
571
+ }
572
+ const result = addSuppression(rawConfig.value, {
573
+ fingerprint: command.fingerprint,
574
+ reason: command.reason,
575
+ expiresAt: command.expiresAt
576
+ });
577
+ await writeConfig(rawConfig.configPath, result.config);
578
+ io.writeStdout(command.jsonOutput
579
+ ? renderSuppressionMutationJson("suppress.add", rawConfig.configPath, result)
580
+ : renderSuppressionMutation("suppress.add", rawConfig.configPath, result));
581
+ return 0;
582
+ }
583
+ if (command.interactive) {
584
+ const suppressions = listSuppressions(rawConfig.value, now);
585
+ if (suppressions.length === 0) {
586
+ io.writeStderr("No suppressions are available to remove.");
587
+ return 1;
588
+ }
589
+ io.writeStdout(renderInteractiveSuppressionCandidates(suppressions));
590
+ const selection = parseInteractiveSelection(await io.readStdin("Select suppression to remove: "), suppressions.length);
591
+ if (selection === null) {
592
+ io.writeStderr(`Selection must be a number from 1 to ${suppressions.length}.`);
593
+ return 2;
594
+ }
595
+ const result = removeSuppressionByIndex(rawConfig.value, suppressions[selection].index);
596
+ const confirmation = await io.readStdin("Type yes to confirm: ");
597
+ if (confirmation.trim().toLowerCase() !== "yes") {
598
+ io.writeStdout("Suppression remove cancelled.");
599
+ return 0;
600
+ }
601
+ await writeConfig(rawConfig.configPath, result.config);
602
+ io.writeStdout(renderSuppressionMutation("suppress.remove", rawConfig.configPath, result));
603
+ return 0;
604
+ }
605
+ const result = command.selector.type === "index"
606
+ ? removeSuppressionByIndex(rawConfig.value, command.selector.index)
607
+ : removeSuppressionByFingerprint(rawConfig.value, command.selector.fingerprint);
608
+ await writeConfig(rawConfig.configPath, result.config);
609
+ io.writeStdout(command.jsonOutput
610
+ ? renderSuppressionMutationJson("suppress.remove", rawConfig.configPath, result)
611
+ : renderSuppressionMutation("suppress.remove", rawConfig.configPath, result));
612
+ return 0;
613
+ }
614
+ catch (error) {
615
+ io.writeStderr(error instanceof Error ? error.message : "Unknown suppression command error.");
616
+ return command.action === "add" &&
617
+ command.interactive &&
618
+ error instanceof SuppressionManagementError &&
619
+ error.code === "suppression_invalid_record"
620
+ ? 2
621
+ : 1;
622
+ }
623
+ }
97
624
  function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
98
625
  if (!commandTarget || commandTarget.startsWith("--")) {
99
626
  return "Missing target path. Usage: codex-plugin-doctor mcp <path> [--json] [--output <path>]";
@@ -1586,6 +2113,21 @@ export async function runCli(args, io = defaultIo, options = {}) {
1586
2113
  io.writeStdout(report);
1587
2114
  return matrixExitCode(matrix);
1588
2115
  }
2116
+ if (command === "suppress") {
2117
+ const parsedSuppressCommand = parseSuppressCommand([maybePath ?? "", ...remainingArgs]);
2118
+ if ("message" in parsedSuppressCommand) {
2119
+ io.writeStderr(parsedSuppressCommand.message);
2120
+ if (parsedSuppressCommand.showUsage) {
2121
+ printSuppressUsage(io);
2122
+ }
2123
+ return 2;
2124
+ }
2125
+ return executeSuppressCommand(parsedSuppressCommand, io, {
2126
+ runCheckImpl: options.runCheckImpl,
2127
+ writeRawDoctorConfigImpl: options.writeRawDoctorConfigImpl,
2128
+ now: options.now
2129
+ });
2130
+ }
1589
2131
  if (command !== "check") {
1590
2132
  printUsage(io);
1591
2133
  return 2;