Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,57 @@
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。

## [0.3.2] - 2026-07-03

> **主题:可选 CI**——`.github/workflows/` 从默认拷贝改为按需加装。

### 背景

用户反馈:**不是所有派生仓都用 GitHub Actions**(GitLab CI / Jenkins /
self-hosted / 或干脆不用 CI)。之前 `init` 默认拷贝
`.github/workflows/rules-consistency.yml` 到派生仓,对上述场景形成隐性侵入
——即便文件已技术中立,"未经我同意就配好 CI"本身就是一种侵入。

延续 v0.3.1 减法哲学,将 CI 一致性守护从"默认设施"降级为"可选加装"。

### Added

- **`lingshu ci install`**:新增命令,从内置模板拷贝
`.github/workflows/rules-consistency.yml` 到当前项目。
- 幂等:文件已存在时输出"已存在,跳过",不覆盖用户已有内容。
- `--force`:强制覆盖已存在的 workflow(用于修复被误改的情况)。
- 与 `lingshu hooks install` 形成对称的可选设施命令族。

### Changed

- **`lingshu init` 默认不再拷贝 `.github/`**:派生仓开箱不含 CI workflow。
- "下一步"提示中加入 `lingshu ci install # 加装 GitHub CI 一致性守护(可选)`。
- `copyTemplate` 增加 `exclude` 参数(如 `['.github']`),支持调用方精细控制排除项。
- **`lingshu doctor` 扩为 4 步**:新增 `[4/4] 可选设施` 检查——存在
`.github/workflows/rules-consistency.yml` 时打勾,不存在时以 dim 灰字提示
`可运行 lingshu ci install 加装`(**不是错误,也不是 warning**)。
- **模板 `README.md` 目录树**:`.github/workflows/` 一行移除(init 不装则不出现);
相关"CI 守护"表述加"可选"注脚。

### Migration

**存量派生仓完全不受影响**——他们的 `.github/workflows/rules-consistency.yml`
若已存在,`lingshu upgrade` 不主动删除,继续正常校验 SSoT 与产物一致性。

**新 init 项目**默认无 CI;如需守护:
```bash
lingshu ci install
```

### Tests

- smoke 22 → 25(+3):
- `ci install:init 之后手动加装 workflow`
- `ci install:幂等(跳过)+ --force 覆盖`
- `ci install:非灵枢项目应报错`
- 修改 `init 创建零侵入项目结构` 断言:`.github/workflows/*.yml` 从"应存在"
移到"禁止存在"(零侵入契约)。

## [0.3.1] - 2026-07-03

> **主题:工作流瘦身(Slim Workflow)** —— 连自己写的规矩也开始做减法。
Expand Down Expand Up @@ -261,6 +312,7 @@ lingshu upgrade # 执行
- 核心命令 `init` / `sync` / `doctor` / `tool` / `limb`。
- 包名 `@ruobai/lingshu`(若白知行 npm scope)。

