ai-spec-dev 0.41.0 → 0.46.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/.ai-spec-workspace.json +17 -0
- package/.ai-spec.json +7 -0
- package/README.md +33 -17
- package/cli/commands/create.ts +232 -11
- package/cli/commands/init.ts +310 -107
- package/cli/commands/model.ts +7 -11
- package/cli/index.ts +1 -1
- package/cli/pipeline/single-repo.ts +19 -10
- package/cli/utils.ts +72 -4
- package/core/cli-ui.ts +136 -0
- package/core/code-generator.ts +4 -2
- package/core/config-defaults.ts +44 -0
- package/core/constitution-generator.ts +2 -1
- package/core/dsl-extractor.ts +2 -1
- package/core/error-feedback.ts +7 -4
- package/core/openapi-exporter.ts +3 -2
- package/core/provider-utils.ts +8 -7
- 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/spec-generator.ts +27 -42
- package/core/token-budget.ts +3 -8
- package/core/vcr.ts +3 -1
- package/dist/cli/index.js +1042 -533
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1042 -533
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +123 -61
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +123 -61
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/RELEASE_LOG.md +0 -2962
- package/purpose.md +0 -1434
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bookmark-demo",
|
|
3
|
+
"repos": [
|
|
4
|
+
{
|
|
5
|
+
"name": "demo-backend",
|
|
6
|
+
"path": "demo-backend",
|
|
7
|
+
"type": "node-express",
|
|
8
|
+
"role": "backend"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "demo-frontend",
|
|
12
|
+
"path": "demo-frontend",
|
|
13
|
+
"type": "react",
|
|
14
|
+
"role": "frontend"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
package/.ai-spec.json
ADDED
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
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.
|
|
15
|
+
<img src="https://img.shields.io/badge/version-0.45.0-blue" alt="version" />
|
|
16
16
|
<img src="https://img.shields.io/badge/tests-409%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" />
|
|
@@ -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,8 +122,9 @@ ai-spec create "Add login functionality to user module"
|
|
|
117
122
|
### Commands
|
|
118
123
|
|
|
119
124
|
```
|
|
120
|
-
ai-spec init
|
|
121
|
-
ai-spec
|
|
125
|
+
ai-spec init Register repos, generate project + global constitutions
|
|
126
|
+
ai-spec init --add-repo Quick-add a single repo
|
|
127
|
+
ai-spec create [idea] Select repo(s) → full pipeline: spec → DSL → codegen → review
|
|
122
128
|
ai-spec update [change] Incremental update: modify spec → re-extract DSL → regenerate affected files
|
|
123
129
|
ai-spec learn [lesson] Inject knowledge directly into constitution §9
|
|
124
130
|
ai-spec review [file] 3-pass AI code review (architecture + implementation + impact)
|
|
@@ -204,10 +210,11 @@ MIT
|
|
|
204
210
|
**ai-spec** 是一个 AI 驱动的功能开发编排工具 SDK & CLI —— 从一句话需求到可运行代码的完整流水线,支持单 Repo 及多 Repo 跨端联动。
|
|
205
211
|
|
|
206
212
|
```
|
|
207
|
-
|
|
213
|
+
init: 注册仓库 → 项目级宪法 → 全局宪法汇总
|
|
214
|
+
create: 需求描述 → 选择仓库 → 项目感知 → Spec+Tasks → 交互式润色(Diff预览)
|
|
208
215
|
→ Spec质量预评估 → Approval Gate → DSL提取+校验 → DSL Gap Feedback
|
|
209
216
|
→ Git 隔离 → 代码生成(同层并行) → TDD测试/测试骨架 → 错误反馈自动修复
|
|
210
|
-
→ 3-pass 代码审查 → Review→DSL Loop → Harness Self-Eval
|
|
217
|
+
→ 3-pass 代码审查 → Review→DSL Loop → Harness Self-Eval
|
|
211
218
|
```
|
|
212
219
|
|
|
213
220
|
**多 Repo 模式(工作区):**
|
|
@@ -242,16 +249,20 @@ npm run build
|
|
|
242
249
|
# 设置 API Key(以 Gemini 为例)
|
|
243
250
|
export GEMINI_API_KEY=your_key_here
|
|
244
251
|
|
|
245
|
-
#
|
|
252
|
+
# 初始化:注册仓库 + 生成宪法链路
|
|
246
253
|
ai-spec init
|
|
247
254
|
|
|
248
|
-
#
|
|
255
|
+
# 开始开发(选择已注册仓库 → 跑 pipeline)
|
|
249
256
|
ai-spec create "给用户模块增加登录功能"
|
|
250
257
|
```
|
|
251
258
|
|
|
252
259
|
### 流水线演示
|
|
253
260
|
|
|
254
261
|
```
|
|
262
|
+
[Repo] 选择本次开发的仓库:
|
|
263
|
+
● my-vue-app (vue / frontend)
|
|
264
|
+
○ my-node-api (node-express / backend)
|
|
265
|
+
✔ 已选择 1 个仓库
|
|
255
266
|
[1/6] 加载项目上下文...
|
|
256
267
|
Constitution : ✔ found
|
|
257
268
|
[2/6] 使用 gemini/gemini-2.5-pro 生成 Spec...
|
|
@@ -307,8 +318,9 @@ ai-spec create "给用户模块增加登录功能"
|
|
|
307
318
|
### 命令总览
|
|
308
319
|
|
|
309
320
|
```
|
|
310
|
-
ai-spec init
|
|
311
|
-
ai-spec
|
|
321
|
+
ai-spec init 注册仓库,生成项目宪法 + 全局宪法
|
|
322
|
+
ai-spec init --add-repo 快速添加单个仓库
|
|
323
|
+
ai-spec create [idea] 选择仓库 → 完整流水线:spec → DSL → 代码生成 → 审查
|
|
312
324
|
ai-spec update [change] 增量更新:修改 Spec → 重提取 DSL → 重新生成受影响文件
|
|
313
325
|
ai-spec learn [lesson] 零摩擦知识注入,直接写入宪法 §9
|
|
314
326
|
ai-spec review [file] 3-pass AI 代码审查(架构 + 实现 + 影响面)
|
|
@@ -387,13 +399,17 @@ ai-spec export --server <url> # 指定服务器 URL
|
|
|
387
399
|
### 工作流详解
|
|
388
400
|
|
|
389
401
|
<details>
|
|
390
|
-
<summary>Step
|
|
402
|
+
<summary>Step 0 — 初始化(ai-spec init)</summary>
|
|
391
403
|
|
|
392
|
-
|
|
404
|
+
`ai-spec init` 完成工作区准备:
|
|
393
405
|
|
|
394
|
-
|
|
406
|
+
1. **注册仓库**:输入仓库绝对路径 → 自动检测类型/角色(Vue/React/Node/Java/...)
|
|
407
|
+
2. **生成项目宪法**:对每个仓库自动扫描并生成 `.ai-spec-constitution.md`(§1–§9)
|
|
408
|
+
3. **生成全局宪法**:汇总所有仓库的项目宪法要点 → 生成 `.ai-spec-global-constitution.md`
|
|
395
409
|
|
|
396
|
-
|
|
410
|
+
宪法链路:**仓库注册 → 项目宪法 → 全局宪法汇总**,运行时自动 merge(全局为基线,项目级覆盖)。
|
|
411
|
+
|
|
412
|
+
`--add-repo` 支持快速追加仓库,`--consolidate` 将 §9 积累经验提炼归并到 §1–§8。
|
|
397
413
|
</details>
|
|
398
414
|
|
|
399
415
|
<details>
|
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
|
|
|
@@ -60,22 +217,86 @@ export function registerCreate(program: Command): void {
|
|
|
60
217
|
});
|
|
61
218
|
}
|
|
62
219
|
|
|
63
|
-
// ──
|
|
220
|
+
// ── Check for existing workspace config (legacy path) ──────────────────
|
|
64
221
|
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);
|
|
222
|
+
const existingWorkspaceConfig = await workspaceLoader.load();
|
|
71
223
|
|
|
224
|
+
if (existingWorkspaceConfig) {
|
|
225
|
+
console.log(chalk.cyan(`\n[Workspace] Detected workspace: ${existingWorkspaceConfig.name}`));
|
|
226
|
+
console.log(chalk.gray(` Repos: ${existingWorkspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
227
|
+
const pipelineResults = await runMultiRepoPipeline(idea!, existingWorkspaceConfig, opts, currentDir, config);
|
|
72
228
|
if (opts.serve) {
|
|
73
229
|
await handleAutoServe(pipelineResults);
|
|
74
230
|
}
|
|
75
231
|
return;
|
|
76
232
|
}
|
|
77
233
|
|
|
78
|
-
// ──
|
|
79
|
-
|
|
234
|
+
// ── Resolve provider (for quick-register if needed) ────────────────────
|
|
235
|
+
const providerName = (opts.provider as string) || config.provider || "gemini";
|
|
236
|
+
const modelName = (opts.model as string) || config.model || DEFAULT_MODELS[providerName];
|
|
237
|
+
const apiKey = await resolveApiKey(providerName, opts.key as string | undefined);
|
|
238
|
+
const specProvider = createProvider(providerName, apiKey, modelName);
|
|
239
|
+
|
|
240
|
+
// ── Select repo(s) from registered list ────────────────────────────────
|
|
241
|
+
const registeredRepos = await getRegisteredRepos();
|
|
242
|
+
|
|
243
|
+
if (registeredRepos.length === 0) {
|
|
244
|
+
console.log(chalk.yellow("\n No repos registered. Please register repos first."));
|
|
245
|
+
console.log(chalk.gray(" Run: ai-spec init"));
|
|
246
|
+
console.log(chalk.gray(" Or add a repo now:\n"));
|
|
247
|
+
|
|
248
|
+
const addNow = await select({
|
|
249
|
+
message: "Add a repo now?",
|
|
250
|
+
choices: [
|
|
251
|
+
{ name: "Yes — register a repo and continue", value: "yes" as const },
|
|
252
|
+
{ name: "No — exit", value: "no" as const },
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (addNow === "no") {
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const newRepo = await quickRegisterRepo(specProvider);
|
|
261
|
+
registeredRepos.push(newRepo);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Filter out repos whose paths no longer exist
|
|
265
|
+
const validRepos = [];
|
|
266
|
+
for (const r of registeredRepos) {
|
|
267
|
+
if (await fs.pathExists(r.path)) {
|
|
268
|
+
validRepos.push(r);
|
|
269
|
+
} else {
|
|
270
|
+
console.log(chalk.yellow(` ⚠ Skipping ${r.name}: path not found (${r.path})`));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (validRepos.length === 0) {
|
|
275
|
+
console.log(chalk.red(" No valid repos available. Run: ai-spec init"));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const selectedRepos = await selectRepos(validRepos, specProvider);
|
|
280
|
+
|
|
281
|
+
// ── Route to appropriate pipeline ──────────────────────────────────────
|
|
282
|
+
if (selectedRepos.length === 1) {
|
|
283
|
+
// Single-repo pipeline
|
|
284
|
+
const repo = selectedRepos[0];
|
|
285
|
+
console.log(chalk.cyan(`\n[Repo] ${repo.name} (${repo.type}/${repo.role}) → ${repo.path}`));
|
|
286
|
+
await runSingleRepoPipeline(idea!, opts, repo.path, config);
|
|
287
|
+
} else {
|
|
288
|
+
// Multi-repo pipeline — sort backend before frontend
|
|
289
|
+
const workspaceConfig = buildWorkspaceConfig(selectedRepos);
|
|
290
|
+
|
|
291
|
+
console.log(chalk.cyan(`\n[Workspace] ${selectedRepos.length} repo(s) selected:`));
|
|
292
|
+
for (const repo of selectedRepos) {
|
|
293
|
+
console.log(chalk.gray(` ${repo.name} (${repo.type}/${repo.role}) → ${repo.path}`));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const pipelineResults = await runMultiRepoPipeline(idea!, workspaceConfig, opts, currentDir, config);
|
|
297
|
+
if (opts.serve) {
|
|
298
|
+
await handleAutoServe(pipelineResults);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
80
301
|
});
|
|
81
302
|
}
|