ai-spec-dev 0.1.0 → 0.14.1

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.
Files changed (60) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/README.md +1211 -146
  3. package/RELEASE_LOG.md +1444 -0
  4. package/cli/index.ts +1961 -0
  5. package/cli/welcome.ts +151 -0
  6. package/core/code-generator.ts +740 -0
  7. package/core/combined-generator.ts +63 -0
  8. package/core/constitution-consolidator.ts +141 -0
  9. package/core/constitution-generator.ts +89 -0
  10. package/core/context-loader.ts +453 -0
  11. package/core/contract-bridge.ts +217 -0
  12. package/core/dsl-extractor.ts +337 -0
  13. package/core/dsl-types.ts +166 -0
  14. package/core/dsl-validator.ts +450 -0
  15. package/core/error-feedback.ts +354 -0
  16. package/core/frontend-context-loader.ts +602 -0
  17. package/core/global-constitution.ts +88 -0
  18. package/core/key-store.ts +49 -0
  19. package/core/knowledge-memory.ts +171 -0
  20. package/core/mock-server-generator.ts +571 -0
  21. package/core/openapi-exporter.ts +361 -0
  22. package/core/requirement-decomposer.ts +198 -0
  23. package/core/reviewer.ts +259 -0
  24. package/core/spec-assessor.ts +99 -0
  25. package/core/spec-generator.ts +428 -0
  26. package/core/spec-refiner.ts +89 -0
  27. package/core/spec-updater.ts +227 -0
  28. package/core/spec-versioning.ts +213 -0
  29. package/core/task-generator.ts +174 -0
  30. package/core/test-generator.ts +273 -0
  31. package/core/workspace-loader.ts +256 -0
  32. package/dist/cli/index.js +6717 -672
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/index.mjs +6717 -670
  35. package/dist/cli/index.mjs.map +1 -1
  36. package/dist/index.d.mts +147 -27
  37. package/dist/index.d.ts +147 -27
  38. package/dist/index.js +2337 -286
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +2329 -285
  41. package/dist/index.mjs.map +1 -1
  42. package/git/worktree.ts +109 -0
  43. package/index.ts +9 -0
  44. package/package.json +4 -28
  45. package/prompts/codegen.prompt.ts +259 -0
  46. package/prompts/consolidate.prompt.ts +73 -0
  47. package/prompts/constitution.prompt.ts +63 -0
  48. package/prompts/decompose.prompt.ts +168 -0
  49. package/prompts/dsl.prompt.ts +203 -0
  50. package/prompts/frontend-spec.prompt.ts +191 -0
  51. package/prompts/global-constitution.prompt.ts +61 -0
  52. package/prompts/spec-assess.prompt.ts +53 -0
  53. package/prompts/spec.prompt.ts +102 -0
  54. package/prompts/tasks.prompt.ts +35 -0
  55. package/prompts/testgen.prompt.ts +84 -0
  56. package/prompts/update.prompt.ts +131 -0
  57. package/purpose.docx +0 -0
  58. package/purpose.md +444 -0
  59. package/tsconfig.json +14 -0
  60. package/tsup.config.ts +10 -0