[0.3.2]: https://github.com/imrui/lingshu-cli/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/imrui/lingshu-cli/compare/v0.3.0...v0.3.1
[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
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ 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.3.1
npm install -g git+ssh://git@github.com/imrui/lingshu-cli.git#v0.3.2
```

> 团队协作提示:v0.3 起派生仓不含 `package.json`,同步统一走全局 `lingshu sync`。**团队每位成员各全局安装一次**即可;CI 用 `npx -y @ruobai/lingshu` 临时调用。
Expand Down Expand Up @@ -119,6 +119,15 @@ lingshu init my-lingshu-app \
|--------|------|
| `install` | 在当前项目安装内置 git hooks(`post-merge`:`git pull` 后自动 `lingshu sync`)。`init` 时已自动安装,本命令供存量项目补装 |

### `lingshu ci <subcmd>`

**可选**的 GitHub Actions CI 一致性守护。v0.3.2 起 `init` 不再默认拷贝
`.github/workflows/`,由本命令按需加装。

| 子命令 | 说明 |
|--------|------|
| `install` | 拷贝 `.github/workflows/rules-consistency.yml` 到当前项目。幂等(已存在则跳过),加 `--force` 覆盖 |

### `lingshu upgrade`

把存量项目迁移到 v0.3 零侵入结构。
Expand Down
5 changes: 5 additions & 0 deletions bin/lingshu.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* tool <subcmd> 管理 AI 工具支持(list/track/untrack)
* limb <subcmd> 管理肢体仓(list/add)
* hooks <subcmd> 安装 git hooks(install)
* ci <subcmd> 安装可选 CI 一致性守护(install)
* upgrade 迁移存量项目到 v0.3 零侵入结构
* --version, -v 显示版本
* --help, -h 显示帮助
Expand Down Expand Up @@ -58,6 +59,9 @@ ${pkg.description} v${pkg.version}
hooks <subcmd> 安装灵枢 git hooks
子命令: install

ci <subcmd> 管理可选 CI 一致性守护(GitHub Actions workflow)
子命令: install [--force]

upgrade 将存量项目迁移到 v0.3 零侵入结构
选项: --dry-run(仅预览), --force

Expand All @@ -74,6 +78,7 @@ const COMMANDS = {
tool: () => import('../src/commands/tool.mjs'),
limb: () => import('../src/commands/limb.mjs'),
hooks: () => import('../src/commands/hooks.mjs'),
ci: () => import('../src/commands/ci.mjs'),
upgrade: () => import('../src/commands/upgrade.mjs'),
};

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ruobai/lingshu",
"version": "0.3.1",
"version": "0.3.2",
"description": "灵枢架构 CLI — AI 原生项目脚手架 | 若白知行出品",
"type": "module",
"bin": {
Expand Down
69 changes: 69 additions & 0 deletions src/commands/ci.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* lingshu ci <subcmd>
*
* 子命令:
* install 在当前项目安装 GitHub CI 一致性守护
* workflow (.github/workflows/rules-consistency.yml)。
* 幂等:文件已存在则跳过;--force 覆盖。
*
* v0.3.2 起 CI 一致性守护改为可选设施——`init` 不再默认拷贝 .github/,
* 用户如需 GitHub Actions 校验真源与产物一致性,运行 `lingshu ci install`
* 显式加装。存量项目已装的 workflow 不受影响。
*/

import { existsSync, mkdirSync, copyFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { parseArgs } from '../utils/args.mjs';
import { log, c } from '../utils/log.mjs';
import { isLingshuProject } from '../core/adapters.mjs';

const WORKFLOW_REL = '.github/workflows/rules-consistency.yml';

export default async function ci({ args, pkgRoot }) {
const subcmd = args[0];
const rest = args.slice(1);

if (!subcmd || subcmd === '--help' || subcmd === '-h') {
console.log(`
lingshu ci <subcmd>

子命令:
install 安装 GitHub CI 一致性守护 workflow(幂等,可加 --force 覆盖)

示例:
lingshu ci install
lingshu ci install --force
`);
return;
}

if (subcmd !== 'install') {
throw new Error(`未知子命令: ci ${subcmd}(可用: install)`);
}

const { flags } = parseArgs(rest, { booleanFlags: ['force'] });
const root = process.cwd();

if (!isLingshuProject(root)) {
throw new Error('当前目录不是灵枢项目(未找到 reference/rules/)');
}

const src = join(pkgRoot, 'templates/default', WORKFLOW_REL);
const dst = join(root, WORKFLOW_REL);

if (!existsSync(src)) {
throw new Error(`模板中未找到 workflow 文件: ${WORKFLOW_REL}`);
}

if (existsSync(dst) && !flags.force) {
log.warn(`已存在 ${WORKFLOW_REL},跳过`);
log.hint(`如需覆盖:${c.bold('lingshu ci install --force')}`);
return;
}

mkdirSync(dirname(dst), { recursive: true });
copyFileSync(src, dst);

log.ok(`已安装 GitHub CI 一致性守护 → ${WORKFLOW_REL}`);
log.hint('workflow 在 PR/push 时校验 reference/rules/ 真源与 CLAUDE.md/AGENTS.md 产物一致性');
}
14 changes: 11 additions & 3 deletions src/commands/doctor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default async function doctor() {
const fail = m => { console.log(c.red (` ✗ ${m}`)); errors++; };

// 1. 物理完整性(必需目录)
console.log(c.cyan('[1/3] 物理完整性'));
console.log(c.cyan('[1/4] 物理完整性'));
const requiredDirs = [
'reference/rules',
'reference/docs',
Expand All @@ -31,7 +31,7 @@ export default async function doctor() {
if (existsSync(join(root, 'reference/decisions'))) ok('reference/decisions(可选)');

// 2. SSoT 真源
console.log(c.cyan('\n[2/3] SSoT 真源'));
console.log(c.cyan('\n[2/4] SSoT 真源'));
const ssotFiles = [
'reference/rules/ai-behavior.md',
'reference/rules/lingshu-core.md',
Expand All @@ -42,7 +42,7 @@ export default async function doctor() {
}

// 3. 基线产物
console.log(c.cyan('\n[3/3] 基线产物'));
console.log(c.cyan('\n[3/4] 基线产物'));
const baselineFiles = [
'CLAUDE.md',
'AGENTS.md',
Expand All @@ -52,6 +52,14 @@ export default async function doctor() {
else warn(`${f} 缺失(可运行 lingshu sync --baseline 生成)`);
}

// 4. 可选设施
console.log(c.cyan('\n[4/4] 可选设施'));
if (existsSync(join(root, '.github/workflows/rules-consistency.yml'))) {
ok('GitHub CI 一致性守护已安装');
} else {
console.log(c.dim(' · GitHub CI 一致性守护 未安装(可运行 `lingshu ci install` 加装)'));
}

log.blank();
if (errors === 0 && warnings === 0) {
log.ok('架构健康度:优秀');
Expand Down
4 changes: 3 additions & 1 deletion src/commands/init.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ export default async function init({ args, pkgRoot }) {
log.hint(`模板路径: ${templatePath}`);

// Step 1: 拷贝模板
// .github/ 从 v0.3.2 起改为可选设施,init 默认不装;用户按需 `lingshu ci install`
log.step('拷贝模板');
if (!here) mkdirSync(targetDir, { recursive: true });
copyTemplate(templatePath, targetDir);
copyTemplate(templatePath, targetDir, { exclude: ['.github'] });

// _gitignore → .gitignore:npm publish 会把 .gitignore 当 .npmignore 吞掉,
// 模板内只能用 _gitignore 入包,init 时复原为真正的 .gitignore。
Expand Down Expand Up @@ -151,6 +152,7 @@ export default async function init({ args, pkgRoot }) {
if (!here) console.log(` cd ${finalName}`);
console.log(' lingshu doctor # 健康检查');
console.log(' lingshu sync # 重新分发规则到本地 AI 工具');
console.log(' lingshu ci install # 加装 GitHub CI 一致性守护(可选)');
console.log(' git push -u origin master # 推送到远程(如已设置 remote)');
log.blank();
}
14 changes: 13 additions & 1 deletion src/core/template.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ import { join, resolve, extname, sep } from 'node:path';
*
* 过滤规则只看 src 相对 srcDir 的部分——这样无论 CLI 自身被全局安装到
* 哪个 node_modules 路径下,都不会误把整个模板树都过滤掉。
*
* @param {string} srcDir
* @param {string} dstDir
* @param {object} [options]
* @param {string[]} [options.exclude] 额外排除的相对路径(对目录或文件均生效),
* 如 ['.github'] 会排除整个 .github/,['.github/workflows/rules-consistency.yml']
* 仅排除单个文件。
*/
export function copyTemplate(srcDir, dstDir) {
export function copyTemplate(srcDir, dstDir, options = {}) {
if (!existsSync(srcDir)) throw new Error(`模板目录不存在: ${srcDir}`);
const srcRoot = resolve(srcDir);
const extraExcludes = (options.exclude ?? []).map((p) => p.replace(/\\/g, '/').replace(/\/$/, ''));
cpSync(srcDir, dstDir, {
recursive: true,
filter: (src) => {
Expand All @@ -27,6 +35,10 @@ export function copyTemplate(srcDir, dstDir) {
if (lower.includes('/.git/') || lower.endsWith('/.git')) return false;
if (lower.includes('/node_modules/') || lower.endsWith('/node_modules')) return false;
if (lower.endsWith('/.claude/settings.local.json')) return false;
// 调用方额外指定的排除
for (const ex of extraExcludes) {
if (rel === ex || rel.startsWith(ex + '/')) return false;
}
return true;
},
});
Expand Down
6 changes: 4 additions & 2 deletions templates/default/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@
├── .cursor/ .trae/ .qoder/ # AI 工具规则目录(按需生成 / gitignore)
├── .agent/ # Antigravity 规则目录
├── .github/workflows/ # CI 一致性守护
├── .gitignore # 忽略规则(物理隔绝肢体仓 + 个人产物)
└── README.md

# 可选设施(默认不装,需运行 `lingshu ci install` 加装)
# └── .github/workflows/ # GitHub Actions CI 一致性守护
```

---
Expand Down Expand Up @@ -80,7 +82,7 @@
### 自动化机制

- **`git pull` 后** → `post-merge` hook 检测 `reference/rules/` 变更,自动 `lingshu sync`
- **PR 提交时** → GitHub Actions 校验 baseline 产物一致性,漂移即拒绝合并
- **PR 提交时** → GitHub Actions 校验 baseline 产物一致性(可选,`lingshu ci install` 加装)

---

Expand Down
57 changes: 55 additions & 2 deletions tests/smoke.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ test('init 创建零侵入项目结构(无 .lingshu / 无 package.json)', ()
'CLAUDE.md',
'AGENTS.md',
'.gitignore',
'.github/workflows/rules-consistency.yml',
]) {
assert.ok(existsSync(join(proj, p)), `缺失: ${p}`);
}

