codex-plugin-doctor 1.0.2 → 1.1.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
@@ -282,7 +282,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
282
282
 
283
283
  `self-test` runs the bundled runtime-complete sample through static validation, runtime MCP probes, and the compatibility scorecard. It is the fastest post-install check after `npm install -g codex-plugin-doctor`.
284
284
 
285
- `doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor contract` publishes the machine-readable output contract, including public JSON schema surfaces, stable-through-1.0 compatibility metadata, and a frozen rule catalog digest. Add `--json` for automation or `--output output-contract.json` to write the contract to disk. `doctor corpus` runs the bundled validation corpus against healthy runtime, risky security, starter skill, and generic MCP packages, then reports whether each case matched its expected outcome. Add `--json` for automation or `--output validation-corpus.json` to write the corpus report to disk. `doctor npm <package>` runs a preinstall scan by packing the npm package with scripts disabled, extracting the publish tarball, and running validation, security, trust, and recommendation checks against the shipped contents. Use a published Codex plugin package as the target; scanning `codex-plugin-doctor` itself intentionally reports a missing plugin manifest because this CLI package is not a plugin package. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor attest <path>` creates a local attestation with stable package/report digests, validation/security/compatibility/trust summary, and verification metadata. Add `--sign-key-env NAME` to attach a local HMAC-SHA256 signature without printing the secret, or `--json --output attestation.json` to write the artifact to disk. `doctor attest verify <attestation.json> --target <path> --sign-key-env NAME` recomputes the package fingerprint, report digest, and HMAC signature offline; verification intentionally treats `generatedAt`, `targetPath`, `verification`, and `signature.keyHint` as unsigned display metadata. `doctor inspector <path>` builds a safe MCP Inspector launch command from a packaged `.mcp.json` file without starting the Inspector proxy automatically. Use `--server <name>` when the package contains multiple MCP server entries. `doctor diff --before <path> --after <path>` compares two package roots and reports new findings, resolved findings, trust score delta, and whether risk increased. `doctor recommend <path>` turns validation, security, and compatibility signals into a prioritized action plan with blocker, high, medium, and info actions. Add `--json` for automation or `--output recommendations.json` to write the report to disk. `doctor trust <path>` creates a local trust score from package lifecycle scripts, dependency specs, and MCP security findings. Use it before release when you want supply-chain risks summarized as one score. `doctor perf <path>` profiles the shared package analysis pipeline and reports per-stage durations for validation, config, security, compatibility, trust, recommendations, and total runtime. Add `--max-total-ms <ms>` or repeatable `--max-stage-ms stage=ms` to fail CI when a budget is exceeded. `doctor mcp <path>` exposes the generic MCP static health report under the doctor command family without starting local MCP servers. `doctor export --bundle <path>` creates a redacted operator handoff bundle that includes validation JSON, security scorecard data, compatibility matrix, recommendations, and trust score in one file. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
285
+ `doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor contract` publishes the machine-readable output contract, including public JSON schema surfaces, stable-through-1.0 compatibility metadata, and a frozen rule catalog digest. Add `--json` for automation or `--output output-contract.json` to write the contract to disk. `doctor corpus` runs the bundled validation corpus against healthy runtime, risky security, starter skill, and generic MCP packages, then reports whether each case matched its expected outcome. Add `--json` for automation or `--output validation-corpus.json` to write the corpus report to disk. `doctor npm <package>` runs a preinstall scan by packing the npm package with scripts disabled, extracting the publish tarball, and running validation, security, trust, and recommendation checks against the shipped contents. Use a published Codex plugin package as the target; scanning `codex-plugin-doctor` itself intentionally reports a missing plugin manifest because this CLI package is not a plugin package. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor attest <path>` creates a local attestation with stable package/report digests, validation/security/compatibility/trust summary, and verification metadata. Add `--sign-key-env NAME` to attach a local HMAC-SHA256 signature without printing the secret, or `--json --output attestation.json` to write the artifact to disk. `doctor attest verify <attestation.json> --target <path> --sign-key-env NAME` recomputes the package fingerprint, report digest, and HMAC signature offline; verification intentionally treats `generatedAt`, `targetPath`, `verification`, and `signature.keyHint` as unsigned display metadata. `doctor release-evidence <path> --sign-key-env NAME` creates one redacted release bundle with signed attestation, offline verification, corpus, performance, security, trust, package metadata, and git release gates. Strict release evidence requires a clean tagged worktree; use `--allow-dirty` or `--allow-untagged` only for local rehearsal. `doctor release-evidence verify <evidence.json> --target <path> --sign-key-env NAME` verifies a shared release evidence artifact offline against an explicit package path; the artifact target path is treated as display metadata, not trusted input. `doctor release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME` writes a signed release evidence file and prints the `gh release upload` command; add `--upload` to run the upload through GitHub CLI with `--clobber`. `doctor inspector <path>` builds a safe MCP Inspector launch command from a packaged `.mcp.json` file without starting the Inspector proxy automatically. Use `--server <name>` when the package contains multiple MCP server entries. `doctor diff --before <path> --after <path>` compares two package roots and reports new findings, resolved findings, trust score delta, and whether risk increased. `doctor recommend <path>` turns validation, security, and compatibility signals into a prioritized action plan with blocker, high, medium, and info actions. Add `--json` for automation or `--output recommendations.json` to write the report to disk. `doctor trust <path>` creates a local trust score from package lifecycle scripts, dependency specs, and MCP security findings. Use it before release when you want supply-chain risks summarized as one score. `doctor perf <path>` profiles the shared package analysis pipeline and reports per-stage durations for validation, config, security, compatibility, trust, recommendations, and total runtime. Add `--max-total-ms <ms>` or repeatable `--max-stage-ms stage=ms` to fail CI when a budget is exceeded. `doctor mcp <path>` exposes the generic MCP static health report under the doctor command family without starting local MCP servers. `doctor export --bundle <path>` creates a redacted operator handoff bundle that includes validation JSON, security scorecard data, compatibility matrix, recommendations, and trust score in one file. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
286
286
 
287
287
  `audit --installed` runs a local ecosystem audit against every discovered Codex plugin in the installed plugin cache. Add `--security` to include security scorecards, `--compat` to include the all-client compatibility matrix, and `--json --output local-audit.json` when you want a shareable machine-readable report. Add `--cache` to reuse unchanged plugin results between runs; add `--changed` to only report plugins whose fingerprint changed since the last cached audit. Use `--cache-file path/to/audit-cache.json` when CI or scripted runs need an explicit cache location.
288
288
 
@@ -348,9 +348,9 @@ jobs:
348
348
  runs-on: ubuntu-latest
349
349
  steps:
