// ide-extension-parser.mjs — Parse VS Code extension package.json into normalized manifest. // Zero dependencies (Node.js builtins only). import { readFile, access } from 'node:fs/promises'; import { join } from 'node:path'; async function pathExists(p) { try { await access(p); return true; } catch { return false; } } /** * @typedef {object} ParsedManifest * @property {string} id * @property {string} publisher * @property {string} name * @property {string} version * @property {object} engines * @property {string|null} main * @property {string|null} browser * @property {string[]} activationEvents * @property {object} contributes * @property {string[]} extensionPack * @property {string[]} extensionDependencies * @property {string[]} extensionKind * @property {string[]} categories * @property {object} capabilities * @property {object} scripts * @property {object|string|null} repository * @property {object} dependencies * @property {boolean} hasSignature */ /** * Parse a VS Code extension directory. * @param {string} extRoot - Absolute path to extracted extension root. * @returns {Promise<{ manifest: ParsedManifest, warnings: string[] } | null>} */ export async function parseVSCodeExtension(extRoot) { const warnings = []; const pkgPath = join(extRoot, 'package.json'); let raw; try { raw = await readFile(pkgPath, 'utf8'); } catch (err) { return null; } let pkg; try { pkg = JSON.parse(raw); } catch (err) { warnings.push(`malformed package.json at ${pkgPath}: ${err.message}`); return null; } if (!pkg || typeof pkg !== 'object') { warnings.push(`package.json at ${pkgPath} is not an object`); return null; } const publisher = typeof pkg.publisher === 'string' ? pkg.publisher : ''; const name = typeof pkg.name === 'string' ? pkg.name : ''; const version = typeof pkg.version === 'string' ? pkg.version : ''; if (!publisher || !name) { warnings.push(`missing publisher/name in ${pkgPath}`); return null; } const hasSignature = await pathExists(join(extRoot, '.signature.p7s')); const manifest = { id: `${publisher}.${name}`.toLowerCase(), publisher: publisher.toLowerCase(), name: name.toLowerCase(), version, engines: pkg.engines && typeof pkg.engines === 'object' ? pkg.engines : {}, main: typeof pkg.main === 'string' ? pkg.main : null, browser: typeof pkg.browser === 'string' ? pkg.browser : null, activationEvents: Array.isArray(pkg.activationEvents) ? pkg.activationEvents.filter(e => typeof e === 'string') : [], contributes: pkg.contributes && typeof pkg.contributes === 'object' ? pkg.contributes : {}, extensionPack: Array.isArray(pkg.extensionPack) ? pkg.extensionPack.filter(e => typeof e === 'string') : [], extensionDependencies: Array.isArray(pkg.extensionDependencies) ? pkg.extensionDependencies.filter(e => typeof e === 'string') : [], extensionKind: Array.isArray(pkg.extensionKind) ? pkg.extensionKind.filter(e => typeof e === 'string') : [], categories: Array.isArray(pkg.categories) ? pkg.categories.filter(c => typeof c === 'string') : [], capabilities: pkg.capabilities && typeof pkg.capabilities === 'object' ? pkg.capabilities : {}, scripts: pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}, repository: pkg.repository || null, dependencies: pkg.dependencies && typeof pkg.dependencies === 'object' ? pkg.dependencies : {}, hasSignature, }; return { manifest, warnings }; } /** * Parse a .vsix file. Stub for v1 — user must extract first. * @param {string} vsixPath * @throws {Error} */ export async function parseVsixFile(vsixPath) { throw new Error(`VSIX parsing not implemented in v6.3.0. Extract manually (unzip ${vsixPath}) and pass the extracted directory.`); } /** * Parse an IntelliJ plugin. Stub for v1.1. * @param {string} pluginRoot * @returns {Promise} */ export async function parseIntelliJPlugin(pluginRoot) { return null; }