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 +28 -2
- package/dist/core/doctor-size.d.ts +16 -0
- package/dist/core/doctor-size.js +139 -0
- package/dist/core/mcp-registry-preflight.d.ts +38 -0
- package/dist/core/mcp-registry-preflight.js +358 -0
- package/dist/core/output-contract.js +92 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/run-cli.js +48 -6
- package/package.json +1 -1
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.
|
|
483
|
+
- uses: Esquetta/CodexPluginDoctor@v1.56.0
|
|
458
484
|
with:
|
|
459
|
-
version: "1.
|
|
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
|
|
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
|
-
|
|
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