codex-plugin-doctor 1.28.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/README.md +2 -2
- package/dist/core/output-contract.js +6 -0
- package/dist/core/suppression-management.d.ts +5 -0
- package/dist/core/suppression-management.js +11 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/reporting/render-suppression-management.d.ts +7 -1
- package/dist/reporting/render-suppression-management.js +35 -0
- package/dist/run-cli.js +139 -7
- package/package.json +1 -1
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.
|
|
403
|
+
- uses: Esquetta/CodexPluginDoctor@v1.29.0
|
|
404
404
|
with:
|
|
405
|
-
version: "1.
|
|
405
|
+
version: "1.29.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" &&
|
|
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
|
|
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