refactor: consolidate extension system to general-only with manifest-driven architecture
- Remove all sector-specific extensions (construction, ecommerce, export, hotel, restaurant, tech) — only general-purpose extensions remain - Move NE-bilaga and SRU export from extensions to core reports (lib/reports/) - Move moms-box-mapping from extensions/export/shared to lib/vat/ - Replace per-extension API routes with catch-all dispatcher (app/api/extensions/ext/[...path]/route.ts) - Add manifest.json for each extension with metadata, env vars, and deps - Add api-routes.ts pattern for extension-defined API endpoints - Add code generation scripts (generate-extension-registry, create-extension) - Add extensions.config.json for opt-in extension loading - Add extensions.schema.json for config validation - Add email service interface with noop default (lib/email/service.ts) - Add CI workflow (core-build.yml) to verify core builds with zero extensions - Add migration 045: expand account_type CHECK for untaxed_reserves - Update CLAUDE.md with comprehensive extension system documentation - Update all report engines and bookkeeping services for new imports - Clean up extensions.schema.json to only list existing extensions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
39e407644d
commit
03b569d708
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Scaffold a new extension with all required files.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/create-extension.ts \
|
||||
* --name my-extension \
|
||||
* --sector general \
|
||||
* --category operations \
|
||||
* --description "Short description of the extension"
|
||||
*
|
||||
* This will:
|
||||
* 1. Create extensions/<sector>/<name>/ directory
|
||||
* 2. Generate manifest.json, index.ts, and api-routes.ts
|
||||
* 3. Add the extension ID to extensions.schema.json enum array
|
||||
*/
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────
|
||||
|
||||
const VALID_SECTORS = [
|
||||
'general',
|
||||
'restaurant',
|
||||
'construction',
|
||||
'hotel',
|
||||
'tech',
|
||||
'ecommerce',
|
||||
'export',
|
||||
] as const
|
||||
|
||||
const VALID_CATEGORIES = [
|
||||
'import',
|
||||
'operations',
|
||||
'reports',
|
||||
'accounting',
|
||||
] as const
|
||||
|
||||
type Sector = (typeof VALID_SECTORS)[number]
|
||||
type Category = (typeof VALID_CATEGORIES)[number]
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const ROOT = path.resolve(__dirname, '..')
|
||||
|
||||
function usage(): never {
|
||||
console.error(`
|
||||
Usage:
|
||||
npx tsx scripts/create-extension.ts \\
|
||||
--name <extension-name> \\
|
||||
--sector <${VALID_SECTORS.join(' | ')}> \\
|
||||
--category <${VALID_CATEGORIES.join(' | ')}> \\
|
||||
--description "Short description"
|
||||
|
||||
Options:
|
||||
--name Extension slug (kebab-case, e.g. "my-extension")
|
||||
--sector Business sector for the extension
|
||||
--category Extension category
|
||||
--description Short description of the extension
|
||||
|
||||
Example:
|
||||
npx tsx scripts/create-extension.ts \\
|
||||
--name inventory-tracker \\
|
||||
--sector restaurant \\
|
||||
--category operations \\
|
||||
--description "Track inventory levels for restaurant supplies"
|
||||
`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse CLI arguments into a key-value map.
|
||||
* Supports --key value pairs.
|
||||
*/
|
||||
function parseArgs(argv: string[]): Record<string, string> {
|
||||
const args: Record<string, string> = {}
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i]
|
||||
if (arg.startsWith('--')) {
|
||||
const key = arg.slice(2)
|
||||
const value = argv[i + 1]
|
||||
if (!value || value.startsWith('--')) {
|
||||
console.error(`Error: Missing value for --${key}`)
|
||||
usage()
|
||||
}
|
||||
args[key] = value
|
||||
i++ // skip the value
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a kebab-case slug to camelCase.
|
||||
* e.g. "my-extension" -> "myExtension"
|
||||
*/
|
||||
function toCamelCase(slug: string): string {
|
||||
return slug.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a kebab-case slug to a camelCase export name.
|
||||
* e.g. "my-extension" -> "myExtensionExtension"
|
||||
*/
|
||||
function toExportName(slug: string): string {
|
||||
return `${toCamelCase(slug)}Extension`
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a kebab-case slug to a Title Case display name.
|
||||
* e.g. "my-extension" -> "My Extension"
|
||||
*/
|
||||
function toDisplayName(slug: string): string {
|
||||
return slug
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the extension name is a valid kebab-case slug.
|
||||
*/
|
||||
function validateName(name: string): void {
|
||||
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name)) {
|
||||
console.error(
|
||||
`Error: Extension name "${name}" is not valid kebab-case.`
|
||||
)
|
||||
console.error(' Must start with a lowercase letter, use only lowercase letters, digits, and hyphens.')
|
||||
console.error(' Example: "my-extension", "pos-import", "billable-hours"')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ── File generators ──────────────────────────────────────────
|
||||
|
||||
function generateManifest(
|
||||
name: string,
|
||||
sector: Sector,
|
||||
category: Category,
|
||||
description: string,
|
||||
exportName: string,
|
||||
entryPoint: string
|
||||
): string {
|
||||
const manifest = {
|
||||
id: name,
|
||||
sector,
|
||||
exportName,
|
||||
entryPoint,
|
||||
workspace: null,
|
||||
requiredEnvVars: [] as string[],
|
||||
optionalEnvVars: [] as string[],
|
||||
npmDependencies: [] as string[],
|
||||
definition: {
|
||||
name: toDisplayName(name),
|
||||
category,
|
||||
icon: 'Box',
|
||||
dataPattern: 'core',
|
||||
description,
|
||||
longDescription: description,
|
||||
},
|
||||
}
|
||||
return JSON.stringify(manifest, null, 2) + '\n'
|
||||
}
|
||||
|
||||
function generateIndexTs(
|
||||
name: string,
|
||||
sector: Sector,
|
||||
exportName: string,
|
||||
displayName: string
|
||||
): string {
|
||||
const apiRoutesVar = `${toCamelCase(name)}ApiRoutes`
|
||||
return `import type { Extension } from '@/lib/extensions/types'
|
||||
import { ${apiRoutesVar} } from './api-routes'
|
||||
|
||||
/**
|
||||
* ${displayName} Extension
|
||||
*
|
||||
* TODO: Add extension description here.
|
||||
*/
|
||||
export const ${exportName}: Extension = {
|
||||
id: '${name}',
|
||||
name: '${displayName}',
|
||||
version: '0.1.0',
|
||||
sector: '${sector}',
|
||||
apiRoutes: ${apiRoutesVar},
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
function generateApiRoutesTs(name: string): string {
|
||||
const apiRoutesVar = `${toCamelCase(name)}ApiRoutes`
|
||||
return `import type { ApiRouteDefinition } from '@/lib/extensions/types'
|
||||
|
||||
/**
|
||||
* API routes for the ${toDisplayName(name)} extension.
|
||||
*
|
||||
* Add route definitions here as the extension grows.
|
||||
* Each route will be served under /api/extensions/${name}/<path>.
|
||||
*/
|
||||
export const ${apiRoutesVar}: ApiRouteDefinition[] = []
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the new extension ID to extensions.schema.json enum array.
|
||||
*/
|
||||
function updateSchemaJson(name: string): void {
|
||||
const schemaPath = path.join(ROOT, 'extensions.schema.json')
|
||||
|
||||
if (!fs.existsSync(schemaPath)) {
|
||||
console.warn(' Warning: extensions.schema.json not found, skipping enum update.')
|
||||
return
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(schemaPath, 'utf-8')
|
||||
const schema = JSON.parse(content)
|
||||
|
||||
const enumArray: string[] = schema?.properties?.extensions?.items?.enum
|
||||
if (!Array.isArray(enumArray)) {
|
||||
console.warn(' Warning: Could not find enum array in extensions.schema.json, skipping.')
|
||||
return
|
||||
}
|
||||
|
||||
if (enumArray.includes(name)) {
|
||||
console.log(` extensions.schema.json already contains "${name}", skipping.`)
|
||||
return
|
||||
}
|
||||
|
||||
enumArray.push(name)
|
||||
|
||||
fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 2) + '\n', 'utf-8')
|
||||
console.log(` Updated extensions.schema.json — added "${name}" to enum array.`)
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────
|
||||
|
||||
function main(): void {
|
||||
const args = parseArgs(process.argv.slice(2))
|
||||
|
||||
const name = args['name']
|
||||
const sector = args['sector'] as Sector | undefined
|
||||
const category = args['category'] as Category | undefined
|
||||
const description = args['description']
|
||||
|
||||
// Validate required arguments
|
||||
if (!name || !sector || !category || !description) {
|
||||
const missing: string[] = []
|
||||
if (!name) missing.push('--name')
|
||||
if (!sector) missing.push('--sector')
|
||||
if (!category) missing.push('--category')
|
||||
if (!description) missing.push('--description')
|
||||
console.error(`Error: Missing required arguments: ${missing.join(', ')}`)
|
||||
usage()
|
||||
}
|
||||
|
||||
validateName(name)
|
||||
|
||||
if (!VALID_SECTORS.includes(sector)) {
|
||||
console.error(`Error: Invalid sector "${sector}".`)
|
||||
console.error(` Valid sectors: ${VALID_SECTORS.join(', ')}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!VALID_CATEGORIES.includes(category)) {
|
||||
console.error(`Error: Invalid category "${category}".`)
|
||||
console.error(` Valid categories: ${VALID_CATEGORIES.join(', ')}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const exportName = toExportName(name)
|
||||
const displayName = toDisplayName(name)
|
||||
const entryPoint = `@/extensions/${sector}/${name}`
|
||||
const extensionDir = path.join(ROOT, 'extensions', sector, name)
|
||||
|
||||
// Check if extension already exists
|
||||
if (fs.existsSync(extensionDir)) {
|
||||
console.error(`Error: Extension directory already exists: ${extensionDir}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`\nScaffolding extension: ${displayName}`)
|
||||
console.log(` ID: ${name}`)
|
||||
console.log(` Sector: ${sector}`)
|
||||
console.log(` Category: ${category}`)
|
||||
console.log(` Export: ${exportName}`)
|
||||
console.log(` Entry: ${entryPoint}`)
|
||||
console.log()
|
||||
|
||||
// Create directory
|
||||
fs.mkdirSync(extensionDir, { recursive: true })
|
||||
console.log(` Created directory: extensions/${sector}/${name}/`)
|
||||
|
||||
// Write manifest.json
|
||||
const manifestPath = path.join(extensionDir, 'manifest.json')
|
||||
fs.writeFileSync(
|
||||
manifestPath,
|
||||
generateManifest(name, sector, category, description, exportName, entryPoint),
|
||||
'utf-8'
|
||||
)
|
||||
console.log(` Created: extensions/${sector}/${name}/manifest.json`)
|
||||
|
||||
// Write index.ts
|
||||
const indexPath = path.join(extensionDir, 'index.ts')
|
||||
fs.writeFileSync(
|
||||
indexPath,
|
||||
generateIndexTs(name, sector, exportName, displayName),
|
||||
'utf-8'
|
||||
)
|
||||
console.log(` Created: extensions/${sector}/${name}/index.ts`)
|
||||
|
||||
// Write api-routes.ts
|
||||
const apiRoutesPath = path.join(extensionDir, 'api-routes.ts')
|
||||
fs.writeFileSync(apiRoutesPath, generateApiRoutesTs(name), 'utf-8')
|
||||
console.log(` Created: extensions/${sector}/${name}/api-routes.ts`)
|
||||
|
||||
// Update extensions.schema.json
|
||||
updateSchemaJson(name)
|
||||
|
||||
// Summary
|
||||
console.log(`
|
||||
Done! Next steps:
|
||||
|
||||
1. Edit the manifest.json to customize icon, dataPattern, and longDescription:
|
||||
extensions/${sector}/${name}/manifest.json
|
||||
|
||||
2. Implement extension logic in index.ts:
|
||||
extensions/${sector}/${name}/index.ts
|
||||
|
||||
3. Add API routes if needed in api-routes.ts:
|
||||
extensions/${sector}/${name}/api-routes.ts
|
||||
|
||||
4. Add a static import to FIRST_PARTY_EXTENSIONS in lib/extensions/loader.ts:
|
||||
import { ${exportName} } from '@/extensions/${sector}/${name}'
|
||||
|
||||
5. Add extension metadata to the sector registry in lib/extensions/sectors.ts
|
||||
|
||||
6. Enable the extension in extensions.config.json:
|
||||
Add "${name}" to the extensions array
|
||||
`)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* Extension Registry Generator
|
||||
*
|
||||
* Reads extensions.config.json and manifest.json files to generate:
|
||||
* - lib/extensions/_generated/extension-list.ts
|
||||
* - lib/extensions/_generated/workspace-map.tsx
|
||||
* - lib/extensions/_generated/sector-definitions.ts
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/generate-extension-registry.ts # Generate files
|
||||
* npx tsx scripts/generate-extension-registry.ts --list # List available extensions
|
||||
*/
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const ROOT = path.resolve(__dirname, '..')
|
||||
const CONFIG_PATH = path.join(ROOT, 'extensions.config.json')
|
||||
const EXTENSIONS_DIR = path.join(ROOT, 'extensions')
|
||||
const OUTPUT_DIR = path.join(ROOT, 'lib', 'extensions', '_generated')
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────
|
||||
|
||||
interface ManifestDefinition {
|
||||
name: string
|
||||
category: string
|
||||
icon: string
|
||||
dataPattern: string
|
||||
readsCoreTables?: string[]
|
||||
hasOwnData?: boolean
|
||||
description: string
|
||||
longDescription: string
|
||||
quickAction?: {
|
||||
label: string
|
||||
description: string
|
||||
icon: string
|
||||
href?: string
|
||||
event?: string
|
||||
order?: number
|
||||
}
|
||||
subscriptionNotice?: string
|
||||
}
|
||||
|
||||
interface Manifest {
|
||||
id: string
|
||||
sector: string
|
||||
exportName: string | null
|
||||
entryPoint: string | null
|
||||
workspace: string | null
|
||||
requiredEnvVars: string[]
|
||||
optionalEnvVars: string[]
|
||||
npmDependencies: string[]
|
||||
definition: ManifestDefinition
|
||||
}
|
||||
|
||||
interface Config {
|
||||
extensions: string[]
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────
|
||||
|
||||
function findManifests(dir: string): string[] {
|
||||
const results: string[] = []
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...findManifests(fullPath))
|
||||
} else if (entry.name === 'manifest.json') {
|
||||
results.push(fullPath)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
function loadAllManifests(): Map<string, Manifest> {
|
||||
const manifestPaths = findManifests(EXTENSIONS_DIR)
|
||||
const map = new Map<string, Manifest>()
|
||||
for (const mp of manifestPaths) {
|
||||
const manifest: Manifest = JSON.parse(fs.readFileSync(mp, 'utf-8'))
|
||||
if (map.has(manifest.id)) {
|
||||
console.error(`ERROR: Duplicate extension ID "${manifest.id}" found in:\n ${mp}\n (already defined elsewhere)`)
|
||||
process.exit(1)
|
||||
}
|
||||
map.set(manifest.id, manifest)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
function loadConfig(): Config {
|
||||
if (!fs.existsSync(CONFIG_PATH)) {
|
||||
console.warn('Warning: extensions.config.json not found, using empty config')
|
||||
return { extensions: [] }
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'))
|
||||
}
|
||||
|
||||
// ── Generators ───────────────────────────────────────────────
|
||||
|
||||
function generateExtensionList(manifests: Manifest[]): string {
|
||||
const withRuntime = manifests.filter(m => m.exportName && m.entryPoint)
|
||||
|
||||
const imports = withRuntime.map(
|
||||
m => `import { ${m.exportName} } from '${m.entryPoint}'`
|
||||
)
|
||||
|
||||
const entries = withRuntime.map(m => ` ${m.exportName},`)
|
||||
|
||||
return [
|
||||
`// AUTO-GENERATED — do not edit. Run \`npm run setup:extensions\` to regenerate.`,
|
||||
`import type { Extension } from '../types'`,
|
||||
...imports,
|
||||
``,
|
||||
`export const FIRST_PARTY_EXTENSIONS: Extension[] = [`,
|
||||
...entries,
|
||||
`]`,
|
||||
``,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function generateWorkspaceMap(manifests: Manifest[]): string {
|
||||
const withWorkspace = manifests.filter(m => m.workspace)
|
||||
|
||||
const dynamicImports = withWorkspace.map(m => {
|
||||
const key = `${m.sector}/${m.id}`
|
||||
return ` '${key}': dynamic(() => import('${m.workspace}')),`
|
||||
})
|
||||
|
||||
return [
|
||||
`// AUTO-GENERATED — do not edit. Run \`npm run setup:extensions\` to regenerate.`,
|
||||
`import dynamic from 'next/dynamic'`,
|
||||
`import type { ComponentType } from 'react'`,
|
||||
`import type { WorkspaceComponentProps } from '../workspace-registry'`,
|
||||
``,
|
||||
`export const WORKSPACES: Record<string, ComponentType<WorkspaceComponentProps>> = {`,
|
||||
...dynamicImports,
|
||||
`}`,
|
||||
``,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function generateSectorDefinitions(manifests: Manifest[]): string {
|
||||
// Group by sector
|
||||
const bySector = new Map<string, Manifest[]>()
|
||||
for (const m of manifests) {
|
||||
const existing = bySector.get(m.sector) ?? []
|
||||
existing.push(m)
|
||||
bySector.set(m.sector, existing)
|
||||
}
|
||||
|
||||
const sectorEntries: string[] = []
|
||||
for (const [sector, sectorManifests] of bySector) {
|
||||
const defs = sectorManifests.map(m => {
|
||||
const def: Record<string, unknown> = {
|
||||
slug: m.id,
|
||||
name: m.definition.name,
|
||||
sector: m.sector,
|
||||
category: m.definition.category,
|
||||
icon: m.definition.icon,
|
||||
dataPattern: m.definition.dataPattern,
|
||||
description: m.definition.description,
|
||||
longDescription: m.definition.longDescription,
|
||||
}
|
||||
if (m.definition.readsCoreTables) def.readsCoreTables = m.definition.readsCoreTables
|
||||
if (m.definition.hasOwnData) def.hasOwnData = m.definition.hasOwnData
|
||||
if (m.definition.quickAction) def.quickAction = m.definition.quickAction
|
||||
if (m.definition.subscriptionNotice) def.subscriptionNotice = m.definition.subscriptionNotice
|
||||
return ` ${JSON.stringify(def, null, 6).split('\n').join('\n ')},`
|
||||
})
|
||||
sectorEntries.push(` '${sector}': [\n${defs.join('\n')}\n ],`)
|
||||
}
|
||||
|
||||
return [
|
||||
`// AUTO-GENERATED — do not edit. Run \`npm run setup:extensions\` to regenerate.`,
|
||||
`import type { ExtensionDefinition } from '../types'`,
|
||||
``,
|
||||
`export const EXTENSION_DEFINITIONS: Record<string, ExtensionDefinition[]> = {`,
|
||||
...sectorEntries,
|
||||
`}`,
|
||||
``,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function generateEnabledExtensions(manifests: Manifest[]): string {
|
||||
const ids = manifests.map(m => ` '${m.id}',`)
|
||||
|
||||
return [
|
||||
`// AUTO-GENERATED — do not edit. Run \`npm run setup:extensions\` to regenerate.`,
|
||||
``,
|
||||
`export const ENABLED_EXTENSION_IDS: ReadonlySet<string> = new Set([`,
|
||||
...ids,
|
||||
`])`,
|
||||
``,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
// ── Env var check ────────────────────────────────────────────
|
||||
|
||||
function checkEnvVars(manifests: Manifest[]): void {
|
||||
const warnings: string[] = []
|
||||
for (const m of manifests) {
|
||||
for (const envVar of m.requiredEnvVars) {
|
||||
if (!process.env[envVar]) {
|
||||
warnings.push(` ${envVar} (required by ${m.id})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (warnings.length > 0) {
|
||||
console.warn('\nWarning: Missing environment variables:')
|
||||
for (const w of warnings) {
|
||||
console.warn(w)
|
||||
}
|
||||
console.warn('Extensions will be loaded but may not function correctly.\n')
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────
|
||||
|
||||
function main(): void {
|
||||
const args = process.argv.slice(2)
|
||||
const allManifests = loadAllManifests()
|
||||
|
||||
// --list mode: print all available extensions
|
||||
if (args.includes('--list')) {
|
||||
console.log('\nAvailable extensions:\n')
|
||||
const sorted = [...allManifests.values()].sort((a, b) => {
|
||||
if (a.sector !== b.sector) return a.sector.localeCompare(b.sector)
|
||||
return a.id.localeCompare(b.id)
|
||||
})
|
||||
let currentSector = ''
|
||||
for (const m of sorted) {
|
||||
if (m.sector !== currentSector) {
|
||||
currentSector = m.sector
|
||||
console.log(`\n [${currentSector}]`)
|
||||
}
|
||||
const envNote = m.requiredEnvVars.length > 0
|
||||
? ` (requires: ${m.requiredEnvVars.join(', ')})`
|
||||
: ''
|
||||
console.log(` ${m.id.padEnd(25)} ${m.definition.name}${envNote}`)
|
||||
}
|
||||
console.log('')
|
||||
return
|
||||
}
|
||||
|
||||
// Normal mode: generate registry files
|
||||
const config = loadConfig()
|
||||
|
||||
// Validate enabled IDs
|
||||
for (const id of config.extensions) {
|
||||
if (!allManifests.has(id)) {
|
||||
console.error(`ERROR: Unknown extension ID "${id}" in extensions.config.json`)
|
||||
console.error(`Available IDs: ${[...allManifests.keys()].sort().join(', ')}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
const enabledManifests = config.extensions.map(id => allManifests.get(id)!)
|
||||
|
||||
// Ensure output directory exists
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true })
|
||||
|
||||
// Generate files
|
||||
fs.writeFileSync(
|
||||
path.join(OUTPUT_DIR, 'extension-list.ts'),
|
||||
generateExtensionList(enabledManifests),
|
||||
)
|
||||
fs.writeFileSync(
|
||||
path.join(OUTPUT_DIR, 'workspace-map.tsx'),
|
||||
generateWorkspaceMap(enabledManifests),
|
||||
)
|
||||
fs.writeFileSync(
|
||||
path.join(OUTPUT_DIR, 'sector-definitions.ts'),
|
||||
generateSectorDefinitions(enabledManifests),
|
||||
)
|
||||
fs.writeFileSync(
|
||||
path.join(OUTPUT_DIR, 'enabled-extensions.ts'),
|
||||
generateEnabledExtensions(enabledManifests),
|
||||
)
|
||||
|
||||
// Summary
|
||||
const enabledNames = enabledManifests.map(m => m.id)
|
||||
if (enabledNames.length > 0) {
|
||||
console.log(`Enabled: ${enabledNames.join(', ')}`)
|
||||
} else {
|
||||
console.log('No extensions enabled (core-only mode)')
|
||||
}
|
||||
|
||||
// Check env vars
|
||||
checkEnvVars(enabledManifests)
|
||||
}
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user