/** * mx actor command implementation. * * Scaffolds a complete actor package with TypeScript, tests, examples, and skills. * Implementation: B014 */ import * as fs from 'node:fs'; import * as path from 'node:path'; import { toKebabCase, toPascalCase, validateMatrixManifest, writeJsonFile, generatePackageJson, generateTsconfig } from '../utils/scaffold.js'; export interface IActorCommandOptions { readonly cwd?: string; readonly outputDir?: string; } export interface IActorCommandResult { readonly rootDir: string; readonly className: string; readonly mount: string; readonly workspacePath?: string; } function actorTemplate(className: string, matrixActorImport: string): string { return [ `import { MatrixActor } from ${JSON.stringify(matrixActorImport)};`, '', `export class ${className} extends MatrixActor {`, ' static override accepts = {', " getStatus: {},", ' };', '', ' static override emits = {', " statusChanged: { status: 'string' },", ' };', '', " private _status = 'ready';", '', ' protected onGetStatus(): { status: string } {', ' return { status: this._status };', ' }', '}', '', ].join('\n'); } function actorIndexTemplate(className: string): string { return [ `export { ${className} } from './${className}.js';`, `export { createStandalone${className} } from './create-standalone-${className}.js';`, '', ].join('\n'); } function actorFactoryTemplate(className: string, mount: string, matrixActorImport: string): string { const factoryName = `createStandalone${className}`; const actorFile = `./${className}.js`; return [ `import { InMemoryBroker, InMemoryTransport, MatrixRuntime } from ${JSON.stringify(matrixActorImport)};`, '', `import { ${className} } from ${JSON.stringify(actorFile)};`, '', 'interface IStandaloneBootContext {', ' readonly root?: string;', ' readonly mount?: string;', '}', '', 'interface IStandaloneRunnerServices {', ' readonly runtime?: MatrixRuntime;', '}', '', 'function readTrimmedString(value: unknown): string | undefined {', " if (typeof value !== 'string') {", ' return undefined;', ' }', ' const trimmed = value.trim();', ' return trimmed.length > 0 ? trimmed : undefined;', '}', '', 'function requiredStandaloneRoot(requestedRoot: string | undefined, runtime: MatrixRuntime): string {', ' const runtimeRoot = readTrimmedString(runtime.transport.root);', ' const root = requestedRoot ?? runtimeRoot;', ' if (!root) {', ` throw new Error('${factoryName} requires bootContext.root or a runtime transport root');`, ' }', ' return root;', '}', '', `export async function ${factoryName}(`, ' _overrides: Record = {},', ' services?: IStandaloneRunnerServices,', ' _config?: unknown,', ' bootContext: IStandaloneBootContext = {},', '): Promise<{', ' runtime: MatrixRuntime;', ` actor: ${className};`, ' root: string;', ' mount: string;', '}> {', ' const requestedRoot = readTrimmedString(bootContext.root);', ' const runtime = services?.runtime ?? new MatrixRuntime({', ' transport: new InMemoryTransport(new InMemoryBroker(), {', ` name: ${JSON.stringify(`${mount}-standalone`)},`, ' ...(requestedRoot ? { root: requestedRoot } : {}),', ' }),', ' logging: false,', ' });', ' const root = requiredStandaloneRoot(requestedRoot, runtime);', ` const mount = readTrimmedString(bootContext.mount) ?? ${JSON.stringify(mount)};`, ` const actor = await runtime.createSupervised(${className}, mount);`, ' return { runtime, actor, root, mount };', '}', '', ].join('\n'); } function actorFactoryManifest(className: string, mount: string): Record { return { kind: 'factory', export: `createStandalone${className}`, instance: { id: `RUNTIME-HOST-${mount.toUpperCase().replace(/[^A-Z0-9]+/g, '-')}`, class: 'service', rootKind: 'component', componentType: className, autoStart: true, }, }; } function actorTestTemplate(className: string): string { return [ "import { describe, it } from 'node:test';", "import { strict as assert } from 'node:assert';", `import { ${className} } from '../src/${className}.ts';`, '', `describe('${className}', () => {`, " it('declares getStatus command', () => {", ` assert.ok(Object.prototype.hasOwnProperty.call(${className}.accepts, 'getStatus'));`, ' });', '});', '', ].join('\n'); } function actorSkillTemplate(name: string, className: string): string { return [ '---', `name: ${name}`, 'intents:', ' - action: query', ' dataType: status', ' category: system', '---', '', `# ${className}`, '', 'Returns actor status for orchestration and health checks.', '', ].join('\n'); } export async function actorCommand(name: string, options: IActorCommandOptions = {}): Promise { const normalizedName = toKebabCase(name); if (!normalizedName) { throw new Error('Actor name must not be empty'); } const className = `${toPascalCase(normalizedName)}Actor`; const mount = normalizedName; const cwd = path.resolve(options.cwd ?? process.cwd()); const rootDir = path.resolve(options.outputDir ?? path.join(cwd, normalizedName)); const matrixActorImport = '@open-matrix/core'; fs.mkdirSync(path.join(rootDir, 'src'), { recursive: true }); fs.mkdirSync(path.join(rootDir, 'examples'), { recursive: true }); fs.mkdirSync(path.join(rootDir, 'skills'), { recursive: true }); fs.mkdirSync(path.join(rootDir, 'tests'), { recursive: true }); const manifest = { name: `@matrix/${normalizedName}`, version: '0.1.0', description: `${className} generated by mx actor`, runtime: { language: 'typescript', entry: 'dist/index.js', factory: actorFactoryManifest(className, mount), }, components: [ { type: className, export: className, mount, surface: 'headless', accepts: ['getStatus'], emits: ['statusChanged'], }, ], permissions: { fsPolicy: 'none' }, }; const validation = validateMatrixManifest(manifest); if (!validation.valid) { throw new Error(`Generated matrix.json failed validation: ${validation.errors.join('; ')}`); } writeJsonFile(path.join(rootDir, 'matrix.json'), manifest); writeJsonFile(path.join(rootDir, 'package.json'), generatePackageJson(normalizedName, { service: true })); writeJsonFile(path.join(rootDir, 'tsconfig.json'), generateTsconfig({ service: true })); fs.writeFileSync( path.join(rootDir, 'src', `${className}.ts`), actorTemplate(className, matrixActorImport), 'utf8' ); fs.writeFileSync( path.join(rootDir, 'src', `create-standalone-${className}.ts`), actorFactoryTemplate(className, mount, matrixActorImport), 'utf8' ); fs.writeFileSync(path.join(rootDir, 'src', 'index.ts'), actorIndexTemplate(className), 'utf8'); writeJsonFile(path.join(rootDir, 'examples', 'get-status.mxq'), { label: 'Get Status', operation: 'getStatus', payload: {}, description: `Runs ${className}.getStatus`, }); fs.writeFileSync( path.join(rootDir, 'skills', `${normalizedName}.skill.md`), actorSkillTemplate(normalizedName, className), 'utf8' ); fs.writeFileSync(path.join(rootDir, 'tests', `${normalizedName}.spec.ts`), actorTestTemplate(className), 'utf8'); console.log(`Created actor scaffold at ${rootDir}`); return { rootDir, className, mount }; }