@finleywdx/ai-init 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xfp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # @finleywdx/ai-init
2
+
3
+ > AI 工程化工作流初始化器 —— 一条命令,把「spec-kit 规范驱动开发 + 跨会话记忆层 + 质量门禁」铺进你的新项目。
4
+
5
+ `@finleywdx/ai-init` 提供命令行工具 `ai-init`。在你的新项目(如 Next.js)里运行它,它会:
6
+
7
+ 1. **编排 [spec-kit](https://github.com/github/spec-kit)**:为 5 个主流 AI 编码 agent(Cursor、Claude、Codex、Gemini、Copilot)一次性安装 `/speckit.*` 系列命令,把「规范 → 计划 → 任务 → 实现」的规范驱动开发(SDD)流程带进项目。
8
+ 2. **铺设 Finley 增量层**:一套轻量的 `.finley/` 质量门禁 + 记忆脚本(纯 Python 标准库实现,无第三方依赖)。
9
+ 3. **写好 AI 助手说明**:在 `AGENTS.md` 里生成一个 Finley 工作流区块,告诉任意 AI 助手该怎么在本项目里干活。
10
+
11
+ ## 它和 spec-kit 是什么关系?
12
+
13
+ spec-kit 负责**规范驱动开发的主流程**(constitution → specify → plan → tasks → implement)。`ai-init` 不重造这套流程,而是**编排调用** spec-kit 的 `specify` CLI 完成安装,再在其之上补两块 spec-kit 不覆盖的工程纪律:
14
+
15
+ - **质量门禁(Quality Gate)**:一条 `gate.py`,在提交前统一跑 lint / typecheck / test,强制「写完代码必须过门禁」。
16
+ - **记忆层(Memory)**:一套 `add_session.py` + journal,把「本次会话做了什么」持久化到仓库,让下次会话不必从零理解项目。
17
+
18
+ > 灵感来自 [Trellis](https://github.com/) 的「规范/任务/记忆搬进仓库」理念,但 Finley 只做最小可用的增量层,把主流程交给 spec-kit。
19
+
20
+ ## 环境要求
21
+
22
+ 运行 `ai-init init` 的机器需要具备(缺失会被检测并阻断,同时打印中文安装指引):
23
+
24
+ | 依赖 | 版本 | 用途 |
25
+ | --- | --- | --- |
26
+ | Node.js | ≥ 18 | 运行本 CLI |
27
+ | Git | 任意近版 | 版本控制 / journal 分支探测 |
28
+ | Python | ≥ 3.11 | 运行 `gate.py` / `add_session.py`(需标准库 `tomllib`) |
29
+ | uv | 任意 | spec-kit 的安装与运行依赖 |
30
+
31
+ 此外还需要 spec-kit 的 `specify` CLI。若未安装,`ai-init` 会给出用 uv 安装的指引(替换 `<tag>` 为最新版本,如 `v0.12.4`):
32
+
33
+ ```bash
34
+ uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@<tag>
35
+ ```
36
+
37
+ ## 安装
38
+
39
+ 在你的项目里作为开发依赖安装:
40
+
41
+ ```bash
42
+ npm i -D @finleywdx/ai-init
43
+ ```
44
+
45
+ 或全局安装:
46
+
47
+ ```bash
48
+ npm i -g @finleywdx/ai-init
49
+ ```
50
+
51
+ 也可以免安装直接用 npx:
52
+
53
+ ```bash
54
+ npx @finleywdx/ai-init init
55
+ ```
56
+
57
+ ## 用法
58
+
59
+ 在**目标项目根目录**运行:
60
+
61
+ ```bash
62
+ ai-init init
63
+ ```
64
+
65
+ 它会依次执行:环境检测 → 编排 spec-kit → 铺设 `.finley/` → 合并 `AGENTS.md`。
66
+
67
+ ### 常用选项
68
+
69
+ ```bash
70
+ # 只安装指定的 spec-kit integration(逗号分隔)
71
+ ai-init init --integration cursor,claude
72
+
73
+ # 跳过 spec-kit 编排,只铺设 Finley 增量层
74
+ ai-init init --skip-speckit
75
+
76
+ # 只打印将执行的操作,不真正调用 specify、不写盘
77
+ ai-init init --dry-run
78
+
79
+ # 不向 specify 传 --ignore-agent-tools(默认会传,以免因未装某个 agent CLI 而失败)
80
+ ai-init init --no-ignore-agent-tools
81
+
82
+ ai-init --version
83
+ ai-init --help
84
+ ```
85
+
86
+ > 关于多 integration:spec-kit 的 `specify init` 每次只接受**单个** `--integration`。`ai-init` 因此采用「首个 integration 用 `specify init .` 初始化,其余用 `specify integration install <name> --force` 逐个追加」的方式一次装齐 5 个 agent。
87
+
88
+ ## 初始化后的目录
89
+
90
+ ```text
91
+ 你的项目/
92
+ ├── .specify/ # spec-kit 管理(规格 / 模板 / 记忆)
93
+ ├── .cursor/ .claude/ ... # 各 agent 的 /speckit.* 命令(spec-kit 生成)
94
+ ├── .finley/
95
+ │ ├── config.yaml # 质量门禁命令映射 + 开发者/journal 配置
96
+ │ ├── scripts/
97
+ │ │ ├── gate.py # 质量门禁:lint / typecheck / test
98
+ │ │ └── add_session.py # 记忆层:写 journal
99
+ │ └── workspace/<开发者>/ # 跨会话记忆 journal(自动生成)
100
+ ├── .agents/skills/
101
+ │ ├── finley-quality-gate/SKILL.md
102
+ │ └── finley-memory/SKILL.md
103
+ └── AGENTS.md # 含 Finley 工作流区块
104
+ ```
105
+
106
+ ## 初始化后的日常工作流
107
+
108
+ 1. **确立原则**:`/speckit.constitution` —— 定义代码质量、测试、UX、性能等治理原则。
109
+ 2. **写规格**:`/speckit.specify` —— 描述「做什么 / 为什么」,先别谈技术栈。
110
+ 3. **澄清**:`/speckit.clarify` —— 补齐规格里模糊的地方(建议在 plan 之前)。
111
+ 4. **定方案**:`/speckit.plan` —— 给出技术栈与架构选择。
112
+ 5. **拆任务**:`/speckit.tasks` —— 生成可执行的任务清单。
113
+ 6. **一致性检查**:`/speckit.analyze` —— 跨规格/计划/任务做一致性与覆盖度检查(建议在 implement 之前)。
114
+ 7. **实现**:`/speckit.implement` —— 按任务清单写代码。
115
+ 8. **质量门禁(提交前强制)**:
116
+ ```bash
117
+ python .finley/scripts/gate.py
118
+ ```
119
+ 有红项必须修复后重跑,直至全绿。门禁会读取 `.finley/config.yaml`,占位命令会自动从 `package.json` scripts(前端)与 `pyproject.toml`(后端 ruff/mypy/pytest)探测。
120
+ 9. **收尾写 journal(记忆层)**:
121
+ ```bash
122
+ python .finley/scripts/add_session.py --title "本次工作标题" --summary "做了什么" --commit "abc1234"
123
+ ```
124
+ 新会话开场时,主动读取 `.finley/workspace/<开发者>/` 里最近的 journal 恢复上下文。
125
+
126
+ > Codex CLI 的 skills 模式使用 `$speckit-*`;斜杠命令的确切形态以你所用 agent 平台为准。
127
+
128
+ ## 配置 `.finley/config.yaml`
129
+
130
+ ```yaml
131
+ quality:
132
+ frontend:
133
+ lint: "<占位: 前端 lint 命令,如 npm run lint>"
134
+ typecheck: "<占位: 前端类型检查命令,如 npm run typecheck>"
135
+ test: "<占位: 前端测试命令,如 npm test>"
136
+ backend:
137
+ lint: "<占位: 后端 lint 命令,如 ruff check .>"
138
+ typecheck: "<占位: 后端类型检查命令,如 mypy .>"
139
+ test: "<占位: 后端测试命令,如 pytest>"
140
+ developer: "" # 留空则自动探测(FINLEY_DEVELOPER > git user.name > 系统用户名)
141
+ journal:
142
+ max_lines: 2000 # journal 超过该行数自动轮转
143
+ ```
144
+
145
+ - 值形如 `<占位: ...>` 时,`gate.py` 会尝试自动探测;你也可以随时替换成项目真实命令。
146
+ - 前端探测:`package.json` 的 `scripts` 里的 `lint` / `typecheck`(或 `type-check`) / `test`,并按锁文件选择 npm / pnpm / yarn / bun。
147
+ - 后端探测:`pyproject.toml` 里检测到 `ruff` → `ruff check .`;`mypy` → `mypy .`;`pytest` → `pytest`。
148
+
149
+ ---
150
+
151
+ ## 发布这个包到 npm(维护者向)
152
+
153
+ 本仓库是 `@finleywdx/ai-init` 的源码。发布步骤:
154
+
155
+ ```bash
156
+ # 1. 安装依赖
157
+ npm install
158
+
159
+ # 2. 构建(tsup 产物在 dist/;prepublishOnly 会在 publish 前自动 clean + build)
160
+ npm run build
161
+
162
+ # 3. 本地自检
163
+ node dist/cli.js --version
164
+ node dist/cli.js --help
165
+
166
+ # 4. 预览将要发布的文件清单
167
+ npm pack --dry-run
168
+
169
+ # 5. 登录 npm
170
+ npm login
171
+
172
+ # 6. 发布(scope 包默认私有,公开发布必须加 --access public)
173
+ npm publish --access public
174
+ ```
175
+
176
+ 发布内容由 `package.json` 的 `files` 白名单控制,仅包含 `dist/`、`templates/`、`README.md`、`LICENSE`。`bin` 字段把 `ai-init` 指向构建产物 `dist/cli.js`。
177
+
178
+ ### 本地联调
179
+
180
+ ```bash
181
+ npm run build
182
+ npm link # 在本仓库
183
+ # 到另一个测试项目里:
184
+ npm link @finleywdx/ai-init
185
+ ai-init init --dry-run
186
+ ```
187
+
188
+ ## 许可证
189
+
190
+ [MIT](./LICENSE)
package/dist/cli.js ADDED
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/utils/fs.ts
7
+ import { fileURLToPath } from "url";
8
+ import path from "path";
9
+ import fs from "fs";
10
+ function templatesDir() {
11
+ return fileURLToPath(new URL("../templates", import.meta.url));
12
+ }
13
+ function readOwnPackageJson() {
14
+ const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
15
+ const raw = fs.readFileSync(pkgPath, "utf-8");
16
+ return JSON.parse(raw);
17
+ }
18
+ function copyDir(srcDir, destDir, destRoot = destDir, result = { created: [], skipped: [] }) {
19
+ fs.mkdirSync(destDir, { recursive: true });
20
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
21
+ const srcPath = path.join(srcDir, entry.name);
22
+ const destPath = path.join(destDir, entry.name);
23
+ if (entry.isDirectory()) {
24
+ copyDir(srcPath, destPath, destRoot, result);
25
+ } else {
26
+ const rel = path.relative(destRoot, destPath);
27
+ if (fs.existsSync(destPath)) {
28
+ result.skipped.push(rel);
29
+ } else {
30
+ fs.copyFileSync(srcPath, destPath);
31
+ result.created.push(rel);
32
+ }
33
+ }
34
+ }
35
+ return result;
36
+ }
37
+ function readText(filePath) {
38
+ return fs.readFileSync(filePath, "utf-8");
39
+ }
40
+ function writeText(filePath, content) {
41
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
42
+ fs.writeFileSync(filePath, content, "utf-8");
43
+ }
44
+ function exists(filePath) {
45
+ return fs.existsSync(filePath);
46
+ }
47
+
48
+ // src/utils/exec.ts
49
+ import { execa } from "execa";
50
+ async function commandExists(command) {
51
+ const probe = process.platform === "win32" ? "where" : "which";
52
+ try {
53
+ await execa(probe, [command]);
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+ async function tryOutput(command, args = []) {
60
+ try {
61
+ const { stdout } = await execa(command, args);
62
+ return stdout.trim();
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ async function run(command, args = [], options = {}) {
68
+ try {
69
+ const result = await execa(command, args, { reject: false, ...options });
70
+ return {
71
+ ok: result.exitCode === 0,
72
+ exitCode: result.exitCode,
73
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
74
+ stderr: typeof result.stderr === "string" ? result.stderr : ""
75
+ };
76
+ } catch (err) {
77
+ const e = err;
78
+ return {
79
+ ok: false,
80
+ exitCode: e.exitCode,
81
+ stdout: typeof e.stdout === "string" ? e.stdout : "",
82
+ stderr: typeof e.stderr === "string" ? e.stderr : e.message ?? String(err)
83
+ };
84
+ }
85
+ }
86
+
87
+ // src/utils/logger.ts
88
+ import pc from "picocolors";
89
+ var log = {
90
+ info(msg) {
91
+ console.log(`${pc.cyan("\u203A")} ${msg}`);
92
+ },
93
+ step(msg) {
94
+ console.log(`
95
+ ${pc.bold(pc.cyan("\u25B6"))} ${pc.bold(msg)}`);
96
+ },
97
+ success(msg) {
98
+ console.log(`${pc.green("\u2714")} ${msg}`);
99
+ },
100
+ warn(msg) {
101
+ console.warn(`${pc.yellow("\u26A0")} ${msg}`);
102
+ },
103
+ error(msg) {
104
+ console.error(`${pc.red("\u2717")} ${msg}`);
105
+ },
106
+ detail(msg) {
107
+ console.log(` ${pc.dim(msg)}`);
108
+ },
109
+ title(msg) {
110
+ console.log(pc.bold(pc.magenta(msg)));
111
+ }
112
+ };
113
+
114
+ // src/steps/check-env.ts
115
+ import pc2 from "picocolors";
116
+ function parseMajor(version) {
117
+ if (!version) return null;
118
+ const m = version.match(/(\d+)\.(\d+)(?:\.(\d+))?/);
119
+ return m ? Number(m[1]) : null;
120
+ }
121
+ function parseMinor(version) {
122
+ if (!version) return null;
123
+ const m = version.match(/(\d+)\.(\d+)/);
124
+ return m ? [Number(m[1]), Number(m[2])] : null;
125
+ }
126
+ async function checkNode() {
127
+ const major = parseMajor(process.version);
128
+ const ok = major !== null && major >= 18;
129
+ return {
130
+ ok,
131
+ label: "Node.js \u2265 18",
132
+ detail: process.version,
133
+ hint: "\u8BF7\u5B89\u88C5 Node.js 18 \u6216\u66F4\u9AD8\u7248\u672C\uFF1Ahttps://nodejs.org/ \uFF08\u63A8\u8350\u7528 nvm/fnm \u7BA1\u7406\u7248\u672C\uFF09\u3002"
134
+ };
135
+ }
136
+ async function checkGit() {
137
+ const has = await commandExists("git");
138
+ const ver = has ? await tryOutput("git", ["--version"]) : null;
139
+ return {
140
+ ok: has,
141
+ label: "Git",
142
+ detail: ver ?? "\u672A\u5B89\u88C5",
143
+ hint: "\u8BF7\u5B89\u88C5 Git\uFF1Ahttps://git-scm.com/downloads"
144
+ };
145
+ }
146
+ async function checkPython() {
147
+ const bin = await commandExists("python3") ? "python3" : await commandExists("python") ? "python" : null;
148
+ const ver = bin ? await tryOutput(bin, ["--version"]) : null;
149
+ const mm = parseMinor(ver);
150
+ const ok = mm !== null && (mm[0] > 3 || mm[0] === 3 && mm[1] >= 11);
151
+ return {
152
+ ok,
153
+ label: "Python \u2265 3.11",
154
+ detail: ver ?? "\u672A\u5B89\u88C5",
155
+ hint: "\u8BF7\u5B89\u88C5 Python 3.11 \u6216\u66F4\u9AD8\u7248\u672C\uFF1Ahttps://www.python.org/downloads/ \u3002\n \u95E8\u7981\u4E0E\u8BB0\u5FC6\u811A\u672C\u7528\u7EAF\u6807\u51C6\u5E93\uFF08tomllib\uFF09\u5B9E\u73B0\uFF0C\u9700 3.11+\u3002"
156
+ };
157
+ }
158
+ async function checkUv() {
159
+ const has = await commandExists("uv");
160
+ const ver = has ? await tryOutput("uv", ["--version"]) : null;
161
+ return {
162
+ ok: has,
163
+ label: "uv\uFF08spec-kit \u4F9D\u8D56\uFF09",
164
+ detail: ver ?? "\u672A\u5B89\u88C5",
165
+ hint: "spec-kit \u901A\u8FC7 uv \u5B89\u88C5\u4E0E\u8FD0\u884C\u3002\u8BF7\u5B89\u88C5 uv\uFF1A\n macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh\n \u6216\u89C1 https://docs.astral.sh/uv/"
166
+ };
167
+ }
168
+ async function checkSpecify() {
169
+ const available = await commandExists("specify");
170
+ const version = available ? await tryOutput("specify", ["--version"]) : null;
171
+ return { available, version };
172
+ }
173
+ async function checkEnv() {
174
+ log.step("\u6B65\u9AA4 1/4\uFF1A\u73AF\u5883\u68C0\u6D4B");
175
+ const checks = await Promise.all([
176
+ checkNode(),
177
+ checkGit(),
178
+ checkPython(),
179
+ checkUv()
180
+ ]);
181
+ for (const c of checks) {
182
+ if (c.ok) {
183
+ log.success(`${c.label} \u2014 ${pc2.dim(c.detail)}`);
184
+ } else {
185
+ log.error(`${c.label} \u2014 ${pc2.dim(c.detail)}`);
186
+ }
187
+ }
188
+ const failed = checks.filter((c) => !c.ok);
189
+ if (failed.length > 0) {
190
+ console.log();
191
+ log.error(`\u73AF\u5883\u68C0\u6D4B\u672A\u901A\u8FC7\uFF1A\u7F3A\u5C11 ${failed.length} \u9879\u5FC5\u9700\u4F9D\u8D56\u3002`);
192
+ console.log(pc2.bold("\n\u8BF7\u5148\u5B89\u88C5\u4EE5\u4E0B\u4F9D\u8D56\u540E\u91CD\u8BD5\uFF1A\n"));
193
+ for (const c of failed) {
194
+ console.log(`${pc2.red("\u2022")} ${pc2.bold(c.label)}`);
195
+ console.log(` ${c.hint}
196
+ `);
197
+ }
198
+ return false;
199
+ }
200
+ const specify = await checkSpecify();
201
+ if (specify.available) {
202
+ log.success(`specify CLI \u2014 ${pc2.dim(specify.version ?? "\u5DF2\u5B89\u88C5")}`);
203
+ } else {
204
+ log.warn("\u672A\u68C0\u6D4B\u5230 spec-kit \u7684 specify CLI\u3002");
205
+ console.log(
206
+ pc2.dim(
207
+ " \u53EF\u7528 uv \u5B89\u88C5\uFF08\u66FF\u6362 <tag> \u4E3A\u6700\u65B0\u7248\u672C\uFF0C\u5982 v0.12.4\uFF09\uFF1A\n uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@<tag>\n \u5B89\u88C5\u540E\u91CD\u8DD1 ai-init init \u5373\u53EF\u81EA\u52A8\u7F16\u6392 spec-kit\u3002"
208
+ )
209
+ );
210
+ }
211
+ return true;
212
+ }
213
+
214
+ // src/steps/run-speckit.ts
215
+ import pc3 from "picocolors";
216
+ var DEFAULT_INTEGRATIONS = [
217
+ "cursor",
218
+ "claude",
219
+ "codex",
220
+ "gemini",
221
+ "copilot"
222
+ ];
223
+ async function runSpeckit(options) {
224
+ const {
225
+ cwd,
226
+ integrations = DEFAULT_INTEGRATIONS,
227
+ ignoreAgentTools = true,
228
+ dryRun = false
229
+ } = options;
230
+ log.step("\u6B65\u9AA4 2/4\uFF1A\u7F16\u6392 spec-kit");
231
+ const list = [...integrations];
232
+ if (list.length === 0) {
233
+ log.warn("\u672A\u6307\u5B9A\u4EFB\u4F55 integration\uFF0C\u8DF3\u8FC7 spec-kit \u7F16\u6392\u3002");
234
+ return { ok: true, installed: [], failed: [], skipped: true };
235
+ }
236
+ const specify = await checkSpecify();
237
+ if (!specify.available && !dryRun) {
238
+ log.error("\u672A\u627E\u5230 specify CLI\uFF0C\u65E0\u6CD5\u7F16\u6392 spec-kit\u3002");
239
+ console.log(
240
+ pc3.dim(
241
+ " \u8BF7\u5148\u5B89\u88C5 spec-kit\uFF08\u66FF\u6362 <tag> \u4E3A\u6700\u65B0\u7248\u672C\uFF0C\u5982 v0.12.4\uFF09\uFF1A\n uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@<tag>\n \u5B89\u88C5\u540E\u91CD\u8DD1 ai-init init\u3002\u672C\u6B21\u5C06\u8DF3\u8FC7 spec-kit\uFF0C\u4EC5\u94FA\u8BBE Finley \u589E\u91CF\u5C42\u3002"
242
+ )
243
+ );
244
+ return { ok: false, installed: [], failed: [...list], skipped: true };
245
+ }
246
+ const installed = [];
247
+ const failed = [];
248
+ const [first, ...rest] = list;
249
+ const initArgs = ["init", ".", "--integration", first, "--force"];
250
+ if (ignoreAgentTools) initArgs.push("--ignore-agent-tools");
251
+ log.info(`\u521D\u59CB\u5316 spec-kit\uFF08\u9996\u4E2A integration\uFF09\uFF1A${pc3.bold(first)}`);
252
+ log.detail(`$ specify ${initArgs.join(" ")}`);
253
+ if (dryRun) {
254
+ installed.push(first);
255
+ } else {
256
+ const res = await run("specify", initArgs, { cwd, stdio: "inherit" });
257
+ if (res.ok) {
258
+ log.success(`spec-kit \u5DF2\u521D\u59CB\u5316\u5E76\u5B89\u88C5 integration\uFF1A${first}`);
259
+ installed.push(first);
260
+ } else {
261
+ log.error(`specify init \u5931\u8D25\uFF08integration=${first}\uFF0C\u9000\u51FA\u7801 ${res.exitCode ?? "?"}\uFF09\u3002`);
262
+ if (res.stderr) log.detail(res.stderr.split("\n").slice(0, 8).join("\n"));
263
+ failed.push(first);
264
+ return { ok: false, installed, failed: [...list], skipped: false };
265
+ }
266
+ }
267
+ for (const name of rest) {
268
+ const args = ["integration", "install", name, "--force"];
269
+ log.info(`\u8FFD\u52A0 integration\uFF1A${pc3.bold(name)}`);
270
+ log.detail(`$ specify ${args.join(" ")}`);
271
+ if (dryRun) {
272
+ installed.push(name);
273
+ continue;
274
+ }
275
+ const res = await run("specify", args, { cwd, stdio: "inherit" });
276
+ if (res.ok) {
277
+ log.success(`\u5DF2\u5B89\u88C5 integration\uFF1A${name}`);
278
+ installed.push(name);
279
+ } else {
280
+ log.warn(`integration \u5B89\u88C5\u5931\u8D25\uFF1A${name}\uFF08\u9000\u51FA\u7801 ${res.exitCode ?? "?"}\uFF09\uFF0C\u7EE7\u7EED\u5176\u4F59\u9879\u3002`);
281
+ if (res.stderr) log.detail(res.stderr.split("\n").slice(0, 6).join("\n"));
282
+ failed.push(name);
283
+ }
284
+ }
285
+ const ok = failed.length === 0;
286
+ if (ok) {
287
+ log.success(`spec-kit \u7F16\u6392\u5B8C\u6210\uFF0C\u5DF2\u5B89\u88C5 integration\uFF1A${installed.join(", ")}`);
288
+ } else {
289
+ log.warn(
290
+ `spec-kit \u7F16\u6392\u90E8\u5206\u5B8C\u6210\u3002\u6210\u529F\uFF1A${installed.join(", ") || "\u65E0"}\uFF1B\u5931\u8D25\uFF1A${failed.join(", ")}\u3002`
291
+ );
292
+ }
293
+ return { ok, installed, failed, skipped: false };
294
+ }
295
+
296
+ // src/steps/scaffold-finley.ts
297
+ import path2 from "path";
298
+ import pc4 from "picocolors";
299
+ var MARKER_START = "<!-- FINLEY:START -->";
300
+ var MARKER_END = "<!-- FINLEY:END -->";
301
+ function scaffoldFinley(options) {
302
+ const { cwd } = options;
303
+ const tpl = templatesDir();
304
+ log.step("\u6B65\u9AA4 3/4\uFF1A\u94FA\u8BBE Finley \u589E\u91CF\u5C42");
305
+ const finleyResult = copyDir(path2.join(tpl, "finley"), path2.join(cwd, ".finley"), cwd);
306
+ const skillsResult = copyDir(
307
+ path2.join(tpl, "agents-skills"),
308
+ path2.join(cwd, ".agents", "skills"),
309
+ cwd
310
+ );
311
+ const created = [...finleyResult.created, ...skillsResult.created];
312
+ const skipped = [...finleyResult.skipped, ...skillsResult.skipped];
313
+ for (const f of created) log.success(`\u521B\u5EFA ${f}`);
314
+ for (const f of skipped) log.detail(`\u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7 ${pc4.dim(f)}`);
315
+ mergeAgentsMd(cwd, tpl);
316
+ }
317
+ function mergeAgentsMd(cwd, tpl) {
318
+ const agentsPath = path2.join(cwd, "AGENTS.md");
319
+ const section = readText(path2.join(tpl, "AGENTS.section.md")).trim();
320
+ log.step("\u6B65\u9AA4 4/4\uFF1A\u5408\u5E76 AGENTS.md");
321
+ if (!exists(agentsPath)) {
322
+ writeText(agentsPath, section + "\n");
323
+ log.success("\u521B\u5EFA AGENTS.md \u5E76\u5199\u5165 Finley \u533A\u5757\u3002");
324
+ return;
325
+ }
326
+ const original = readText(agentsPath);
327
+ const startIdx = original.indexOf(MARKER_START);
328
+ const endIdx = original.indexOf(MARKER_END);
329
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
330
+ const before = original.slice(0, startIdx);
331
+ const after = original.slice(endIdx + MARKER_END.length);
332
+ const updated = `${before}${section}${after}`;
333
+ if (updated === original) {
334
+ log.detail("AGENTS.md \u7684 Finley \u533A\u5757\u5DF2\u662F\u6700\u65B0\uFF0C\u65E0\u9700\u6539\u52A8\u3002");
335
+ } else {
336
+ writeText(agentsPath, updated);
337
+ log.success("\u66F4\u65B0 AGENTS.md \u4E2D\u5DF2\u6709\u7684 Finley \u533A\u5757\u3002");
338
+ }
339
+ return;
340
+ }
341
+ const sep = original.endsWith("\n") ? "\n" : "\n\n";
342
+ writeText(agentsPath, original + sep + section + "\n");
343
+ log.success("\u5728\u5DF2\u6709 AGENTS.md \u672B\u5C3E\u8FFD\u52A0 Finley \u533A\u5757\u3002");
344
+ }
345
+
346
+ // src/commands/init.ts
347
+ import pc5 from "picocolors";
348
+ async function init(options = {}) {
349
+ const cwd = options.cwd ?? process.cwd();
350
+ const integrations = options.integrations?.length ? options.integrations : [...DEFAULT_INTEGRATIONS];
351
+ log.title("\n\u{1F331} Finley \xB7 AI \u5DE5\u7A0B\u5316\u5DE5\u4F5C\u6D41\u521D\u59CB\u5316\u5668\n");
352
+ log.info(`\u76EE\u6807\u76EE\u5F55\uFF1A${pc5.bold(cwd)}`);
353
+ if (options.dryRun) log.warn("dry-run \u6A21\u5F0F\uFF1A\u4E0D\u4F1A\u771F\u6B63\u8C03\u7528 specify\uFF0C\u4E5F\u4E0D\u4F1A\u6C61\u67D3\u76EE\u5F55\u3002");
354
+ const envOk = await checkEnv();
355
+ if (!envOk) {
356
+ log.error("\n\u521D\u59CB\u5316\u5DF2\u4E2D\u6B62\uFF1A\u8BF7\u5B89\u88C5\u4E0A\u9762\u5217\u51FA\u7684\u4F9D\u8D56\u540E\u91CD\u8BD5\u3002");
357
+ return 1;
358
+ }
359
+ let speckitSkipped = false;
360
+ if (options.skipSpeckit) {
361
+ log.step("\u6B65\u9AA4 2/4\uFF1A\u7F16\u6392 spec-kit");
362
+ log.warn("\u5DF2\u901A\u8FC7 --skip-speckit \u8DF3\u8FC7 spec-kit \u7F16\u6392\u3002");
363
+ speckitSkipped = true;
364
+ } else {
365
+ const res = await runSpeckit({
366
+ cwd,
367
+ integrations,
368
+ ignoreAgentTools: options.ignoreAgentTools ?? true,
369
+ dryRun: options.dryRun
370
+ });
371
+ speckitSkipped = res.skipped;
372
+ }
373
+ if (options.dryRun) {
374
+ log.step("\u6B65\u9AA4 3/4\uFF1A\u94FA\u8BBE Finley \u589E\u91CF\u5C42");
375
+ log.warn("dry-run\uFF1A\u8DF3\u8FC7\u590D\u5236\u6A21\u677F\u3002");
376
+ log.step("\u6B65\u9AA4 4/4\uFF1A\u5408\u5E76 AGENTS.md");
377
+ log.warn("dry-run\uFF1A\u8DF3\u8FC7\u5199\u5165 AGENTS.md\u3002");
378
+ } else {
379
+ scaffoldFinley({ cwd });
380
+ }
381
+ console.log();
382
+ log.title("\u2705 \u521D\u59CB\u5316\u5B8C\u6210");
383
+ printNextSteps(speckitSkipped);
384
+ return 0;
385
+ }
386
+ function printNextSteps(speckitSkipped) {
387
+ console.log(pc5.bold("\n\u63A5\u4E0B\u6765\uFF1A"));
388
+ const lines = [];
389
+ if (speckitSkipped) {
390
+ lines.push(
391
+ "\u5148\u5B89\u88C5 spec-kit \u540E\u91CD\u8DD1\u4EE5\u8865\u5168\u547D\u4EE4\uFF1A",
392
+ " uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@<tag>"
393
+ );
394
+ }
395
+ lines.push(
396
+ "\u7F16\u8F91 .finley/config.yaml\uFF0C\u628A quality \u91CC\u7684\u5360\u4F4D\u66FF\u6362\u6210\u9879\u76EE\u771F\u5B9E\u547D\u4EE4\uFF08\u4E5F\u53EF\u7559\u7ED9 gate.py \u81EA\u52A8\u63A2\u6D4B\uFF09\u3002",
397
+ "\u5728 AI \u52A9\u624B\u91CC\u6309 spec-kit \u6D41\u7A0B\u63A8\u8FDB\uFF1A",
398
+ " /speckit.constitution \u2192 /speckit.specify \u2192 /speckit.clarify \u2192 /speckit.plan \u2192",
399
+ " /speckit.tasks \u2192 /speckit.analyze \u2192 /speckit.implement",
400
+ "\u5B9E\u73B0\u5B8C\u6210\u3001\u63D0\u4EA4\u524D\u5FC5\u987B\u8DD1\u8D28\u91CF\u95E8\u7981\uFF1A",
401
+ " python .finley/scripts/gate.py",
402
+ "\u4E00\u6BB5\u5DE5\u4F5C\u6536\u5C3E\u65F6\u8BB0\u5F55 journal\uFF1A",
403
+ ' python .finley/scripts/add_session.py --title "..." --summary "..."'
404
+ );
405
+ for (const l of lines) console.log(` ${pc5.cyan("\u203A")} ${l}`);
406
+ console.log(pc5.dim("\n\u8BE6\u89C1\u9879\u76EE\u6839\u76EE\u5F55\u7684 AGENTS.md \u4E0E .agents/skills/\u3002"));
407
+ }
408
+
409
+ // src/cli.ts
410
+ function main() {
411
+ const pkg = readOwnPackageJson();
412
+ const program = new Command();
413
+ program.name("ai-init").description(
414
+ "AI \u5DE5\u7A0B\u5316\u5DE5\u4F5C\u6D41\u521D\u59CB\u5316\u5668\uFF1A\u628A spec-kit \u5DE5\u4F5C\u6D41 + \u8BB0\u5FC6\u5C42 + \u8D28\u91CF\u95E8\u7981\u521D\u59CB\u5316\u8FDB\u4F60\u7684\u9879\u76EE\u3002"
415
+ ).version(pkg.version, "-v, --version", "\u8F93\u51FA\u7248\u672C\u53F7");
416
+ program.command("init").description("\u5728\u5F53\u524D\u76EE\u5F55\u521D\u59CB\u5316 Finley \u5DE5\u4F5C\u6D41\uFF08\u7F16\u6392 spec-kit + \u94FA\u8BBE\u589E\u91CF\u5C42 + \u5408\u5E76 AGENTS.md\uFF09").option(
417
+ "-i, --integration <names>",
418
+ "\u4EE5\u9017\u53F7\u5206\u9694\u6307\u5B9A\u8981\u5B89\u88C5\u7684 spec-kit integration\uFF08\u9ED8\u8BA4 cursor,claude,codex,gemini,copilot\uFF09"
419
+ ).option("--skip-speckit", "\u8DF3\u8FC7 spec-kit \u7F16\u6392\uFF0C\u4EC5\u94FA\u8BBE Finley \u589E\u91CF\u5C42").option("--no-ignore-agent-tools", "\u4E0D\u5411 specify \u4F20 --ignore-agent-tools\uFF08\u9ED8\u8BA4\u4F1A\u4F20\uFF09").option("--dry-run", "\u53EA\u6253\u5370\u5C06\u6267\u884C\u7684\u64CD\u4F5C\uFF0C\u4E0D\u771F\u6B63\u8C03\u7528 specify\u3001\u4E0D\u5199\u76D8").action(async (opts) => {
420
+ const integrations = typeof opts.integration === "string" ? opts.integration.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
421
+ try {
422
+ const code = await init({
423
+ integrations,
424
+ skipSpeckit: opts.skipSpeckit,
425
+ // commander 的 --no-ignore-agent-tools 会把 ignoreAgentTools 置为 false
426
+ ignoreAgentTools: opts.ignoreAgentTools,
427
+ dryRun: opts.dryRun
428
+ });
429
+ process.exit(code);
430
+ } catch (err) {
431
+ log.error(`\u521D\u59CB\u5316\u8FC7\u7A0B\u4E2D\u53D1\u751F\u672A\u9884\u671F\u9519\u8BEF\uFF1A${err.message}`);
432
+ process.exit(1);
433
+ }
434
+ });
435
+ program.parse(process.argv);
436
+ }
437
+ main();