237 lines
7.6 KiB
TypeScript
Raw Normal View History

/**
* 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<string, unknown> = {},',
' 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<string, unknown> {
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<IActorCommandResult> {
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 };
}