@agent_forge/forge 1.14.0 → 1.15.1

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.
Files changed (3) hide show
  1. package/package.json +7 -4
  2. package/run.js +25 -48
  3. package/install.js +0 -182
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "@agent_forge/forge",
3
- "version": "1.14.0",
3
+ "version": "1.15.1",
4
4
  "description": "AI 开发质量门禁引擎 — 结构化门禁管道,在 AI 生成的代码进入仓库前进行质量锻造",
5
5
  "bin": {
6
6
  "forge": "run.js"
7
7
  },
8
- "scripts": {
9
- "postinstall": "node install.js"
8
+ "optionalDependencies": {
9
+ "@agent_forge/forge-darwin-arm64": "1.15.1",
10
+ "@agent_forge/forge-darwin-x64": "1.15.1",
11
+ "@agent_forge/forge-linux-arm64": "1.15.1",
12
+ "@agent_forge/forge-linux-x64": "1.15.1",
13
+ "@agent_forge/forge-win32-x64": "1.15.1"
10
14
  },
11
15
  "files": [
12
- "install.js",
13
16
  "run.js",
14
17
  "README.md"
15
18
  ],
package/run.js CHANGED
@@ -1,55 +1,32 @@
1
1
  #!/usr/bin/env node
2
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
- }
3
+ const { spawn } = require("child_process");
4
+
5
+ // 通过 optionalDependencies 平台子包定位当前平台的二进制。
6
+ // npm install 时按 os/cpu 只装匹配当前平台的一个子包(@agent_forge/forge-<platform>-<arch>),
7
+ // 二进制随子包落进 node_modules,无需 install script——npm 12 install scripts 默认
8
+ // 禁用,平台分包是官方推荐的二进制分发方式(esbuild/rollup/turbo 同模式)。
9
+ const exe = process.platform === "win32" ? "forge.exe" : "forge";
10
+ const platformPkg = `@agent_forge/forge-${process.platform}-${process.arch}`;
11
+
12
+ // 支持的平台白名单(goreleaser 构建矩阵:linux/darwin × amd64/arm64 + windows/amd64)。
13
+ // 不在此列的平台(如 windows-arm64)无对应子包,require.resolve 必失败——明确 stderr 提示,
14
+ // 不混入"子包未装"的静默 approve,避免用户误以为 Forge 正常工作却零拦截。
15
+ const SUPPORTED = new Set([
16
+ "darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-x64",
17
+ ]);
18
+ const platKey = `${process.platform}-${process.arch}`;
19
+ if (!SUPPORTED.has(platKey)) {
20
+ console.error(`[forge] unsupported platform: ${platKey} (no prebuilt binary); failing open — hooks will not fire.`);
21
+ console.log('{"decision":"approve"}');
22
+ process.exit(0);
46
23
  }
47
24
 
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.
25
+ let binaryPath;
26
+ try {
27
+ binaryPath = require.resolve(`${platformPkg}/bin/${exe}`);
28
+ } catch (_) {
29
+ // 支持的平台但子包未装(npm 12 + --omit=optional / 安装中断)——静默 approve 避免阻塞 hooks。
53
30
  console.log('{"decision":"approve"}');
54
31
  process.exit(0);
55
32
  }
package/install.js DELETED
@@ -1,182 +0,0 @@
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
- });