codex-plugin-doctor 1.26.0 → 1.28.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 +114 -84
- package/dist/core/doctor-config-store.d.ts +12 -0
- package/dist/core/doctor-config-store.js +61 -0
- package/dist/core/doctor-config.d.ts +4 -1
- package/dist/core/doctor-config.js +78 -5
- package/dist/core/npm-package-doctor.js +8 -0
- package/dist/core/output-contract.js +18 -0
- package/dist/core/suppression-management.d.ts +34 -0
- package/dist/core/suppression-management.js +170 -0
- package/dist/core/suppression-record.d.ts +26 -0
- package/dist/core/suppression-record.js +62 -0
- package/dist/domain/types.d.ts +16 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/reporting/render-json-report.js +7 -1
- package/dist/reporting/render-markdown-report.js +35 -15
- package/dist/reporting/render-sarif-report.js +9 -1
- package/dist/reporting/render-suppression-management.d.ts +7 -0
- package/dist/reporting/render-suppression-management.js +117 -0
- package/dist/reporting/render-text-report.js +14 -1
- package/dist/rules/rule-catalog.d.ts +1 -1
- package/dist/rules/rule-catalog.js +18 -0
- package/dist/run-cli.d.ts +3 -0
- package/dist/run-cli.js +411 -1
- package/package.json +64 -64
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 } 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, 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,16 @@ 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
|
+
}
|
|
83
|
+
const suppressUsageText = [
|
|
84
|
+
"Usage:",
|
|
85
|
+
" 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]"
|
|
88
|
+
].join("\n");
|
|
89
|
+
function printSuppressUsage(io) {
|
|
90
|
+
io.writeStderr(suppressUsageText);
|
|
80
91
|
}
|
|
81
92
|
const performanceStageNames = new Set([
|
|
82
93
|
"validation",
|
|
@@ -94,6 +105,390 @@ function parseNonNegativeNumber(value) {
|
|
|
94
105
|
const parsed = Number(value);
|
|
95
106
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
96
107
|
}
|
|
108
|
+
function parseSuppressCommand(args) {
|
|
109
|
+
const [action, targetPath, ...flags] = args;
|
|
110
|
+
if (!action) {
|
|
111
|
+
return {
|
|
112
|
+
message: "Missing suppress action.",
|
|
113
|
+
showUsage: true
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (action !== "add" && action !== "list" && action !== "remove") {
|
|
117
|
+
return {
|
|
118
|
+
message: `Unknown suppress action: ${action}.`,
|
|
119
|
+
showUsage: true
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (!targetPath || targetPath.startsWith("--")) {
|
|
123
|
+
return {
|
|
124
|
+
message: "Missing suppression target path.",
|
|
125
|
+
showUsage: true
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
let configPath = null;
|
|
129
|
+
let jsonOutput = false;
|
|
130
|
+
let fingerprint = null;
|
|
131
|
+
let reason = null;
|
|
132
|
+
let expiresAt = null;
|
|
133
|
+
let indexValue = null;
|
|
134
|
+
const seenFlags = new Set();
|
|
135
|
+
const markSeenFlag = (flag) => {
|
|
136
|
+
if (seenFlags.has(flag)) {
|
|
137
|
+
return {
|
|
138
|
+
message: `Duplicate suppress flag: ${flag}.`,
|
|
139
|
+
showUsage: false
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
seenFlags.add(flag);
|
|
143
|
+
return null;
|
|
144
|
+
};
|
|
145
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
146
|
+
const flag = flags[index];
|
|
147
|
+
if (flag === "--json") {
|
|
148
|
+
const duplicateError = markSeenFlag(flag);
|
|
149
|
+
if (duplicateError) {
|
|
150
|
+
return duplicateError;
|
|
151
|
+
}
|
|
152
|
+
jsonOutput = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (flag === "--config") {
|
|
156
|
+
const duplicateError = markSeenFlag(flag);
|
|
157
|
+
if (duplicateError) {
|
|
158
|
+
return duplicateError;
|
|
159
|
+
}
|
|
160
|
+
const value = flags[index + 1];
|
|
161
|
+
if (!value || value.startsWith("--")) {
|
|
162
|
+
return {
|
|
163
|
+
message: "Missing path after --config.",
|
|
164
|
+
showUsage: false
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
configPath = value;
|
|
168
|
+
index += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (action === "add" && flag === "--fingerprint") {
|
|
172
|
+
const duplicateError = markSeenFlag(flag);
|
|
173
|
+
if (duplicateError) {
|
|
174
|
+
return duplicateError;
|
|
175
|
+
}
|
|
176
|
+
const value = flags[index + 1];
|
|
177
|
+
if (!value || value.startsWith("--")) {
|
|
178
|
+
return {
|
|
179
|
+
message: "Missing fingerprint after --fingerprint.",
|
|
180
|
+
showUsage: false
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
fingerprint = value;
|
|
184
|
+
index += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (action === "add" && flag === "--reason") {
|
|
188
|
+
const duplicateError = markSeenFlag(flag);
|
|
189
|
+
if (duplicateError) {
|
|
190
|
+
return duplicateError;
|
|
191
|
+
}
|
|
192
|
+
const value = flags[index + 1];
|
|
193
|
+
if (!value || value.startsWith("--")) {
|
|
194
|
+
return {
|
|
195
|
+
message: "Missing reason after --reason.",
|
|
196
|
+
showUsage: false
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
reason = value;
|
|
200
|
+
index += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (action === "add" && flag === "--expires-at") {
|
|
204
|
+
const duplicateError = markSeenFlag(flag);
|
|
205
|
+
if (duplicateError) {
|
|
206
|
+
return duplicateError;
|
|
207
|
+
}
|
|
208
|
+
const value = flags[index + 1];
|
|
209
|
+
if (!value || value.startsWith("--")) {
|
|
210
|
+
return {
|
|
211
|
+
message: "Missing date after --expires-at.",
|
|
212
|
+
showUsage: false
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
expiresAt = value;
|
|
216
|
+
index += 1;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (action === "remove" && flag === "--fingerprint") {
|
|
220
|
+
const duplicateError = markSeenFlag(flag);
|
|
221
|
+
if (duplicateError) {
|
|
222
|
+
return duplicateError;
|
|
223
|
+
}
|
|
224
|
+
const value = flags[index + 1];
|
|
225
|
+
if (!value || value.startsWith("--")) {
|
|
226
|
+
return {
|
|
227
|
+
message: "Missing fingerprint after --fingerprint.",
|
|
228
|
+
showUsage: false
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
fingerprint = value;
|
|
232
|
+
index += 1;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (action === "remove" && flag === "--index") {
|
|
236
|
+
const duplicateError = markSeenFlag(flag);
|
|
237
|
+
if (duplicateError) {
|
|
238
|
+
return duplicateError;
|
|
239
|
+
}
|
|
240
|
+
const value = flags[index + 1];
|
|
241
|
+
if (!value || value.startsWith("--")) {
|
|
242
|
+
return {
|
|
243
|
+
message: "Missing value after --index.",
|
|
244
|
+
showUsage: false
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
indexValue = value;
|
|
248
|
+
index += 1;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
message: `Unknown suppress flag: ${flag}.`,
|
|
253
|
+
showUsage: false
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (action === "list") {
|
|
257
|
+
return {
|
|
258
|
+
action,
|
|
259
|
+
targetPath,
|
|
260
|
+
configPath,
|
|
261
|
+
jsonOutput
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (action === "add") {
|
|
265
|
+
const suppliedFlagCount = [fingerprint, reason, expiresAt].filter((value) => value !== null).length;
|
|
266
|
+
if (suppliedFlagCount === 0) {
|
|
267
|
+
return {
|
|
268
|
+
action,
|
|
269
|
+
targetPath,
|
|
270
|
+
configPath,
|
|
271
|
+
jsonOutput,
|
|
272
|
+
interactive: true
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (suppliedFlagCount !== 3 || !fingerprint || !reason || !expiresAt) {
|
|
276
|
+
return {
|
|
277
|
+
message: "suppress add requires --fingerprint, --reason, and --expires-at together.",
|
|
278
|
+
showUsage: false
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
action,
|
|
283
|
+
targetPath,
|
|
284
|
+
configPath,
|
|
285
|
+
jsonOutput,
|
|
286
|
+
interactive: false,
|
|
287
|
+
fingerprint,
|
|
288
|
+
reason,
|
|
289
|
+
expiresAt
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (fingerprint && indexValue !== null) {
|
|
293
|
+
return {
|
|
294
|
+
message: "Use exactly one of --index or --fingerprint for suppress remove.",
|
|
295
|
+
showUsage: false
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (!fingerprint && indexValue === null) {
|
|
299
|
+
return {
|
|
300
|
+
action,
|
|
301
|
+
targetPath,
|
|
302
|
+
configPath,
|
|
303
|
+
jsonOutput,
|
|
304
|
+
interactive: true
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (indexValue !== null) {
|
|
308
|
+
if (!/^\d+$/.test(indexValue)) {
|
|
309
|
+
return {
|
|
310
|
+
message: "--index must be a non-negative integer.",
|
|
311
|
+
showUsage: false
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const parsedIndex = Number(indexValue);
|
|
315
|
+
if (!Number.isSafeInteger(parsedIndex)) {
|
|
316
|
+
return {
|
|
317
|
+
message: "--index must be a non-negative integer.",
|
|
318
|
+
showUsage: false
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
action,
|
|
323
|
+
targetPath,
|
|
324
|
+
configPath,
|
|
325
|
+
jsonOutput,
|
|
326
|
+
interactive: false,
|
|
327
|
+
selector: {
|
|
328
|
+
type: "index",
|
|
329
|
+
index: parsedIndex
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
action,
|
|
335
|
+
targetPath,
|
|
336
|
+
configPath,
|
|
337
|
+
jsonOutput,
|
|
338
|
+
interactive: false,
|
|
339
|
+
selector: {
|
|
340
|
+
type: "fingerprint",
|
|
341
|
+
fingerprint: fingerprint
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function formatLocalDate(date) {
|
|
346
|
+
const year = date.getFullYear();
|
|
347
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
348
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
349
|
+
return `${year}-${month}-${day}`;
|
|
350
|
+
}
|
|
351
|
+
function defaultSuppressionExpiry(now) {
|
|
352
|
+
const expiry = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
353
|
+
expiry.setDate(expiry.getDate() + 30);
|
|
354
|
+
return formatLocalDate(expiry);
|
|
355
|
+
}
|
|
356
|
+
function parseInteractiveSelection(answer, count) {
|
|
357
|
+
const trimmed = answer.trim();
|
|
358
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const selection = Number(trimmed);
|
|
362
|
+
return Number.isSafeInteger(selection) && selection >= 1 && selection <= count
|
|
363
|
+
? selection - 1
|
|
364
|
+
: null;
|
|
365
|
+
}
|
|
366
|
+
function renderInteractiveFindingCandidates(findings) {
|
|
367
|
+
return findings
|
|
368
|
+
.map((finding, index) => `[${index + 1}] ${finding.severity.toUpperCase()} ${finding.id} - ${finding.message}\n ${finding.fingerprint}`)
|
|
369
|
+
.join("\n");
|
|
370
|
+
}
|
|
371
|
+
function renderInteractiveSuppressionCandidates(suppressions) {
|
|
372
|
+
return suppressions
|
|
373
|
+
.map((suppression, selectionIndex) => {
|
|
374
|
+
const prefix = `[${selectionIndex + 1}] ${suppression.status.toUpperCase()} config index ${suppression.index}`;
|
|
375
|
+
return suppression.status === "invalid"
|
|
376
|
+
? `${prefix} ${suppression.invalidField}`
|
|
377
|
+
: `${prefix} ${suppression.fingerprint}`;
|
|
378
|
+
})
|
|
379
|
+
.join("\n");
|
|
380
|
+
}
|
|
381
|
+
async function executeSuppressCommand(command, io, options = {}) {
|
|
382
|
+
const writeConfig = options.writeRawDoctorConfigImpl ?? writeRawDoctorConfig;
|
|
383
|
+
if (command.action !== "list" && command.interactive) {
|
|
384
|
+
if (command.jsonOutput) {
|
|
385
|
+
io.writeStderr(`Interactive suppress ${command.action} does not support --json.`);
|
|
386
|
+
return 2;
|
|
387
|
+
}
|
|
388
|
+
if (!io.readStdin) {
|
|
389
|
+
io.writeStderr(`Interactive suppress ${command.action} requires stdin input.`);
|
|
390
|
+
return 2;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
const rawConfig = await readRawDoctorConfig(command.targetPath, command.configPath);
|
|
395
|
+
const now = options.now?.() ?? new Date();
|
|
396
|
+
if (command.action === "list") {
|
|
397
|
+
const suppressions = listSuppressions(rawConfig.value, now);
|
|
398
|
+
io.writeStdout(command.jsonOutput
|
|
399
|
+
? renderSuppressionListJson(rawConfig.configPath, suppressions)
|
|
400
|
+
: renderSuppressionList(rawConfig.configPath, suppressions));
|
|
401
|
+
return 0;
|
|
402
|
+
}
|
|
403
|
+
if (command.action === "add") {
|
|
404
|
+
if (command.interactive) {
|
|
405
|
+
listSuppressions(rawConfig.value, now);
|
|
406
|
+
const configuredResult = applyDoctorConfig(await (options.runCheckImpl ?? runCheck)(command.targetPath), await loadDoctorConfig(command.targetPath, command.configPath), { now });
|
|
407
|
+
const candidates = configuredResult.findings.filter((finding) => Boolean(finding.fingerprint) &&
|
|
408
|
+
!finding.id.startsWith("suppression."));
|
|
409
|
+
if (candidates.length === 0) {
|
|
410
|
+
io.writeStderr("No active fingerprinted findings are available to suppress.");
|
|
411
|
+
return 1;
|
|
412
|
+
}
|
|
413
|
+
io.writeStdout(renderInteractiveFindingCandidates(candidates));
|
|
414
|
+
const selection = parseInteractiveSelection(await io.readStdin("Select finding to suppress: "), candidates.length);
|
|
415
|
+
if (selection === null) {
|
|
416
|
+
io.writeStderr(`Selection must be a number from 1 to ${candidates.length}.`);
|
|
417
|
+
return 2;
|
|
418
|
+
}
|
|
419
|
+
const reason = (await io.readStdin("Reason: ")).trim();
|
|
420
|
+
if (!reason) {
|
|
421
|
+
io.writeStderr("Suppression reason must not be blank.");
|
|
422
|
+
return 2;
|
|
423
|
+
}
|
|
424
|
+
const defaultExpiry = defaultSuppressionExpiry(now);
|
|
425
|
+
const expiryAnswer = (await io.readStdin(`Expiration date [${defaultExpiry}]: `)).trim();
|
|
426
|
+
const result = addSuppression(rawConfig.value, {
|
|
427
|
+
fingerprint: candidates[selection].fingerprint,
|
|
428
|
+
reason,
|
|
429
|
+
expiresAt: expiryAnswer || defaultExpiry
|
|
430
|
+
});
|
|
431
|
+
const confirmation = await io.readStdin("Type yes to confirm: ");
|
|
432
|
+
if (confirmation.trim().toLowerCase() !== "yes") {
|
|
433
|
+
io.writeStdout("Suppression add cancelled.");
|
|
434
|
+
return 0;
|
|
435
|
+
}
|
|
436
|
+
await writeConfig(rawConfig.configPath, result.config);
|
|
437
|
+
io.writeStdout(renderSuppressionMutation("suppress.add", rawConfig.configPath, result));
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
440
|
+
const result = addSuppression(rawConfig.value, {
|
|
441
|
+
fingerprint: command.fingerprint,
|
|
442
|
+
reason: command.reason,
|
|
443
|
+
expiresAt: command.expiresAt
|
|
444
|
+
});
|
|
445
|
+
await writeConfig(rawConfig.configPath, result.config);
|
|
446
|
+
io.writeStdout(command.jsonOutput
|
|
447
|
+
? renderSuppressionMutationJson("suppress.add", rawConfig.configPath, result)
|
|
448
|
+
: renderSuppressionMutation("suppress.add", rawConfig.configPath, result));
|
|
449
|
+
return 0;
|
|
450
|
+
}
|
|
451
|
+
if (command.interactive) {
|
|
452
|
+
const suppressions = listSuppressions(rawConfig.value, now);
|
|
453
|
+
if (suppressions.length === 0) {
|
|
454
|
+
io.writeStderr("No suppressions are available to remove.");
|
|
455
|
+
return 1;
|
|
456
|
+
}
|
|
457
|
+
io.writeStdout(renderInteractiveSuppressionCandidates(suppressions));
|
|
458
|
+
const selection = parseInteractiveSelection(await io.readStdin("Select suppression to remove: "), suppressions.length);
|
|
459
|
+
if (selection === null) {
|
|
460
|
+
io.writeStderr(`Selection must be a number from 1 to ${suppressions.length}.`);
|
|
461
|
+
return 2;
|
|
462
|
+
}
|
|
463
|
+
const result = removeSuppressionByIndex(rawConfig.value, suppressions[selection].index);
|
|
464
|
+
const confirmation = await io.readStdin("Type yes to confirm: ");
|
|
465
|
+
if (confirmation.trim().toLowerCase() !== "yes") {
|
|
466
|
+
io.writeStdout("Suppression remove cancelled.");
|
|
467
|
+
return 0;
|
|
468
|
+
}
|
|
469
|
+
await writeConfig(rawConfig.configPath, result.config);
|
|
470
|
+
io.writeStdout(renderSuppressionMutation("suppress.remove", rawConfig.configPath, result));
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
const result = command.selector.type === "index"
|
|
474
|
+
? removeSuppressionByIndex(rawConfig.value, command.selector.index)
|
|
475
|
+
: removeSuppressionByFingerprint(rawConfig.value, command.selector.fingerprint);
|
|
476
|
+
await writeConfig(rawConfig.configPath, result.config);
|
|
477
|
+
io.writeStdout(command.jsonOutput
|
|
478
|
+
? renderSuppressionMutationJson("suppress.remove", rawConfig.configPath, result)
|
|
479
|
+
: renderSuppressionMutation("suppress.remove", rawConfig.configPath, result));
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
io.writeStderr(error instanceof Error ? error.message : "Unknown suppression command error.");
|
|
484
|
+
return command.action === "add" &&
|
|
485
|
+
command.interactive &&
|
|
486
|
+
error instanceof SuppressionManagementError &&
|
|
487
|
+
error.code === "suppression_invalid_record"
|
|
488
|
+
? 2
|
|
489
|
+
: 1;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
97
492
|
function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
|
|
98
493
|
if (!commandTarget || commandTarget.startsWith("--")) {
|
|
99
494
|
return "Missing target path. Usage: codex-plugin-doctor mcp <path> [--json] [--output <path>]";
|
|
@@ -1586,6 +1981,21 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1586
1981
|
io.writeStdout(report);
|
|
1587
1982
|
return matrixExitCode(matrix);
|
|
1588
1983
|
}
|
|
1984
|
+
if (command === "suppress") {
|
|
1985
|
+
const parsedSuppressCommand = parseSuppressCommand([maybePath ?? "", ...remainingArgs]);
|
|
1986
|
+
if ("message" in parsedSuppressCommand) {
|
|
1987
|
+
io.writeStderr(parsedSuppressCommand.message);
|
|
1988
|
+
if (parsedSuppressCommand.showUsage) {
|
|
1989
|
+
printSuppressUsage(io);
|
|
1990
|
+
}
|
|
1991
|
+
return 2;
|
|
1992
|
+
}
|
|
1993
|
+
return executeSuppressCommand(parsedSuppressCommand, io, {
|
|
1994
|
+
runCheckImpl: options.runCheckImpl,
|
|
1995
|
+
writeRawDoctorConfigImpl: options.writeRawDoctorConfigImpl,
|
|
1996
|
+
now: options.now
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1589
1999
|
if (command !== "check") {
|
|
1590
2000
|
printUsage(io);
|
|
1591
2001
|
return 2;
|
package/package.json
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "codex-plugin-doctor",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
|
-
"bin": {
|
|
9
|
-
"codex-plugin-doctor": "dist/cli.js"
|
|
10
|
-
},
|
|
11
|
-
"exports": {
|
|
12
|
-
".": {
|
|
13
|
-
"types": "./dist/index.d.ts",
|
|
14
|
-
"import": "./dist/index.js"
|
|
15
|
-
},
|
|
16
|
-
"./package.json": "./package.json"
|
|
17
|
-
},
|
|
18
|
-
"files": [
|
|
19
|
-
"dist",
|
|
20
|
-
"examples"
|
|
21
|
-
],
|
|
22
|
-
"scripts": {
|
|
23
|
-
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
24
|
-
"build": "npm run clean && tsc -p tsconfig.json",
|
|
25
|
-
"dev": "tsx src/cli.ts",
|
|
26
|
-
"generate-validation-artifacts": "node scripts/generate-validation-artifacts.mjs",
|
|
27
|
-
"prepare-rc": "tsx scripts/prepare-release-candidate.ts",
|
|
28
|
-
"prepare-release": "npm test && npm run build && npm pack --dry-run",
|
|
29
|
-
"release-check": "node scripts/release-check.mjs",
|
|
30
|
-
"verify-release-sync": "node scripts/verify-release-sync.mjs",
|
|
31
|
-
"prepublishOnly": "npm test && npm run build",
|
|
32
|
-
"test": "vitest run",
|
|
33
|
-
"test:watch": "vitest"
|
|
34
|
-
},
|
|
35
|
-
"packageManager": "npm@11.11.1",
|
|
36
|
-
"engines": {
|
|
37
|
-
"node": ">=22"
|
|
38
|
-
},
|
|
39
|
-
"keywords": [
|
|
40
|
-
"codex",
|
|
41
|
-
"mcp",
|
|
42
|
-
"plugin",
|
|
43
|
-
"skills",
|
|
44
|
-
"validation"
|
|
45
|
-
],
|
|
46
|
-
"license": "MIT",
|
|
47
|
-
"repository": {
|
|
48
|
-
"type": "git",
|
|
49
|
-
"url": "git+https://github.com/Esquetta/CodexPluginDoctor.git"
|
|
50
|
-
},
|
|
51
|
-
"bugs": {
|
|
52
|
-
"url": "https://github.com/Esquetta/CodexPluginDoctor/issues"
|
|
53
|
-
},
|
|
54
|
-
"homepage": "https://github.com/Esquetta/CodexPluginDoctor#readme",
|
|
55
|
-
"publishConfig": {
|
|
56
|
-
"access": "public"
|
|
57
|
-
},
|
|
58
|
-
"devDependencies": {
|
|
59
|
-
"@types/node": "^24.2.1",
|
|
60
|
-
"tsx": "^4.20.4",
|
|
61
|
-
"typescript": "^5.9.2",
|
|
62
|
-
"vitest": "^3.2.4"
|
|
63
|
-
}
|
|
64
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-plugin-doctor",
|
|
3
|
+
"version": "1.28.0",
|
|
4
|
+
"description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"codex-plugin-doctor": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"examples"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
24
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
25
|
+
"dev": "tsx src/cli.ts",
|
|
26
|
+
"generate-validation-artifacts": "node scripts/generate-validation-artifacts.mjs",
|
|
27
|
+
"prepare-rc": "tsx scripts/prepare-release-candidate.ts",
|
|
28
|
+
"prepare-release": "npm test && npm run build && npm pack --dry-run",
|
|
29
|
+
"release-check": "node scripts/release-check.mjs",
|
|
30
|
+
"verify-release-sync": "node scripts/verify-release-sync.mjs",
|
|
31
|
+
"prepublishOnly": "npm test && npm run build",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest"
|
|
34
|
+
},
|
|
35
|
+
"packageManager": "npm@11.11.1",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"codex",
|
|
41
|
+
"mcp",
|
|
42
|
+
"plugin",
|
|
43
|
+
"skills",
|
|
44
|
+
"validation"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/Esquetta/CodexPluginDoctor.git"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/Esquetta/CodexPluginDoctor/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/Esquetta/CodexPluginDoctor#readme",
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^24.2.1",
|
|
60
|
+
"tsx": "^4.20.4",
|
|
61
|
+
"typescript": "^5.9.2",
|
|
62
|
+
"vitest": "^3.2.4"
|
|
63
|
+
}
|
|
64
|
+
}
|