import type { Command } from 'commander'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { ExtensionManager, downloadPackage } from './manager'; import { startExtensionBridge } from './bridge'; import { packExtension } from './package'; import { createExtensionProject, testExtension } from './author'; export function registerExtensionCommands(program: Command): void { const extensions = program.command('extensions').description('Install and manage framework extensions'); extensions.command('create ').description('Create a portable Python event-map extension starter and author tests') .requiredOption('--id ', 'Unique lowercase extension id') .action((directory, options) => { createExtensionProject(directory, options.id); console.log(`Created ${path.resolve(directory)}. Next: codegraph extensions test ${directory}`); }); extensions.command('test ').description('Check compatibility, graph assertions, determinism and removal in disposable projects; executes your extension') .option('--fixtures ', 'Author fixture JSON relative to extension directory', 'extension.test.json') .action(async (directory, options) => { console.log(JSON.stringify(await testExtension(directory, { fixtures: options.fixtures }), null, 2)); }); extensions.command('list').option('--path ', 'Project directory', '.').action(options => { console.log(JSON.stringify(new ExtensionManager(options.path).list(), null, 2)); }); for (const action of ['install', 'update']) extensions.command(`${action} `) .description('Install a .cgext package or HTTPS release; extension code runs with your permissions') .option('--path ', 'Project directory', '.') .option('--replaces ', 'Explicitly replace comma-separated built-in resolvers') .action(async (source, options) => { const bytes = /^https?:/.test(source) ? await downloadPackage(source) : fs.readFileSync(path.resolve(source)); await new ExtensionManager(options.path, p => console.error(`${p.state}: ${p.message}`)).install({ bytes, source: /^https?:/.test(source) ? source : undefined, replaces: options.replaces?.split(',') }); }); for (const action of ['disable', 'enable', 'remove']) extensions.command(`${action} `) .option('--path ', 'Project directory', '.').action(async (id, options) => { const manager = new ExtensionManager(options.path, p => console.error(`${p.state}: ${p.message}`)); if (action === 'remove') await manager.remove(id); else await manager.setEnabled(id, action === 'enable'); }); extensions.command('connect [projects...]').description('Connect selected local projects to a marketplace; keep this process running') .requiredOption('--marketplace ', 'Marketplace website URL') .option('--port ', 'Local companion port', '0').action(async (projects: string[], options) => { const bridge = await startExtensionBridge(projects.length ? projects : ['.'], options.marketplace, Number(options.port)); console.log(`Open this connection link in your browser:\n${bridge.connectionUrl}\n\nKeep this process running. Press Ctrl+C to disconnect.`); process.once('SIGINT', () => { void bridge.close(); }); process.once('SIGTERM', () => { void bridge.close(); }); }); extensions.command('pack ').description('Bundle an extension directory; validates metadata without executing code') .requiredOption('--out ', 'Output .cgext file').action((directory, options) => { const bytes = packExtension(directory); fs.writeFileSync(options.out, bytes); console.log(`Packed extension into ${options.out}`); }); }