From a0261afede0a9335a38f83863060783eed76bdff Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Tue, 30 Dec 2025 23:28:47 +0000 Subject: [PATCH] code: scripts,package,generate (1 files) --- scripts/generate-package.ts | 583 ++++++++++++++++++++++++++++++++++++ 1 file changed, 583 insertions(+) create mode 100644 scripts/generate-package.ts diff --git a/scripts/generate-package.ts b/scripts/generate-package.ts new file mode 100644 index 000000000..13026a0f2 --- /dev/null +++ b/scripts/generate-package.ts @@ -0,0 +1,583 @@ +#!/usr/bin/env node +/** + * Package Template Generator CLI + * + * Usage: + * npx ts-node scripts/generate-package.ts [options] + * + * Examples: + * npx ts-node scripts/generate-package.ts my_package + * npx ts-node scripts/generate-package.ts my_forum --with-schema --entities ForumThread,ForumPost + * npx ts-node scripts/generate-package.ts my_widget --dependency --category ui + */ + +import * as fs from 'fs' +import * as path from 'path' + +interface PackageConfig { + packageId: string + name: string + description: string + author: string + category: string + minLevel: number + primary: boolean + withSchema: boolean + withTests: boolean + withComponents: boolean + entities: string[] + components: string[] + dependencies: string[] +} + +interface GeneratedFile { + path: string + content: string +} + +const CATEGORIES = [ + 'ui', 'editors', 'tools', 'social', 'media', 'gaming', + 'admin', 'config', 'core', 'demo', 'development', 'managers' +] + +function parseArgs(args: string[]): { packageId: string; options: Partial } { + const options: Partial = { + category: 'ui', + minLevel: 2, + primary: true, + withSchema: false, + withTests: true, + withComponents: false, + entities: [], + components: [], + dependencies: [] + } + + let packageId = '' + + for (let i = 0; i < args.length; i++) { + const arg = args[i] + + if (!arg.startsWith('--') && !packageId) { + packageId = arg + } else if (arg === '--name') { + options.name = args[++i] + } else if (arg === '--description') { + options.description = args[++i] + } else if (arg === '--category') { + options.category = args[++i] + } else if (arg === '--min-level') { + options.minLevel = parseInt(args[++i], 10) + } else if (arg === '--primary') { + options.primary = true + } else if (arg === '--dependency') { + options.primary = false + } else if (arg === '--with-schema') { + options.withSchema = true + } else if (arg === '--entities') { + options.entities = args[++i].split(',').map(e => e.trim()) + } else if (arg === '--with-components') { + options.withComponents = true + } else if (arg === '--components') { + options.components = args[++i].split(',').map(c => c.trim()) + } else if (arg === '--deps') { + options.dependencies = args[++i].split(',').map(d => d.trim()) + } + } + + return { packageId, options } +} + +function toPascalCase(str: string): string { + return str.split('_').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join('') +} + +function toDisplayName(packageId: string): string { + return packageId.split('_').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(' ') +} + +function generateMetadata(config: PackageConfig): string { + const prefix = config.packageId.replace(/_/g, '.') + + const permissions: Record = { + [`${prefix}.view`]: { + minLevel: config.minLevel, + description: `View ${config.name}` + } + } + + if (config.primary) { + permissions[`${prefix}.edit`] = { + minLevel: config.minLevel, + description: `Edit ${config.name} content` + } + } + + if (config.withSchema && config.entities.length > 0) { + for (const entity of config.entities) { + const entityLower = entity.toLowerCase() + permissions[`${prefix}.${entityLower}.create`] = { + minLevel: config.minLevel, + description: `Create ${entity}` + } + permissions[`${prefix}.${entityLower}.update`] = { + minLevel: config.minLevel, + description: `Update ${entity}` + } + permissions[`${prefix}.${entityLower}.delete`] = { + minLevel: Math.min(config.minLevel + 1, 6), + description: `Delete ${entity}` + } + } + } + + const metadata = { + packageId: config.packageId, + name: config.name, + version: '1.0.0', + description: config.description, + icon: 'static_content/icon.svg', + author: config.author, + category: config.category, + primary: config.primary, + dependencies: config.dependencies, + devDependencies: ['lua_test'], + exports: { + components: config.components, + scripts: ['init'] + }, + tests: { + scripts: ['tests/metadata.test.lua', 'tests/components.test.lua'], + cases: ['tests/metadata.cases.json', 'tests/components.cases.json'] + }, + minLevel: config.minLevel, + ...(config.withSchema && config.entities.length > 0 ? { + schema: { + entities: config.entities, + path: 'schema/entities.yaml' + } + } : {}), + permissions + } + + return JSON.stringify(metadata, null, 2) +} + +function generateInitLua(config: PackageConfig): string { + return `--- ${config.name} initialization +--- @module init + +local M = {} + +---@class InstallContext +---@field version string + +---@class InstallResult +---@field message string +---@field version string + +---Called when package is installed +---@param context InstallContext +---@return InstallResult +function M.on_install(context) + return { + message = "${config.name} installed successfully", + version = context.version + } +end + +---Called when package is uninstalled +---@return table +function M.on_uninstall() + return { message = "${config.name} removed" } +end + +return M +` +} + +function generateTestLua(testName: string, config: PackageConfig): string { + return `-- ${testName} tests for ${config.packageId} + +describe("${config.name} - ${testName}", function() + it("should pass basic validation", function() + expect(true).toBe(true) + end) + + it("should have required fields", function() + -- TODO: Add specific tests + expect(true).toBe(true) + end) +end) +` +} + +function generateTestCases(): string { + return JSON.stringify([ + { name: 'valid_input', input: {}, expected: { valid: true } }, + { name: 'invalid_input', input: { invalid: true }, expected: { valid: false } } + ], null, 2) +} + +function generateSchemaYaml(config: PackageConfig): string { + if (!config.entities.length) return '# No entities defined\n' + + const lines = [ + `# ${config.name} Entity Definitions`, + '# Auto-generated by package template generator', + '' + ] + + for (const entity of config.entities) { + const prefixedEntity = `Pkg_${toPascalCase(config.packageId)}_${entity}` + lines.push(`${prefixedEntity}:`) + lines.push(` description: "${entity} entity for ${config.name}"`) + lines.push(' fields:') + lines.push(' id:') + lines.push(' type: string') + lines.push(' primary: true') + lines.push(' tenantId:') + lines.push(' type: string') + lines.push(' required: true') + lines.push(' index: true') + lines.push(' createdAt:') + lines.push(' type: datetime') + lines.push(' default: now') + lines.push(' updatedAt:') + lines.push(' type: datetime') + lines.push(' onUpdate: now') + lines.push(' # TODO: Add entity-specific fields') + lines.push('') + } + + return lines.join('\n') +} + +function generateDbOperations(config: PackageConfig): string { + const lines = [ + `-- Database operations for ${config.name}`, + '-- Auto-generated by package template generator', + '', + 'local M = {}', + '' + ] + + for (const entity of config.entities) { + const entityLower = entity.toLowerCase() + + lines.push(`-- ${entity} operations`) + lines.push('') + lines.push(`---List all ${entity} records`) + lines.push('---@param ctx DBALContext') + lines.push('---@return table[]') + lines.push(`function M.list_${entityLower}(ctx)`) + lines.push(' -- TODO: Implement list operation') + lines.push(' return {}') + lines.push('end') + lines.push('') + lines.push(`---Get a single ${entity} by ID`) + lines.push('---@param ctx DBALContext') + lines.push('---@param id string') + lines.push('---@return table|nil') + lines.push(`function M.get_${entityLower}(ctx, id)`) + lines.push(' -- TODO: Implement get operation') + lines.push(' return nil') + lines.push('end') + lines.push('') + lines.push(`---Create a new ${entity}`) + lines.push('---@param ctx DBALContext') + lines.push('---@param data table') + lines.push('---@return table') + lines.push(`function M.create_${entityLower}(ctx, data)`) + lines.push(' -- TODO: Implement create operation') + lines.push(' return data') + lines.push('end') + lines.push('') + lines.push(`---Update an existing ${entity}`) + lines.push('---@param ctx DBALContext') + lines.push('---@param id string') + lines.push('---@param data table') + lines.push('---@return table|nil') + lines.push(`function M.update_${entityLower}(ctx, id, data)`) + lines.push(' -- TODO: Implement update operation') + lines.push(' return nil') + lines.push('end') + lines.push('') + lines.push(`---Delete a ${entity}`) + lines.push('---@param ctx DBALContext') + lines.push('---@param id string') + lines.push('---@return boolean') + lines.push(`function M.delete_${entityLower}(ctx, id)`) + lines.push(' -- TODO: Implement delete operation') + lines.push(' return false') + lines.push('end') + lines.push('') + } + + lines.push('return M') + return lines.join('\n') +} + +function generateComponentsJson(config: PackageConfig): string { + const components = config.components.map(name => ({ + id: `${config.packageId}_${name.toLowerCase()}`, + type: 'container', + name, + description: `${name} component for ${config.name}`, + props: {}, + layout: { type: 'flex', props: { direction: 'column', gap: 2 } }, + bindings: {} + })) + return JSON.stringify(components, null, 2) +} + +function generateLayoutJson(config: PackageConfig): string { + const layout = { + id: `${config.packageId}_layout`, + name: `${config.name} Layout`, + type: 'page', + props: { title: config.name, minLevel: config.minLevel }, + children: [ + { + id: `${config.packageId}_header`, + type: 'container', + props: { variant: 'header' }, + children: [ + { id: `${config.packageId}_title`, type: 'text', props: { variant: 'h1', content: config.name } }, + { id: `${config.packageId}_description`, type: 'text', props: { variant: 'body1', content: config.description } } + ] + }, + { + id: `${config.packageId}_content`, + type: 'container', + props: { variant: 'main' }, + children: [ + { id: `${config.packageId}_placeholder`, type: 'text', props: { content: 'Add your components here' } } + ] + } + ] + } + return JSON.stringify(layout, null, 2) +} + +function generateIconSvg(config: PackageConfig): string { + const letter = config.name.charAt(0).toUpperCase() + return ` + + ${letter} +` +} + +function generateReadme(config: PackageConfig): string { + const lines = [ + `# ${config.name}`, + '', + config.description, + '', + '## Installation', + '', + 'This package is part of the MetaBuilder platform and is installed automatically.', + '', + '## Access Level', + '', + `Minimum level required: **${config.minLevel}**`, + '', + config.primary + ? 'This is a **primary package** that can own routes.' + : 'This is a **dependency package** that provides shared functionality.', + '' + ] + + if (config.withSchema && config.entities.length > 0) { + lines.push('## Entities') + lines.push('') + for (const entity of config.entities) { + lines.push(`- ${entity}`) + } + lines.push('') + } + + if (config.components.length > 0) { + lines.push('## Components') + lines.push('') + for (const comp of config.components) { + lines.push(`- \`${comp}\``) + } + lines.push('') + } + + lines.push('## Development') + lines.push('') + lines.push('```bash') + lines.push('# Run tests') + lines.push(`npm run test:package ${config.packageId}`) + lines.push('```') + lines.push('') + + return lines.join('\n') +} + +function generateIndexTs(config: PackageConfig): string { + return `// ${config.name} package exports +// Auto-generated by package template generator + +import metadata from './metadata.json' +import components from './components.json' +import layout from './layout.json' + +export const packageSeed = { + metadata, + components, + layout, +} + +export default packageSeed +` +} + +function generate(config: PackageConfig): GeneratedFile[] { + const files: GeneratedFile[] = [] + + // Core files + files.push({ path: 'seed/metadata.json', content: generateMetadata(config) }) + files.push({ path: 'seed/components.json', content: generateComponentsJson(config) }) + files.push({ path: 'seed/layout.json', content: generateLayoutJson(config) }) + files.push({ path: 'seed/scripts/init.lua', content: generateInitLua(config) }) + files.push({ path: 'seed/index.ts', content: generateIndexTs(config) }) + + // Schema files + if (config.withSchema && config.entities.length > 0) { + files.push({ path: 'seed/schema/entities.yaml', content: generateSchemaYaml(config) }) + files.push({ path: 'seed/scripts/db/operations.lua', content: generateDbOperations(config) }) + } + + // Test files + if (config.withTests) { + files.push({ path: 'seed/scripts/tests/metadata.test.lua', content: generateTestLua('metadata', config) }) + files.push({ path: 'seed/scripts/tests/components.test.lua', content: generateTestLua('components', config) }) + files.push({ path: 'seed/scripts/tests/metadata.cases.json', content: generateTestCases() }) + files.push({ path: 'seed/scripts/tests/components.cases.json', content: generateTestCases() }) + } + + // Static content + files.push({ path: 'static_content/icon.svg', content: generateIconSvg(config) }) + files.push({ path: 'README.md', content: generateReadme(config) }) + + return files +} + +function writeFiles(packagePath: string, files: GeneratedFile[]): void { + for (const file of files) { + const fullPath = path.join(packagePath, file.path) + const dir = path.dirname(fullPath) + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + + fs.writeFileSync(fullPath, file.content) + console.log(` Created: ${file.path}`) + } +} + +function printHelp(): void { + console.log(` +Package Template Generator +========================== + +Usage: npx ts-node scripts/generate-package.ts [options] + +Options: + --name Display name (default: derived from package_id) + --description Package description + --category Package category (default: ui) + --min-level Minimum access level 0-6 (default: 2) + --primary Package can own routes (default) + --dependency Package is dependency-only + --with-schema Include database schema scaffolding + --entities Entity names for schema (comma-separated, PascalCase) + --with-components Include component scaffolding + --components Component names (comma-separated, PascalCase) + --deps Package dependencies (comma-separated) + +Categories: ${CATEGORIES.join(', ')} + +Examples: + npx ts-node scripts/generate-package.ts my_package + npx ts-node scripts/generate-package.ts my_forum --with-schema --entities ForumThread,ForumPost + npx ts-node scripts/generate-package.ts my_widget --dependency --category ui + npx ts-node scripts/generate-package.ts my_dashboard --with-components --components StatCard,Chart +`) +} + +// Main +const args = process.argv.slice(2) + +if (args.length === 0 || args.includes('--help') || args.includes('-h')) { + printHelp() + process.exit(0) +} + +const { packageId, options } = parseArgs(args) + +if (!packageId) { + console.error('Error: package_id is required') + process.exit(1) +} + +if (!/^[a-z][a-z0-9_]*$/.test(packageId)) { + console.error('Error: package_id must be lowercase with underscores, starting with a letter') + process.exit(1) +} + +if (options.category && !CATEGORIES.includes(options.category)) { + console.error(`Error: Invalid category. Must be one of: ${CATEGORIES.join(', ')}`) + process.exit(1) +} + +const config: PackageConfig = { + packageId, + name: options.name || toDisplayName(packageId), + description: options.description || `${toDisplayName(packageId)} package for MetaBuilder`, + author: 'MetaBuilder', + category: options.category || 'ui', + minLevel: options.minLevel ?? 2, + primary: options.primary ?? true, + withSchema: options.withSchema || false, + withTests: options.withTests ?? true, + withComponents: options.withComponents || false, + entities: options.entities || [], + components: options.components || [], + dependencies: options.dependencies || [] +} + +const packagesDir = path.join(process.cwd(), 'packages') +const packagePath = path.join(packagesDir, packageId) + +if (fs.existsSync(packagePath)) { + console.error(`Error: Package directory already exists: ${packagePath}`) + process.exit(1) +} + +console.log(`\nGenerating package: ${config.name}`) +console.log(` Location: ${packagePath}`) +console.log(` Category: ${config.category}`) +console.log(` Level: ${config.minLevel}`) +console.log(` Type: ${config.primary ? 'Primary' : 'Dependency'}`) +if (config.withSchema) console.log(` Entities: ${config.entities.join(', ')}`) +if (config.components.length) console.log(` Components: ${config.components.join(', ')}`) +console.log('') + +const files = generate(config) +writeFiles(packagePath, files) + +console.log(`\n✅ Package '${packageId}' created successfully!`) +console.log(`\nNext steps:`) +console.log(` 1. Review generated files in packages/${packageId}/`) +console.log(` 2. Add package-specific logic to seed/scripts/`) +console.log(` 3. Update components.json with your component definitions`) +console.log(` 4. Run tests: npm run test:package ${packageId}`)