ai-spec-dev 0.37.0 → 0.41.0

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 (67) hide show
  1. package/README.md +381 -1796
  2. package/RELEASE_LOG.md +231 -0
  3. package/cli/commands/create.ts +9 -1176
  4. package/cli/commands/dashboard.ts +1 -1
  5. package/cli/pipeline/helpers.ts +34 -0
  6. package/cli/pipeline/multi-repo.ts +483 -0
  7. package/cli/pipeline/single-repo.ts +755 -0
  8. package/cli/utils.ts +2 -0
  9. package/core/code-generator.ts +52 -341
  10. package/core/codegen/helpers.ts +219 -0
  11. package/core/codegen/topo-sort.ts +98 -0
  12. package/core/constitution-consolidator.ts +2 -2
  13. package/core/dsl-coverage-checker.ts +298 -0
  14. package/core/dsl-extractor.ts +19 -46
  15. package/core/dsl-feedback.ts +1 -1
  16. package/core/dsl-validator.ts +74 -0
  17. package/core/error-feedback.ts +95 -11
  18. package/core/frontend-context-loader.ts +27 -5
  19. package/core/knowledge-memory.ts +52 -0
  20. package/core/mock/fixtures.ts +89 -0
  21. package/core/mock/proxy.ts +380 -0
  22. package/core/mock-server-generator.ts +12 -460
  23. package/core/requirement-decomposer.ts +4 -28
  24. package/core/reviewer.ts +1 -1
  25. package/core/safe-json.ts +76 -0
  26. package/core/spec-updater.ts +5 -21
  27. package/core/token-budget.ts +124 -0
  28. package/core/vcr.ts +20 -1
  29. package/dist/cli/index.js +4110 -3534
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/cli/index.mjs +4237 -3661
  32. package/dist/cli/index.mjs.map +1 -1
  33. package/dist/index.d.mts +18 -16
  34. package/dist/index.d.ts +18 -16
  35. package/dist/index.js +310 -182
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +308 -180
  38. package/dist/index.mjs.map +1 -1
  39. package/package.json +2 -2
  40. package/purpose.md +173 -33
  41. package/tests/auto-consolidation.test.ts +109 -0
  42. package/tests/combined-generator.test.ts +81 -0
  43. package/tests/constitution-consolidator.test.ts +161 -0
  44. package/tests/constitution-generator.test.ts +94 -0
  45. package/tests/contract-bridge.test.ts +201 -0
  46. package/tests/design-dialogue.test.ts +108 -0
  47. package/tests/dsl-coverage-checker.test.ts +230 -0
  48. package/tests/dsl-feedback.test.ts +45 -0
  49. package/tests/dsl-validator-xref.test.ts +99 -0
  50. package/tests/error-feedback-repair.test.ts +319 -0
  51. package/tests/error-feedback-validation.test.ts +91 -0
  52. package/tests/frontend-context-loader.test.ts +609 -0
  53. package/tests/global-constitution.test.ts +110 -0
  54. package/tests/key-store.test.ts +73 -0
  55. package/tests/knowledge-memory.test.ts +327 -0
  56. package/tests/project-index.test.ts +206 -0
  57. package/tests/prompt-hasher.test.ts +19 -0
  58. package/tests/requirement-decomposer.test.ts +171 -0
  59. package/tests/reviewer.test.ts +4 -1
  60. package/tests/run-logger.test.ts +289 -0
  61. package/tests/run-snapshot.test.ts +113 -0
  62. package/tests/safe-json.test.ts +63 -0
  63. package/tests/spec-updater.test.ts +161 -0
  64. package/tests/test-generator.test.ts +146 -0
  65. package/tests/token-budget.test.ts +124 -0
  66. package/tests/vcr-hash.test.ts +101 -0
  67. package/tests/workspace-loader.test.ts +277 -0
@@ -53,7 +53,7 @@ export function registerDashboard(program: Command): void {
53
53
  : process.platform === "win32"
54
54
  ? `start "" "${outputPath}"`
55
55
  : `xdg-open "${outputPath}"`;
56
- execSync(cmd);
56
+ execSync(cmd, { timeout: 10_000 });
57
57
  } catch {
58
58
  // Non-fatal — file was already written
59
59
  }
