@agent_forge/forge 0.27.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/README.md +158 -0
- package/install.js +182 -0
- package/package.json +35 -0
- package/run.js +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Forge — AI 开发质量门禁引擎
|
|
2
|
+
|
|
3
|
+
AI 写的代码,你放心直接提交吗?
|
|
4
|
+
|
|
5
|
+
Forge 在 AI 编码过程中自动插入结构化质量门禁——从任务创建到代码提交,确保每一步产出物都经过验证。配合 Claude Code 的 Hook 系统实现实时拦截,不需要你手动检查。
|
|
6
|
+
|
|
7
|
+
## 核心功能
|
|
8
|
+
|
|
9
|
+
- **项目级管道** — 从需求定义到发布的全流程质量门禁,按项目规模自动选择
|
|
10
|
+
- **任务级门禁** — 每个开发任务走 5 道轻量门禁:理解 → 方案 → 实现 → 验证 → 完成
|
|
11
|
+
- **实时 Hook 拦截** — 8 个内置 Hook,在 AI 写代码的同时自动检查质量、防止绕过
|
|
12
|
+
- **安全纵深防御** — 三层防御架构:工具拦截 → 文件监控 → 自身保护
|
|
13
|
+
- **跨项目经验库** — 提取项目踩坑经验,在新项目中自动提示
|
|
14
|
+
- **质量评分** — 每个任务完成后自动评分,量化 AI 编码质量
|
|
15
|
+
|
|
16
|
+
## 快速开始
|
|
17
|
+
|
|
18
|
+
需要 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 已安装。
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 安装
|
|
22
|
+
npm install -g @agent_forge/forge
|
|
23
|
+
|
|
24
|
+
# 在项目目录初始化
|
|
25
|
+
cd your-project
|
|
26
|
+
forge init
|
|
27
|
+
|
|
28
|
+
# 在 Claude Code 中开始工作
|
|
29
|
+
# AI 会自动读取 Forge 生成的 Skill 并驱动门禁流程
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
初始化后 Forge 会创建:
|
|
33
|
+
- `.forge/` — 管道定义、Hook 脚本、任务状态
|
|
34
|
+
- `.claude/settings.local.json` — Hook 集成配置
|
|
35
|
+
- `.claude/CLAUDE.md` — 质量协议引用
|
|
36
|
+
- `.claude/skills/` — 管道编排 Skill
|
|
37
|
+
|
|
38
|
+
## 工作流程
|
|
39
|
+
|
|
40
|
+
### 项目级管道
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
forge init → 自动检测项目规模 → 生成门禁管道
|
|
44
|
+
|
|
45
|
+
gate-1-prd (需求定义) → gate-3-plan (实现计划) → gate-4-implement (代码实现) → gate-5-test (测试验证) → gate-6-acceptance (项目验收)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`forge init` 根据项目规模自动选择门禁数量(small / medium / large),`forge status` 查看所有启用的 gate。
|
|
49
|
+
|
|
50
|
+
### 任务级门禁
|
|
51
|
+
|
|
52
|
+
每个开发任务自动走 3 道门禁:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
forge task start --ref feat/add-login --branch # 创建任务 + 分支
|
|
56
|
+
# AI 自动完成工作...
|
|
57
|
+
forge task gate task-implement # ✅ 代码实现(自动检查编译 + 断言)
|
|
58
|
+
forge task gate task-verify # ✅ 测试验证
|
|
59
|
+
forge task gate task-complete # ✅ 完成确认
|
|
60
|
+
forge task score # 查看质量评分
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
门禁之间有时间和活动检查,防止 AI 跳过阶段直接提交。`task-implement` 是自动门禁,会检查编译通过和断言未被弱化。
|
|
64
|
+
|
|
65
|
+
## Hook 系统
|
|
66
|
+
|
|
67
|
+
Forge 通过 Claude Code 的 Hook 机制实现实时质量检查:
|
|
68
|
+
|
|
69
|
+
| Hook | 触发时机 | 功能 |
|
|
70
|
+
|------|----------|------|
|
|
71
|
+
| **task-guard** | Write/Edit 前 | 无活跃任务时 WARN(仅 `.forge/*` 自保护文件 FAIL),保护 Forge 配置不被篡改 |
|
|
72
|
+
| **assertion-check** | Write/Edit 前 | 检测断言弱化(t.Fatal → t.Log、assert! 被删除等),advisory 提醒不阻塞(agent 自检) |
|
|
73
|
+
| **bash-guard** | Bash 前 | 检测命令中的写文件模式(writeFile、cat >、sed -i 等),无任务时 WARN(源码随后被 file-sentinel 隔离) |
|
|
74
|
+
| **auto-compile** | Write/Edit 后 | advisory 提醒用对应技术栈编译命令自检(go build / cargo check / mvn / tsc 等),不强制编译 |
|
|
75
|
+
| **file-sentinel** | Bash 后 | 监控文件变更,未授权修改隔离到 .forge/quarantine/(可恢复,不删除) |
|
|
76
|
+
| **tool-track** | Read 后 | 静默记录 Read 调用到 toollog,供 task-verify 的 read-before-edit 门禁判断(agent 是否先读代码再改) |
|
|
77
|
+
| **task-verify** | 会话结束 | advisory:任务门禁/主分支保护/mandatory review 提醒到 stderr+checklog(不阻塞会话结束) |
|
|
78
|
+
| **skill-scan** | 会话开始 | advisory:扫描 ~/.claude/skills 安全性(forge audit 19 规则),补 install 门控缺口(手动 clone/junction/git pull 进入的 skill),全局 hook 不依赖 forge project |
|
|
79
|
+
|
|
80
|
+
### 安全架构
|
|
81
|
+
|
|
82
|
+
三层纵深防御,监控的是文件而非工具:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
Layer 1: PreToolUse 快速拦截
|
|
86
|
+
├─ task-guard: Write/Edit → 检查任务状态 + 保护 .forge/*
|
|
87
|
+
└─ bash-guard: Bash → 检测写文件模式
|
|
88
|
+
|
|
89
|
+
Layer 2: PostToolUse 文件监控
|
|
90
|
+
└─ file-sentinel: Bash → 对比执行前后 git 状态,未授权变更自动 revert
|
|
91
|
+
|
|
92
|
+
Layer 3: 会话结束验证
|
|
93
|
+
└─ task-verify: 检查任务完成度 + 主分支保护 + 自身版本
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Agent 无法通过 `node -e "fs.writeFileSync()"`、`cat > file`、直接编辑 task JSON 等方式绕过——bash-guard 拦截工具层,file-sentinel 监控文件层,task-guard 保护配置层。
|
|
97
|
+
|
|
98
|
+
## 命令参考
|
|
99
|
+
|
|
100
|
+
### 项目管理
|
|
101
|
+
|
|
102
|
+
| 命令 | 说明 |
|
|
103
|
+
|------|------|
|
|
104
|
+
| `forge init [--mode small\|medium\|large]` | 初始化管道(自动检测项目规模) |
|
|
105
|
+
| `forge status [--json]` | 查看管道状态 |
|
|
106
|
+
| `forge gate <gate-id>` | 验证指定门禁 |
|
|
107
|
+
| `forge validate` | 检查 pipeline.yml 配置 |
|
|
108
|
+
| `forge verify` | 项目完整性检查 + 回归测试 |
|
|
109
|
+
| `forge snapshot` | 检测项目开发阶段 |
|
|
110
|
+
| `forge update` | 自更新到最新版本 |
|
|
111
|
+
|
|
112
|
+
### 任务管理
|
|
113
|
+
|
|
114
|
+
| 命令 | 说明 |
|
|
115
|
+
|------|------|
|
|
116
|
+
| `forge task start --ref <type/desc> --branch` | 创建任务(自动创建分支) |
|
|
117
|
+
| `forge task status` | 查看当前任务门禁状态 |
|
|
118
|
+
| `forge task list` | 列出所有任务 |
|
|
119
|
+
| `forge task gate <gate-id>` | 验证单道任务门禁 |
|
|
120
|
+
| `forge task complete` | 标记任务完成(自动评分) |
|
|
121
|
+
| `forge task score` | 查看任务质量评分 |
|
|
122
|
+
|
|
123
|
+
### 经验库
|
|
124
|
+
|
|
125
|
+
| 命令 | 说明 |
|
|
126
|
+
|------|------|
|
|
127
|
+
| `forge experience list` | 查看经验条目 |
|
|
128
|
+
| `forge knowledge` | 跨项目经验库管理 |
|
|
129
|
+
|
|
130
|
+
## 安装
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# npm(推荐)
|
|
134
|
+
npm install -g @agent_forge/forge
|
|
135
|
+
|
|
136
|
+
# 或从 GitHub Releases 下载二进制
|
|
137
|
+
# https://github.com/MjxUpUp/Forge/releases
|
|
138
|
+
|
|
139
|
+
# 支持平台:macOS (x86_64/ARM64)、Linux (x86_64/ARM64)、Windows (x86_64)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## 项目门禁管道
|
|
143
|
+
|
|
144
|
+
| Gate ID | 名称 | small | medium | large |
|
|
145
|
+
|---------|------|:-----:|:------:|:-----:|
|
|
146
|
+
| gate-1-prd | 需求定义 | ✓ | ✓ | ✓ |
|
|
147
|
+
| gate-3-plan | 实现计划 | ✓ | ✓ | ✓ |
|
|
148
|
+
| gate-4-implement | 代码实现 | | ✓ | ✓ |
|
|
149
|
+
| gate-5-test | 测试验证 | | ✓ | ✓ |
|
|
150
|
+
| gate-6-acceptance | 项目验收 | | ✓ | ✓ |
|
|
151
|
+
| gate-8-release | 发布 | | | ✓ |
|
|
152
|
+
| gate-0-research | 立项调研 | | | ✓ |
|
|
153
|
+
| gate-2-design | 技术方案 | | | ✓ |
|
|
154
|
+
| gate-7-archive | 经验归档 | | | ✓ |
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
Apache-2.0
|
package/install.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const { execSync } = require("child_process");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const https = require("https");
|
|
5
|
+
const { createWriteStream } = require("fs");
|
|
6
|
+
const { pipeline } = require("stream/promises");
|
|
7
|
+
|
|
8
|
+
const VERSION = require("./package.json").version;
|
|
9
|
+
|
|
10
|
+
function getPlatform() {
|
|
11
|
+
const platform = process.platform;
|
|
12
|
+
const arch = process.arch;
|
|
13
|
+
const osMap = { darwin: "darwin", linux: "linux", win32: "windows" };
|
|
14
|
+
const archMap = { x64: "x86_64", arm64: "aarch64" };
|
|
15
|
+
|
|
16
|
+
const goos = osMap[platform];
|
|
17
|
+
const goarch = archMap[arch];
|
|
18
|
+
|
|
19
|
+
if (!goos || !goarch) {
|
|
20
|
+
throw new Error(`Unsupported platform: ${platform}/${arch}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { goos, goarch };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getBinaryName() {
|
|
27
|
+
return process.platform === "win32" ? "forge.exe" : "forge";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function download(url, dest) {
|
|
31
|
+
let lastError;
|
|
32
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
33
|
+
if (attempt > 0) {
|
|
34
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 8000);
|
|
35
|
+
console.log(`Retrying (${attempt + 1}/3) after ${delay / 1000}s...`);
|
|
36
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
let currentUrl = url;
|
|
41
|
+
while (true) {
|
|
42
|
+
const res = await new Promise((resolve, reject) => {
|
|
43
|
+
https
|
|
44
|
+
.get(currentUrl, { timeout: 30000 }, resolve)
|
|
45
|
+
.on("error", reject);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
49
|
+
res.destroy();
|
|
50
|
+
currentUrl = res.headers.location;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (res.statusCode !== 200) {
|
|
54
|
+
res.destroy();
|
|
55
|
+
throw new Error(`Download failed: HTTP ${res.statusCode}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await pipeline(res, createWriteStream(dest));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
lastError = err;
|
|
63
|
+
// Clean up partial download
|
|
64
|
+
try { fs.unlinkSync(dest); } catch (_) {}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw lastError || new Error("Download failed after 3 attempts");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if an existing binary matches the target version.
|
|
72
|
+
* Runs `forge --version` and parses the version string.
|
|
73
|
+
*/
|
|
74
|
+
function existingBinaryMatches(binaryPath) {
|
|
75
|
+
try {
|
|
76
|
+
const output = execSync(`"${binaryPath}" --version`, {
|
|
77
|
+
timeout: 5000,
|
|
78
|
+
stdio: "pipe",
|
|
79
|
+
env: { ...process.env, FORGE_SKIP_UPDATE_CHECK: "1" },
|
|
80
|
+
}).toString().trim();
|
|
81
|
+
// Output format: "forge version 0.14.4 (commit: ...)" or "forge version dev"
|
|
82
|
+
const match = output.match(/forge version (\S+)/);
|
|
83
|
+
return match && match[1] === VERSION;
|
|
84
|
+
} catch (_) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function main() {
|
|
90
|
+
const binDir = path.join(__dirname, "bin");
|
|
91
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
92
|
+
|
|
93
|
+
const binaryName = getBinaryName();
|
|
94
|
+
const binaryPath = path.join(binDir, binaryName);
|
|
95
|
+
|
|
96
|
+
// Skip download if existing binary already matches the target version.
|
|
97
|
+
// This avoids "file busy" errors on Windows when upgrading while
|
|
98
|
+
// forge hooks are actively running in another Claude Code session.
|
|
99
|
+
if (existingBinaryMatches(binaryPath)) {
|
|
100
|
+
console.log(`forge v${VERSION} already installed.`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { goos, goarch } = getPlatform();
|
|
105
|
+
const archiveName = `forge_${VERSION}_${goos}_${goarch}.tar.gz`;
|
|
106
|
+
|
|
107
|
+
// Support custom binary host for regions with poor GitHub connectivity.
|
|
108
|
+
// Usage: FORGE_BINARY_HOST=https://mirror.example.com npm install -g @agent_forge/forge
|
|
109
|
+
const baseUrl = process.env.FORGE_BINARY_HOST
|
|
110
|
+
|| "https://github.com/MjxUpUp/Forge/releases/download";
|
|
111
|
+
const url = `${baseUrl}/v${VERSION}/${archiveName}`;
|
|
112
|
+
|
|
113
|
+
const archivePath = path.join(binDir, archiveName);
|
|
114
|
+
console.log(`Downloading forge v${VERSION} for ${goos}/${goarch}...`);
|
|
115
|
+
|
|
116
|
+
await download(url, archivePath);
|
|
117
|
+
console.log(`Downloaded to ${archivePath}`);
|
|
118
|
+
|
|
119
|
+
// Extract using relative path (cwd=binDir) to avoid Windows tar
|
|
120
|
+
// interpreting "X:/path" as a remote host connection.
|
|
121
|
+
//
|
|
122
|
+
// On Windows, if forge.exe is currently running (e.g. another Claude Code
|
|
123
|
+
// session has hooks active), tar cannot overwrite it. Handle this by:
|
|
124
|
+
// 1. Renaming the old binary to .old (preserves running process)
|
|
125
|
+
// 2. Extracting the new binary
|
|
126
|
+
// 3. Cleaning up archive
|
|
127
|
+
// The run.js wrapper will recover from .old on next launch if needed.
|
|
128
|
+
let renamedOld = false;
|
|
129
|
+
if (process.platform === "win32" && fs.existsSync(binaryPath)) {
|
|
130
|
+
const oldPath = binaryPath + ".old";
|
|
131
|
+
try { fs.unlinkSync(oldPath); } catch (_) {}
|
|
132
|
+
try {
|
|
133
|
+
fs.renameSync(binaryPath, oldPath);
|
|
134
|
+
renamedOld = true;
|
|
135
|
+
} catch (_) {
|
|
136
|
+
// Cannot rename either — will try extract anyway
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
execSync(`tar xzf "${archiveName}"`, { cwd: binDir, stdio: "pipe", timeout: 30000 });
|
|
142
|
+
} catch (err) {
|
|
143
|
+
// Extract failed — likely file busy on Windows
|
|
144
|
+
if (renamedOld) {
|
|
145
|
+
// Restore old binary so the package isn't left broken
|
|
146
|
+
const oldPath = binaryPath + ".old";
|
|
147
|
+
try {
|
|
148
|
+
if (!fs.existsSync(binaryPath)) {
|
|
149
|
+
fs.renameSync(oldPath, binaryPath);
|
|
150
|
+
console.error(`[forge] Extract failed, restored previous binary. Will upgrade on next launch.`);
|
|
151
|
+
}
|
|
152
|
+
} catch (_) {}
|
|
153
|
+
}
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Failed to extract binary (file may be in use). ` +
|
|
156
|
+
`Close other Claude Code sessions and run: npm install -g @agent_forge/forge`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Make executable (Unix)
|
|
161
|
+
if (process.platform !== "win32") {
|
|
162
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Cleanup archive and .old backup
|
|
166
|
+
fs.unlinkSync(archivePath);
|
|
167
|
+
if (renamedOld) {
|
|
168
|
+
try { fs.unlinkSync(binaryPath + ".old"); } catch (_) {}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(`forge v${VERSION} installed successfully.`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main().catch((err) => {
|
|
175
|
+
console.error("Installation failed:", err.message);
|
|
176
|
+
console.error("");
|
|
177
|
+
console.error("If GitHub is unreachable, set a mirror:");
|
|
178
|
+
console.error(" FORGE_BINARY_HOST=https://your-mirror.com npm install -g @agent_forge/forge");
|
|
179
|
+
console.error("");
|
|
180
|
+
console.error("Or download manually: https://github.com/MjxUpUp/Forge/releases");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent_forge/forge",
|
|
3
|
+
"version": "0.27.0",
|
|
4
|
+
"description": "AI 开发质量门禁引擎 — 结构化门禁管道,在 AI 生成的代码进入仓库前进行质量锻造",
|
|
5
|
+
"bin": {
|
|
6
|
+
"forge": "run.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node install.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"install.js",
|
|
13
|
+
"run.js",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/MjxUpUp/Forge"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ai",
|
|
22
|
+
"gate",
|
|
23
|
+
"pipeline",
|
|
24
|
+
"quality",
|
|
25
|
+
"claude-code"
|
|
26
|
+
],
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=16"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"registry": "https://registry.npmjs.org/",
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/run.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn, execSync } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
|
|
7
|
+
const binaryName = process.platform === "win32" ? "forge.exe" : "forge";
|
|
8
|
+
const binaryPath = path.join(__dirname, "bin", binaryName);
|
|
9
|
+
const oldPath = binaryPath + ".old";
|
|
10
|
+
|
|
11
|
+
// --- Windows crash recovery (from lark-cli pattern) ---
|
|
12
|
+
// If forge.exe.old exists, we're recovering from an interrupted or failed update.
|
|
13
|
+
function recoverOldBinary() {
|
|
14
|
+
if (!fs.existsSync(oldPath)) return;
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(binaryPath)) {
|
|
17
|
+
// forge.exe missing but .old exists — rename first, then probe.
|
|
18
|
+
// On Windows, .old is not a recognized executable extension,
|
|
19
|
+
// so we must restore the .exe name before testing.
|
|
20
|
+
try {
|
|
21
|
+
fs.renameSync(oldPath, binaryPath);
|
|
22
|
+
execSync(`"${binaryPath}" --version`, { timeout: 5000, stdio: "pipe", env: { ...process.env, FORGE_SKIP_UPDATE_CHECK: "1" } });
|
|
23
|
+
console.error("[forge] Recovered binary from .old backup");
|
|
24
|
+
} catch (e) {
|
|
25
|
+
// Restored binary is broken — put it back as .old so we don't lose it
|
|
26
|
+
try { fs.renameSync(binaryPath, oldPath); } catch (_) {}
|
|
27
|
+
console.error("[forge] WARNING: .old binary is also broken");
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
// Both exist — verify current binary works, then clean up .old
|
|
31
|
+
try {
|
|
32
|
+
execSync(`"${binaryPath}" --version`, { timeout: 5000, stdio: "pipe", env: { ...process.env, FORGE_SKIP_UPDATE_CHECK: "1" } });
|
|
33
|
+
fs.unlinkSync(oldPath);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// Current binary broken — replace with .old (rename first, then probe)
|
|
36
|
+
try { fs.unlinkSync(binaryPath); } catch (_) {}
|
|
37
|
+
try {
|
|
38
|
+
fs.renameSync(oldPath, binaryPath);
|
|
39
|
+
execSync(`"${binaryPath}" --version`, { timeout: 5000, stdio: "pipe", env: { ...process.env, FORGE_SKIP_UPDATE_CHECK: "1" } });
|
|
40
|
+
console.error("[forge] Recovered binary from .old backup");
|
|
41
|
+
} catch (e2) {
|
|
42
|
+
console.error("[forge] WARNING: both binary and .old are broken");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
recoverOldBinary();
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(binaryPath)) {
|
|
51
|
+
// Binary not available (e.g., mid npm upgrade). Silently approve to avoid
|
|
52
|
+
// blocking Claude Code hooks during installation.
|
|
53
|
+
console.log('{"decision":"approve"}');
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
58
|
+
stdio: "inherit",
|
|
59
|
+
env: process.env,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
child.on("exit", (code) => {
|
|
63
|
+
process.exit(code || 0);
|
|
64
|
+
});
|