237 lines
7.6 KiB
TypeScript
237 lines
7.6 KiB
TypeScript
/**
|
|
* 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 };
|
|
}
|