350
350
  - uses: actions/checkout@v5
351
- - uses: Esquetta/CodexPluginDoctor@v1.0.2
351
+ - uses: Esquetta/CodexPluginDoctor@v1.1.0
352
352
  with:
353
- version: "1.0.2"
353
+ version: "1.1.0"
354
354
  path: .
355
355
  runtime: "true"
356
356
  policy: codex-publish
@@ -93,9 +93,11 @@ export interface DoctorAttestationVerificationCheck {
93
93
  }
94
94
  export interface VerifyDoctorAttestationOptions {
95
95
  signingKey: string;
96
+ artifactPath?: string;
96
97
  }
97
98
  export declare function buildDoctorAttestation(targetPath: string, options?: BuildDoctorAttestationOptions): Promise<DoctorAttestation>;
98
99
  export declare function verifyDoctorAttestation(artifactPath: string, targetPath: string, options: VerifyDoctorAttestationOptions): Promise<DoctorAttestationVerificationReport>;
100
+ export declare function verifyDoctorAttestationObject(artifact: unknown, targetPath: string, options: VerifyDoctorAttestationOptions): Promise<DoctorAttestationVerificationReport>;
99
101
  export declare function renderDoctorAttestationJson(attestation: DoctorAttestation): string;
100
102
  export declare function renderDoctorAttestationVerificationJson(report: DoctorAttestationVerificationReport): string;
