ai-spec-dev 0.42.0 → 0.55.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 (70) hide show
  1. package/README.md +86 -40
  2. package/cli/commands/config.ts +129 -1
  3. package/cli/commands/create.ts +246 -11
  4. package/cli/commands/fix-history.ts +176 -0
  5. package/cli/commands/init.ts +344 -106
  6. package/cli/index.ts +3 -7
  7. package/cli/pipeline/helpers.ts +6 -0
  8. package/cli/pipeline/multi-repo.ts +291 -26
  9. package/cli/pipeline/single-repo.ts +103 -2
  10. package/cli/utils.ts +95 -4
  11. package/core/code-generator.ts +63 -14
  12. package/core/config-defaults.ts +44 -0
  13. package/core/constitution-generator.ts +2 -1
  14. package/core/cross-stack-verifier.ts +395 -0
  15. package/core/dsl-extractor.ts +2 -1
  16. package/core/error-feedback.ts +3 -2
  17. package/core/fix-history.ts +333 -0
  18. package/core/import-fixer.ts +827 -0
  19. package/core/import-verifier.ts +569 -0
  20. package/core/knowledge-memory.ts +55 -6
  21. package/core/openapi-exporter.ts +3 -2
  22. package/core/repo-store.ts +95 -0
  23. package/core/reviewer.ts +14 -13
  24. package/core/run-logger.ts +3 -4
  25. package/core/run-snapshot.ts +2 -3
  26. package/core/run-trend.ts +3 -4
  27. package/core/self-evaluator.ts +44 -7
  28. package/core/spec-generator.ts +30 -45
  29. package/core/token-budget.ts +3 -8
  30. package/core/types-generator.ts +2 -2
  31. package/core/vcr.ts +3 -1
  32. package/dist/cli/index.js +3889 -1937
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/index.mjs +3888 -1936
  35. package/dist/cli/index.mjs.map +1 -1
  36. package/dist/index.d.mts +17 -2
  37. package/dist/index.d.ts +17 -2
  38. package/dist/index.js +292 -181
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +292 -181
  41. package/dist/index.mjs.map +1 -1
  42. package/package.json +2 -2
  43. package/tests/cross-stack-verifier.test.ts +301 -0
  44. package/tests/fix-history.test.ts +335 -0
  45. package/tests/import-fixer.test.ts +944 -0
  46. package/tests/import-verifier.test.ts +420 -0
  47. package/tests/knowledge-memory.test.ts +40 -0
  48. package/tests/self-evaluator.test.ts +97 -0
  49. package/cli/commands/model.ts +0 -156
  50. package/cli/commands/scan.ts +0 -99
  51. package/cli/commands/workspace.ts +0 -219
  52. package/demo-backend/.ai-spec-constitution.md +0 -65
  53. package/demo-backend/package.json +0 -21
  54. package/demo-backend/prisma/schema.prisma +0 -22
  55. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
  56. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
  57. package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
  58. package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
  59. package/demo-backend/src/index.ts +0 -17
  60. package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
  61. package/demo-backend/src/routes/bookmark.routes.ts +0 -11
  62. package/demo-backend/src/routes/index.ts +0 -8
  63. package/demo-backend/src/services/bookmark.service.test.ts +0 -433
  64. package/demo-backend/src/services/bookmark.service.ts +0 -261
  65. package/demo-backend/tsconfig.json +0 -12
  66. package/demo-frontend/.ai-spec-constitution.md +0 -95
  67. package/demo-frontend/package.json +0 -23
  68. package/demo-frontend/src/App.tsx +0 -12
  69. package/demo-frontend/src/main.tsx +0 -9
  70. package/demo-frontend/tsconfig.json +0 -13
package/README.md CHANGED
@@ -12,8 +12,8 @@
12
12
  <p align="center">
13
13
  <a href="https://github.com/hzhongzhong/ai-spec"><img src="https://img.shields.io/badge/GitHub-ai--spec-181717?logo=github" alt="GitHub" /></a>
14
14
  <a href="https://www.npmjs.com/package/ai-spec-dev"><img src="https://img.shields.io/npm/v/ai-spec-dev?color=cb3837&logo=npm" alt="npm" /></a>
15
- <img src="https://img.shields.io/badge/version-0.37.0-blue" alt="version" />
16
- <img src="https://img.shields.io/badge/tests-409%20passed-brightgreen" alt="tests" />
15
+ <img src="https://img.shields.io/badge/version-0.55.0-blue" alt="version" />
16
+ <img src="https://img.shields.io/badge/tests-913%20passed-brightgreen" alt="tests" />
17
17
  <img src="https://img.shields.io/badge/providers-9-orange" alt="providers" />
18
18
  <img src="https://img.shields.io/badge/license-MIT-green" alt="license" />
19
19
  </p>
