Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
28 kB · 903 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904import type { Dirent, Stats } from 'fs'import { readdir, readFile, stat } from 'fs/promises'import * as path from 'path'import { z } from 'zod/v4'import { errorMessage, getErrnoCode, isENOENT } from '../errors.js'import { FRONTMATTER_REGEX } from '../frontmatterParser.js'import { jsonParse } from '../slowOperations.js'import { parseYaml } from '../yaml.js'import { PluginHooksSchema, PluginManifestSchema, PluginMarketplaceEntrySchema, PluginMarketplaceSchema,} from './schemas.js'
/** * Fields that belong in marketplace.json entries (PluginMarketplaceEntrySchema) * but not plugin.json (PluginManifestSchema). Plugin authors reasonably copy * one into the other. Surfaced as warnings by `claude plugin validate` since * they're a known confusion point — the load path silently strips all unknown * keys via zod's default behavior, so they're harmless at runtime but worth * flagging to authors. */const MARKETPLACE_ONLY_MANIFEST_FIELDS = new Set([ 'category', 'source', 'tags', 'strict', 'id',])
export type ValidationResult = { success: boolean errors: ValidationError[] warnings: ValidationWarning[] filePath: string fileType: 'plugin' | 'marketplace' | 'skill' | 'agent' | 'command' | 'hooks'}
export type ValidationError = { path: string message: string code?: string}
export type ValidationWarning = { path: string message: string}
/** * Detect whether a file is a plugin manifest or marketplace manifest */function detectManifestType( filePath: string,): 'plugin' | 'marketplace' | 'unknown' { const fileName = path.basename(filePath) const dirName = path.basename(path.dirname(filePath))
// Check filename patterns if (fileName === 'plugin.json') return 'plugin' if (fileName === 'marketplace.json') return 'marketplace'
// Check if it's in .claude-plugin directory if (dirName === '.claude-plugin') { return 'plugin' // Most likely plugin.json }
return 'unknown'}
/** * Format Zod validation errors into a readable format */function formatZodErrors(zodError: z.ZodError): ValidationError[] { return zodError.issues.map(error => ({ path: error.path.join('.') || 'root', message: error.message, code: error.code, }))}
/** * Check for parent-directory segments ('..') in a path string. * * For plugin.json component paths this is a security concern (escaping the plugin dir). * For marketplace.json source paths it's almost always a resolution-base misunderstanding: * paths resolve from the marketplace repo root, not from marketplace.json itself, so the * '..' a user added to "climb out of .claude-plugin/" is unnecessary. Callers pass `hint` * to attach the right explanation. */function checkPathTraversal( p: string, field: string, errors: ValidationError[], hint?: string,): void { if (p.includes('..')) { errors.push({ path: field, message: hint ? `Path contains "..": ${p}. ${hint}` : `Path contains ".." which could be a path traversal attempt: ${p}`, }) }}
// Shown when a marketplace plugin source contains '..'. Most users hit this because// they expect paths to resolve relative to marketplace.json (inside .claude-plugin/),// but resolution actually starts at the marketplace repo root — see gh-29485.// Computes a tailored "use X instead of Y" suggestion from the user's actual path// rather than a hardcoded example (review feedback on #20895).function marketplaceSourceHint(p: string): string { // Strip leading ../ segments: the '..' a user added to "climb out of // .claude-plugin/" is unnecessary since paths already start at the repo root. // If '..' appears mid-path (rare), fall back to a generic example. const stripped = p.replace(/^(\.\.\/)+/, '') const corrected = stripped !== p ? `./${stripped}` : './plugins/my-plugin' return ( 'Plugin source paths are resolved relative to the marketplace root (the directory ' + 'containing .claude-plugin/), not relative to marketplace.json. ' + `Use "${corrected}" instead of "${p}".` )}
/** * Validate a plugin manifest file (plugin.json) */export async function validatePluginManifest( filePath: string,): Promise<ValidationResult> { const errors: ValidationError[] = [] const warnings: ValidationWarning[] = [] const absolutePath = path.resolve(filePath)
// Read file content — handle ENOENT / EISDIR / permission errors directly let content: string try { content = await readFile(absolutePath, { encoding: 'utf-8' }) } catch (error: unknown) { const code = getErrnoCode(error) let message: string if (code === 'ENOENT') { message = `File not found: ${absolutePath}` } else if (code === 'EISDIR') { message = `Path is not a file: ${absolutePath}` } else { message = `Failed to read file: ${errorMessage(error)}` } return { success: false, errors: [{ path: 'file', message, code }], warnings: [], filePath: absolutePath, fileType: 'plugin', } }
let parsed: unknown try { parsed = jsonParse(content) } catch (error) { return { success: false, errors: [ { path: 'json', message: `Invalid JSON syntax: ${errorMessage(error)}`, }, ], warnings: [], filePath: absolutePath, fileType: 'plugin', } }
// Check for path traversal in the parsed JSON before schema validation // This ensures we catch security issues even if schema validation fails if (parsed && typeof parsed === 'object') { const obj = parsed as Record<string, unknown>
// Check commands if (obj.commands) { const commands = Array.isArray(obj.commands) ? obj.commands : [obj.commands] commands.forEach((cmd, i) => { if (typeof cmd === 'string') { checkPathTraversal(cmd, `commands[${i}]`, errors) } }) }
// Check agents if (obj.agents) { const agents = Array.isArray(obj.agents) ? obj.agents : [obj.agents] agents.forEach((agent, i) => { if (typeof agent === 'string') { checkPathTraversal(agent, `agents[${i}]`, errors) } }) }
// Check skills if (obj.skills) { const skills = Array.isArray(obj.skills) ? obj.skills : [obj.skills] skills.forEach((skill, i) => { if (typeof skill === 'string') { checkPathTraversal(skill, `skills[${i}]`, errors) } }) } }
// Surface marketplace-only fields as a warning BEFORE validation flags // them. `claude plugin validate` is a developer tool — authors running it // want to know these fields don't belong here. But it's a warning, not an // error: the plugin loads fine at runtime (the base schema strips unknown // keys). We strip them here so the .strict() call below doesn't double- // report them as unrecognized-key errors on top of the targeted warnings. let toValidate = parsed if (typeof parsed === 'object' && parsed !== null) { const obj = parsed as Record<string, unknown> const strayKeys = Object.keys(obj).filter(k => MARKETPLACE_ONLY_MANIFEST_FIELDS.has(k), ) if (strayKeys.length > 0) { const stripped = { ...obj } for (const key of strayKeys) { delete stripped[key] warnings.push({ path: key, message: `Field '${key}' belongs in the marketplace entry (marketplace.json), ` + `not plugin.json. It's harmless here but unused — Claude Code ` + `ignores it at load time.`, }) } toValidate = stripped } }
// Validate against schema (post-strip, so marketplace fields don't fail it). // We call .strict() locally here even though the base schema is lenient — // the runtime load path silently strips unknown keys for resilience, but // this is a developer tool and authors running it want typo feedback. const result = PluginManifestSchema().strict().safeParse(toValidate)
if (!result.success) { errors.push(...formatZodErrors(result.error)) }
// Check for common issues and add warnings if (result.success) { const manifest = result.data
// Warn if name isn't strict kebab-case. CC's schema only rejects spaces, // but the Claude.ai marketplace sync rejects non-kebab names. Surfacing // this here lets authors catch it in CI before the sync fails on them. if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(manifest.name)) { warnings.push({ path: 'name', message: `Plugin name "${manifest.name}" is not kebab-case. Claude Code accepts ` + `it, but the Claude.ai marketplace sync requires kebab-case ` + `(lowercase letters, digits, and hyphens only, e.g., "my-plugin").`, }) }
// Warn if no version specified if (!manifest.version) { warnings.push({ path: 'version', message: 'No version specified. Consider adding a version following semver (e.g., "1.0.0")', }) }
// Warn if no description if (!manifest.description) { warnings.push({ path: 'description', message: 'No description provided. Adding a description helps users understand what your plugin does', }) }
// Warn if no author if (!manifest.author) { warnings.push({ path: 'author', message: 'No author information provided. Consider adding author details for plugin attribution', }) } }
return { success: errors.length === 0, errors, warnings, filePath: absolutePath, fileType: 'plugin', }}
/** * Validate a marketplace manifest file (marketplace.json) */export async function validateMarketplaceManifest( filePath: string,): Promise<ValidationResult> { const errors: ValidationError[] = [] const warnings: ValidationWarning[] = [] const absolutePath = path.resolve(filePath)
// Read file content — handle ENOENT / EISDIR / permission errors directly let content: string try { content = await readFile(absolutePath, { encoding: 'utf-8' }) } catch (error: unknown) { const code = getErrnoCode(error) let message: string if (code === 'ENOENT') { message = `File not found: ${absolutePath}` } else if (code === 'EISDIR') { message = `Path is not a file: ${absolutePath}` } else { message = `Failed to read file: ${errorMessage(error)}` } return { success: false, errors: [{ path: 'file', message, code }], warnings: [], filePath: absolutePath, fileType: 'marketplace', } }
let parsed: unknown try { parsed = jsonParse(content) } catch (error) { return { success: false, errors: [ { path: 'json', message: `Invalid JSON syntax: ${errorMessage(error)}`, }, ], warnings: [], filePath: absolutePath, fileType: 'marketplace', } }
// Check for path traversal in plugin sources before schema validation // This ensures we catch security issues even if schema validation fails if (parsed && typeof parsed === 'object') { const obj = parsed as Record<string, unknown>
if (Array.isArray(obj.plugins)) { obj.plugins.forEach((plugin: unknown, i: number) => { if (plugin && typeof plugin === 'object' && 'source' in plugin) { const source = (plugin as { source: unknown }).source // Check string sources (relative paths) if (typeof source === 'string') { checkPathTraversal( source, `plugins[${i}].source`, errors, marketplaceSourceHint(source), ) } // Check object-source .path (git-subdir: subdirectory within the // remote repo, sparse-cloned). '..' here is a genuine traversal attempt // within the remote repo tree, not a marketplace-root misunderstanding — // keep the security framing (no marketplaceSourceHint). See #20895 review. if ( source && typeof source === 'object' && 'path' in source && typeof (source as { path: unknown }).path === 'string' ) { checkPathTraversal( (source as { path: string }).path, `plugins[${i}].source.path`, errors, ) } } }) } }
// Validate against schema. // The base schemas are lenient (strip unknown keys) for runtime resilience, // but this is a developer tool — authors want typo feedback. We rebuild the // schema with .strict() here. Note .strict() on the outer object does NOT // propagate into z.array() elements, so we also override the plugins array // with strict entries to catch typos inside individual plugin entries too. const strictMarketplaceSchema = PluginMarketplaceSchema() .extend({ plugins: z.array(PluginMarketplaceEntrySchema().strict()), }) .strict() const result = strictMarketplaceSchema.safeParse(parsed)
if (!result.success) { errors.push(...formatZodErrors(result.error)) }
// Check for common issues and add warnings if (result.success) { const marketplace = result.data
// Warn if no plugins if (!marketplace.plugins || marketplace.plugins.length === 0) { warnings.push({ path: 'plugins', message: 'Marketplace has no plugins defined', }) }
// Check each plugin entry if (marketplace.plugins) { marketplace.plugins.forEach((plugin, i) => { // Check for duplicate plugin names const duplicates = marketplace.plugins.filter( p => p.name === plugin.name, ) if (duplicates.length > 1) { errors.push({ path: `plugins[${i}].name`, message: `Duplicate plugin name "${plugin.name}" found in marketplace`, }) } })
// Version-mismatch check: for local-source entries that declare a // version, compare against the plugin's own plugin.json. At install // time, calculatePluginVersion (pluginVersioning.ts) prefers the // manifest version and silently ignores the entry version — so a // stale entry.version is invisible user confusion (marketplace UI // shows one version, /status shows another after install). // Only local sources: remote sources would need cloning to check. const manifestDir = path.dirname(absolutePath) const marketplaceRoot = path.basename(manifestDir) === '.claude-plugin' ? path.dirname(manifestDir) : manifestDir for (const [i, entry] of marketplace.plugins.entries()) { if ( !entry.version || typeof entry.source !== 'string' || !entry.source.startsWith('./') ) { continue } const pluginJsonPath = path.join( marketplaceRoot, entry.source, '.claude-plugin', 'plugin.json', ) let manifestVersion: string | undefined try { const raw = await readFile(pluginJsonPath, { encoding: 'utf-8' }) const parsed = jsonParse(raw) as { version?: unknown } if (typeof parsed.version === 'string') { manifestVersion = parsed.version } } catch { // Missing/unreadable plugin.json is someone else's error to report continue } if (manifestVersion && manifestVersion !== entry.version) { warnings.push({ path: `plugins[${i}].version`, message: `Entry declares version "${entry.version}" but ${entry.source}/.claude-plugin/plugin.json says "${manifestVersion}". ` + `At install time, plugin.json wins (calculatePluginVersion precedence) — the entry version is silently ignored. ` + `Update this entry to "${manifestVersion}" to match.`, }) } } }
// Warn if no description in metadata if (!marketplace.metadata?.description) { warnings.push({ path: 'metadata.description', message: 'No marketplace description provided. Adding a description helps users understand what this marketplace offers', }) } }
return { success: errors.length === 0, errors, warnings, filePath: absolutePath, fileType: 'marketplace', }}/** * Validate the YAML frontmatter in a plugin component markdown file. * * The runtime loader (parseFrontmatter) silently drops unparseable YAML to a * debug log and returns an empty object. That's the right resilience choice * for the load path, but authors running `claude plugin validate` want a hard * signal. This re-parses the frontmatter block and surfaces what the loader * would silently swallow. */function validateComponentFile( filePath: string, content: string, fileType: 'skill' | 'agent' | 'command',): ValidationResult { const errors: ValidationError[] = [] const warnings: ValidationWarning[] = []
const match = content.match(FRONTMATTER_REGEX) if (!match) { warnings.push({ path: 'frontmatter', message: 'No frontmatter block found. Add YAML frontmatter between --- delimiters ' + 'at the top of the file to set description and other metadata.', }) return { success: true, errors, warnings, filePath, fileType } }
const frontmatterText = match[1] || '' let parsed: unknown try { parsed = parseYaml(frontmatterText) } catch (e) { errors.push({ path: 'frontmatter', message: `YAML frontmatter failed to parse: ${errorMessage(e)}. ` + `At runtime this ${fileType} loads with empty metadata (all frontmatter ` + `fields silently dropped).`, }) return { success: false, errors, warnings, filePath, fileType } }
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { errors.push({ path: 'frontmatter', message: 'Frontmatter must be a YAML mapping (key: value pairs), got ' + `${Array.isArray(parsed) ? 'an array' : parsed === null ? 'null' : typeof parsed}.`, }) return { success: false, errors, warnings, filePath, fileType } }
const fm = parsed as Record<string, unknown>
// description: must be scalar. coerceDescriptionToString logs+drops arrays/objects at runtime. if (fm.description !== undefined) { const d = fm.description if ( typeof d !== 'string' && typeof d !== 'number' && typeof d !== 'boolean' && d !== null ) { errors.push({ path: 'description', message: `description must be a string, got ${Array.isArray(d) ? 'array' : typeof d}. ` + `At runtime this value is dropped.`, }) } } else { warnings.push({ path: 'description', message: `No description in frontmatter. A description helps users and Claude ` + `understand when to use this ${fileType}.`, }) }
// name: if present, must be a string (skills/commands use it as displayName; // plugin agents use it as the agentType stem — non-strings would stringify to garbage) if ( fm.name !== undefined && fm.name !== null && typeof fm.name !== 'string' ) { errors.push({ path: 'name', message: `name must be a string, got ${typeof fm.name}.`, }) }
// allowed-tools: string or array of strings const at = fm['allowed-tools'] if (at !== undefined && at !== null) { if (typeof at !== 'string' && !Array.isArray(at)) { errors.push({ path: 'allowed-tools', message: `allowed-tools must be a string or array of strings, got ${typeof at}.`, }) } else if (Array.isArray(at) && at.some(t => typeof t !== 'string')) { errors.push({ path: 'allowed-tools', message: 'allowed-tools array must contain only strings.', }) } }
// shell: 'bash' | 'powershell' (controls !`cmd` block routing) const sh = fm.shell if (sh !== undefined && sh !== null) { if (typeof sh !== 'string') { errors.push({ path: 'shell', message: `shell must be a string, got ${typeof sh}.`, }) } else { // Normalize to match parseShellFrontmatter() runtime behavior — // `shell: PowerShell` should not fail validation but work at runtime. const normalized = sh.trim().toLowerCase() if (normalized !== 'bash' && normalized !== 'powershell') { errors.push({ path: 'shell', message: `shell must be 'bash' or 'powershell', got '${sh}'.`, }) } } }
return { success: errors.length === 0, errors, warnings, filePath, fileType }}
/** * Validate a plugin's hooks.json file. Unlike frontmatter, this one HARD-ERRORS * at runtime (pluginLoader uses .parse() not .safeParse()) — a bad hooks.json * breaks the whole plugin. Surfacing it here is essential. */async function validateHooksJson(filePath: string): Promise<ValidationResult> { let content: string try { content = await readFile(filePath, { encoding: 'utf-8' }) } catch (e: unknown) { const code = getErrnoCode(e) // ENOENT is fine — hooks are optional if (code === 'ENOENT') { return { success: true, errors: [], warnings: [], filePath, fileType: 'hooks', } } return { success: false, errors: [ { path: 'file', message: `Failed to read file: ${errorMessage(e)}` }, ], warnings: [], filePath, fileType: 'hooks', } }
let parsed: unknown try { parsed = jsonParse(content) } catch (e) { return { success: false, errors: [ { path: 'json', message: `Invalid JSON syntax: ${errorMessage(e)}. ` + `At runtime this breaks the entire plugin load.`, }, ], warnings: [], filePath, fileType: 'hooks', } }
const result = PluginHooksSchema().safeParse(parsed) if (!result.success) { return { success: false, errors: formatZodErrors(result.error), warnings: [], filePath, fileType: 'hooks', } }
return { success: true, errors: [], warnings: [], filePath, fileType: 'hooks', }}
/** * Recursively collect .md files under a directory. Uses withFileTypes to * avoid a stat per entry. Returns absolute paths so error messages stay * readable. */async function collectMarkdown( dir: string, isSkillsDir: boolean,): Promise<string[]> { let entries: Dirent[] try { entries = await readdir(dir, { withFileTypes: true }) } catch (e: unknown) { const code = getErrnoCode(e) if (code === 'ENOENT' || code === 'ENOTDIR') return [] throw e }
// Skills use <name>/SKILL.md — only descend one level, only collect SKILL.md. // Matches the runtime loader: single .md files in skills/ are NOT loaded, // and subdirectories of a skill dir aren't scanned. Paths are speculative // (the subdir may lack SKILL.md); the caller handles ENOENT. if (isSkillsDir) { return entries .filter(e => e.isDirectory()) .map(e => path.join(dir, e.name, 'SKILL.md')) }
// Commands/agents: recurse and collect all .md files. const out: string[] = [] for (const entry of entries) { const full = path.join(dir, entry.name) if (entry.isDirectory()) { out.push(...(await collectMarkdown(full, false))) } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) { out.push(full) } } return out}
/** * Validate the content files inside a plugin directory — skills, agents, * commands, and hooks.json. Scans the default component directories (the * manifest can declare custom paths but the default layout covers the vast * majority of plugins; this is a linter, not a loader). * * Returns one ValidationResult per file that has errors or warnings. A clean * plugin returns an empty array. */export async function validatePluginContents( pluginDir: string,): Promise<ValidationResult[]> { const results: ValidationResult[] = []
const dirs: Array<['skill' | 'agent' | 'command', string]> = [ ['skill', path.join(pluginDir, 'skills')], ['agent', path.join(pluginDir, 'agents')], ['command', path.join(pluginDir, 'commands')], ]
for (const [fileType, dir] of dirs) { const files = await collectMarkdown(dir, fileType === 'skill') for (const filePath of files) { let content: string try { content = await readFile(filePath, { encoding: 'utf-8' }) } catch (e: unknown) { // ENOENT is expected for speculative skill paths (subdirs without SKILL.md) if (isENOENT(e)) continue results.push({ success: false, errors: [ { path: 'file', message: `Failed to read: ${errorMessage(e)}` }, ], warnings: [], filePath, fileType, }) continue } const r = validateComponentFile(filePath, content, fileType) if (r.errors.length > 0 || r.warnings.length > 0) { results.push(r) } } }
const hooksResult = await validateHooksJson( path.join(pluginDir, 'hooks', 'hooks.json'), ) if (hooksResult.errors.length > 0 || hooksResult.warnings.length > 0) { results.push(hooksResult) }
return results}
/** * Validate a manifest file or directory (auto-detects type) */export async function validateManifest( filePath: string,): Promise<ValidationResult> { const absolutePath = path.resolve(filePath)
// Stat path to check if it's a directory — handle ENOENT inline let stats: Stats | null = null try { stats = await stat(absolutePath) } catch (e: unknown) { if (!isENOENT(e)) { throw e } }
if (stats?.isDirectory()) { // Look for manifest files in .claude-plugin directory // Prefer marketplace.json over plugin.json const marketplacePath = path.join( absolutePath, '.claude-plugin', 'marketplace.json', ) const marketplaceResult = await validateMarketplaceManifest(marketplacePath) // Only fall through if the marketplace file was not found (ENOENT) if (marketplaceResult.errors[0]?.code !== 'ENOENT') { return marketplaceResult }
const pluginPath = path.join(absolutePath, '.claude-plugin', 'plugin.json') const pluginResult = await validatePluginManifest(pluginPath) if (pluginResult.errors[0]?.code !== 'ENOENT') { return pluginResult }
return { success: false, errors: [ { path: 'directory', message: `No manifest found in directory. Expected .claude-plugin/marketplace.json or .claude-plugin/plugin.json`, }, ], warnings: [], filePath: absolutePath, fileType: 'plugin', } }
const manifestType = detectManifestType(filePath)
switch (manifestType) { case 'plugin': return validatePluginManifest(filePath) case 'marketplace': return validateMarketplaceManifest(filePath) case 'unknown': { // Try to parse and guess based on content try { const content = await readFile(absolutePath, { encoding: 'utf-8' }) const parsed = jsonParse(content) as Record<string, unknown>
// Heuristic: if it has a "plugins" array, it's probably a marketplace if (Array.isArray(parsed.plugins)) { return validateMarketplaceManifest(filePath) } } catch (e: unknown) { const code = getErrnoCode(e) if (code === 'ENOENT') { return { success: false, errors: [ { path: 'file', message: `File not found: ${absolutePath}`, }, ], warnings: [], filePath: absolutePath, fileType: 'plugin', // Default to plugin for error reporting } } // Fall through to default validation for other errors (e.g., JSON parse) }
// Default: validate as plugin manifest return validatePluginManifest(filePath) } }}