@andyqiu/codeforge 0.3.11 → 0.3.13

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/bin/codeforge.mjs CHANGED
@@ -14,6 +14,8 @@
14
14
  * rollback [--target=<path>] [--dry-run]
15
15
  * 恢复到上一次 backup(auto_install 失败时手动救场)
16
16
  * runtime where [<path>] 打印当前项目对应的全局运行时目录
17
+ * adr-init [--force] [--dry-run] [--write-prepare] [--no-pre-push]
18
+ * 把 ADR 体系(hooks + scripts + 模板)下发到当前 git 项目
17
19
  * help [<cmd>] 打印帮助
18
20
  *
19
21
  * 默认 --project(项目级,最安全)。
@@ -299,6 +301,79 @@ function cmdRuntime(args) {
299
301
  return r.status ?? 1
300
302
  }
301
303
 
304
+ // ────────────────────────────────────────────────────────────────────
305
+ // 子命令:adr-init —— 把 ADR 体系下发到当前 git 项目 (ADR:adr-distribution-init)
306
+ // ────────────────────────────────────────────────────────────────────
307
+ //
308
+ // 实现走 dist/adr-init.js 这个**确定路径**(由 scripts/build-with-version.mjs 第二
309
+ // bun build entry 产出)。既适用 npm 包安装(pkgRoot/dist/adr-init.js),也适用本
310
+ // 仓库 dev 期(repoRoot/dist/adr-init.js)。没有 fallback —— 缺 dist 文件就明确报错
311
+ // 提示跑 npm run build,比 ts-node / 源码动态 import 简单可靠。
312
+ async function cmdAdrInit(args) {
313
+ const cwd = process.cwd()
314
+ const opts = {
315
+ cwd,
316
+ force: !!args.flags.force,
317
+ dryRun: !!args.flags["dry-run"] || !!args.flags.dryRun,
318
+ writePrepare: !!args.flags["write-prepare"] || !!args.flags.writePrepare,
319
+ installPrePush: args.flags["no-pre-push"] ? false : true,
320
+ }
321
+
322
+ const adrInitPath = path.join(REPO_ROOT, "dist", "adr-init.js")
323
+ if (!existsSync(adrInitPath)) {
324
+ err(`找不到 ${adrInitPath}`)
325
+ err(`如果是从源码运行:请先跑 npm run build`)
326
+ err(`如果是 npm 安装:可能是发布缺文件,请重装或反馈给 @andyqiu/codeforge`)
327
+ return 1
328
+ }
329
+
330
+ log(`CodeForge adr-init —— 下发 ADR 体系到 ${cwd}`)
331
+ log(` force : ${opts.force}`)
332
+ log(` dry-run : ${opts.dryRun}`)
333
+ log(` write-prepare : ${opts.writePrepare}`)
334
+ log(` install pre-push: ${opts.installPrePush}`)
335
+ hr()
336
+
337
+ // ESM dynamic import 必须用 file:// URL,Win 路径才能正确解析
338
+ let mod
339
+ try {
340
+ mod = await import(url.pathToFileURL(adrInitPath).href)
341
+ } catch (e) {
342
+ err(`加载 ${adrInitPath} 失败:${e?.message ?? e}`)
343
+ return 1
344
+ }
345
+ if (typeof mod.runAdrInit !== "function") {
346
+ err(`${adrInitPath} 缺少 runAdrInit 导出(bundle 可能损坏,请重建 dist/)`)
347
+ return 1
348
+ }
349
+
350
+ let result
351
+ try {
352
+ result = await mod.runAdrInit(opts)
353
+ } catch (e) {
354
+ err(`adr-init 执行抛错:${e?.stack ?? e}`)
355
+ return 1
356
+ }
357
+
358
+ if (result.wrote?.length) {
359
+ ok(`已写入 ${result.wrote.length} 个文件:`)
360
+ for (const f of result.wrote) console.log(` + ${f}`)
361
+ }
362
+ if (result.skipped?.length) {
363
+ warn(`已跳过 ${result.skipped.length} 个已存在文件(用 --force 覆盖):`)
364
+ for (const f of result.skipped) console.log(` · ${f}`)
365
+ }
366
+ if (result.backedUp?.length) {
367
+ ok(`已备份 ${result.backedUp.length} 个原文件:`)
368
+ for (const f of result.backedUp) console.log(` ↳ ${f}`)
369
+ }
370
+ for (const s of result.suggestions ?? []) log(s)
371
+ for (const w of result.warnings ?? []) warn(w)
372
+ if (opts.dryRun) log("dry-run 模式:以上未实际写盘")
373
+ hr()
374
+ return result.ok === false ? 1 : 0
375
+ }
376
+
302
377
  // ────────────────────────────────────────────────────────────────────