@@ -0,0 +1,34 @@
1
+ import chalk from "chalk";
2
+ import { SpecDSL } from "../../core/dsl-types";
3
+
4
+ // ─── Shared types ────────────────────────────────────────────────────────────
5
+
6
+ export type MultiRepoResult = {
7
+ repoName: string;
8
+ status: "success" | "failed" | "skipped";
9
+ specFile: string | null;
10
+ dsl: SpecDSL | null;
11
+ repoAbsPath: string;
12
+ role: string;
13
+ };
14
+
15
+ // ─── Banner ──────────────────────────────────────────────────────────────────
16
+
17
+ export function printBanner(opts: {
18
+ specProvider: string;
19
+ specModel: string;
20
+ codegenMode: string;
21
+ codegenProvider: string;
22
+ codegenModel: string;
23
+ }) {
24
+ console.log(chalk.blue("\n" + "─".repeat(52)));
25
+ console.log(chalk.bold(" ai-spec — AI-driven Development Orchestrator"));
26
+ console.log(chalk.blue("─".repeat(52)));
27
+ console.log(chalk.gray(` Spec : ${opts.specProvider} / ${opts.specModel}`));
28
+ console.log(
29
+ chalk.gray(
30
+ ` Codegen : ${opts.codegenMode} (${opts.codegenProvider} / ${opts.codegenModel})`
31
+ )
32
+ );
33
+ console.log(chalk.blue("─".repeat(52) + "\n"));
34
+ }
@@ -0,0 +1,483 @@
1
+ import * as path from "path";
2
+ import chalk from "chalk";
3
+ import { select } from "@inquirer/prompts";
4
+ import {
5
+ AIProvider,
6
+ createProvider,
7
+ DEFAULT_MODELS,
8
+ } from "../../core/spec-generator";
9
+ import { ContextLoader, isFrontendDeps } from "../../core/context-loader";
10
+ import { CodeGenerator, CodeGenMode } from "../../core/code-generator";
11
+ import { CodeReviewer } from "../../core/reviewer";
12
+ import { GitWorktreeManager } from "../../git/worktree";
13
+ import { ConstitutionGenerator } from "../../core/constitution-generator";
14
+ import { generateSpecWithTasks } from "../../core/combined-generator";
15
+ import { slugify, nextVersionPath } from "../../core/spec-versioning";
16
+ import { DslExtractor } from "../../core/dsl-extractor";
17
+ import { TestGenerator } from "../../core/test-generator";
18
+ import { runErrorFeedback } from "../../core/error-feedback";
19
+ import { accumulateReviewKnowledge } from "../../core/knowledge-memory";
20
+ import {
21
+ WorkspaceLoader,
22
+ WorkspaceConfig,
23
+ detectRepoType,
24
+ } from "../../core/workspace-loader";
25
+ import { SpecDSL } from "../../core/dsl-types";
26
+ import {
27
+ generateMockAssets,
28
+ applyMockProxy,
29
+ startMockServerBackground,
30
+ saveMockServerPid,
31
+ } from "../../core/mock-server-generator";
32
+ import { RequirementDecomposer, DecompositionResult } from "../../core/requirement-decomposer";
33
+ import { buildFrontendApiContract, buildContractContextSection } from "../../core/contract-bridge";
34
+ import { loadFrontendContext } from "../../core/frontend-context-loader";
35
+ import { buildFrontendSpecPrompt } from "../../prompts/frontend-spec.prompt";
36
+ import { AiSpecConfig, resolveApiKey } from "../utils";
37
+ import { printBanner, MultiRepoResult } from "./helpers";
38
+ import * as fs from "fs-extra";
39
+
40
+ // ─── Single-repo workspace pipeline ──────────────────────────────────────────
41
+
42
+ export async function runSingleRepoPipelineInWorkspace(opts: {
43
+ idea: string;
44
+ specProvider: AIProvider;
45
+ specProviderName: string;
46
+ specModelName: string;
47
+ codegenProvider: AIProvider;
48
+ codegenMode: CodeGenMode;
49
+ repoAbsPath: string;
50
+ repoName: string;
51
+ cliOpts: Record<string, unknown>;
52
+ contractContextSection?: string;
53
+ }): Promise<{ dsl: SpecDSL | null; specFile: string | null }> {
54
+ const {
55
+ idea,
56
+ specProvider,
57
+ specProviderName,
58
+ specModelName,
59
+ codegenProvider,
60
+ codegenMode,
61
+ repoAbsPath,
62
+ repoName,
63
+ cliOpts,
64
+ contractContextSection,
65
+ } = opts;
66
+
67
+ console.log(chalk.blue(`\n [${repoName}] Loading project context...`));
68
+ const loader = new ContextLoader(repoAbsPath);
69
+ let context = await loader.loadProjectContext();
70
+
71
+ const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
72
+
73
+ console.log(chalk.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
74
+ console.log(chalk.gray(` Dependencies: ${context.dependencies.length} packages`));
75
+ if (context.constitution && context.constitution.length > 6000) {
76
+ console.log(chalk.yellow(` ⚠ Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
77
+ }
78
+
79
+ if (!context.constitution) {
80
+ console.log(chalk.yellow(` Constitution: not found — auto-generating...`));
81
+ try {
82
+ const constitutionGen = new ConstitutionGenerator(specProvider);
83
+ const constitutionContent = await constitutionGen.generate(repoAbsPath);
84
+ await constitutionGen.saveConstitution(repoAbsPath, constitutionContent);
85
+ context.constitution = constitutionContent;
86
+ console.log(chalk.green(` Constitution: generated`));
87
+ } catch (err) {
88
+ console.log(chalk.yellow(` Constitution: auto-generation failed (${(err as Error).message}), continuing.`));
89
+ }
90
+ } else {
91
+ console.log(chalk.green(` Constitution: found`));
92
+ }
93
+
94
+ let fullIdea = idea;
95
+ if (contractContextSection) {
96
+ fullIdea = `${idea}\n\n${contractContextSection}`;
97
+ }
98
+
99
+ console.log(chalk.blue(` [${repoName}] Generating spec...`));
100
+ let finalSpec: string;
101
+ try {
102
+ const result = await generateSpecWithTasks(specProvider, fullIdea, context);
103
+ finalSpec = result.spec;
104
+ console.log(chalk.green(` Spec generated.`));
105
+ } catch (err) {
106
+ console.error(chalk.red(` Spec generation failed: ${(err as Error).message}`));
107
+ return { dsl: null, specFile: null };
108
+ }
109
+
110
+ // DSL Extraction
111
+ let extractedDsl: SpecDSL | null = null;
112
+ if (!cliOpts.skipDsl) {
113
+ console.log(chalk.blue(` [${repoName}] Extracting DSL...`));
114
+ try {
115
+ const dslExtractor = new DslExtractor(specProvider);
116
+ const repoIsFrontend = isFrontendDeps(context.dependencies);
117
+ extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
118
+ if (extractedDsl) {
119
+ console.log(chalk.green(` DSL extracted.`));
120
+ }
121
+ } catch (err) {
122
+ console.log(chalk.yellow(` DSL extraction failed: ${(err as Error).message}`));
123
+ }
124
+ }
125
+
126
+ // Git Worktree — auto-skip for frontend repos
127
+ const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
128
+ const skipWorktreeForRepo = cliOpts.worktree
129
+ ? false
130
+ : cliOpts.skipWorktree || isFrontendRepo;
131
+
132
+ let workingDir = repoAbsPath;
133
+ if (!skipWorktreeForRepo) {
134
+ console.log(chalk.blue(` [${repoName}] Setting up git worktree...`));
135
+ try {
136
+ const worktreeManager = new GitWorktreeManager(repoAbsPath);
137
+ const worktreePath = await worktreeManager.createWorktree(idea);
138
+ if (worktreePath) workingDir = worktreePath;
139
+ } catch (err) {
140
+ console.log(chalk.yellow(` Worktree setup failed: ${(err as Error).message}. Using main branch.`));
141
+ }
142
+ } else {
143
+ console.log(chalk.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
144
+ }
145
+
146
+ // Save Spec
147
+ const specsDir = path.join(workingDir, "specs");
148
+ await fs.ensureDir(specsDir);
149
+ const featureSlug = slugify(idea);
150
+ const { filePath: specFile } = await nextVersionPath(specsDir, featureSlug);
151
+ await fs.writeFile(specFile, finalSpec, "utf-8");
152
+ console.log(chalk.green(` Spec saved: ${path.relative(repoAbsPath, specFile)}`));
153
+
154
+ let savedDslFile: string | null = null;
155
+ if (extractedDsl) {
156
+ const dslExtractorForSave = new DslExtractor(specProvider);
157
+ savedDslFile = await dslExtractorForSave.saveDsl(extractedDsl, specFile);
158
+ console.log(chalk.green(` DSL saved: ${path.relative(repoAbsPath, savedDslFile)}`));
159
+ }
160
+
161
+ // Code Generation
162
+ console.log(chalk.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
163
+ try {
164
+ const codegen = new CodeGenerator(codegenProvider, codegenMode);
165
+ await codegen.generateCode(specFile, workingDir, context, {
166
+ auto: true,
167
+ dslFilePath: savedDslFile ?? undefined,
168
+ repoType: detectedRepoType,
169
+ });
170
+ console.log(chalk.green(` Code generation complete.`));
171
+ } catch (err) {
172
+ console.log(chalk.yellow(` Code generation failed: ${(err as Error).message}`));
173
+ }
174
+
175
+ // Test Generation
176
+ if (!cliOpts.skipTests && extractedDsl) {
177
+ console.log(chalk.blue(` [${repoName}] Generating test skeletons...`));
178
+ try {
179
+ const testGen = new TestGenerator(codegenProvider);
180
+ const testFiles = await testGen.generate(extractedDsl, workingDir);
181
+ console.log(chalk.green(` ${testFiles.length} test file(s) generated.`));
182
+ } catch (err) {
183
+ console.log(chalk.yellow(` Test generation failed: ${(err as Error).message}`));
184
+ }
185
+ }
186
+
187
+ // Error Feedback
188
+ if (!cliOpts.skipErrorFeedback) {
189
+ try {
190
+ await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
191
+ } catch (err) {
192
+ console.log(chalk.yellow(` Error feedback failed: ${(err as Error).message}`));
193
+ }
194
+ }
195
+
196
+ // Code Review
197
+ if (!cliOpts.skipReview) {
198
+ console.log(chalk.blue(` [${repoName}] Running code review...`));
199
+ try {
200
+ const reviewer = new CodeReviewer(specProvider, workingDir);
201
+ const reviewResult = await reviewer.reviewCode(finalSpec);
202
+ await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
203
+ console.log(chalk.green(` Code review complete.`));
204
+ } catch (err) {
205
+ console.log(chalk.yellow(` Code review failed: ${(err as Error).message}`));
206
+ }
207
+ }
208
+
209
+ return { dsl: extractedDsl, specFile };
210
+ }
211
+
212
+ // ─── Multi-repo pipeline ────────────────────────────────────────────────────
213
+
214
+ /**
215
+ * Multi-repo pipeline: decompose → order repos → run each repo in order → bridge contracts.
216
+ */
217
+ export async function runMultiRepoPipeline(
218
+ idea: string,
219
+ workspace: WorkspaceConfig,
220
+ opts: Record<string, unknown>,
221
+ currentDir: string,
222
+ config: AiSpecConfig
223
+ ): Promise<MultiRepoResult[]> {
224
+ // ── Resolve providers ──────────────────────────────────────────────────────
225
+ const specProviderName = (opts.provider as string) || config.provider || "gemini";
226
+ const specModelName = (opts.model as string) || config.model || DEFAULT_MODELS[specProviderName];
227
+ const specApiKey = await resolveApiKey(specProviderName, opts.key as string | undefined);
228
+ const specProvider = createProvider(specProviderName, specApiKey, specModelName);
229
+
230
+ const codegenMode: CodeGenMode = ((opts.codegen as string) as CodeGenMode) || config.codegen || "claude-code";
231
+ const codegenProviderName = (opts.codegenProvider as string) || config.codegenProvider || specProviderName;
232
+ const codegenModelName = (opts.codegenModel as string) || config.codegenModel || DEFAULT_MODELS[codegenProviderName];
233
+ const codegenApiKey =
234
+ codegenProviderName === specProviderName
235
+ ? specApiKey
236
+ : await resolveApiKey(codegenProviderName, opts.codegenKey as string | undefined);
237
+ const codegenProvider =
238
+ codegenProviderName === specProviderName && codegenApiKey === specApiKey
239
+ ? specProvider
240
+ : createProvider(codegenProviderName, codegenApiKey, codegenModelName);
241
+
242
+ printBanner({
243
+ specProvider: specProviderName,
244
+ specModel: specModelName,
245
+ codegenMode,
246
+ codegenProvider: codegenProviderName,
247
+ codegenModel: codegenModelName,
248
+ });
249
+
250
+ const workspaceLoader = new WorkspaceLoader(currentDir);
251
+
252
+ // ── Step 1: Load per-repo contexts ─────────────────────────────────────────
253
+ console.log(chalk.blue("\n[W1] Loading per-repo contexts..."));
254
+ const contexts = new Map<string, import("../../core/context-loader").ProjectContext>();
255
+ const frontendContexts = new Map<string, import("../../core/frontend-context-loader").FrontendContext>();
256
+
257
+ for (const repo of workspace.repos) {
258
+ const repoAbsPath = workspaceLoader.resolveAbsPath(repo);
259
+ try {
260
+ const loader = new ContextLoader(repoAbsPath);
261
+ const ctx = await loader.loadProjectContext();
262
+ contexts.set(repo.name, ctx);
263
+
264
+ if (repo.role === "frontend" || repo.role === "mobile") {
265
+ const fctx = await loadFrontendContext(repoAbsPath);
266
+ frontendContexts.set(repo.name, fctx);
267
+ console.log(chalk.gray(` ${repo.name}: ${fctx.framework} / ${fctx.httpClient} / hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
268
+ } else {
269
+ console.log(chalk.gray(` ${repo.name}: ${ctx.techStack.join(", ") || "unknown"} (${ctx.dependencies.length} deps)`));
270
+ }
271
+ } catch (err) {
272
+ console.log(chalk.yellow(` ${repo.name}: context load failed — ${(err as Error).message}`));
273
+ }
274
+ }
275
+
276
+ // ── Step 2: Decompose requirement ─────────────────────────────────────────
277
+ console.log(chalk.blue("\n[W2] Decomposing requirement across repos..."));
278
+ const decomposer = new RequirementDecomposer(specProvider);
279
+ let decomposition: DecompositionResult;
280
+
281
+ try {
282
+ decomposition = await decomposer.decompose(idea, workspace, contexts, frontendContexts);
283
+ console.log(chalk.green(` Summary: ${decomposition.summary}`));
284
+ console.log(chalk.gray(` Repos affected: ${decomposition.repos.map((r) => r.repoName).join(", ")}`));
285
+ if (decomposition.coordinationNotes) {
286
+ console.log(chalk.gray(` Coordination: ${decomposition.coordinationNotes}`));
287
+ }
288
+ } catch (err) {
289
+ console.error(chalk.red(` Decomposition failed: ${(err as Error).message}`));
290
+ console.log(chalk.yellow(" Falling back to running all repos independently."));
291
+ decomposition = {
292
+ originalRequirement: idea,
293
+ summary: idea,
294
+ coordinationNotes: "",
295
+ repos: workspace.repos.map((repo) => ({
296
+ repoName: repo.name,
297
+ role: repo.role,
298
+ specIdea: idea,
299
+ isContractProvider: repo.role === "backend",
300
+ dependsOnRepos: repo.role !== "backend" ? workspace.repos.filter((r) => r.role === "backend").map((r) => r.name) : [],
301
+ uxDecisions: null,
302
+ })),
303
+ };
304
+ }
305
+
306
+ // ── Step 3: Show decomposition preview + confirmation ─────────────────────
307
+ if (!opts.auto) {
308
+ console.log(chalk.cyan("\n[W3] Decomposition Preview:"));
309
+ console.log(chalk.cyan("─".repeat(52)));
310
+ for (const r of decomposition.repos) {
311
+ console.log(chalk.bold(` ${r.repoName} (${r.role})`));
312
+ console.log(chalk.gray(` ${r.specIdea.slice(0, 150)}${r.specIdea.length > 150 ? "..." : ""}`));
313
+ if (r.uxDecisions) {
314
+ const ux = r.uxDecisions;
315
+ const uxSummary = [
316
+ ux.throttleMs ? `throttle ${ux.throttleMs}ms` : "",
317
+ ux.debounceMs ? `debounce ${ux.debounceMs}ms` : "",
318
+ ux.optimisticUpdate ? "optimistic-update" : "",
319
+ ux.errorRollback ? "rollback" : "",
320
+ ]
321
+ .filter(Boolean)
322
+ .join(", ");
323
+ if (uxSummary) console.log(chalk.cyan(` UX: ${uxSummary}`));
324
+ }
325
+ if (r.dependsOnRepos.length > 0) {
326
+ console.log(chalk.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
327
+ }
328
+ }
329
+ console.log(chalk.cyan("─".repeat(52)));
330
+
331
+ const gate = await select({
332
+ message: "Proceed with multi-repo pipeline?",
333
+ choices: [
334
+ { name: "Proceed — run all repos", value: "proceed" },
335
+ { name: "Abort", value: "abort" },
336
+ ],
337
+ });
338
+
339
+ if (gate === "abort") {
340
+ console.log(chalk.yellow(" Aborted."));
341
+ process.exit(0);
342
+ }
343
+ }
344
+
345
+ // ── Step 4: Sort repos by dependency order ─────────────────────────────────
346
+ const sortedRepoRequirements = RequirementDecomposer.sortByDependency(decomposition.repos);
347
+
348
+ const contractDsls = new Map<string, SpecDSL>();
349
+
350
+ // ── Step 5: Run each repo's pipeline ──────────────────────────────────────
351
+ console.log(chalk.blue(`\n[W4] Running pipeline for ${sortedRepoRequirements.length} repo(s)...`));
352
+
353
+ const results: MultiRepoResult[] = [];
354
+
355
+ for (const repoReq of sortedRepoRequirements) {
356
+ const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
357
+ if (!repoConfig) {
358
+ console.log(chalk.yellow(` Skipping ${repoReq.repoName} — not found in workspace config.`));
359
+ results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
360
+ continue;
361
+ }
362
+
363
+ const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
364
+
365
+ console.log(chalk.bold.blue(`\n ── ${repoReq.repoName} (${repoReq.role}) ──────────────────────`));
366
+
367
+ let contractContextSection: string | undefined;
368
+ if (repoReq.dependsOnRepos.length > 0) {
369
+ const contractParts: string[] = [];
370
+ for (const depName of repoReq.dependsOnRepos) {
371
+ const depDsl = contractDsls.get(depName);
372
+ if (depDsl) {
373
+ console.log(chalk.gray(` Using API contract from: ${depName}`));
374
+ const contract = buildFrontendApiContract(depDsl);
375
+ contractParts.push(buildContractContextSection(contract));
376
+ }
377
+ }
378
+ if (contractParts.length > 0) {
379
+ contractContextSection = contractParts.join("\n\n");
380
+ }
381
+ }
382
+
383
+ let specIdea = repoReq.specIdea;
384
+ if (
385
+ (repoReq.role === "frontend" || repoReq.role === "mobile") &&
386
+ repoReq.uxDecisions
387
+ ) {
388
+ const frontendCtx = await loadFrontendContext(repoAbsPath);
389
+
390
+ specIdea = buildFrontendSpecPrompt({
391
+ specIdea: repoReq.specIdea,
392
+ apiContractSection: contractContextSection,
393
+ uxDecisions: repoReq.uxDecisions,
394
+ frontendContext: frontendCtx,
395
+ });
396
+
397
+ contractContextSection = undefined;
398
+
399
+ console.log(chalk.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
400
+ }
401
+
402
+ try {
403
+ const { dsl, specFile } = await runSingleRepoPipelineInWorkspace({
404
+ idea: specIdea,
405
+ specProvider,
406
+ specProviderName,
407
+ specModelName,
408
+ codegenProvider,
409
+ codegenMode,
410
+ repoAbsPath,
411
+ repoName: repoReq.repoName,
412
+ cliOpts: opts,
413
+ contractContextSection,
414
+ });
415
+
416
+ if (repoReq.isContractProvider && dsl) {
417
+ contractDsls.set(repoReq.repoName, dsl);
418
+ console.log(chalk.green(` Contract stored for downstream repos.`));
419
+ }
420
+
421
+ results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
422
+ console.log(chalk.green(` ✔ ${repoReq.repoName} complete`));
423
+ } catch (err) {
424
+ console.error(chalk.red(` ✘ ${repoReq.repoName} failed: ${(err as Error).message}`));
425
+ results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
426
+ }
427
+ }
428
+
429
+ // ── Done ──────────────────────────────────────────────────────────────────
430
+ console.log(chalk.bold.green("\n✔ Multi-repo pipeline complete!"));
431
+ console.log(chalk.gray(` Workspace: ${workspace.name}`));
432
+ console.log(chalk.gray(` Requirement: ${idea}`));
433
+ console.log();
434
+ for (const r of results) {
435
+ const icon = r.status === "success" ? chalk.green("✔") : r.status === "failed" ? chalk.red("✘") : chalk.gray("−");
436
+ const specInfo = r.specFile ? chalk.gray(` → ${r.specFile}`) : "";
437
+ console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
438
+ }
439
+
440
+ return results;
441
+ }
442
+
443
+ // ─── Auto-serve helper ──────────────────────────────────────────────────────
444
+
445
+ export async function handleAutoServe(
446
+ pipelineResults: MultiRepoResult[]
447
+ ): Promise<void> {
448
+ console.log(chalk.blue("\n─── Auto-serve: starting mock server ───────────"));
449
+ const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
450
+ const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
451
+
452
+ if (!backendResult) {
453
+ console.log(chalk.yellow(" No successful backend with DSL found — skipping auto-serve."));
454
+ return;
455
+ }
456
+
457
+ const mockPort = 3001;
458
+ const mockResult = await generateMockAssets(backendResult.dsl!, backendResult.repoAbsPath, { port: mockPort });
459
+ const serverJsPath = path.join(backendResult.repoAbsPath, "mock", "server.js");
460
+ console.log(chalk.green(` ✔ Mock assets generated (${mockResult.files.length} file(s))`));
461
+
462
+ const pid = startMockServerBackground(serverJsPath, mockPort);
463
+ console.log(chalk.green(` ✔ Mock server started (PID ${pid}) → http://localhost:${mockPort}`));
464
+
465
+ if (frontendResult) {
466
+ const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl!.endpoints);
467
+ await saveMockServerPid(frontendResult.repoAbsPath, pid);
468
+ if (proxyResult.applied) {
469
+ console.log(chalk.green(` ✔ Frontend proxy patched (${proxyResult.framework})`));
470
+ console.log(chalk.bold.cyan(`\n Ready! Run your frontend dev server:`));
471
+ console.log(chalk.white(` cd ${frontendResult.repoAbsPath}`));
472
+ console.log(chalk.white(` ${proxyResult.devCommand}`));
473
+ console.log(chalk.gray(`\n When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
474
+ } else {
475
+ console.log(chalk.yellow(` ⚠ Auto-patch not available for ${proxyResult.framework}.`));
476
+ if (proxyResult.note) console.log(chalk.gray(` ${proxyResult.note}`));
477
+ console.log(chalk.gray(` Mock server: http://localhost:${mockPort}`));
478
+ }
479
+ } else {
480
+ console.log(chalk.gray(` No frontend repo found — mock server is running at http://localhost:${mockPort}`));
481
+ console.log(chalk.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
482
+ }
483
+ }