@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.
- package/dist/libs/instance-factories/applications/templates/generic/main-generator.js +3 -3
- package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +16 -6
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +11 -3
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +27 -17
- package/dist/libs/instance-factories/shared/path-resolver.js +1 -1
- package/libs/instance-factories/applications/templates/generic/main-generator.ts +3 -3
- package/libs/instance-factories/applications/templates/react/api-client-generator.ts +16 -6
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +11 -3
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +27 -17
- package/libs/instance-factories/shared/path-resolver.ts +8 -2
- package/package.json +1 -1
- package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js.bak +0 -244
|
@@ -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 || '
|
|
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 || '
|
|
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 || '
|
|
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
|
-
'
|
|
343
|
-
\`/\${resource}/\${entityId}/
|
|
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
|
-
'
|
|
449
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
@@ -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 || '
|
|
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 || '
|
|
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 || '
|
|
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
|
-
'
|
|
379
|
-
\`/\${resource}/\${entityId}/
|
|
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
|
-
'
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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://
|
|
104
|
+
return `http://127.0.0.1:${port}`;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
/**
|
package/package.json
CHANGED
package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js.bak
DELETED
|
@@ -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
|
-
};
|