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.
- package/README.md +86 -40
- package/cli/commands/config.ts +129 -1
- package/cli/commands/create.ts +246 -11
- package/cli/commands/fix-history.ts +176 -0
- package/cli/commands/init.ts +344 -106
- package/cli/index.ts +3 -7
- package/cli/pipeline/helpers.ts +6 -0
- package/cli/pipeline/multi-repo.ts +291 -26
- package/cli/pipeline/single-repo.ts +103 -2
- package/cli/utils.ts +95 -4
- package/core/code-generator.ts +63 -14
- package/core/config-defaults.ts +44 -0
- package/core/constitution-generator.ts +2 -1
- package/core/cross-stack-verifier.ts +395 -0
- package/core/dsl-extractor.ts +2 -1
- package/core/error-feedback.ts +3 -2
- package/core/fix-history.ts +333 -0
- package/core/import-fixer.ts +827 -0
- package/core/import-verifier.ts +569 -0
- package/core/knowledge-memory.ts +55 -6
- package/core/openapi-exporter.ts +3 -2
- package/core/repo-store.ts +95 -0
- package/core/reviewer.ts +14 -13
- package/core/run-logger.ts +3 -4
- package/core/run-snapshot.ts +2 -3
- package/core/run-trend.ts +3 -4
- package/core/self-evaluator.ts +44 -7
- package/core/spec-generator.ts +30 -45
- package/core/token-budget.ts +3 -8
- package/core/types-generator.ts +2 -2
- package/core/vcr.ts +3 -1
- package/dist/cli/index.js +3889 -1937
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +3888 -1936
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +292 -181
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +292 -181
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/tests/cross-stack-verifier.test.ts +301 -0
- package/tests/fix-history.test.ts +335 -0
- package/tests/import-fixer.test.ts +944 -0
- package/tests/import-verifier.test.ts +420 -0
- package/tests/knowledge-memory.test.ts +40 -0
- package/tests/self-evaluator.test.ts +97 -0
- package/cli/commands/model.ts +0 -156
- package/cli/commands/scan.ts +0 -99
- package/cli/commands/workspace.ts +0 -219
- package/demo-backend/.ai-spec-constitution.md +0 -65
- package/demo-backend/package.json +0 -21
- package/demo-backend/prisma/schema.prisma +0 -22
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
- package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
- package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
- package/demo-backend/src/index.ts +0 -17
- package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
- package/demo-backend/src/routes/bookmark.routes.ts +0 -11
- package/demo-backend/src/routes/index.ts +0 -8
- package/demo-backend/src/services/bookmark.service.test.ts +0 -433
- package/demo-backend/src/services/bookmark.service.ts +0 -261
- package/demo-backend/tsconfig.json +0 -12
- package/demo-frontend/.ai-spec-constitution.md +0 -95
- package/demo-frontend/package.json +0 -23
- package/demo-frontend/src/App.tsx +0 -12
- package/demo-frontend/src/main.tsx +0 -9
- 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.
|
|
16
|
-
<img src="https://img.shields.io/badge/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
|
-
|
|
34
|
+
init: Register Repos → Project Constitutions → Global 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
|
|
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
|
|
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
|
-
|
|
121
|
-
ai-spec
|
|
122
|
-
ai-spec
|
|
123
|
-
ai-spec
|
|
124
|
-
ai-spec
|
|
125
|
-
ai-spec
|
|
126
|
-
ai-spec
|
|
127
|
-
ai-spec
|
|
128
|
-
ai-spec
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
ai-spec
|
|
132
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
311
|
-
ai-spec
|
|
312
|
-
ai-spec
|
|
313
|
-
ai-spec
|
|
314
|
-
ai-spec
|
|
315
|
-
ai-spec
|
|
316
|
-
ai-spec types
|
|
317
|
-
ai-spec
|
|
318
|
-
ai-spec
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
ai-spec
|
|
322
|
-
|
|
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
|
|
432
|
+
<summary>Step 0 — 初始化(ai-spec init)</summary>
|
|
433
|
+
|
|
434
|
+
`ai-spec init` 完成工作区准备:
|
|
391
435
|
|
|
392
|
-
|
|
436
|
+
1. **注册仓库**:输入仓库绝对路径 → 自动检测类型/角色(Vue/React/Node/Java/...)
|
|
437
|
+
2. **生成项目宪法**:对每个仓库自动扫描并生成 `.ai-spec-constitution.md`(§1–§9)
|
|
438
|
+
3. **生成全局宪法**:汇总所有仓库的项目宪法要点 → 生成 `.ai-spec-global-constitution.md`
|
|
393
439
|
|
|
394
|
-
|
|
440
|
+
宪法链路:**仓库注册 → 项目宪法 → 全局宪法汇总**,运行时自动 merge(全局为基线,项目级覆盖)。
|
|
395
441
|
|
|
396
|
-
|
|
442
|
+
`--add-repo` 支持快速追加仓库,`--consolidate` 将 §9 积累经验提炼归并到 §1–§8。
|
|
397
443
|
</details>
|
|
398
444
|
|
|
399
445
|
<details>
|
package/cli/commands/config.ts
CHANGED
|
@@ -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(`
|
|
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) {
|
package/cli/commands/create.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
// ──
|
|
222
|
+
// ── Check for existing workspace config (legacy path) ──────────────────
|
|
64
223
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
65
|
-
const
|
|
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
|
-
// ──
|
|
79
|
-
|
|
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
|
}
|