@andyqiu/codeforge 0.5.14 → 0.5.16

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/README.md CHANGED
@@ -7,18 +7,10 @@ CodeForge — [opencode](https://github.com/sst/opencode) 的零侵入扩展包
7
7
  需要 **opencode ≥ 1.14** 和 **Node ≥ 20**。
8
8
 
9
9
  ```bash
10
- # 方式一:npm 全局安装(推荐,装完直接有 codeforge 命令)
11
10
  npm install -g @andyqiu/codeforge
12
- codeforge install --global
13
-
14
- # 方式二:npx 免安装(一次性,不占全局)
15
- npx @andyqiu/codeforge install --global
16
-
17
- # 只装当前项目(不推荐,每个项目都要装)
18
- npx @andyqiu/codeforge install
19
11
  ```
20
12
 
21
- 装完**重启 opencode**。
13
+ 装完**重启 opencode**。(postinstall 脚本会自动完成全局配置,无需额外操作。)
22
14
 
23
15
  ## 怎么用
24
16
 
@@ -49,9 +41,15 @@ npx @andyqiu/codeforge install
49
41
  | `/review` | 审当前暂存的改动 |
50
42
  | `/refactor <目标>` | 安全重构(先补测试锁行为,再改) |
51
43
  | `/tdd <需求>` | 严格 RED → GREEN → REFACTOR |
52
- | `/parallel <任务1>,<任务2>,...` | 多个独立任务并发跑 |
44
+ | `/merge` | 触发 review + squash merge 入主仓 |
45
+ | `/discard-session` | 放弃当前 session 的所有改动 |
53
46
  | `/adr-init` | 为当前项目初始化 ADR 决策记录体系 |
54
-
47
+ | `/debug <问题>` | 调试模式,深入分析问题根因 |
48
+ | `/deep <需求>` | 强制升档用高端推理模型处理 |
49
+ | `/quick <需求>` | 强制降档用快速模型处理 |
50
+ | `/changes` | 查看当前 session 的改动摘要 |
51
+ | `/pause` | 暂停当前任务 |
52
+ | `/parallel <任务1>,<任务2>` | 多个独立任务并发跑 |
55
53
 
56
54
  ### Discover Agent — 虚拟产品经理
57
55
 
@@ -68,7 +66,7 @@ discover 会跑 5 个阶段:
68
66
  4. **PRD 起草** — 生成 EARS 句式 PRD.md + 机读 handoff.yaml
69
67
  5. **复核** — challenger 二次复核
70
68
 
71
- 产物在 `.codeforge/specs/<slug>/{PRD.md, handoff.yaml}`,会被下游 codeforge / planner / coder **自动消费**(通过 `discover-spec-suggest` plugin 注入候选提示)。
69
+ 产物在 `.codeforge/specs/<slug>/{PRD.md, handoff.yaml}`,会被下游 codeforge / planner / coder **自动消费**。
72
70
 
73
71
  详见 [docs/discover/README.md](./docs/discover/README.md)。
74
72
 
@@ -77,10 +75,10 @@ discover 会跑 5 个阶段:
77
75
  在任意 git 项目根目录执行一次,把完整的 ADR 校验体系下发到该项目:
78
76
 
79
77
  ```bash
80
- # 方式一:CLI 命令
78
+ # CLI 命令
81
79
  codeforge adr-init
82
80
 
83
- # 方式二:opencode 里直接说
81
+ # opencode 里直接说
84
82
  /adr-init
85
83
 
86
84
  # 常用选项
@@ -109,60 +107,13 @@ git config core.hooksPath .githooks
109
107
 
110
108
  npm 项目可加 `--write-prepare` 让 `npm install` 自动完成上面这步。
111
109
 
112
- ### 难度分级与三道防线(Phase 2 引入)
113
-
114
- CodeForge 给每个 agent 配三档变体,让简单任务省 token、复杂任务自动升档保质量:
115
-
116
- | 档位 | 模型基线 | 用途 |
117
- |---|---|---|
118
- | `coder-quick` | 低成本快速模型 | typo / 单行改动 |
119
- | `coder`(默认) | 中端模型 | 单文件中等改动 |
120
- | `coder-deep` | 高端推理模型 | 跨文件重构 / 安全 / 数据迁移 |
121
-
122
- 三道防线决定最终用哪档(优先级递减):
123
-
124
- - **A · 用户显式 override**:`/deep` 升档、`/quick` 降档;锁定后 B/C 都不会覆盖
125
- - **B · 前置预判**(Phase 2b 接线中):派 task 前看跨文件数 / 关键词(auth / refactor / migration / schema)自动选档
126
- - **C · 运行时升档**:reviewer 连续 REQUEST_CHANGES、stuck-detector 触发、测试连续失败 → 兜底升档(带 quota + debounce 去噪)
127
-
128
- 升档不会静默改配置——当前会记录日志并提示;完成 auto_escalate 接线后,配置变更才会进入 session worktree 等你通过 `/merge` 审批。完整设计见 `docs/adr/model-tier-three-layer-escalation.md`。
129
-
130
-
131
110
  ### 代码改动如何落地
132
111
 
133
- 每个 session 绑定独立 git worktree,AI 改动直接写到 worktree(不影响主仓);
134
- 审批通过 `/merge` 命令触发 review-fix-review 闭环,通过后 squash merge 入主仓。
135
- 完整设计见 `docs/adr/worktree-session-isolation.md`。
136
-
137
- ```bash
138
- # 查看当前 session worktree 改动
139
- git -C <worktree> diff
140
-
141
- # 触发 review + 合并闭环
142
- /merge # TUI 内 slash command
143
- /discard-session # 放弃当前 session
144
- ```
145
-
146
- ## 查版本 / 升级 / 回滚
112
+ 每个 session 绑定独立 git worktree,AI 改动直接写到 worktree(不影响主仓);审批通过 `/merge` 命令触发 review-fix-review 闭环,通过后 squash merge 入主仓。
147
113
 
148
114
  ```bash
149
- # 查当前版本
150
- cat ~/.config/opencode/codeforge/VERSION
151
- # Windows:Get-Content "$env:USERPROFILE\.config\opencode\codeforge\VERSION"
152
-
153
- # 立即升级到最新
154
- npx @andyqiu/codeforge install --global
155
-
156
- # 回滚到上一版
157
- codeforge rollback
158
- ```
159
-
160
- opencode 启动时**自动后台检查新版**并静默升级,下次启动生效。
161
-
162
- 关闭自动升级(编辑 `~/.config/opencode/codeforge/codeforge.json`):
163
-
164
- ```json
165
- { "update": { "auto_install": false } }
115
+ /merge # 触发 review + 合并闭环
116
+ /discard-session # 放弃当前 session 的所有改动
166
117
  ```
167
118
 
168
119
  ## Knowledge Hub(可选,团队共享经验)
@@ -179,107 +130,16 @@ export KNOWLEDGE_API_KEY=你的-token
179
130
 
180
131
  详细配置 token 找你的团队管理员。
181
132
 
182
- ## 通知集成(可选)
183
-
184
- 支持把 AI 完成的任务、审阅结果推到 Slack / 飞书。**三种配置方式**,同时存在时按以下优先级合并(0.3.11 起补齐全局层,对齐 KH 配置惯例):
185
-
186
- | 优先级 | 来源 | 适用场景 |
187
- |---|---|---|
188
- | 1(最高)| `CODEFORGE_CHANNELS_JSON` env | CI / 本机临时覆盖 / 紧急静音(设为 `[]` 即全禁) |
189
- | 2 | `<project>/.codeforge/channels.json` | 团队 git 共享(0.3.11 推荐路径) |
190
- | 3(最低)| `~/.config/codeforge/channels.json` | 个人跨项目共享 |
191
-
192
- > 同名 channel:上层覆盖下层(env > project > global),覆盖时打 warn 提示。
193
- > env 显式为 `[]` 时清空 **三层全部**通知(0.3.11 行为变更,详见 CHANGELOG)。
194
- >
195
- > 0.3.10 兼容路径 `<root>/.codeforge/config/channels.json` 继续工作但启动 warn 提示迁移;两条路径同时存在时**只读 0.3.11 推荐路径**。
196
-
197
- ### 方式 ⓪ 全局个人配置(跨项目共享)
198
-
199
- 适用:你个人想在所有项目里都收同一个飞书/Slack 通知,不想每个项目都重配一遍。
200
-
201
- ```bash
202
- # macOS / Linux
203
- mkdir -p ~/.config/codeforge
204
- cat > ~/.config/codeforge/channels.json <<'JSON'
205
- [
206
- {
207
- "type": "lark",
208
- "name": "personal-lark",
209
- "webhook_url": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
210
- }
211
- ]
212
- JSON
213
- chmod 600 ~/.config/codeforge/channels.json # 含 webhook secret,建议 600
214
- ```
215
-
216
- ```powershell
217
- # Windows PowerShell
218
- $dir = "$env:USERPROFILE\.config\codeforge"
219
- New-Item -ItemType Directory -Force -Path $dir | Out-Null
220
- @'
221
- [{"type":"lark","name":"personal-lark","webhook_url":"https://open.feishu.cn/open-apis/bot/v2/hook/xxx"}]
222
- '@ | Set-Content -Encoding UTF8 "$dir\channels.json"
223
- ```
224
-
225
- > 含 webhook secret,权限松(group/other 可读)时会启动打一次性 warn 提示 `chmod 600`,但不阻止加载。
226
-
227
- ### 方式 ① 项目共享配置(推荐团队场景)
228
-
229
- 在项目根创建 `.codeforge/channels.json`(**0.3.11 推荐路径**,可 git 提交):
230
-
231
- ```json
232
- {
233
- "channels": [
234
- {
235
- "type": "slack",
236
- "name": "dev-alerts",
237
- "webhook_url": "https://hooks.slack.com/services/T.../B.../xxx",
238
- "events": ["workflow.failed", "approval.required"]
239
- },
240
- {
241
- "type": "lark",
242
- "name": "team-lark",
243
- "webhook_url": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx",
244
- "secret": "可选签名密钥",
245
- "mentions": ["@all"]
246
- }
247
- ]
248
- }
249
- ```
250
-
251
- > 0.3.10 兼容路径 `.codeforge/config/channels.json` 仍可用,但启动会 warn 提示迁移到 `.codeforge/channels.json`(与 KH 配置 `<root>/.codeforge/kh.json` 风格统一)。0.3.10 用户继续工作不受影响。
252
-
253
- 完整字段定义见 `lib/channels.ts` 中 `SlackChannel` / `LarkChannel` 接口。
254
-
255
- ### 方式 ② 环境变量(CI 友好 / 临时覆盖)
133
+ ## 升级
256
134
 
257
135
  ```bash
258
- export CODEFORGE_CHANNELS_JSON='[{"type":"lark","name":"ci","webhook_url":"https://..."}]'
136
+ npm update -g @andyqiu/codeforge
259
137
  ```
260
138
 
261
- ### ⚠️ 老配置迁移
262
-
263
- 如果你按旧版 README 配过 `~/.config/opencode/codeforge/codeforge.json` 里的 `channels`(**该路径从未被 plugin 读取**,原本就发不出通知),或者 0.3.10 配过 `<root>/.codeforge/config/channels.json`,请按下表迁移:
264
-
265
- | 旧 | 新(0.3.11 推荐) |
266
- |---|---|
267
- | `~/.config/opencode/codeforge/codeforge.json` 的 `channels` 字段 | `~/.config/codeforge/channels.json`(全局)或 `<project>/.codeforge/channels.json`(项目) |
268
- | `<project>/.codeforge/config/channels.json`(0.3.10 兼容路径) | `<project>/.codeforge/channels.json`(0.3.11 推荐路径) |
269
- | `{ "type": "slack", "webhook": "..." }` | `{ "type": "slack", "name": "<必填>", "webhook_url": "..." }` |
270
- | `{ "type": "lark", "webhook": "...", "secret": "..." }` | `{ "type": "lark", "name": "<必填>", "webhook_url": "...", "secret": "..." }` |
271
-
272
- 变更点:
273
- 1. **加载路径**:3 层 `env > <project>/.codeforge/channels.json > ~/.config/codeforge/channels.json`(旧 `~/.config/opencode/codeforge/codeforge.json` 无效)
274
- 2. **字段名**:`webhook` → `webhook_url`
275
- 3. **新必填**:`name`(用于跨层合并去重)
276
-
277
- 配置错误时会在 plugin 启动日志打 warn,告知被忽略的 channel 名与缺失字段。
278
-
279
139
  ## 卸载
280
140
 
281
141
  ```bash
282
- npx @andyqiu/codeforge uninstall --global
142
+ npm uninstall -g @andyqiu/codeforge
283
143
  ```
284
144
 
285
145
  ## 出问题怎么办
@@ -288,9 +148,9 @@ npx @andyqiu/codeforge uninstall --global
288
148
  |---|---|
289
149
  | 装完 opencode 没识别新命令 | 重启 opencode |
290
150
  | AI 改动没写入主仓 | 改动在 session worktree 内;用 `/merge` 触发审批 + 合并闭环 |
291
- | 自动升级把功能搞坏了 | `codeforge rollback` 回上一版 |
292
- | 启动报"opencode 版本不兼容" | 升级 opencode:`npm i -g opencode-ai@latest` |
293
- | 命令找不到 | 全局装时确认 npm global bin 在 PATH:`npm config get prefix` |
151
+ | 启动报"opencode 版本不兼容" | 升级 opencode:`npm install -g opencode@latest` |
152
+ | 命令找不到 | 确认 npm global bin 在 PATH:`npm config get prefix` |
153
+ | 需要查当前版本 | `codeforge --version` |
294
154
 
295
155
  其它问题联系 [@andyqiu](https://www.npmjs.com/~andyqiu)。
296
156
 
package/bin/codeforge.mjs CHANGED
@@ -3,12 +3,12 @@
3
3
  * codeforge — opencode 零侵入扩展包的安装入口
4
4
  *
5
5
  * 一行命令安装:
6
- * npx @andyqiu/codeforge install
6
+ * npm install -g @andyqiu/codeforge
7
7
  *
8
8
  * 子命令:
9
- * install [--global|--project] [--dry-run] [--enable-legacy-tools]
9
+ * install [--dry-run] [--enable-legacy-tools]
10
10
  * 把 CodeForge 单 bundle 注入到 opencode(写 opencode.json plugin entry)
11
- * uninstall [--global|--project] 卸载(不动 opencode 自身)
11
+ * uninstall 卸载(不动 opencode 自身)
12
12
  * list 探测 opencode 是否在机器上
13
13
  * version 打印版本
14
14
  * rollback [--target=<path>] [--dry-run]
@@ -18,7 +18,7 @@
18
18
  * 把 ADR 体系(hooks + scripts + 模板)下发到当前 git 项目
19
19
  * help [<cmd>] 打印帮助
20
20
  *
21
- * 默认 --project(项目级,最安全)。
21
+ * 强制全局安装(~/.config/opencode/),不再支持项目级安装。
22
22
  *
23
23
  * 只支持 opencode:CodeForge 是单 bundle plugin,跑在 opencode 1.14+ 上(持续跟主线)。
24
24
  * Cursor / Claude Code / Codex 都不能加载这种 plugin(API 不兼容),所以不假装支持。
@@ -115,7 +115,7 @@ function installOpencode({ scope, dryRun, extraArgs, quiet = false }) {
115
115
  if (dryRun) flagArgs.push("--dry-run")
116
116
  }
117
117
  const all = [...baseArgs, ...flagArgs, ...(extraArgs ?? [])]
118
- log(`opencode: ${cmd} ${all.join(" ")}`)
118
+ if (!quiet) log(`opencode: ${cmd} ${all.join(" ")}`)
119
119
  const r = spawnSync(cmd, all, { stdio: quiet ? "pipe" : "inherit", cwd: REPO_ROOT })
120
120
  if (quiet && r.status !== 0) {
121
121
  if (r.stderr) process.stderr.write(r.stderr)
@@ -148,7 +148,7 @@ function detectOpencode() {
148
148
  // 子命令:install
149
149
  // ────────────────────────────────────────────────────────────────────
150
150
  function cmdInstall(args) {
151
- const scope = args.flags.global ? "global" : "project"
151
+ const scope = "global"
152
152
  const dryRun = !!args.flags["dry-run"] || !!args.flags.dryRun
153
153
  const fromNpm = isFromNpm()
154
154
  // npm 场景下 dist/ 已经被 prepack 打过包随 tarball 发布,强制 --skip-build
@@ -189,7 +189,7 @@ function cmdInstall(args) {
189
189
  // 子命令:uninstall
190
190
  // ────────────────────────────────────────────────────────────────────
191
191
  function cmdUninstall(args) {
192
- const scope = args.flags.global ? "global" : "project"
192
+ const scope = "global"
193
193
  log(`CodeForge uninstaller`)
194
194
  log(` scope : ${scope}`)
195
195
  hr()
@@ -267,7 +267,7 @@ function cmdRollback(args) {
267
267
 
268
268
  if (backups.length === 0) {
269
269
  err(`找不到任何 backup(pattern: ${prefix}*)`)
270
- err(`提示:自动更新尚未执行过 / backup 已被清理。可跑 npx @andyqiu/codeforge install 重装。`)
270
+ err(`提示:自动更新尚未执行过 / backup 已被清理。可跑 npm install -g @andyqiu/codeforge 重装。`)
271
271
  return 1
272
272
  }
273
273
 
@@ -476,8 +476,8 @@ function cmdHelp() {
476
476
  console.log(`${C.bold}codeforge${C.reset} — opencode 的零侵入扩展包安装入口
477
477
 
478
478
  用法:
479
- codeforge install [--global|--project] [--dry-run] [--skip-build]
480
- codeforge uninstall [--global|--project]
479
+ codeforge install [--dry-run] [--skip-build]
480
+ codeforge uninstall
481
481
  codeforge list
482
482
  codeforge version
483
483
  codeforge rollback [--target=<path>] [--dry-run] # 恢复最近 backup(auto_install 失败救场)
@@ -486,13 +486,10 @@ function cmdHelp() {
486
486
  codeforge adr-init [--force] [--dry-run] [--write-prepare] [--no-pre-push]
487
487
  # 把 ADR 校验体系(hooks + scripts + 模板)下发到当前 git 项目
488
488
 
489
- 一行命令(远程安装):
490
- npx @andyqiu/codeforge install
491
- npx @andyqiu/codeforge install --global
489
+ 安装:
490
+ npm install -g @andyqiu/codeforge
492
491
 
493
492
  参数:
494
- --global 装到全局(~/.config/opencode/)
495
- --project 装到项目(./.opencode/,默认)
496
493
  --dry-run 只打印操作,不真改
497
494
  --skip-build 跳过 npm run build(已有 dist/index.js 时增量装)
498
495
  --enable-legacy-tools 启用旧版 file-based tools(默认禁用,避免 zod 跨实例 bug)
package/commands/merge.md CHANGED
@@ -71,6 +71,22 @@ session_merge(action="merge", plan_id=<可选>, force=<可选>)
71
71
  }
72
72
  ```
73
73
 
74
+ ### `.codeforge/codeforge.json::merge.postMergeScript`(可选,团队共享)
75
+
76
+ 声明 `/merge` squash 成功后要跑的 npm script(dist 重 build)。未配置则跳过。
77
+
78
+ ```json
79
+ {
80
+ "merge": {
81
+ "postMergeScript": "build:dev"
82
+ }
83
+ }
84
+ ```
85
+
86
+ - 非 Node 项目(C# / Go / Rust)一般不需要此字段
87
+ - script 失败会触发主仓 `git reset --hard HEAD` 兜底(保 merge 原子性,详见 ADR:merge-back-rollback)
88
+ - 详见 ADR:merge-post-script-config
89
+
74
90
  ## 与其他命令的关系
75
91
 
76
92
  | 命令 | 用途 |
package/dist/index.js CHANGED
@@ -8220,10 +8220,11 @@ function shouldYieldToLocalPlugin(opts = {}) {
8220
8220
  if (envVal === "1" || envVal === "true" || envVal === "yes") {
8221
8221
  return { yield: true, reason: "env" };
8222
8222
  }
8223
+ const usingFallback = !opts.directory;
8223
8224
  const startDir = opts.directory ? resolve(opts.directory) : process.cwd();
8224
- const maxDepth = Math.max(1, opts.maxDepth ?? 20);
8225
+ const effectiveMaxDepth = opts.maxDepth !== undefined ? Math.max(1, opts.maxDepth) : usingFallback ? 1 : 3;
8225
8226
  let cur = startDir;
8226
- for (let i = 0;i < maxDepth; i++) {
8227
+ for (let i = 0;i < effectiveMaxDepth; i++) {
8227
8228
  const markerPath = resolve(cur, MARKER_REL);
8228
8229
  try {
8229
8230
  if (fileExists(markerPath)) {
@@ -13130,6 +13131,7 @@ function sleep(ms) {
13130
13131
  }
13131
13132
 
13132
13133
  // lib/session-worktree.ts
13134
+ init_global_config();
13133
13135
  var REGISTRY_VERSION = 1;
13134
13136
  var DEFAULT_WORKTREE_SUBDIR = path13.join(".git", "codeforge-worktrees");
13135
13137
  function registryDir(mainRoot) {
@@ -13269,8 +13271,8 @@ async function mergeSessionBack(opts) {
13269
13271
  });
13270
13272
  throw new Error(`mergeSessionBack: squash merge 失败(已 reset 主仓兜底): ${err.message}`);
13271
13273
  }
13272
- const hasDevOnce = await packageHasScript(mainRoot, "dev:once");
13273
- if (hasDevOnce) {
13274
+ const buildScript = await getBuildScript(mainRoot);
13275
+ if (buildScript) {
13274
13276
  const stagedRaw = await runGit2(mainRoot, [
13275
13277
  "diff",
13276
13278
  "--cached",
@@ -13278,21 +13280,21 @@ async function mergeSessionBack(opts) {
13278
13280
  "--diff-filter=ACMR"
13279
13281
  ]);
13280
13282
  const stagedPaths = stagedRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
13281
- const canSkipDevOnce = await shouldSkipDevOnce(mainRoot, stagedPaths);
13283
+ const canSkipDevOnce = await shouldSkipDevOnce(mainRoot, stagedPaths, wt);
13282
13284
  if (canSkipDevOnce) {
13283
13285
  const sourceCount = stagedPaths.filter((p) => /^(plugins|lib|src)\//.test(p) && !/\.(md|test\.ts)$/.test(p)).length;
13284
- console.log(`[session-worktree] skip dev:once: dist 已是最新(${sourceCount} staged 源文件 mtime <= dist mtime)`);
13286
+ console.log(`[session-worktree] skip ${buildScript}: dist 已是最新(${sourceCount} staged 源文件 mtime <= dist mtime)`);
13285
13287
  } else {
13286
13288
  try {
13287
- await runCmd("npm", ["run", "dev:once"], mainRoot);
13289
+ await runCmd("npm", ["run", buildScript], mainRoot);
13288
13290
  } catch (err) {
13289
13291
  await runGit2(mainRoot, ["reset", "--hard", "HEAD"]).catch(() => {});
13290
13292
  const msg = err instanceof Error ? err.message : String(err);
13291
- throw new Error(`dev:once 失败已 reset 主仓: ${msg}`);
13293
+ throw new Error(`${buildScript} 失败已 reset 主仓: ${msg}`);
13292
13294
  }
13293
13295
  }
13294
13296
  } else {
13295
- console.log(`[session-worktree] skip dev:once: not configured in ${mainRoot}/package.json`);
13297
+ console.warn(`[session-worktree] skip build step: merge.postMergeScript not configured in .codeforge/codeforge.json (project may not need post-merge build)`);
13296
13298
  }
13297
13299
  const squashedRaw = await runGit2(wt, ["log", "--format=%s", `${baseSha}..HEAD`]);
13298
13300
  const squashedCommits = squashedRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
@@ -13418,19 +13420,18 @@ function runGitWithEnv(cwd, args, envOverrides, timeoutMs = 1e4) {
13418
13420
  });
13419
13421
  });
13420
13422
  }
13421
- async function packageHasScript(mainRoot, scriptName) {
13422
- try {
13423
- const pkgPath = path13.join(mainRoot, "package.json");
13424
- const raw = await fs10.readFile(pkgPath, "utf8");
13425
- const pkg = JSON.parse(raw);
13426
- if (!pkg.scripts || typeof pkg.scripts !== "object")
13427
- return false;
13428
- return typeof pkg.scripts[scriptName] === "string";
13429
- } catch {
13430
- return false;
13431
- }
13423
+ async function getBuildScript(mainRoot) {
13424
+ const cfg = getCodeforgeConfig({ root: mainRoot });
13425
+ const merge = cfg["merge"];
13426
+ if (!merge || typeof merge !== "object" || Array.isArray(merge))
13427
+ return null;
13428
+ const script = merge["postMergeScript"];
13429
+ if (typeof script !== "string")
13430
+ return null;
13431
+ const trimmed = script.trim();
13432
+ return trimmed.length > 0 ? trimmed : null;
13432
13433
  }
13433
- async function shouldSkipDevOnce(mainRoot, stagedPaths) {
13434
+ async function shouldSkipDevOnce(mainRoot, stagedPaths, worktreePath) {
13434
13435
  let distMtimeSec;
13435
13436
  try {
13436
13437
  const st = await fs10.stat(path13.join(mainRoot, "dist/index.js"));
@@ -13442,18 +13443,28 @@ async function shouldSkipDevOnce(mainRoot, stagedPaths) {
13442
13443
  if (relevant.length === 0)
13443
13444
  return true;
13444
13445
  for (const rel of relevant) {
13445
- try {
13446
- const abs = path13.join(mainRoot, rel);
13447
- const st = await fs10.stat(abs);
13448
- const srcMtimeSec = Math.floor(st.mtimeMs / 1000);
13449
- if (srcMtimeSec > distMtimeSec)
13450
- return false;
13451
- } catch {
13446
+ const srcMtimeSec = await statSourceMtime(rel, mainRoot, worktreePath);
13447
+ if (srcMtimeSec === null)
13448
+ return false;
13449
+ if (srcMtimeSec > distMtimeSec)
13452
13450
  return false;
13453
- }
13454
13451
  }
13455
13452
  return true;
13456
13453
  }
13454
+ async function statSourceMtime(rel, mainRoot, worktreePath) {
13455
+ if (worktreePath) {
13456
+ try {
13457
+ const st = await fs10.stat(path13.join(worktreePath, rel));
13458
+ return Math.floor(st.mtimeMs / 1000);
13459
+ } catch {}
13460
+ }
13461
+ try {
13462
+ const st = await fs10.stat(path13.join(mainRoot, rel));
13463
+ return Math.floor(st.mtimeMs / 1000);
13464
+ } catch {
13465
+ return null;
13466
+ }
13467
+ }
13457
13468
  function runCmd(cmd, args, cwd, timeoutMs = 300000) {
13458
13469
  return new Promise((resolve11, reject) => {
13459
13470
  execFile3(cmd, args, { cwd, timeout: timeoutMs, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
@@ -16208,10 +16219,11 @@ var codeforgeToolsServer = async (ctx) => {
16208
16219
  session_merge: tool({
16209
16220
  description: description26,
16210
16221
  args: {
16211
- action: z30.enum(["merge", "status", "discard"]).describe("操作类型:merge=合并到主仓(orchestrator 专用)/ status=查询状态 / discard=放弃"),
16222
+ action: z30.enum(["merge", "status", "discard", "diff"]).describe("操作类型:merge=合并到主仓(orchestrator 专用)/ status=查询状态 / discard=放弃 / diff=查看 worktree 改动"),
16212
16223
  session_id: z30.string().optional().describe("目标 session id;不传则用当前 session"),
16213
16224
  plan_id: z30.string().optional().describe("关联的 plan_id(reviewer 校验时用),格式 plan-YYYYMMDD-HHmmss-NNN"),
16214
- force: z30.boolean().optional().describe("action=merge 时跳过 review 直接 squash merge(写审计)")
16225
+ force: z30.boolean().optional().describe("action=merge 时跳过 review 直接 squash merge(写审计)"),
16226
+ stat: z30.boolean().optional().describe("action=diff 时:true=只显示文件列表+统计,false=完整 diff(默认 false)")
16215
16227
  },
16216
16228
  async execute(args, input) {
16217
16229
  return await runSafe("session_merge", async () => {
@@ -21355,9 +21367,9 @@ var RISK_PATTERNS = [
21355
21367
  },
21356
21368
  {
21357
21369
  tag: "write_secrets",
21358
- kinds: ["bash"],
21359
- re: /(\.env(?:\.\w+)?|id_[edr]sa|\.ssh\/id_|\.pem|\.p12|secret\.json)/i,
21360
- matchOn: ["command"]
21370
+ kinds: ["bash", "edit"],
21371
+ re: /(?:^|[\s/"'])(id_[edr]sa|secret\.json|\.env(?:\.\w+)?)(?:$|[\s"'/])|[\w./-]+\.(?:pem|p12)(?:$|[\s"'])|\.ssh\/id_/i,
21372
+ matchOn: ["command", "filePath", "path", "target"]
21361
21373
  },
21362
21374
  {
21363
21375
  tag: "write_etc",
@@ -21778,7 +21790,7 @@ import * as zlib from "node:zlib";
21778
21790
  // lib/version-injected.ts
21779
21791
  function getInjectedVersion() {
21780
21792
  try {
21781
- const v = "0.5.14";
21793
+ const v = "0.5.16";
21782
21794
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
21783
21795
  return v;
21784
21796
  }
@@ -23058,14 +23070,18 @@ var handler24 = workflowEngineServer;
23058
23070
  import path27 from "node:path";
23059
23071
  var PLUGIN_NAME25 = "session-worktree-guard";
23060
23072
  logLifecycle(PLUGIN_NAME25, "import", {});
23061
- var WRITE_INTENT_RE = />(?![=&])|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
23062
- var READ_ONLY_COMMANDS = /^\s*(?:ls|cat|head|tail|grep|rg|find|fd|wc|stat|file|which|whereis|echo|pwd|cd|pushd|popd|env|printenv|type|less|more|sort|uniq|awk|tr|cut|jq|date|whoami|id|uname|git(?:\s+-C\s+\S+)?\s+(?:log|show|diff|status|branch|tag|remote|config\s+--get|rev-parse|rev-list|ls-files|ls-tree|cat-file|describe|reflog|blame|shortlog|name-rev|symbolic-ref|merge-base|worktree\s+list|stash\s+list|stash\s+show))\b/;
23063
- var SIDE_EFFECT_TOKEN_RE = />(?![=&])|\|\s*tee\b|\btee\b/;
23073
+ var WRITE_INTENT_RE = />(?![=&])(?!\s*\/dev\/(?:null|stdout|stderr|fd\/\d+)\b)|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
23074
+ var READ_ONLY_COMMANDS = /^\s*(?:ls|cat|head|tail|grep|rg|find|fd|wc|stat|file|which|whereis|echo|pwd|cd|pushd|popd|env|printenv|type|less|more|sort|uniq|awk|tr|cut|jq|date|whoami|id|uname|node|npx|tsc|diff|python3?|git(?:\s+-C\s+\S+)?\s+(?:log|show|diff|status|branch|tag|remote|config\s+--get|rev-parse|rev-list|ls-files|ls-tree|cat-file|describe|reflog|blame|shortlog|name-rev|symbolic-ref|merge-base|worktree\s+list|stash\s+list|stash\s+show))\b/;
23075
+ var SIDE_EFFECT_TOKEN_RE = />(?![=&])(?!\s*\/dev\/(?:null|stdout|stderr|fd\/\d+)\b)|\|\s*tee\b|\btee\b/;
23064
23076
  function isReadOnlyBashCommand(command) {
23065
23077
  if (!READ_ONLY_COMMANDS.test(command))
23066
23078
  return false;
23067
23079
  if (SIDE_EFFECT_TOKEN_RE.test(command))
23068
23080
  return false;
23081
+ for (const re of INTERPRETER_WRITE_RES) {
23082
+ if (re.test(command))
23083
+ return false;
23084
+ }
23069
23085
  return true;
23070
23086
  }
23071
23087
  var INTERPRETER_WRITE_RES = [
@@ -23073,6 +23089,9 @@ var INTERPRETER_WRITE_RES = [
23073
23089
  /node.*writeFile/,
23074
23090
  /perl.*>/
23075
23091
  ];
23092
+ function stripCommitMessageArgs(command) {
23093
+ return command.replace(/(?:^|\s)(?:-m|--message|-F|--file)(?:=|\s+)("[^"]*"|'[^']*')/g, " ");
23094
+ }
23076
23095
  function escapeRegex2(s) {
23077
23096
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23078
23097
  }
@@ -23083,6 +23102,70 @@ function buildGitVcsWriteRegex(mainRoot) {
23083
23102
  var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
23084
23103
  var TOUCH_THROTTLE_MS = 5 * 60000;
23085
23104
  var _touchCache = new Map;
23105
+ var _sessionIdMissingWarned = false;
23106
+ var _bindFailNotified = new Set;
23107
+ function formatLazyBindDenyReason(input) {
23108
+ const { sessionId, mainRoot, toolName, errMsg, alreadyNotified } = input;
23109
+ const firstLine = errMsg.split(`
23110
+ `).find((l) => l.trim().length > 0)?.trim() ?? errMsg;
23111
+ const lower = firstLine.toLowerCase();
23112
+ if (alreadyNotified) {
23113
+ return `[session-worktree-guard] DENIED: session ${sessionId} 仍无法绑定 worktree(${firstLine});` + `详见首次报错或 ~/.cache/codeforge/plugins.log。本 session 已永久无法绑定,可设 ` + `CODEFORGE_DISABLE_WORKTREE_GUARD=1 临时绕过(会失去隔离保护)。`;
23114
+ }
23115
+ if (lower.includes("not a git repository")) {
23116
+ return [
23117
+ `[session-worktree-guard] DENIED: 无法为 session ${sessionId} 绑定 worktree,写操作可能污染主工作区,已拒绝执行`,
23118
+ `原因:当前项目不是 git 仓库(git 错误:${firstLine})`,
23119
+ `影响工具:${toolName}`,
23120
+ `解决方案:`,
23121
+ ` 1. 确认你在正确的项目根目录(当前 mainRoot=${mainRoot})`,
23122
+ ` 2. 若该项目应该是 git 仓库:\`git init && git add -A && git commit -m "initial"\``,
23123
+ ` 3. 若该项目不需要 worktree 隔离(如临时脚本目录),可设置环境变量绕过:`,
23124
+ ` \`CODEFORGE_DISABLE_WORKTREE_GUARD=1 opencode\`(**会失去隔离保护,谨慎使用**)`,
23125
+ `排查日志:\`cat ~/.cache/codeforge/plugins.log | grep session-worktree-guard\``
23126
+ ].join(`
23127
+ `);
23128
+ }
23129
+ if (lower.includes("git: command not found") || lower.includes("enoent") && lower.includes("git")) {
23130
+ return [
23131
+ `[session-worktree-guard] DENIED: 无法为 session ${sessionId} 绑定 worktree,写操作可能污染主工作区,已拒绝执行`,
23132
+ `原因:找不到 git 可执行文件(错误:${firstLine})`,
23133
+ `影响工具:${toolName}`,
23134
+ `解决方案:`,
23135
+ ` 1. 安装 git(>= 2.5 才支持 worktree):Linux \`apt install git\` / macOS \`brew install git\``,
23136
+ ` 2. 确认 git 在 PATH 中:\`command -v git\``,
23137
+ ` 3. 若临时无法安装,可设置环境变量绕过:`,
23138
+ ` \`CODEFORGE_DISABLE_WORKTREE_GUARD=1 opencode\`(**会失去隔离保护,谨慎使用**)`,
23139
+ `排查日志:\`cat ~/.cache/codeforge/plugins.log | grep session-worktree-guard\``
23140
+ ].join(`
23141
+ `);
23142
+ }
23143
+ if (lower.includes("worktree") || lower.includes("head")) {
23144
+ return [
23145
+ `[session-worktree-guard] DENIED: 无法为 session ${sessionId} 绑定 worktree,写操作可能污染主工作区,已拒绝执行`,
23146
+ `原因:worktree 创建失败(git 错误:${firstLine})`,
23147
+ `影响工具:${toolName}`,
23148
+ `常见原因:`,
23149
+ ` - 仓库尚无任何 commit(HEAD 不存在):先 \`git commit\` 一次`,
23150
+ ` - 同名分支已被其他 worktree 占用:清理 \`git worktree prune\``,
23151
+ ` - .git 目录权限/损坏:检查 \`git fsck\``,
23152
+ `逃生口:\`CODEFORGE_DISABLE_WORKTREE_GUARD=1 opencode\`(**会失去隔离保护,谨慎使用**)`,
23153
+ `排查日志:\`cat ~/.cache/codeforge/plugins.log | grep session-worktree-guard\``
23154
+ ].join(`
23155
+ `);
23156
+ }
23157
+ return [
23158
+ `[session-worktree-guard] DENIED: 无法为 session ${sessionId} 绑定 worktree,写操作可能污染主工作区,已拒绝执行`,
23159
+ `原因:bindSessionWorktree 抛错(${firstLine})`,
23160
+ `影响工具:${toolName}`,
23161
+ `mainRoot:${mainRoot}`,
23162
+ `解决方案:`,
23163
+ ` 1. 重试一次(可能是瞬时锁冲突)`,
23164
+ ` 2. 看完整日志:\`cat ~/.cache/codeforge/plugins.log | grep session-worktree-guard\``,
23165
+ ` 3. 临时绕过:\`CODEFORGE_DISABLE_WORKTREE_GUARD=1 opencode\`(**会失去隔离保护,谨慎使用**)`
23166
+ ].join(`
23167
+ `);
23168
+ }
23086
23169
  var CLASS_B_CALLER_WHITELIST = new Set([
23087
23170
  "codeforge",
23088
23171
  "reviewer",
@@ -23161,13 +23244,14 @@ function commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePat
23161
23244
  function detectBashWriteIntent(command, mainRoot) {
23162
23245
  if (isReadOnlyBashCommand(command))
23163
23246
  return false;
23164
- if (WRITE_INTENT_RE.test(command))
23247
+ const sanitized = stripCommitMessageArgs(command);
23248
+ if (WRITE_INTENT_RE.test(sanitized))
23165
23249
  return true;
23166
23250
  for (const re of INTERPRETER_WRITE_RES) {
23167
- if (re.test(command))
23251
+ if (re.test(sanitized))
23168
23252
  return true;
23169
23253
  }
23170
- if (buildGitVcsWriteRegex(mainRoot).test(command))
23254
+ if (buildGitVcsWriteRegex(mainRoot).test(sanitized))
23171
23255
  return true;
23172
23256
  return false;
23173
23257
  }
@@ -23181,13 +23265,14 @@ function isWriteOperation(toolName, argsObj, mainRoot) {
23181
23265
  return false;
23182
23266
  if (isReadOnlyBashCommand(command))
23183
23267
  return false;
23184
- if (WRITE_INTENT_RE.test(command))
23268
+ const sanitized = stripCommitMessageArgs(command);
23269
+ if (WRITE_INTENT_RE.test(sanitized))
23185
23270
  return true;
23186
23271
  for (const re of INTERPRETER_WRITE_RES) {
23187
- if (re.test(command))
23272
+ if (re.test(sanitized))
23188
23273
  return true;
23189
23274
  }
23190
- if (mainRoot && buildGitVcsWriteRegex(mainRoot).test(command))
23275
+ if (mainRoot && buildGitVcsWriteRegex(mainRoot).test(sanitized))
23191
23276
  return true;
23192
23277
  return false;
23193
23278
  }
@@ -23211,6 +23296,18 @@ function resolveMainRoot2(rawDir) {
23211
23296
  return rawDir;
23212
23297
  }
23213
23298
  var sessionWorktreeGuardPlugin = async (ctx) => {
23299
+ const disableEnv = process.env["CODEFORGE_DISABLE_WORKTREE_GUARD"];
23300
+ if (disableEnv === "1" || disableEnv === "true" || disableEnv === "yes") {
23301
+ log14.warn("[guard] CODEFORGE_DISABLE_WORKTREE_GUARD 已启用,session-worktree-guard 全部 hook 跳过;" + "本次 opencode 会话所有写操作将直接落到主工作区(失去隔离保护)", { env: disableEnv });
23302
+ safeWriteLog(PLUGIN_NAME25, {
23303
+ hook: "activate",
23304
+ action: "skip",
23305
+ source: "disable-env",
23306
+ env_value: disableEnv
23307
+ });
23308
+ logLifecycle(PLUGIN_NAME25, "activate", { disabled_by_env: true });
23309
+ return {};
23310
+ }
23214
23311
  const mainRoot = resolveMainRoot2(ctx.directory ?? process.cwd());
23215
23312
  let policyCfg = {};
23216
23313
  try {
@@ -23229,8 +23326,24 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
23229
23326
  return {
23230
23327
  "tool.execute.before": async (input, output) => {
23231
23328
  const sessionId = input.sessionID ?? process.env["CODEFORGE_SESSION_ID"];
23232
- if (!sessionId)
23329
+ if (!sessionId) {
23330
+ const isWrite = isWriteOperation(input.tool, output.args ?? {}, mainRoot);
23331
+ if (isWrite && !_sessionIdMissingWarned) {
23332
+ _sessionIdMissingWarned = true;
23333
+ log14.warn("[guard] sessionID 缺失,无法绑定 worktree;本会话所有写操作将落到主工作区(仅本进程内 warn 一次)。" + "排查:grep no-session-id ~/.cache/codeforge/plugins.log", {
23334
+ tool: input.tool,
23335
+ opencode_version_hint: "需 opencode >= 0.x 才会在 tool.execute.before 注入 input.sessionID"
23336
+ });
23337
+ }
23338
+ safeWriteLog(PLUGIN_NAME25, {
23339
+ hook: "tool.execute.before",
23340
+ tool: input.tool,
23341
+ action: "skip",
23342
+ source: "no-session-id",
23343
+ is_write: isWrite
23344
+ });
23233
23345
  return;
23346
+ }
23234
23347
  let denied;
23235
23348
  await safeAsync(PLUGIN_NAME25, "tool.execute.before", async () => {
23236
23349
  const toolName = input.tool;
@@ -23285,14 +23398,32 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
23285
23398
  worktreePath: entry.worktreePath
23286
23399
  });
23287
23400
  } catch (err) {
23288
- log14.warn(`[lazy-bind] failed (落到主仓写)`, { sessionId, error: err instanceof Error ? err.message : String(err) });
23401
+ const errMsg = err instanceof Error ? err.message : String(err);
23402
+ const alreadyNotified = _bindFailNotified.has(sessionId);
23403
+ const reason = formatLazyBindDenyReason({
23404
+ sessionId,
23405
+ mainRoot,
23406
+ toolName,
23407
+ errMsg,
23408
+ alreadyNotified
23409
+ });
23410
+ _bindFailNotified.add(sessionId);
23411
+ log14.warn(`[lazy-bind] DENY (bind failed)`, {
23412
+ sessionId,
23413
+ tool: toolName,
23414
+ error: errMsg,
23415
+ throttled: alreadyNotified
23416
+ });
23289
23417
  safeWriteLog(PLUGIN_NAME25, {
23290
23418
  hook: "tool.execute.before",
23291
23419
  tool: toolName,
23292
23420
  sessionID: input.sessionID,
23293
- action: "lazy-bind-failed",
23294
- error: err instanceof Error ? err.message : String(err)
23421
+ action: "deny",
23422
+ source: "lazy-bind-failed",
23423
+ error: errMsg,
23424
+ throttled: alreadyNotified
23295
23425
  });
23426
+ denied = new DeniedError(reason);
23296
23427
  return;
23297
23428
  }
23298
23429
  }
package/install.sh CHANGED
@@ -595,6 +595,91 @@ if [ -f ".codeforge/.dev-marker" ] && [ -f "scripts/dev-sync.mjs" ]; then
595
595
  fi
596
596
 
597
597
 
598
+ # Step 9/9: 安装后自检(ADR:worktree-guard-fail-loud Fix D)
599
+ # 在「验证清单」前主动跑 4 项关键依赖检查;warn 不阻塞 install,但用户能立刻看到红绿
600
+ verify_install() {
601
+ local has_warn=0
602
+ log "Step 9/9: 安装后自检"
603
+
604
+ # 1. git 版本(worktree 隔离硬依赖;需要 >= 2.5)
605
+ if command -v git >/dev/null 2>&1; then
606
+ local gv major minor
607
+ gv="$(git --version 2>/dev/null | awk '{print $3}')"
608
+ major="$(printf '%s' "$gv" | cut -d. -f1)"
609
+ minor="$(printf '%s' "$gv" | cut -d. -f2)"
610
+ # 数值校验失败时按 0 处理,避免 "[: : integer expression expected"
611
+ case "$major" in *[!0-9]*|"") major=0 ;; esac
612
+ case "$minor" in *[!0-9]*|"") minor=0 ;; esac
613
+ if [ "$major" -gt 2 ] || { [ "$major" -eq 2 ] && [ "$minor" -ge 5 ]; }; then
614
+ ok "git $gv (>= 2.5 ✓ worktree 支持)"
615
+ else
616
+ warn "git $gv 版本太旧(worktree 需要 >= 2.5);worktree 隔离将失效(写操作会被 DENY)"
617
+ has_warn=1
618
+ fi
619
+ else
620
+ warn "git 未安装;worktree 隔离将失效(写操作会被 DENY)"
621
+ has_warn=1
622
+ fi
623
+
624
+ # 1b. git 仓库探测(reviewer 追加:Fix D 补 git rev-parse --git-dir 检查)
625
+ # 全局模式下当前目录通常**不是** CodeForge 自己的项目,因此只 info 提示;
626
+ # 项目模式下若当前目录不是 git 仓库,必然影响 worktree 隔离 → warn。
627
+ if command -v git >/dev/null 2>&1; then
628
+ if git rev-parse --git-dir >/dev/null 2>&1; then
629
+ ok "当前目录是 git 仓库(git rev-parse --git-dir ✓)"
630
+ else
631
+ if [ "$MODE" = "project" ]; then
632
+ warn "当前目录不是 git 仓库(git rev-parse --git-dir 失败);worktree 隔离将失效"
633
+ warn " 解决:\`git init && git add -A && git commit -m 'initial'\`"
634
+ has_warn=1
635
+ else
636
+ log " ↳ 当前目录非 git 仓库(全局安装无碍;进入实际项目使用时该项目需是 git 仓库)"
637
+ fi
638
+ fi
639
+ fi
640
+
641
+ # 2. opencode 版本
642
+ if command -v opencode >/dev/null 2>&1; then
643
+ local ov
644
+ ov="$(opencode --version 2>/dev/null || echo unknown)"
645
+ ok "opencode $ov"
646
+ else
647
+ warn "opencode 未在 PATH 中;CodeForge 无入口可用(请先安装 opencode)"
648
+ has_warn=1
649
+ fi
650
+
651
+ # 3. plugin 注册检查
652
+ local cfg
653
+ cfg="$(opencode_cfg_path)"
654
+ if [ -f "$cfg" ] && grep -q '"codeforge"' "$cfg" 2>/dev/null; then
655
+ ok "plugin 已注册:$cfg"
656
+ else
657
+ if [ "$MODE" = "project" ]; then
658
+ log " ↳ 项目级配置 $cfg 未包含 codeforge entry(如需用 codeforge agent,请编辑该文件)"
659
+ else
660
+ warn "plugin 未在 $cfg 中注册"
661
+ has_warn=1
662
+ fi
663
+ fi
664
+
665
+ # 4. dev-marker 误用检测(仅 global 模式有意义)
666
+ if [ "$MODE" = "global" ]; then
667
+ if [ -f "$HOME/.codeforge/.dev-marker" ] || [ -f "$HOME/.dev-marker" ]; then
668
+ warn "检测到 \$HOME 附近有 .codeforge/.dev-marker —— 可能导致所有项目让位到本地 plugin"
669
+ warn " 建议删除:rm \$HOME/.codeforge/.dev-marker"
670
+ has_warn=1
671
+ fi
672
+ fi
673
+
674
+ if [ "$has_warn" -eq 0 ]; then
675
+ ok "${C_BOLD}自检通过${C_RESET}"
676
+ else
677
+ warn "${C_BOLD}自检发现 $has_warn 项问题,详见上方${C_RESET}"
678
+ fi
679
+ }
680
+ verify_install
681
+
682
+
598
683
  # 验证清单
599
684
  hr
600
685
  ok "${C_BOLD}CodeForge v${CF_VERSION} 安装完成${C_RESET}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andyqiu/codeforge",
3
- "version": "0.5.14",
3
+ "version": "0.5.16",
4
4
  "description": "CodeForge — opencode 的零侵入扩展包",
5
5
  "type": "module",
6
6
  "private": false,
@@ -33,7 +33,7 @@
33
33
  "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly",
34
34
  "build:watch": "node ./scripts/build-with-version.mjs --watch",
35
35
  "dev": "node ./scripts/dev-sync.mjs --watch",
36
- "dev:once": "node ./scripts/dev-sync.mjs",
36
+ "build:dev": "node ./scripts/dev-sync.mjs",
37
37
  "dev-sync": "node ./scripts/dev-sync.mjs",
38
38
  "dev-sync:watch": "node ./scripts/dev-sync.mjs --watch",
39
39
  "typecheck": "tsc -p tsconfig.json --noEmit",
@@ -72,14 +72,9 @@
72
72
  "replay": "node ./scripts/codeforge-replay.mjs",
73
73
  "release": "node ./scripts/release.mjs",
74
74
  "release:dry": "node ./scripts/release.mjs --dry-run",
75
- "publish:patch": "bash ./scripts/publish.sh patch",
76
- "publish:minor": "bash ./scripts/publish.sh minor",
77
- "publish:major": "bash ./scripts/publish.sh major",
78
- "publish:dry": "bash ./scripts/publish.sh --dry-run",
79
- "publish:win:patch": "powershell -NoProfile -ExecutionPolicy Bypass -File ./scripts/publish.ps1",
80
- "publish:win:minor": "powershell -NoProfile -ExecutionPolicy Bypass -File ./scripts/publish.ps1 -Minor",
81
- "publish:win:major": "powershell -NoProfile -ExecutionPolicy Bypass -File ./scripts/publish.ps1 -Major",
82
- "publish:win:dry": "powershell -NoProfile -ExecutionPolicy Bypass -File ./scripts/publish.ps1 -DryRun",
75
+ "release:patch": "node ./scripts/release.mjs patch",
76
+ "release:minor": "node ./scripts/release.mjs minor",
77
+ "release:major": "node ./scripts/release.mjs major",
83
78
  "prepublishOnly": "npm run build && npm run typecheck && npm test",
84
79
  "prepare": "husky"
85
80
  },