/** * Scaffolding utilities for mx CLI. * * Provides functions for creating package structures, templates, etc. */ import * as fs from 'node:fs'; import * as path from 'node:path'; import { validateManifestForMxCli } from './manifestValidator.js'; export interface ScaffoldOptions { name: string; description?: string; version?: string; } /** * Create a basic package structure. */ export function createPackageStructure(options: ScaffoldOptions): void { const root = path.resolve(options.name); const folders = ['src', 'examples', 'skills', 'tests']; fs.mkdirSync(root, { recursive: true }); for (const folder of folders) { fs.mkdirSync(path.join(root, folder), { recursive: true }); } } /** * Generate matrix.json content. */ export function generateMatrixJson(options: ScaffoldOptions): string { return JSON.stringify({ name: options.name, version: options.version ?? '0.1.0', description: options.description ?? '', runtime: { language: 'typescript', entry: 'src/index.ts', }, components: [], permissions: { fsPolicy: 'none', }, }, null, 2); } export function toKebabCase(value: string): string { return value .trim() .replace(/([a-z0-9])([A-Z])/g, '$1-$2') .replace(/[\s_]+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .toLowerCase(); } export function toPascalCase(value: string): string { return value .trim() .replace(/[_-]+/g, ' ') .split(/\s+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(''); } export function writeJsonFile(filePath: string, value: unknown): void { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n', 'utf8'); } export function validateMatrixManifest(manifest: unknown): { valid: boolean; errors: string[] } { const result = validateManifestForMxCli(manifest); return { valid: result.valid, errors: result.errors }; } /** * Generate package.json for a scaffolded package. */ export function generatePackageJson(name: string, options?: { webapp?: boolean; service?: boolean; scripts?: Record; }): Record { const scripts: Record = options?.scripts ?? (options?.webapp ? { dev: 'vite', build: 'vite build', test: 'node --import tsx --test tests/**/*.spec.ts' } : options?.service ? { build: 'tsc -p tsconfig.json', 'build:watch': 'tsc -w -p tsconfig.json --preserveWatchOutput', dev: 'tsc -w -p tsconfig.json --preserveWatchOutput', test: 'node --import tsx --test tests/**/*.spec.ts', } : { build: 'tsc', test: 'node --import tsx --test tests/**/*.spec.ts' }); const dependencies: Record = {}; const devDependencies: Record = { 'typescript': '^5.0.0', }; if (options?.service) { dependencies['@open-matrix/core'] = '*'; } else { devDependencies['@open-matrix/core'] = '*'; } if (options?.webapp) { devDependencies['@open-matrix/federation'] = '*'; devDependencies['vite'] = '^5.0.0'; devDependencies['nats'] = '^5.0.0'; } return { name: `@matrix/${name}`, version: '0.1.0', type: 'module', files: [ 'dist', 'matrix.json', ], scripts, ...(Object.keys(dependencies).length > 0 ? { dependencies } : {}), devDependencies, }; } /** * Generate tsconfig.json for a scaffolded package. */ export function generateTsconfig(options?: { webapp?: boolean; service?: boolean }): Record { const compilerOptions: Record = { target: 'ES2022', module: 'NodeNext', moduleResolution: 'NodeNext', outDir: 'dist', rootDir: 'src', declaration: true, strict: true, esModuleInterop: true, skipLibCheck: true, }; if (options?.service) { compilerOptions.incremental = true; compilerOptions.tsBuildInfoFile = '.tsbuildinfo'; compilerOptions.sourceMap = true; } if (options?.webapp) { compilerOptions.module = 'ESNext'; compilerOptions.moduleResolution = 'Bundler'; compilerOptions.noEmit = true; compilerOptions.lib = ['ES2022', 'DOM', 'DOM.Iterable']; delete compilerOptions.outDir; delete compilerOptions.rootDir; delete compilerOptions.declaration; } return { compilerOptions, include: ['src'], }; }