codex-plugin-doctor 1.54.0 → 1.56.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
@@ -108,6 +108,32 @@ codex-plugin-doctor registry inspect io.github.example/weather --allow-network
108
108
 
109
109
  Local checks validate metadata, ownership consistency, package integrity, transports, and Codex installability. Exact Registry lookup is read-only, requires explicit network consent, and contacts only the fixed official Registry endpoint. It never follows advertised package, icon, repository, or remote MCP URLs. See [MCP Registry Readiness](./docs/architecture/mcp-registry-readiness.md).
110
110
 
111
+ ### MCP Registry Publication Preflight
112
+
113
+ Check a publication candidate locally without making a network request:
114
+
115
+ ```bash
116
+ codex-plugin-doctor registry preflight path/to/server.json --json
117
+ ```
118
+
119
+ Opt in to bounded public npm and MCP Registry metadata checks:
120
+
121
+ ```bash
122
+ codex-plugin-doctor registry preflight path/to/server.json --allow-network --json
123
+ ```
124
+
125
+ Require a publish-ready result in CI:
126
+
127
+ ```bash
128
+ codex-plugin-doctor registry preflight path/to/server.json --allow-network --require-publish-ready --json
129
+ ```
130
+
131
+ Preflight checks the exact npm package version, then the exact Registry version
132
+ and latest Registry record on fixed public hosts. An existing exact Registry
133
+ version is an immutable collision and must be replaced by a version bump. The
134
+ command is advisory: it never authenticates, publishes, or changes npm or
135
+ Registry records. See [MCP Registry Publication Preflight](./docs/architecture/mcp-registry-publication-preflight.md).
136
+
111
137
  Output formats:
112
138
 
113
139
  - human text output
@@ -454,9 +480,9 @@ jobs:
454
480
  runs-on: ubuntu-latest
455
481
  steps:
456
482
  - uses: actions/checkout@v5
457
- - uses: Esquetta/CodexPluginDoctor@v1.54.0
483
+ - uses: Esquetta/CodexPluginDoctor@v1.56.0
458
484
  with:
459
- version: "1.54.0"
485
+ version: "1.56.0"
460
486
  path: .
461
487
  runtime: "true"
462
488
  policy: codex-publish