@@ -31,10 +31,11 @@
31
31
  **ai-spec** is an AI-driven development orchestrator SDK & CLI that transforms a one-line requirement into production-ready code through a fully automated pipeline:
32
32
 
33
33
  ```
34
- Requirement ConstitutionContext Spec + Tasks Interactive Refinement
34
+ init: Register ReposProject ConstitutionsGlobal Constitution
35
+ create: Requirement → Select Repo(s) → Context → Spec + Tasks → Refinement
35
36
  → Quality Assessment → Approval Gate → DSL Extraction → Gap Feedback
36
37
  → Git Isolation → Code Generation → TDD / Test Skeleton → Auto Error Fix
37
- → 3-Pass Code Review → Review→DSL Loop → Harness Self-Eval → Knowledge Memory
38
+ → 3-Pass Code Review → Review→DSL Loop → Harness Self-Eval
38
39
  ```
39
40
 
40
41
  **Multi-Repo mode (Workspace):**
@@ -69,16 +70,20 @@ npm run build
69
70
  # Set API key (Gemini example)
70
71
  export GEMINI_API_KEY=your_key_here
71
72
 
72
- # Initialize project constitution (optional create auto-triggers it)
73
+ # Initialize: register repos + generate constitutions
73
74
  ai-spec init
74
75
 
75
- # Start developing
76
+ # Start developing (select registered repo → run pipeline)
76
77
  ai-spec create "Add login functionality to user module"
77
78
  ```
78
79
 
79
80
  ### Pipeline Demo
80
81
 
81
82
  ```
83
+ [Repo] Select repo(s) for this feature:
84
+ ● my-vue-app (vue / frontend)
85
+ ○ my-node-api (node-express / backend)
86
+ ✔ 1 repo selected
82
87
  [1/6] Loading project context...
83
88
  Constitution : ✔ found
84
89
  [2/6] Generating spec with gemini/gemini-2.5-pro...
@@ -117,19 +122,35 @@ ai-spec create "Add login functionality to user module"
117
122
  ### Commands
118
123
 
119
124
  ```
120
- ai-spec init Analyze codebase, generate project constitution
121
- ai-spec create [idea] Full pipeline: constitution spec → DSL → codegen → review → self-eval
122
- ai-spec update [change] Incremental update: modify spec → re-extract DSL regenerate affected files
123
- ai-spec learn [lesson] Inject knowledge directly into constitution §9
124
- ai-spec review [file] 3-pass AI code review (architecture + implementation + impact)
125
- ai-spec export DSL OpenAPI 3.1.0 YAML/JSON
126
- ai-spec types DSL TypeScript types (models + endpoint types + API_ENDPOINTS)
127
- ai-spec mock DSL Mock server + MSW handlers + proxy config
128
- ai-spec dashboard Generate static HTML harness dashboard
129
- ai-spec restore <runId> Rollback all files modified by a specific run
130
- ai-spec model Interactive provider/model switcher
131
- ai-spec config View/modify project configuration
132
- ai-spec workspace init Initialize multi-repo workspace
125
+ # Core workflow
126
+ ai-spec init Register repos, generate constitutions (auto-scans projects)
127
+ ai-spec init --add-repo Quick-add a single repo
128
+ ai-spec init --status Show registered repos and constitution health
129
+ ai-spec create [idea] Full pipeline: spec DSL codegen review
130
+ ai-spec create [idea] --openapi Also auto-generate OpenAPI 3.1.0 YAML after DSL
131
+ ai-spec create [idea] --types Also auto-generate TypeScript types after DSL
132
+ ai-spec update [change] Incremental: modify spec re-extract DSL regen affected files
133
+ ai-spec review [file] 3-pass AI code review (architecture + implementation + impact)
134
+
135
+ # Knowledge & constitution
136
+ ai-spec learn [lesson] Inject a lesson directly into constitution §9
137
+
138
+ # DSL-derived artifacts
139
+ ai-spec export DSL → OpenAPI 3.1.0 YAML/JSON
140
+ ai-spec types DSL → TypeScript types (models + endpoint types + API_ENDPOINTS)
141
+ ai-spec mock DSL → Mock server + MSW handlers + proxy config
142
+
143
+ # Configuration (run without flags for interactive model/provider picker)
144
+ ai-spec config Interactive provider/model setup (merged from `model`)
145
+ ai-spec config --show Print current configuration
146
+ ai-spec config --list List all available providers and models
147
+
148
+ # Observability
149
+ ai-spec logs [runId] View run history or stage breakdown
150
+ ai-spec trend Harness score trend across runs
151
+ ai-spec dashboard Generate static HTML harness dashboard
152
+ ai-spec restore <runId> Rollback all files modified by a specific run
153
+ ai-spec vcr list List VCR recordings
133
154
  ```
134
155
 
135
156
  ### Architecture
