Files
DeskClaw/scripts/generate-ext-bridge.mjs

163 lines
5.7 KiB
JavaScript

#!/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.`,
);
}