@specverse/engines 4.1.13 → 4.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/prompts/core/standard/v9/behavior.prompt.yaml +126 -0
- package/dist/ai/behavior-ai-service.d.ts +65 -0
- package/dist/ai/behavior-ai-service.d.ts.map +1 -0
- package/dist/ai/behavior-ai-service.js +205 -0
- package/dist/ai/behavior-ai-service.js.map +1 -0
- package/dist/ai/index.d.ts +27 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +30 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/prompt-loader.js +2 -2
- package/dist/inference/quint-transpiler.d.ts.map +1 -1
- package/dist/inference/quint-transpiler.js +204 -4
- package/dist/inference/quint-transpiler.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +4 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +2 -2
- package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -0
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +97 -22
- package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +31 -31
- package/dist/libs/instance-factories/communication/templates/eventemitter/types-generator.js +79 -0
- package/dist/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.js +96 -0
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +45 -9
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +20 -2
- package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +10 -2
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +249 -0
- package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +72 -45
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +61 -54
- package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +31 -10
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +101 -84
- package/dist/libs/instance-factories/views/templates/react/components-generator.js +40 -10
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +192 -23
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +4 -1
- package/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.ts +2 -2
- package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +6 -1
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +115 -22
- package/libs/instance-factories/communication/event-emitter.yaml +16 -12
- package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +33 -36
- package/libs/instance-factories/communication/templates/eventemitter/types-generator.ts +95 -0
- package/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.ts +105 -0
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +57 -11
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +23 -2
- package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +23 -2
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +376 -0
- package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +116 -45
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +83 -59
- package/libs/instance-factories/services/templates/prisma/service-generator.ts +40 -10
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +169 -85
- package/libs/instance-factories/views/templates/react/components-generator.ts +50 -10
- package/package.json +1 -1
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +0 -232
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +0 -49
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +0 -18
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.js +0 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.js +0 -97
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +0 -64
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +0 -182
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +0 -1210
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +0 -172
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +0 -240
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +0 -147
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +0 -281
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +0 -409
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +0 -414
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +0 -467
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +0 -135
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/types/index.js +0 -0
- package/dist/libs/instance-factories/tools/templates/vscode/static/extension.js +0 -965
|
@@ -44,7 +44,7 @@ export default function generateCommand(context: TemplateContext): string {
|
|
|
44
44
|
const alias = flag.alias ? `${flag.alias}, ` : '';
|
|
45
45
|
const flagType = flag.type?.toLowerCase();
|
|
46
46
|
const valuePart = flagType === 'boolean' ? '' : ` <${flagName.replace(/^--/, '')}>`;
|
|
47
|
-
const defaultVal = flag.default !== undefined ? `, ${
|
|
47
|
+
const defaultVal = flag.default !== undefined ? `, ${commanderDefault(flag.default, flagType)}` : '';
|
|
48
48
|
const desc = flag.description || `${flagName} option`;
|
|
49
49
|
return ` .option('${alias}${flagName}${valuePart}', '${desc}'${defaultVal})`;
|
|
50
50
|
});
|
|
@@ -96,7 +96,18 @@ export default function generateCommand(context: TemplateContext): string {
|
|
|
96
96
|
if (ENGINE_HANDLERS[key]?.imports) allImportSets.push(ENGINE_HANDLERS[key].imports);
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
// Generate the body first so we can filter imports to only those actually used
|
|
100
|
+
const registerBody = hasSubcommands
|
|
101
|
+
? generateCommandWithSubcommands(name, description, subcommands, optionDefs)
|
|
102
|
+
: generateLeafCommand(name, description, commandStr, optionDefs, actionParams, serviceRef, exitCodes);
|
|
103
|
+
|
|
104
|
+
const bodyForUsage = `${serviceImport}\n${registerBody}\n${subcommandRegistrations}`;
|
|
105
|
+
const commandOptionsUsed = new RegExp('\\bCommandOptions\\b').test(bodyForUsage);
|
|
106
|
+
const engineImports = deduplicateImports(allImportSets, bodyForUsage);
|
|
107
|
+
|
|
108
|
+
const commandOptionsDecl = commandOptionsUsed
|
|
109
|
+
? `interface CommandOptions {\n${optionTypes.length > 0 ? optionTypes.join('\n') : ' [key: string]: any;'}\n}\n`
|
|
110
|
+
: '';
|
|
100
111
|
|
|
101
112
|
return `/**
|
|
102
113
|
* ${name} command
|
|
@@ -108,17 +119,14 @@ import { Command } from 'commander';
|
|
|
108
119
|
${serviceImport}
|
|
109
120
|
${engineImports}
|
|
110
121
|
|
|
111
|
-
|
|
112
|
-
${optionTypes.length > 0 ? optionTypes.join('\n') : ' [key: string]: any;'}
|
|
113
|
-
}
|
|
114
|
-
|
|
122
|
+
${commandOptionsDecl}
|
|
115
123
|
${exitCodeComments ? `/**\n * Exit codes:\n${exitCodeComments}\n */` : ''}
|
|
116
124
|
|
|
117
125
|
/**
|
|
118
126
|
* Register the ${name} command on the program.
|
|
119
127
|
*/
|
|
120
128
|
export function register${capitalize(name)}Command(program: Command): void {
|
|
121
|
-
${
|
|
129
|
+
${registerBody}
|
|
122
130
|
}
|
|
123
131
|
${subcommandRegistrations}
|
|
124
132
|
`;
|
|
@@ -280,6 +288,44 @@ import type { ParserEngine, InferenceEngine, RealizeEngine } from '@specverse/ty
|
|
|
280
288
|
}
|
|
281
289
|
}
|
|
282
290
|
|
|
291
|
+
// Merge custom actions from original spec controllers into inferred controllers
|
|
292
|
+
// Inference generates standard CURED; custom actions (openPoll, castVote) come from the spec
|
|
293
|
+
const origControllers = Array.isArray((origComponent as any).controllers)
|
|
294
|
+
? (origComponent as any).controllers
|
|
295
|
+
: Object.values((origComponent as any).controllers || {});
|
|
296
|
+
const inferredControllers = componentData.controllers || {};
|
|
297
|
+
for (const origCtrl of origControllers) {
|
|
298
|
+
const ctrlName = (origCtrl as any).name;
|
|
299
|
+
if (ctrlName && (origCtrl as any).actions) {
|
|
300
|
+
const inferredCtrl = inferredControllers[ctrlName];
|
|
301
|
+
if (inferredCtrl) {
|
|
302
|
+
inferredCtrl.actions = { ...(inferredCtrl.actions || {}), ...(origCtrl as any).actions };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Merge service operation steps from original spec into inferred services
|
|
308
|
+
// Inference generates service shells with preconditions/returns but drops
|
|
309
|
+
// the declarative steps arrays — we restore them from the parsed AST.
|
|
310
|
+
const origServices = Array.isArray((origComponent as any).services)
|
|
311
|
+
? (origComponent as any).services
|
|
312
|
+
: Object.values((origComponent as any).services || {});
|
|
313
|
+
const inferredServices = componentData.services || {};
|
|
314
|
+
for (const origSvc of origServices) {
|
|
315
|
+
const svcName = (origSvc as any).name;
|
|
316
|
+
if (!svcName) continue;
|
|
317
|
+
const inferredSvc = inferredServices[svcName];
|
|
318
|
+
if (!inferredSvc) continue;
|
|
319
|
+
|
|
320
|
+
const origOps = (origSvc as any).operations || {};
|
|
321
|
+
const inferredOps = inferredSvc.operations || {};
|
|
322
|
+
for (const [opName, origOp] of Object.entries(origOps) as [string, any][]) {
|
|
323
|
+
if (inferredOps[opName] && origOp.steps) {
|
|
324
|
+
inferredOps[opName].steps = origOp.steps;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
283
329
|
// Inject key as name into entity maps, and expand convention-format attributes
|
|
284
330
|
// Iterate all object-valued sections (not hardcoded — covers any entity type)
|
|
285
331
|
const entitySections = Object.keys(componentData).filter(k =>
|
|
@@ -348,7 +394,7 @@ import type { ParserEngine, InferenceEngine, RealizeEngine } from '@specverse/ty
|
|
|
348
394
|
if (!realizeEngine) { console.error('No realize engine found.'); process.exit(1); }
|
|
349
395
|
await realizeEngine.initialize({ manifestPath: effectiveManifestPath, workingDir: (process.env.SPECVERSE_USER_CWD || process.cwd()) });
|
|
350
396
|
|
|
351
|
-
const outputDir =
|
|
397
|
+
const outputDir = resolve((process.env.SPECVERSE_USER_CWD || process.cwd()), options.output || 'generated/code');
|
|
352
398
|
console.log('Realizing ' + type + ' from ' + file + '...');
|
|
353
399
|
await (realizeEngine as any).realizeAll(inferredSpec, outputDir);
|
|
354
400
|
|
|
@@ -826,10 +872,12 @@ import { EngineRegistry } from '@specverse/entities';`,
|
|
|
826
872
|
imports: ``,
|
|
827
873
|
handler: `const { SessionManager } = await import('@specverse/engines/ai');
|
|
828
874
|
const manager = new SessionManager();
|
|
829
|
-
const job = await manager.submit(
|
|
830
|
-
jobId: options.jobId,
|
|
831
|
-
|
|
832
|
-
operation: options.operation,
|
|
875
|
+
const job = await manager.submit({
|
|
876
|
+
jobId: options.jobId || ('job-' + Date.now()),
|
|
877
|
+
sessionId,
|
|
878
|
+
operation: (options.operation || 'create') as 'create' | 'analyse' | 'materialise' | 'realize',
|
|
879
|
+
requirements,
|
|
880
|
+
output: options.output,
|
|
833
881
|
});
|
|
834
882
|
console.log('Job submitted: ' + job.jobId);
|
|
835
883
|
console.log('Status: ' + job.status);
|
|
@@ -845,8 +893,10 @@ import { EngineRegistry } from '@specverse/entities';`,
|
|
|
845
893
|
} else {
|
|
846
894
|
console.log('ID: ' + (status.sessionId || status.jobId));
|
|
847
895
|
console.log('Status: ' + status.status);
|
|
848
|
-
if (status.
|
|
849
|
-
if (status.
|
|
896
|
+
if (status.submitted) console.log('Submitted: ' + new Date(status.submitted).toLocaleString());
|
|
897
|
+
if (status.started) console.log('Started: ' + new Date(status.started).toLocaleString());
|
|
898
|
+
if (status.completed) console.log('Completed: ' + new Date(status.completed).toLocaleString());
|
|
899
|
+
if (status.duration) console.log('Duration: ' + status.duration);
|
|
850
900
|
}`
|
|
851
901
|
},
|
|
852
902
|
'session.process': {
|
|
@@ -854,7 +904,7 @@ import { EngineRegistry } from '@specverse/entities';`,
|
|
|
854
904
|
handler: `const { SessionManager } = await import('@specverse/engines/ai');
|
|
855
905
|
const manager = new SessionManager();
|
|
856
906
|
console.log('Processing job: ' + jobId);
|
|
857
|
-
await manager.
|
|
907
|
+
await manager.processJob(jobId);
|
|
858
908
|
console.log('Job processed successfully');`
|
|
859
909
|
},
|
|
860
910
|
};
|
|
@@ -879,11 +929,15 @@ function generateLeafCommand(
|
|
|
879
929
|
: `console.log('Executing ${name}...');
|
|
880
930
|
// TODO: Wire to service`;
|
|
881
931
|
|
|
882
|
-
|
|
932
|
+
// Rename unused action params with underscore prefix so tsc's
|
|
933
|
+
// noUnusedParameters doesn't flag them.
|
|
934
|
+
const finalActionParams = renameUnusedActionParams(actionParams, handler);
|
|
935
|
+
|
|
936
|
+
return `program
|
|
883
937
|
.command('${commandStr}')
|
|
884
938
|
.description('${description}')
|
|
885
939
|
${optionDefs.join('\n')}
|
|
886
|
-
.action(async (${
|
|
940
|
+
.action(async (${finalActionParams}) => {
|
|
887
941
|
try {
|
|
888
942
|
${handler}
|
|
889
943
|
} catch (error: any) {
|
|
@@ -893,6 +947,35 @@ ${optionDefs.join('\n')}
|
|
|
893
947
|
});`;
|
|
894
948
|
}
|
|
895
949
|
|
|
950
|
+
/**
|
|
951
|
+
* Commander's `.option()` default value parameter is typed as
|
|
952
|
+
* `string | boolean | string[] | undefined`. Numeric defaults from the spec
|
|
953
|
+
* need to be stringified unless the option is a boolean flag.
|
|
954
|
+
*/
|
|
955
|
+
function commanderDefault(value: unknown, flagType?: string): string {
|
|
956
|
+
if (flagType === 'boolean') return JSON.stringify(Boolean(value));
|
|
957
|
+
if (typeof value === 'number') return JSON.stringify(String(value));
|
|
958
|
+
return JSON.stringify(value);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Rename each action parameter with a leading underscore if its name isn't
|
|
963
|
+
* referenced in the handler body.
|
|
964
|
+
*/
|
|
965
|
+
function renameUnusedActionParams(actionParams: string, handler: string): string {
|
|
966
|
+
if (!actionParams.trim()) return actionParams;
|
|
967
|
+
return actionParams.split(',').map(param => {
|
|
968
|
+
const trimmed = param.trim();
|
|
969
|
+
const nameMatch = trimmed.match(/^(\w+)/);
|
|
970
|
+
if (!nameMatch) return param;
|
|
971
|
+
const name = nameMatch[1];
|
|
972
|
+
if (name.startsWith('_')) return param;
|
|
973
|
+
const re = new RegExp(`\\b${name}\\b`);
|
|
974
|
+
if (re.test(handler)) return param;
|
|
975
|
+
return param.replace(new RegExp(`\\b${name}\\b`), `_${name}`);
|
|
976
|
+
}).join(', ');
|
|
977
|
+
}
|
|
978
|
+
|
|
896
979
|
function generateCommandWithSubcommands(
|
|
897
980
|
name: string,
|
|
898
981
|
description: string,
|
|
@@ -915,7 +998,7 @@ function generateCommandWithSubcommands(
|
|
|
915
998
|
const alias = flag.alias ? `${flag.alias}, ` : '';
|
|
916
999
|
const flagType = flag.type?.toLowerCase();
|
|
917
1000
|
const valuePart = flagType === 'boolean' ? '' : ` <${flagName.replace(/^--/, '')}>`;
|
|
918
|
-
const defaultVal = flag.default !== undefined ? `, ${
|
|
1001
|
+
const defaultVal = flag.default !== undefined ? `, ${commanderDefault(flag.default, flagType)}` : '';
|
|
919
1002
|
return ` .option('${alias}${flagName}${valuePart}', '${flag.description || flagName}'${defaultVal})`;
|
|
920
1003
|
});
|
|
921
1004
|
|
|
@@ -935,12 +1018,14 @@ function generateCommandWithSubcommands(
|
|
|
935
1018
|
? engineHandler.handler
|
|
936
1019
|
: `console.log('${name} ${subName}: not yet implemented via engine');`;
|
|
937
1020
|
|
|
1021
|
+
const finalSubActionParams = renameUnusedActionParams(subActionParams, handler);
|
|
1022
|
+
|
|
938
1023
|
return `
|
|
939
1024
|
cmd
|
|
940
1025
|
.command('${subCmdStr}')
|
|
941
1026
|
.description('${subDesc}')
|
|
942
1027
|
${subOptionDefs.join('\n')}
|
|
943
|
-
.action(async (${
|
|
1028
|
+
.action(async (${finalSubActionParams}) => {
|
|
944
1029
|
try {
|
|
945
1030
|
${handler}
|
|
946
1031
|
} catch (error: any) {
|
|
@@ -989,11 +1074,16 @@ function capitalize(str: string): string {
|
|
|
989
1074
|
* Deduplicate import statements across multiple handler import blocks.
|
|
990
1075
|
* Merges named imports from the same module and removes exact duplicates.
|
|
991
1076
|
*/
|
|
992
|
-
function deduplicateImports(importBlocks: string[]): string {
|
|
1077
|
+
function deduplicateImports(importBlocks: string[], usedIn?: string): string {
|
|
993
1078
|
const seen = new Map<string, Set<string>>(); // module -> set of named imports
|
|
994
1079
|
const typeImports = new Map<string, Set<string>>(); // module -> set of type imports
|
|
995
1080
|
const rawLines = new Set<string>(); // non-mergeable lines
|
|
996
1081
|
|
|
1082
|
+
const isUsed = (name: string): boolean => {
|
|
1083
|
+
if (!usedIn) return true;
|
|
1084
|
+
return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(usedIn);
|
|
1085
|
+
};
|
|
1086
|
+
|
|
997
1087
|
for (const block of importBlocks) {
|
|
998
1088
|
for (const line of block.split('\n').map(l => l.trim()).filter(l => l)) {
|
|
999
1089
|
// Match: import { X, Y } from 'module';
|
|
@@ -1020,12 +1110,15 @@ function deduplicateImports(importBlocks: string[]): string {
|
|
|
1020
1110
|
|
|
1021
1111
|
const result: string[] = [];
|
|
1022
1112
|
for (const [mod, names] of seen.entries()) {
|
|
1023
|
-
|
|
1113
|
+
const used = [...names].filter(isUsed);
|
|
1114
|
+
if (used.length > 0) {
|
|
1115
|
+
result.push(`import { ${used.join(', ')} } from '${mod}';`);
|
|
1116
|
+
}
|
|
1024
1117
|
}
|
|
1025
1118
|
for (const [mod, names] of typeImports.entries()) {
|
|
1026
1119
|
// Remove type imports that are already in regular imports
|
|
1027
1120
|
const regularNames = seen.get(mod) || new Set();
|
|
1028
|
-
const typeOnly = [...names].filter(n => !regularNames.has(n));
|
|
1121
|
+
const typeOnly = [...names].filter(n => !regularNames.has(n)).filter(isUsed);
|
|
1029
1122
|
if (typeOnly.length > 0) {
|
|
1030
1123
|
result.push(`import type { ${typeOnly.join(', ')} } from '${mod}';`);
|
|
1031
1124
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
name: EventEmitter
|
|
2
|
-
version: "
|
|
2
|
+
version: "2.0.0"
|
|
3
3
|
category: communication
|
|
4
|
-
description: "
|
|
4
|
+
description: "Typed in-memory event bus with WebSocket bridge for real-time frontend updates"
|
|
5
5
|
|
|
6
6
|
metadata:
|
|
7
7
|
author: "SpecVerse Team"
|
|
8
8
|
license: "MIT"
|
|
9
|
-
tags: [events, in-memory, pubsub, eventemitter, development]
|
|
9
|
+
tags: [events, in-memory, pubsub, websocket, eventemitter, development]
|
|
10
10
|
|
|
11
11
|
compatibility:
|
|
12
12
|
specverse: ">=3.3.0"
|
|
@@ -17,6 +17,7 @@ capabilities:
|
|
|
17
17
|
- "messaging.pubsub"
|
|
18
18
|
- "messaging.events"
|
|
19
19
|
- "messaging.inmemory"
|
|
20
|
+
- "messaging.websocket"
|
|
20
21
|
requires: []
|
|
21
22
|
|
|
22
23
|
technology:
|
|
@@ -29,28 +30,31 @@ dependencies:
|
|
|
29
30
|
runtime:
|
|
30
31
|
- name: "eventemitter3"
|
|
31
32
|
version: "^5.0.0"
|
|
33
|
+
- name: "@fastify/websocket"
|
|
34
|
+
version: "^11.0.0"
|
|
32
35
|
|
|
33
36
|
dev:
|
|
34
37
|
- name: "typescript"
|
|
35
38
|
version: "^5.2.0"
|
|
36
39
|
|
|
37
40
|
codeTemplates:
|
|
38
|
-
|
|
41
|
+
types:
|
|
39
42
|
engine: typescript
|
|
40
|
-
generator: "libs/instance-factories/communication/templates/eventemitter/
|
|
41
|
-
outputPattern: "src/events/
|
|
43
|
+
generator: "libs/instance-factories/communication/templates/eventemitter/types-generator.ts"
|
|
44
|
+
outputPattern: "{backendDir}/src/events/event-types.ts"
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
bus:
|
|
44
47
|
engine: typescript
|
|
45
|
-
generator: "libs/instance-factories/communication/templates/eventemitter/
|
|
46
|
-
outputPattern: "src/events/
|
|
48
|
+
generator: "libs/instance-factories/communication/templates/eventemitter/bus-generator.ts"
|
|
49
|
+
outputPattern: "{backendDir}/src/events/eventBus.ts"
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
websocket:
|
|
49
52
|
engine: typescript
|
|
50
|
-
generator: "libs/instance-factories/communication/templates/eventemitter/
|
|
51
|
-
outputPattern: "src/events/
|
|
53
|
+
generator: "libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.ts"
|
|
54
|
+
outputPattern: "{backendDir}/src/events/websocket-bridge.ts"
|
|
52
55
|
|
|
53
56
|
configuration:
|
|
54
57
|
maxListeners: 100
|
|
55
58
|
captureRejections: true
|
|
56
59
|
errorHandling: "log"
|
|
60
|
+
websocket: true
|
|
@@ -1,33 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EventEmitter Bus Generator
|
|
3
3
|
*
|
|
4
|
-
* Generates in-memory event bus using EventEmitter3
|
|
4
|
+
* Generates typed in-memory event bus using EventEmitter3.
|
|
5
|
+
* Payload types imported from generated event-types.ts.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { TemplateContext } from '@specverse/types';
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
-
* Generate EventEmitter bus singleton
|
|
11
|
-
*/
|
|
12
10
|
export default function generateEventBus(context: TemplateContext): string {
|
|
13
11
|
const { spec } = context;
|
|
14
|
-
|
|
15
|
-
const
|
|
12
|
+
const hasEvents = spec.events && Object.keys(spec.events).length > 0;
|
|
13
|
+
const models = spec.models ? Object.keys(spec.models) : [];
|
|
16
14
|
|
|
17
15
|
return `/**
|
|
18
16
|
* Event Bus
|
|
19
|
-
*
|
|
17
|
+
* Typed in-memory event bus using EventEmitter3
|
|
20
18
|
* Generated from SpecVerse specification
|
|
21
19
|
*/
|
|
22
20
|
|
|
23
|
-
import EventEmitter from 'eventemitter3';
|
|
21
|
+
import { EventEmitter } from 'eventemitter3';
|
|
22
|
+
${hasEvents || models.length > 0 ? `import type { EventPayloads } from './event-types.js';` : ''}
|
|
24
23
|
|
|
25
|
-
//
|
|
26
|
-
${
|
|
24
|
+
// Re-export types for consumers
|
|
25
|
+
${hasEvents || models.length > 0 ? `export type { EventPayloads, EventName } from './event-types.js';` : ''}
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Event history entry
|
|
29
|
+
*/
|
|
30
|
+
interface EventHistoryEntry {
|
|
31
|
+
event: string;
|
|
32
|
+
payload: any;
|
|
33
|
+
timestamp: Date;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -35,15 +38,13 @@ ${events.map(event => ` ${event} = '${event}',`).join('\n')}
|
|
|
35
38
|
*/
|
|
36
39
|
class EventBus extends EventEmitter {
|
|
37
40
|
private static instance: EventBus;
|
|
41
|
+
private history: EventHistoryEntry[] = [];
|
|
42
|
+
private maxHistory = 1000;
|
|
38
43
|
|
|
39
44
|
private constructor() {
|
|
40
45
|
super();
|
|
41
|
-
this.setMaxListeners(100); // Configure max listeners
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
/**
|
|
45
|
-
* Get singleton instance
|
|
46
|
-
*/
|
|
47
48
|
public static getInstance(): EventBus {
|
|
48
49
|
if (!EventBus.instance) {
|
|
49
50
|
EventBus.instance = new EventBus();
|
|
@@ -52,37 +53,33 @@ class EventBus extends EventEmitter {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
|
-
* Publish
|
|
56
|
+
* Publish a typed event
|
|
56
57
|
*/
|
|
57
|
-
public publish<
|
|
58
|
-
|
|
58
|
+
public publish<K extends string>(event: K, payload: K extends keyof EventPayloads ? EventPayloads[K] : any): void {
|
|
59
|
+
this.history.push({ event, payload, timestamp: new Date() });
|
|
60
|
+
if (this.history.length > this.maxHistory) this.history.shift();
|
|
59
61
|
this.emit(event, payload);
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
/**
|
|
63
|
-
* Subscribe to
|
|
65
|
+
* Subscribe to a typed event
|
|
64
66
|
*/
|
|
65
|
-
public subscribe<
|
|
66
|
-
event:
|
|
67
|
-
handler: (payload:
|
|
67
|
+
public subscribe<K extends string>(
|
|
68
|
+
event: K,
|
|
69
|
+
handler: (payload: K extends keyof EventPayloads ? EventPayloads[K] : any) => void | Promise<void>
|
|
68
70
|
): () => void {
|
|
69
|
-
console.log(\`[EventBus] Subscribing to event: \${event}\`);
|
|
70
71
|
this.on(event, handler);
|
|
71
|
-
|
|
72
|
-
// Return unsubscribe function
|
|
73
|
-
return () => {
|
|
74
|
-
this.off(event, handler);
|
|
75
|
-
};
|
|
72
|
+
return () => { this.off(event, handler); };
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
/**
|
|
79
|
-
*
|
|
76
|
+
* Get event history
|
|
80
77
|
*/
|
|
81
|
-
public
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
public getHistory(eventName?: string, limit = 50): EventHistoryEntry[] {
|
|
79
|
+
const filtered = eventName
|
|
80
|
+
? this.history.filter(e => e.event === eventName)
|
|
81
|
+
: this.history;
|
|
82
|
+
return filtered.slice(-limit);
|
|
86
83
|
}
|
|
87
84
|
}
|
|
88
85
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Types Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates typed event payload interfaces from spec event definitions.
|
|
5
|
+
* Replaces the `any` payload types in the bus generator with real types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TemplateContext } from '@specverse/types';
|
|
9
|
+
|
|
10
|
+
/** Map SpecVerse types to TypeScript types */
|
|
11
|
+
function tsType(specType: string): string {
|
|
12
|
+
const map: Record<string, string> = {
|
|
13
|
+
String: 'string', Text: 'string', Email: 'string', URL: 'string',
|
|
14
|
+
Integer: 'number', Float: 'number', Number: 'number', Decimal: 'number', Money: 'number',
|
|
15
|
+
Boolean: 'boolean',
|
|
16
|
+
Date: 'string', DateTime: 'string', Timestamp: 'string',
|
|
17
|
+
UUID: 'string',
|
|
18
|
+
Object: 'Record<string, any>', Array: 'any[]',
|
|
19
|
+
};
|
|
20
|
+
return map[specType] || 'any';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function generateEventTypes(context: TemplateContext): string {
|
|
24
|
+
const { spec } = context;
|
|
25
|
+
const events = spec.events && typeof spec.events === 'object'
|
|
26
|
+
? Object.entries(spec.events) : [];
|
|
27
|
+
|
|
28
|
+
if (events.length === 0) {
|
|
29
|
+
return `/**
|
|
30
|
+
* Event Types — no events defined in specification
|
|
31
|
+
*/
|
|
32
|
+
export type EventPayloads = Record<string, any>;
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const interfaces: string[] = [];
|
|
37
|
+
const payloadMapEntries: string[] = [];
|
|
38
|
+
|
|
39
|
+
for (const [eventName, eventDef] of events) {
|
|
40
|
+
const def = eventDef as any;
|
|
41
|
+
const attrs = def.attributes || def.payload || {};
|
|
42
|
+
|
|
43
|
+
// Handle both array and object attribute formats
|
|
44
|
+
const attrEntries: Array<[string, any]> = Array.isArray(attrs)
|
|
45
|
+
? attrs.map((a: any) => [a.name, a])
|
|
46
|
+
: Object.entries(attrs);
|
|
47
|
+
|
|
48
|
+
const fields = attrEntries.map(([name, attrDef]) => {
|
|
49
|
+
const type = typeof attrDef === 'string'
|
|
50
|
+
? tsType(attrDef.split(' ')[0])
|
|
51
|
+
: tsType(attrDef?.type || 'String');
|
|
52
|
+
const required = typeof attrDef === 'string'
|
|
53
|
+
? attrDef.includes('required')
|
|
54
|
+
: attrDef?.required;
|
|
55
|
+
return ` ${name}${required ? '' : '?'}: ${type};`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
interfaces.push(`export interface ${eventName}Payload {
|
|
59
|
+
${fields.join('\n')}
|
|
60
|
+
}`);
|
|
61
|
+
|
|
62
|
+
payloadMapEntries.push(` ${eventName}: ${eventName}Payload;`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Also add standard CURED events for each model
|
|
66
|
+
const models = spec.models ? Object.keys(spec.models) : [];
|
|
67
|
+
for (const model of models) {
|
|
68
|
+
for (const suffix of ['Created', 'Updated', 'Deleted', 'Evolved']) {
|
|
69
|
+
const name = `${model}${suffix}`;
|
|
70
|
+
if (!payloadMapEntries.some(e => e.includes(name))) {
|
|
71
|
+
payloadMapEntries.push(` ${name}: { id: string; timestamp: string; [key: string]: any };`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return `/**
|
|
77
|
+
* Event Payload Types
|
|
78
|
+
* Generated from SpecVerse specification
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
${interfaces.join('\n\n')}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Map of all event names to their payload types
|
|
85
|
+
*/
|
|
86
|
+
export type EventPayloads = {
|
|
87
|
+
${payloadMapEntries.join('\n')}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* All known event names
|
|
92
|
+
*/
|
|
93
|
+
export type EventName = keyof EventPayloads;
|
|
94
|
+
`;
|
|
95
|
+
}
|
package/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Bridge Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates a WebSocket server that bridges the event bus to frontend clients.
|
|
5
|
+
* Clients can subscribe to specific events and receive real-time updates.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TemplateContext } from '@specverse/types';
|
|
9
|
+
|
|
10
|
+
export default function generateWebSocketBridge(context: TemplateContext): string {
|
|
11
|
+
const { spec } = context;
|
|
12
|
+
const models = spec.models ? Object.keys(spec.models) : [];
|
|
13
|
+
|
|
14
|
+
// Build list of all events the bridge should forward
|
|
15
|
+
const specEvents = spec.events ? Object.keys(spec.events) : [];
|
|
16
|
+
const curedEvents = models.flatMap(m =>
|
|
17
|
+
['Created', 'Updated', 'Deleted', 'Evolved'].map(s => `${m}${s}`)
|
|
18
|
+
);
|
|
19
|
+
const allEvents = [...new Set([...specEvents, ...curedEvents])];
|
|
20
|
+
|
|
21
|
+
return `/**
|
|
22
|
+
* WebSocket Bridge
|
|
23
|
+
* Bridges the event bus to frontend clients for real-time updates.
|
|
24
|
+
* Generated from SpecVerse specification
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { FastifyInstance } from 'fastify';
|
|
28
|
+
import type { WebSocket } from '@fastify/websocket';
|
|
29
|
+
import { eventBus } from './eventBus.js';
|
|
30
|
+
|
|
31
|
+
interface ClientSubscription {
|
|
32
|
+
ws: WebSocket;
|
|
33
|
+
events: Set<string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const clients = new Map<WebSocket, ClientSubscription>();
|
|
37
|
+
|
|
38
|
+
// All events this bridge knows about
|
|
39
|
+
const ALL_EVENTS = ${JSON.stringify(allEvents, null, 2)};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register WebSocket bridge with Fastify
|
|
43
|
+
*/
|
|
44
|
+
export async function registerWebSocketBridge(fastify: FastifyInstance): Promise<void> {
|
|
45
|
+
// Register @fastify/websocket plugin
|
|
46
|
+
const websocketPlugin = await import('@fastify/websocket');
|
|
47
|
+
await fastify.register(websocketPlugin.default || websocketPlugin);
|
|
48
|
+
|
|
49
|
+
// WebSocket route
|
|
50
|
+
fastify.get('/ws', { websocket: true }, (socket: WebSocket) => {
|
|
51
|
+
const subscription: ClientSubscription = { ws: socket, events: new Set() };
|
|
52
|
+
clients.set(socket, subscription);
|
|
53
|
+
|
|
54
|
+
socket.on('message', (raw: Buffer) => {
|
|
55
|
+
try {
|
|
56
|
+
const msg = JSON.parse(raw.toString());
|
|
57
|
+
|
|
58
|
+
if (msg.type === 'subscribe' && msg.event) {
|
|
59
|
+
subscription.events.add(msg.event);
|
|
60
|
+
socket.send(JSON.stringify({ type: 'subscribed', event: msg.event }));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (msg.type === 'unsubscribe' && msg.event) {
|
|
64
|
+
subscription.events.delete(msg.event);
|
|
65
|
+
socket.send(JSON.stringify({ type: 'unsubscribed', event: msg.event }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (msg.type === 'subscribe-all') {
|
|
69
|
+
ALL_EVENTS.forEach(e => subscription.events.add(e));
|
|
70
|
+
socket.send(JSON.stringify({ type: 'subscribed-all', events: ALL_EVENTS }));
|
|
71
|
+
}
|
|
72
|
+
} catch { /* ignore malformed messages */ }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
socket.on('close', () => {
|
|
76
|
+
clients.delete(socket);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Send initial connection confirmation
|
|
80
|
+
socket.send(JSON.stringify({
|
|
81
|
+
type: 'connected',
|
|
82
|
+
availableEvents: ALL_EVENTS,
|
|
83
|
+
}));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Subscribe to all events and broadcast to interested clients
|
|
87
|
+
for (const eventName of ALL_EVENTS) {
|
|
88
|
+
eventBus.subscribe(eventName, (payload: any) => {
|
|
89
|
+
const message = JSON.stringify({
|
|
90
|
+
type: 'event',
|
|
91
|
+
event: eventName,
|
|
92
|
+
payload,
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
for (const [ws, sub] of clients) {
|
|
97
|
+
if (sub.events.has(eventName) && ws.readyState === 1) {
|
|
98
|
+
try { ws.send(message); } catch { /* client disconnected */ }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
}
|