@specverse/engines 4.1.28 → 4.1.30

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.
@@ -121,7 +121,7 @@ ${serviceMap}
121
121
  ${routeRegistrations}
122
122
 
123
123
  const preferredPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
124
- const host = process.env.HOST || '0.0.0.0';
124
+ const host = process.env.HOST || '::';
125
125
 
126
126
  // Try to start server with automatic port fallback
127
127
  let port = preferredPort;
@@ -201,7 +201,7 @@ ${routeRegistrations}
201
201
 
202
202
  async function start() {
203
203
  const preferredPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
204
- const host = process.env.HOST || '0.0.0.0';
204
+ const host = process.env.HOST || '::';
205
205
  const maxAttempts = 10;
206
206
 
207
207
  // Try to start server with automatic port fallback
@@ -254,7 +254,7 @@ async function bootstrap() {
254
254
  });
255
255
 
256
256
  const preferredPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
257
- const host = process.env.HOST || '0.0.0.0';
257
+ const host = process.env.HOST || '::';
258
258
  const maxAttempts = 10;
259
259
 
260
260
  // Try to start server with automatic port fallback
@@ -324,7 +324,14 @@ export async function executeServiceOperation(
324
324
  }
325
325
 
326
326
  /**
327
- * Transition entity lifecycle state
327
+ * Transition entity lifecycle state.
328
+ *
329
+ * Calls the realized backend's CURVED evolve endpoint
330
+ * (PATCH /api/{resource}/:id/evolve) \u2014 matches what
331
+ * engines/libs/instance-factories/controllers/templates/fastify/routes-generator.ts
332
+ * emits and what scripts/smoke-parity.sh exercises. The older
333
+ * /transition URL this function used to call never existed on the
334
+ * realized backend.
328
335
  */