@@ -0,0 +1,16 @@
1
+ export interface DoctorSizeReport {
2
+ targetPath: string;
3
+ status: "pass" | "warn" | "fail";
4
+ totalSize: number;
5
+ totalSizeHuman: string;
6
+ fileCount: number;
7
+ largeFiles: Array<{
8
+ path: string;
9
+ size: number;
10
+ sizeHuman: string;
11
+ }>;
12
+ warnings: string[];
13
+ }
14
+ export declare function buildDoctorSize(targetPath: string): Promise<DoctorSizeReport>;
15
+ export declare function renderDoctorSize(report: DoctorSizeReport): string;
16
+ export declare function renderDoctorSizeJson(report: DoctorSizeReport): string;
@@ -0,0 +1,139 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const skippedDirs = new Set([
4
+ "node_modules",
5
+ ".git",
6
+ ".cache",
7
+ "dist",
8
+ "coverage",
9
+ "__pycache__",
10
+ ".turbo",
11
+ ".next",
12
+ ".nuxt"
13
+ ]);
14
+ const largeFileThreshold = 1024 * 1024;
15
+ const packageSizeWarnThreshold = 10 * 1024 * 1024;
16
+ const packageSizeFailThreshold = 50 * 1024 * 1024;
17
+ function formatSize(bytes) {
18
+ if (bytes < 1024)
19
+ return `${bytes} B`;
20
+ if (bytes < 1024 * 1024)
21
+ return `${(bytes / 1024).toFixed(1)} KB`;
22
+ if (bytes < 1024 * 1024 * 1024)
23
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
24
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
25
+ }
26
+ async function scanDirectory(rootPath, currentPath = rootPath) {
27
+ let totalSize = 0;
28
+ let fileCount = 0;
29
+ const largeFiles = [];
30
+ let entries;
31
+ try {
32
+ entries = await readdir(currentPath, { withFileTypes: true });
33
+ }
34
+ catch {
35
+ return { totalSize: 0, fileCount: 0, largeFiles: [] };
36
+ }
37
+ for (const entry of entries) {
38
+ const entryPath = path.join(currentPath, entry.name);
39
+ if (entry.isDirectory()) {
40
+ if (skippedDirs.has(entry.name)) {
41
+ continue;
42
+ }
43
+ const subResult = await scanDirectory(rootPath, entryPath);
44
+ totalSize += subResult.totalSize;
45
+ fileCount += subResult.fileCount;
46
+ largeFiles.push(...subResult.largeFiles);
47
+ continue;
48
+ }
49
+ if (!entry.isFile()) {
50
+ continue;
51
+ }
52
+ let details;
53
+ try {
54
+ details = await stat(entryPath);
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ totalSize += details.size;
60
+ fileCount += 1;
61
+ if (details.size >= largeFileThreshold) {
62
+ largeFiles.push({
63
+ path: path.relative(rootPath, entryPath).replace(/\\/g, "/"),
64
+ size: details.size,
65
+ sizeHuman: formatSize(details.size)
66
+ });
67
+ }
68
+ }
69
+ return { totalSize, fileCount, largeFiles };
70
+ }
71
+ export async function buildDoctorSize(targetPath) {
72
+ const resolvedPath = path.resolve(targetPath);
73
+ const { totalSize, fileCount, largeFiles } = await scanDirectory(resolvedPath);
74
+ const warnings = [];
75
+ let status = "pass";
76
+ if (totalSize >= packageSizeFailThreshold) {
77
+ status = "fail";
78
+ warnings.push(`Package size (${formatSize(totalSize)}) exceeds ${formatSize(packageSizeFailThreshold)}.`);
79
+ }
80
+ else if (totalSize >= packageSizeWarnThreshold) {
81
+ status = "warn";
82
+ warnings.push(`Package size (${formatSize(totalSize)}) exceeds ${formatSize(packageSizeWarnThreshold)}.`);
83
+ }
84
+ if (largeFiles.length > 0) {
85
+ if (status === "pass")
86
+ status = "warn";
87
+ warnings.push(`${largeFiles.length} file(s) exceed ${formatSize(largeFileThreshold)}.`);
88
+ }
89
+ largeFiles.sort((a, b) => b.size - a.size);
90
+ return {
91
+ targetPath: resolvedPath,
92
+ status,
93
+ totalSize,
94
+ totalSizeHuman: formatSize(totalSize),
95
+ fileCount,
96
+ largeFiles: largeFiles.slice(0, 10),
97
+ warnings
98
+ };
99
+ }
100
+ export function renderDoctorSize(report) {
101
+ const lines = [
102
+ "Codex Plugin Doctor Size Analysis",
103
+ "=================================",
104
+ `Path: ${report.targetPath}`,
105
+ `Status: ${report.status.toUpperCase()}`,
106
+ `Total size: ${report.totalSizeHuman} (${report.fileCount} files)`,
107
+ ""
108
+ ];
109
+ if (report.warnings.length > 0) {
110
+ lines.push("Warnings", "--------");
111
+ for (const warning of report.warnings) {
112
+ lines.push(` - ${warning}`);
113
+ }
114
+ lines.push("");
115
+ }
116
+ if (report.largeFiles.length > 0) {
117
+ lines.push("Large Files", "-----------");
118
+ for (const file of report.largeFiles) {
119
+ lines.push(` ${file.sizeHuman.padEnd(8)} ${file.path}`);
120
+ }
121
+ lines.push("");
122
+ }
123
+ if (report.status === "pass") {
124
+ lines.push("Package size within acceptable limits.");
125
+ }
126
+ return lines.join("\n");
127
+ }
128
+ export function renderDoctorSizeJson(report) {
129
+ return JSON.stringify({
130
+ schemaVersion: "1.0.0",
131
+ targetPath: report.targetPath,
132
+ status: report.status,
133
+ totalSize: report.totalSize,
134
+ totalSizeHuman: report.totalSizeHuman,
135
+ fileCount: report.fileCount,
136
+ largeFiles: report.largeFiles,
137
+ warnings: report.warnings
138
+ }, null, 2);
139
+ }
@@ -0,0 +1,38 @@
1
+ import { type BoundedHttpRequestOptions, type BoundedHttpResponse } from "./bounded-http-client.js";
2
+ export type McpRegistryPublicationPreflightStatus = "pass" | "warn" | "fail";
3
+ type RegistryRequest = (url: string, options?: BoundedHttpRequestOptions) => Promise<BoundedHttpResponse>;
4
+ export interface McpRegistryPublicationPreflightFinding {
5
+ id: string;
6
+ severity: "warn" | "fail";
7
+ message: string;
8
+ }
9
+ export interface McpRegistryPublicationPreflightReport {
10
+ schemaVersion: "1.0.0";
11
+ kind: "mcp-registry-publication-preflight";
12
+ generatedAt: string;
13
+ target: "server.json";
14
+ serverName?: string;
15
+ serverVersion?: string;
16
+ status: McpRegistryPublicationPreflightStatus;
17
+ localReadiness: McpRegistryPublicationPreflightStatus;
18
+ packagePublication: "pass" | "fail" | "skipped" | "unknown";
19
+ registryVersionAvailability: "available-first-publication" | "available-new-version" | "already-published" | "unknown";
20
+ publisherPlan: {
21
+ executable: false;
22
+ steps: Array<{
23
+ order: number;
24
+ command: "mcp-publisher login github" | "mcp-publisher publish";
25
+ purpose: string;
26
+ }>;
27
+ };
28
+ findings: McpRegistryPublicationPreflightFinding[];
29
+ }
30
+ export interface BuildMcpRegistryPublicationPreflightOptions {
31
+ allowNetwork?: boolean;
32
+ request?: RegistryRequest;
33
+ }
34
+ export declare function buildMcpRegistryPublicationPreflight(targetPath: string, options?: BuildMcpRegistryPublicationPreflightOptions): Promise<McpRegistryPublicationPreflightReport>;
35
+ export declare function renderMcpRegistryPublicationPreflightJson(report: McpRegistryPublicationPreflightReport): string;
36
+ export declare function renderMcpRegistryPublicationPreflight(report: McpRegistryPublicationPreflightReport): string;
37
+ export declare function registryPublicationPreflightExitCode(report: McpRegistryPublicationPreflightReport, requirePublishReady?: boolean): 0 | 1;
38
+ export {};
@@ -0,0 +1,358 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { buildMcpRegistryReadiness } from "./mcp-registry.js";
4
+ import { requestBoundedHttp } from "./bounded-http-client.js";
5
+ const SERVER_NAME_PATTERN = /^[a-zA-Z0-9.-]+\/[a-zA-Z0-9._-]+$/;
6
+ const VERSION_RANGE_PATTERN = /^(?:latest|[~^]|[<>]=?)|(?:\s+\|\|\s+)|(?:^|[.\s])[x*](?:$|[.\s])/i;
7
+ const OFFICIAL_SCHEMA_PATTERN = /^https:\/\/static\.modelcontextprotocol\.io\/schemas\/\d{4}-\d{2}-\d{2}\/server\.schema\.json$/;
8
+ const NPM_IDENTIFIER_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]{0,213}\/)?[a-z0-9][a-z0-9._-]{0,213}$/;
9
+ const INTEGRITY_PATTERN = /^(sha256|sha384|sha512)-([A-Za-z0-9+/]*={0,2})$/;
10
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
11
+ const NPM_REGISTRY = "https://registry.npmjs.org";
12
+ const MCP_REGISTRY = "https://registry.modelcontextprotocol.io";
13
+ const REQUEST_OPTIONS = {
14
+ method: "GET",
15
+ headers: {
16
+ accept: "application/json",
17
+ "user-agent": "codex-plugin-doctor"
18
+ }
19
+ };
20
+ function isRecord(value) {
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ }
23
+ function publicFinding(finding) {
24
+ return {
25
+ id: finding.id,
26
+ severity: finding.severity,
27
+ message: finding.message
28
+ };
29
+ }
30
+ function statusFromFindings(findings) {
31
+ if (findings.some((finding) => finding.severity === "fail")) {
32
+ return "fail";
33
+ }
34
+ return findings.some((finding) => finding.severity === "warn") ? "warn" : "pass";
35
+ }
36
+ function isSafeServerName(value) {
37
+ return value !== undefined && value.length >= 3 && value.length <= 200 && SERVER_NAME_PATTERN.test(value);
38
+ }
39
+ function isSafeServerVersion(value) {
40
+ return value !== undefined && value.length >= 1 && value.length <= 255
41
+ && !/[\\/\u0000-\u001F\u007F]/.test(value) && !VERSION_RANGE_PATTERN.test(value);
42
+ }
43
+ function isSafeNpmIdentifier(value) {
44
+ return typeof value === "string" && NPM_IDENTIFIER_PATTERN.test(value);
45
+ }
46
+ function isIntegrity(value) {
47
+ if (typeof value !== "string") {
48
+ return false;
49
+ }
50
+ const match = INTEGRITY_PATTERN.exec(value);
51
+ if (!match || match[2].length % 4 !== 0) {
52
+ return false;
53
+ }
54
+ const digest = Buffer.from(match[2], "base64");
55
+ const expectedBytes = match[1] === "sha256" ? 32 : match[1] === "sha384" ? 48 : 64;
56
+ return digest.length === expectedBytes && digest.toString("base64") === match[2];
57
+ }
58
+ function responseJson(response) {
59
+ try {
60
+ return JSON.parse(response.body.toString("utf8"));
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ function validNotFound(response) {
67
+ const payload = responseJson(response);
68
+ if (response.statusCode !== 404 || !isRecord(payload)) {
69
+ return false;
70
+ }
71
+ return (typeof payload.error === "string" && payload.error.trim().length > 0)
72
+ || (payload.title === "Not Found"
73
+ && payload.status === 404
74
+ && typeof payload.detail === "string"
75
+ && payload.detail.trim().length > 0);
76
+ }
77
+ function hasValidHttpUrl(value) {
78
+ if (typeof value !== "string" || value.length === 0 || /\s/.test(value)) {
79
+ return false;
80
+ }
81
+ try {
82
+ const url = new URL(value.replace(/\{[^{}]+\}/g, "template-value"));
83
+ return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ function hasValidPackageShape(value) {
90
+ if (!isRecord(value)
91
+ || typeof value.registryType !== "string" || value.registryType.length === 0
92
+ || typeof value.identifier !== "string" || value.identifier.length === 0
93
+ || !isRecord(value.transport)
94
+ || !["stdio", "streamable-http", "sse"].includes(value.transport.type)) {
95
+ return false;
96
+ }
97
+ if (/^https?:\/\//i.test(value.identifier) && !hasValidHttpUrl(value.identifier)) {
98
+ return false;
99
+ }
100
+ if (typeof value.version === "string" && VERSION_RANGE_PATTERN.test(value.version)) {
101
+ return false;
102
+ }
103
+ return value.registryType !== "mcpb" || (typeof value.fileSha256 === "string" && SHA256_PATTERN.test(value.fileSha256));
104
+ }
105
+ function hasValidRemoteShape(value) {
106
+ return isRecord(value)
107
+ && (value.type === "streamable-http" || value.type === "sse")
108
+ && hasValidHttpUrl(value.url);
109
+ }
110
+ function hasValidRegistryServerShape(server) {
111
+ return typeof server.$schema === "string"
112
+ && OFFICIAL_SCHEMA_PATTERN.test(server.$schema)
113
+ && isSafeServerName(typeof server.name === "string" ? server.name : undefined)
114
+ && isSafeServerVersion(typeof server.version === "string" ? server.version : undefined)
115
+ && typeof server.description === "string"
116
+ && server.description.length >= 1
117
+ && server.description.length <= 100
118
+ && (server.packages === undefined || (Array.isArray(server.packages) && server.packages.every(hasValidPackageShape)))
119
+ && (server.remotes === undefined || (Array.isArray(server.remotes) && server.remotes.every(hasValidRemoteShape)));
120
+ }
121
+ function matchingRegistryServer(response, serverName, serverVersion) {
122
+ const payload = responseJson(response);
123
+ if (response.statusCode !== 200 || !isRecord(payload) || !isRecord(payload.server)) {
124
+ return false;
125
+ }
126
+ const server = payload.server;
127
+ return hasValidRegistryServerShape(server)
128
+ && server.name === serverName
129
+ && (serverVersion === undefined || server.version === serverVersion);
130
+ }
131
+ function matchingNpmPackument(response, packageName, packageVersion, serverName) {
132
+ const payload = responseJson(response);
133
+ if (response.statusCode !== 200 || !isRecord(payload) || payload.name !== packageName || !isRecord(payload.versions)) {
134
+ return false;
135
+ }
136
+ const version = payload.versions[packageVersion];
137
+ return isRecord(version)
138
+ && version.name === packageName
139
+ && version.version === packageVersion
140
+ && version.mcpName === serverName
141
+ && isRecord(version.dist)
142
+ && isIntegrity(version.dist.integrity);
143
+ }
144
+ function addFinding(findings, id, message) {
145
+ findings.push({ id, severity: "fail", message });
146
+ }
147
+ async function resolveServerJsonPath(targetPath) {
148
+ const resolved = path.resolve(targetPath);
149
+ try {
150
+ return (await stat(resolved)).isDirectory() ? path.join(resolved, "server.json") : resolved;
151
+ }
152
+ catch {
153
+ return resolved.endsWith(".json") ? resolved : path.join(resolved, "server.json");
154
+ }
155
+ }
156
+ async function readJson(pathname) {
157
+ try {
158
+ const parsed = JSON.parse(await readFile(pathname, "utf8"));
159
+ return isRecord(parsed) ? parsed : null;
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ }
165
+ function npmDeclarations(server) {
166
+ return Array.isArray(server?.packages)
167
+ ? server.packages.filter((entry) => isRecord(entry) && entry.registryType === "npm")
168
+ : [];
169
+ }
170
+ function publisherPlan() {
171
+ return {
172
+ executable: false,
173
+ steps: [
174
+ {
175
+ order: 1,
176
+ command: "mcp-publisher login github",
177
+ purpose: "Authenticate with GitHub for Registry publication."
178
+ },
179
+ {
180
+ order: 2,
181
+ command: "mcp-publisher publish",
182
+ purpose: "Publish the validated Registry metadata."
183
+ }
184
+ ]
185
+ };
186
+ }
187
+ export async function buildMcpRegistryPublicationPreflight(targetPath, options = {}) {
188
+ void options;
189
+ const readiness = await buildMcpRegistryReadiness(targetPath);
190
+ const findings = readiness.findings.map(publicFinding);
191
+ const serverJsonPath = await resolveServerJsonPath(targetPath);
192
+ const server = await readJson(serverJsonPath);
193
+ const declarations = npmDeclarations(server);
194
+ const packageJson = await readJson(path.join(path.dirname(serverJsonPath), "package.json"));
195
+ if (declarations.length > 1) {
196
+ findings.push({
197
+ id: "registry.preflight.package.multiple-npm-declarations",
198
+ severity: "fail",
199
+ message: "Publication evidence supports exactly one npm package declaration."
200
+ });
201
+ }
202
+ else if (declarations.length === 1
203
+ && typeof declarations[0].identifier === "string"
204
+ && packageJson
205
+ && packageJson.name !== declarations[0].identifier) {
206
+ findings.push({
207
+ id: "registry.preflight.package.local-name-mismatch",
208
+ severity: "fail",
209
+ message: "package.json name must match the declared npm package identifier."
210
+ });
211
+ }
212
+ if (declarations.length === 1 && !isSafeNpmIdentifier(declarations[0].identifier)) {
213
+ findings.push({
214
+ id: "registry.preflight.package.invalid-npm-identifier",
215
+ severity: "fail",
216
+ message: "The declared npm package identifier is not valid for public registry verification."
217
+ });
218
+ }
219
+ const localReadiness = statusFromFindings(findings);
220
+ let packagePublication = declarations.length === 0
221
+ ? "skipped"
222
+ : localReadiness === "fail" ? "fail" : "unknown";
223
+ let registryVersionAvailability = "unknown";
224
+ const declaration = declarations.length === 1 ? declarations[0] : undefined;
225
+ const serverName = isSafeServerName(readiness.serverName) ? readiness.serverName : undefined;
226
+ const serverVersion = isSafeServerVersion(readiness.serverVersion) ? readiness.serverVersion : undefined;
227
+ const packageName = isSafeNpmIdentifier(declaration?.identifier) ? declaration.identifier : undefined;
228
+ const declarationVersion = typeof declaration?.version === "string" ? declaration.version : undefined;
229
+ const packageVersion = isSafeServerVersion(declarationVersion)
230
+ ? declarationVersion
231
+ : undefined;
232
+ const canVerifyNetwork = options.allowNetwork === true
233
+ && localReadiness === "pass"
234
+ && declaration !== undefined
235
+ && serverName !== undefined
236
+ && serverVersion !== undefined
237
+ && packageName !== undefined
238
+ && packageVersion !== undefined;
239
+ if (!canVerifyNetwork && localReadiness !== "fail") {
240
+ findings.push({
241
+ id: "registry.preflight.network-unverified",
242
+ severity: "warn",
243
+ message: "Package publication and Registry version availability require explicit network verification."
244
+ });
245
+ }
246
+ if (canVerifyNetwork && packageName && packageVersion && serverName && serverVersion) {
247
+ const request = options.request ?? requestBoundedHttp;
248
+ let npmResponse;
249
+ try {
250
+ npmResponse = await request(`${NPM_REGISTRY}/${encodeURIComponent(packageName)}`, REQUEST_OPTIONS);
251
+ }
252
+ catch {
253
+ addFinding(findings, "registry.preflight.npm.request", "Public npm metadata could not be verified.");
254
+ packagePublication = "unknown";
255
+ return buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings);
256
+ }
257
+ if (npmResponse.statusCode !== 200 && npmResponse.statusCode !== 404) {
258
+ addFinding(findings, "registry.preflight.npm.response", "Public npm metadata could not prove package publication.");
259
+ packagePublication = "unknown";
260
+ return buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings);
261
+ }
262
+ if (!matchingNpmPackument(npmResponse, packageName, packageVersion, serverName)) {
263
+ addFinding(findings, "registry.preflight.npm.metadata", "Published npm metadata does not match the declared package version.");
264
+ packagePublication = "fail";
265
+ return buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings);
266
+ }
267
+ packagePublication = "pass";
268
+ let exactResponse;
269
+ try {
270
+ exactResponse = await request(`${MCP_REGISTRY}/v0.1/servers/${encodeURIComponent(serverName)}/versions/${encodeURIComponent(serverVersion)}`, REQUEST_OPTIONS);
271
+ }
272
+ catch {
273
+ addFinding(findings, "registry.preflight.registry.exact-request", "Exact Registry version availability could not be verified.");
274
+ return buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings);
275
+ }
276
+ if (matchingRegistryServer(exactResponse, serverName, serverVersion)) {
277
+ registryVersionAvailability = "already-published";
278
+ addFinding(findings, "registry.preflight.registry.already-published", "The exact Registry version is already published and cannot be overwritten.");
279
+ }
280
+ else if (validNotFound(exactResponse)) {
281
+ let latestResponse;
282
+ try {
283
+ latestResponse = await request(`${MCP_REGISTRY}/v0.1/servers/${encodeURIComponent(serverName)}/versions/latest`, REQUEST_OPTIONS);
284
+ }
285
+ catch {
286
+ addFinding(findings, "registry.preflight.registry.latest-request", "Latest Registry version availability could not be verified.");
287
+ return buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings);
288
+ }
289
+ if (validNotFound(latestResponse)) {
290
+ registryVersionAvailability = "available-first-publication";
291
+ }
292
+ else if (matchingRegistryServer(latestResponse, serverName) && responseJson(latestResponse) !== null) {
293
+ const payload = responseJson(latestResponse);
294
+ const latestVersion = payload.server.version;
295
+ if (latestVersion !== serverVersion) {
296
+ registryVersionAvailability = "available-new-version";
297
+ }
298
+ else {
299
+ addFinding(findings, "registry.preflight.registry.latest-response", "Latest Registry metadata did not prove a different published version.");
300
+ }
301
+ }
302
+ else {
303
+ addFinding(findings, "registry.preflight.registry.latest-response", "Latest Registry metadata could not prove version availability.");
304
+ }
305
+ }
306
+ else {
307
+ addFinding(findings, "registry.preflight.registry.exact-response", "Exact Registry metadata could not prove version availability.");
308
+ }
309
+ }
310
+ return buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings);
311
+ }
312
+ function buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings) {
313
+ return {
314
+ schemaVersion: "1.0.0",
315
+ kind: "mcp-registry-publication-preflight",
316
+ generatedAt: new Date().toISOString(),
317
+ target: "server.json",
318
+ ...(isSafeServerName(readiness.serverName) ? { serverName: readiness.serverName } : {}),
319
+ ...(isSafeServerVersion(readiness.serverVersion) ? { serverVersion: readiness.serverVersion } : {}),
320
+ status: statusFromFindings(findings),
321
+ localReadiness,
322
+ packagePublication,
323
+ registryVersionAvailability,
324
+ publisherPlan: publisherPlan(),
325
+ findings
326
+ };
327
+ }
328
+ export function renderMcpRegistryPublicationPreflightJson(report) {
329
+ return JSON.stringify(report, null, 2);
330
+ }
331
+ export function renderMcpRegistryPublicationPreflight(report) {
332
+ const networkVerification = report.findings.some((finding) => finding.id === "registry.preflight.network-unverified")
333
+ ? "NOT REQUESTED"
334
+ : report.packagePublication === "pass" && report.registryVersionAvailability !== "unknown"
335
+ ? "COMPLETED"
336
+ : report.localReadiness === "fail"
337
+ ? "NOT AVAILABLE"
338
+ : "INCOMPLETE";
339
+ const lines = [
340
+ `Registry publication preflight: ${report.status.toUpperCase()}`,
341
+ `Target: ${report.target}`,
342
+ report.serverName ? `Server: ${report.serverName}@${report.serverVersion ?? "unknown"}` : null,
343
+ `Local readiness: ${report.localReadiness.toUpperCase()}`,
344
+ `Package publication: ${report.packagePublication.toUpperCase()}`,
345
+ `Registry version availability: ${report.registryVersionAvailability.toUpperCase()}`,
346
+ `Network verification: ${networkVerification}`
347
+ ].filter((line) => line !== null);
348
+ if (report.findings.length > 0) {
349
+ lines.push("", "Findings", "--------");
350
+ for (const finding of report.findings) {
351
+ lines.push(`${finding.severity.toUpperCase()} ${finding.id}: ${finding.message}`);
352
+ }
353
+ }
354
+ return lines.join("\n");
355
+ }
356
+ export function registryPublicationPreflightExitCode(report, requirePublishReady = false) {
357
+ return report.status === "fail" || (requirePublishReady && report.status !== "pass") ? 1 : 0;
358
+ }
@@ -9,6 +9,68 @@ const remoteTransportReliabilityStatusSchema = {
9
9
  type: "string",
10
10
  enum: ["pass", "warn", "fail", "skipped"]
11
11
  };