303
378
 
304
379
  // 子命令:help
@@ -313,6 +388,8 @@ function cmdHelp() {
313
388
  codeforge version
314
389
  codeforge rollback [--target=<path>] [--dry-run] # 恢复最近 backup(auto_install 失败救场)
315
390
  codeforge runtime where [<path>] # 打印当前项目的全局运行时目录
391
+ codeforge adr-init [--force] [--dry-run] [--write-prepare] [--no-pre-push]
392
+ # 把 ADR 校验体系(hooks + scripts + 模板)下发到当前 git 项目
316
393
 
317
394
  一行命令(远程安装):
318
395
  npx @andyqiu/codeforge install
@@ -326,6 +403,12 @@ function cmdHelp() {
326
403
  --enable-legacy-tools 启用旧版 file-based tools(默认禁用,避免 zod 跨实例 bug)
327
404
  --target rollback 子命令:指定 index.js 路径(默认自动探测)
328
405
 
406
+ 参数(adr-init 专用):
407
+ --force 覆盖已存在文件(覆盖前自动 .bak.<ts> 备份)
408
+ --dry-run 只打印将要执行的写入计划,不实际写盘
409
+ --write-prepare 合并 git config core.hooksPath 到 package.json scripts.prepare(写前 backup package.json;默认只建议不写)
410
+ --no-pre-push 跳过 .githooks/pre-push 生成(只装 pre-commit)
411
+
329
412
  CodeForge 是 opencode 1.14+ 的单 bundle plugin(持续跟主线,src/index.ts → dist/index.js),
330
413
  通过往 opencode.json 写一行 plugin entry 来装载。
331
414
  不支持 Cursor / Claude Code / Codex —— 它们的扩展机制不同,强行映射只会变成
@@ -336,9 +419,12 @@ CodeForge 是 opencode 1.14+ 的单 bundle plugin(持续跟主线,src/index.
336
419
  }
337
420
 
338
421
  // ────────────────────────────────────────────────────────────────────
339
- // main
422
+ // main —— async (ADR:adr-distribution-init: adr-init 需 dynamic import)
340
423
  // ────────────────────────────────────────────────────────────────────
341
- function main() {
424
+ //
425
+ // 改成 async 后所有同步 case `return <number>` 自动被包成 Promise<number>,外层 .then
426
+ // 拿到数字调 process.exit 即可;现有 case 的 exit code 行为不变。
427
+ async function main() {
342
428
  const argv = process.argv.slice(2)
343
429
  if (argv.length === 0 || ["-h", "--help"].includes(argv[0])) {
344
430
  return cmdHelp()
@@ -366,6 +452,8 @@ function main() {
366
452
  return cmdRollback(args)
367
453
  case "runtime":
368
454
  return cmdRuntime(args)
455
+ case "adr-init":
456
+ return cmdAdrInit(args)
369
457
 
370
458
  case "help":
371
459
  return cmdHelp()
@@ -376,4 +464,9 @@ function main() {
376
464
  }
377
465
  }
378
466
 
379
- process.exit(main())
467
+ main()
468
+ .then((code) => process.exit(typeof code === "number" ? code : 0))
469
+ .catch((e) => {
470
+ err(`未捕获错误:${e?.stack ?? e}`)
471
+ process.exit(1)
472
+ })
package/codeforge.json CHANGED
@@ -65,6 +65,30 @@
65
65
  "anthropic/claude-sonnet-4-6",
66
66
  "google/gemini-3-pro"
67
67
  ]
68
+ },
69
+ "discover": {
70
+ "_doc": "需求澄清者(采访人设):长程对话 + 强 thinking,复用 planner 同档(Opus deep)。",
71
+ "model": "anthropic/claude-opus-4-7",
72
+ "category": "deep",
73
+ "tier": "deep",
74
+ "thinking": { "type": "enabled", "budget_tokens": 6000 },
75
+ "fallback_models": [
76
+ "openai/gpt-5.5",
77
+ "anthropic/claude-sonnet-4-6",
78
+ "google/gemini-3-pro"
79
+ ]
80
+ },
81
+ "discover-challenger": {
82
+ "_doc": "强对抗子 agent:跨家族避免同源偏见(参考 reviewer 选型),结论由离散触发组合锁死。",
83
+ "model": "openai/gpt-5.5",
84
+ "category": "ultrabrain",
85
+ "tier": "deep",
86
+ "thinking": { "type": "enabled", "budget_tokens": 3000 },
87
+ "fallback_models": [
88
+ "anthropic/claude-opus-4-7",
89
+ "anthropic/claude-sonnet-4-6",
90
+ "google/gemini-3-pro"
91
+ ]
68
92
  }
69
93
  },
70
94
 
@@ -0,0 +1,67 @@
1
+ ---
2
+ description: 一键把 ADR 体系(adr-check + git hooks + 模板)下发到当前 git 项目
3
+ ---
4
+
5
+ <!-- ADR:adr-distribution-init -->
6
+ <!--
7
+ codeforge 元数据(opencode 不读,由 plugins / workflow-engine 解析):
8
+ name: adr-init
9
+ version: 1.0.0
10
+ trigger_workflow: null
11
+ allowed_tools: adr-init
12
+ 说明:单 tool 命令,把 CodeForge 的 ADR 体系一键下发到当前 git 项目
13
+ -->
14
+
15
+
16
+ # /adr-init — 一键下发 ADR 体系
17
+
18
+ 把 CodeForge 维护的 ADR 体系(决策先行 + 三向引用 + 触发式补全 + README 索引同步)下发到**当前 git 项目**:
19
+
20
+ - `scripts/adr-check.mjs`、`scripts/adr-index-sync.mjs`(泛化版,支持 `ADR_ROOT` / `ADR_DIR` 环境变量)
21
+ - `.githooks/pre-commit`(staged 模式校验)、`.githooks/pre-push`(last-commit 模式校验)
22
+ - `docs/adr/README.md`(索引骨架,含 HTML 注释 marker)、`docs/adr/template.md`
23
+ - `.github/pull_request_template.md`(ADR checklist)
24
+
25
+ 零 npm 依赖:用 `git config core.hooksPath .githooks`(自动启用,无需 husky)。
26
+
27
+ ## 输入
28
+
29
+ 可选参数:$ARGUMENTS
30
+
31
+ 支持的 flag(在用户对话里说,由 LLM 解析后传给 `adr-init` tool):
32
+
33
+ - `--force`:覆盖已存在文件(覆盖前自动 `.bak.<时间戳>` 备份)
34
+ - `--dry-run`:只打印将要执行的写入计划,不实际写盘
35
+ - `--write-prepare`:(npm 项目)自动合并 `git config core.hooksPath .githooks` 到 `package.json scripts.prepare`;写前自动 backup package.json
36
+ - `--no-pre-push`:只装 pre-commit,不装 pre-push
37
+
38
+ ## 用法
39
+
40
+ ```
41
+ /adr-init
42
+ /adr-init --dry-run
43
+ /adr-init --force
44
+ /adr-init --write-prepare
45
+ ```
46
+
47
+ 也可在 shell 跑等价命令:
48
+
49
+ ```
50
+ npx @andyqiu/codeforge adr-init [--force] [--dry-run] [--write-prepare] [--no-pre-push]
51
+ ```
52
+
53
+ ## 行为
54
+
55
+ 1. 检查当前目录是否 git 仓库(否则报错并停止)
56
+ 2. 解析 `assets/adr-init/` 资产根(兼容 npm 全局 / npx / 本仓库 dev 三种安装形态)
57
+ 3. 按清单复制文件:
58
+ - 默认 `exists → skip`;`--force` 覆盖前自动 `.bak.<ts>` 备份
59
+ - hook 文件自动 `chmod 0o755`(Windows 上吞错)
60
+ 4. 运行 `git config core.hooksPath .githooks` 启用 hooks
61
+ 5. 报告写入 / 跳过 / 备份 / 建议 / 警告
62
+
63
+ ## 失败回退
64
+
65
+ - 当前目录非 git 仓库 → 返回 `reason: "not_git_repo"`,建议用户先 `git init`
66
+ - 资产路径找不到(npm 包 `files` 未含 `assets/`)→ 返回 `reason: "assets_not_found"`,提示重装
67
+ - `git config` 失败 → 仍返回 ok=true,但 warnings 含具体错误,请用户手动执行
@@ -0,0 +1,207 @@
1
+ // lib/adr-init.ts
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, promises as fsp } from "node:fs";
4
+ import * as path from "node:path";
5
+ import * as url from "node:url";
6
+ function resolveAssetsRoot() {
7
+ const here = path.dirname(url.fileURLToPath(import.meta.url));
8
+ let dir = here;
9
+ for (let i = 0;i < 6; i++) {
10
+ if (existsSync(path.join(dir, "package.json")) && existsSync(path.join(dir, "assets", "adr-init"))) {
11
+ return path.join(dir, "assets", "adr-init");
12
+ }
13
+ const parent = path.dirname(dir);
14
+ if (parent === dir)
15
+ break;
16
+ dir = parent;
17
+ }
18
+ throw new Error(`assets/adr-init/ 未找到(从 ${here} 上溯 6 层);` + `如果是 npm 安装请检查 package.json "files" 字段是否包含 "assets/"`);
19
+ }
20
+ function isGitRepo(cwd) {
21
+ try {
22
+ const r = spawnSync("git", ["rev-parse", "--show-toplevel"], {
23
+ cwd,
24
+ stdio: "pipe",
25
+ encoding: "utf8"
26
+ });
27
+ return r.status === 0;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+ function runGitConfigHooksPath(cwd) {
33
+ try {
34
+ const r = spawnSync("git", ["config", "core.hooksPath", ".githooks"], {
35
+ cwd,
36
+ stdio: "pipe",
37
+ encoding: "utf8"
38
+ });
39
+ if (r.status === 0)
40
+ return { ok: true };
41
+ return { ok: false, error: (r.stderr ?? "").trim() || `exit=${r.status}` };
42
+ } catch (e) {
43
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
44
+ }
45
+ }
46
+ async function runAdrInit(opts = {}) {
47
+ const cwd = path.resolve(opts.cwd ?? process.cwd());
48
+ const force = !!opts.force;
49
+ const dryRun = !!opts.dryRun;
50
+ const writePrepare = !!opts.writePrepare;
51
+ const installPrePush = opts.installPrePush !== false;
52
+ const result = {
53
+ ok: true,
54
+ wrote: [],
55
+ skipped: [],
56
+ backedUp: [],
57
+ suggestions: [],
58
+ warnings: [],
59
+ dryRun
60
+ };
61
+ if (!isGitRepo(cwd)) {
62
+ result.ok = false;
63
+ result.reason = "not_git_repo";
64
+ result.warnings.push(`${cwd} 不是 git 仓库;adr-init 需要 git 才能下发 hooks`);
65
+ return result;
66
+ }
67
+ let assetsRoot;
68
+ try {
69
+ assetsRoot = resolveAssetsRoot();
70
+ } catch (e) {
71
+ result.ok = false;
72
+ result.reason = "assets_not_found";
73
+ result.warnings.push(e instanceof Error ? e.message : String(e));
74
+ return result;
75
+ }
76
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
77
+ const plan = [
78
+ { src: "scripts/adr-check.mjs", dst: "scripts/adr-check.mjs" },
79
+ { src: "scripts/adr-index-sync.mjs", dst: "scripts/adr-index-sync.mjs" },
80
+ { src: "docs/adr/README.md", dst: "docs/adr/README.md" },
81
+ { src: "docs/adr/template.md", dst: "docs/adr/template.md" },
82
+ {
83
+ src: ".github/pull_request_template.md",
84
+ dst: ".github/pull_request_template.md"
85
+ },
86
+ { src: "githooks/pre-commit.sh", dst: ".githooks/pre-commit", chmod: 493 }
87
+ ];
88
+ if (installPrePush) {
89
+ plan.push({
90
+ src: "githooks/pre-push.sh",
91
+ dst: ".githooks/pre-push",
92
+ chmod: 493
93
+ });
94
+ }
95
+ for (const item of plan) {
96
+ const srcAbs = path.join(assetsRoot, item.src);
97
+ const dstAbs = path.join(cwd, item.dst);
98
+ if (!existsSync(srcAbs)) {
99
+ result.warnings.push(`资产缺失:${item.src}(跳过 ${item.dst})`);
100
+ continue;
101
+ }
102
+ if (existsSync(dstAbs)) {
103
+ if (!force) {
104
+ result.skipped.push(item.dst);
105
+ continue;
106
+ }
107
+ const bakRel = `${item.dst}.bak.${ts}`;
108
+ if (!dryRun) {
109
+ try {
110
+ await fsp.copyFile(dstAbs, path.join(cwd, bakRel));
111
+ } catch (e) {
112
+ result.ok = false;
113
+ result.reason = "io_error";
114
+ result.warnings.push(`备份 ${item.dst} 失败:${e instanceof Error ? e.message : String(e)}`);
115
+ return result;
116
+ }
117
+ }
118
+ result.backedUp.push(bakRel);
119
+ }
120
+ if (!dryRun) {
121
+ try {
122
+ await fsp.mkdir(path.dirname(dstAbs), { recursive: true });
123
+ await fsp.copyFile(srcAbs, dstAbs);
124
+ if (item.chmod !== undefined) {
125
+ try {
126
+ await fsp.chmod(dstAbs, item.chmod);
127
+ } catch (e) {
128
+ if (process.platform !== "win32") {
129
+ result.warnings.push(`chmod ${item.dst} 失败:${e instanceof Error ? e.message : String(e)}`);
130
+ }
131
+ }
132
+ }
133
+ } catch (e) {
134
+ result.ok = false;
135
+ result.reason = "io_error";
136
+ result.warnings.push(`写入 ${item.dst} 失败:${e instanceof Error ? e.message : String(e)}`);
137
+ return result;
138
+ }
139
+ }
140
+ result.wrote.push(item.dst);
141
+ }
142
+ if (!dryRun) {
143
+ const r = runGitConfigHooksPath(cwd);
144
+ if (r.ok) {
145
+ result.suggestions.push("已运行:git config core.hooksPath .githooks(hooks 已启用)");
146
+ } else {
147
+ result.warnings.push(`自动启用 hooks 失败:${r.error ?? "未知错误"};请手动执行 git config core.hooksPath .githooks`);
148
+ }
149
+ } else {
150
+ result.suggestions.push("[dry-run] 将运行:git config core.hooksPath .githooks");
151
+ }
152
+ const pkgPath = path.join(cwd, "package.json");
153
+ const isNpm = existsSync(pkgPath);
154
+ if (isNpm) {
155
+ if (writePrepare) {
156
+ try {
157
+ const raw = await fsp.readFile(pkgPath, "utf8");
158
+ const pkg = JSON.parse(raw);
159
+ const existing = (pkg.scripts && typeof pkg.scripts.prepare === "string" ? pkg.scripts.prepare : "") ?? "";
160
+ const target = "git config core.hooksPath .githooks";
161
+ if (existing.includes(target)) {
162
+ result.suggestions.push(`package.json scripts.prepare 已含目标命令,无需改动`);
163
+ } else {
164
+ const bakRel = `package.json.bak.${ts}`;
165
+ if (!dryRun) {
166
+ try {
167
+ await fsp.copyFile(pkgPath, path.join(cwd, bakRel));
168
+ } catch (e) {
169
+ result.warnings.push(`备份 package.json 失败:${e instanceof Error ? e.message : String(e)}`);
170
+ }
171
+ }
172
+ result.backedUp.push(bakRel);
173
+ if (existing.includes("husky")) {
174
+ result.warnings.push(`package.json scripts.prepare 已含 husky,仍合并写入;如有问题可回滚 ${bakRel}`);
175
+ }
176
+ const merged = existing ? `${existing} && ${target}` : target;
177
+ pkg.scripts = { ...pkg.scripts ?? {}, prepare: merged };
178
+ if (!dryRun) {
179
+ try {
180
+ await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + `
181
+ `, "utf8");
182
+ } catch (e) {
183
+ result.ok = false;
184
+ result.reason = "io_error";
185
+ result.warnings.push(`写入 package.json 失败:${e instanceof Error ? e.message : String(e)}`);
186
+ return result;
187
+ }
188
+ }
189
+ result.wrote.push("package.json");
190
+ }
191
+ } catch (e) {
192
+ result.warnings.push(`读取 package.json 失败:${e instanceof Error ? e.message : String(e)}`);
193
+ }
194
+ } else {
195
+ result.suggestions.push(`(可选) 在 package.json scripts.prepare 追加:"git config core.hooksPath .githooks",` + `或下次跑 codeforge adr-init --write-prepare 自动合并(写前自动 backup package.json)`);
196
+ }
197
+ } else {
198
+ result.suggestions.push("非 npm 项目:请在 README / AGENTS.md 提醒首次 clone 后跑 git config core.hooksPath .githooks");
199
+ }
200
+ return result;
201
+ }
202
+ var adr_init_default = { runAdrInit, resolveAssetsRoot };
203
+ export {
204
+ runAdrInit,
205
+ resolveAssetsRoot,
206
+ adr_init_default as default
207
+ };