@@ -204,10 +225,11 @@ MIT
204
225
  **ai-spec** 是一个 AI 驱动的功能开发编排工具 SDK & CLI —— 从一句话需求到可运行代码的完整流水线,支持单 Repo 及多 Repo 跨端联动。
205
226
 
206
227
  ```
207
- 需求描述 项目宪法 项目感知Spec+Tasks → 交互式润色(Diff预览)
228
+ init: 注册仓库项目级宪法全局宪法汇总
229
+ create: 需求描述 → 选择仓库 → 项目感知 → Spec+Tasks → 交互式润色(Diff预览)
208
230
  → Spec质量预评估 → Approval Gate → DSL提取+校验 → DSL Gap Feedback
209
231
  → Git 隔离 → 代码生成(同层并行) → TDD测试/测试骨架 → 错误反馈自动修复
210
- → 3-pass 代码审查 → Review→DSL Loop → Harness Self-Eval → 经验积累(宪法§9)
232
+ → 3-pass 代码审查 → Review→DSL Loop → Harness Self-Eval
211
233
  ```
212
234
 
213
235
  **多 Repo 模式(工作区):**
@@ -242,16 +264,20 @@ npm run build
242
264
  # 设置 API Key(以 Gemini 为例)
243
265
  export GEMINI_API_KEY=your_key_here
244
266
 
245
- # 首次使用:生成项目宪法(可选,create 会自动触发)
267
+ # 初始化:注册仓库 + 生成宪法链路
246
268
  ai-spec init
247
269
 
248
- # 开始开发
270
+ # 开始开发(选择已注册仓库 → 跑 pipeline)
249
271
  ai-spec create "给用户模块增加登录功能"
250
272
  ```
251
273
 
252
274
  ### 流水线演示
253
275
 
254
276
  ```
277
+ [Repo] 选择本次开发的仓库:
278
+ ● my-vue-app (vue / frontend)
279
+ ○ my-node-api (node-express / backend)
280
+ ✔ 已选择 1 个仓库
255
281
  [1/6] 加载项目上下文...
256
282
  Constitution : ✔ found
257
283
  [2/6] 使用 gemini/gemini-2.5-pro 生成 Spec...
@@ -307,19 +333,35 @@ ai-spec create "给用户模块增加登录功能"
307
333
  ### 命令总览
308
334
 
309
335
  ```
310
- ai-spec init 分析代码库,生成项目宪法(.ai-spec-constitution.md)
311
- ai-spec create [idea] 完整流水线:宪法 → spec → DSL → 代码生成 → 审查 → 自评
312
- ai-spec update [change] 增量更新:修改 Spec → 重提取 DSL → 重新生成受影响文件
313
- ai-spec learn [lesson] 零摩擦知识注入,直接写入宪法 §9
314
- ai-spec review [file] 3-pass AI 代码审查(架构 + 实现 + 影响面)
315
- ai-spec export DSL OpenAPI 3.1.0 YAML/JSON
316
- ai-spec types DSL TypeScript 类型文件
317
- ai-spec mock DSL Mock 服务器 + MSW Handlers + 代理配置
318
- ai-spec dashboard 生成静态 HTML Harness Dashboard
319
- ai-spec restore <runId> 回滚指定 run 修改的所有文件
320
- ai-spec model 交互式切换 AI provider/model
321
- ai-spec config 查看/修改项目配置
322
- ai-spec workspace init 初始化多 Repo 工作区
336
+ # 核心开发流程
337
+ ai-spec init 注册仓库,生成项目宪法 + 全局宪法(自动扫描项目)
338
+ ai-spec init --add-repo 快速添加单个仓库
339
+ ai-spec init --status 查看已注册仓库和宪法状态
340
+ ai-spec create [idea] 完整流水线:spec DSL 代码生成 审查
341
+ ai-spec create [idea] --openapi DSL 提取后自动生成 OpenAPI 3.1.0 YAML
342
+ ai-spec create [idea] --types DSL 提取后自动生成 TypeScript 类型文件
343
+ ai-spec update [change] 增量更新:修改 Spec 重提取 DSL 重新生成受影响文件
344
+ ai-spec review [file] 3-pass AI 代码审查(架构 + 实现 + 影响面)
345
+
346
+ # 知识 & 宪法
347
+ ai-spec learn [lesson] 零摩擦知识注入,直接写入宪法 §9
348
+
349
+ # DSL 衍生产物
350
+ ai-spec export DSL → OpenAPI 3.1.0 YAML/JSON
351
+ ai-spec types DSL → TypeScript 类型文件
352
+ ai-spec mock DSL → Mock 服务器 + MSW Handlers + 代理配置
353
+
354
+ # 配置(无参数运行进入交互式 provider/model 选择)
355
+ ai-spec config 交互式 provider/model 配置(已合并原 model 命令)
356
+ ai-spec config --show 查看当前配置
357
+ ai-spec config --list 列出所有可用 provider 和模型
358
+
359
+ # 可观测性
360
+ ai-spec logs [runId] 查看 run 历史 / 指定 run 的 stage 详情
361
+ ai-spec trend Harness score 趋势分析
362
+ ai-spec dashboard 生成静态 HTML Harness Dashboard
363
+ ai-spec restore <runId> 回滚指定 run 修改的所有文件
364
+ ai-spec vcr list 列出 VCR 录制
323
365
  ```