12
+ const registryPublicationPreflightStatusSchema = {
13
+ type: "string",
14
+ enum: ["pass", "warn", "fail"]
15
+ };
16
+ const registryPackagePublicationSchema = {
17
+ type: "string",
18
+ enum: ["pass", "fail", "skipped", "unknown"]
19
+ };
20
+ const registryVersionAvailabilitySchema = {
21
+ type: "string",
22
+ enum: ["available-first-publication", "available-new-version", "already-published", "unknown"]
23
+ };
24
+ const registryPublisherPlanSchema = {
25
+ type: "object",
26
+ properties: {
27
+ executable: {
28
+ const: false
29
+ },
30
+ steps: {
31
+ type: "array",
32
+ minItems: 2,
33
+ maxItems: 2,
34
+ prefixItems: [
35
+ {
36
+ type: "object",
37
+ properties: {
38
+ order: {
39
+ const: 1
40
+ },
41
+ command: {
42
+ const: "mcp-publisher login github"
43
+ },
44
+ purpose: {
45
+ type: "string"
46
+ }
47
+ },
48
+ required: ["order", "command", "purpose"],
49
+ additionalProperties: false
50
+ },
51
+ {
52
+ type: "object",
53
+ properties: {
54
+ order: {
55
+ const: 2
56
+ },
57
+ command: {
58
+ const: "mcp-publisher publish"
59
+ },
60
+ purpose: {
61
+ type: "string"
62
+ }
63
+ },
64
+ required: ["order", "command", "purpose"],
65
+ additionalProperties: false
66
+ }
67
+ ],
68
+ items: false
69
+ }
70
+ },
71
+ required: ["executable", "steps"],
72
+ additionalProperties: false
73
+ };
12
74
  const runtimeConformanceSchema = {
13
75
  type: "object",
14
76
  properties: {
@@ -198,6 +260,36 @@ const publicSchemaDefinitions = [
198
260
  }
199
261
  }
200
262
  },