101
103
  export declare function renderDoctorAttestationVerification(report: DoctorAttestationVerificationReport, options?: {
@@ -298,12 +298,19 @@ function createDigestVerificationCheck(id, message, expected, actual, includeDig
298
298
  export async function verifyDoctorAttestation(artifactPath, targetPath, options) {
299
299
  const resolvedArtifactPath = path.resolve(artifactPath);
300
300
  const artifact = await readJsonFile(resolvedArtifactPath);
301
+ return verifyDoctorAttestationObject(artifact, targetPath, {
302
+ ...options,
303
+ artifactPath: resolvedArtifactPath
304
+ });
305
+ }
306
+ export async function verifyDoctorAttestationObject(artifact, targetPath, options) {
307
+ const artifactPath = options.artifactPath ?? "inline:doctor.attestation";
301
308
  if (!isDoctorAttestation(artifact)) {
302
309
  return {
303
310
  schemaVersion: "1.0.0",
304
311
  kind: "doctor.attestation.verification",
305
312
  generatedAt: new Date().toISOString(),
306
- artifactPath: resolvedArtifactPath,
313
+ artifactPath,
307
314
  targetPath: path.resolve(targetPath),
308
315
  status: "fail",
309
316
  exitCode: 1,
@@ -359,7 +366,7 @@ export async function verifyDoctorAttestation(artifactPath, targetPath, options)
359
366
  schemaVersion: "1.0.0",
360
367
  kind: "doctor.attestation.verification",
361
368
  generatedAt: new Date().toISOString(),
362
- artifactPath: resolvedArtifactPath,
369
+ artifactPath,
363
370
  targetPath: expected.targetPath,
364
371
  status: failedChecks.length === 0 ? "pass" : "fail",
365
372
  exitCode: failedChecks.length === 0 ? 0 : 1,
@@ -15,6 +15,8 @@ export interface DoctorExportBundle {
15
15
  recommendations: DoctorRecommendationsReport;
16
16
  trust: TrustScoreReport;
17
17
  }
18
+ export declare function redactString(value: string): string;
19
+ export declare function redactValue(value: unknown): unknown;
18
20
  export declare function buildDoctorExportBundle(targetPath: string, environment?: CompatibilityEnvironment): Promise<DoctorExportBundle>;
19
21
  export declare function renderDoctorExportBundleJson(bundle: DoctorExportBundle): string;
20
22
  export declare function renderDoctorExportBundle(bundle: DoctorExportBundle, options?: {
@@ -1,12 +1,12 @@
1
1
  import { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./package-analysis.js";
2
- function redactString(value) {
2
+ export function redactString(value) {
3
3
  return value
4
4
  .replace(/sk-[A-Za-z0-9_-]{12,}/g, "[REDACTED_SECRET]")
5
5
  .replace(/npm_[A-Za-z0-9_-]{12,}/g, "[REDACTED_SECRET]")
6
6
  .replace(/gh[pousr]_[A-Za-z0-9_]{12,}/g, "[REDACTED_SECRET]")
7
7
  .replace(/SHOULD_NOT_LEAK/g, "[REDACTED_SECRET]");
8
8
  }
9
- function redactValue(value) {
9
+ export function redactValue(value) {
10
10
  if (typeof value === "string") {
11
11
  return redactString(value);
12
12
  }
@@ -89,6 +89,24 @@ const publicSchemaDefinitions = [
89
89
  outputKind: "doctor.attestation.verification",
90
90
  required: ["schemaVersion", "kind", "generatedAt", "artifactPath", "targetPath", "status", "exitCode", "summary", "unsignedFields", "checks"]
91
91
  },
92
+ {
93
+ id: "doctor.release.evidence.json",
94
+ command: "codex-plugin-doctor doctor release-evidence <path> --json",
95
+ outputKind: "doctor.release.evidence",
96
+ required: ["schemaVersion", "kind", "generatedAt", "version", "targetPath", "status", "exitCode", "releaseReady", "summary", "package", "git", "releaseGates", "attestation", "attestationVerification", "corpus", "performance", "security", "trust", "evidenceSignature"]
97
+ },
98
+ {
99
+ id: "doctor.release.evidence.verification.json",
100
+ command: "codex-plugin-doctor doctor release-evidence verify <evidence.json> --target <path> --json",
101
+ outputKind: "doctor.release.evidence.verification",
102
+ required: ["schemaVersion", "kind", "generatedAt", "artifactPath", "targetPath", "status", "exitCode", "summary", "checks", "attestation"]
103
+ },
104
+ {
105
+ id: "doctor.release.evidence.asset.json",
106
+ command: "codex-plugin-doctor doctor release-evidence asset <path> --tag <tag> --output <evidence.json> --json",
107
+ outputKind: "doctor.release.evidence.asset",
108
+ required: ["schemaVersion", "kind", "generatedAt", "version", "targetPath", "tag", "artifactPath", "status", "exitCode", "uploaded", "uploadCommand", "releaseEvidence"]
109
+ },
92
110
  {
93
111
  id: "doctor.npm.json",
94
112
  command: "codex-plugin-doctor doctor npm <package> --json",
@@ -0,0 +1,129 @@
1
+ import { type DoctorAttestation, type DoctorAttestationVerificationReport } from "./attestation.js";
2
+ import { type DoctorPerformanceReport, type DoctorPerformanceThresholdOptions } from "./performance-report.js";
3
+ import { type DoctorValidationCorpusReport } from "./validation-corpus.js";
4
+ import { type SecurityAudit } from "../security/security-audit.js";
5
+ import { type TrustScoreReport } from "../security/trust-score.js";
6
+ import type { CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
7
+ import type { CheckResult } from "../domain/types.js";
8
+ type EvidenceStatus = "pass" | "warn" | "fail";
9
+ export interface DoctorReleaseEvidenceSignature {
10
+ status: "signed";
11
+ algorithm: "hmac-sha256";
12
+ digest: string;
13
+ payloadDigest: string;
14
+ keyHint: string;
15
+ }
16
+ export interface DoctorReleaseEvidencePackageMetadata {
17
+ name: string | null;
18
+ version: string | null;
19
+ private: boolean | null;
20
+ }
21
+ export interface DoctorReleaseEvidenceGitMetadata {
22
+ commit: string | null;
23
+ tag: string | null;
24
+ dirty: boolean | null;
25
+ }
26
+ export interface DoctorReleaseEvidenceReport {
27
+ schemaVersion: "1.0.0";
28
+ kind: "doctor.release.evidence";
29
+ generatedAt: string;
30
+ version: string;
31
+ targetPath: string;
32
+ status: "pass" | "fail";
33
+ exitCode: 0 | 1;
34
+ releaseReady: boolean;
35
+ summary: {
36
+ attestation: EvidenceStatus;
37
+ attestationVerification: EvidenceStatus;
38
+ corpus: EvidenceStatus;
39
+ performance: EvidenceStatus;
40
+ releaseGates: EvidenceStatus;
41
+ security: EvidenceStatus;
42
+ trust: EvidenceStatus;
43
+ };
44
+ releaseGates: {
45
+ status: EvidenceStatus;
46
+ checks: Array<{
47
+ id: string;
48
+ status: EvidenceStatus;
49
+ message: string;
50
+ }>;
51
+ };
52
+ package: DoctorReleaseEvidencePackageMetadata;
53
+ git: DoctorReleaseEvidenceGitMetadata;
54
+ attestation: DoctorAttestation;
55
+ attestationVerification: DoctorAttestationVerificationReport;
56
+ corpus: DoctorValidationCorpusReport;
57
+ performance: DoctorPerformanceReport;
58
+ security: Pick<SecurityAudit, "status" | "score" | "findingCounts">;
59
+ trust: Pick<TrustScoreReport, "status" | "score" | "findingCounts">;
60
+ evidenceSignature: DoctorReleaseEvidenceSignature;
61
+ }
62
+ export interface DoctorReleaseEvidenceVerificationReport {
63
+ schemaVersion: "1.0.0";
64
+ kind: "doctor.release.evidence.verification";
65
+ generatedAt: string;
66
+ artifactPath: string;
67
+ targetPath: string | null;
68
+ status: "pass" | "fail";
69
+ exitCode: 0 | 1;
70
+ summary: {
71
+ artifact: EvidenceStatus;
72
+ attestation: EvidenceStatus;
73
+ evidenceSignature: EvidenceStatus;
74
+ releaseReady: EvidenceStatus;
75
+ releaseGates: EvidenceStatus;
76
+ };
77
+ checks: Array<{
78
+ id: string;
79
+ status: EvidenceStatus;
80
+ message: string;
81
+ }>;
82
+ attestation: DoctorAttestationVerificationReport | null;
83
+ }
84
+ export interface DoctorReleaseEvidenceAssetReport {
85
+ schemaVersion: "1.0.0";
86
+ kind: "doctor.release.evidence.asset";
87
+ generatedAt: string;
88
+ version: string;
89
+ targetPath: string;
90
+ tag: string;
91
+ artifactPath: string;
92
+ status: "pass" | "fail";
93
+ exitCode: 0 | 1;
94
+ uploaded: boolean;
95
+ uploadCommand: string[];
96
+ releaseEvidence: {
97
+ status: "pass" | "fail";
98
+ releaseReady: boolean;
99
+ evidenceSignature: "signed";
100
+ };
101
+ }
102
+ export interface BuildDoctorReleaseEvidenceOptions {
103
+ signingKey: string;
104
+ signingKeyEnv: string;
105
+ allowDirty?: boolean;
106
+ allowUntagged?: boolean;
107
+ environment?: CompatibilityEnvironment;
108
+ runCheck?: (targetPath: string) => Promise<CheckResult>;
109
+ performanceThresholds?: DoctorPerformanceThresholdOptions;
110
+ }
111
+ export declare function buildDoctorReleaseEvidenceReport(targetPath: string, options: BuildDoctorReleaseEvidenceOptions): Promise<DoctorReleaseEvidenceReport>;
112
+ export declare function renderDoctorReleaseEvidenceJson(report: DoctorReleaseEvidenceReport): string;
113
+ export declare function verifyDoctorReleaseEvidence(artifactPath: string, options: {
114
+ signingKey: string;
115
+ targetPath: string;
116
+ }): Promise<DoctorReleaseEvidenceVerificationReport>;
117
+ export declare function renderDoctorReleaseEvidenceVerificationJson(report: DoctorReleaseEvidenceVerificationReport): string;
118
+ export declare function renderDoctorReleaseEvidenceVerification(report: DoctorReleaseEvidenceVerificationReport, options?: {
119
+ outputPath?: string | null;
120
+ }): string;
121
+ export declare function buildDoctorReleaseEvidenceAssetReport(evidence: DoctorReleaseEvidenceReport, options: {
122
+ tag: string;
123
+ artifactPath: string;
124
+ uploaded: boolean;
125
+ }): DoctorReleaseEvidenceAssetReport;
126
+ export declare function renderDoctorReleaseEvidenceAssetJson(report: DoctorReleaseEvidenceAssetReport): string;
127
+ export declare function renderDoctorReleaseEvidenceAsset(report: DoctorReleaseEvidenceAssetReport): string;
128
+ export declare function renderDoctorReleaseEvidence(report: DoctorReleaseEvidenceReport): string;
129
+ export {};
@@ -0,0 +1,455 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
3
+ import { promisify } from "node:util";
4
+ import path from "node:path";
5
+ import { buildDoctorAttestation, verifyDoctorAttestationObject } from "./attestation.js";
6
+ import { buildDoctorPerformanceReport } from "./performance-report.js";
7
+ import { buildDoctorValidationCorpusReport } from "./validation-corpus.js";
8
+ import { redactValue } from "./doctor-export-bundle.js";
9
+ import { readJsonFile } from "./read-json-file.js";
10
+ import { buildSecurityAudit } from "../security/security-audit.js";
11
+ import { buildTrustScore } from "../security/trust-score.js";
12
+ import { packageVersion } from "../version.js";
13
+ const execFileAsync = promisify(execFile);
14
+ function isPlainObject(value) {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+ function stableStringify(value) {
18
+ if (Array.isArray(value)) {
19
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
20
+ }
21
+ if (isPlainObject(value)) {
22
+ return `{${Object.keys(value)
23
+ .sort()
24
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
25
+ .join(",")}}`;
26
+ }
27
+ return JSON.stringify(value);
28
+ }
29
+ function sha256(value) {
30
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
31
+ }
32
+ function digestMatches(expected, actual) {
33
+ const expectedBuffer = Buffer.from(expected);
34
+ const actualBuffer = Buffer.from(actual);
35
+ return expectedBuffer.length === actualBuffer.length &&
36
+ timingSafeEqual(expectedBuffer, actualBuffer);
37
+ }
38
+ function isDoctorReleaseEvidenceReport(value) {
39
+ return isPlainObject(value) &&
40
+ value.schemaVersion === "1.0.0" &&
41
+ value.kind === "doctor.release.evidence" &&
42
+ typeof value.targetPath === "string" &&
43
+ isPlainObject(value.summary) &&
44
+ isPlainObject(value.releaseGates) &&
45
+ isPlainObject(value.attestation) &&
46
+ isPlainObject(value.evidenceSignature);
47
+ }
48
+ function toEvidenceStatus(status) {
49
+ return status;
50
+ }
51
+ async function readPackageMetadata(rootPath) {
52
+ try {
53
+ const packageJson = await readJsonFile(path.join(rootPath, "package.json"));
54
+ return {
55
+ name: typeof packageJson.name === "string" ? packageJson.name : null,
56
+ version: typeof packageJson.version === "string" ? packageJson.version : null,
57
+ private: typeof packageJson.private === "boolean" ? packageJson.private : null
58
+ };
59
+ }
60
+ catch {
61
+ return {
62
+ name: null,
63
+ version: null,
64
+ private: null
65
+ };
66
+ }
67
+ }
68
+ async function readGitValue(args, cwd) {
69
+ try {
70
+ const { stdout } = await execFileAsync("git", args, { cwd });
71
+ const value = stdout.trim();
72
+ return value.length > 0 ? value : null;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ async function readGitDirty(cwd) {
79
+ try {
80
+ const { stdout } = await execFileAsync("git", ["status", "--short"], { cwd });
81
+ return stdout.trim().length > 0;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ async function readGitMetadata(rootPath) {
88
+ const [commit, tag, dirtyOutput] = await Promise.all([
89
+ readGitValue(["rev-parse", "HEAD"], rootPath),
90
+ readGitValue(["describe", "--tags", "--exact-match"], rootPath),
91
+ readGitDirty(rootPath)
92
+ ]);
93
+ return {
94
+ commit,
95
+ tag,
96
+ dirty: dirtyOutput
97
+ };
98
+ }
99
+ function releaseReady(report) {
100
+ return report.attestation.summary.status !== "fail" &&
101
+ report.attestationVerification.status === "pass" &&
102
+ report.corpus.summary.status === "pass" &&
103
+ report.performance.status === "pass" &&
104
+ report.releaseGates.status === "pass" &&
105
+ report.security.status !== "fail" &&
106
+ report.trust.status !== "fail";
107
+ }
108
+ function buildReleaseGateReport(git, options) {
109
+ const checks = [
110
+ {
111
+ id: "git.commit.present",
112
+ status: git.commit ? "pass" : "fail",
113
+ message: git.commit
114
+ ? "Git commit resolved for this release evidence bundle."
115
+ : "Git commit could not be resolved."
116
+ },
117
+ {
118
+ id: "git.tag.exact",
119
+ status: git.tag || options.allowUntagged ? "pass" : "fail",
120
+ message: git.tag
121
+ ? `Current commit is tagged as ${git.tag}.`
122
+ : options.allowUntagged
123
+ ? "Current commit is not on an exact git tag, but --allow-untagged was explicitly set."
124
+ : "Current commit is not on an exact git tag."
125
+ },
126
+ {
127
+ id: "git.worktree.clean",
128
+ status: git.dirty === false || options.allowDirty ? "pass" : "fail",
129
+ message: git.dirty === false
130
+ ? "Git worktree is clean."
131
+ : options.allowDirty
132
+ ? "Git worktree is dirty, but --allow-dirty was explicitly set."
133
+ : "Git worktree has uncommitted changes."
134
+ }
135
+ ];
136
+ return {
137
+ status: checks.every((check) => check.status === "pass") ? "pass" : "fail",
138
+ checks
139
+ };
140
+ }
141
+ function buildReleaseEvidenceSigningPayload(report) {
142
+ return {
143
+ schemaVersion: report.schemaVersion,
144
+ kind: "doctor.release.evidence.signature.v1",
145
+ version: report.version,
146
+ status: report.status,
147
+ releaseReady: report.releaseReady,
148
+ summary: report.summary,
149
+ package: report.package,
150
+ git: report.git,
151
+ releaseGates: report.releaseGates,
152
+ attestation: {
153
+ version: report.attestation.version,
154
+ subject: report.attestation.subject,
155
+ packageFingerprint: report.attestation.packageFingerprint,
156
+ reportDigest: report.attestation.reportDigest,
157
+ summary: report.attestation.summary,
158
+ signature: report.attestation.signature
159
+ },
160
+ attestationVerification: {
161
+ status: report.attestationVerification.status,
162
+ summary: report.attestationVerification.summary
163
+ },
164
+ corpus: {
165
+ status: report.corpus.summary.status,
166
+ summary: report.corpus.summary
167
+ },
168
+ performance: {
169
+ status: report.performance.status,
170
+ summary: report.performance.summary,
171
+ thresholds: report.performance.thresholds
172
+ },
173
+ security: report.security,
174
+ trust: report.trust
175
+ };
176
+ }
177
+ function signReleaseEvidence(report, signingKey, keyHint) {
178
+ const redactedReport = redactValue(report);
179
+ const serializedPayload = stableStringify(buildReleaseEvidenceSigningPayload(redactedReport));
180
+ return {
181
+ status: "signed",
182
+ algorithm: "hmac-sha256",
183
+ digest: `sha256:${createHmac("sha256", signingKey)
184
+ .update(serializedPayload)
185
+ .digest("hex")}`,
186
+ payloadDigest: sha256(serializedPayload),
187
+ keyHint
188
+ };
189
+ }
190
+ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
191
+ const rootPath = path.resolve(targetPath);
192
+ const security = await buildSecurityAudit(rootPath);
193
+ const [attestation, corpus, performance, trust, packageMetadata, git] = await Promise.all([
194
+ buildDoctorAttestation(rootPath, {
195
+ signingKey: options.signingKey,
196
+ signingKeyHint: `env:${options.signingKeyEnv}`,
197
+ recomputeKeyEnv: options.signingKeyEnv
198
+ }),
199
+ buildDoctorValidationCorpusReport({ environment: options.environment }),
200
+ buildDoctorPerformanceReport(rootPath, {
201
+ environment: options.environment,
202
+ runCheck: options.runCheck,
203
+ thresholds: options.performanceThresholds
204
+ }),
205
+ buildTrustScore(rootPath, { securityAudit: security }),
206
+ readPackageMetadata(rootPath),
207
+ readGitMetadata(rootPath)
208
+ ]);
209
+ const normalizedPackageMetadata = {
210
+ name: packageMetadata.name ?? attestation.subject.name,
211
+ version: packageMetadata.version ?? attestation.subject.version,
212
+ private: packageMetadata.private
213
+ };
214
+ const attestationVerification = await verifyDoctorAttestationObject(attestation, rootPath, {
215
+ signingKey: options.signingKey,
216
+ artifactPath: "inline:doctor.release-evidence.attestation"
217
+ });
218
+ const releaseGates = buildReleaseGateReport(git, options);
219
+ const partialReport = {
220
+ schemaVersion: "1.0.0",
221
+ kind: "doctor.release.evidence",
222
+ generatedAt: new Date().toISOString(),
223
+ version: packageVersion,
224
+ targetPath: rootPath,
225
+ summary: {
226
+ attestation: toEvidenceStatus(attestation.summary.status),
227
+ attestationVerification: attestationVerification.status,
228
+ corpus: corpus.summary.status,
229
+ performance: performance.status,
230
+ releaseGates: releaseGates.status,
231
+ security: toEvidenceStatus(security.status),
232
+ trust: toEvidenceStatus(trust.status)
233
+ },
234
+ package: normalizedPackageMetadata,
235
+ git,
236
+ releaseGates,
237
+ attestation,
238
+ attestationVerification,
239
+ corpus,
240
+ performance,
241
+ security: {
242
+ status: security.status,
243
+ score: security.score,
244
+ findingCounts: security.findingCounts
245
+ },
246
+ trust: {
247
+ status: trust.status,
248
+ score: trust.score,
249
+ findingCounts: trust.findingCounts
250
+ }
251
+ };
252
+ const ready = releaseReady(partialReport);
253
+ const report = {
254
+ ...partialReport,
255
+ status: (ready ? "pass" : "fail"),
256
+ exitCode: (ready ? 0 : 1),
257
+ releaseReady: ready
258
+ };
259
+ return {
260
+ ...report,
261
+ evidenceSignature: signReleaseEvidence(report, options.signingKey, `env:${options.signingKeyEnv}`)
262
+ };
263
+ }
264
+ export function renderDoctorReleaseEvidenceJson(report) {
265
+ return JSON.stringify(redactValue(report), null, 2);
266
+ }
267
+ export async function verifyDoctorReleaseEvidence(artifactPath, options) {
268
+ const resolvedArtifactPath = path.resolve(artifactPath);
269
+ const artifact = await readJsonFile(resolvedArtifactPath);
270
+ if (!isDoctorReleaseEvidenceReport(artifact)) {
271
+ return {
272
+ schemaVersion: "1.0.0",
273
+ kind: "doctor.release.evidence.verification",
274
+ generatedAt: new Date().toISOString(),
275
+ artifactPath: resolvedArtifactPath,
276
+ targetPath: options.targetPath ? path.resolve(options.targetPath) : null,
277
+ status: "fail",
278
+ exitCode: 1,
279
+ summary: {
280
+ artifact: "fail",
281
+ attestation: "fail",
282
+ evidenceSignature: "fail",
283
+ releaseReady: "fail",
284
+ releaseGates: "fail"
285
+ },
286
+ checks: [
287
+ {
288
+ id: "release_evidence.artifact.invalid",
289
+ status: "fail",
290
+ message: "The release evidence artifact is not a valid doctor release evidence bundle."
291
+ }
292
+ ],
293
+ attestation: null
294
+ };
295
+ }
296
+ const targetPath = path.resolve(options.targetPath);
297
+ const attestation = await verifyDoctorAttestationObject(artifact.attestation, targetPath, {
298
+ signingKey: options.signingKey,
299
+ artifactPath: `${resolvedArtifactPath}#attestation`
300
+ });
301
+ const unsignedArtifact = { ...artifact };
302
+ delete unsignedArtifact.evidenceSignature;
303
+ const expectedEvidenceSignature = signReleaseEvidence(unsignedArtifact, options.signingKey, "verification");
304
+ const signatureStatus = artifact.evidenceSignature.status === "signed" &&
305
+ artifact.evidenceSignature.algorithm === "hmac-sha256" &&
306
+ digestMatches(expectedEvidenceSignature.payloadDigest, artifact.evidenceSignature.payloadDigest) &&
307
+ digestMatches(expectedEvidenceSignature.digest, artifact.evidenceSignature.digest)
308
+ ? "pass"
309
+ : "fail";
310
+ const checks = [
311
+ {
312
+ id: "release_evidence.artifact.valid",
313
+ status: "pass",
314
+ message: "The release evidence artifact has the expected schema and kind."
315
+ },
316
+ {
317
+ id: "release_evidence.signature",
318
+ status: signatureStatus,
319
+ message: signatureStatus === "pass"
320
+ ? "The release evidence signature matches the canonical release evidence payload."
321
+ : "The release evidence signature does not match the canonical release evidence payload."
322
+ },
323
+ {
324
+ id: "release_evidence.release_ready",
325
+ status: artifact.releaseReady && artifact.status === "pass" ? "pass" : "fail",
326
+ message: artifact.releaseReady && artifact.status === "pass"
327
+ ? "The release evidence bundle reports releaseReady=true and status=pass."
328
+ : "The release evidence bundle was not release-ready when it was created."
329
+ },
330
+ {
331
+ id: "release_evidence.release_gates",
332
+ status: artifact.releaseGates.status === "pass" &&
333
+ artifact.releaseGates.checks.every((check) => check.status === "pass")
334
+ ? "pass"
335
+ : "fail",
336
+ message: artifact.releaseGates.status === "pass"
337
+ ? "All recorded release gates passed."
338
+ : "One or more recorded release gates failed."
339
+ },
340
+ {
341
+ id: "release_evidence.attestation",
342
+ status: attestation.status,
343
+ message: attestation.status === "pass"
344
+ ? "The embedded signed attestation verifies against the target package."
345
+ : "The embedded signed attestation does not verify against the target package."
346
+ }
347
+ ];
348
+ const failedChecks = checks.filter((check) => check.status === "fail");
349
+ return {
350
+ schemaVersion: "1.0.0",
351
+ kind: "doctor.release.evidence.verification",
352
+ generatedAt: new Date().toISOString(),
353
+ artifactPath: resolvedArtifactPath,
354
+ targetPath,
355
+ status: failedChecks.length === 0 ? "pass" : "fail",
356
+ exitCode: failedChecks.length === 0 ? 0 : 1,
357
+ summary: {
358
+ artifact: "pass",
359
+ attestation: attestation.status,
360
+ evidenceSignature: signatureStatus,
361
+ releaseReady: checks.find((check) => check.id === "release_evidence.release_ready")?.status ?? "fail",
362
+ releaseGates: checks.find((check) => check.id === "release_evidence.release_gates")?.status ?? "fail"
363
+ },
364
+ checks,
365
+ attestation
366
+ };
367
+ }
368
+ export function renderDoctorReleaseEvidenceVerificationJson(report) {
369
+ return JSON.stringify(redactValue(report), null, 2);
370
+ }
371
+ export function renderDoctorReleaseEvidenceVerification(report, options = {}) {
372
+ const lines = [
373
+ "Doctor Release Evidence Verification",
374
+ "====================================",
375
+ `Artifact: ${report.artifactPath}`,
376
+ `Target: ${report.targetPath ?? "unknown"}`,
377
+ `Status: ${report.status.toUpperCase()}`,
378
+ `Attestation: ${report.summary.attestation.toUpperCase()}`,
379
+ `Release ready: ${report.summary.releaseReady.toUpperCase()}`,
380
+ `Release gates: ${report.summary.releaseGates.toUpperCase()}`
381
+ ];
382
+ if (options.outputPath) {
383
+ lines.push(`Output: ${options.outputPath}`);
384
+ }
385
+ lines.push("", "Checks", "------");
386
+ for (const check of report.checks) {
387
+ lines.push(`${check.status === "pass" ? "PASS" : "FAIL"} ${check.id}`);
388
+ lines.push(` ${check.message}`);
389
+ }
390
+ return lines.join("\n");
391
+ }
392
+ export function buildDoctorReleaseEvidenceAssetReport(evidence, options) {
393
+ const artifactPath = path.resolve(options.artifactPath);
394
+ const status = evidence.status === "pass" && evidence.releaseReady
395
+ ? "pass"
396
+ : "fail";
397
+ return {
398
+ schemaVersion: "1.0.0",
399
+ kind: "doctor.release.evidence.asset",
400
+ generatedAt: new Date().toISOString(),
401
+ version: packageVersion,
402
+ targetPath: evidence.targetPath,
403
+ tag: options.tag,
404
+ artifactPath,
405
+ status,
406
+ exitCode: status === "pass" ? 0 : 1,
407
+ uploaded: options.uploaded,
408
+ uploadCommand: [
409
+ "gh",
410
+ "release",
411
+ "upload",
412
+ options.tag,
413
+ artifactPath,
414
+ "--clobber"
415
+ ],
416
+ releaseEvidence: {
417
+ status: evidence.status,
418
+ releaseReady: evidence.releaseReady,
419
+ evidenceSignature: evidence.evidenceSignature.status
420
+ }
421
+ };
422
+ }
423
+ export function renderDoctorReleaseEvidenceAssetJson(report) {
424
+ return JSON.stringify(redactValue(report), null, 2);
425
+ }
426
+ export function renderDoctorReleaseEvidenceAsset(report) {
427
+ return [
428
+ "Doctor Release Evidence Asset",
429
+ "=============================",
430
+ `Target: ${report.targetPath}`,
431
+ `Tag: ${report.tag}`,
432
+ `Artifact: ${report.artifactPath}`,
433
+ `Status: ${report.status.toUpperCase()}`,
434
+ `Uploaded: ${report.uploaded ? "yes" : "no"}`,
435
+ `Upload command: ${report.uploadCommand.join(" ")}`
436
+ ].join("\n");
437
+ }
438
+ export function renderDoctorReleaseEvidence(report) {
439
+ return [
440
+ "Doctor Release Evidence",
441
+ "=======================",
442
+ `Target: ${report.targetPath}`,
443
+ `Status: ${report.status.toUpperCase()}`,
444
+ `Release ready: ${report.releaseReady ? "yes" : "no"}`,
445
+ `Attestation: ${report.summary.attestation.toUpperCase()}`,
446
+ `Attestation verification: ${report.summary.attestationVerification.toUpperCase()}`,
447
+ `Corpus: ${report.summary.corpus.toUpperCase()}`,
448
+ `Performance: ${report.summary.performance.toUpperCase()}`,
449
+ `Release gates: ${report.summary.releaseGates.toUpperCase()}`,
450
+ `Security: ${report.summary.security.toUpperCase()} (${report.security.score}/100)`,
451
+ `Trust: ${report.summary.trust.toUpperCase()} (${report.trust.score}/100)`,
452
+ `Git commit: ${report.git.commit ?? "unknown"}`,
453
+ `Git tag: ${report.git.tag ?? "not tagged"}`
454
+ ].join("\n");
455
+ }
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutp
9
9
  export { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport, type BuildDoctorValidationCorpusOptions, type DoctorValidationCorpusReport, type ValidationCorpusCaseDefinition, type ValidationCorpusCaseResult } from "./core/validation-corpus.js";
10
10
  export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis, type PackageAnalysis, type PackageAnalysisOptions, type PackageAnalysisStage, type PackageAnalysisTiming } from "./core/package-analysis.js";
11
11
  export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson, type BuildDoctorPerformanceReportOptions, type DoctorPerformanceReport, type DoctorPerformanceStage, type DoctorPerformanceStageName } from "./core/performance-report.js";
12
+ export { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence, type BuildDoctorReleaseEvidenceOptions, type DoctorReleaseEvidenceAssetReport, type DoctorReleaseEvidenceGitMetadata, type DoctorReleaseEvidencePackageMetadata, type DoctorReleaseEvidenceReport, type DoctorReleaseEvidenceVerificationReport } from "./core/release-evidence.js";
12
13
  export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson, type BuildDoctorNpmPackageReportOptions, type DoctorNpmPackageReport } from "./core/npm-package-doctor.js";
13
14
  export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson, type BuildDoctorRiskDiffReportOptions, type DoctorRiskDiffReport, type RiskDiffFinding, type RiskFindingCategory } from "./core/risk-diff.js";
14
15
  export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson, type BuildDoctorInspectorReportOptions, type DoctorInspectorReport } from "./core/inspector-bridge.js";
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutp
9
9
  export { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
10
10
  export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./core/package-analysis.js";
11
11
  export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
12
+ export { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence } from "./core/release-evidence.js";
12
13
  export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
13
14
  export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
14
15
  export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson } from "./core/inspector-bridge.js";
package/dist/run-cli.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface CliTerminalContext {
13
13
  export interface RunCliOptions {
14
14
  terminalContext?: CliTerminalContext;
15
15
  runCheckImpl?: typeof runCheck;
16
+ releaseAssetUploadImpl?: (args: string[]) => Promise<void>;
16
17
  resolveLatestVersion?: () => Promise<string>;
17
18
  }
18
19
  export declare function runCli(args: string[], io?: CliIo, options?: RunCliOptions): Promise<number>;
package/dist/run-cli.js CHANGED
@@ -20,6 +20,7 @@ import { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestatio
20
20
  import { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson } from "./core/output-contract.js";
21
21
  import { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
22
22
  import { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
23
+ import { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence } from "./core/release-evidence.js";
23
24
  import { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
24
25
  import { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
25
26
  import { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson } from "./core/inspector-bridge.js";
@@ -69,7 +70,7 @@ const defaultIo = {
69
70
  }
70
71
  };
71
72
  function printUsage(io) {
72
- 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] [--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 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 doctor [npm <package>|contract|corpus|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|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 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");
73
+ 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] [--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 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 doctor [npm <package>|contract|corpus|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]|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 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");
73
74
  }
74
75
  const performanceStageNames = new Set([
75
76
  "validation",
@@ -218,6 +219,17 @@ async function resolveLatestNpmVersion() {
218
219
  });
219
220
  });
220
221
  }
222
+ async function uploadGitHubReleaseAsset(args) {
223
+ return new Promise((resolve, reject) => {
224
+ execFile("gh", args, { shell: process.platform === "win32" }, (error, _stdout, stderr) => {
225
+ if (error) {
226
+ reject(new Error(stderr.trim() || error.message));
227
+ return;
228
+ }
229
+ resolve();
230
+ });
231
+ });
232
+ }
221
233
  function renderUpdateCheck(latestVersion) {
222
234
  const updateAvailable = latestVersion !== packageVersion;
223
235
  return [
@@ -339,6 +351,219 @@ export async function runCli(args, io = defaultIo, options = {}) {
339
351
  io.writeStdout(renderedReport);
340
352
  return report.exitCode;
341
353
  }
354
+ if (maybePath === "release-evidence") {
355
+ if (remainingArgs[0] === "asset") {
356
+ const targetPath = remainingArgs[1] && !remainingArgs[1].startsWith("--")
357
+ ? remainingArgs[1]
358
+ : null;
359
+ const assetFlags = targetPath ? remainingArgs.slice(2) : remainingArgs.slice(1);
360
+ const jsonOutput = assetFlags.includes("--json");
361
+ const upload = assetFlags.includes("--upload");
362
+ const outputIndex = assetFlags.indexOf("--output");
363
+ const outputPath = outputIndex === -1 ? null : assetFlags[outputIndex + 1];
364
+ const tagIndex = assetFlags.indexOf("--tag");
365
+ const tag = tagIndex === -1 ? null : assetFlags[tagIndex + 1];
366
+ const signKeyIndex = assetFlags.indexOf("--sign-key");
367
+ const signKeyEnvIndex = assetFlags.indexOf("--sign-key-env");
368
+ const signKeyEnv = signKeyEnvIndex === -1 ? null : assetFlags[signKeyEnvIndex + 1];
369
+ const allowDirty = assetFlags.includes("--allow-dirty");
370
+ const allowUntagged = assetFlags.includes("--allow-untagged");
371
+ if (!targetPath) {
372
+ io.writeStderr("Missing target path for release evidence asset.");
373
+ return 2;
374
+ }
375
+ if (tagIndex === -1) {
376
+ io.writeStderr("Missing release tag. Use --tag <tag>.");
377
+ return 2;
378
+ }
379
+ if (!tag || tag.startsWith("--")) {
380
+ io.writeStderr("Missing release tag after --tag.");
381
+ return 2;
382
+ }
383
+ if (outputIndex === -1) {
384
+ io.writeStderr("Missing output path. Use --output <path>.");
385
+ return 2;
386
+ }
387
+ if (!outputPath || outputPath.startsWith("--")) {
388
+ io.writeStderr("Missing path after --output.");
389
+ return 2;
390
+ }
391
+ if (signKeyIndex !== -1) {
392
+ io.writeStderr("Use --sign-key-env for release evidence assets; inline signing keys are not supported.");
393
+ return 2;
394
+ }
395
+ if (signKeyEnvIndex === -1) {
396
+ io.writeStderr("Missing signing key. Use --sign-key-env <name>.");
397
+ return 2;
398
+ }
399
+ if (!signKeyEnv || signKeyEnv.startsWith("--")) {
400
+ io.writeStderr("Missing environment variable name after --sign-key-env.");
401
+ return 2;
402
+ }
403
+ const signingKey = terminalContext.env[signKeyEnv];
404
+ if (!signingKey) {
405
+ io.writeStderr(`Environment variable ${signKeyEnv} is not set.`);
406
+ return 2;
407
+ }
408
+ const parsedThresholds = parsePerformanceThresholds(assetFlags);
409
+ if (typeof parsedThresholds === "string") {
410
+ io.writeStderr(parsedThresholds);
411
+ return 2;
412
+ }
413
+ const resolvedOutputPath = path.resolve(outputPath);
414
+ const evidence = await buildDoctorReleaseEvidenceReport(targetPath, {
415
+ signingKey,
416
+ signingKeyEnv: signKeyEnv,
417
+ allowDirty,
418
+ allowUntagged,
419
+ environment: {
420
+ env: terminalContext.env,
421
+ platform: terminalContext.platform
422
+ },
423
+ runCheck: options.runCheckImpl
424
+ ? (pathToCheck) => options.runCheckImpl(pathToCheck)
425
+ : undefined,
426
+ performanceThresholds: parsedThresholds.thresholds
427
+ });
428
+ await writeFile(resolvedOutputPath, renderDoctorReleaseEvidenceJson(evidence), "utf8");
429
+ let uploaded = false;
430
+ const uploadArgs = ["release", "upload", tag, resolvedOutputPath, "--clobber"];
431
+ if (upload && evidence.status === "pass" && evidence.releaseReady) {
432
+ const uploadImpl = options.releaseAssetUploadImpl ?? uploadGitHubReleaseAsset;
433
+ await uploadImpl(uploadArgs);
434
+ uploaded = true;
435
+ }
436
+ const report = buildDoctorReleaseEvidenceAssetReport(evidence, {
437
+ tag,
438
+ artifactPath: resolvedOutputPath,
439
+ uploaded
440
+ });
441
+ const reportJson = renderDoctorReleaseEvidenceAssetJson(report);
442
+ io.writeStdout(jsonOutput ? reportJson : renderDoctorReleaseEvidenceAsset(report));
443
+ return report.exitCode;
444
+ }
445
+ if (remainingArgs[0] === "verify") {
446
+ const artifactPath = remainingArgs[1] && !remainingArgs[1].startsWith("--")
447
+ ? remainingArgs[1]
448
+ : null;
449
+ const verifyFlags = artifactPath ? remainingArgs.slice(2) : remainingArgs.slice(1);
450
+ const jsonOutput = verifyFlags.includes("--json");
451
+ const outputIndex = verifyFlags.indexOf("--output");
452
+ const outputPath = outputIndex === -1 ? null : verifyFlags[outputIndex + 1];
453
+ const targetIndex = verifyFlags.indexOf("--target");
454
+ const targetPath = targetIndex === -1 ? null : verifyFlags[targetIndex + 1];
455
+ const signKeyIndex = verifyFlags.indexOf("--sign-key");
456
+ const signKeyEnvIndex = verifyFlags.indexOf("--sign-key-env");
457
+ const signKeyEnv = signKeyEnvIndex === -1 ? null : verifyFlags[signKeyEnvIndex + 1];
458
+ if (!artifactPath) {
459
+ io.writeStderr("Missing release evidence artifact path.");
460
+ return 2;
461
+ }
462
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
463
+ io.writeStderr("Missing path after --output.");
464
+ return 2;
465
+ }
466
+ if (targetIndex !== -1 && (!targetPath || targetPath.startsWith("--"))) {
467
+ io.writeStderr("Missing path after --target.");
468
+ return 2;
469
+ }
470
+ if (targetIndex === -1) {
471
+ io.writeStderr("Missing target path. Use --target <path>.");
472
+ return 2;
473
+ }
474
+ if (signKeyIndex !== -1) {
475
+ io.writeStderr("Use --sign-key-env for release evidence verification; inline signing keys are not supported.");
476
+ return 2;
477
+ }
478
+ if (signKeyEnvIndex === -1) {
479
+ io.writeStderr("Missing signing key. Use --sign-key-env <name>.");
480
+ return 2;
481
+ }
482
+ if (!signKeyEnv || signKeyEnv.startsWith("--")) {
483
+ io.writeStderr("Missing environment variable name after --sign-key-env.");
484
+ return 2;
485
+ }
486
+ const signingKey = terminalContext.env[signKeyEnv];
487
+ if (!signingKey) {
488
+ io.writeStderr(`Environment variable ${signKeyEnv} is not set.`);
489
+ return 2;
490
+ }
491
+ const report = await verifyDoctorReleaseEvidence(artifactPath, {
492
+ signingKey,
493
+ targetPath: targetPath
494
+ });
495
+ const reportJson = renderDoctorReleaseEvidenceVerificationJson(report);
496
+ const renderedReport = jsonOutput
497
+ ? reportJson
498
+ : renderDoctorReleaseEvidenceVerification(report, { outputPath });
499
+ if (outputPath) {
500
+ await writeFile(outputPath, reportJson, "utf8");
501
+ }
502
+ io.writeStdout(renderedReport);
503
+ return report.exitCode;
504
+ }
505
+ const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
506
+ ? remainingArgs[0]
507
+ : ".";
508
+ const evidenceFlags = remainingArgs[0] && !remainingArgs[0].startsWith("--")
509
+ ? remainingArgs.slice(1)
510
+ : remainingArgs;
511
+ const jsonOutput = evidenceFlags.includes("--json");
512
+ const outputIndex = evidenceFlags.indexOf("--output");
513
+ const outputPath = outputIndex === -1 ? null : evidenceFlags[outputIndex + 1];
514
+ const signKeyIndex = evidenceFlags.indexOf("--sign-key");
515
+ const signKeyEnvIndex = evidenceFlags.indexOf("--sign-key-env");
516
+ const signKeyEnv = signKeyEnvIndex === -1 ? null : evidenceFlags[signKeyEnvIndex + 1];
517
+ const allowDirty = evidenceFlags.includes("--allow-dirty");
518
+ const allowUntagged = evidenceFlags.includes("--allow-untagged");
519
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
520
+ io.writeStderr("Missing path after --output.");
521
+ return 2;
522
+ }
523
+ if (signKeyIndex !== -1) {
524
+ io.writeStderr("Use --sign-key-env for release evidence; inline signing keys are not supported.");
525
+ return 2;
526
+ }
527
+ if (signKeyEnvIndex === -1) {
528
+ io.writeStderr("Missing signing key. Use --sign-key-env <name>.");
529
+ return 2;
530
+ }
531
+ if (!signKeyEnv || signKeyEnv.startsWith("--")) {
532
+ io.writeStderr("Missing environment variable name after --sign-key-env.");
533
+ return 2;
534
+ }
535
+ const signingKey = terminalContext.env[signKeyEnv];
536
+ if (!signingKey) {
537
+ io.writeStderr(`Environment variable ${signKeyEnv} is not set.`);
538
+ return 2;
539
+ }
540
+ const parsedThresholds = parsePerformanceThresholds(evidenceFlags);
541
+ if (typeof parsedThresholds === "string") {
542
+ io.writeStderr(parsedThresholds);
543
+ return 2;
544
+ }
545
+ const report = await buildDoctorReleaseEvidenceReport(targetPath, {
546
+ signingKey,
547
+ signingKeyEnv: signKeyEnv,
548
+ allowDirty,
549
+ allowUntagged,
550
+ environment: {
551
+ env: terminalContext.env,
552
+ platform: terminalContext.platform
553
+ },
554
+ runCheck: options.runCheckImpl
555
+ ? (pathToCheck) => options.runCheckImpl(pathToCheck)
556
+ : undefined,
557
+ performanceThresholds: parsedThresholds.thresholds
558
+ });
559
+ const reportJson = renderDoctorReleaseEvidenceJson(report);
560
+ const renderedReport = jsonOutput ? reportJson : renderDoctorReleaseEvidence(report);
561
+ if (outputPath) {
562
+ await writeFile(outputPath, reportJson, "utf8");
563
+ }
564
+ io.writeStdout(renderedReport);
565
+ return report.exitCode;
566
+ }
342
567
  if (maybePath === "corpus") {
343
568
  const jsonOutput = remainingArgs.includes("--json");
344
569
  const outputIndex = remainingArgs.indexOf("--output");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.0.2",
3
+ "version": "1.1.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",