@@ -0,0 +1,571 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs-extra";
3
+ import { SpecDSL, ApiEndpoint, FieldMap } from "./dsl-types";
4
+
5
+ // ─── Types ────────────────────────────────────────────────────────────────────
6
+
7
+ export interface MockServerOptions {
8
+ /** Port for the mock server (default: 3001) */
9
+ port?: number;
10
+ /** Generate MSW handlers file */
11
+ msw?: boolean;
12
+ /** Generate frontend proxy config */
13
+ proxy?: boolean;
14
+ /** Output directory for generated files (default: mock/) */
15
+ outputDir?: string;
16
+ }
17
+
18
+ export interface MockGenerationResult {
19
+ files: Array<{ path: string; description: string }>;
20
+ }
21
+
22
+ // ─── Fixture Generator ────────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * Convert a type-description string to a fixture value (JavaScript literal).
26
+ */
27
+ function typeToFixture(fieldName: string, typeDesc: string): unknown {
28
+ const t = typeDesc.toLowerCase();
29
+
30
+ if (t.includes("boolean") || t === "bool") return true;
31
+ if (t.includes("int") || t.includes("number") || t.includes("float") || t.includes("decimal")) {
32
+ if (fieldName.toLowerCase().includes("id")) return 1;
33
+ if (fieldName.toLowerCase().includes("count") || fieldName.toLowerCase().includes("total")) return 42;
34
+ if (fieldName.toLowerCase().includes("price") || fieldName.toLowerCase().includes("amount")) return 9.99;
35
+ return 1;
36
+ }
37
+ if (t.includes("datetime") || t.includes("date") || t.includes("timestamp")) {
38
+ return "2024-01-15T10:30:00.000Z";
39
+ }
40
+ if (t.includes("[]") || t.includes("array") || t.includes("list")) return [];
41
+ if (t.includes("object") || t.includes("json") || t.includes("record")) return {};
42
+
43
+ // String heuristics by field name
44
+ const name = fieldName.toLowerCase();
45
+ if (name === "id" || name.endsWith("id")) return "abc123";
46
+ if (name.includes("email")) return "user@example.com";
47
+ if (name.includes("phone")) return "+1-555-0100";
48
+ if (name.includes("url") || name.includes("image") || name.includes("avatar")) return "https://example.com/sample.jpg";
49
+ if (name.includes("token") || name.includes("secret")) return "mock-token-xyz";
50
+ if (name.includes("name")) return "Example Name";
51
+ if (name.includes("title")) return "Example Title";
52
+ if (name.includes("description") || name.includes("content") || name.includes("body")) return "Example description text";
53
+ if (name.includes("status")) return "active";
54
+ if (name.includes("type") || name.includes("role")) return "default";
55
+ if (name.includes("code")) return "CODE001";
56
+
57
+ return `example_${fieldName}`;
58
+ }
59
+
60
+ function buildFixtureObject(fields: FieldMap): Record<string, unknown> {
61
+ const obj: Record<string, unknown> = {};
62
+ for (const [name, type] of Object.entries(fields)) {
63
+ obj[name] = typeToFixture(name, type);
64
+ }
65
+ return obj;
66
+ }
67
+
68
+ /**
69
+ * Build a fixture response object for an endpoint.
70
+ * For endpoints without explicit response schemas, generate minimal fixtures from model context.
71
+ */
72
+ function buildEndpointFixture(endpoint: ApiEndpoint, dsl: SpecDSL): unknown {
73
+ const method = endpoint.method;
74
+ const status = endpoint.successStatus;
75
+
76
+ // DELETE with 204 → no body
77
+ if (status === 204) return null;
78
+
79
+ // Try to derive fixture from model names mentioned in endpoint description
80
+ const descLower = endpoint.description.toLowerCase();
81
+ const matchedModel = dsl.models.find((m) =>
82
+ descLower.includes(m.name.toLowerCase())
83
+ );
84
+
85
+ if (matchedModel) {
86
+ const fields: FieldMap = {};
87
+ for (const f of matchedModel.fields) {
88
+ fields[f.name] = f.type;
89
+ }
90
+ const item = buildFixtureObject(fields);
91
+
92
+ // List endpoints return arrays
93
+ if (method === "GET" && (descLower.includes("list") || descLower.includes("all") || descLower.includes("paginate"))) {
94
+ return {
95
+ data: [item, { ...item, id: "def456" }],
96
+ total: 2,
97
+ page: 1,
98
+ pageSize: 10,
99
+ };
100
+ }
101
+ return { data: item };
102
+ }
103
+
104
+ // Fallback based on method
105
+ if (method === "POST") return { data: { id: "abc123", createdAt: "2024-01-15T10:30:00.000Z" } };
106
+ if (method === "GET") return { data: { id: "abc123" } };
107
+ return { success: true };
108
+ }
109
+
110
+ // ─── Express Mock Server Generator ───────────────────────────────────────────
111
+
112
+ function generateMockServerJs(dsl: SpecDSL, port: number): string {
113
+ const lines: string[] = [];
114
+
115
+ lines.push(`// ─── Mock Server ─────────────────────────────────────────────`);
116
+ lines.push(`// Auto-generated by ai-spec mock`);
117
+ lines.push(`// Feature: ${dsl.feature.title}`);
118
+ lines.push(`// DO NOT use in production. Standalone dev mock only.`);
119
+ lines.push(``);
120
+ lines.push(`const express = require('express');`);
121
+ lines.push(`const app = express();`);
122
+ lines.push(``);
123
+ lines.push(`app.use(express.json());`);
124
+ lines.push(``);
125
+ lines.push(`// ─── CORS (dev only) ─────────────────────────────────────────`);
126
+ lines.push(`app.use((req, res, next) => {`);
127
+ lines.push(` res.setHeader('Access-Control-Allow-Origin', '*');`);
128
+ lines.push(` res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');`);
129
+ lines.push(` res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');`);
130
+ lines.push(` if (req.method === 'OPTIONS') return res.sendStatus(204);`);
131
+ lines.push(` next();`);
132
+ lines.push(`});`);
133
+ lines.push(``);
134
+
135
+ // Auth middleware
136
+ const hasAuthEndpoints = dsl.endpoints.some((e) => e.auth);
137
+ if (hasAuthEndpoints) {
138
+ lines.push(`// ─── Auth check (returns 401 if Authorization header missing) ──`);
139
+ lines.push(`function requireAuth(req, res, next) {`);
140
+ lines.push(` const authHeader = req.headers['authorization'];`);
141
+ lines.push(` if (!authHeader || !authHeader.startsWith('Bearer ')) {`);
142
+ lines.push(` return res.status(401).json({ code: 'UNAUTHORIZED', message: 'Missing or invalid Authorization header' });`);
143
+ lines.push(` }`);
144
+ lines.push(` next();`);
145
+ lines.push(`}`);
146
+ lines.push(``);
147
+ }
148
+
149
+ lines.push(`// ─── Routes ──────────────────────────────────────────────────`);
150
+
151
+ for (const endpoint of dsl.endpoints) {
152
+ const fixture = buildEndpointFixture(endpoint, dsl);
153
+ const fixtureJson = JSON.stringify(fixture, null, 2)
154
+ .split("\n")
155
+ .map((l, i) => (i === 0 ? l : ` ${l}`))
156
+ .join("\n");
157
+
158
+ const expressMethod = endpoint.method.toLowerCase();
159
+ // Convert DSL path params :param to express :param (already compatible)
160
+ const expressPath = endpoint.path;
161
+
162
+ lines.push(`// ${endpoint.id}: ${endpoint.description}`);
163
+ if (endpoint.auth) {
164
+ lines.push(`app.${expressMethod}('${expressPath}', requireAuth, (req, res) => {`);
165
+ } else {
166
+ lines.push(`app.${expressMethod}('${expressPath}', (req, res) => {`);
167
+ }
168
+
169
+ if (fixture === null) {
170
+ lines.push(` res.sendStatus(${endpoint.successStatus});`);
171
+ } else {
172
+ lines.push(` res.status(${endpoint.successStatus}).json(${fixtureJson});`);
173
+ }
174
+
175
+ // Error simulation query param: ?simulate_error=ERRORCODE
176
+ if (endpoint.errors && endpoint.errors.length > 0) {
177
+ const firstError = endpoint.errors[0];
178
+ lines.push(` // Simulate error: GET /this/path?simulate_error=${firstError.code}`);
179
+ }
180
+
181
+ lines.push(`});`);
182
+ lines.push(``);
183
+ }
184
+
185
+ lines.push(`// ─── Start ───────────────────────────────────────────────────`);
186
+ lines.push(`const PORT = process.env.MOCK_PORT || ${port};`);
187
+ lines.push(`app.listen(PORT, () => {`);
188
+ lines.push(` console.log(\`[ai-spec mock] Mock server running at http://localhost:\${PORT}\`);`);
189
+ lines.push(` console.log('[ai-spec mock] Endpoints:');`);
190
+ for (const endpoint of dsl.endpoints) {
191
+ lines.push(` console.log(' ${endpoint.method.padEnd(7)} ${endpoint.path}');`);
192
+ }
193
+ lines.push(`});`);
194
+ lines.push(``);
195
+
196
+ return lines.join("\n");
197
+ }
198
+
199
+ // ─── Proxy Config Generators ──────────────────────────────────────────────────
200
+
201
+ function detectFrontendFramework(projectDir: string): "vite" | "next" | "webpack" | "cra" | "unknown" {
202
+ // Check vite.config
203
+ for (const f of ["vite.config.ts", "vite.config.js", "vite.config.mts"]) {
204
+ if (fs.existsSync(path.join(projectDir, f))) return "vite";
205
+ }
206
+ // Check next.config
207
+ for (const f of ["next.config.js", "next.config.ts", "next.config.mjs"]) {
208
+ if (fs.existsSync(path.join(projectDir, f))) return "next";
209
+ }
210
+ // Check for CRA (react-scripts in package.json)
211
+ const pkgPath = path.join(projectDir, "package.json");
212
+ if (fs.existsSync(pkgPath)) {
213
+ try {
214
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
215
+ const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
216
+ if (deps["react-scripts"]) return "cra";
217
+ } catch { /* ignore */ }
218
+ }
219
+ // Check webpack.config
220
+ for (const f of ["webpack.config.js", "webpack.config.ts"]) {
221
+ if (fs.existsSync(path.join(projectDir, f))) return "webpack";
222
+ }
223
+ return "unknown";
224
+ }
225
+
226
+ function generateViteProxyBlock(mockPort: number, endpoints: ApiEndpoint[]): string {
227
+ // Collect unique path prefixes
228
+ const prefixes = new Set<string>();
229
+ for (const ep of endpoints) {
230
+ const parts = ep.path.split("/").filter(Boolean);
231
+ if (parts.length > 0) prefixes.add(`/${parts[0]}`);
232
+ }
233
+ const target = `http://localhost:${mockPort}`;
234
+ const proxyEntries = Array.from(prefixes)
235
+ .map((p) => ` '${p}': { target: '${target}', changeOrigin: true }`)
236
+ .join(",\n");
237
+
238
+ return `// Add this proxy block to your vite.config.ts / vite.config.js
239
+ // Inside the defineConfig({ server: { proxy: { ... } } }) section:
240
+ //
241
+ // server: {
242
+ // proxy: {
243
+ ${proxyEntries
244
+ .split("\n")
245
+ .map((l) => `// ${l}`)
246
+ .join("\n")}
247
+ // }
248
+ // }
249
+
250
+ // ─── Standalone proxy snippet for vite.config.ts ─────────────────────────────
251
+ // import { defineConfig } from 'vite';
252
+ // export default defineConfig({
253
+ // server: {
254
+ // proxy: {
255
+ ${proxyEntries
256
+ .split("\n")
257
+ .map((l) => `// ${l.trim()}`)
258
+ .join("\n")}
259
+ // }
260
+ // }
261
+ // });
262
+ `;
263
+ }
264
+
265
+ function generateNextProxyBlock(mockPort: number, endpoints: ApiEndpoint[]): string {
266
+ const prefixes = new Set<string>();
267
+ for (const ep of endpoints) {
268
+ const parts = ep.path.split("/").filter(Boolean);
269
+ if (parts.length > 0) prefixes.add(`/${parts[0]}`);
270
+ }
271
+ const rewrites = Array.from(prefixes).map(
272
+ (p) => ` { source: '${p}/:path*', destination: 'http://localhost:${mockPort}${p}/:path*' }`
273
+ );
274
+
275
+ return `// Add this to your next.config.js rewrites():
276
+ //
277
+ // module.exports = {
278
+ // async rewrites() {
279
+ // return [
280
+ ${rewrites.map((r) => `// ${r}`).join(",\n")}
281
+ // ];
282
+ // },
283
+ // };
284
+ `;
285
+ }
286
+
287
+ function generateWebpackProxyBlock(mockPort: number, endpoints: ApiEndpoint[]): string {
288
+ const prefixes = new Set<string>();
289
+ for (const ep of endpoints) {
290
+ const parts = ep.path.split("/").filter(Boolean);
291
+ if (parts.length > 0) prefixes.add(`/${parts[0]}`);
292
+ }
293
+ const proxyEntries = Array.from(prefixes)
294
+ .map(
295
+ (p) =>
296
+ ` '${p}': {\n target: 'http://localhost:${mockPort}',\n changeOrigin: true\n }`
297
+ )
298
+ .join(",\n");
299
+
300
+ return `// Add this to your webpack.config.js devServer.proxy section:
301
+ //
302
+ // devServer: {
303
+ // proxy: {
304
+ ${proxyEntries
305
+ .split("\n")
306
+ .map((l) => `// ${l}`)
307
+ .join("\n")}
308
+ // }
309
+ // }
310
+ `;
311
+ }
312
+
313
+ function generateCraProxyBlock(mockPort: number): string {
314
+ return `// For Create React App: add a "proxy" field to package.json
315
+ // This only proxies requests that don't match static files:
316
+ //
317
+ // {
318
+ // "proxy": "http://localhost:${mockPort}"
319
+ // }
320
+ //
321
+ // Or use src/setupProxy.js for per-path control:
322
+ // const { createProxyMiddleware } = require('http-proxy-middleware');
323
+ // module.exports = function(app) {
324
+ // app.use('/api', createProxyMiddleware({ target: 'http://localhost:${mockPort}', changeOrigin: true }));
325
+ // };
326
+ `;
327
+ }
328
+
329
+ function generateProxyConfig(
330
+ dsl: SpecDSL,
331
+ mockPort: number,
332
+ projectDir: string
333
+ ): { content: string; filename: string } {
334
+ const framework = detectFrontendFramework(projectDir);
335
+
336
+ switch (framework) {
337
+ case "vite":
338
+ return {
339
+ filename: "mock/proxy.vite.comment.txt",
340
+ content: generateViteProxyBlock(mockPort, dsl.endpoints),
341
+ };
342
+ case "next":
343
+ return {
344
+ filename: "mock/proxy.next.comment.txt",
345
+ content: generateNextProxyBlock(mockPort, dsl.endpoints),
346
+ };
347
+ case "cra":
348
+ return {
349
+ filename: "mock/proxy.cra.comment.txt",
350
+ content: generateCraProxyBlock(mockPort),
351
+ };
352
+ default:
353
+ return {
354
+ filename: "mock/proxy.webpack.comment.txt",
355
+ content: generateWebpackProxyBlock(mockPort, dsl.endpoints),
356
+ };
357
+ }
358
+ }
359
+
360
+ // ─── MSW Handler Generator ────────────────────────────────────────────────────
361
+
362
+ function generateMswHandlers(dsl: SpecDSL): string {
363
+ const lines: string[] = [];
364
+
365
+ lines.push(`// ─── MSW Handlers ────────────────────────────────────────────`);
366
+ lines.push(`// Auto-generated by ai-spec mock --msw`);
367
+ lines.push(`// Feature: ${dsl.feature.title}`);
368
+ lines.push(`// Place this file at: src/mocks/handlers.ts`);
369
+ lines.push(``);
370
+ lines.push(`import { http, HttpResponse } from 'msw';`);
371
+ lines.push(``);
372
+
373
+ for (const endpoint of dsl.endpoints) {
374
+ const fixture = buildEndpointFixture(endpoint, dsl);
375
+ const fixtureJson = JSON.stringify(fixture, null, 4)
376
+ .split("\n")
377
+ .map((l, i) => (i === 0 ? l : ` ${l}`))
378
+ .join("\n");
379
+ const method = endpoint.method.toLowerCase();
380
+ lines.push(`// ${endpoint.id}: ${endpoint.description}`);
381
+ if (fixture === null) {
382
+ lines.push(`http.${method}('${endpoint.path}', () => {`);
383
+ lines.push(` return new HttpResponse(null, { status: ${endpoint.successStatus} });`);
384
+ } else {
385
+ lines.push(`http.${method}('${endpoint.path}', () => {`);
386
+ lines.push(` return HttpResponse.json(${fixtureJson}, { status: ${endpoint.successStatus} });`);
387
+ }
388
+ lines.push(`}),`);
389
+ lines.push(``);
390
+ }
391
+
392
+ // Wrap in export
393
+ const handlerLines = lines.join("\n");
394
+
395
+ return `// ─── MSW Handlers ────────────────────────────────────────────
396
+ // Auto-generated by ai-spec mock --msw
397
+ // Feature: ${dsl.feature.title}
398
+ // Place this file at: src/mocks/handlers.ts
399
+
400
+ import { http, HttpResponse } from 'msw';
401
+
402
+ export const handlers = [
403
+ ${dsl.endpoints
404
+ .map((endpoint) => {
405
+ const fixture = buildEndpointFixture(endpoint, dsl);
406
+ const method = endpoint.method.toLowerCase();
407
+ const indent = " ";
408
+ const fixtureJson = JSON.stringify(fixture, null, 2)
409
+ .split("\n")
410
+ .map((l, i) => (i === 0 ? l : `${indent} ${l}`))
411
+ .join("\n");
412
+
413
+ const comment = `${indent}// ${endpoint.id}: ${endpoint.description}`;
414
+ let handler: string;
415
+ if (fixture === null) {
416
+ handler = `${indent}http.${method}('${endpoint.path}', () => {\n${indent} return new HttpResponse(null, { status: ${endpoint.successStatus} });\n${indent}})`;
417
+ } else {
418
+ handler = `${indent}http.${method}('${endpoint.path}', () => {\n${indent} return HttpResponse.json(${fixtureJson}, { status: ${endpoint.successStatus} });\n${indent}})`;
419
+ }
420
+ return `${comment}\n${handler}`;
421
+ })
422
+ .join(",\n\n")}
423
+ ];
424
+ `;
425
+ }
426
+
427
+ function generateMswBrowser(): string {
428
+ return `// src/mocks/browser.ts
429
+ // MSW browser setup — import and call start() in your app entry point (dev only)
430
+ import { setupWorker } from 'msw/browser';
431
+ import { handlers } from './handlers';
432
+
433
+ export const worker = setupWorker(...handlers);
434
+ `;
435
+ }
436
+
437
+ // ─── Public API ───────────────────────────────────────────────────────────────
438
+
439
+ /**
440
+ * Generate mock server, proxy config, and/or MSW handlers from a SpecDSL.
441
+ * Writes files relative to projectDir.
442
+ */
443
+ export async function generateMockAssets(
444
+ dsl: SpecDSL,
445
+ projectDir: string,
446
+ opts: MockServerOptions = {}
447
+ ): Promise<MockGenerationResult> {
448
+ const port = opts.port ?? 3001;
449
+ const outputDir = path.join(projectDir, opts.outputDir ?? "mock");
450
+ const result: MockGenerationResult = { files: [] };
451
+
452
+ await fs.ensureDir(outputDir);
453
+
454
+ // 1. Express mock server (always generated)
455
+ const serverJs = generateMockServerJs(dsl, port);
456
+ const serverPath = path.join(outputDir, "server.js");
457
+ await fs.writeFile(serverPath, serverJs, "utf-8");
458
+ result.files.push({
459
+ path: path.relative(projectDir, serverPath),
460
+ description: `Express mock server — run with: node mock/server.js`,
461
+ });
462
+
463
+ // README inside mock/
464
+ const mockReadme = `# Mock Server
465
+
466
+ Auto-generated by \`ai-spec mock\` for **${dsl.feature.title}**.
467
+
468
+ ## Quick start
469
+
470
+ \`\`\`bash
471
+ # Install express (one-time)
472
+ npm install --save-dev express
473
+
474
+ # Start the mock server
475
+ node mock/server.js
476
+ # → Listening on http://localhost:${port}
477
+ \`\`\`
478
+
479
+ ## Endpoints
480
+
481
+ | Method | Path | Auth | Status |
482
+ |--------|------|------|--------|
483
+ ${dsl.endpoints
484
+ .map(
485
+ (e) =>
486
+ `| ${e.method} | \`${e.path}\` | ${e.auth ? "✓" : "—"} | ${e.successStatus} |`
487
+ )
488
+ .join("\n")}
489
+
490
+ ## Error simulation
491
+
492
+ Append \`?simulate_error=1\` to any request to test error handling (not yet auto-wired — edit server.js manually).
493
+ `;
494
+ const readmePath = path.join(outputDir, "README.md");
495
+ await fs.writeFile(readmePath, mockReadme, "utf-8");
496
+ result.files.push({
497
+ path: path.relative(projectDir, readmePath),
498
+ description: "Mock server usage guide",
499
+ });
500
+
501
+ // 2. Proxy config (optional)
502
+ if (opts.proxy) {
503
+ const { content, filename } = generateProxyConfig(dsl, port, projectDir);
504
+ const proxyPath = path.join(projectDir, filename);
505
+ await fs.ensureDir(path.dirname(proxyPath));
506
+ await fs.writeFile(proxyPath, content, "utf-8");
507
+ result.files.push({
508
+ path: filename,
509
+ description: "Proxy config snippet — copy instructions into your framework config",
510
+ });
511
+ }
512
+
513
+ // 3. MSW handlers (optional)
514
+ if (opts.msw) {
515
+ const mswDir = path.join(projectDir, "src", "mocks");
516
+ await fs.ensureDir(mswDir);
517
+
518
+ const handlersContent = generateMswHandlers(dsl);
519
+ const handlersPath = path.join(mswDir, "handlers.ts");
520
+ await fs.writeFile(handlersPath, handlersContent, "utf-8");
521
+ result.files.push({
522
+ path: path.relative(projectDir, handlersPath),
523
+ description: "MSW request handlers",
524
+ });
525
+
526
+ const browserContent = generateMswBrowser();
527
+ const browserPath = path.join(mswDir, "browser.ts");
528
+ await fs.writeFile(browserPath, browserContent, "utf-8");
529
+ result.files.push({
530
+ path: path.relative(projectDir, browserPath),
531
+ description: "MSW browser worker setup",
532
+ });
533
+ }
534
+
535
+ return result;
536
+ }
537
+
538
+ /**
539
+ * Find the latest DSL file in a project directory.
540
+ * Looks for *.dsl.json files under .ai-spec/
541
+ */
542
+ export async function findLatestDslFile(projectDir: string): Promise<string | null> {
543
+ const specDir = path.join(projectDir, ".ai-spec");
544
+ if (!(await fs.pathExists(specDir))) return null;
545
+
546
+ const allFiles: string[] = [];
547
+
548
+ async function scan(dir: string): Promise<void> {
549
+ const entries = await fs.readdir(dir);
550
+ for (const entry of entries) {
551
+ const abs = path.join(dir, entry);
552
+ const stat = await fs.stat(abs);
553
+ if (stat.isDirectory()) {
554
+ await scan(abs);
555
+ } else if (entry.endsWith(".dsl.json")) {
556
+ allFiles.push(abs);
557
+ }
558
+ }
559
+ }
560
+
561
+ await scan(specDir);
562
+
563
+ if (allFiles.length === 0) return null;
564
+
565
+ // Return most recently modified
566
+ const withMtimes = await Promise.all(
567
+ allFiles.map(async (f) => ({ f, mtime: (await fs.stat(f)).mtime }))
568
+ );
569
+ withMtimes.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
570
+ return withMtimes[0].f;
571
+ }