From df9c6a77b302f04307a2d5940470e0e0ec6364c6 Mon Sep 17 00:00:00 2001 From: rui Date: Sun, 31 May 2026 02:09:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v0.3=20=E9=9B=B6=E4=BE=B5=E5=85=A5?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=20+=20lingshu=20upgrade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 内置 adapter 注册表,同步引擎不再随派生仓分发 - 派生仓移除 .lingshu/ 与 package.json,sync 统一走全局 CLI - tool 命令 baseline/personal → track/untrack(直接编辑 .gitignore) - 新增 lingshu upgrade(v0.2.x→v0.3 迁移 + 1.0 检测)与 hooks install - 模板瘦身、规则文件注入 frontmatter、CI 改 npx - smoke 测试扩至 17 项;新增 CHANGELOG.md;版本 bump 0.3.0 BREAKING CHANGE: 派生仓不再含 .lingshu/ 与 package.json,需全局安装 @ruobai/lingshu --- CHANGELOG.md | 102 +++++ README.md | 54 ++- bin/lingshu.mjs | 16 +- package.json | 5 +- src/commands/doctor.mjs | 2 - src/commands/hooks.mjs | 31 ++ src/commands/init.mjs | 64 +--- src/commands/sync.mjs | 13 +- src/commands/tool.mjs | 109 ++++-- src/commands/upgrade.mjs | 228 +++++++++++ src/core/adapters.mjs | 119 ++++-- src/core/hooks.mjs | 45 +++ src/core/registry.mjs | 76 ++++ .../.github/workflows/rules-consistency.yml | 6 +- templates/default/.lingshu/README.md | 66 ---- .../default/.lingshu/config/adapters.mjs | 95 ----- templates/default/.lingshu/hooks/post-merge | 10 - templates/default/.lingshu/scripts/doctor.mjs | 83 ---- .../.lingshu/scripts/install-hooks.mjs | 43 --- .../default/.lingshu/scripts/sync-rules.mjs | 180 --------- templates/default/AGENTS.md | 105 ----- templates/default/CLAUDE.md | 105 ----- templates/default/README.md | 80 ++-- templates/default/_gitignore | 2 +- templates/default/package.json | 19 - .../default/reference/rules/ai-behavior.md | 8 + .../default/reference/rules/lingshu-core.md | 14 +- tests/smoke.test.mjs | 360 +++++++++++------- 28 files changed, 993 insertions(+), 1047 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/commands/hooks.mjs create mode 100644 src/commands/upgrade.mjs create mode 100644 src/core/hooks.mjs create mode 100644 src/core/registry.mjs delete mode 100644 templates/default/.lingshu/README.md delete mode 100644 templates/default/.lingshu/config/adapters.mjs delete mode 100644 templates/default/.lingshu/hooks/post-merge delete mode 100644 templates/default/.lingshu/scripts/doctor.mjs delete mode 100644 templates/default/.lingshu/scripts/install-hooks.mjs delete mode 100644 templates/default/.lingshu/scripts/sync-rules.mjs delete mode 100644 templates/default/AGENTS.md delete mode 100644 templates/default/CLAUDE.md delete mode 100644 templates/default/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2163922 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,102 @@ +# 更新日志 (Changelog) + +本项目的所有显著变更都记录于此。 + +格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), +版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 + +## [0.3.0] - 2026-05-31 + +> **主题:零侵入 (Zero-Intrusion)** —— CLI 即唯一引擎,派生仓只保留治理资产。 + +### ⚠️ BREAKING CHANGES + +- **派生仓不再携带 `.lingshu/` 目录与 `package.json`**。同步引擎完全内置于 CLI, + 仓库只保留 `reference/` 治理资产与 AI 工具产物。 +- **同步统一走 `lingshu sync`**,不再有 `npm run sync`。团队每位成员需各自 + `npm install -g @ruobai/lingshu` 一次;CI 改用 `npx -y @ruobai/lingshu`。 +- **`tool` 命令重设计**:`baseline`/`personal` → `track`/`untrack`,直接编辑 + `.gitignore`(`.gitignore` 成为「入库 vs 忽略」的唯一真相)。 + +### Added + +- **内置 adapter 注册表**(`src/core/registry.mjs`):6 大工具开箱即用,零配置。 +- **`lingshu upgrade`**:将存量项目迁移到零侵入结构。 + - v0.2.x → v0.3:删 `.lingshu/`、删/瘦身 `package.json`、把 cursor frontmatter + 迁入 `reference/rules/*.md`、改造 CI、重装 hooks、重生成基线产物。 + - 灵枢 1.0(无 `reference/rules/` 真源):检测并给出手动迁移指引,不做有损迁移。 + - 支持 `--dry-run` 预览、`--force`。 +- **`lingshu hooks install`**:为存量项目补装内置 git hooks。 +- **零配置逃生舱**:可选 `reference/.lingshu.json` 声明自定义适配器 / 覆盖基线。 +- **规则文件自带 frontmatter**:`reference/rules/*.md` 通过 `order` 控制合并顺序, + cursor 的 `.mdc` frontmatter 直接读自规则文件本身。 + +### Changed + +- 同步引擎(`src/core/adapters.mjs`)从「读项目内 `.lingshu/config/adapters.mjs`」 + 改为「读内置注册表 + 约定发现 `reference/rules/*.md`」。 +- `init` 不再写入 `package.json` / `adapters.mjs`;git hooks 改为 CLI 内联写入 + `.git/hooks/`(不再依赖 `postinstall`)。 +- 模板瘦身:移除 `templates/default/` 内的 `.lingshu/`、`package.json` 及预置 + `CLAUDE.md`/`AGENTS.md`(改为 `init` 时生成)。 +- 生成产物头部与规则真源中对旧引擎(`.lingshu/scripts` / `npm run sync`)的引用 + 统一更新为 `lingshu sync`。 +- smoke 测试扩充至 17 项,覆盖零侵入契约、`tool track/untrack`、`upgrade` 全路径。 + +## [0.2.6] - 2026-05-08 + +### Fixed + +- `limb add/init/adopt` 成功后自动维护 `.gitignore`:非约定命名追加 `/`, + 约定命名(被 `*-server/` 等通配覆盖)幂等跳过。 + +## [0.2.5] - 2026-05-08 + +### Fixed + +- 修复派生项目缺失 `.gitignore`:npm publish 会把 `.gitignore` 当 `.npmignore` + 并自身排除。模板内改名为 `_gitignore`,`init` 时复原为 `.gitignore`。 + +## [0.2.4] - 2026-05-08 + +### Added + +- `sync` 默认 auto 模式:仅同步 baseline + 已存在产物的 personal 工具。 +- `limb init`(创建空肢体 + git init)与 `limb adopt`(纳入已有本地目录)。 + +## [0.2.3] - 2026-05-08 + +### Changed + +- `init` 默认仅生成基线产物(baseline-only),新增 `--all-tools`。 +- `init` 注入项目身份至 `package.json` 的 `description`。 + +## [0.2.2] - 2026-05-08 + +### Fixed + +- 修复致命 bug:`copyTemplate` 用绝对路径匹配 `node_modules`,导致全局安装时 + 整个模板树被误过滤为空。改为只看相对模板根的路径。 + +## [0.2.1] - 2026-05-08 + +### Changed + +- 文档清理:去除 SyncBase 双品牌、夸大词与 🌀 图标。 + +## [0.2.0] - 2026-05-08 + +### Added + +- 首次开源发布:GitHub + npm 公网 + MIT 协议。 +- 核心命令 `init` / `sync` / `doctor` / `tool` / `limb`。 +- 包名 `@ruobai/lingshu`(若白知行 npm scope)。 + +[0.3.0]: https://github.com/imrui/lingshu-cli/compare/v0.2.6...v0.3.0 +[0.2.6]: https://github.com/imrui/lingshu-cli/compare/v0.2.5...v0.2.6 +[0.2.5]: https://github.com/imrui/lingshu-cli/compare/v0.2.4...v0.2.5 +[0.2.4]: https://github.com/imrui/lingshu-cli/compare/v0.2.3...v0.2.4 +[0.2.3]: https://github.com/imrui/lingshu-cli/compare/v0.2.2...v0.2.3 +[0.2.2]: https://github.com/imrui/lingshu-cli/compare/v0.2.1...v0.2.2 +[0.2.1]: https://github.com/imrui/lingshu-cli/compare/v0.2.0...v0.2.1 +[0.2.0]: https://github.com/imrui/lingshu-cli/releases/tag/v0.2.0 diff --git a/README.md b/README.md index c663464..aca95a8 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ `@ruobai/lingshu` 是 [灵枢架构 (LingShu)](https://github.com/imrui/lingshu-template) 的官方命令行工具,把 7 步手动流程压缩为 1 条命令。 +> **v0.3 零侵入**:同步引擎完全内置于本 CLI,派生仓**不再携带** `.lingshu/` 目录与 `package.json`——只保留 `reference/` 治理资产与 AI 工具产物。存量项目可用 `lingshu upgrade` 一键迁移。 + --- ## 1 分钟上手 @@ -34,9 +36,11 @@ npm install -g @ruobai/lingshu npm install -g git+ssh://git@github.com/imrui/lingshu-cli.git # 锁定版本 -npm install -g git+ssh://git@github.com/imrui/lingshu-cli.git#v0.2.6 +npm install -g git+ssh://git@github.com/imrui/lingshu-cli.git#v0.3.0 ``` +> 团队协作提示:v0.3 起派生仓不含 `package.json`,同步统一走全局 `lingshu sync`。**团队每位成员各全局安装一次**即可;CI 用 `npx -y @ruobai/lingshu` 临时调用。 + ## 创建新项目 ```bash @@ -48,7 +52,7 @@ lingshu init my-lingshu-app \ > 把 `your-org` 替换为你的 GitHub 组织或用户名。 -一条命令完成:拷贝模板 → 注入项目身份 → 配置 AI 工具基线 → 生成 `CLAUDE.md / AGENTS.md` → `git init` 与 remote → 安装 git hooks → 克隆肢体仓。 +一条命令完成:拷贝模板 → 注入项目身份 → 生成 `CLAUDE.md / AGENTS.md` → `git init` 与 remote → 安装 git hooks(内置写入 `.git/hooks/`)→ 克隆肢体仓。 --- @@ -61,7 +65,7 @@ lingshu init my-lingshu-app \ | `` | 项目目录名(位置参数) | | `--here` | 在当前目录初始化(不创建子目录) | | `--remote=` | 设置 git remote origin | -| `--tools=` | 基线工具列表(默认 `claude-code,codex`) | +| `--tools=` | 指定生成哪些工具的产物(默认基线 `claude-code,codex`) | | `--all-tools` | 同时生成 personal 工具产物(cursor / trae / qoder / antigravity);默认不生成,留待开发者本地按需 `lingshu sync` | | `--limbs=` | 肢体仓 `name:url,name:url` 格式 | | `--no-git` | 跳过 git init | @@ -90,11 +94,13 @@ lingshu init my-lingshu-app \ ### `lingshu tool ` +管理「某工具产物是否入 git」。v0.3 起 `.gitignore` 本身就是唯一真相,子命令直接增删它。 + | 子命令 | 说明 | |--------|------| -| `list` | 列出所有适配器及状态 | -| `baseline ` | 将工具改为基线(产物入库) | -| `personal ` | 将工具改为个人(产物 gitignore) | +| `list` | 列出所有内置工具及其追踪状态(tracked / ignored) | +| `track ` | 让该工具产物入库(从 `.gitignore` 移除其忽略规则) | +| `untrack ` | 让该工具产物不入库(向 `.gitignore` 追加其忽略规则) | ### `lingshu limb ` @@ -105,7 +111,26 @@ lingshu init my-lingshu-app \ | `init ` | 创建空肢体目录 `/` 并完成 `git init`(无 remote) | | `adopt ` | 把已有本地目录复制到 `/` 纳入肢体管理 | -> 三个新增子命令覆盖了"远程未建好就先本地起"和"把现有目录纳入"等真实场景。`add` 仍然是远程克隆的快捷方式。 +> 三个子命令覆盖了"远程未建好就先本地起"和"把现有目录纳入"等真实场景。`add` 仍然是远程克隆的快捷方式。 + +### `lingshu hooks ` + +| 子命令 | 说明 | +|--------|------| +| `install` | 在当前项目安装内置 git hooks(`post-merge`:`git pull` 后自动 `lingshu sync`)。`init` 时已自动安装,本命令供存量项目补装 | + +### `lingshu upgrade` + +把存量项目迁移到 v0.3 零侵入结构。 + +| 选项 | 说明 | +|------|------| +| _(无参数)_ | 执行迁移 | +| `--dry-run` | 仅预览将执行的动作,不写盘 | +| `--force` | 即使检测到风险(如 `package.json` 含业务依赖)也继续 | + +- **v0.2.x → v0.3**:删除 `.lingshu/`、删除/瘦身 `package.json`、把原 `adapters.mjs` 的 cursor frontmatter 迁入 `reference/rules/*.md`、改造 CI 为 `npx`、重装 hooks、重生成基线产物。 +- **灵枢 1.0**(规则散落在产物、无 `reference/rules/` 真源):无法无损自动迁移,给出明确的手动迁移指引。 --- @@ -120,7 +145,7 @@ lingshu init my-lingshu-app \ | Qoder | `.qoder/rules/*.md` | 个人 | | Antigravity | `.agent/rules/*.md` | 个人 | -新增工具:编辑生成项目的 `.lingshu/config/adapters.mjs` 即可。 +内置 6 大工具开箱即用,无需任何配置。若需接入未内置的工具,可在项目的 `reference/.lingshu.json` 声明自定义适配器(可选逃生舱)。 --- @@ -128,8 +153,8 @@ lingshu init my-lingshu-app \ 1. **零依赖**:纯 Node 内置模块,避免 `node_modules` 膨胀 2. **跨平台**:兼容 Win / macOS / Linux -3. **薄包装**:CLI 不做模板里 `.lingshu/scripts/` 已能做的事 -4. **可演进**:模板可替换,适配器可扩展 +3. **零侵入**:同步引擎在 CLI 内,派生仓只留治理资产,不背引擎与 `package.json` +4. **可演进**:模板可替换,适配器内置可扩展 --- @@ -140,10 +165,11 @@ lingshu init my-lingshu-app \ | `init` | ✅ | | `sync` | ✅ | | `doctor` | ✅ | -| `tool` | ✅ | +| `tool`(track/untrack) | ✅ | | `limb` | ✅ | +| `hooks` | ✅ | +| `upgrade` | ✅ | | `archive` | 🚧 待规划 | -| `upgrade` | 🚧 待规划 | --- @@ -153,7 +179,7 @@ lingshu init my-lingshu-app \ git clone git@github.com:imrui/lingshu-cli.git cd lingshu-cli -npm test # 运行 5 个 smoke 测试 +npm test # 运行 smoke 测试(17 项) node bin/lingshu.mjs --help # 本地试运行 npm link # 全局链接,方便调试 ``` @@ -187,4 +213,4 @@ npm link # 全局链接,方便调试 [MIT](./LICENSE) © 2026 imrui -> CLI 内嵌的模板(`templates/default/package.json`)`license` 字段保持 `UNLICENSED` 占位,由通过 `lingshu init` 派生的新项目作者自决。 +> 本 CLI 以 MIT 协议开源。通过 `lingshu init` 派生的新项目不含任何脚手架 `package.json`,协议由项目作者自决。 diff --git a/bin/lingshu.mjs b/bin/lingshu.mjs index 771946d..d6788e4 100644 --- a/bin/lingshu.mjs +++ b/bin/lingshu.mjs @@ -8,8 +8,10 @@ * init 初始化新的灵枢项目 * sync 重新分发规则到本地 AI 工具 * doctor 架构健康检查 - * tool 管理 AI 工具支持(list/baseline/personal) + * tool 管理 AI 工具支持(list/track/untrack) * limb 管理肢体仓(list/add) + * hooks 安装 git hooks(install) + * upgrade 迁移存量项目到 v0.3 零侵入结构 * --version, -v 显示版本 * --help, -h 显示帮助 */ @@ -43,8 +45,8 @@ ${pkg.description} v${pkg.version} doctor 架构健康检查(物理结构 + SSoT 真源 + 一致性) - tool 管理 AI 工具支持 - 子命令: list | baseline | personal + tool 管理 AI 工具产物的 git 追踪状态 + 子命令: list | track | untrack limb 管理肢体仓 子命令: @@ -53,6 +55,12 @@ ${pkg.description} v${pkg.version} init 创建空肢体(mkdir + git init) adopt 纳入已有本地目录 + hooks 安装灵枢 git hooks + 子命令: install + + upgrade 将存量项目迁移到 v0.3 零侵入结构 + 选项: --dry-run(仅预览), --force + --version, -v 显示版本 --help, -h 显示帮助 @@ -65,6 +73,8 @@ const COMMANDS = { doctor: () => import('../src/commands/doctor.mjs'), tool: () => import('../src/commands/tool.mjs'), limb: () => import('../src/commands/limb.mjs'), + hooks: () => import('../src/commands/hooks.mjs'), + upgrade: () => import('../src/commands/upgrade.mjs'), }; async function main() { diff --git a/package.json b/package.json index 34c774d..77a5166 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ruobai/lingshu", - "version": "0.2.6", + "version": "0.3.0", "description": "灵枢架构 CLI — AI 原生项目脚手架 | 若白知行出品", "type": "module", "bin": { @@ -16,7 +16,8 @@ "src", "templates", "LICENSE", - "README.md" + "README.md", + "CHANGELOG.md" ], "engines": { "node": ">=18" diff --git a/src/commands/doctor.mjs b/src/commands/doctor.mjs index af1e6be..1d81168 100644 --- a/src/commands/doctor.mjs +++ b/src/commands/doctor.mjs @@ -26,8 +26,6 @@ export default async function doctor() { 'reference/management/tasks', 'reference/management/walkthroughs', 'reference/management/reports', - '.lingshu/scripts', - '.lingshu/config', ]; for (const d of requiredDirs) { if (existsSync(join(root, d))) ok(d); diff --git a/src/commands/hooks.mjs b/src/commands/hooks.mjs new file mode 100644 index 0000000..d602bf4 --- /dev/null +++ b/src/commands/hooks.mjs @@ -0,0 +1,31 @@ +/** + * lingshu hooks + * + * install 在当前项目安装灵枢 git hooks(post-merge 自动同步) + * + * 供存量项目补装 hooks;init 时已自动安装。 + */ + +import { log } from '../utils/log.mjs'; +import { installHooks } from '../core/hooks.mjs'; +import { isLingshuProject } from '../core/adapters.mjs'; + +export default async function hooks({ args }) { + const [sub = 'install'] = args; + const root = process.cwd(); + + if (sub !== 'install') { + log.error(`未知子命令: ${sub}`); + log.hint('可用: install'); + process.exit(1); + } + + if (!isLingshuProject(root)) throw new Error('当前目录不是灵枢项目(未找到 reference/rules/)'); + + const { installed, skipped } = installHooks(root); + if (skipped) { + log.warn('当前目录不是 git 仓库,跳过 hooks 安装'); + return; + } + log.ok(`已安装 git hooks: ${installed.join(', ')}`); +} diff --git a/src/commands/init.mjs b/src/commands/init.mjs index 177223f..3a67750 100644 --- a/src/commands/init.mjs +++ b/src/commands/init.mjs @@ -11,12 +11,13 @@ * --template= 使用自定义模板路径(默认内置 default) */ -import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, renameSync, rmSync } from 'node:fs'; +import { existsSync, mkdirSync, readdirSync, renameSync, rmSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { parseArgs, parseList } from '../utils/args.mjs'; import { log, c } from '../utils/log.mjs'; import { copyTemplate, replacePlaceholders } from '../core/template.mjs'; import { distribute } from '../core/adapters.mjs'; +import { installHooks } from '../core/hooks.mjs'; import { isGitAvailable, gitInit, git, gitClone } from '../core/git.mjs'; export default async function init({ args, pkgRoot }) { @@ -77,51 +78,27 @@ export default async function init({ args, pkgRoot }) { log.ok('模板已拷贝'); - // Step 2: 替换占位符 + 重写 package.json 关键字段 + // Step 2: 替换占位符(注入项目身份) log.step('注入项目身份'); replacePlaceholders(targetDir, { 'lingshu-template': finalName, }); - const pkgPath = join(targetDir, 'package.json'); - if (existsSync(pkgPath)) { - try { - const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); - pkg.description = `${finalName} — 灵枢架构项目`; - writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8'); - } catch { /* 解析失败不阻断 init,作者后续手改即可 */ } - } log.ok(`项目身份已注入: ${c.bold(finalName)}`); - // Step 3: 配置基线工具(如有 --tools) - if (opts.tools) { - log.step('配置基线工具'); - const tools = parseList(opts.tools); - const cfgPath = join(targetDir, '.lingshu/config/adapters.mjs'); - if (existsSync(cfgPath)) { - let content = readFileSync(cfgPath, 'utf8'); - const re = /export const baseline = \[[^\]]*\];/; - const replacement = `export const baseline = [${tools.map(t => `'${t}'`).join(', ')}];`; - if (re.test(content)) { - content = content.replace(re, replacement); - writeFileSync(cfgPath, content, 'utf8'); - log.ok(`基线工具: ${tools.join(', ')}`); - } else { - log.warn('未在 adapters.mjs 中找到 baseline 配置,跳过'); - } - } - } - - // Step 4: 生成产物(默认仅 baseline,--all-tools 时包括 personal) + // Step 3: 生成产物 + // 默认 baseline-only(claude-code/codex);--all-tools 含个人工具;--tools 指定集合 const allTools = !!flags['all-tools']; - log.step(allTools ? '生成所有工具产物' : '生成基线工具产物'); - const result = await distribute({ + const tools = opts.tools ? parseList(opts.tools) : undefined; + log.step(tools ? `生成指定工具产物(${tools.join(', ')})` : allTools ? '生成所有工具产物' : '生成基线工具产物'); + const result = distribute({ projectRoot: targetDir, - baselineOnly: !allTools, - all: allTools, + tools, + baselineOnly: !tools && !allTools, + all: !tools && allTools, }); log.ok(`已生成 ${result.written.length} 个产物文件`); for (const f of result.written) log.hint(` ${f}`); - if (!allTools) log.hint(`如需个人工具产物(cursor/trae/...),稍后跑 ${c.bold('lingshu sync')} 或加 ${c.bold('--all-tools')}`); + if (!allTools && !tools) log.hint(`如需个人工具产物(cursor/trae/...),稍后跑 ${c.bold('lingshu sync --only=')} 或加 ${c.bold('--all-tools')}`); // Step 5: git init if (!flags['no-git']) { @@ -138,17 +115,12 @@ export default async function init({ args, pkgRoot }) { } } - // Step 6: 安装 git hooks + // Step 6: 安装 git hooks(内置,无需项目内脚本) if (!flags['no-install-hooks']) { log.step('安装 git hooks'); - const hooksScript = join(targetDir, '.lingshu/scripts/install-hooks.mjs'); - if (existsSync(hooksScript)) { - const { spawnSync } = await import('node:child_process'); - const r = spawnSync(process.execPath, [hooksScript], { cwd: targetDir, stdio: 'inherit' }); - if (r.status !== 0) log.warn('install-hooks 退出码非 0'); - } else { - log.warn('未找到 install-hooks.mjs,跳过'); - } + const { installed, skipped } = installHooks(targetDir); + if (skipped) log.warn('非 git 仓库,跳过 hooks 安装'); + else log.ok(`已安装 git hooks: ${installed.join(', ')}`); } // Step 7: 拉取肢体仓 @@ -177,8 +149,8 @@ export default async function init({ args, pkgRoot }) { log.banner('初始化完成'); console.log(c.dim('下一步:')); if (!here) console.log(` cd ${finalName}`); - console.log(' npm install # 安装依赖(含 hooks 自动安装)'); - console.log(' npm run doctor # 健康检查'); + console.log(' lingshu doctor # 健康检查'); + console.log(' lingshu sync # 重新分发规则到本地 AI 工具'); console.log(' git push -u origin master # 推送到远程(如已设置 remote)'); log.blank(); } diff --git a/src/commands/sync.mjs b/src/commands/sync.mjs index 09481f8..e17ff54 100644 --- a/src/commands/sync.mjs +++ b/src/commands/sync.mjs @@ -1,26 +1,23 @@ /** * lingshu sync [options] * - * 在当前灵枢项目目录执行规则分发。 - * 等价于 npm run sync,但提供统一的 CLI 入口。 + * 在当前灵枢项目目录执行规则分发(零配置:读内置注册表 + reference/rules/)。 */ import { parseArgs, parseList } from '../utils/args.mjs'; import { log, c } from '../utils/log.mjs'; -import { distribute } from '../core/adapters.mjs'; -import { existsSync } from 'node:fs'; -import { join } from 'node:path'; +import { distribute, isLingshuProject } from '../core/adapters.mjs'; export default async function sync({ args }) { const { opts, flags } = parseArgs(args, { booleanFlags: ['check', 'baseline', 'all'] }); const projectRoot = process.cwd(); - if (!existsSync(join(projectRoot, '.lingshu/config/adapters.mjs'))) { - throw new Error('当前目录不是灵枢项目(未找到 .lingshu/config/adapters.mjs)'); + if (!isLingshuProject(projectRoot)) { + throw new Error('当前目录不是灵枢项目(未找到 reference/rules/)'); } const tools = opts.only ? parseList(opts.only) : undefined; - const result = await distribute({ + const result = distribute({ projectRoot, tools, baselineOnly: !!flags.baseline, diff --git a/src/commands/tool.mjs b/src/commands/tool.mjs index a67d067..3d83116 100644 --- a/src/commands/tool.mjs +++ b/src/commands/tool.mjs @@ -1,78 +1,103 @@ /** * lingshu tool * - * list 列出所有适配器及其状态 - * baseline 将工具标记为基线(产物入库) - * personal 将工具标记为个人(产物 gitignore) + * list 列出所有内置 AI 工具及其 git 追踪状态 + * track 让该工具产物入库(从 .gitignore 移除其忽略规则) + * untrack 让该工具产物不入库(向 .gitignore 追加其忽略规则) + * + * v0.3 起零配置:「入库 vs 忽略」的唯一真相就是 .gitignore 本身, + * track/untrack 只增删 .gitignore,不再维护任何配置文件。 */ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { log, c } from '../utils/log.mjs'; -import { loadConfig } from '../core/adapters.mjs'; +import { ADAPTERS } from '../core/registry.mjs'; +import { isLingshuProject } from '../core/adapters.mjs'; + +/** 工具产物在 .gitignore 中的归一化忽略路径(目录型带尾斜杠) */ +function ignorePath(cfg) { + return cfg.type === 'directory' ? cfg.target.replace(/\/?$/, '/') : cfg.target; +} + +/** .gitignore 是否含与该忽略路径完全相等的一行(按行精确匹配) */ +function hasIgnoreLine(content, path) { + return content.split(/\r?\n/).some((l) => l.trim() === path); +} export default async function tool({ args }) { const [sub, name] = args; const root = process.cwd(); - const cfgPath = join(root, '.lingshu/config/adapters.mjs'); - if (!existsSync(cfgPath)) throw new Error('当前目录不是灵枢项目'); + if (!isLingshuProject(root)) throw new Error('当前目录不是灵枢项目(未找到 reference/rules/)'); - if (!sub || sub === 'list') { - return list(root); - } + if (!sub || sub === 'list') return list(root); - if (!name) { - log.error(`请提供工具名: lingshu tool ${sub} `); - process.exit(1); + if (sub === 'track' || sub === 'untrack') { + if (!name) { + log.error(`请提供工具名: lingshu tool ${sub} `); + log.hint(`可用工具: ${Object.keys(ADAPTERS).join(', ')}`); + process.exit(1); + } + return setTracked(root, name, sub === 'track'); } - if (sub === 'baseline') return setBaseline(cfgPath, name, true); - if (sub === 'personal') return setBaseline(cfgPath, name, false); - log.error(`未知子命令: ${sub}`); - log.hint('可用: list | baseline | personal '); + log.hint('可用: list | track | untrack '); process.exit(1); } -async function list(root) { - const { adapters, baseline } = await loadConfig(root); +function list(root) { + const giPath = join(root, '.gitignore'); + const gi = existsSync(giPath) ? readFileSync(giPath, 'utf8') : ''; log.banner('AI 工具支持矩阵'); - for (const [name, cfg] of Object.entries(adapters)) { - const tag = baseline.includes(name) ? c.green('[baseline]') : c.dim('[personal]'); + for (const [name, cfg] of Object.entries(ADAPTERS)) { + const ignored = hasIgnoreLine(gi, ignorePath(cfg)); + const tag = ignored ? c.dim('[ignored]') : c.green('[tracked]'); const target = cfg.type === 'directory' ? cfg.target + '*' + cfg.extension : cfg.target; console.log(` ${tag} ${c.bold(name.padEnd(14))} → ${c.dim(target)}`); } log.blank(); - log.hint('修改基线: lingshu tool baseline|personal '); + log.hint('入库: lingshu tool track 忽略: lingshu tool untrack '); } -function setBaseline(cfgPath, name, makeBaseline) { - let content = readFileSync(cfgPath, 'utf8'); - const re = /export const baseline = \[([^\]]*)\];/; - const m = content.match(re); - if (!m) throw new Error('未在 adapters.mjs 中找到 baseline 数组'); +function setTracked(root, name, makeTracked) { + const cfg = ADAPTERS[name]; + if (!cfg) { + log.error(`未知工具: ${name}`); + log.hint(`可用工具: ${Object.keys(ADAPTERS).join(', ')}`); + process.exit(1); + } + + const giPath = join(root, '.gitignore'); + if (!existsSync(giPath)) { + log.error('当前项目无 .gitignore,无法管理追踪状态'); + process.exit(1); + } - const current = m[1].match(/'([^']+)'/g)?.map(s => s.slice(1, -1)) ?? []; - let next = [...current]; + const path = ignorePath(cfg); + let content = readFileSync(giPath, 'utf8'); + const already = hasIgnoreLine(content, path); - if (makeBaseline) { - if (current.includes(name)) { - log.info(`${name} 已在基线中`); + if (makeTracked) { + if (!already) { + log.info(`${name} 已是入库状态(.gitignore 未忽略 ${path})`); return; } - next.push(name); + // 移除该忽略行 + content = content + .split(/\r?\n/) + .filter((l) => l.trim() !== path) + .join('\n'); + writeFileSync(giPath, content, 'utf8'); + log.ok(`${name} → ${c.green('tracked')}(已从 .gitignore 移除 ${path})`); } else { - if (!current.includes(name)) { - log.info(`${name} 已是个人工具`); + if (already) { + log.info(`${name} 已是忽略状态`); return; } - next = current.filter(n => n !== name); + const prefix = content.endsWith('\n') || content === '' ? '' : '\n'; + writeFileSync(giPath, content + prefix + path + '\n', 'utf8'); + log.ok(`${name} → ${c.dim('ignored')}(已向 .gitignore 追加 ${path})`); } - - const newLine = `export const baseline = [${next.map(t => `'${t}'`).join(', ')}];`; - content = content.replace(re, newLine); - writeFileSync(cfgPath, content, 'utf8'); - - log.ok(`${name} → ${makeBaseline ? c.green('baseline') : c.dim('personal')}`); - log.hint('记得运行 `lingshu sync` 重新分发,并相应更新 .gitignore'); + log.hint('记得运行 `lingshu sync` 重新生成产物'); } diff --git a/src/commands/upgrade.mjs b/src/commands/upgrade.mjs new file mode 100644 index 0000000..b0398f9 --- /dev/null +++ b/src/commands/upgrade.mjs @@ -0,0 +1,228 @@ +/** + * lingshu upgrade [options] + * + * --dry-run 仅打印将要执行的迁移动作,不写盘 + * --force 即使检测到风险也继续(如 package.json 含业务依赖) + * + * 将存量灵枢项目迁移到 v0.3 零侵入结构: + * - 删除 .lingshu/ 引擎目录 + * - 删除/瘦身 package.json(仅当是同步脚手架) + * - 把原 adapters.mjs 中 cursor 的 frontmatter 迁入 reference/rules/*.md + * - 重装内置 git hooks、改造 CI、重生成基线产物 + * + * 仅支持 v0.2.x → v0.3 的机械迁移;灵枢 1.0(规则散落在产物、无 reference/rules 真源) + * 无法无损自动迁移,会给出明确指引。 + */ + +import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { parseArgs } from '../utils/args.mjs'; +import { log, c } from '../utils/log.mjs'; +import { distribute, isLingshuProject } from '../core/adapters.mjs'; +import { installHooks } from '../core/hooks.mjs'; + +/** 探测当前项目的灵枢结构版本 */ +function detectVersion(root) { + const hasEngine = existsSync(join(root, '.lingshu/config/adapters.mjs')); + const hasRules = existsSync(join(root, 'reference/rules')); + if (hasRules && !hasEngine) return 'v0.3'; // 已零侵入 + if (hasRules && hasEngine) return 'v0.2'; // 旧引擎 + 真源 + // 无 reference/rules:可能是 1.0(规则散落产物)或非灵枢项目 + const hasLegacyProducts = ['.cursor/rules', '.trae/rules', '.qoder/rules', '.agent/rules'] + .some((p) => existsSync(join(root, p))); + if (hasLegacyProducts) return 'v1.0'; + return 'unknown'; +} + +/** 是否已带 frontmatter(以 --- 开头) */ +function hasFrontmatter(content) { + return /^---\r?\n/.test(content); +} + +/** 序列化 frontmatter(与引擎一致的 YAML 子集) */ +function renderFrontmatter(meta) { + const lines = ['---']; + for (const [k, v] of Object.entries(meta)) lines.push(`${k}: ${v}`); + lines.push('---', ''); + return lines.join('\n'); +} + +export default async function upgrade({ args }) { + const { flags } = parseArgs(args, { booleanFlags: ['dry-run', 'force'] }); + const dryRun = !!flags['dry-run']; + const root = process.cwd(); + + log.banner('灵枢项目升级(→ v0.3 零侵入)'); + + const version = detectVersion(root); + + if (version === 'unknown') { + throw new Error('当前目录不是灵枢项目(既无 reference/rules/,也无可识别的工具产物)'); + } + + if (version === 'v0.3') { + log.ok('当前已是 v0.3 零侵入结构,无需迁移'); + return; + } + + if (version === 'v1.0') { + log.warn('检测到灵枢 1.0 结构(规则直接写在产物中,无 reference/rules/ 真源)'); + log.blank(); + log.hint('1.0 无单一真源,无法无损自动迁移。建议手动迁移:'); + log.hint(' 1) 新建 reference/rules/lingshu-core.md 与 ai-behavior.md'); + log.hint(' 2) 把 .cursor/rules/*.mdc(或 .agent/rules)的规则正文整理进上述真源'); + log.hint(' 3) 删除散落的旧产物,运行 `lingshu sync --all` 由真源统一重生成'); + log.hint(' 4) 完成后再次运行 `lingshu upgrade` 校验'); + process.exit(1); + } + + // ===== v0.2 → v0.3 机械迁移 ===== + const actions = []; // 待执行动作(用于 dry-run 展示) + const warnings = []; + + // 1. 读取旧 adapters.mjs:取 sources 顺序 + cursor frontmatter + baseline + const cfgPath = join(root, '.lingshu/config/adapters.mjs'); + let oldSources = []; + let cursorFm = {}; + try { + const mod = await import(pathToFileURL(cfgPath).href); + oldSources = mod.sources ?? []; + cursorFm = mod.adapters?.cursor?.frontmatter ?? {}; + } catch (e) { + warnings.push(`读取旧 adapters.mjs 失败(将用默认 frontmatter): ${e.message}`); + } + + // 2. 为缺 frontmatter 的规则文件注入(order 按 sources 顺序,元数据取自旧 cursor 配置) + const rulesDir = join(root, 'reference/rules'); + const ruleFiles = existsSync(rulesDir) + ? readdirSync(rulesDir).filter((f) => f.endsWith('.md')) + : []; + const fmInjections = []; + for (const file of ruleFiles) { + const name = file.replace(/\.md$/, ''); + const full = join(rulesDir, file); + const content = readFileSync(full, 'utf8'); + if (hasFrontmatter(content)) continue; + const idx = oldSources.findIndex((s) => s.name === name); + const meta = { + order: idx >= 0 ? idx + 1 : 99, + name, + description: cursorFm[name]?.description ?? `${name} 规则`, + globs: cursorFm[name]?.globs ?? '**/*', + trigger: cursorFm[name]?.trigger ?? 'always_on', + }; + fmInjections.push({ full, file, meta, body: content }); + actions.push(`注入 frontmatter → reference/rules/${file}`); + } + + // 3. 删除 .lingshu/ + actions.push('删除 .lingshu/'); + + // 4. package.json:仅当是同步脚手架才删;含业务依赖则瘦身并保留 + const pkgPath = join(root, 'package.json'); + let pkgAction = null; + if (existsSync(pkgPath)) { + try { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); + const isScaffold = typeof pkg.scripts?.sync === 'string' + && pkg.scripts.sync.includes('.lingshu/scripts'); + const hasDeps = pkg.dependencies && Object.keys(pkg.dependencies).length > 0; + const hasDevDeps = pkg.devDependencies && Object.keys(pkg.devDependencies).length > 0; + if (isScaffold && !hasDeps && !hasDevDeps) { + pkgAction = { type: 'delete' }; + actions.push('删除 package.json(纯同步脚手架)'); + } else if (isScaffold) { + pkgAction = { type: 'trim', pkg }; + actions.push('瘦身 package.json(移除 lingshu 同步脚本,保留业务字段)'); + warnings.push('package.json 含业务依赖,已保留并仅移除 lingshu 脚本'); + } else { + warnings.push('package.json 非灵枢脚手架,未改动(请自行确认)'); + } + } catch { + warnings.push('package.json 解析失败,未改动'); + } + } + + // 5. CI 工作流改造 + const ciPath = join(root, '.github/workflows/rules-consistency.yml'); + let ciNew = null; + if (existsSync(ciPath)) { + const ci = readFileSync(ciPath, 'utf8'); + const patched = ci + .replace(/node \.lingshu\/scripts\/sync-rules\.mjs --check --baseline/g, 'npx -y @ruobai/lingshu sync --check --baseline') + .replace(/node \.lingshu\/scripts\/doctor\.mjs/g, 'npx -y @ruobai/lingshu doctor') + .replace(/^[ \t]*-[ \t]*'\.lingshu\/\*\*'[ \t]*\r?\n/gm, ''); + if (patched !== ci) { + ciNew = patched; + actions.push('改造 .github/workflows/rules-consistency.yml(npx 调用全局 CLI)'); + } + } + + // 6. 重装 hooks(覆盖旧的引用 .lingshu/scripts 的 post-merge) + if (existsSync(join(root, '.git'))) actions.push('重装 git hooks(post-merge → lingshu sync)'); + + // 7. 重生成基线产物 + actions.push('重新生成基线产物(lingshu sync --baseline)'); + + // 8. 扫描规则正文/README 中遗留的旧引擎引用(仅告警,不改写) + const staleRefs = []; + for (const file of ruleFiles) { + const txt = readFileSync(join(rulesDir, file), 'utf8'); + if (txt.includes('.lingshu/scripts') || txt.includes('npm run sync')) { + staleRefs.push(`reference/rules/${file}`); + } + } + if (existsSync(join(root, 'README.md'))) { + const txt = readFileSync(join(root, 'README.md'), 'utf8'); + if (txt.includes('.lingshu/') || txt.includes('npm run sync')) staleRefs.push('README.md'); + } + + // ---- 展示 / 执行 ---- + log.hint(`检测到结构版本: ${c.bold('v0.2')}`); + log.blank(); + console.log(c.cyan('将执行以下迁移动作:')); + for (const a of actions) console.log(` • ${a}`); + log.blank(); + + if (dryRun) { + log.ok('dry-run:以上动作未执行'); + if (warnings.length) { log.blank(); for (const w of warnings) log.warn(w); } + return; + } + + // 执行 + for (const { full, meta, body } of fmInjections) { + writeFileSync(full, renderFrontmatter(meta) + body, 'utf8'); + } + rmSync(join(root, '.lingshu'), { recursive: true, force: true }); + if (pkgAction?.type === 'delete') { + rmSync(pkgPath, { force: true }); + } else if (pkgAction?.type === 'trim') { + const pkg = pkgAction.pkg; + for (const k of ['sync', 'sync:check', 'sync:baseline', 'doctor', 'hooks:install', 'postinstall']) { + delete pkg.scripts?.[k]; + } + if (pkg.scripts && Object.keys(pkg.scripts).length === 0) delete pkg.scripts; + writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8'); + } + if (ciNew !== null) writeFileSync(ciPath, ciNew, 'utf8'); + if (existsSync(join(root, '.git'))) installHooks(root); + + // 重生成产物(此时 .lingshu 已删除,引擎走零配置) + if (!isLingshuProject(root)) { + throw new Error('迁移后未找到 reference/rules/,请检查项目结构'); + } + const result = distribute({ projectRoot: root, baselineOnly: true }); + + log.blank(); + log.ok(`迁移完成(重生成 ${result.written.length} 个基线产物)`); + if (warnings.length) { log.blank(); for (const w of warnings) log.warn(w); } + if (staleRefs.length) { + log.blank(); + log.warn('以下文件正文仍引用旧引擎(.lingshu/scripts 或 npm run sync),请手动更新:'); + for (const f of staleRefs) log.hint(` - ${f}`); + } + log.blank(); + log.hint('建议运行 `lingshu doctor` 与 `lingshu sync --check --baseline` 校验'); +} diff --git a/src/core/adapters.mjs b/src/core/adapters.mjs index 47744ac..34c871d 100644 --- a/src/core/adapters.mjs +++ b/src/core/adapters.mjs @@ -1,69 +1,128 @@ /** - * 适配器引擎:从 SSoT 真源生成各 AI 工具产物 + * 适配器引擎(v0.3 零配置) * * 设计:纯逻辑层,不直接打印日志(由 commands 层负责)。 - * 可在两种场景使用: - * 1. 在已存在的灵枢项目目录中(运行时通过 import 该项目的 .lingshu/config/adapters.mjs) - * 2. 在 init 阶段对模板渲染(配置作为参数传入) + * + * 零配置约定(取代 v0.2 的 .lingshu/config/adapters.mjs): + * 1. adapter 定义来自内置注册表 src/core/registry.mjs + * 2. SSoT 真源 = 约定发现 reference/rules/*.md(每个文件一个 source) + * 3. directory 型适配器(cursor)的 frontmatter 来自规则文件自身的 frontmatter + * 4. 合并顺序由规则文件 frontmatter 的 `order` 字段决定,缺省按文件名 + * 5. 逃生舱:存在 reference/.lingshu.json 时,可覆盖 adapters / baseline / sources */ import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, readdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; -import { pathToFileURL } from 'node:url'; +import { ADAPTERS, DEFAULT_BASELINE } from './registry.mjs'; + +/** frontmatter 中仅供引擎使用、不输出到产物的保留字段 */ +const RESERVED_META = new Set(['order']); + +/** 判断目录是否为灵枢项目(约定:存在 reference/rules/) */ +export function isLingshuProject(projectRoot) { + return existsSync(join(projectRoot, 'reference/rules')); +} + +/** + * 解析项目的零配置视图:sources + adapters + baseline。 + * 不再依赖任何项目内配置文件;可选 reference/.lingshu.json 做覆盖。 + */ +export function resolveProject(projectRoot) { + if (!isLingshuProject(projectRoot)) { + throw new Error('当前目录不是灵枢项目(未找到 reference/rules/)'); + } -/** 从指定项目根目录加载其 .lingshu/config/adapters.mjs */ -export async function loadConfig(projectRoot) { - const cfgPath = join(projectRoot, '.lingshu/config/adapters.mjs'); - if (!existsSync(cfgPath)) { - throw new Error(`未找到 .lingshu/config/adapters.mjs(不是灵枢项目?)`); + // 约定发现 reference/rules/*.md + const rulesDir = join(projectRoot, 'reference/rules'); + const sources = readdirSync(rulesDir) + .filter((f) => f.endsWith('.md')) + .map((f) => { + const name = f.replace(/\.md$/, ''); + const path = `reference/rules/${f}`; + const { meta } = loadSource(projectRoot, path); + const order = Number(meta.order ?? Number.POSITIVE_INFINITY); + return { name, path, meta, order }; + }) + .sort((a, b) => (a.order - b.order) || a.name.localeCompare(b.name)); + + let adapters = ADAPTERS; + let baseline = DEFAULT_BASELINE; + + // 逃生舱:可选项目级覆盖(自定义工具 / 改基线) + const overridePath = join(projectRoot, 'reference/.lingshu.json'); + if (existsSync(overridePath)) { + try { + const ov = JSON.parse(readFileSync(overridePath, 'utf8')); + if (ov.adapters) adapters = { ...ADAPTERS, ...ov.adapters }; + if (Array.isArray(ov.baseline)) baseline = ov.baseline; + } catch { /* 覆盖文件损坏不阻断,回退默认 */ } } - const mod = await import(pathToFileURL(cfgPath).href); - return { sources: mod.sources, adapters: mod.adapters, baseline: mod.baseline ?? [] }; + + return { sources, adapters, baseline }; } +/** 序列化 frontmatter(YAML 子集),跳过保留字段 */ function renderFrontmatter(meta) { if (!meta) return ''; + const entries = Object.entries(meta).filter(([k]) => !RESERVED_META.has(k)); + if (entries.length === 0) return ''; const lines = ['---']; - for (const [k, v] of Object.entries(meta)) lines.push(`${k}: ${v}`); + for (const [k, v] of entries) lines.push(`${k}: ${v}`); lines.push('---', ''); return lines.join('\n'); } +/** 解析 frontmatter(YAML 子集:顶层 `key: value` 标量),保留插入顺序 */ +function parseFrontmatter(raw) { + const meta = {}; + for (const line of raw.split(/\r?\n/)) { + const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); + if (m) meta[m[1]] = m[2].trim(); + } + return meta; +} + +/** 读取 SSoT 文件,返回 { meta, body }(剥离 frontmatter,防止重复包裹) */ function loadSource(projectRoot, srcPath) { const full = join(projectRoot, srcPath); if (!existsSync(full)) throw new Error(`SSoT 文件缺失: ${srcPath}`); let content = readFileSync(full, 'utf8'); - const m = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/); - if (m) content = content.slice(m[0].length); - return content.replace(/^\s*\n+/, ''); + let meta = {}; + const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/); + if (m) { + meta = parseFrontmatter(m[1]); + content = content.slice(m[0].length); + } + return { meta, body: content.replace(/^\s*\n+/, '') }; } /** * 渲染单个适配器,返回产物列表。 * @returns {Array<{to: string, content: string}>} */ -export function renderAdapter({ projectRoot, toolName, cfg, sources, dryRun = false }) { +export function renderAdapter({ projectRoot, cfg, sources, dryRun = false }) { const out = []; if (cfg.type === 'directory') { const targetDir = join(projectRoot, cfg.target); if (existsSync(targetDir) && !dryRun) { + // 清理与 sources 同名的旧产物(保护用户自定义文件) for (const f of readdirSync(targetDir)) { const stem = f.replace(/\.(md|mdc)$/, ''); - if (sources.find(s => s.name === stem)) { + if (sources.find((s) => s.name === stem)) { rmSync(join(targetDir, f)); } } } for (const src of sources) { - const body = loadSource(projectRoot, src.path); - const fm = cfg.frontmatter?.[src.name]; - const content = (fm ? renderFrontmatter(fm) : '') + body; + const { body } = loadSource(projectRoot, src.path); + const fm = cfg.emitFrontmatter ? renderFrontmatter(src.meta) : ''; + const content = fm + body; const relTarget = cfg.target.replace(/\/?$/, '/') + src.name + cfg.extension; out.push({ to: relTarget, content }); } } else if (cfg.type === 'file') { - const parts = sources.map(src => loadSource(projectRoot, src.path)); + const parts = sources.map((src) => loadSource(projectRoot, src.path).body); const sep = cfg.separator ?? '\n\n---\n\n'; const header = cfg.header ?? ''; const body = (header ? header + sep : '') + parts.join(sep); @@ -82,10 +141,7 @@ export function renderAdapter({ projectRoot, toolName, cfg, sources, dryRun = fa * 1) tools 显式列表 — 完全按列表(--only=...) * 2) baselineOnly=true — 仅 baseline(init / sync --baseline) * 3) all=true — 所有 adapter(sync --all) - * 4) 默认(auto) — baseline 必装 + 已存在产物的 personal 工具 - * - * 第 4 项是常态:用户首次想接入某个 personal 工具,请显式 `--only=` 触发。 - * 之后该工具产物已存在,下次默认 sync 会自动维护它,不需要再传 --only。 + * 4) 默认(auto) — baseline 必装 + 已存在产物的其它工具 * * @param {object} options * @param {string} options.projectRoot @@ -94,8 +150,8 @@ export function renderAdapter({ projectRoot, toolName, cfg, sources, dryRun = fa * @param {boolean} [options.all] * @param {boolean} [options.check] */ -export async function distribute({ projectRoot, tools, baselineOnly, all, check }) { - const { sources, adapters, baseline } = await loadConfig(projectRoot); +export function distribute({ projectRoot, tools, baselineOnly, all, check }) { + const { sources, adapters, baseline } = resolveProject(projectRoot); let targets; if (tools) { @@ -105,12 +161,11 @@ export async function distribute({ projectRoot, tools, baselineOnly, all, check } else if (all) { targets = Object.keys(adapters); } else { - // auto:baseline + 已有产物的 personal + // auto:baseline + 已存在产物的其它工具 targets = Object.keys(adapters).filter((name) => { if (baseline.includes(name)) return true; const cfg = adapters[name]; - const productPath = join(projectRoot, cfg.target); - return existsSync(productPath); + return existsSync(join(projectRoot, cfg.target)); }); } @@ -122,7 +177,7 @@ export async function distribute({ projectRoot, tools, baselineOnly, all, check result.processed.push({ tool, status: 'unknown' }); continue; } - const items = renderAdapter({ projectRoot, toolName: tool, cfg, sources, dryRun: !!check }); + const items = renderAdapter({ projectRoot, cfg, sources, dryRun: !!check }); const itemResults = []; for (const { to, content } of items) { const fullTarget = join(projectRoot, to); diff --git a/src/core/hooks.mjs b/src/core/hooks.mjs new file mode 100644 index 0000000..a41264e --- /dev/null +++ b/src/core/hooks.mjs @@ -0,0 +1,45 @@ +/** + * Git hooks 安装(零侵入) + * + * v0.3 起 hook 内容内置于 CLI,直接写入 .git/hooks/, + * 派生仓不再携带 .lingshu/hooks/ 或 install-hooks 脚本。 + */ + +import { writeFileSync, chmodSync, mkdirSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; + +/** post-merge:git pull/merge 后若规则真源变更,自动用全局 CLI 重新分发 */ +const POST_MERGE = `#!/bin/sh +# 灵枢 post-merge hook(由 lingshu 安装) +# git pull / merge 后,若 reference/rules/ 变更,自动重新分发规则到本地工具。 + +CHANGED=$(git diff HEAD@{1} HEAD --name-only 2>/dev/null) + +if echo "$CHANGED" | grep -qE "^reference/rules/"; then + echo "检测到灵枢规则变更,正在重新分发..." + lingshu sync +fi +`; + +const HOOKS = { 'post-merge': POST_MERGE }; + +/** + * 把内置 hooks 写入 /.git/hooks/。 + * @returns {{installed: string[], skipped: boolean}} + */ +export function installHooks(projectRoot) { + const gitDir = join(projectRoot, '.git'); + if (!existsSync(gitDir)) return { installed: [], skipped: true }; + + const hooksDir = join(gitDir, 'hooks'); + mkdirSync(hooksDir, { recursive: true }); + + const installed = []; + for (const [name, content] of Object.entries(HOOKS)) { + const dst = join(hooksDir, name); + writeFileSync(dst, content, 'utf8'); + try { chmodSync(dst, 0o755); } catch { /* Windows 下忽略权限错误 */ } + installed.push(name); + } + return { installed, skipped: false }; +} diff --git a/src/core/registry.mjs b/src/core/registry.mjs new file mode 100644 index 0000000..3aba54b --- /dev/null +++ b/src/core/registry.mjs @@ -0,0 +1,76 @@ +/** + * 内置 AI 工具适配器注册表(零配置) + * + * v0.3 起,adapter 定义内置于 CLI,派生仓不再携带 .lingshu/config/adapters.mjs。 + * 同步引擎从此读本注册表 + 约定发现 reference/rules/*.md,无需任何项目配置文件。 + * + * 适配器类型: + * - 'directory':每个 source 在目标目录下生成独立文件(Cursor/Trae/Qoder/Antigravity) + * - 'file':所有 source 合并为单一文件(Claude Code 的 CLAUDE.md、Codex 的 AGENTS.md) + * + * emitFrontmatter:directory 型适配器是否输出规则文件自身的 frontmatter + * (仅 Cursor 的 .mdc 需要;其余 .md 仅输出正文)。 + */ + +/** 单文件型产物的统一头部 */ +function fileHeader(title) { + return [ + '', + '', + '', + '', + `# 灵枢 (LingShu) — ${title}`, + '', + '> 本文件由 `lingshu sync` 自动生成。所有规则的真理来源位于 `reference/rules/`。', + '', + ].join('\n'); +} + +/** 内置适配器矩阵 */ +export const ADAPTERS = { + cursor: { + type: 'directory', + target: '.cursor/rules/', + extension: '.mdc', + emitFrontmatter: true, + }, + + trae: { + type: 'directory', + target: '.trae/rules/', + extension: '.md', + }, + + qoder: { + type: 'directory', + target: '.qoder/rules/', + extension: '.md', + }, + + antigravity: { + type: 'directory', + target: '.agent/rules/', + extension: '.md', + }, + + 'claude-code': { + type: 'file', + target: 'CLAUDE.md', + header: fileHeader('Claude Code 项目指令'), + separator: '\n\n---\n\n', + }, + + codex: { + type: 'file', + target: 'AGENTS.md', + header: fileHeader('AI Agents 项目指令'), + separator: '\n\n---\n\n', + }, +}; + +/** + * 默认基线工具(团队产物入库,保证克隆即用)。 + * 注意:项目实际「入库 vs 忽略」以 .gitignore 为准(见 `lingshu tool track/untrack`); + * 本常量仅用于 auto 同步时「始终生成」的工具集合的兜底默认。 + */ +export const DEFAULT_BASELINE = ['claude-code', 'codex']; diff --git a/templates/default/.github/workflows/rules-consistency.yml b/templates/default/.github/workflows/rules-consistency.yml index 15e11d0..fb624ce 100644 --- a/templates/default/.github/workflows/rules-consistency.yml +++ b/templates/default/.github/workflows/rules-consistency.yml @@ -3,6 +3,7 @@ # ========================================== # 防止 reference/rules/ 真源与基线工具产物(CLAUDE.md / AGENTS.md)发生漂移。 # 仅校验 baseline 工具,因为只有它们入库;个人偏好工具产物在 .gitignore 中。 +# v0.3 零侵入:通过 npx 调用全局 CLI,仓库内不再携带同步引擎。 name: rules-consistency @@ -12,7 +13,6 @@ on: branches: [master, main] paths: - 'reference/rules/**' - - '.lingshu/**' - 'CLAUDE.md' - 'AGENTS.md' @@ -26,7 +26,7 @@ jobs: with: node-version: '20' - name: 校验 SSoT 与基线产物一致性 - run: node .lingshu/scripts/sync-rules.mjs --check --baseline + run: npx -y @ruobai/lingshu sync --check --baseline doctor: name: 架构健康度体检 @@ -38,4 +38,4 @@ jobs: with: node-version: '20' - name: 运行 doctor - run: node .lingshu/scripts/doctor.mjs + run: npx -y @ruobai/lingshu doctor diff --git a/templates/default/.lingshu/README.md b/templates/default/.lingshu/README.md deleted file mode 100644 index 881d031..0000000 --- a/templates/default/.lingshu/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# .lingshu/ — 项目元数据空间 - -> 本目录是 **灵枢架构** 的"项目内核",独立于任何 AI 工具,承载项目级脚本与配置。 - -## 目录结构 - -``` -.lingshu/ -├── config/ -│ └── adapters.mjs # AI 工具适配清单(SSoT → 各工具产物的映射) -├── scripts/ -│ ├── sync-rules.mjs # 规则分发脚本(核心) -│ ├── doctor.mjs # 架构健康检查 -│ └── install-hooks.mjs # 安装 git hooks -└── hooks/ - └── post-merge # 拉取后自动同步规则 -``` - -## 命名说明 - -`.lingshu/` 不是任何 AI 工具的目录(区别于 `.cursor/`、`.trae/`、`.qoder/`、`.agent/`), -而是 **灵枢架构自身** 的元数据空间,故名 `.lingshu`。 - -## 常用命令 - -```bash -npm run sync # 分发规则到所有 AI 工具目录 -npm run sync:check # 仅校验一致性(用于 CI) -npm run sync -- --only=cursor,codex # 仅同步指定工具 -npm run sync -- --baseline # 仅同步基线工具 -npm run doctor # 运行架构健康检查 -npm run hooks:install # 安装/更新 git hooks -``` - -## 工作原理 - -``` - ┌──────────────────────────────────┐ - │ reference/rules/ (SSoT 真源) │ - │ ├── ai-behavior.md │ - │ └── lingshu-core.md │ - └─────────────┬────────────────────┘ - │ - │ sync-rules.mjs - │ (按 adapters.mjs 配置) - ▼ - ┌──────────────────┴──────────────────┐ - │ 生成产物 (artifact) │ - ├──────────────────────────────────────┤ - │ ✅ baseline (入库): │ - │ - CLAUDE.md (Claude Code) │ - │ - AGENTS.md (Codex) │ - │ │ - │ ❌ personal (gitignore): │ - │ - .cursor/rules/ │ - │ - .trae/rules/ │ - │ - .qoder/rules/ │ - │ - .agent/rules/ │ - └──────────────────────────────────────┘ -``` - -- **真源唯一**:所有规则改动只能在 `reference/rules/` 进行 -- **基线工具入库**:保证团队成员克隆即用(无需先跑脚本) -- **个人偏好工具**:本地生成,不污染 git -- **CI 守护**:GitHub Actions 校验入库产物与真源的一致性 -- **Hook 自动化**:`git pull` 后 post-merge 自动重新分发 diff --git a/templates/default/.lingshu/config/adapters.mjs b/templates/default/.lingshu/config/adapters.mjs deleted file mode 100644 index b6b0b9b..0000000 --- a/templates/default/.lingshu/config/adapters.mjs +++ /dev/null @@ -1,95 +0,0 @@ -/** - * 灵枢 AI 工具适配清单 - * - * 职责:定义 SSoT 真源 → 各 AI 工具产物的映射规则。 - * 修改本文件后执行 `npm run sync` 重新分发。 - * - * 适配器类型: - * - 'directory':每个 source 在目标目录下生成一个独立文件(适用 Cursor/Trae/Qoder/Antigravity) - * - 'file':所有 source 合并为单一文件(适用 Claude Code 的 CLAUDE.md、Codex 的 AGENTS.md) - */ - -/** SSoT 真源文件清单 */ -export const sources = [ - { name: 'lingshu-core', path: 'reference/rules/lingshu-core.md' }, - { name: 'ai-behavior', path: 'reference/rules/ai-behavior.md' }, -]; - -/** 各 AI 工具适配器 */ -export const adapters = { - cursor: { - type: 'directory', - target: '.cursor/rules/', - extension: '.mdc', - frontmatter: { - 'ai-behavior': { - name: 'ai-behavior', - description: '灵枢智能体行为准则:Plan 模式存档、真理同步流与原子化交付', - globs: '**/*', - trigger: 'always_on', - }, - 'lingshu-core': { - name: 'lingshu-core', - description: '灵枢架构核心准则:脑体解耦、Git 物理隔离与路径映射', - globs: '**/*', - trigger: 'always_on', - }, - }, - }, - - trae: { - type: 'directory', - target: '.trae/rules/', - extension: '.md', - }, - - qoder: { - type: 'directory', - target: '.qoder/rules/', - extension: '.md', - }, - - antigravity: { - type: 'directory', - target: '.agent/rules/', - extension: '.md', - }, - - 'claude-code': { - type: 'file', - target: 'CLAUDE.md', - header: [ - '', - '', - '', - '', - '# 灵枢 (LingShu) — Claude Code 项目指令', - '', - '> 本文件由灵枢分发脚本自动生成。所有规则的真理来源位于 `reference/rules/`。', - '', - ].join('\n'), - separator: '\n\n---\n\n', - }, - - codex: { - type: 'file', - target: 'AGENTS.md', - header: [ - '', - '', - '', - '', - '# 灵枢 (LingShu) — AI Agents 项目指令', - '', - '> 本文件由灵枢分发脚本自动生成。所有规则的真理来源位于 `reference/rules/`。', - '', - ].join('\n'), - separator: '\n\n---\n\n', - }, -}; - -/** - * 团队基线工具:默认入库(产物受 git 追踪,保证克隆即用) - * 个人偏好工具:默认 ignore(每位开发者本地生成,互不干扰) - */ -export const baseline = ['claude-code', 'codex']; diff --git a/templates/default/.lingshu/hooks/post-merge b/templates/default/.lingshu/hooks/post-merge deleted file mode 100644 index cc6ca1d..0000000 --- a/templates/default/.lingshu/hooks/post-merge +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -# 灵枢 post-merge hook -# 当 git pull / git merge 后,若 SSoT 或适配器配置变更,自动重新分发规则到本地工具。 - -CHANGED=$(git diff HEAD@{1} HEAD --name-only 2>/dev/null) - -if echo "$CHANGED" | grep -qE "^(reference/rules/|\.lingshu/config/)"; then - echo "检测到灵枢规则变更,正在重新分发..." - node .lingshu/scripts/sync-rules.mjs -fi diff --git a/templates/default/.lingshu/scripts/doctor.mjs b/templates/default/.lingshu/scripts/doctor.mjs deleted file mode 100644 index ed7f88c..0000000 --- a/templates/default/.lingshu/scripts/doctor.mjs +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env node -/** - * 灵枢架构健康检查(跨平台) - * 替代 .agent/workflows/self-audit.md 的 PowerShell 版本,可在 Windows / macOS / Linux 运行。 - */ - -import { existsSync, readFileSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const ROOT = resolve(__dirname, '../..'); - -const c = { - green: s => `\x1b[32m${s}\x1b[0m`, - red: s => `\x1b[31m${s}\x1b[0m`, - yellow: s => `\x1b[33m${s}\x1b[0m`, - cyan: s => `\x1b[36m${s}\x1b[0m`, - dim: s => `\x1b[2m${s}\x1b[0m`, - bold: s => `\x1b[1m${s}\x1b[0m`, -}; - -let errors = 0, warnings = 0; -const ok = m => console.log(c.green(` ✓ ${m}`)); -const warn = m => { console.log(c.yellow(` ⚠ ${m}`)); warnings++; }; -const fail = m => { console.log(c.red (` ✗ ${m}`)); errors++; }; - -console.log(c.cyan(c.bold('\n灵枢架构健康检查\n'))); - -// 1. 物理完整性 -console.log(c.cyan('[1/4] 物理完整性')); -const requiredDirs = [ - 'reference/rules', - 'reference/docs', - 'reference/management/plans', - 'reference/management/tasks', - 'reference/management/walkthroughs', - 'reference/management/reports', - '.lingshu/scripts', - '.lingshu/config', -]; -for (const d of requiredDirs) { - if (existsSync(join(ROOT, d))) ok(d); - else fail(`${d} 缺失`); -} - -// 2. SSoT 真源 -console.log(c.cyan('\n[2/4] SSoT 真源完整性')); -const ssotFiles = [ - 'reference/rules/ai-behavior.md', - 'reference/rules/lingshu-core.md', -]; -for (const f of ssotFiles) { - if (existsSync(join(ROOT, f))) ok(f); - else fail(`${f} 缺失`); -} - -// 3. 灵枢宣言 -console.log(c.cyan('\n[3/4] 灵枢宣言注入')); -const manifesto = join(ROOT, 'reference/README.md'); -if (existsSync(manifesto)) { - const txt = readFileSync(manifesto, 'utf8'); - if (txt.includes('中枢一动,全栈皆通')) ok('灵枢宣言已注入'); - else warn('reference/README.md 缺少核心宣言口号'); -} else { - fail('reference/README.md (灵枢宣言) 缺失'); -} - -// 4. 规则一致性提示 -console.log(c.cyan('\n[4/4] 规则一致性')); -console.log(c.dim(' (调用 sync-rules --check 进行严格校验)')); -console.log(c.dim(' 推荐执行: npm run sync:check\n')); - -// 总结 -if (errors === 0 && warnings === 0) { - console.log(c.green(c.bold('✅ 灵枢架构健康度:优秀\n'))); -} else if (errors === 0) { - console.log(c.yellow(c.bold(`⚠️ 通过,但有 ${warnings} 处提醒\n`))); -} else { - console.log(c.red(c.bold(`❌ 失败:${errors} 处错误,${warnings} 处提醒\n`))); - process.exit(1); -} diff --git a/templates/default/.lingshu/scripts/install-hooks.mjs b/templates/default/.lingshu/scripts/install-hooks.mjs deleted file mode 100644 index 0531827..0000000 --- a/templates/default/.lingshu/scripts/install-hooks.mjs +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -/** - * 安装灵枢 Git Hooks(跨平台) - * - * 将 .lingshu/hooks/ 下的脚本复制到 .git/hooks/,并赋予执行权限。 - * 用法: node .lingshu/scripts/install-hooks.mjs - */ - -import { copyFileSync, chmodSync, existsSync, readdirSync, mkdirSync, statSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const ROOT = resolve(__dirname, '../..'); - -const HOOKS_SRC = join(ROOT, '.lingshu/hooks'); -const HOOKS_DST = join(ROOT, '.git/hooks'); - -if (!existsSync(join(ROOT, '.git'))) { - console.log('ℹ️ 跳过 hooks 安装:当前目录不是 git 仓库'); - process.exit(0); -} - -if (!existsSync(HOOKS_SRC)) { - console.log('ℹ️ 跳过 hooks 安装:.lingshu/hooks/ 不存在'); - process.exit(0); -} - -mkdirSync(HOOKS_DST, { recursive: true }); - -let installed = 0; -for (const hook of readdirSync(HOOKS_SRC)) { - const src = join(HOOKS_SRC, hook); - if (!statSync(src).isFile()) continue; - const dst = join(HOOKS_DST, hook); - copyFileSync(src, dst); - try { chmodSync(dst, 0o755); } catch { /* Windows 下忽略权限错误 */ } - console.log(` ✓ 已安装 hook: ${hook}`); - installed++; -} - -console.log(`\n✅ 共安装 ${installed} 个 git hook\n`); diff --git a/templates/default/.lingshu/scripts/sync-rules.mjs b/templates/default/.lingshu/scripts/sync-rules.mjs deleted file mode 100644 index 84f714b..0000000 --- a/templates/default/.lingshu/scripts/sync-rules.mjs +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env node -/** - * 灵枢规则分发脚本 - * - * 用法: - * node .lingshu/scripts/sync-rules.mjs # 默认 = baseline + 已存在产物的 personal - * node .lingshu/scripts/sync-rules.mjs --all # 全部工具 - * node .lingshu/scripts/sync-rules.mjs --baseline # 仅基线 - * node .lingshu/scripts/sync-rules.mjs --only=cursor,codex # 仅特定工具 - * node .lingshu/scripts/sync-rules.mjs --check # 仅校验,不写入(用于 CI) - * - * 跨平台:纯 Node.js 内置模块,零依赖。 - */ - -import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, readdirSync } from 'node:fs'; -import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { adapters, sources, baseline } from '../config/adapters.mjs'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const ROOT = resolve(__dirname, '../..'); - -const args = process.argv.slice(2); -const isCheck = args.includes('--check'); -const onlyBaseline = args.includes('--baseline'); -const onlyAll = args.includes('--all'); -const onlyArg = args.find(a => a.startsWith('--only=')); -const onlyTools = onlyArg ? onlyArg.slice('--only='.length).split(',').map(s => s.trim()).filter(Boolean) : null; - -const c = { - green: s => `\x1b[32m${s}\x1b[0m`, - red: s => `\x1b[31m${s}\x1b[0m`, - yellow: s => `\x1b[33m${s}\x1b[0m`, - cyan: s => `\x1b[36m${s}\x1b[0m`, - dim: s => `\x1b[2m${s}\x1b[0m`, - bold: s => `\x1b[1m${s}\x1b[0m`, -}; - -/** 序列化 frontmatter(YAML 子集) */ -function renderFrontmatter(meta) { - if (!meta) return ''; - const lines = ['---']; - for (const [k, v] of Object.entries(meta)) { - lines.push(`${k}: ${v}`); - } - lines.push('---', ''); - return lines.join('\n'); -} - -/** 读取 SSoT 文件,剥除已有 frontmatter(防止重复包裹) */ -function loadSource(srcPath) { - const full = join(ROOT, srcPath); - if (!existsSync(full)) { - throw new Error(`SSoT 文件缺失: ${srcPath}`); - } - let content = readFileSync(full, 'utf8'); - if (content.startsWith('---\n') || content.startsWith('---\r\n')) { - const m = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/); - if (m) content = content.slice(m[0].length); - } - return content.replace(/^\s*\n+/, ''); -} - -/** 渲染单个适配器,返回 [{ to, content }] 列表 */ -function renderAdapter(toolName, cfg) { - const out = []; - - if (cfg.type === 'directory') { - const targetDir = join(ROOT, cfg.target); - - // 清理与 sources 同名的旧产物(保护用户自定义文件) - if (existsSync(targetDir)) { - for (const f of readdirSync(targetDir)) { - const stem = f.replace(/\.(md|mdc)$/, ''); - if (sources.find(s => s.name === stem)) { - if (!isCheck) rmSync(join(targetDir, f)); - } - } - } - - for (const src of sources) { - const body = loadSource(src.path); - const fm = cfg.frontmatter?.[src.name]; - const content = (fm ? renderFrontmatter(fm) : '') + body; - const relTarget = cfg.target.replace(/\/?$/, '/') + src.name + cfg.extension; - out.push({ to: relTarget, content }); - } - } else if (cfg.type === 'file') { - const parts = sources.map(src => loadSource(src.path)); - const sep = cfg.separator ?? '\n\n---\n\n'; - const header = cfg.header ?? ''; - const body = (header ? header + sep : '') + parts.join(sep); - out.push({ to: cfg.target, content: body }); - } else { - throw new Error(`未知适配器类型: ${cfg.type}`); - } - - return out; -} - -function decideTargets() { - if (onlyTools) return onlyTools; - if (onlyBaseline) return baseline; - if (onlyAll) return Object.keys(adapters); - // auto:baseline + 已存在产物的 personal - return Object.keys(adapters).filter((name) => { - if (baseline.includes(name)) return true; - const cfg = adapters[name]; - return existsSync(join(ROOT, cfg.target)); - }); -} - -function main() { - const targets = decideTargets(); - let exitCode = 0; - let writes = 0, drifts = 0, missing = 0; - - console.log(c.cyan(c.bold('\n灵枢规则分发'))); - console.log(c.dim(` 模式: ${isCheck ? 'CHECK(仅校验)' : 'SYNC(生成产物)'}`)); - console.log(c.dim(` 根目录: ${ROOT}`)); - console.log(c.dim(` 目标工具: ${targets.join(', ')}\n`)); - - for (const tool of targets) { - const cfg = adapters[tool]; - if (!cfg) { - console.log(c.yellow(` ⚠ 未知工具: ${tool}(跳过)`)); - continue; - } - - const tag = baseline.includes(tool) ? c.green('[baseline]') : c.dim('[personal]'); - console.log(`${tag} ${c.bold(tool)}:`); - - try { - const items = renderAdapter(tool, cfg); - for (const { to, content } of items) { - const fullTarget = join(ROOT, to); - - if (isCheck) { - if (!existsSync(fullTarget)) { - console.log(c.red(` ✗ 缺失: ${to}`)); - missing++; exitCode = 1; - } else { - const existing = readFileSync(fullTarget, 'utf8'); - if (existing !== content) { - console.log(c.red(` ✗ 漂移: ${to}`)); - drifts++; exitCode = 1; - } else { - console.log(c.green(` ✓ ${to}`)); - } - } - } else { - mkdirSync(dirname(fullTarget), { recursive: true }); - writeFileSync(fullTarget, content, 'utf8'); - console.log(c.green(` ✓ ${to}`)); - writes++; - } - } - } catch (e) { - console.log(c.red(` ✗ 错误: ${e.message}`)); - exitCode = 1; - } - } - - console.log(''); - if (isCheck) { - if (exitCode === 0) { - console.log(c.green(c.bold('✅ 一致性校验通过\n'))); - } else { - console.log(c.red(c.bold(`❌ 一致性校验失败(缺失 ${missing},漂移 ${drifts})`))); - console.log(c.yellow(' 请运行 `npm run sync` 重新生成,并提交基线工具产物。\n')); - } - } else { - console.log(c.green(c.bold(`✅ 规则分发完成(共写入 ${writes} 个文件)\n`))); - } - - process.exit(exitCode); -} - -main(); diff --git a/templates/default/AGENTS.md b/templates/default/AGENTS.md deleted file mode 100644 index 7e0bd9c..0000000 --- a/templates/default/AGENTS.md +++ /dev/null @@ -1,105 +0,0 @@ - - - - -# 灵枢 (LingShu) — AI Agents 项目指令 - -> 本文件由灵枢分发脚本自动生成。所有规则的真理来源位于 `reference/rules/`。 - - ---- - -# 灵枢架构核心准则 (LingShu Core Principles) - -## 1. 架构拓扑定义 (Architecture Topology) -本仓库遵循 **LingShu** 架构,实行"逻辑中枢"与"执行肢体"的物理分离。 - -- **🧠 中枢仓 (The Brain)**: - - **路径**: 当前根目录 `./` - - **职责**: 定义真理 (Reference)、规则 (Rules)、计划 (Plans) 与审计 (Audit)。 - - **特征**: 仅追踪文档与配置,不应包含业务源代码。 - -- **💪 肢体仓 (The Limbs)**: - - **路径**: 根目录下的具体工程子目录(通常命名为 `*-server`, `*-ui`, `*-mobile`, `*-web`)。 - - **职责**: 承载具体的业务代码实现。 - - **识别规则**: AI 需自动扫描根目录,识别包含 `.git` (子模块/嵌套仓) 或具体语言配置(如 `pyproject.toml`, `package.json`)的子文件夹作为"肢体"。 - -## 2. Git 物理隔离规则 (Git Isolation Rules) -由于采用嵌套仓库结构,必须严格遵守以下操作边界: - -- **🚫 禁止根目录通配提交**: - - **严禁** 在根目录执行 `git add .` 或 `git commit -a`,这会导致肢体仓的代码被错误地纳入中枢仓版本控制。 - - 根目录 Git **仅允许** 追踪:`reference/`, `.lingshu/`, AI 工具基线产物(`CLAUDE.md`, `AGENTS.md`),以及 `reference/management/` 下的任务文档。 - -- **✅ 肢体仓独立提交**: - - 修改具体业务代码后,必须显式 `cd [limb-folder_name]/` 进入子目录。 - - 确认 `git status` 显示的是子仓库的状态后,再执行提交。 - -- **🛡️ 提交前自检 (Pre-commit Check)**: - - AI 在生成 Git 指令前,必须判断当前变更的文件路径。 - - 若路径属于 `*-server/` 或 `*-ui/`,必须输出 `cd` 指令作为前置操作。 - -## 3. 规则真源约束 (SSoT for Rules) - -- **真源唯一**: 所有 AI 行为规则的真源位于 `reference/rules/`。 -- **产物只读**: `.cursor/rules/`、`.trae/rules/`、`.qoder/rules/`、`.agent/rules/`、`CLAUDE.md`、`AGENTS.md` 均为 **由 `.lingshu/scripts/sync-rules.mjs` 自动生成的产物**,禁止手动编辑。 -- **变更流程**: 规则修改必须改动 `reference/rules/` 真源,再执行 `npm run sync` 重新分发。 -- **CI 保障**: GitHub Actions 会校验入库产物与真源的一致性,防止漂移。 - -## 4. 环境与依赖标准 (Stack Standard) -*(注:此部分定义模版默认技术栈,可随项目调整)* -- **Python 环境**: 推荐使用 `uv` 或 `pip` 管理依赖。 - - Windows 特殊指令: `uv` 相关命令须带 `--link-mode copy --no-install-project`。 -- **Node 环境**: 推荐使用 `npm` 管理依赖与脚本(团队统一标准)。 -- **交互语言**: 始终使用 **简体中文**。 - ---- - - ---- - -# 🤖 灵枢智能体行为准则 (Agentic Workflow) - -## 1. 思考模式:真理驱动 (Truth Driven) -在 **Ask 模式** 或 **Composer 模式** 中,遵循以下逻辑链: - -1. **🔍 寻源 (Locate Truth)**: - - 遇到需求变更,**首先** 检查 `reference/docs/` 下的 API 契约、数据库 Schema 或 PRD。 - - 若文档未定义,先建议用户更新文档,而不是直接写代码。 - - **口号**: "逻辑不入中枢,代码不动分毫。" - -2. **📝 规划 (Planning)**: - - 进入 Composer (Plan) 模式后,必须生成/更新 `.plan.md`。 - - **强制存档 (CRITICAL)**: 在执行代码修改前,必须将当前的 `.plan.md` 完整复制备份到 `reference/management/plans/` 目录下,并更新 `reference/management/tasks/` 中的执行清单。文件名格式建议:`YYYYMMDD-TaskName.md`。 - -3. **⚡ 执行 (Execution)**: - - **跨端同步**: 始终检查后端变更对前端的影响(如 API 字段变动),并主动提示同步修改。 - - **原子化**: 每次只处理一个具体的逻辑闭环,避免跨多个不相关的功能模块修改。 - -## 2. 自动化归档守卫 (Archival Guard) -每当一个任务进入 **Done** 状态,AI 必须强制执行以下同步操作: - -1. **方案固化**: 将最终修订的 `.plan.md` 完整备份至 `reference/management/plans/`。 -2. **存证生成**: 生成一份详尽的 `Walkthrough`,记录本次重构的逻辑决策、核心代码变更及验证结果,存放至 `reference/management/walkthroughs/`。 -3. **任务闭环**: 更新 `reference/management/tasks/` 中的执行记录,标注所有步骤已完成。 -4. **专项审计 (Conditional)**: - - 若本次任务涉及 **数据库变更** (Schema/Migration),必须在 `reference/management/reports/` 中生成一份 `Migration_Report`。 - - 文件命名标准: `YYYYMMDD_ID_TaskName.[plan|tasks|walkthrough|report].md` - -## 3. 交付规约 (Delivery Protocol) - -- **交互语言**: 始终使用 **简体中文** 进行对话回复。 -- **Git 提交信息格式**: - - **语言**: 必须使用 **简体中文** 描述变更。 - - **中枢仓**: `docs: <描述>`, `chore: <描述>`, `plan: <描述>` - - *示例*: `docs: 更新发票 API 契约` - - **肢体仓**: `(): <描述>` - - *示例*: `feat(storage): 实现文件生命周期管理` - -- **分支管理**: - - 开发工作必须在 `feat/*` 或 `refactor/*` 分支进行,严禁直推 `master/main`。 - -- **自我修正 (Self-Correction)**: - - 如果用户指出代码与文档不符,**必须** 优先以文档(中枢真理)为准进行修正,或者询问用户是否需要反向更新文档。 - ---- diff --git a/templates/default/CLAUDE.md b/templates/default/CLAUDE.md deleted file mode 100644 index 0aaf927..0000000 --- a/templates/default/CLAUDE.md +++ /dev/null @@ -1,105 +0,0 @@ - - - - -# 灵枢 (LingShu) — Claude Code 项目指令 - -> 本文件由灵枢分发脚本自动生成。所有规则的真理来源位于 `reference/rules/`。 - - ---- - -# 灵枢架构核心准则 (LingShu Core Principles) - -## 1. 架构拓扑定义 (Architecture Topology) -本仓库遵循 **LingShu** 架构,实行"逻辑中枢"与"执行肢体"的物理分离。 - -- **🧠 中枢仓 (The Brain)**: - - **路径**: 当前根目录 `./` - - **职责**: 定义真理 (Reference)、规则 (Rules)、计划 (Plans) 与审计 (Audit)。 - - **特征**: 仅追踪文档与配置,不应包含业务源代码。 - -- **💪 肢体仓 (The Limbs)**: - - **路径**: 根目录下的具体工程子目录(通常命名为 `*-server`, `*-ui`, `*-mobile`, `*-web`)。 - - **职责**: 承载具体的业务代码实现。 - - **识别规则**: AI 需自动扫描根目录,识别包含 `.git` (子模块/嵌套仓) 或具体语言配置(如 `pyproject.toml`, `package.json`)的子文件夹作为"肢体"。 - -## 2. Git 物理隔离规则 (Git Isolation Rules) -由于采用嵌套仓库结构,必须严格遵守以下操作边界: - -- **🚫 禁止根目录通配提交**: - - **严禁** 在根目录执行 `git add .` 或 `git commit -a`,这会导致肢体仓的代码被错误地纳入中枢仓版本控制。 - - 根目录 Git **仅允许** 追踪:`reference/`, `.lingshu/`, AI 工具基线产物(`CLAUDE.md`, `AGENTS.md`),以及 `reference/management/` 下的任务文档。 - -- **✅ 肢体仓独立提交**: - - 修改具体业务代码后,必须显式 `cd [limb-folder_name]/` 进入子目录。 - - 确认 `git status` 显示的是子仓库的状态后,再执行提交。 - -- **🛡️ 提交前自检 (Pre-commit Check)**: - - AI 在生成 Git 指令前,必须判断当前变更的文件路径。 - - 若路径属于 `*-server/` 或 `*-ui/`,必须输出 `cd` 指令作为前置操作。 - -## 3. 规则真源约束 (SSoT for Rules) - -- **真源唯一**: 所有 AI 行为规则的真源位于 `reference/rules/`。 -- **产物只读**: `.cursor/rules/`、`.trae/rules/`、`.qoder/rules/`、`.agent/rules/`、`CLAUDE.md`、`AGENTS.md` 均为 **由 `.lingshu/scripts/sync-rules.mjs` 自动生成的产物**,禁止手动编辑。 -- **变更流程**: 规则修改必须改动 `reference/rules/` 真源,再执行 `npm run sync` 重新分发。 -- **CI 保障**: GitHub Actions 会校验入库产物与真源的一致性,防止漂移。 - -## 4. 环境与依赖标准 (Stack Standard) -*(注:此部分定义模版默认技术栈,可随项目调整)* -- **Python 环境**: 推荐使用 `uv` 或 `pip` 管理依赖。 - - Windows 特殊指令: `uv` 相关命令须带 `--link-mode copy --no-install-project`。 -- **Node 环境**: 推荐使用 `npm` 管理依赖与脚本(团队统一标准)。 -- **交互语言**: 始终使用 **简体中文**。 - ---- - - ---- - -# 🤖 灵枢智能体行为准则 (Agentic Workflow) - -## 1. 思考模式:真理驱动 (Truth Driven) -在 **Ask 模式** 或 **Composer 模式** 中,遵循以下逻辑链: - -1. **🔍 寻源 (Locate Truth)**: - - 遇到需求变更,**首先** 检查 `reference/docs/` 下的 API 契约、数据库 Schema 或 PRD。 - - 若文档未定义,先建议用户更新文档,而不是直接写代码。 - - **口号**: "逻辑不入中枢,代码不动分毫。" - -2. **📝 规划 (Planning)**: - - 进入 Composer (Plan) 模式后,必须生成/更新 `.plan.md`。 - - **强制存档 (CRITICAL)**: 在执行代码修改前,必须将当前的 `.plan.md` 完整复制备份到 `reference/management/plans/` 目录下,并更新 `reference/management/tasks/` 中的执行清单。文件名格式建议:`YYYYMMDD-TaskName.md`。 - -3. **⚡ 执行 (Execution)**: - - **跨端同步**: 始终检查后端变更对前端的影响(如 API 字段变动),并主动提示同步修改。 - - **原子化**: 每次只处理一个具体的逻辑闭环,避免跨多个不相关的功能模块修改。 - -## 2. 自动化归档守卫 (Archival Guard) -每当一个任务进入 **Done** 状态,AI 必须强制执行以下同步操作: - -1. **方案固化**: 将最终修订的 `.plan.md` 完整备份至 `reference/management/plans/`。 -2. **存证生成**: 生成一份详尽的 `Walkthrough`,记录本次重构的逻辑决策、核心代码变更及验证结果,存放至 `reference/management/walkthroughs/`。 -3. **任务闭环**: 更新 `reference/management/tasks/` 中的执行记录,标注所有步骤已完成。 -4. **专项审计 (Conditional)**: - - 若本次任务涉及 **数据库变更** (Schema/Migration),必须在 `reference/management/reports/` 中生成一份 `Migration_Report`。 - - 文件命名标准: `YYYYMMDD_ID_TaskName.[plan|tasks|walkthrough|report].md` - -## 3. 交付规约 (Delivery Protocol) - -- **交互语言**: 始终使用 **简体中文** 进行对话回复。 -- **Git 提交信息格式**: - - **语言**: 必须使用 **简体中文** 描述变更。 - - **中枢仓**: `docs: <描述>`, `chore: <描述>`, `plan: <描述>` - - *示例*: `docs: 更新发票 API 契约` - - **肢体仓**: `(): <描述>` - - *示例*: `feat(storage): 实现文件生命周期管理` - -- **分支管理**: - - 开发工作必须在 `feat/*` 或 `refactor/*` 分支进行,严禁直推 `master/main`。 - -- **自我修正 (Self-Correction)**: - - 如果用户指出代码与文档不符,**必须** 优先以文档(中枢真理)为准进行修正,或者询问用户是否需要反向更新文档。 - ---- diff --git a/templates/default/README.md b/templates/default/README.md index 60699f0..812f1fd 100644 --- a/templates/default/README.md +++ b/templates/default/README.md @@ -23,7 +23,7 @@ | 🧠 **中枢-肢体解耦** | 文档/规则集中于中枢仓,业务代码分散于嵌套肢体仓 | | 📜 **规则 SSoT** | 所有 AI 行为规则统一写在 `reference/rules/`,避免多副本漂移 | | 🤖 **多 AI 工具适配** | 一处定义,自动分发至 6 大主流 AI 编码工具 | -| 🌍 **跨平台脚本** | 纯 Node.js 实现,Win/macOS/Linux 通用 | +| 🪶 **零侵入** | 同步引擎在全局 CLI 内,仓库只留治理资产,无 `.lingshu/`、无 `package.json` | | 🛡️ **CI 守护** | GitHub Actions 自动校验真源与产物的一致性 | | ⚙️ **拉取自动重分发** | `git pull` 后 post-merge hook 自动重新分发规则 | @@ -40,8 +40,8 @@ | Qoder | `.qoder/rules/*.md` | ❌ | 个人偏好 | | Antigravity | `.agent/rules/*.md` | ❌ | 个人偏好 | -> **基线工具** 产物入库,保证团队成员克隆即用;**个人偏好工具** 由各开发者本地按需生成。 -> 新增工具支持:仅需在 `.lingshu/config/adapters.mjs` 添加一项配置。 +> **是否入库由 `.gitignore` 决定**,可用 `lingshu tool track/untrack <工具>` 调整。 +> 内置工具不满足需求时,可在 `reference/.lingshu.json` 声明自定义适配器(可选逃生舱)。 --- @@ -49,19 +49,9 @@ ```text . -├── .lingshu/ # 项目元数据空间(不属于任何 AI 工具) -│ ├── config/ -│ │ └── adapters.mjs # AI 工具适配清单 -│ ├── scripts/ -│ │ ├── sync-rules.mjs # 规则分发(核心) -│ │ ├── doctor.mjs # 架构健康检查 -│ │ └── install-hooks.mjs # Git hooks 安装器 -│ └── hooks/ -│ └── post-merge # 拉取后自动同步规则 -│ -├── reference/ # 真源 (Reference) +├── reference/ # 真源 (Reference) — 中枢的全部资产 │ ├── rules/ # AI 规则 SSoT -│ │ ├── lingshu-core.md # 架构核心宪法 +│ │ ├── lingshu-core.md # 架构核心准则 │ │ └── ai-behavior.md # 智能体行为准则 │ ├── docs/ # 静态真理:PRD、契约、架构 │ ├── experience/ # 经验复利:高分 Prompt、避坑笔记 @@ -71,28 +61,29 @@ │ ├── walkthroughs/ # 存证:逻辑决策、代码演练 │ └── reports/ # 审计:汇总报告、数据库变更 │ -├── CLAUDE.md # Claude Code 入口(基线,自动生成) -├── AGENTS.md # Codex / 通用 Agents 入口(基线,自动生成) +├── CLAUDE.md # Claude Code 入口(基线,由 lingshu sync 生成) +├── AGENTS.md # Codex / 通用 Agents 入口(基线,由 lingshu sync 生成) │ -├── .cursor/ .trae/ .qoder/ # AI 工具规则目录(自动生成 / gitignore) -├── .agent/ # Antigravity:rules 自动生成,workflows 入库 +├── .cursor/ .trae/ .qoder/ # AI 工具规则目录(按需生成 / gitignore) +├── .agent/ # Antigravity 规则目录 │ -├── package.json # npm 脚本入口 ├── .github/workflows/ # GitHub Actions:CI 一致性守护 ├── .gitignore # 灵枢版忽略规则(物理隔绝肢体仓 + 个人产物) └── README.md ``` +> 同步引擎不在仓库内,而在全局 CLI [@ruobai/lingshu](https://www.npmjs.com/package/@ruobai/lingshu)。仓库保持纯净的开发资产。 + --- ## 快速开始 (Getting Started) ### 推荐方式:使用 @ruobai/lingshu CLI(一条命令) -[@ruobai/lingshu](https://www.npmjs.com/package/@ruobai/lingshu) 是灵枢架构的官方脚手架(若白知行出品),把下方 7 步手动流程压缩为 1 条命令: +[@ruobai/lingshu](https://www.npmjs.com/package/@ruobai/lingshu) 是灵枢架构的官方脚手架(若白知行出品),把下方手动流程压缩为 1 条命令: ```bash -# 一次性安装 +# 一次性安装(团队每位成员各装一次) npm install -g @ruobai/lingshu # 一键创建项目(请将 your-org 替换为你的 GitHub 组织或用户名) @@ -123,10 +114,10 @@ git push -u origin master git clone git@github.com:your-org/my-lingshu-app-server.git my-lingshu-app-server git clone git@github.com:your-org/my-lingshu-app-ui.git my-lingshu-app-ui -# 4. 初始化灵枢工具链 -npm install # 安装依赖(含自动安装 git hooks) -npm run sync # 分发规则到本地 AI 工具目录 -npm run doctor # 架构健康检查 +# 4. 生成基线产物 + 安装 hooks(需先全局安装 @ruobai/lingshu) +lingshu sync --baseline # 分发规则到 CLAUDE.md / AGENTS.md +lingshu hooks install # 安装 git hooks +lingshu doctor # 架构健康检查 ``` ### 项目结构概览(接入后) @@ -135,7 +126,6 @@ npm run doctor # 架构健康检查 my-lingshu-app/ # [中枢仓] 逻辑定义与 AI 指令中心 ├── my-lingshu-app-server/ # [肢体仓 A] 后端代码(嵌套子仓) ├── my-lingshu-app-ui/ # [肢体仓 B] 前端代码(嵌套子仓) -├── .lingshu/ # 项目元数据 ├── reference/ # 真理之源 ├── CLAUDE.md / AGENTS.md # 基线 AI 指令 └── README.md # 项目指挥总纲 @@ -153,7 +143,7 @@ grep -rl "lingshu-template" --exclude-dir=node_modules . | xargs sed -i 's/lings # "请将仓库内所有文件中的 'lingshu-template' 替换为 'my-lingshu-app'" ``` -替换后执行 `npm run sync` 重新生成基线产物。 +替换后执行 `lingshu sync` 重新生成基线产物。 --- @@ -164,7 +154,7 @@ grep -rl "lingshu-template" --exclude-dir=node_modules . | xargs sed -i 's/lings ``` reference/rules/*.md ← 编辑这里(唯一真源) ↓ - npm run sync ← 一键分发 + lingshu sync ← 一键分发 ↓ ┌──────────┬──────────────┐ ↓ ↓ ↓ @@ -176,18 +166,20 @@ grep -rl "lingshu-template" --exclude-dir=node_modules . | xargs sed -i 's/lings | 命令 | 用途 | |------|------| -| `npm run sync` | 分发规则到所有 AI 工具 | -| `npm run sync:baseline` | 仅同步基线工具(CLAUDE.md / AGENTS.md) | -| `npm run sync:check` | 校验一致性(CI 用,不写文件) | -| `npm run sync -- --only=cursor,codex` | 仅同步指定工具 | -| `npm run doctor` | 架构健康检查 | -| `npm run hooks:install` | 安装/重装 git hooks | +| `lingshu sync` | 分发规则(baseline + 已激活的个人工具) | +| `lingshu sync --baseline` | 仅同步基线工具(CLAUDE.md / AGENTS.md) | +| `lingshu sync --all` | 同步所有工具 | +| `lingshu sync --only=cursor,codex` | 仅同步指定工具 | +| `lingshu sync --check` | 校验一致性(CI 用,不写文件) | +| `lingshu tool list` | 查看工具矩阵与入库状态 | +| `lingshu tool track/untrack <工具>` | 调整某工具产物是否入库 | +| `lingshu doctor` | 架构健康检查 | +| `lingshu hooks install` | 安装/重装 git hooks | ### 自动化机制 -- ⚡ **`git pull` 后** → `post-merge` hook 检测 SSoT 变更,自动 `sync` +- ⚡ **`git pull` 后** → `post-merge` hook 检测 `reference/rules/` 变更,自动 `lingshu sync` - 🛡️ **PR 提交时** → GitHub Actions 校验 baseline 产物一致性,漂移即拒绝合并 -- 🔄 **`npm install` 后** → `postinstall` 钩子自动安装 git hooks --- @@ -208,20 +200,8 @@ grep -rl "lingshu-template" --exclude-dir=node_modules . | xargs sed -i 's/lings --- -## 演进路线 (Roadmap) - -| 阶段 | 状态 | 目标 | -|:---:|:---:|------| -| **P0** | ✅ 完成 | 中枢-肢体架构 + 多 AI 工具规则副本 | -| **P1** | ✅ 完成 | 规则 SSoT + 跨平台分发 + CI 守护 | -| **P2** | ✅ 完成 | [@ruobai/lingshu](https://www.npmjs.com/package/@ruobai/lingshu) 一键脚手架(init / sync / doctor / tool / limb) | -| **P3** | 📋 待启动 | 文档温度分层 + 自动归档(`lingshu archive`) | -| **P4** | 📋 待启动 | 模板版本管理(`lingshu upgrade`) | - ---- - ## License [MIT](./LICENSE) © 2026 imrui -> 注:本仓库为架构模板。基于本模板派生的新项目可自行选择协议(默认 `templates/default/package.json` 中 `license` 字段为 `UNLICENSED` 占位,由作者自决)。 +> 注:本仓库为架构模板。基于本模板派生的新项目可自行选择协议。 diff --git a/templates/default/_gitignore b/templates/default/_gitignore index 4861791..84b980b 100644 --- a/templates/default/_gitignore +++ b/templates/default/_gitignore @@ -39,7 +39,7 @@ out/ # ========================================== # 🤖 AI 工具规则产物(由 reference/rules/ 自动生成) -# 修改请编辑 reference/rules/ 真源,然后执行 `npm run sync` +# 修改请编辑 reference/rules/ 真源,然后执行 `lingshu sync` # ========================================== # # 团队基线工具(入库,保证克隆即用): diff --git a/templates/default/package.json b/templates/default/package.json deleted file mode 100644 index d4303b5..0000000 --- a/templates/default/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "lingshu-template", - "version": "0.2.1", - "description": "灵枢架构 (LingShu) — AI 原生开发的中枢模版", - "private": true, - "type": "module", - "scripts": { - "sync": "node .lingshu/scripts/sync-rules.mjs", - "sync:check": "node .lingshu/scripts/sync-rules.mjs --check", - "sync:baseline": "node .lingshu/scripts/sync-rules.mjs --baseline", - "doctor": "node .lingshu/scripts/doctor.mjs", - "hooks:install": "node .lingshu/scripts/install-hooks.mjs", - "postinstall": "node .lingshu/scripts/install-hooks.mjs" - }, - "engines": { - "node": ">=18" - }, - "license": "UNLICENSED" -} diff --git a/templates/default/reference/rules/ai-behavior.md b/templates/default/reference/rules/ai-behavior.md index 2f3477f..1065d67 100644 --- a/templates/default/reference/rules/ai-behavior.md +++ b/templates/default/reference/rules/ai-behavior.md @@ -1,3 +1,11 @@ +--- +order: 2 +name: ai-behavior +description: 灵枢智能体行为准则:Plan 模式存档、真理同步流与原子化交付 +globs: **/* +trigger: always_on +--- + # 🤖 灵枢智能体行为准则 (Agentic Workflow) ## 1. 思考模式:真理驱动 (Truth Driven) diff --git a/templates/default/reference/rules/lingshu-core.md b/templates/default/reference/rules/lingshu-core.md index 52cdd2c..4fe01a6 100644 --- a/templates/default/reference/rules/lingshu-core.md +++ b/templates/default/reference/rules/lingshu-core.md @@ -1,3 +1,11 @@ +--- +order: 1 +name: lingshu-core +description: 灵枢架构核心准则:脑体解耦、Git 物理隔离与路径映射 +globs: **/* +trigger: always_on +--- + # 灵枢架构核心准则 (LingShu Core Principles) ## 1. 架构拓扑定义 (Architecture Topology) @@ -18,7 +26,7 @@ - **🚫 禁止根目录通配提交**: - **严禁** 在根目录执行 `git add .` 或 `git commit -a`,这会导致肢体仓的代码被错误地纳入中枢仓版本控制。 - - 根目录 Git **仅允许** 追踪:`reference/`, `.lingshu/`, AI 工具基线产物(`CLAUDE.md`, `AGENTS.md`),以及 `reference/management/` 下的任务文档。 + - 根目录 Git **仅允许** 追踪:`reference/`, AI 工具基线产物(`CLAUDE.md`, `AGENTS.md`),以及 `reference/management/` 下的任务文档。 - **✅ 肢体仓独立提交**: - 修改具体业务代码后,必须显式 `cd [limb-folder_name]/` 进入子目录。 @@ -31,8 +39,8 @@ ## 3. 规则真源约束 (SSoT for Rules) - **真源唯一**: 所有 AI 行为规则的真源位于 `reference/rules/`。 -- **产物只读**: `.cursor/rules/`、`.trae/rules/`、`.qoder/rules/`、`.agent/rules/`、`CLAUDE.md`、`AGENTS.md` 均为 **由 `.lingshu/scripts/sync-rules.mjs` 自动生成的产物**,禁止手动编辑。 -- **变更流程**: 规则修改必须改动 `reference/rules/` 真源,再执行 `npm run sync` 重新分发。 +- **产物只读**: `.cursor/rules/`、`.trae/rules/`、`.qoder/rules/`、`.agent/rules/`、`CLAUDE.md`、`AGENTS.md` 均为 **由 `lingshu sync` 自动生成的产物**,禁止手动编辑。 +- **变更流程**: 规则修改必须改动 `reference/rules/` 真源,再执行 `lingshu sync` 重新分发。 - **CI 保障**: GitHub Actions 会校验入库产物与真源的一致性,防止漂移。 ## 4. 环境与依赖标准 (Stack Standard) diff --git a/tests/smoke.test.mjs b/tests/smoke.test.mjs index 9ee8062..b9077f6 100644 --- a/tests/smoke.test.mjs +++ b/tests/smoke.test.mjs @@ -1,5 +1,5 @@ /** - * Smoke 测试:验证 init 命令能产出可用项目 + * Smoke 测试:验证 init 命令能产出可用项目(v0.3 零侵入契约) * * 运行: node --test tests/smoke.test.mjs */ @@ -7,7 +7,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; -import { mkdirSync, rmSync, existsSync, readFileSync, cpSync } from 'node:fs'; +import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, cpSync } from 'node:fs'; import { join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -25,11 +25,17 @@ function runCli(args, opts = {}) { }); } +function freshTmp() { + if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true }); + mkdirSync(TMP, { recursive: true }); +} + test('CLI 帮助', () => { const r = runCli(['--help'], { cwd: PKG_ROOT }); assert.equal(r.status, 0); assert.match(r.stdout, /用法/); assert.match(r.stdout, /init/); + assert.match(r.stdout, /track.*untrack/, '帮助应展示新的 tool 子命令'); }); test('CLI 版本号', () => { @@ -38,10 +44,8 @@ test('CLI 版本号', () => { assert.match(r.stdout, /^v\d+\.\d+\.\d+/); }); -test('init 创建完整项目结构(默认 baseline-only)', () => { - if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true }); - mkdirSync(TMP, { recursive: true }); - +test('init 创建零侵入项目结构(无 .lingshu / 无 package.json)', () => { + freshTmp(); const r = runCli(['init', 'demo-lingshu', '--no-git', '--no-install-hooks']); if (r.status !== 0) { console.error('STDOUT:', r.stdout); @@ -50,69 +54,53 @@ test('init 创建完整项目结构(默认 baseline-only)', () => { assert.equal(r.status, 0, '初始化退出码应为 0'); const proj = join(TMP, 'demo-lingshu'); - // 关键路径存在 + + // 应存在的治理资产与基线产物 for (const p of [ - '.lingshu/config/adapters.mjs', - '.lingshu/scripts/sync-rules.mjs', 'reference/rules/lingshu-core.md', 'reference/rules/ai-behavior.md', 'CLAUDE.md', 'AGENTS.md', - 'package.json', '.gitignore', '.github/workflows/rules-consistency.yml', ]) { assert.ok(existsSync(join(proj, p)), `缺失: ${p}`); } - // 关键回归(v0.2.5):.gitignore 不能被 npm 吞掉 - // 内容上必须含通配的肢体仓忽略与 personal 工具产物忽略 + // 零侵入核心断言:派生仓不应再包含引擎与 package.json + for (const forbidden of ['.lingshu', 'package.json', '_gitignore']) { + assert.ok( + !existsSync(join(proj, forbidden)), + `零侵入契约:派生仓不应包含 ${forbidden}`, + ); + } + + // .gitignore 回归(v0.2.5):不能被 npm 吞掉,且含通配 + personal 忽略 const gitignore = readFileSync(join(proj, '.gitignore'), 'utf8'); assert.match(gitignore, /\*-server\//, '.gitignore 应含肢体仓通配'); assert.match(gitignore, /\*-ui\//, '.gitignore 应含肢体仓通配'); assert.match(gitignore, /\.cursor\/rules\//, '.gitignore 应忽略 personal 工具产物'); - // _gitignore 不应残留(应已被 rename 为 .gitignore) - assert.ok( - !existsSync(join(proj, '_gitignore')), - '_gitignore 应已被 rename 为 .gitignore,不应残留', - ); - - // 关键回归(v0.2.3):默认不应生成 personal 工具的产物目录 + // 默认不应生成 personal 工具产物(v0.2.3 行为保留) for (const personal of [ '.cursor/rules/lingshu-core.mdc', '.trae/rules/lingshu-core.md', '.qoder/rules/lingshu-core.md', '.agent/rules/lingshu-core.md', ]) { - assert.ok( - !existsSync(join(proj, personal)), - `默认不应生成 personal 工具产物: ${personal}(应当通过 lingshu sync 或 --all-tools 明确触发)`, - ); + assert.ok(!existsSync(join(proj, personal)), `默认不应生成 personal 工具产物: ${personal}`); } - // package.json:name 与 description 都应被注入项目身份 - const pkg = JSON.parse(readFileSync(join(proj, 'package.json'), 'utf8')); - assert.equal(pkg.name, 'demo-lingshu', 'package.json name 应已替换'); - assert.equal( - pkg.description, - 'demo-lingshu — 灵枢架构项目', - 'package.json description 应被重写为项目级描述', - ); - - // CLAUDE.md 不应再含 lingshu-template + // CLAUDE.md:不残留模板名,且头部引用零侵入的 `lingshu sync` const claude = readFileSync(join(proj, 'CLAUDE.md'), 'utf8'); assert.ok(!claude.includes('lingshu-template'), 'CLAUDE.md 不应残留模板名'); + assert.match(claude, /generated by `lingshu sync`/, 'CLAUDE.md 头部应引用 lingshu sync'); + assert.ok(!claude.includes('.lingshu/scripts'), 'CLAUDE.md 不应再引用本地引擎脚本'); }); test('init --all-tools 仍可生成 personal 工具产物', () => { - if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true }); - mkdirSync(TMP, { recursive: true }); - - const r = runCli([ - 'init', 'all-tools-test', - '--all-tools', '--no-git', '--no-install-hooks', - ]); + freshTmp(); + const r = runCli(['init', 'all-tools-test', '--all-tools', '--no-git', '--no-install-hooks']); assert.equal(r.status, 0); const proj = join(TMP, 'all-tools-test'); @@ -128,69 +116,60 @@ test('init --all-tools 仍可生成 personal 工具产物', () => { } }); -test('init --tools 修改基线列表', () => { - if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true }); - mkdirSync(TMP, { recursive: true }); - +test('init --tools 只生成指定工具产物', () => { + freshTmp(); const r = runCli([ - 'init', 'baseline-test', + 'init', 'tools-test', '--tools=cursor,claude-code', '--no-git', '--no-install-hooks', ]); assert.equal(r.status, 0); - const adapters = readFileSync( - join(TMP, 'baseline-test/.lingshu/config/adapters.mjs'), 'utf8' - ); - assert.match(adapters, /baseline = \['cursor', 'claude-code'\]/); + const proj = join(TMP, 'tools-test'); + // 指定的工具应生成 + assert.ok(existsSync(join(proj, '.cursor/rules/lingshu-core.mdc')), '应生成 cursor 产物'); + assert.ok(existsSync(join(proj, 'CLAUDE.md')), '应生成 claude-code 产物'); + // 未指定的不应生成 + assert.ok(!existsSync(join(proj, 'AGENTS.md')), '未指定的 codex 不应生成'); + assert.ok(!existsSync(join(proj, '.trae/rules/lingshu-core.md')), '未指定的 trae 不应生成'); }); -test('sync 默认 auto:仅同步 baseline + 已存在产物的 personal(v0.2.4 行为)', () => { - if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true }); - mkdirSync(TMP, { recursive: true }); +test('生成的 cursor 产物含规则文件自身的 frontmatter', () => { + freshTmp(); + runCli(['init', 'fm-test', '--all-tools', '--no-git', '--no-install-hooks']); + const proj = join(TMP, 'fm-test'); + const mdc = readFileSync(join(proj, '.cursor/rules/lingshu-core.mdc'), 'utf8'); + assert.match(mdc, /^---\n/, 'cursor .mdc 应以 frontmatter 开头'); + assert.match(mdc, /description: 灵枢架构核心准则/, '应含规则文件的 description'); + assert.ok(!mdc.includes('order:'), '保留字段 order 不应输出到产物'); +}); - // init 默认 baseline-only,不会生成 personal 工具产物 +test('sync 默认 auto:仅同步 baseline + 已存在产物的 personal', () => { + freshTmp(); runCli(['init', 'auto-sync', '--no-git', '--no-install-hooks']); const proj = join(TMP, 'auto-sync'); - // 第一次跑 lingshu sync(默认 auto)→ 不应"主动激活" personal 工具 + // 第一次 lingshu sync(auto)→ 不应主动激活 personal 工具 let r = runCli(['sync'], { cwd: proj }); assert.equal(r.status, 0); - for (const personal of [ - '.cursor/rules/lingshu-core.mdc', - '.trae/rules/lingshu-core.md', - ]) { - assert.ok( - !existsSync(join(proj, personal)), - `auto 模式不应主动创建未激活的 personal 工具产物: ${personal}`, - ); + for (const personal of ['.cursor/rules/lingshu-core.mdc', '.trae/rules/lingshu-core.md']) { + assert.ok(!existsSync(join(proj, personal)), `auto 不应主动创建: ${personal}`); } - // 显式 --only=cursor 激活一次 cursor + // 显式 --only=cursor 激活 r = runCli(['sync', '--only=cursor'], { cwd: proj }); assert.equal(r.status, 0); - assert.ok( - existsSync(join(proj, '.cursor/rules/lingshu-core.mdc')), - '--only=cursor 之后应当生成 .cursor 产物', - ); + assert.ok(existsSync(join(proj, '.cursor/rules/lingshu-core.mdc')), '--only=cursor 应生成 cursor 产物'); - // 再次 lingshu sync(auto),由于 .cursor 已存在 → 应当自动维护它 + // 再次 auto sync:cursor 已存在 → 自动维护,其余仍不激活 r = runCli(['sync'], { cwd: proj }); assert.equal(r.status, 0); - assert.ok( - existsSync(join(proj, '.cursor/rules/lingshu-core.mdc')), - 'auto 模式应当持续维护已激活工具', - ); - assert.ok( - !existsSync(join(proj, '.trae/rules/lingshu-core.md')), - '其它未激活工具仍不应被动激活', - ); + assert.ok(existsSync(join(proj, '.cursor/rules/lingshu-core.mdc')), 'auto 应持续维护已激活工具'); + assert.ok(!existsSync(join(proj, '.trae/rules/lingshu-core.md')), '未激活工具仍不应被动激活'); }); test('sync --all:显式同步所有工具', () => { - if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true }); - mkdirSync(TMP, { recursive: true }); - + freshTmp(); runCli(['init', 'all-sync', '--no-git', '--no-install-hooks']); const proj = join(TMP, 'all-sync'); @@ -206,93 +185,97 @@ test('sync --all:显式同步所有工具', () => { } }); -test('limb init:创建空肢体目录 + git init', () => { - if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true }); - mkdirSync(TMP, { recursive: true }); +test('生成项目可独立通过 lingshu sync --check --baseline', () => { + freshTmp(); + runCli(['init', 'check-proj', '--no-git', '--no-install-hooks']); + const proj = join(TMP, 'check-proj'); + const r = runCli(['sync', '--check', '--baseline'], { cwd: proj }); + if (r.status !== 0) { + console.error('CHECK STDOUT:', r.stdout); + console.error('CHECK STDERR:', r.stderr); + } + assert.equal(r.status, 0, '生成项目应通过 sync --check --baseline'); +}); + +test('tool track/untrack 直接编辑 .gitignore', () => { + freshTmp(); + runCli(['init', 'tool-test', '--no-git', '--no-install-hooks']); + const proj = join(TMP, 'tool-test'); + const giPath = join(proj, '.gitignore'); + + // 初始:cursor 被忽略 + assert.match(readFileSync(giPath, 'utf8'), /^\.cursor\/rules\/$/m, 'fixture:cursor 初始应被忽略'); + + // track cursor → 从 .gitignore 移除 + let r = runCli(['tool', 'track', 'cursor'], { cwd: proj }); + assert.equal(r.status, 0); + assert.ok( + !/^\.cursor\/rules\/$/m.test(readFileSync(giPath, 'utf8')), + 'track 后 .cursor/rules/ 应从 .gitignore 移除', + ); + + // untrack cursor → 重新追加 + r = runCli(['tool', 'untrack', 'cursor'], { cwd: proj }); + assert.equal(r.status, 0); + const giFinal = readFileSync(giPath, 'utf8'); + assert.match(giFinal, /^\.cursor\/rules\/$/m, 'untrack 后应重新忽略 .cursor/rules/'); + // 幂等:不应重复追加 + const count = (giFinal.match(/^\.cursor\/rules\/$/gm) || []).length; + assert.equal(count, 1, 'untrack 应幂等,不重复追加'); + + // tool list 应可运行 + r = runCli(['tool', 'list'], { cwd: proj }); + assert.equal(r.status, 0); + assert.match(r.stdout, /cursor/); +}); + +test('limb init:创建空肢体目录 + git init', () => { + freshTmp(); runCli(['init', 'with-limbs', '--no-git', '--no-install-hooks']); const proj = join(TMP, 'with-limbs'); const r = runCli(['limb', 'init', 'fresh-server'], { cwd: proj }); assert.equal(r.status, 0); assert.ok(existsSync(join(proj, 'fresh-server')), '空肢体目录应已创建'); - assert.ok( - existsSync(join(proj, 'fresh-server/.git')), - '应在新肢体目录下完成 git init', - ); + assert.ok(existsSync(join(proj, 'fresh-server/.git')), '应在新肢体目录下完成 git init'); }); -test('limb 自动维护 .gitignore:约定命名跳过、非约定命名追加(v0.2.6)', () => { - if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true }); - mkdirSync(TMP, { recursive: true }); - +test('limb 自动维护 .gitignore:约定命名跳过、非约定命名追加', () => { + freshTmp(); runCli(['init', 'gi-test', '--no-git', '--no-install-hooks']); const proj = join(TMP, 'gi-test'); const giBefore = readFileSync(join(proj, '.gitignore'), 'utf8'); assert.match(giBefore, /\*-server\//, 'fixture 校验:模板已含通配'); - // 1) 约定命名:已被 *-server/ 通配覆盖,不应追加重复行 + // 约定命名:被 *-server/ 通配覆盖,不追加 let r = runCli(['limb', 'init', 'app-server'], { cwd: proj }); assert.equal(r.status, 0); - const giAfterServer = readFileSync(join(proj, '.gitignore'), 'utf8'); - assert.equal(giAfterServer, giBefore, '约定命名应被通配覆盖,.gitignore 不变'); + assert.equal(readFileSync(join(proj, '.gitignore'), 'utf8'), giBefore, '约定命名 .gitignore 不变'); - // 2) 非约定命名:应被自动追加 + // 非约定命名:追加一行 r = runCli(['limb', 'init', 'demo-abc'], { cwd: proj }); assert.equal(r.status, 0); - const giAfterAbc = readFileSync(join(proj, '.gitignore'), 'utf8'); - assert.match(giAfterAbc, /^demo-abc\/$/m, '非约定命名应被追加 "demo-abc/" 一行'); + assert.match(readFileSync(join(proj, '.gitignore'), 'utf8'), /^demo-abc\/$/m, '非约定命名应被追加'); - // 3) 重复 init 同名(虽然会因目录已存在失败,但即使追加幂等也不应重复行) - // 用 adopt 子命令模拟:先准备一个外部目录 + // adopt 幂等 const outside = join(TMP, 'outside-source'); mkdirSync(outside); - // 用一个新名字测幂等 r = runCli(['limb', 'adopt', 'demo-xyz', outside], { cwd: proj }); assert.equal(r.status, 0); - - // 再追加 demo-xyz/ — 显式调用一次 (用 init 失败,但若用户手动改 .gitignore 后再执行 limb 也不应重复) - // 直接断言追加只发生一次 const giAfterXyz = readFileSync(join(proj, '.gitignore'), 'utf8'); - const xyzCount = (giAfterXyz.match(/^demo-xyz\/$/gm) || []).length; - assert.equal(xyzCount, 1, 'demo-xyz/ 应只追加一次(幂等)'); -}); - -test('生成项目可独立通过 sync:check --baseline', () => { - if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true }); - mkdirSync(TMP, { recursive: true }); - - runCli(['init', 'check-proj', '--no-git', '--no-install-hooks']); - const proj = join(TMP, 'check-proj'); - - // init 默认 baseline-only,所以校验也限定 baseline - const r = spawnSync( - process.execPath, - ['.lingshu/scripts/sync-rules.mjs', '--check', '--baseline'], - { cwd: proj, encoding: 'utf8', env: { ...process.env, NO_COLOR: '1' } }, - ); - if (r.status !== 0) { - console.error('CHECK STDOUT:', r.stdout); - console.error('CHECK STDERR:', r.stderr); - } - assert.equal(r.status, 0, '生成项目应通过 sync:check --baseline'); + assert.equal((giAfterXyz.match(/^demo-xyz\/$/gm) || []).length, 1, 'demo-xyz/ 应只追加一次'); }); test('源路径含 node_modules 时模板仍能正确拷贝(回归 v0.2.1 bug)', () => { - // 模拟 npm 全局安装:模板源路径形如 - // /usr/lib/node_modules/@ruobai/lingshu/templates/default - // 之前 copyTemplate filter 误用绝对路径匹配 'node_modules', - // 导致整个模板树被全部过滤,最终生成空目录。 - if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true }); - mkdirSync(TMP, { recursive: true }); - + freshTmp(); const fakeInstall = join(TMP, 'node_modules', '@ruobai', 'lingshu'); const templateInGlobal = join(fakeInstall, 'templates', 'default'); cpSync(join(PKG_ROOT, 'templates/default'), templateInGlobal, { recursive: true }); assert.ok( - existsSync(join(templateInGlobal, '.lingshu/config/adapters.mjs')), - 'fixture 准备失败:源路径下 .lingshu/config/adapters.mjs 应存在', + existsSync(join(templateInGlobal, 'reference/rules/lingshu-core.md')), + 'fixture 准备失败:源路径下 reference/rules/ 应存在', ); const r = runCli([ @@ -308,11 +291,118 @@ test('源路径含 node_modules 时模板仍能正确拷贝(回归 v0.2.1 bug const proj = join(TMP, 'global-install-sim'); assert.ok( - existsSync(join(proj, '.lingshu/config/adapters.mjs')), - '回归断言:模板内的 .lingshu/ 必须被拷贝,不可因祖先路径含 node_modules 而过滤', + existsSync(join(proj, 'reference/rules/lingshu-core.md')), + '回归断言:模板内容必须被拷贝,不可因祖先路径含 node_modules 而过滤', ); }); +// ===== upgrade(F6)===== + +/** 在 dir 下构造一个 v0.2.x 结构的合成项目 */ +function buildV02Project(dir) { + mkdirSync(join(dir, '.lingshu/config'), { recursive: true }); + mkdirSync(join(dir, '.lingshu/scripts'), { recursive: true }); + mkdirSync(join(dir, 'reference/rules'), { recursive: true }); + mkdirSync(join(dir, '.github/workflows'), { recursive: true }); + + writeFileSync(join(dir, '.lingshu/config/adapters.mjs'), ` +export const sources = [ + { name: 'lingshu-core', path: 'reference/rules/lingshu-core.md' }, + { name: 'ai-behavior', path: 'reference/rules/ai-behavior.md' }, +]; +export const adapters = { + cursor: { type: 'directory', target: '.cursor/rules/', extension: '.mdc', frontmatter: { + 'lingshu-core': { name: 'lingshu-core', description: '核心准则', globs: '**/*', trigger: 'always_on' }, + 'ai-behavior': { name: 'ai-behavior', description: '行为准则', globs: '**/*', trigger: 'always_on' }, + } }, + 'claude-code': { type: 'file', target: 'CLAUDE.md' }, + codex: { type: 'file', target: 'AGENTS.md' }, +}; +export const baseline = ['claude-code', 'codex']; +`, 'utf8'); + writeFileSync(join(dir, '.lingshu/scripts/sync-rules.mjs'), '// 旧引擎占位\n', 'utf8'); + // 规则文件:无 frontmatter + writeFileSync(join(dir, 'reference/rules/lingshu-core.md'), '# 核心\n\n正文。\n', 'utf8'); + writeFileSync(join(dir, 'reference/rules/ai-behavior.md'), '# 行为\n\n正文。\n', 'utf8'); + // 脚手架 package.json + writeFileSync(join(dir, 'package.json'), JSON.stringify({ + name: 'legacy', version: '0.0.0', + scripts: { sync: 'node .lingshu/scripts/sync-rules.mjs' }, + }, null, 2), 'utf8'); + writeFileSync(join(dir, '.gitignore'), '.cursor/rules/\n.trae/rules/\n', 'utf8'); + writeFileSync(join(dir, '.github/workflows/rules-consistency.yml'), + 'on:\n push:\n paths:\n - \'.lingshu/**\'\nsteps:\n - run: node .lingshu/scripts/sync-rules.mjs --check --baseline\n', + 'utf8'); +} + +test('upgrade:v0.2.x → v0.3 机械迁移', () => { + freshTmp(); + const proj = join(TMP, 'v02-proj'); + buildV02Project(proj); + + const r = runCli(['upgrade'], { cwd: proj }); + if (r.status !== 0) { console.error('STDOUT:', r.stdout); console.error('STDERR:', r.stderr); } + assert.equal(r.status, 0, 'upgrade 退出码应为 0'); + + // .lingshu 与 package.json 应被删除 + assert.ok(!existsSync(join(proj, '.lingshu')), '.lingshu/ 应被删除'); + assert.ok(!existsSync(join(proj, 'package.json')), '纯脚手架 package.json 应被删除'); + + // 规则文件应被注入 frontmatter(含原 cursor 的 description) + const core = readFileSync(join(proj, 'reference/rules/lingshu-core.md'), 'utf8'); + assert.match(core, /^---\r?\norder: 1/, '应注入 order: 1'); + assert.match(core, /description: 核心准则/, '应迁移原 cursor frontmatter 的 description'); + + // 基线产物应被重生成 + assert.ok(existsSync(join(proj, 'CLAUDE.md')), '应重生成 CLAUDE.md'); + assert.ok(existsSync(join(proj, 'AGENTS.md')), '应重生成 AGENTS.md'); + + // CI 应改为 npx 调用 + const ci = readFileSync(join(proj, '.github/workflows/rules-consistency.yml'), 'utf8'); + assert.match(ci, /npx -y @ruobai\/lingshu sync --check --baseline/, 'CI 应改造为 npx'); + assert.ok(!ci.includes('.lingshu/'), 'CI 不应再引用 .lingshu/'); + + // 迁移后应能通过一致性校验 + const check = runCli(['sync', '--check', '--baseline'], { cwd: proj }); + assert.equal(check.status, 0, '迁移后应通过 sync --check --baseline'); +}); + +test('upgrade:已是 v0.3 时幂等(无需迁移)', () => { + freshTmp(); + runCli(['init', 'already-v3', '--no-git', '--no-install-hooks']); + const proj = join(TMP, 'already-v3'); + + const r = runCli(['upgrade'], { cwd: proj }); + assert.equal(r.status, 0); + assert.match(r.stdout, /已是 v0\.3|无需迁移/, '应提示无需迁移'); +}); + +test('upgrade:检测 1.0 结构并给出指引(不自动迁移)', () => { + freshTmp(); + const proj = join(TMP, 'v1-proj'); + mkdirSync(join(proj, '.cursor/rules'), { recursive: true }); + writeFileSync(join(proj, '.cursor/rules/core.mdc'), '# 1.0 规则散落产物\n', 'utf8'); + + const r = runCli(['upgrade'], { cwd: proj }); + assert.equal(r.status, 1, '1.0 无法自动迁移,应退出码 1'); + assert.match(r.stdout, /1\.0/, '应说明检测到 1.0 结构'); +}); + +test('upgrade --dry-run:只预览不写盘', () => { + freshTmp(); + const proj = join(TMP, 'dry-proj'); + buildV02Project(proj); + + const r = runCli(['upgrade', '--dry-run'], { cwd: proj }); + assert.equal(r.status, 0); + assert.match(r.stdout, /dry-run/); + // dry-run 不应改动任何文件 + assert.ok(existsSync(join(proj, '.lingshu')), 'dry-run 不应删除 .lingshu'); + assert.ok(existsSync(join(proj, 'package.json')), 'dry-run 不应删除 package.json'); + const core = readFileSync(join(proj, 'reference/rules/lingshu-core.md'), 'utf8'); + assert.ok(!/^---/.test(core), 'dry-run 不应注入 frontmatter'); +}); + test.after(() => { if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true }); });