codex-plugin-doctor 1.59.0 → 1.61.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.
@@ -0,0 +1,259 @@
1
+ import { validateSubmissionAssetsFromReader } from "./submission-assets.js";
2
+ import { inspectSubmissionArchive } from "./submission-archive-reader.js";
3
+ import { submissionArchiveRuleset } from "./submission-archive-ruleset.js";
4
+ import { validateSubmissionListing } from "./submission-preflight.js";
5
+ import { submissionManualChecks } from "./submission-ruleset.js";
6
+ import { validateSubmissionSkillMetadataFromReader } from "./submission-skill-metadata.js";
7
+ const manifestPaths = [".codex-plugin/plugin.json", ".agent-plugin/plugin.json", ".claude-plugin/plugin.json"];
8
+ const maxManifestBytes = 1024 * 1024;
9
+ function archiveFinding(id, severity, message, portalCode) {
10
+ return portalCode === undefined ? { id, severity, message } : { id, severity, message, portalCode };
11
+ }
12
+ function archiveCoverage() {
13
+ return submissionArchiveRuleset.portalRules.map(({ id, status, reason }) => ({ id, status, reason }));
14
+ }
15
+ function checkStatus(findings) {
16
+ return findings.some((finding) => finding.severity === "fail")
17
+ ? "fail"
18
+ : findings.some((finding) => finding.severity === "warn") ? "warn" : "pass";
19
+ }
20
+ function submissionCheck(id, findings) {
21
+ return { id, status: checkStatus(findings), findingIds: findings.map((finding) => finding.id) };
22
+ }
23
+ function manualChecklist() {
24
+ return submissionManualChecks.map((item) => ({
25
+ id: item.id,
26
+ label: item.label,
27
+ state: item.mcpOnly ? "not_applicable" : "required"
28
+ }));
29
+ }
30
+ function reportSummary(checks, findings, checklist) {
31
+ return {
32
+ passed: checks.filter((check) => check.status === "pass").length,
33
+ warnings: findings.filter((finding) => finding.severity === "warn").length,
34
+ blockers: findings.filter((finding) => finding.severity === "fail").length,
35
+ manualChecks: checklist.filter((item) => item.state === "required").length
36
+ };
37
+ }
38
+ function normalizePath(value, allowRoot = false) {
39
+ if (value === "" && allowRoot)
40
+ return "";
41
+ if (typeof value !== "string" || value === "" || value.startsWith("/") || value.startsWith("\\")
42
+ || /^[A-Za-z]:/u.test(value) || value.includes("\\") || /[\u0000-\u001F\u007F]/u.test(value))
43
+ return null;
44
+ const segments = value.split("/");
45
+ return segments.some((segment) => segment === "" || segment === "." || segment === "..") ? null : segments.join("/");
46
+ }
47
+ function discoverRoot(entries) {
48
+ const paths = entries.map((entry) => entry.packagePath);
49
+ if (paths.length === 0) {
50
+ return { discovery: null, findings: [archiveFinding("plugin.submission.archive.root_missing", "fail", "Archive does not contain a plugin root.")] };
51
+ }
52
+ const rootManifests = manifestPaths.filter((manifestPath) => paths.includes(manifestPath));
53
+ const nestedRoots = new Set();
54
+ for (const entryPath of paths) {
55
+ const separator = entryPath.indexOf("/");
56
+ if (separator > 0 && manifestPaths.includes(entryPath.slice(separator + 1))) {
57
+ nestedRoots.add(entryPath.slice(0, separator));
58
+ }
59
+ }
60
+ if (rootManifests.length + nestedRoots.size > 1) {
61
+ return { discovery: null, findings: [archiveFinding("plugin.submission.archive.root_ambiguous", "fail", "Archive contains more than one plugin root.")] };
62
+ }
63
+ if (rootManifests.length === 1) {
64
+ return {
65
+ discovery: { rootPrefix: "", rootLayout: "archive-root", manifestPath: rootManifests[0], manifestFormat: rootManifests[0] },
66
+ findings: []
67
+ };
68
+ }
69
+ if (nestedRoots.size === 0) {
70
+ return { discovery: null, findings: [archiveFinding("plugin.submission.archive.manifest_missing", "fail", "Archive root does not contain a recognized plugin manifest.")] };
71
+ }
72
+ const hasRootFile = entries.some((entry) => !entry.packagePath.includes("/") && entry.resolvedKind === "file");
73
+ const topLevels = new Set(paths.map((entryPath) => entryPath.split("/", 1)[0]));
74
+ if (hasRootFile || nestedRoots.size !== 1 || topLevels.size !== 1) {
75
+ return { discovery: null, findings: [archiveFinding("plugin.submission.archive.root_siblings", "fail", "A top-level plugin directory cannot have siblings.")] };
76
+ }
77
+ const topLevel = [...nestedRoots][0];
78
+ const prefix = `${topLevel}/`;
79
+ const nestedManifests = manifestPaths.filter((manifestPath) => paths.includes(`${prefix}${manifestPath}`));
80
+ if (nestedManifests.length === 0) {
81
+ return { discovery: null, findings: [archiveFinding("plugin.submission.archive.manifest_missing", "fail", "Archive root does not contain a recognized plugin manifest.")] };
82
+ }
83
+ if (nestedManifests.length > 1) {
84
+ return { discovery: null, findings: [archiveFinding("plugin.submission.archive.root_ambiguous", "fail", "Archive contains more than one plugin manifest.")] };
85
+ }
86
+ return {
87
+ discovery: {
88
+ rootPrefix: prefix,
89
+ rootLayout: "single-top-level-directory",
90
+ manifestPath: `${prefix}${nestedManifests[0]}`,
91
+ manifestFormat: nestedManifests[0]
92
+ },
93
+ findings: []
94
+ };
95
+ }
96
+ function rootedReader(inspection, rootPrefix) {
97
+ if (inspection.reader === null)
98
+ return null;
99
+ const entries = inspection.entries
100
+ .filter((entry) => entry.packagePath.startsWith(rootPrefix))
101
+ .map((entry) => ({ ...entry, packagePath: entry.packagePath.slice(rootPrefix.length) }));
102
+ const byPath = new Map(entries.map((entry) => [entry.packagePath, entry]));
103
+ const reader = inspection.reader;
104
+ function directoryEntry(packagePath) {
105
+ if (entries.some((entry) => entry.packagePath.startsWith(`${packagePath}/`))) {
106
+ return { packagePath, kind: "directory", resolvedKind: "directory", size: 0, safeResolution: "safe" };
107
+ }
108
+ return null;
109
+ }
110
+ return {
111
+ async list(directory) {
112
+ const normalizedDirectory = normalizePath(directory, true);
113
+ if (normalizedDirectory === null)
114
+ return [];
115
+ const prefix = normalizedDirectory === "" ? "" : `${normalizedDirectory}/`;
116
+ const children = new Map();
117
+ for (const entry of entries) {
118
+ if (!entry.packagePath.startsWith(prefix))
119
+ continue;
120
+ const remaining = entry.packagePath.slice(prefix.length);
121
+ if (remaining === "")
122
+ continue;
123
+ const [name] = remaining.split("/", 1);
124
+ const packagePath = normalizedDirectory === "" ? name : `${normalizedDirectory}/${name}`;
125
+ if (remaining.includes("/")) {
126
+ children.set(packagePath, directoryEntry(packagePath));
127
+ }
128
+ else {
129
+ children.set(packagePath, entry);
130
+ }
131
+ }
132
+ return [...children.values()].sort((left, right) => left.packagePath.localeCompare(right.packagePath));
133
+ },
134
+ async stat(packagePath) {
135
+ const normalizedPath = normalizePath(packagePath);
136
+ return normalizedPath === null ? null : byPath.get(normalizedPath) ?? directoryEntry(normalizedPath);
137
+ },
138
+ async read(packagePath, maxBytes) {
139
+ const normalizedPath = normalizePath(packagePath);
140
+ if (normalizedPath === null || !byPath.has(normalizedPath))
141
+ return null;
142
+ return reader.read(`${rootPrefix}${normalizedPath}`, maxBytes);
143
+ }
144
+ };
145
+ }
146
+ async function parseManifest(reader, manifestPath) {
147
+ const bytes = await reader.read(manifestPath, maxManifestBytes).catch(() => null);
148
+ if (bytes === null)
149
+ return null;
150
+ try {
151
+ const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
152
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ function hasImmediateSkill(entries) {
159
+ return entries.some((entry) => /^skills\/[^/]+\/SKILL\.md$/u.test(entry.packagePath) && entry.resolvedKind === "file");
160
+ }
161
+ function exclusionFindings(manifest, entries) {
162
+ const findings = [];
163
+ const interfaceValue = manifest.interface;
164
+ if (manifest.mcpServers !== undefined || entries.some((entry) => entry.packagePath === ".mcp.json")) {
165
+ findings.push(archiveFinding("plugin.submission.archive.mcp_excluded", "warn", "MCP configuration requires the MCP-backed submission flow.", "mcp_configuration_excluded"));
166
+ }
167
+ if (manifest.apps !== undefined || entries.some((entry) => entry.packagePath === ".app.json")) {
168
+ findings.push(archiveFinding("plugin.submission.archive.app_excluded", "warn", "App configuration requires the MCP-backed submission flow.", "app_configuration_excluded"));
169
+ }
170
+ if (typeof interfaceValue === "object" && interfaceValue !== null && !Array.isArray(interfaceValue)
171
+ && "screenshots" in interfaceValue && interfaceValue.screenshots !== undefined) {
172
+ findings.push(archiveFinding("plugin.submission.archive.screenshot_excluded", "warn", "Screenshots require the MCP-backed submission flow.", "screenshot_configuration_excluded"));
173
+ }
174
+ return findings;
175
+ }
176
+ async function nestedSubmission(manifest, reader) {
177
+ const targetType = "skills-only";
178
+ const listing = validateSubmissionListing(manifest, targetType);
179
+ const components = [];
180
+ const assets = (await validateSubmissionAssetsFromReader(manifest, reader)).findings;
181
+ const skills = (await validateSubmissionSkillMetadataFromReader(manifest, targetType, reader)).findings;
182
+ const checks = [
183
+ submissionCheck("listing", listing),
184
+ submissionCheck("components", components),
185
+ submissionCheck("assets", assets),
186
+ submissionCheck("skills", skills)
187
+ ];
188
+ const findings = [...listing, ...components, ...assets, ...skills];
189
+ const manual = manualChecklist();
190
+ const summary = reportSummary(checks, findings, manual);
191
+ return {
192
+ schemaVersion: "1.0.0",
193
+ rulesetVersion: "openai-directory-2026-08-15",
194
+ targetType,
195
+ status: summary.blockers > 0 ? "fail" : "pass",
196
+ readiness: summary.blockers > 0 ? "blocked" : "manual_review_required",
197
+ summary,
198
+ checks,
199
+ findings,
200
+ manualChecklist: manual
201
+ };
202
+ }
203
+ export function submissionArchiveExitCode(report, requireReady) {
204
+ return requireReady && report.status === "fail" ? 1 : 0;
205
+ }
206
+ export async function buildSubmissionArchivePreflight(archivePath) {
207
+ const inspection = await inspectSubmissionArchive(archivePath);
208
+ const coverage = [...inspection.coverage, ...archiveCoverage()];
209
+ const manual = manualChecklist();
210
+ const archiveFindings = [...inspection.findings];
211
+ let rootLayout = "unavailable";
212
+ let submission = null;
213
+ if (!archiveFindings.some((finding) => finding.severity === "fail") && inspection.reader !== null) {
214
+ const root = discoverRoot(inspection.entries);
215
+ archiveFindings.push(...root.findings);
216
+ if (root.discovery !== null) {
217
+ rootLayout = root.discovery.rootLayout;
218
+ const reader = rootedReader(inspection, root.discovery.rootPrefix);
219
+ const manifest = reader === null ? null : await parseManifest(reader, root.discovery.manifestPath.slice(root.discovery.rootPrefix.length));
220
+ if (manifest === null || reader === null) {
221
+ archiveFindings.push(archiveFinding("plugin.submission.archive.manifest_missing", "fail", "Plugin manifest must be a bounded UTF-8 JSON object."));
222
+ }
223
+ else {
224
+ const entries = inspection.entries
225
+ .filter((entry) => entry.packagePath.startsWith(root.discovery.rootPrefix))
226
+ .map((entry) => ({ ...entry, packagePath: entry.packagePath.slice(root.discovery.rootPrefix.length) }));
227
+ if (!hasImmediateSkill(entries)) {
228
+ archiveFindings.push(archiveFinding("plugin.submission.archive.skill_missing", "fail", "Archive must contain an immediate skills/<skill>/SKILL.md entrypoint."));
229
+ }
230
+ archiveFindings.push(...exclusionFindings(manifest, entries));
231
+ submission = await nestedSubmission(manifest, reader);
232
+ }
233
+ }
234
+ }
235
+ const findings = [...archiveFindings, ...(submission?.findings ?? [])];
236
+ const blockers = findings.filter((finding) => finding.severity === "fail").length;
237
+ const summary = submission === null
238
+ ? { passed: 0, warnings: findings.filter((finding) => finding.severity === "warn").length, blockers, manualChecks: manual.filter((item) => item.state === "required").length }
239
+ : { ...submission.summary, warnings: findings.filter((finding) => finding.severity === "warn").length, blockers };
240
+ return {
241
+ schemaVersion: "1.0.0",
242
+ rulesetVersion: submissionArchiveRuleset.version,
243
+ status: blockers > 0 ? "fail" : "pass",
244
+ readiness: blockers > 0 ? "blocked" : "manual_review_required",
245
+ archive: {
246
+ fileName: inspection.fileName,
247
+ compressedBytes: inspection.compressedBytes,
248
+ uncompressedBytes: inspection.uncompressedBytes,
249
+ entryCount: inspection.entryCount,
250
+ rootLayout
251
+ },
252
+ summary,
253
+ archiveChecks: [],
254
+ submission,
255
+ findings,
256
+ coverage,
257
+ manualChecklist: manual
258
+ };
259
+ }
@@ -0,0 +1,21 @@
1
+ import type { SubmissionPackageEntry, SubmissionPackageReader } from "./submission-package-reader.js";
2
+ import type { SubmissionFinding } from "./submission-preflight.js";
3
+ export interface SubmissionArchiveFinding extends SubmissionFinding {
4
+ id: `plugin.submission.archive.${string}`;
5
+ }
6
+ export interface SubmissionArchiveCoverage {
7
+ id: string;
8
+ status: "automatic" | "manual" | "unavailable";
9
+ reason: string;
10
+ }
11
+ export interface SubmissionArchiveInspection {
12
+ fileName: string;
13
+ compressedBytes: number;
14
+ uncompressedBytes: number;
15
+ entryCount: number;
16
+ entries: readonly SubmissionPackageEntry[];
17
+ findings: readonly SubmissionArchiveFinding[];
18
+ coverage: readonly SubmissionArchiveCoverage[];
19
+ reader: SubmissionPackageReader | null;
20
+ }
21
+ export declare function inspectSubmissionArchive(zipPath: string): Promise<SubmissionArchiveInspection>;