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.
@@ -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
@@ -0,0 +1,7 @@
1
+ {
2
+ "provider": "glm",
3
+ "model": "glm-4.5-air",
4
+ "codegenMode": "api",
5
+ "codegenProvider": "glm",
6
+ "codegenModel": "glm-4.5-air"
7
+ }
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.37.0-blue" alt="version" />
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
- 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,8 +122,9 @@ 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
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
- 需求描述 项目宪法 项目感知Spec+Tasks → 交互式润色(Diff预览)
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 → 经验积累(宪法§9)
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
- # 首次使用:生成项目宪法(可选,create 会自动触发)
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 分析代码库,生成项目宪法(.ai-spec-constitution.md)
311
- ai-spec create [idea] 完整流水线:宪法 → spec → DSL → 代码生成 → 审查 → 自评
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 1项目宪法 + Context</summary>
402
+ <summary>Step 0初始化(ai-spec init)</summary>
391
403
 
392
- **项目宪法**(`.ai-spec-constitution.md`)包含 §1–§9 共 9 个章节,涵盖架构规则、命名规范、API 规范、数据层、错误处理、禁区、测试规范、共享配置清单、累积经验。
404
+ `ai-spec init` 完成工作区准备:
393
405
 
394
- 自动扫描的上下文包括:`package.json` / `composer.json` / `pom.xml` 依赖列表、Prisma schema、路由文件、错误处理模式、i18n/constants/enums 等共享配置。
406
+ 1. **注册仓库**:输入仓库绝对路径 自动检测类型/角色(Vue/React/Node/Java/...)
407
+ 2. **生成项目宪法**:对每个仓库自动扫描并生成 `.ai-spec-constitution.md`(§1–§9)
408
+ 3. **生成全局宪法**:汇总所有仓库的项目宪法要点 → 生成 `.ai-spec-global-constitution.md`
395
409
 
396
- **Constitution Rebase**:§9 积累超过 8 条时,运行 `ai-spec init --consolidate` 将经验提炼归并到 §1–§8。
410
+ 宪法链路:**仓库注册 项目宪法 全局宪法汇总**,运行时自动 merge(全局为基线,项目级覆盖)。
411
+
412
+ `--add-repo` 支持快速追加仓库,`--consolidate` 将 §9 积累经验提炼归并到 §1–§8。
397
413
  </details>
398
414
 
399
415
  <details>
@@ -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
 
@@ -60,22 +217,86 @@ export function registerCreate(program: Command): void {
60
217
  });
61
218
  }
62
219
 
63
- // ── Detect workspace mode ───────────────────────────────────────────────
220
+ // ── Check for existing workspace config (legacy path) ──────────────────
64
221
  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);
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
- // ── Single-repo pipeline ────────────────────────────────────────────────
79
- await runSingleRepoPipeline(idea!, opts, currentDir, config);
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
  }