324
366
 
325
367
  <details>
@@ -387,13 +429,17 @@ ai-spec export --server <url> # 指定服务器 URL
387
429
  ### 工作流详解
388
430
 
389
431
  <details>
390
- <summary>Step 1项目宪法 + Context</summary>
432
+ <summary>Step 0初始化(ai-spec init)</summary>
433
+
434
+ `ai-spec init` 完成工作区准备:
391
435
 
392
- **项目宪法**(`.ai-spec-constitution.md`)包含 §1–§9 9 个章节,涵盖架构规则、命名规范、API 规范、数据层、错误处理、禁区、测试规范、共享配置清单、累积经验。
436
+ 1. **注册仓库**:输入仓库绝对路径 自动检测类型/角色(Vue/React/Node/Java/...)
437
+ 2. **生成项目宪法**:对每个仓库自动扫描并生成 `.ai-spec-constitution.md`(§1–§9)
438
+ 3. **生成全局宪法**:汇总所有仓库的项目宪法要点 → 生成 `.ai-spec-global-constitution.md`
393
439
 
394
- 自动扫描的上下文包括:`package.json` / `composer.json` / `pom.xml` 依赖列表、Prisma schema、路由文件、错误处理模式、i18n/constants/enums 等共享配置。
440
+ 宪法链路:**仓库注册 项目宪法 全局宪法汇总**,运行时自动 merge(全局为基线,项目级覆盖)。
395
441
 
396
- **Constitution Rebase**:§9 积累超过 8 条时,运行 `ai-spec init --consolidate` 将经验提炼归并到 §1–§8。
442
+ `--add-repo` 支持快速追加仓库,`--consolidate` §9 积累经验提炼归并到 §1–§8。
397
443
  </details>
398
444
 
399
445
  <details>
@@ -2,14 +2,21 @@ import { Command } from "commander";
2
2
  import * as path from "path";
3
3
  import * as fs from "fs-extra";
4
4
  import chalk from "chalk";
5
+ import { input, select, confirm } from "@inquirer/prompts";
5
6
  import { CodeGenMode } from "../../core/code-generator";
6
7
  import { clearAllKeys, clearKey, getSavedKey, KEY_STORE_FILE } from "../../core/key-store";
7
8
  import { AiSpecConfig, CONFIG_FILE, loadConfig } from "../utils";
9
+ import {
10
+ DEFAULT_MODELS,
11
+ ENV_KEY_MAP,
12
+ PROVIDER_CATALOG,
13
+ } from "../../core/spec-generator";
14
+ import { AiSpecGlobalConfig, GLOBAL_CONFIG_FILE, loadGlobalConfig, saveGlobalConfig } from "../utils";
8
15
 
