@andyqiu/codeforge 0.5.15 → 0.5.17
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 +21 -161
- package/bin/codeforge.mjs +11 -14
- package/commands/merge.md +16 -0
- package/dist/index.js +152 -37
- package/install.sh +85 -0
- package/package.json +1 -1
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
|
-
| `/
|
|
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
|
|
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
|
-
#
|
|
78
|
+
# CLI 命令
|
|
81
79
|
codeforge adr-init
|
|
82
80
|
|
|
83
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
292
|
-
|
|
|
293
|
-
|
|
|
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
|
-
*
|
|
6
|
+
* npm install -g @andyqiu/codeforge
|
|
7
7
|
*
|
|
8
8
|
* 子命令:
|
|
9
|
-
* install [--
|
|
9
|
+
* install [--dry-run] [--enable-legacy-tools]
|
|
10
10
|
* 把 CodeForge 单 bundle 注入到 opencode(写 opencode.json plugin entry)
|
|
11
|
-
* uninstall
|
|
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
|
-
*
|
|
21
|
+
* 强制全局安装(~/.config/opencode/),不再支持项目级安装。
|
|
22
22
|
*
|
|
23
23
|
* 只支持 opencode:CodeForge 是单 bundle plugin,跑在 opencode 1.14+ 上(持续跟主线)。
|
|
24
24
|
* Cursor / Claude Code / Codex 都不能加载这种 plugin(API 不兼容),所以不假装支持。
|
|
@@ -148,7 +148,7 @@ function detectOpencode() {
|
|
|
148
148
|
// 子命令:install
|
|
149
149
|
// ────────────────────────────────────────────────────────────────────
|
|
150
150
|
function cmdInstall(args) {
|
|
151
|
-
const scope =
|
|
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 =
|
|
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 已被清理。可跑
|
|
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 [--
|
|
480
|
-
codeforge uninstall
|
|
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
|
-
|
|
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
|
|
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 <
|
|
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) {
|
|
@@ -13278,7 +13280,7 @@ 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
13286
|
console.log(`[session-worktree] skip ${buildScript}: dist 已是最新(${sourceCount} staged 源文件 mtime <= dist mtime)`);
|
|
@@ -13292,7 +13294,7 @@ async function mergeSessionBack(opts) {
|
|
|
13292
13294
|
}
|
|
13293
13295
|
}
|
|
13294
13296
|
} else {
|
|
13295
|
-
console.warn(`[session-worktree] skip build step:
|
|
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,26 +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
|
-
}
|
|
13432
|
-
}
|
|
13433
13423
|
async function getBuildScript(mainRoot) {
|
|
13434
|
-
|
|
13435
|
-
|
|
13436
|
-
if (
|
|
13437
|
-
return
|
|
13438
|
-
|
|
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;
|
|
13439
13433
|
}
|
|
13440
|
-
async function shouldSkipDevOnce(mainRoot, stagedPaths) {
|
|
13434
|
+
async function shouldSkipDevOnce(mainRoot, stagedPaths, worktreePath) {
|
|
13441
13435
|
let distMtimeSec;
|
|
13442
13436
|
try {
|
|
13443
13437
|
const st = await fs10.stat(path13.join(mainRoot, "dist/index.js"));
|
|
@@ -13449,18 +13443,28 @@ async function shouldSkipDevOnce(mainRoot, stagedPaths) {
|
|
|
13449
13443
|
if (relevant.length === 0)
|
|
13450
13444
|
return true;
|
|
13451
13445
|
for (const rel of relevant) {
|
|
13452
|
-
|
|
13453
|
-
|
|
13454
|
-
|
|
13455
|
-
|
|
13456
|
-
if (srcMtimeSec > distMtimeSec)
|
|
13457
|
-
return false;
|
|
13458
|
-
} catch {
|
|
13446
|
+
const srcMtimeSec = await statSourceMtime(rel, mainRoot, worktreePath);
|
|
13447
|
+
if (srcMtimeSec === null)
|
|
13448
|
+
return false;
|
|
13449
|
+
if (srcMtimeSec > distMtimeSec)
|
|
13459
13450
|
return false;
|
|
13460
|
-
}
|
|
13461
13451
|
}
|
|
13462
13452
|
return true;
|
|
13463
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
|
+
}
|
|
13464
13468
|
function runCmd(cmd, args, cwd, timeoutMs = 300000) {
|
|
13465
13469
|
return new Promise((resolve11, reject) => {
|
|
13466
13470
|
execFile3(cmd, args, { cwd, timeout: timeoutMs, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
|
|
@@ -16215,10 +16219,11 @@ var codeforgeToolsServer = async (ctx) => {
|
|
|
16215
16219
|
session_merge: tool({
|
|
16216
16220
|
description: description26,
|
|
16217
16221
|
args: {
|
|
16218
|
-
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 改动"),
|
|
16219
16223
|
session_id: z30.string().optional().describe("目标 session id;不传则用当前 session"),
|
|
16220
16224
|
plan_id: z30.string().optional().describe("关联的 plan_id(reviewer 校验时用),格式 plan-YYYYMMDD-HHmmss-NNN"),
|
|
16221
|
-
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)")
|
|
16222
16227
|
},
|
|
16223
16228
|
async execute(args, input) {
|
|
16224
16229
|
return await runSafe("session_merge", async () => {
|
|
@@ -21785,7 +21790,7 @@ import * as zlib from "node:zlib";
|
|
|
21785
21790
|
// lib/version-injected.ts
|
|
21786
21791
|
function getInjectedVersion() {
|
|
21787
21792
|
try {
|
|
21788
|
-
const v = "0.5.
|
|
21793
|
+
const v = "0.5.17";
|
|
21789
21794
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
21790
21795
|
return v;
|
|
21791
21796
|
}
|
|
@@ -23097,6 +23102,70 @@ function buildGitVcsWriteRegex(mainRoot) {
|
|
|
23097
23102
|
var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
|
|
23098
23103
|
var TOUCH_THROTTLE_MS = 5 * 60000;
|
|
23099
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
|
+
}
|
|
23100
23169
|
var CLASS_B_CALLER_WHITELIST = new Set([
|
|
23101
23170
|
"codeforge",
|
|
23102
23171
|
"reviewer",
|
|
@@ -23227,6 +23296,18 @@ function resolveMainRoot2(rawDir) {
|
|
|
23227
23296
|
return rawDir;
|
|
23228
23297
|
}
|
|
23229
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
|
+
}
|
|
23230
23311
|
const mainRoot = resolveMainRoot2(ctx.directory ?? process.cwd());
|
|
23231
23312
|
let policyCfg = {};
|
|
23232
23313
|
try {
|
|
@@ -23245,8 +23326,24 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
23245
23326
|
return {
|
|
23246
23327
|
"tool.execute.before": async (input, output) => {
|
|
23247
23328
|
const sessionId = input.sessionID ?? process.env["CODEFORGE_SESSION_ID"];
|
|
23248
|
-
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
|
+
});
|
|
23249
23345
|
return;
|
|
23346
|
+
}
|
|
23250
23347
|
let denied;
|
|
23251
23348
|
await safeAsync(PLUGIN_NAME25, "tool.execute.before", async () => {
|
|
23252
23349
|
const toolName = input.tool;
|
|
@@ -23301,14 +23398,32 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
23301
23398
|
worktreePath: entry.worktreePath
|
|
23302
23399
|
});
|
|
23303
23400
|
} catch (err) {
|
|
23304
|
-
|
|
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
|
+
});
|
|
23305
23417
|
safeWriteLog(PLUGIN_NAME25, {
|
|
23306
23418
|
hook: "tool.execute.before",
|
|
23307
23419
|
tool: toolName,
|
|
23308
23420
|
sessionID: input.sessionID,
|
|
23309
|
-
action: "
|
|
23310
|
-
|
|
23421
|
+
action: "deny",
|
|
23422
|
+
source: "lazy-bind-failed",
|
|
23423
|
+
error: errMsg,
|
|
23424
|
+
throttled: alreadyNotified
|
|
23311
23425
|
});
|
|
23426
|
+
denied = new DeniedError(reason);
|
|
23312
23427
|
return;
|
|
23313
23428
|
}
|
|
23314
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}"
|