329
336
  export async function transitionState(
330
337
  modelName: string,
@@ -339,8 +346,8 @@ export async function transitionState(
339
346
 
340
347
  const resource = modelName.toLowerCase() + 's';
341
348
  return apiRequest<ApiResponse>(
342
- 'POST',
343
- \`/\${resource}/\${entityId}/transition\`,
349
+ 'PATCH',
350
+ \`/\${resource}/\${entityId}/evolve\`,
344
351
  body
345
352
  );
346
353
  }
@@ -431,7 +438,9 @@ export async function executeServiceOperation(
431
438
  }
432
439
 
433
440
  /**
434
- * Transition entity lifecycle state
441
+ * Transition entity lifecycle state \u2014 realized-backend variant
442
+ * (used by templates that pre-date the unified CURVED evolve path).
443
+ * Matches PATCH /api/{resource}/:id/evolve.
435
444
  */
436
445
  export async function transitionState(
437
446
  modelName: string,
@@ -444,9 +453,10 @@ export async function transitionState(
444
453
  body.lifecycleName = lifecycleName;
445
454
  }
446
455
 
456
+ const resource = modelName.toLowerCase() + 's';
447
457
  return apiRequest<ApiResponse>(
448
- 'POST',
449
- \`/lifecycle/\${modelName}/\${entityId}/transition\`,
458
+ 'PATCH',
459
+ \`/\${resource}/\${entityId}/evolve\`,
450
460
  body
451
461
  );
452
462
  }
@@ -62,8 +62,11 @@ fastify.get('/api/runtime/info', async () => ({
62
62
  services: ${JSON.stringify(servicesList)}
63
63
  }));
64
64
 
65
- ${hasEvents ? `// Event history endpoint
66
- fastify.get('/api/events', async () => eventBus.getHistory());` : ""}
65
+ ${hasEvents ? `// Event history endpoint \u2014 runtime introspection, not part of
66
+ // the user's API surface. Lives under /api/runtime/ so it doesn't
67
+ // collide with the auto-generated Event model route (spec models
68
+ // named "Event" produce a GET /api/events of their own).
69
+ fastify.get('/api/runtime/events', async () => eventBus.getHistory());` : ""}
67
70
 
68
71
  // Register routes
69
72
  ${routeImports}
@@ -84,7 +87,12 @@ const start = async () => {
84
87
  ${hasEvents ? ` // Register WebSocket bridge for real-time frontend events
85
88
  await registerWebSocketBridge(fastify);` : ""}
86
89
  const port = parseInt(process.env.PORT || '3000');
87
- await fastify.listen({ port, host: '0.0.0.0' });
90
+ // Listen on :: (IPv6 wildcard) instead of 0.0.0.0 so the server
91
+ // accepts both IPv6 and IPv4-mapped connections. Node resolves
92
+ // 'localhost' to ::1 first; if the backend only binds IPv4, the
93
+ // vite dev-server WS proxy hits ECONNREFUSED on ::1 and doesn't
94
+ // fall back to 127.0.0.1 cleanly. Binding :: fixes both cases.
95
+ await fastify.listen({ port, host: '::' });
88
96
  console.log(\`Server running at http://localhost:\${port}\`);
89
97
  console.log(\`API endpoints: ${modelNames.map((n) => `/api/${n.toLowerCase()}s`).join(", ")}\`);
90
98
  ${hasEvents ? ` console.log(\`WebSocket: ws://localhost:\${port}/ws\`);
@@ -213,39 +213,49 @@ function generateEvolveMethod(model, modelName, modelVar, prismaDelegate, contro
213
213
  const validTransitions = lifecycle ? buildTransitionMap(lifecycle) : {};
214
214
  return `
215
215
  /**
216
- * Evolve ${modelName} through lifecycle
216
+ * Evolve ${modelName} through lifecycle "${lifecycleName}"
217
217
  * States: ${states.join(" \u2192 ")}
218
+ *
219
+ * Accepts either the client shape { toState, lifecycleName? } or
220
+ * the direct column shape { ${lifecycleName}: ... }. The frontend
221
+ * runtime's useTransitionStateMutation always sends the former; the
222
+ * realized smoke-parity script can send either.
218
223
  */
219
224
  public async evolve(id: string, data: any): Promise<any> {
220
- // Validate input
221
- const validationResult = this.validate(data, { operation: 'evolve' });
222
- if (!validationResult.valid) {
223
- throw new Error(\`Validation failed: \${validationResult.errors.join(', ')}\`);
224
- }
225
-
226
225
  // Get current record to check lifecycle state
227
226
  const current = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
228
227
  if (!current) {
229
228
  throw new Error('${modelName} not found');
230
229
  }
231
230
 
231
+ // Normalize input: translate client { toState: X } shape to the
232
+ // lifecycle's actual column name. If the caller already sent the
233
+ // column directly, keep whatever they sent.
234
+ const targetLifecycle = data?.lifecycleName || '${lifecycleName}';
235
+ const targetState = data?.toState ?? data?.state ?? data?.[targetLifecycle];
236
+ if (!targetState) {
237
+ throw new Error('evolve requires toState (or ${lifecycleName}) in the request body');
238
+ }
239
+
232
240
  ${states.length > 0 ? `
233
- // Validate lifecycle transition
234
- const currentState = (current as any).${lifecycleName};
235
- const newState = data.${lifecycleName};
236
- if (newState) {
237
- const validTransitions: Record<string, string[]> = ${JSON.stringify(validTransitions)};
238
- const allowed = validTransitions[currentState] || [];
239
- if (!allowed.includes(newState)) {
240
- throw new Error(\`Invalid transition: \${currentState} \u2192 \${newState}. Allowed: \${allowed.join(', ') || 'none'}\`);
241
- }
241
+ // Validate lifecycle transition against declared flow
242
+ const currentState = (current as any)[targetLifecycle];
243
+ const validTransitions: Record<string, string[]> = ${JSON.stringify(validTransitions)};
244
+ const allowed = validTransitions[currentState] || [];
245
+ if (!allowed.includes(targetState)) {
246
+ throw new Error(\`Invalid transition: \${currentState} \u2192 \${targetState}. Allowed: \${allowed.join(', ') || 'none'}\`);
242
247
  }
243
248
  ` : ""}
244
249
 
250
+ // Build the Prisma update payload \u2014 only the lifecycle column
251
+ // changes. Strips toState/lifecycleName/state so Prisma doesn't
252
+ // reject unknown fields.
253
+ const updateData: any = { [targetLifecycle]: targetState };
254
+
245
255
  // Update record
246
256
  const ${modelVar} = await ${prismaDelegate}.update({
247
257
  where: { id: parseId(id) },
248
- data${generateIncludeRelationships(model)}
258
+ data: updateData${generateIncludeRelationships(model)}
249
259
  });
250
260
 
251
261
  // Publish CURED event
@@ -39,7 +39,7 @@ function getApiBaseUrl(config) {
39
39
  return "${VITE_API_BASE_URL}";
40
40
  }
41
41
  const port = config.serverPort || 3e3;
42
- return `http://localhost:${port}`;
42
+ return `http://127.0.0.1:${port}`;
43
43
  }
44
44
  function getPathConfig(context) {
45
45
  return {
@@ -154,7 +154,7 @@ ${serviceMap}
154
154
  ${routeRegistrations}
155
155
 
156
156
  const preferredPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
157
- const host = process.env.HOST || '0.0.0.0';
157
+ const host = process.env.HOST || '::';
158
158
 
159
159
  // Try to start server with automatic port fallback
160
160
  let port = preferredPort;
@@ -238,7 +238,7 @@ ${routeRegistrations}
238
238
 
239
239
  async function start() {
240
240
  const preferredPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
241
- const host = process.env.HOST || '0.0.0.0';
241
+ const host = process.env.HOST || '::';
242
242
  const maxAttempts = 10;
243
243
 
244
244
  // Try to start server with automatic port fallback
@@ -292,7 +292,7 @@ async function bootstrap() {
292
292
  });
293
293
 
294
294
  const preferredPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
295
- const host = process.env.HOST || '0.0.0.0';
295
+ const host = process.env.HOST || '::';
296
296
  const maxAttempts = 10;
297
297
 
298
298
  // Try to start server with automatic port fallback
@@ -360,7 +360,14 @@ export async function executeServiceOperation(
360
360
  }
361
361
 
362
362
  /**
363
- * Transition entity lifecycle state
363
+ * Transition entity lifecycle state.
364
+ *
365
+ * Calls the realized backend's CURVED evolve endpoint
366
+ * (PATCH /api/{resource}/:id/evolve) — matches what
367
+ * engines/libs/instance-factories/controllers/templates/fastify/routes-generator.ts
368
+ * emits and what scripts/smoke-parity.sh exercises. The older
369
+ * /transition URL this function used to call never existed on the
370
+ * realized backend.
364
371
  */
365
372
  export async function transitionState(
366
373
  modelName: string,
@@ -375,8 +382,8 @@ export async function transitionState(
375
382
 
376
383
  const resource = modelName.toLowerCase() + 's';
377
384
  return apiRequest<ApiResponse>(
378
- 'POST',
379
- \`/\${resource}/\${entityId}/transition\`,
385
+ 'PATCH',
386
+ \`/\${resource}/\${entityId}/evolve\`,
380
387
  body
381
388
  );
382
389
  }
@@ -468,7 +475,9 @@ export async function executeServiceOperation(
468
475
  }
469
476
 
470
477
  /**
471
- * Transition entity lifecycle state
478
+ * Transition entity lifecycle state — realized-backend variant
479
+ * (used by templates that pre-date the unified CURVED evolve path).
480
+ * Matches PATCH /api/{resource}/:id/evolve.
472
481
  */
473
482
  export async function transitionState(
474
483
  modelName: string,
@@ -481,9 +490,10 @@ export async function transitionState(
481
490
  body.lifecycleName = lifecycleName;
482
491
  }
483
492
 
493
+ const resource = modelName.toLowerCase() + 's';
484
494
  return apiRequest<ApiResponse>(
485
- 'POST',
486
- \`/lifecycle/\${modelName}/\${entityId}/transition\`,
495
+ 'PATCH',
496
+ \`/\${resource}/\${entityId}/evolve\`,
487
497
  body
488
498
  );
489
499
  }
@@ -82,8 +82,11 @@ fastify.get('/api/runtime/info', async () => ({
82
82
  services: ${JSON.stringify(servicesList)}
83
83
  }));
84
84
 
85
- ${hasEvents ? `// Event history endpoint
86
- fastify.get('/api/events', async () => eventBus.getHistory());` : ''}
85
+ ${hasEvents ? `// Event history endpoint — runtime introspection, not part of
86
+ // the user's API surface. Lives under /api/runtime/ so it doesn't
87
+ // collide with the auto-generated Event model route (spec models
88
+ // named "Event" produce a GET /api/events of their own).
89
+ fastify.get('/api/runtime/events', async () => eventBus.getHistory());` : ''}
87
90
 
88
91
  // Register routes
89
92
  ${routeImports}
@@ -102,7 +105,12 @@ const start = async () => {
102
105
  ${hasEvents ? ` // Register WebSocket bridge for real-time frontend events
103
106
  await registerWebSocketBridge(fastify);` : ''}
104
107
  const port = parseInt(process.env.PORT || '3000');
105
- await fastify.listen({ port, host: '0.0.0.0' });
108
+ // Listen on :: (IPv6 wildcard) instead of 0.0.0.0 so the server
109
+ // accepts both IPv6 and IPv4-mapped connections. Node resolves
110
+ // 'localhost' to ::1 first; if the backend only binds IPv4, the
111
+ // vite dev-server WS proxy hits ECONNREFUSED on ::1 and doesn't
112
+ // fall back to 127.0.0.1 cleanly. Binding :: fixes both cases.
113
+ await fastify.listen({ port, host: '::' });
106
114
  console.log(\`Server running at http://localhost:\${port}\`);
107
115
  console.log(\`API endpoints: ${modelNames.map((n: string) => `/api/${n.toLowerCase()}s`).join(', ')}\`);
108
116
  ${hasEvents ? ` console.log(\`WebSocket: ws://localhost:\${port}/ws\`);
@@ -282,39 +282,49 @@ function generateEvolveMethod(model: any, modelName: string, modelVar: string, p
282
282
 
283
283
  return `
284
284
  /**
285
- * Evolve ${modelName} through lifecycle
285
+ * Evolve ${modelName} through lifecycle "${lifecycleName}"
286
286
  * States: ${states.join(' → ')}
287
+ *
288
+ * Accepts either the client shape { toState, lifecycleName? } or
289
+ * the direct column shape { ${lifecycleName}: ... }. The frontend
290
+ * runtime's useTransitionStateMutation always sends the former; the
291
+ * realized smoke-parity script can send either.
287
292
  */
288
293
  public async evolve(id: string, data: any): Promise<any> {
289
- // Validate input
290
- const validationResult = this.validate(data, { operation: 'evolve' });
291
- if (!validationResult.valid) {
292
- throw new Error(\`Validation failed: \${validationResult.errors.join(', ')}\`);
293
- }
294
-
295
294
  // Get current record to check lifecycle state
296
295
  const current = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
297
296
  if (!current) {
298
297
  throw new Error('${modelName} not found');
299
298
  }
300
299
 
300
+ // Normalize input: translate client { toState: X } shape to the
301
+ // lifecycle's actual column name. If the caller already sent the
302
+ // column directly, keep whatever they sent.
303
+ const targetLifecycle = data?.lifecycleName || '${lifecycleName}';
304
+ const targetState = data?.toState ?? data?.state ?? data?.[targetLifecycle];
305
+ if (!targetState) {
306
+ throw new Error('evolve requires toState (or ${lifecycleName}) in the request body');
307
+ }
308
+
301
309
  ${states.length > 0 ? `
302
- // Validate lifecycle transition
303
- const currentState = (current as any).${lifecycleName};
304
- const newState = data.${lifecycleName};
305
- if (newState) {
306
- const validTransitions: Record<string, string[]> = ${JSON.stringify(validTransitions)};
307
- const allowed = validTransitions[currentState] || [];
308
- if (!allowed.includes(newState)) {
309
- throw new Error(\`Invalid transition: \${currentState} → \${newState}. Allowed: \${allowed.join(', ') || 'none'}\`);
310
- }
310
+ // Validate lifecycle transition against declared flow
311
+ const currentState = (current as any)[targetLifecycle];
312
+ const validTransitions: Record<string, string[]> = ${JSON.stringify(validTransitions)};
313
+ const allowed = validTransitions[currentState] || [];
314
+ if (!allowed.includes(targetState)) {
315
+ throw new Error(\`Invalid transition: \${currentState} → \${targetState}. Allowed: \${allowed.join(', ') || 'none'}\`);
311
316
  }
312
317
  ` : ''}
313
318
 
319
+ // Build the Prisma update payload — only the lifecycle column
320
+ // changes. Strips toState/lifecycleName/state so Prisma doesn't
321
+ // reject unknown fields.
322
+ const updateData: any = { [targetLifecycle]: targetState };
323
+
314
324
  // Update record
315
325
  const ${modelVar} = await ${prismaDelegate}.update({
316
326
  where: { id: parseId(id) },
317
- data${generateIncludeRelationships(model)}
327
+ data: updateData${generateIncludeRelationships(model)}
318
328
  });
319
329
 
320
330
  // Publish CURED event
@@ -93,9 +93,15 @@ export function getApiBaseUrl(config: PathResolverConfig & { apiBaseUrl?: string
93
93
  return '${VITE_API_BASE_URL}'; // Environment variable placeholder
94
94
  }
95
95
 
96
- // In monorepo mode, default to local backend
96
+ // In monorepo mode, default to local backend.
97
+ // Use explicit 127.0.0.1 (not `localhost`) to avoid Node's
98
+ // happy-eyeballs IPv6→IPv4 fallback. The vite dev-server WS proxy
99
+ // doesn't handle that fallback for WebSocket upgrades — it would
100
+ // try `[::1]:3000`, get ECONNREFUSED if the backend isn't on IPv6,
101
+ // and log the error instead of retrying on 127.0.0.1. Pinning
102
+ // 127.0.0.1 in the proxy target sidesteps that entirely.
97
103
  const port = (config as any).serverPort || 3000;
98
- return `http://localhost:${port}`;
104
+ return `http://127.0.0.1:${port}`;
99
105
  }
100
106
 
101
107
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specverse/engines",
3
- "version": "4.1.28",
3
+ "version": "4.1.30",
4
4
  "description": "SpecVerse toolchain \u2014 parser, inference, realize, generators, AI, registry",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,244 +0,0 @@
1
- import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, copyFileSync } from "fs";
2
- import { join, dirname } from "path";
3
- import { fileURLToPath } from "url";
4
- const __generatorDir = dirname(fileURLToPath(import.meta.url));
5
- function generateVSCodeExtension(context) {
6
- const { spec, outputDir } = context;
7
- const extensionDir = join(outputDir || ".", "tools", "vscode-extension");
8
- if (!existsSync(extensionDir)) mkdirSync(extensionDir, { recursive: true });
9
- const distribution = extractDistribution(spec);
10
- let cliCommands = [];
11
- if (distribution?.commands) {
12
- cliCommands = distribution.commands.map((cmd) => {
13
- const parts = (cmd.from || cmd.command || "").split(".");
14
- const name = parts[parts.length - 1] || "unknown";
15
- return {
16
- command: `specverse.${name}`,
17
- title: cmd.title || `${capitalize(name)}: ${cmd.description || ""}`,
18
- category: "SpecVerse"
19
- };
20
- });
21
- }
22
- if (cliCommands.length === 0) {
23
- cliCommands = extractCLICommands(spec);
24
- }
25
- if (cliCommands.length === 0) {
26
- cliCommands = getStandardCommands();
27
- }
28
- const entityTypes = extractEntityTypes(spec);
29
- const specVersion = distribution?.version || spec?.metadata?.version || spec?.version || "4.0.0";
30
- const packageJson = generatePackageJson(cliCommands, entityTypes, specVersion);
31
- if (distribution) {
32
- if (distribution.displayName) packageJson.displayName = distribution.displayName;
33
- if (distribution.publisher) packageJson.publisher = distribution.publisher;
34
- if (distribution.description) packageJson.description = distribution.description;
35
- if (distribution.languages && distribution.languages.length > 0) {
36
- packageJson.contributes.languages = distribution.languages.map((lang) => ({
37
- id: lang.id,
38
- aliases: lang.aliases || [lang.id],
39
- extensions: lang.extensions || [],
40
- configuration: `./language-configuration.json`
41
- }));
42
- packageJson.contributes.grammars = distribution.languages.filter((lang) => lang.grammar).map((lang) => ({
43
- language: lang.id,
44
- scopeName: lang.grammar,
45
- path: `./syntaxes/${lang.id}.tmLanguage.json`
46
- }));
47
- }
48
- if (distribution.themes && distribution.themes.length > 0) {
49
- packageJson.contributes.themes = distribution.themes.map((theme) => ({
50
- label: theme.name,
51
- uiTheme: theme.type === "dark" ? "vs-dark" : theme.type === "light" ? "vs" : "hc-black",
52
- path: `./themes/${theme.name.toLowerCase().replace(/\s+/g, "-")}-theme.json`
53
- }));
54
- }
55
- }
56
- writeFileSync(join(extensionDir, "package.json"), JSON.stringify(packageJson, null, 2) + "\n");
57
- let staticDir = join(__generatorDir, "static");
58
- if (!existsSync(staticDir)) {
59
- staticDir = join(__generatorDir.replace("/dist/libs/", "/libs/"), "static");
60
- }
61
- if (existsSync(staticDir)) {
62
- copyRecursive(staticDir, extensionDir);
63
- }
64
- const srcDir = join(extensionDir, "src");
65
- if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
66
- const extTs = join(staticDir, "extension.ts");
67
- if (existsSync(extTs)) {
68
- copyFileSync(extTs, join(srcDir, "extension.ts"));
69
- }
70
- const buildScript = `const esbuild = require('esbuild');
71
- esbuild.build({
72
- entryPoints: ['src/extension.ts'],
73
- bundle: true,
74
- outfile: 'dist/extension.js',
75
- external: ['vscode'],
76
- format: 'cjs',
77
- platform: 'node',
78
- }).catch(() => process.exit(1));
79
- `;
80
- const scriptsDir = join(extensionDir, "scripts");
81
- if (!existsSync(scriptsDir)) mkdirSync(scriptsDir, { recursive: true });
82
- writeFileSync(join(scriptsDir, "build.js"), buildScript);
83
- return `VSCode extension generated in: ${extensionDir}
84
- ${cliCommands.length} commands, ${entityTypes.length} entity keywords`;
85
- }
86
- function extractDistribution(spec) {
87
- const allDistributions = [];
88
- if (spec?.distributions) {
89
- const entries = Array.isArray(spec.distributions) ? spec.distributions : Object.entries(spec.distributions).map(([name, data]) => ({ name, ...data }));
90
- allDistributions.push(...entries);
91
- }
92
- const components = spec?.components || {};
93
- const componentList = Array.isArray(components) ? components : Object.values(components);
94
- for (const comp of componentList) {
95
- const distributions = comp?.distributions;
96
- if (!distributions) continue;
97
- const distEntries = Array.isArray(distributions) ? distributions : Object.entries(distributions).map(([name, data]) => ({ name, ...data }));
98
- allDistributions.push(...distEntries);
99
- }
100
- for (const dist of allDistributions) {
101
- if (dist.type === "ide") return dist;
102
- }
103
- return allDistributions.length > 0 ? allDistributions[0] : null;
104
- }
105
- function extractCLICommands(spec) {
106
- const commands = [];
107
- const components = spec?.components || {};
108
- const componentList = Array.isArray(components) ? components : Object.entries(components).map(([name, data]) => ({ name, ...data }));
109
- for (const comp of componentList) {
110
- const cliCommands = comp?.commands;
111
- if (!cliCommands) continue;
112
- for (const [rootName, rootDef] of Object.entries(cliCommands)) {
113
- const subcommands = rootDef?.subcommands || {};
114
- for (const [subName, subDef] of Object.entries(subcommands)) {
115
- const sub = subDef;
116
- const nestedSubs = sub?.subcommands;
117
- if (nestedSubs) {
118
- for (const [nestedName, nestedDef] of Object.entries(nestedSubs)) {
119
- commands.push({
120
- command: `specverse.${subName}.${nestedName}`,
121
- title: `${capitalize(subName)} ${capitalize(nestedName)}: ${nestedDef.description || ""}`,
122
- category: "SpecVerse"
123
- });
124
- }
125
- } else {
126
- commands.push({
127
- command: `specverse.${subName}`,
128
- title: `${capitalize(subName)}: ${sub.description || ""}`,
129
- category: "SpecVerse"
130
- });
131
- }
132
- }
133
- }
134
- }
135
- return commands;
136
- }
137
- function extractEntityTypes(spec) {
138
- const types = /* @__PURE__ */ new Set();
139
- const components = spec?.components || [];
140
- for (const component of Array.isArray(components) ? components : Object.values(components)) {
141
- const comp = component;
142
- const models = comp?.models;
143
- if (models) {
144
- if (Array.isArray(models)) {
145
- models.forEach((m) => types.add(m.name));
146
- } else {
147
- Object.keys(models).forEach((name) => types.add(name));
148
- }
149
- }
150
- }
151
- return [...types];
152
- }
153
- function generatePackageJson(commands, _entityTypes, specVersion = "1.0.0") {
154
- return {
155
- name: "specverse",
156
- displayName: "SpecVerse",
157
- description: "SpecVerse specification language support \u2014 syntax highlighting, validation, IntelliSense",
158
- version: specVersion,
159
- publisher: "specverse",
160
- engines: { vscode: "^1.80.0" },
161
- categories: ["Programming Languages", "Linters", "Snippets"],
162
- activationEvents: ["onLanguage:specverse"],
163
- main: "./dist/extension.js",
164
- contributes: {
165
- languages: [{
166
- id: "specverse",
167
- aliases: ["SpecVerse", "specly"],
168
- extensions: [".specly", ".specverse"],
169
- configuration: "./language-configuration.json"
170
- }],
171
- grammars: [{
172
- language: "specverse",
173
- scopeName: "source.specverse",
174
- path: "./syntaxes/specverse.tmLanguage.json"
175
- }],
176
- jsonValidation: [{
177
- fileMatch: ["*.specly", "*.specverse"],
178
- url: "./schemas/specverse-v3-schema.json"
179
- }],
180
- themes: [
181
- { label: "SpecVerse Dark", uiTheme: "vs-dark", path: "./themes/specverse-complete-theme.json" },
182
- { label: "SpecVerse Basic", uiTheme: "vs-dark", path: "./themes/specverse-basic-theme.json" }
183
- ],
184
- commands: commands.map((c) => ({
185
- command: c.command,
186
- title: c.title,
187
- category: c.category
188
- }))
189
- },
190
- scripts: {
191
- "vscode:prepublish": "npm run build",
192
- build: "node scripts/build.js",
193
- package: "npx @vscode/vsce package"
194
- },
195
- devDependencies: {
196
- "@types/vscode": "^1.80.0",
197
- "@vscode/vsce": "^3.0.0",
198
- esbuild: "^0.25.0"
199
- },
200
- overrides: {
201
- // Force the patched version of lodash through @vscode/vsce →
202
- // @secretlint → @textlint transitive chain (4.17.23 has a
203
- // code-injection advisory in `_.template`).
204
- lodash: "^4.18.1"
205
- }
206
- };
207
- }
208
- function copyRecursive(src, dest) {
209
- for (const entry of readdirSync(src)) {
210
- if (entry === "extension.ts") continue;
211
- const srcPath = join(src, entry);
212
- const destPath = join(dest, entry);
213
- if (statSync(srcPath).isDirectory()) {
214
- if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
215
- copyRecursive(srcPath, destPath);
216
- } else {
217
- copyFileSync(srcPath, destPath);
218
- }
219
- }
220
- }
221
- function getStandardCommands() {
222
- return [
223
- { command: "specverse.validate", title: "Validate specification", category: "SpecVerse" },
224
- { command: "specverse.infer", title: "Infer full architecture", category: "SpecVerse" },
225
- { command: "specverse.realize", title: "Generate code from specification", category: "SpecVerse" },
226
- { command: "specverse.init", title: "Initialize new project", category: "SpecVerse" },
227
- { command: "specverse.gen.diagrams", title: "Generate Mermaid diagrams", category: "SpecVerse" },
228
- { command: "specverse.gen.docs", title: "Generate documentation", category: "SpecVerse" },
229
- { command: "specverse.gen.uml", title: "Generate UML diagrams", category: "SpecVerse" },
230
- { command: "specverse.dev.format", title: "Format .specly file", category: "SpecVerse" },
231
- { command: "specverse.dev.watch", title: "Watch and validate on change", category: "SpecVerse" },
232
- { command: "specverse.dev.quick", title: "Quick validation", category: "SpecVerse" },
233
- { command: "specverse.cache", title: "Manage import cache", category: "SpecVerse" },
234
- { command: "specverse.ai.docs", title: "Generate AI implementation prompt", category: "SpecVerse" },
235
- { command: "specverse.ai.suggest", title: "Get spec improvement suggestions", category: "SpecVerse" },
236
- { command: "specverse.ai.template", title: "Load AI prompt template", category: "SpecVerse" }
237
- ];
238
- }
239
- function capitalize(str) {
240
- return str.charAt(0).toUpperCase() + str.slice(1);
241
- }
242
- export {
243
- generateVSCodeExtension as default
244
- };