9
16
  export function registerConfig(program: Command): void {
10
17
  program
11
18
  .command("config")
12
- .description(`Set default configuration for this project (saved to ${CONFIG_FILE})`)
19
+ .description(`Configure ai-spec: run without flags for interactive model/provider setup`)
13
20
  .option("--provider <name>", "Default AI provider for spec generation")
14
21
  .option("--model <name>", "Default model for spec generation")
15
22
  .option("--codegen <mode>", "Default code generation mode (claude-code|api|plan)")
@@ -18,7 +25,9 @@ export function registerConfig(program: Command): void {
18
25
  .option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)")
19
26
  .option("--min-harness-score <score>", "Minimum harness score (1-10) for pipeline success (0 = disabled)")
20
27
  .option("--max-error-cycles <n>", "Maximum error-feedback fix cycles (1-10, default: 2)")
28
+ .option("--max-codegen-concurrency <n>", "Max concurrent tasks per batch in api codegen mode (1-10, default: 3)")
21
29
  .option("--show", "Print current configuration")
30
+ .option("--list", "List all available providers and models")
22
31
  .option("--reset", "Reset configuration to empty")
23
32
  .option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json")
24
33
  .option("--clear-key <provider>", "Delete saved API key for a specific provider")
@@ -27,6 +36,117 @@ export function registerConfig(program: Command): void {
27
36
  const currentDir = process.cwd();
28
37
  const configPath = path.join(currentDir, CONFIG_FILE);
29
38
 
39
+ // ── --list: show all providers ──────────────────────────────────────────
40
+ if (opts.list) {
41
+ console.log(chalk.bold("\nAvailable providers & models:\n"));
42
+ for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
43
+ console.log(` ${chalk.bold.cyan(key.padEnd(10))} ${chalk.white(meta.displayName)}`);
44
+ console.log(chalk.gray(` ${meta.description}`));
45
+ console.log(chalk.gray(` env: ${meta.envKey} | models: ${meta.models.join(", ")}`));
46
+ console.log();
47
+ }
48
+ return;
49
+ }
50
+
51
+ // ── No flags → interactive model/provider picker ────────────────────────
52
+ const anyFlagSet = opts.provider || opts.model || opts.codegen || opts.codegenProvider ||
53
+ opts.codegenModel || opts.minSpecScore !== undefined || opts.minHarnessScore !== undefined ||
54
+ opts.maxErrorCycles !== undefined || opts.maxCodegenConcurrency !== undefined ||
55
+ opts.show || opts.reset || opts.clearKeys || opts.clearKey || opts.listKeys;
56
+
57
+ if (!anyFlagSet) {
58
+ const existing: AiSpecGlobalConfig = await loadGlobalConfig();
59
+
60
+ console.log(chalk.blue("\n─── ai-spec config ─────────────────────────────"));
61
+ console.log(chalk.gray(` Global config: ${GLOBAL_CONFIG_FILE}`));
62
+ if (Object.keys(existing).length > 0) {
63
+ console.log(chalk.gray(
64
+ ` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` +
65
+ (existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
66
+ ));
67
+ }
68
+ console.log();
69
+
70
+ const target = await select({
71
+ message: "Configure model for:",
72
+ choices: [
73
+ { name: "Spec generation (spec writing & refinement)", value: "spec" },
74
+ { name: "Code generation (used when --codegen api is active)", value: "codegen" },
75
+ { name: "Both (same provider/model for all tasks)", value: "both" },
76
+ ],
77
+ });
78
+
79
+ async function pickProviderAndModel(label: string): Promise<{ provider: string; model: string }> {
80
+ const providerKey = await select({
81
+ message: `${label} — select provider:`,
82
+ choices: Object.entries(PROVIDER_CATALOG).map(([key, meta]) => ({
83
+ name: `${meta.displayName.padEnd(22)} ${chalk.gray(meta.description)}`,
84
+ value: key,
85
+ short: meta.displayName,
86
+ })),
87
+ });
88
+ const meta = PROVIDER_CATALOG[providerKey];
89
+ const modelChoices = [
90
+ ...meta.models.map((m) => ({ name: m, value: m })),
91
+ { name: chalk.italic("✎ Enter custom model name..."), value: "__custom__" },
92
+ ];
93
+ let chosenModel = await select({
94
+ message: `${label} — select model (${meta.displayName}):`,
95
+ choices: modelChoices,
96
+ });
97
+ if (chosenModel === "__custom__") {
98
+ chosenModel = await input({
99
+ message: "Enter model name:",
100
+ validate: (v) => v.trim().length > 0 || "Model name cannot be empty",
101
+ });
102
+ }
103
+ return { provider: providerKey, model: chosenModel };
104
+ }
105
+
106
+ const updated: AiSpecGlobalConfig = { ...existing };
107
+
108
+ if (target === "spec" || target === "both") {
109
+ const { provider, model } = await pickProviderAndModel("Spec");
110
+ updated.provider = provider;
111
+ updated.model = model;
112
+ }
113
+ if (target === "codegen" || target === "both") {
114
+ if (target === "both") {
115
+ updated.codegenProvider = updated.provider;
116
+ updated.codegenModel = updated.model;
117
+ } else {
118
+ const { provider, model } = await pickProviderAndModel("Codegen");
119
+ updated.codegenProvider = provider;
120
+ updated.codegenModel = model;
121
+ }
122
+ const effectiveCodegenProvider = updated.codegenProvider ?? updated.provider ?? "gemini";
123
+ if (effectiveCodegenProvider !== "claude" && updated.codegen === "claude-code") {
124
+ updated.codegen = "api";
125
+ console.log(chalk.yellow(`\n ⚠ provider "${effectiveCodegenProvider}" does not support "claude-code" mode.`));
126
+ console.log(chalk.gray(` codegen mode auto-set to "api".`));
127
+ }
128
+ }
129
+
130
+ console.log(chalk.blue("\n Preview:"));
131
+ console.log(chalk.gray(` spec → ${updated.provider}/${updated.model}`));
132
+ if (updated.codegenProvider) {
133
+ console.log(chalk.gray(` codegen → ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "api"})`));
134
+ }
135
+
136
+ const ok = await confirm({ message: `Save to ${GLOBAL_CONFIG_FILE}?`, default: true });
137
+ if (!ok) { console.log(chalk.gray(" Cancelled.")); return; }
138
+
139
+ await saveGlobalConfig(updated);
140
+ console.log(chalk.green(`\n ✔ Saved to ${GLOBAL_CONFIG_FILE}`));
141
+
142
+ const providerToCheck = updated.provider ?? "gemini";
143
+ const envKey = ENV_KEY_MAP[providerToCheck];
144
+ if (envKey && !process.env[envKey]) {
145
+ console.log(chalk.yellow(` ⚠ Remember to set ${envKey} in your environment or .env file.`));
146
+ }
147
+ return;
148
+ }
149
+
30
150
  if (opts.clearKeys) {
31
151
  await clearAllKeys();
32
152
  console.log(chalk.green(`✔ All saved API keys cleared.`));
@@ -95,6 +215,14 @@ export function registerConfig(program: Command): void {
95
215
  }
96
216
  updated.minHarnessScore = score;
97
217
  }
218
+ if (opts.maxCodegenConcurrency !== undefined) {
219
+ const n = parseInt(opts.maxCodegenConcurrency, 10);
220
+ if (isNaN(n) || n < 1 || n > 10) {
221
+ console.error(chalk.red(" --max-codegen-concurrency must be a number between 1 and 10"));
222
+ process.exit(1);
223
+ }
224
+ updated.maxCodegenConcurrency = n;
225
+ }
98
226
  if (opts.maxErrorCycles !== undefined) {
99
227
  const cycles = parseInt(opts.maxErrorCycles, 10);
100
228
  if (isNaN(cycles) || cycles < 1 || cycles > 10) {
@@ -1,11 +1,168 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs-extra";
1
3
  import { Command } from "commander";
2
4
  import chalk from "chalk";
3
- import { input } from "@inquirer/prompts";
5
+ import { input, select, checkbox } from "@inquirer/prompts";
6
+ import { RepoRole } from "../../core/workspace-loader";
4
7
  import { SUPPORTED_PROVIDERS } from "../../core/spec-generator";
5
- import { WorkspaceLoader } from "../../core/workspace-loader";
8
+ import {
9
+ WorkspaceLoader,
10
+ WorkspaceConfig,
11
+ detectRepoType,
12
+ } from "../../core/workspace-loader";
6
13
  import { loadConfig } from "../utils";
7
14
  import { runMultiRepoPipeline, handleAutoServe } from "../pipeline/multi-repo";
8
15
  import { runSingleRepoPipeline } from "../pipeline/single-repo";
16
+ import {
17
+ RegisteredRepo,
18
+ getRegisteredRepos,
19
+ registerRepo,
20
+ REPO_STORE_FILE,
21
+ } from "../../core/repo-store";
22
+ import { ConstitutionGenerator, CONSTITUTION_FILE } from "../../core/constitution-generator";
23
+ import { createProvider, DEFAULT_MODELS, AIProvider } from "../../core/spec-generator";
24
+ import { resolveApiKey } from "../utils";
25
+
26
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Prompt user to select a repo role.
30
+ */
31
+ async function promptRepoRole(): Promise<RepoRole> {
32
+ return select<RepoRole>({
33
+ message: "What type of repo is this?",
34
+ choices: [
35
+ { name: "Frontend", value: "frontend" },
36
+ { name: "Backend", value: "backend" },
37
+ { name: "Mobile", value: "mobile" },
38
+ { name: "Shared / Other", value: "shared" },
39
+ ],
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Prompt user for a repo path and quick-register it.
45
+ */
46
+ async function quickRegisterRepo(provider: AIProvider): Promise<RegisteredRepo> {
47
+ const roleOverride = await promptRepoRole();
48
+
49
+ const roleLabels: Record<RepoRole, string> = {
50
+ frontend: "frontend",
51
+ backend: "backend",
52
+ mobile: "mobile",
53
+ shared: "shared",
54
+ };
55
+
56
+ const raw = await input({
57
+ message: `Enter your ${roleLabels[roleOverride]} repo path (absolute path):`,
58
+ validate: (v) => {
59
+ const trimmed = v.trim();
60
+ if (trimmed.length === 0) return "Path cannot be empty";
61
+ if (!path.isAbsolute(trimmed)) return "Please provide an absolute path";
62
+ return true;
63
+ },
64
+ });
65
+
66
+ // Strip shell escape backslashes (e.g. "文稿\ -\ hongzhong" → "文稿 - hongzhong")
67
+ const cleaned = raw.trim().replace(/\\ /g, " ");
68
+ const resolved = path.resolve(cleaned);
69
+ if (!(await fs.pathExists(resolved))) {
70
+ console.log(chalk.red(` Path does not exist: ${resolved}`));
71
+ return quickRegisterRepo(provider);
72
+ }
73
+
74
+ const { type, role: detectedRole } = await detectRepoType(resolved);
75
+ const role = roleOverride ?? detectedRole;
76
+ const repoName = path.basename(resolved);
77
+
78
+ console.log(chalk.gray(` Detected: ${repoName} → ${type} (${role})`));
79
+
80
+ // Generate project constitution if missing
81
+ const constitutionPath = path.join(resolved, CONSTITUTION_FILE);
82
+ let hasConstitution = await fs.pathExists(constitutionPath);
83
+ if (!hasConstitution) {
84
+ console.log(chalk.blue(` Generating constitution for ${repoName}...`));
85
+ try {
86
+ const gen = new ConstitutionGenerator(provider);
87
+ const content = await gen.generate(resolved);
88
+ await gen.saveConstitution(resolved, content);
89
+ hasConstitution = true;
90
+ console.log(chalk.green(` ✔ Constitution saved`));
91
+ } catch (err) {
92
+ console.log(chalk.yellow(` ⚠ Constitution failed: ${(err as Error).message}`));
93
+ }
94
+ }
95
+
96
+ const entry: RegisteredRepo = {
97
+ name: repoName,
98
+ path: resolved,
99
+ type,
100
+ role,
101
+ hasConstitution,
102
+ registeredAt: new Date().toISOString(),
103
+ };
104
+ await registerRepo(entry);
105
+ console.log(chalk.green(` ✔ Repo registered: ${repoName}`));
106
+ return entry;
107
+ }
108
+
109
+ /**
110
+ * Let user select repo(s) from the registered list, with option to add new.
111
+ */
112
+ async function selectRepos(
113
+ registeredRepos: RegisteredRepo[],
114
+ provider: AIProvider
115
+ ): Promise<RegisteredRepo[]> {
116
+ const ADD_NEW = "__add_new__";
117
+
118
+ const choices = [
119
+ ...registeredRepos.map((r) => ({
120
+ name: `${r.name} (${r.type} / ${r.role}) → ${r.path}`,
121
+ value: r.path,
122
+ })),
123
+ { name: chalk.cyan("+ Add new repo"), value: ADD_NEW },
124
+ ];
125
+
126
+ const selected = await checkbox<string>({
127
+ message: "Select repo(s) for this feature (space to toggle, enter to confirm):",
128
+ choices,
129
+ required: true,
130
+ });
131
+
132
+ const result: RegisteredRepo[] = [];
133
+
134
+ for (const val of selected) {
135
+ if (val === ADD_NEW) {
136
+ const newRepo = await quickRegisterRepo(provider);
137
+ result.push(newRepo);
138
+ } else {
139
+ const repo = registeredRepos.find((r) => r.path === val);
140
+ if (repo) result.push(repo);
141
+ }
142
+ }
143
+
144
+ if (result.length === 0) {
145
+ console.log(chalk.yellow(" No repos selected. Please select at least one."));
146
+ return selectRepos(registeredRepos, provider);
147
+ }
148
+
149
+ return result;
150
+ }
151
+
152
+ /**
153
+ * Build WorkspaceConfig from selected repos for multi-repo pipeline.
154
+ */
155
+ function buildWorkspaceConfig(repos: RegisteredRepo[]): WorkspaceConfig {
156
+ return {
157
+ name: "ai-spec-workspace",
158
+ repos: repos.map((r) => ({
159
+ name: r.name,
160
+ path: r.path,
161
+ type: r.type,
162
+ role: r.role,
163
+ })),
164
+ };
165
+ }
9
166
 
10
167
  // ─── Command: create ──────────────────────────────────────────────────────────
11
168
 
@@ -48,6 +205,8 @@ export function registerCreate(program: Command): void {
48
205
  .option("--serve", "After workspace pipeline completes, auto-start mock server + patch frontend proxy")
49
206
  .option("--vcr-record", "Record all AI responses to .ai-spec-vcr/ for offline replay")
50
207
  .option("--vcr-replay <runId>", "Replay AI responses from a previous recording (zero API calls)")
208
+ .option("--openapi", "Auto-generate OpenAPI 3.1.0 YAML after DSL extraction")
209
+ .option("--types", "Auto-generate TypeScript types after DSL extraction")
51
210
  .action(async (idea: string | undefined, opts) => {
52
211
  const currentDir = process.cwd();
53
212
  const config = await loadConfig(currentDir);
@@ -60,22 +219,98 @@ export function registerCreate(program: Command): void {
60
219
  });
61
220
  }
62
221
 
63
- // ── Detect workspace mode ───────────────────────────────────────────────
222
+ // ── Check for existing workspace config (legacy path) ──────────────────
64
223
  const workspaceLoader = new WorkspaceLoader(currentDir);
65
- const workspaceConfig = await workspaceLoader.load();
66
-
67
- if (workspaceConfig) {
68
- console.log(chalk.cyan(`\n[Workspace] Detected workspace: ${workspaceConfig.name}`));
69
- console.log(chalk.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
70
- const pipelineResults = await runMultiRepoPipeline(idea!, workspaceConfig, opts, currentDir, config);
224
+ const existingWorkspaceConfig = await workspaceLoader.load();
71
225
 
226
+ if (existingWorkspaceConfig) {
227
+ console.log(chalk.cyan(`\n[Workspace] Detected workspace: ${existingWorkspaceConfig.name}`));
228
+ console.log(chalk.gray(` Repos: ${existingWorkspaceConfig.repos.map((r) => r.name).join(", ")}`));
229
+ const pipelineResults = await runMultiRepoPipeline(idea!, existingWorkspaceConfig, opts, currentDir, config);
72
230
  if (opts.serve) {
73
231
  await handleAutoServe(pipelineResults);
74
232
  }
75
233
  return;
76
234
  }
77
235
 
78
- // ── Single-repo pipeline ────────────────────────────────────────────────
79
- await runSingleRepoPipeline(idea!, opts, currentDir, config);
236
+ // ── Resolve provider (for quick-register if needed) ────────────────────
237
+ const providerName = (opts.provider as string) || config.provider || "gemini";
238
+ const modelName = (opts.model as string) || config.model || DEFAULT_MODELS[providerName];
239
+ const apiKey = await resolveApiKey(providerName, opts.key as string | undefined);
240
+ const specProvider = createProvider(providerName, apiKey, modelName);
241
+
242
+ // Persist the resolved key on opts so downstream pipelines (single/multi-repo)
243
+ // short-circuit `resolveApiKey` via the cliKey path and skip a duplicate prompt.
244
+ // Without this, the user gets prompted twice for the same provider when
245
+ // create.ts and the pipeline both call resolveApiKey independently.
246
+ if (!opts.key) opts.key = apiKey;
247
+ // If user did NOT specify a separate codegen provider/key, the pipeline will
248
+ // fall back to spec provider — pre-fill codegenKey too so that path is silent.
249
+ const codegenProviderName = (opts.codegenProvider as string) || config.codegenProvider || providerName;
250
+ if (codegenProviderName === providerName && !opts.codegenKey) {
251
+ opts.codegenKey = apiKey;
252
+ }
253
+
254
+ // ── Select repo(s) from registered list ────────────────────────────────
255
+ const registeredRepos = await getRegisteredRepos();
256
+
257
+ if (registeredRepos.length === 0) {
258
+ console.log(chalk.yellow("\n No repos registered. Please register repos first."));
259
+ console.log(chalk.gray(" Run: ai-spec init"));
260
+ console.log(chalk.gray(" Or add a repo now:\n"));
261
+
262
+ const addNow = await select({
263
+ message: "Add a repo now?",
264
+ choices: [
265
+ { name: "Yes — register a repo and continue", value: "yes" as const },
266
+ { name: "No — exit", value: "no" as const },
267
+ ],
268
+ });
269
+
270
+ if (addNow === "no") {
271
+ process.exit(0);
272
+ }
273
+
274
+ const newRepo = await quickRegisterRepo(specProvider);
275
+ registeredRepos.push(newRepo);
276
+ }
277
+
278
+ // Filter out repos whose paths no longer exist
279
+ const validRepos = [];
280
+ for (const r of registeredRepos) {
281
+ if (await fs.pathExists(r.path)) {
282
+ validRepos.push(r);
283
+ } else {
284
+ console.log(chalk.yellow(` ⚠ Skipping ${r.name}: path not found (${r.path})`));
285
+ }
286
+ }
287
+
288
+ if (validRepos.length === 0) {
289
+ console.log(chalk.red(" No valid repos available. Run: ai-spec init"));
290
+ process.exit(1);
291
+ }
292
+
293
+ const selectedRepos = await selectRepos(validRepos, specProvider);
294
+
295
+ // ── Route to appropriate pipeline ──────────────────────────────────────
296
+ if (selectedRepos.length === 1) {
297
+ // Single-repo pipeline
298
+ const repo = selectedRepos[0];
299
+ console.log(chalk.cyan(`\n[Repo] ${repo.name} (${repo.type}/${repo.role}) → ${repo.path}`));
300
+ await runSingleRepoPipeline(idea!, opts, repo.path, config);
301
+ } else {
302
+ // Multi-repo pipeline — sort backend before frontend
303
+ const workspaceConfig = buildWorkspaceConfig(selectedRepos);
304
+
305
+ console.log(chalk.cyan(`\n[Workspace] ${selectedRepos.length} repo(s) selected:`));
306
+ for (const repo of selectedRepos) {
307
+ console.log(chalk.gray(` ${repo.name} (${repo.type}/${repo.role}) → ${repo.path}`));
308
+ }
309
+
310
+ const pipelineResults = await runMultiRepoPipeline(idea!, workspaceConfig, opts, currentDir, config);
311
+ if (opts.serve) {
312
+ await handleAutoServe(pipelineResults);
313
+ }
314
+ }
80
315
  });
81
316
  }