263
+ {
264
+ id: "doctor.registry.preflight.json",
265
+ command: "codex-plugin-doctor registry preflight <server.json|directory> --json",
266
+ outputKind: "mcp-registry-publication-preflight",
267
+ required: [
268
+ "schemaVersion",
269
+ "kind",
270
+ "generatedAt",
271
+ "target",
272
+ "status",
273
+ "localReadiness",
274
+ "packagePublication",
275
+ "registryVersionAvailability",
276
+ "publisherPlan",
277
+ "findings"
278
+ ],
279
+ properties: {
280
+ target: {
281
+ const: "server.json"
282
+ },
283
+ status: registryPublicationPreflightStatusSchema,
284
+ localReadiness: registryPublicationPreflightStatusSchema,
285
+ packagePublication: registryPackagePublicationSchema,
286
+ registryVersionAvailability: registryVersionAvailabilitySchema,
287
+ publisherPlan: registryPublisherPlanSchema,
288
+ findings: {
289
+ type: "array"
290
+ }
291
+ }
292
+ },
201
293
  {
202
294
  id: "doctor.audit.json",
203
295
  command: "codex-plugin-doctor audit --installed --json",
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { CheckOptions, CheckResult } from "./domain/types.js";
2
+ export { buildDoctorSize, renderDoctorSize, renderDoctorSizeJson, type DoctorSizeReport } from "./core/doctor-size.js";
2
3
  export { applyDoctorConfig, loadDoctorConfig, type DoctorConfig } from "./core/doctor-config.js";
3
4
  export { readRawDoctorConfig, resolveDoctorConfigPath, writeRawDoctorConfig, type LoadedDoctorConfig, type RawDoctorConfig } from "./core/doctor-config-store.js";
4
5
  export { classifySuppressionRecord, isValidSuppressionFingerprint, isSuppressionExpired, validateSuppressionRecord, type ClassifiedSuppressionRecord, type InvalidSuppressionRecord, type SuppressionRecordField, type ValidSuppressionRecord } from "./core/suppression-record.js";
@@ -31,6 +32,7 @@ export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, ty
31
32
  export { applyPolicyToDepAudit, applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
32
33
  export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
33
34
  export { buildMcpRegistryReadiness, inspectMcpRegistryServer, registryReadinessExitCode, renderMcpRegistryReadiness, renderMcpRegistryReadinessJson, type InspectMcpRegistryOptions, type McpRegistryFinding, type McpRegistryInstallability, type McpRegistryReadinessReport, type McpRegistryScorecard } from "./core/mcp-registry.js";
35
+ export { buildMcpRegistryPublicationPreflight, registryPublicationPreflightExitCode, renderMcpRegistryPublicationPreflight, renderMcpRegistryPublicationPreflightJson, type BuildMcpRegistryPublicationPreflightOptions, type McpRegistryPublicationPreflightFinding, type McpRegistryPublicationPreflightReport, type McpRegistryPublicationPreflightStatus } from "./core/mcp-registry-preflight.js";
34
36
  export { watchPlugin, type WatchPluginOptions, type WatchPluginResult } from "./core/watch-plugin.js";
35
37
  export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepAuditJson, renderDepAuditSarif, type DepAuditRecommendation, type DepAuditReport, type DepAuditVulnerability } from "./core/dep-audit.js";
36
38
  export { initGitHooks, removeGitHooks, type InitGitHooksResult, type RemoveGitHooksResult } from "./core/init-git-hooks.js";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { validatePlugin } from "./core/validate-plugin.js";
2
+ export { buildDoctorSize, renderDoctorSize, renderDoctorSizeJson } from "./core/doctor-size.js";
2
3
  export { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
3
4
  export { readRawDoctorConfig, resolveDoctorConfigPath, writeRawDoctorConfig } from "./core/doctor-config-store.js";
4
5
  export { classifySuppressionRecord, isValidSuppressionFingerprint, isSuppressionExpired, validateSuppressionRecord } from "./core/suppression-record.js";
@@ -31,6 +32,7 @@ export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } f
31
32
  export { applyPolicyToDepAudit, applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
32
33
  export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
33
34
  export { buildMcpRegistryReadiness, inspectMcpRegistryServer, registryReadinessExitCode, renderMcpRegistryReadiness, renderMcpRegistryReadinessJson } from "./core/mcp-registry.js";
35
+ export { buildMcpRegistryPublicationPreflight, registryPublicationPreflightExitCode, renderMcpRegistryPublicationPreflight, renderMcpRegistryPublicationPreflightJson } from "./core/mcp-registry-preflight.js";
34
36
  export { watchPlugin } from "./core/watch-plugin.js";
35
37
  export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepAuditJson, renderDepAuditSarif } from "./core/dep-audit.js";
36
38
  export { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";
package/dist/run-cli.js CHANGED
@@ -29,10 +29,12 @@ import { buildDoctorReviewBundle, diffDoctorReviewBundles, renderDoctorReviewBun
29
29
  import { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence } from "./core/release-evidence.js";
30
30
  import { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
31
31
  import { buildMcpRegistryReadiness, inspectMcpRegistryServer, registryReadinessExitCode, renderMcpRegistryReadiness, renderMcpRegistryReadinessJson } from "./core/mcp-registry.js";
32
+ import { buildMcpRegistryPublicationPreflight, registryPublicationPreflightExitCode, renderMcpRegistryPublicationPreflight, renderMcpRegistryPublicationPreflightJson } from "./core/mcp-registry-preflight.js";
32
33
  import { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
33
34
  import { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson } from "./core/inspector-bridge.js";
34
35
  import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
35
36
  import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
37
+ import { buildDoctorSize, renderDoctorSize, renderDoctorSizeJson } from "./core/doctor-size.js";
36
38
  import { initCiWorkflow } from "./core/init-ci.js";
37
39
  import { watchPlugin } from "./core/watch-plugin.js";
38
40
  import { buildDepAudit, renderDepAudit, renderDepAuditJson, renderDepAuditSarif } from "./core/dep-audit.js";
@@ -169,7 +171,8 @@ function parseRemoteNetworkFlags(flags, runtime) {
169
171
  function printUsage(io) {
170
172
  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 [--allow-network [--allow-local-network]]] [--sandbox docker] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>]\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> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--runtime [--allow-network [--allow-local-network]]] [--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>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus [--manifest <corpus.json>] [--json] [--output <path>]|corpus metrics --manifest <corpus.json> [--json|--markdown] [--output <path>] [--min-precision <0..1>] [--min-recall <0..1>] [--max-false-positive-rate <0..1>]|runtime-plan <path> [--sandbox docker] [--json|--markdown] [--output <path>]|runtime-policy <path> [--sandbox docker] [--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 [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--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 [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>] [--upload]|mcp <path> [--runtime [--allow-network [--allow-local-network]]]|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 init-git-hooks [path] --remove [--json]\n codex-plugin-doctor completion bash|zsh|fish\n codex-plugin-doctor config validate <path> [--json]\n codex-plugin-doctor release check <path> [--json] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker]\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");
171
173
  io.writeStderr("Registry readiness: codex-plugin-doctor registry check <server.json|directory> [--json] [--output <path>] [--require-registry-readiness]\n"
172
- + " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]");
174
+ + " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]\n"
175
+ + "Registry publication preflight: codex-plugin-doctor registry preflight <server.json|directory> [--allow-network] [--json] [--output <path>] [--require-publish-ready]");
173
176
  io.writeStderr("Corpus quality regression: codex-plugin-doctor doctor corpus metrics diff --before <metrics.json> --after <metrics.json> [--fail-on-regression] [--json|--markdown] [--output <path>]");
174
177
  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]");
175
178
  io.writeStderr("Baseline gating: codex-plugin-doctor baseline create <path> --output <path> [--runtime]\n codex-plugin-doctor check <path> --baseline <path>");
@@ -2099,6 +2102,19 @@ export async function runCli(args, io = defaultIo, options = {}) {
2099
2102
  io.writeStdout(await renderClientDoctor(terminalContext));
2100
2103
  return 0;
2101
2104
  }
2105
+ if (maybePath === "size") {
2106
+ const sizeTarget = remainingArgs[0] && !remainingArgs[0].startsWith("--") ? remainingArgs[0] : ".";
2107
+ const sizeFlags = remainingArgs[0] && remainingArgs[0].startsWith("--")
2108
+ ? remainingArgs
2109
+ : remainingArgs.slice(1);
2110
+ const jsonOutput = sizeFlags.includes("--json");
2111
+ const report = await buildDoctorSize(sizeTarget);
2112
+ const output = jsonOutput
2113
+ ? renderDoctorSizeJson(report)
2114
+ : renderDoctorSize(report);
2115
+ io.writeStdout(output);
2116
+ return report.status === "fail" ? 1 : 0;
2117
+ }
2102
2118
  io.writeStdout(maybePath === "--json"
2103
2119
  ? await renderEnvironmentDoctorJson(terminalContext)
2104
2120
  : await renderEnvironmentDoctor(terminalContext));
@@ -2363,16 +2379,18 @@ export async function runCli(args, io = defaultIo, options = {}) {
2363
2379
  const subcommand = maybePath;
2364
2380
  const target = remainingArgs[0];
2365
2381
  const flags = remainingArgs.slice(1);
2366
- if ((subcommand !== "check" && subcommand !== "inspect") || !target || target.startsWith("--")) {
2382
+ if ((subcommand !== "check" && subcommand !== "inspect" && subcommand !== "preflight") || !target || target.startsWith("--")) {
2367
2383
  io.writeStderr("Usage: codex-plugin-doctor registry check <server.json|directory> [--json] [--output <path>] [--require-registry-readiness]\n"
2368
- + " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]");
2384
+ + " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]\n"
2385
+ + " codex-plugin-doctor registry preflight <server.json|directory> [--allow-network] [--json] [--output <path>] [--require-publish-ready]");
2369
2386
  return 2;
2370
2387
  }
2371
2388
  const allowedFlags = new Set([
2372
2389
  "--json",
2373
2390
  "--output",
2374
2391
  "--allow-network",
2375
- "--require-registry-readiness"
2392
+ "--require-registry-readiness",
2393
+ "--require-publish-ready"
2376
2394
  ]);
2377
2395
  let outputPath = null;
2378
2396
  for (let index = 0; index < flags.length; index += 1) {
@@ -2393,14 +2411,33 @@ export async function runCli(args, io = defaultIo, options = {}) {
2393
2411
  }
2394
2412
  const allowNetwork = flags.includes("--allow-network");
2395
2413
  if (subcommand === "check" && allowNetwork) {
2396
- io.writeStderr("--allow-network is supported only by registry inspect.");
2414
+ io.writeStderr("--allow-network is not supported by registry check.");
2397
2415
  return 2;
2398
2416
  }
2399
2417
  if (subcommand === "inspect" && !allowNetwork) {
2400
2418
  io.writeStderr("registry inspect requires explicit --allow-network consent.");
2401
2419
  return 2;
2402
2420
  }
2421
+ if (subcommand === "preflight" && flags.includes("--require-registry-readiness")) {
2422
+ io.writeStderr("--require-registry-readiness is supported only by registry check or registry inspect.");
2423
+ return 2;
2424
+ }
2425
+ if (subcommand !== "preflight" && flags.includes("--require-publish-ready")) {
2426
+ io.writeStderr("--require-publish-ready is supported only by registry preflight.");
2427
+ return 2;
2428
+ }
2403
2429
  try {
2430
+ if (subcommand === "preflight") {
2431
+ const report = await buildMcpRegistryPublicationPreflight(target, { allowNetwork });
2432
+ const rendered = flags.includes("--json")
2433
+ ? renderMcpRegistryPublicationPreflightJson(report)
2434
+ : renderMcpRegistryPublicationPreflight(report);
2435
+ if (outputPath) {
2436
+ await writeFile(outputPath, rendered, "utf8");
2437
+ }
2438
+ io.writeStdout(rendered);
2439
+ return registryPublicationPreflightExitCode(report, flags.includes("--require-publish-ready"));
2440
+ }
2404
2441
  const report = subcommand === "check"
2405
2442
  ? await buildMcpRegistryReadiness(target)
2406
2443
  : await inspectMcpRegistryServer(target, { allowNetwork: true });
@@ -2414,7 +2451,12 @@ export async function runCli(args, io = defaultIo, options = {}) {
2414
2451
  return registryReadinessExitCode(report, flags.includes("--require-registry-readiness"));
2415
2452
  }
2416
2453
  catch (error) {
2417
- io.writeStderr(`Registry inspection failed: ${error.message}`);
2454
+ if (subcommand === "preflight") {
2455
+ io.writeStderr("Registry publication preflight failed.");
2456
+ }
2457
+ else {
2458
+ io.writeStderr(`Registry inspection failed: ${error.message}`);
2459
+ }
2418
2460
  return 1;
2419
2461
  }
2420
2462
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.54.0",
3
+ "version": "1.56.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",