// 零侵入核心断言:派生仓不应再包含引擎与 package.json
for (const forbidden of ['.lingshu', 'package.json', '_gitignore']) {
// v0.3.2 起 .github/ 也改为可选设施(`lingshu ci install` 加装)
for (const forbidden of ['.lingshu', 'package.json', '_gitignore', '.github']) {
assert.ok(
!existsSync(join(proj, forbidden)),
`零侵入契约:派生仓不应包含 ${forbidden}`,
Expand Down Expand Up @@ -523,6 +523,59 @@ test('upgrade:init 出的新项目立即幂等(已是 v0.3.1)', () => {
assert.match(r.stdout, /v0\.3\.1.*无需迁移|无需迁移/, 'init 之后 upgrade 应立即幂等');
});

// ===== ci install(v0.3.2 可选 CI 一致性守护)=====

test('ci install:init 之后手动加装 workflow', () => {
freshTmp();
runCli(['init', 'ci-test', '--no-git', '--no-install-hooks']);
const proj = join(TMP, 'ci-test');
const workflow = join(proj, '.github/workflows/rules-consistency.yml');

assert.ok(!existsSync(workflow), 'init 后 workflow 不应存在(v0.3.2 零侵入)');

const r = runCli(['ci', 'install'], { cwd: proj });
assert.equal(r.status, 0, 'ci install 应成功');
assert.match(r.stdout, /已安装.*一致性守护/);
assert.ok(existsSync(workflow), '安装后 workflow 应存在');

const content = readFileSync(workflow, 'utf8');
assert.match(content, /npx -y @ruobai\/lingshu sync --check --baseline/,
'workflow 应包含 npx 调用');
});

test('ci install:幂等(文件已存在时跳过),--force 覆盖', () => {
freshTmp();
runCli(['init', 'ci-idem', '--no-git', '--no-install-hooks']);
const proj = join(TMP, 'ci-idem');
const workflow = join(proj, '.github/workflows/rules-consistency.yml');

runCli(['ci', 'install'], { cwd: proj });
assert.ok(existsSync(workflow));

// 篡改文件内容,验证第二次 install 不会覆盖(幂等)
writeFileSync(workflow, '# tampered\n', 'utf8');
let r = runCli(['ci', 'install'], { cwd: proj });
assert.equal(r.status, 0);
assert.match(r.stdout, /已存在.*跳过/);
assert.equal(readFileSync(workflow, 'utf8'), '# tampered\n',
'幂等:不应覆盖已存在的文件');

// --force 覆盖
r = runCli(['ci', 'install', '--force'], { cwd: proj });
assert.equal(r.status, 0);
assert.match(readFileSync(workflow, 'utf8'), /npx -y @ruobai\/lingshu/,
'--force 应覆盖为模板内容');
});

test('ci install:非灵枢项目应报错', () => {
freshTmp();
const notLingshu = join(TMP, 'not-lingshu');
mkdirSync(notLingshu, { recursive: true });
const r = runCli(['ci', 'install'], { cwd: notLingshu });
assert.notEqual(r.status, 0);
assert.match(r.stderr + r.stdout, /不是灵枢项目/);
});

test.after(() => {
if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true });
});
Loading