#!/usr/bin/env node /** * Generates extension bridge files based on clawx-extensions.json and * which packages are actually installed in node_modules. * * Outputs: * electron/extensions/_ext-bridge.generated.ts (main process) * src/extensions/_ext-bridge.generated.ts (renderer) * * Both files are .gitignore'd. When no external extensions are installed, * they export no-op functions so the core compiles cleanly. * * Usage: node scripts/generate-ext-bridge.mjs */ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(__dirname, '..'); const MANIFEST_PATH = resolve(ROOT, 'clawx-extensions.json'); const MAIN_OUT = resolve(ROOT, 'electron/extensions/_ext-bridge.generated.ts'); const RENDERER_OUT = resolve(ROOT, 'src/extensions/_ext-bridge.generated.ts'); function readManifest() { if (!existsSync(MANIFEST_PATH)) return { extensions: {} }; try { return JSON.parse(readFileSync(MANIFEST_PATH, 'utf-8')); } catch { return { extensions: {} }; } } function isExternalId(id) { return id && !id.startsWith('builtin/'); } function resolvePackageName(extensionId) { // Convention: extension IDs like "@scope/pkg/sub" map to package "@scope/pkg" // IDs like "@scope/pkg" map to package "@scope/pkg" const parts = extensionId.split('/'); if (parts[0].startsWith('@') && parts.length >= 2) { return parts.slice(0, 2).join('/'); } return parts[0]; } function isPackageInstalled(pkgName) { return existsSync(resolve(ROOT, 'node_modules', ...pkgName.split('/'))); } function generateMainBridge(manifest) { const externalMain = (manifest.extensions?.main ?? []).filter(isExternalId); const installedExts = externalMain.filter((id) => isPackageInstalled(resolvePackageName(id))); if (installedExts.length === 0) { return [ '// Auto-generated — no external main-process extensions installed.', '// To add extensions, configure clawx-extensions.json and link the package.', 'export function loadExternalMainExtensions(): void { /* no-op */ }', '', ].join('\n'); } const lines = [ '// Auto-generated by scripts/generate-ext-bridge.mjs — do not edit.', "import { extensionRegistry } from './registry';", '', ]; installedExts.forEach((id, i) => { const pkg = resolvePackageName(id); const subpath = id.slice(pkg.length + 1); // e.g. "enterprise-auth" const importPath = subpath ? `${pkg}/${subpath}` : pkg; const factoryName = `ext${i}`; lines.push(`import { ${guessFactoryExport(subpath || id)} as ${factoryName} } from '${importPath}';`); }); lines.push(''); lines.push('export function loadExternalMainExtensions(): void {'); installedExts.forEach((_id, i) => { lines.push(` const e${i} = ext${i}();`); lines.push(` if (e${i}) extensionRegistry.register(e${i});`); }); lines.push('}'); lines.push(''); return lines.join('\n'); } function generateRendererBridge(manifest) { const externalRenderer = (manifest.extensions?.renderer ?? []).filter(isExternalId); const installedExts = externalRenderer.filter((id) => isPackageInstalled(resolvePackageName(id))); if (installedExts.length === 0) { return [ '// Auto-generated — no external renderer extensions installed.', '// To add extensions, configure clawx-extensions.json and link the package.', 'export function loadExternalRendererExtensions(): void { /* no-op */ }', '', ].join('\n'); } const lines = [ '// Auto-generated by scripts/generate-ext-bridge.mjs — do not edit.', "import { rendererExtensionRegistry } from './registry';", '', ]; installedExts.forEach((id, i) => { const pkg = resolvePackageName(id); const subpath = id.slice(pkg.length + 1); const importPath = subpath ? `${pkg}/${subpath}` : pkg; const factoryName = `ext${i}`; lines.push(`import { ${guessFactoryExport(subpath || id)} as ${factoryName} } from '${importPath}';`); }); lines.push(''); lines.push('export function loadExternalRendererExtensions(): void {'); installedExts.forEach((_id, i) => { lines.push(` const e${i} = ext${i}();`); lines.push(` if (e${i}) rendererExtensionRegistry.register(e${i});`); }); lines.push('}'); lines.push(''); return lines.join('\n'); } function guessFactoryExport(subpath) { // "enterprise-auth" → "createEnterpriseAuthExtension" // "enterprise-ui" → "createEnterpriseUIExtension" // "skillshub-marketplace" → "createSkillshubMarketplaceExtension" const camel = subpath .replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase()) .replace(/^./, (c) => c.toUpperCase()); return `create${camel}Extension`; } // ─── Main ─── const manifest = readManifest(); mkdirSync(dirname(MAIN_OUT), { recursive: true }); mkdirSync(dirname(RENDERER_OUT), { recursive: true }); writeFileSync(MAIN_OUT, generateMainBridge(manifest)); writeFileSync(RENDERER_OUT, generateRendererBridge(manifest)); const mainExts = (manifest.extensions?.main ?? []).filter(isExternalId); const rendererExts = (manifest.extensions?.renderer ?? []).filter(isExternalId); const totalExternal = mainExts.length + rendererExts.length; if (totalExternal === 0) { console.log('[ext-bridge] No external extensions configured — generated empty stubs.'); } else { const installedMain = mainExts.filter((id) => isPackageInstalled(resolvePackageName(id))); const installedRenderer = rendererExts.filter((id) => isPackageInstalled(resolvePackageName(id))); console.log( `[ext-bridge] Generated bridges: ${installedMain.length}/${mainExts.length} main, ${installedRenderer.length}/${rendererExts.length} renderer extensions resolved.`, ); }