@gamehoo/vibe-spec 1.0.2 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamehoo/vibe-spec",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "跨项目通用 AI 编程规范管理工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,33 +1,52 @@
1
+ ---
2
+ description: 每次 git commit 时遵循此规范
3
+ ---
4
+
1
5
  # Git 提交规范
2
6
 
3
- 基于 [Conventional Commits](https://www.conventionalcommits.org/) 规范,类型关键字使用英文,描述使用中文。
7
+ 类型关键字使用英文,描述使用中文。
4
8
 
5
9
  ## 格式
6
10
 
7
11
  ```
8
- <类型>(范围): 简短描述
12
+ <类型>[可选的范围]: 简短描述
13
+
14
+ [可选的正文]
9
15
 
10
- 可选的详细说明
16
+ [可选的脚注]
11
17
  ```
12
18
 
13
19
  - **类型**:必填,英文小写。
14
- - **范围**:可选,改动所属模块。改动范围明确时加,跨模块或杂务类可省略。
20
+ - **范围**:可选,描述改动所属模块的名词,用圆括号包围。改动范围明确时加,跨模块或杂务类可省略。
21
+ - **`!`**:可选,紧跟在类型/范围后、冒号前,表示包含破坏性变更。
15
22
  - **简短描述**:必填,中文,不超过 50 字,不加句号。
16
- - **详细说明**:可选,中文,用空行与标题分隔,解释"为什么"而非"做了什么"
23
+ - **正文**:可选,中文,用空行与简短描述分隔,解释"为什么"而非"做了什么",可包含多个段落。
24
+ - **脚注**:可选,用空行与正文分隔,每条脚注格式为 `令牌: 值` 或 `令牌 #值`,令牌中的空格用 `-` 替代。
17
25
  - **不添加** `Co-Authored-By` 等自动生成的尾部标记。
18
26
 
19
27
  ## 类型
20
28
 
21
29
  | 类型 | 用途 |
22
30
  |------|------|
23
- | `feat` | 新功能 |
24
- | `fix` | 修复 bug |
31
+ | `feat` | 新功能(对应语义化版本 MINOR) |
32
+ | `fix` | 修复 bug(对应语义化版本 PATCH) |
25
33
  | `refactor` | 重构(不改变外部行为) |
26
34
  | `docs` | 文档变更 |
27
35
  | `style` | 代码格式调整(不影响逻辑) |
28
- | `chore` | 构建、配置、依赖等杂务 |
29
- | `test` | 测试相关 |
30
36
  | `perf` | 性能优化 |
37
+ | `test` | 测试相关 |
38
+ | `build` | 构建系统或外部依赖变更 |
39
+ | `ci` | 持续集成配置变更 |
40
+ | `chore` | 其他杂务 |
41
+
42
+ ## 破坏性变更
43
+
44
+ 当提交包含破坏性变更时(对应语义化版本 MAJOR),必须通过以下方式之一标记:
45
+
46
+ 1. **在类型/范围后加 `!`**:`feat(api)!: 移除已废弃的用户列表接口`
47
+ 2. **在脚注中声明**:以 `BREAKING CHANGE: 描述` 开头(必须大写)
48
+
49
+ 两种方式可同时使用。使用 `!` 时,脚注中的 `BREAKING CHANGE` 可省略,但简短描述应说明破坏性内容。
31
50
 
32
51
  ## 示例
33
52
 
@@ -48,3 +67,11 @@ refactor(layout): 将面板尺寸逻辑抽取为 useResizable hook
48
67
 
49
68
  原先的 inline 计算散落在多个组件中,难以复用和测试。
50
69
  ```
70
+
71
+ ```
72
+ feat(api)!: 重构认证模块为 OAuth2 流程
73
+
74
+ 旧的 session-based 认证方式不再支持。
75
+
76
+ BREAKING CHANGE: /api/login 接口签名变更,客户端需迁移至 /api/oauth/authorize
77
+ ```
@@ -1,18 +1,15 @@
1
- # 文档规范
1
+ ---
2
+ description: 新建或维护 .docs/ 目录下的项目文档时参考
3
+ ---
2
4
 
3
- ## 文档分类
5
+ # 文档规范
4
6
 
5
- | 位置 | 用途 | 版本控制 |
6
- |------|------|----------|
7
- | `CLAUDE.md` | AI 协作指令:项目理念、开发规范、工作流程、工具、文档索引 | 入库 |
8
- | `CLAUDE.local.md` | 本机特有配置:个人路径、镜像地址、本地环境变量等 | 不入库 |
9
- | `.docs/` | 项目知识文档,按子目录分类(见下方) | 入库 |
7
+ ## 基本原则
10
8
 
11
- 判断原则:
12
- - 只对当前机器有意义 → `CLAUDE.local.md`
13
- - 对所有开发者有意义、指导 AI 行为 → `CLAUDE.md`
14
- - 需要详细展开的项目知识 → `.docs/`,并在 `CLAUDE.md` 文档索引中建立链接
15
9
  - 可以从代码或 git 历史推断的信息不写文档
10
+ - 新建文档前先确认是否可以合并到已有文档中
11
+ - 不符合任何分类的内容不建文档
12
+ - 单次性的调研、笔记不进 `.docs/`
16
13
 
17
14
  ## .docs/ 目录结构
18
15
 
@@ -24,9 +21,21 @@
24
21
  | `测试/` | 功能的手动测试用例,发布前全量回归 | 随功能新增或变更同步更新 | `<功能名>/<子项>.md` |
25
22
  | `产品/` | 需求和产品定义 | 每次新增,写完后基本不变 | `YYYY-MM-DD <主题>.md` |
26
23
 
27
- - 新建文档前先确认是否可以合并到已有文档中
28
- - 不符合任何分类的内容不建文档
29
- - 单次性的调研、笔记不进 `.docs/`
24
+ ## 文档索引
25
+
26
+ 项目应在 AI 指令文件(如 `CLAUDE.md`、`AGENTS.md`)中维护 `.docs/` 的文档索引,让 AI 知道有哪些文档、什么时候该看。
27
+
28
+ - 新增 `.docs/` 文档后,必须在索引中添加条目
29
+ - 每条索引包含链接和使用场景说明(告诉 AI 什么时候需要参考该文档)
30
+ - 索引格式按子目录区分:
31
+
32
+ | 子目录 | 索引格式 |
33
+ |--------|----------|
34
+ | `规范/` | `<主题>` — 使用场景 |
35
+ | `概览/` | `<主题>` — 使用场景 |
36
+ | `方案/` | `YYYY-MM-DD <主题>` — 方案概述 |
37
+ | `测试/` | `<功能名>/` — 覆盖范围 |
38
+ | `产品/` | `YYYY-MM-DD <主题>` — 产品概述 |
30
39
 
31
40
  ## 命名规范
32
41
 
@@ -66,7 +75,3 @@
66
75
 
67
76
  当信息有多个并列维度时(如"每种消息类型的来源"、"每个组件是内置还是自定义"),优先用表格,不要用段落罗列。
68
77
 
69
- ## 维护原则
70
-
71
- - `CLAUDE.md` 保持精简,详细内容下沉到 `.docs/` 子文档
72
- - 新增 `.docs/` 文档后,必须在 `CLAUDE.md` 文档索引中添加条目,包含链接和使用场景说明(告诉 AI 什么时候需要参考该文档)
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { loadProjects, saveProjects } from '../projects.js'
4
+
5
+ const START_MARKER = '<!-- vibe-spec:start -->'
6
+ const END_MARKER = '<!-- vibe-spec:end -->'
7
+
8
+ export async function clean() {
9
+ const cwd = process.cwd()
10
+ const rcPath = path.join(cwd, '.vibespecrc.json')
11
+
12
+ if (!fs.existsSync(rcPath)) {
13
+ console.log('当前项目未接入 vibe-spec,无需清理')
14
+ return
15
+ }
16
+
17
+ const rc = JSON.parse(fs.readFileSync(rcPath, 'utf-8'))
18
+ const specDir = path.join(cwd, rc.specDir)
19
+
20
+ // 删除同步过来的规范文件
21
+ const allNames = [...(rc.specs || []), ...(rc.workflows || [])]
22
+ for (const name of allNames) {
23
+ const file = path.join(specDir, `${name}.md`)
24
+ if (fs.existsSync(file)) {
25
+ fs.unlinkSync(file)
26
+ console.log(`✔ 已删除 ${rc.specDir}/${name}.md`)
27
+ }
28
+ }
29
+
30
+ // 如果规范目录为空则删除
31
+ if (fs.existsSync(specDir) && fs.readdirSync(specDir).length === 0) {
32
+ fs.rmdirSync(specDir)
33
+ console.log(`✔ 已删除空目录 ${rc.specDir}/`)
34
+ }
35
+
36
+ // 移除 CLAUDE.md 中的注入块
37
+ const claudePath = path.join(cwd, 'CLAUDE.md')
38
+ if (fs.existsSync(claudePath)) {
39
+ const content = fs.readFileSync(claudePath, 'utf-8')
40
+ const startIdx = content.indexOf(START_MARKER)
41
+ const endIdx = content.indexOf(END_MARKER)
42
+ if (startIdx !== -1 && endIdx !== -1) {
43
+ const before = content.slice(0, startIdx).replace(/\n+$/, '')
44
+ const after = content.slice(endIdx + END_MARKER.length).replace(/^\n+/, '')
45
+ const result = after ? before + '\n' + after : before
46
+ fs.writeFileSync(claudePath, result + '\n')
47
+ console.log('✔ 已移除 CLAUDE.md 中的 vibe-spec 注入块')
48
+ }
49
+ }
50
+
51
+ // 删除 .vibespecrc.json
52
+ fs.unlinkSync(rcPath)
53
+ console.log('✔ 已删除 .vibespecrc.json')
54
+
55
+ // 从全局项目列表中移除
56
+ const projects = loadProjects()
57
+ const abs = path.resolve(cwd)
58
+ const filtered = projects.filter((p) => p !== abs)
59
+ if (filtered.length < projects.length) {
60
+ saveProjects(filtered)
61
+ console.log('✔ 已从全局项目列表中移除')
62
+ }
63
+ }
@@ -46,16 +46,14 @@ export async function init() {
46
46
  }
47
47
 
48
48
  // 注入 CLAUDE.md
49
- const specNames = selectedSpecs.map((s) => s.name)
50
- const workflowNames = selectedWorkflows.map((w) => w.name)
51
- const injection = buildInjection(specNames, workflowNames, SPEC_DIR)
49
+ const injection = buildInjection(selectedSpecs, selectedWorkflows, SPEC_DIR)
52
50
  injectIntoClaude(cwd, injection)
53
51
  console.log('✔ 已注入 CLAUDE.md [vibe-spec 区域]')
54
52
 
55
53
  // 创建 .vibespecrc.json
56
54
  const rc = {
57
- specs: specNames,
58
- workflows: workflowNames,
55
+ specs: selectedSpecs.map((s) => s.name),
56
+ workflows: selectedWorkflows.map((w) => w.name),
59
57
  specDir: SPEC_DIR,
60
58
  version: getPackageVersion(),
61
59
  }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { SPECS_DIR, WORKFLOWS_DIR } from '../paths.js'
4
- import { getPackageVersion } from '../registry.js'
4
+ import { getAvailableItems, getPackageVersion } from '../registry.js'
5
5
  import { loadProjects, saveProjects } from '../projects.js'
6
6
  import { buildInjection, injectIntoClaude } from '../inject.js'
7
7
 
@@ -44,8 +44,17 @@ async function syncProject(projectDir) {
44
44
  }
45
45
  }
46
46
 
47
- // 注入 CLAUDE.md
48
- const injection = buildInjection(rc.specs, rc.workflows, rc.specDir)
47
+ // 注入 CLAUDE.md(从源文件读取 description)
48
+ const available = getAvailableItems()
49
+ const specItems = rc.specs.map((name) => {
50
+ const found = available.specs.find((s) => s.name === name)
51
+ return { name, description: found?.description || '' }
52
+ })
53
+ const workflowItems = rc.workflows.map((name) => {
54
+ const found = available.workflows.find((w) => w.name === name)
55
+ return { name, description: found?.description || '' }
56
+ })
57
+ const injection = buildInjection(specItems, workflowItems, rc.specDir)
49
58
  injectIntoClaude(projectDir, injection)
50
59
 
51
60
  // 更新版本号
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { init } from './commands/init.js'
2
2
  import { sync } from './commands/sync.js'
3
3
  import { list } from './commands/list.js'
4
+ import { clean } from './commands/clean.js'
4
5
 
5
6
  const HELP = `
6
7
  vibe-spec — 跨项目通用 AI 编程规范管理工具
@@ -10,6 +11,7 @@ vibe-spec — 跨项目通用 AI 编程规范管理工具
10
11
  vibe-spec sync 同步当前项目到最新版本
11
12
  vibe-spec sync --all 一次性推送到所有已注册项目
12
13
  vibe-spec list 查看可用的规范、工作流和已注册项目
14
+ vibe-spec clean 移除当前项目的 vibe-spec 接入(删除规范文件、注入块和配置)
13
15
  vibe-spec --help 显示帮助信息
14
16
  `.trim()
15
17
 
@@ -27,6 +29,9 @@ export async function run(args) {
27
29
  case 'list':
28
30
  await list()
29
31
  break
32
+ case 'clean':
33
+ await clean()
34
+ break
30
35
  case '--help':
31
36
  case '-h':
32
37
  case undefined:
package/src/inject.js CHANGED
@@ -6,25 +6,43 @@ const END_MARKER = '<!-- vibe-spec:end -->'
6
6
 
7
7
  /**
8
8
  * 根据选中的 specs 和 workflows 生成注入内容
9
+ * @param {Array<{name: string, description?: string}>} specs
10
+ * @param {Array<{name: string, description?: string}>} workflows
11
+ * @param {string} specDir
9
12
  */
10
- export function buildInjection(specNames, workflowNames, specDir) {
11
- const lines = [START_MARKER, '## 开发规范', '']
13
+ export function buildInjection(specs, workflows, specDir) {
14
+ const lines = [
15
+ START_MARKER,
16
+ '## 共享规范',
17
+ '',
18
+ '以下是跨项目共享的开发规范和工作流,通过 `vibe-spec sync` 命令同步更新。',
19
+ '',
20
+ ]
12
21
 
13
- for (const name of specNames) {
14
- lines.push(`- [${name}](./${specDir}/${name}.md)`)
22
+ if (specs.length > 0) {
23
+ lines.push('**开发规范:**')
24
+ for (const { name, description } of specs) {
25
+ lines.push(formatItem(name, description, specDir))
26
+ }
15
27
  }
16
28
 
17
- if (workflowNames.length > 0) {
18
- lines.push('', '## 工作流程', '')
19
- for (const name of workflowNames) {
20
- lines.push(`- [${name}](./${specDir}/${name}.md)`)
29
+ if (workflows.length > 0) {
30
+ if (specs.length > 0) lines.push('')
31
+ lines.push('**工作流程:**')
32
+ for (const { name, description } of workflows) {
33
+ lines.push(formatItem(name, description, specDir))
21
34
  }
22
35
  }
23
36
 
24
- lines.push(END_MARKER)
37
+ lines.push('', END_MARKER)
25
38
  return lines.join('\n')
26
39
  }
27
40
 
41
+ function formatItem(name, description, specDir) {
42
+ const link = `[${name}](./${specDir}/${name}.md)`
43
+ return description ? `- ${link} — ${description}` : `- ${link}`
44
+ }
45
+
28
46
  /**
29
47
  * 将内容注入 CLAUDE.md 的标记区域
30
48
  * 如果标记不存在,追加到文件末尾
package/src/registry.js CHANGED
@@ -10,12 +10,14 @@ export function getAvailableItems() {
10
10
  name: path.basename(file, '.md'),
11
11
  file: path.join(SPECS_DIR, file),
12
12
  type: 'spec',
13
+ description: parseFrontmatter(path.join(SPECS_DIR, file)).description || '',
13
14
  }))
14
15
 
15
16
  const workflows = readDir(WORKFLOWS_DIR).map((file) => ({
16
17
  name: path.basename(file, '.md'),
17
18
  file: path.join(WORKFLOWS_DIR, file),
18
19
  type: 'workflow',
20
+ description: parseFrontmatter(path.join(WORKFLOWS_DIR, file)).description || '',
19
21
  }))
20
22
 
21
23
  return { specs, workflows }
@@ -29,6 +31,24 @@ export function getPackageVersion() {
29
31
  return pkg.version
30
32
  }
31
33
 
34
+ /**
35
+ * 解析 Markdown 文件的 YAML frontmatter
36
+ */
37
+ function parseFrontmatter(filePath) {
38
+ const content = fs.readFileSync(filePath, 'utf-8')
39
+ const match = content.match(/^---\n([\s\S]*?)\n---/)
40
+ if (!match) return {}
41
+ const result = {}
42
+ for (const line of match[1].split('\n')) {
43
+ const idx = line.indexOf(':')
44
+ if (idx === -1) continue
45
+ const key = line.slice(0, idx).trim()
46
+ const value = line.slice(idx + 1).trim()
47
+ result[key] = value
48
+ }
49
+ return result
50
+ }
51
+
32
52
  function readDir(dir) {
33
53
  if (!fs.existsSync(dir)) return []
34
54
  return fs.readdirSync(dir).filter((f) => f.endsWith('.md'))
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: 阶段性回顾时执行,检查项目文档的索引完整性、格式合规和内容时效性
3
+ ---
4
+
1
5
  # 文档检查
2
6
 
3
7
  阶段性回顾时执行,检查项目文档的健康状态: