@andyqiu/codeforge 0.6.13 → 0.7.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/bin/codeforge.mjs +17 -127
- package/codeforge.config.yaml +0 -1
- package/codeforge.json +1 -1
- package/commands/parallel.md +1 -1
- package/dist/index.js +114 -619
- package/install.mjs +2 -15
- package/package.json +1 -12
- package/workflows/feature-dev.yaml +0 -3
package/bin/codeforge.mjs
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* codeforge — opencode 零侵入扩展包的安装入口
|
|
4
|
+
* ADR:cli-deadcode-cleanup
|
|
4
5
|
*
|
|
5
6
|
* 一行命令安装:
|
|
6
7
|
* npm install -g @andyqiu/codeforge
|
|
7
8
|
*
|
|
8
9
|
* 子命令:
|
|
9
|
-
* install [--dry-run]
|
|
10
|
+
* install [--dry-run]
|
|
10
11
|
* 把 CodeForge 单 bundle 注入到 opencode(写 opencode.json plugin entry)
|
|
11
12
|
* uninstall 卸载(不动 opencode 自身)
|
|
12
13
|
* list 探测 opencode 是否在机器上
|
|
13
14
|
* version 打印版本
|
|
14
|
-
* rollback [--target=<path>] [--dry-run]
|
|
15
|
-
* 恢复到上一次 backup(auto_install 失败时手动救场)
|
|
16
15
|
* runtime where [<path>] 打印当前项目对应的全局运行时目录
|
|
17
16
|
* adr-init [--force] [--dry-run] [--write-prepare] [--no-pre-push]
|
|
18
17
|
* 把 ADR 体系(hooks + scripts + 模板)下发到当前 git 项目
|
|
@@ -25,8 +24,7 @@
|
|
|
25
24
|
*/
|
|
26
25
|
|
|
27
26
|
import { spawnSync } from "node:child_process"
|
|
28
|
-
import {
|
|
29
|
-
import { homedir } from "node:os"
|
|
27
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
30
28
|
import * as path from "node:path"
|
|
31
29
|
import * as url from "node:url"
|
|
32
30
|
|
|
@@ -94,15 +92,12 @@ function parseArgs(argv) {
|
|
|
94
92
|
|
|
95
93
|
// ────────────────────────────────────────────────────────────────────
|
|
96
94
|
// opencode installer:薄壳,统一调 install.mjs(Node ESM 跨平台)
|
|
97
|
-
// ADR:unify-install-to-node-mjs
|
|
98
|
-
//
|
|
99
|
-
// 用于 upgrade 命令:旧版 CLI 调新包时,新包里只有 install.mjs
|
|
95
|
+
// ADR:unify-install-to-node-mjs / ADR:cli-deadcode-cleanup
|
|
96
|
+
// install.sh/.ps1 已合并进 install.mjs,installer 永远走 node。
|
|
100
97
|
// ────────────────────────────────────────────────────────────────────
|
|
101
98
|
function resolveInstallerScript() {
|
|
102
99
|
const mjsScript = path.join(REPO_ROOT, "install.mjs")
|
|
103
|
-
if (existsSync(mjsScript)) return { script: mjsScript
|
|
104
|
-
const shScript = path.join(REPO_ROOT, "install.sh")
|
|
105
|
-
if (existsSync(shScript)) return { script: shScript, useNode: false }
|
|
100
|
+
if (existsSync(mjsScript)) return { script: mjsScript }
|
|
106
101
|
return null
|
|
107
102
|
}
|
|
108
103
|
|
|
@@ -112,24 +107,14 @@ function installOpencode({ scope, dryRun, extraArgs, quiet = false, verbose = fa
|
|
|
112
107
|
err(`installer 脚本不存在:${path.join(REPO_ROOT, "install.mjs")}`)
|
|
113
108
|
return 2
|
|
114
109
|
}
|
|
115
|
-
const { script
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (extraArgs) args.push(...extraArgs)
|
|
124
|
-
} else {
|
|
125
|
-
// fallback:旧版包里只有 install.sh(v0.5 过渡期)
|
|
126
|
-
cmd = "bash"
|
|
127
|
-
args = [script]
|
|
128
|
-
if (scope === "global") args.push("--global")
|
|
129
|
-
if (dryRun) args.push("--dry-run")
|
|
130
|
-
if (extraArgs) args.push(...extraArgs)
|
|
131
|
-
}
|
|
132
|
-
if (!quiet) log(`opencode: ${useNode ? "node" : "bash"} ${args.join(" ")}`)
|
|
110
|
+
const { script } = resolved
|
|
111
|
+
const cmd = process.execPath
|
|
112
|
+
const args = [script]
|
|
113
|
+
if (scope === "global") args.push("--global")
|
|
114
|
+
if (dryRun) args.push("--dry-run")
|
|
115
|
+
if (verbose) args.push("--verbose")
|
|
116
|
+
if (extraArgs) args.push(...extraArgs)
|
|
117
|
+
if (!quiet) log(`opencode: node ${args.join(" ")}`)
|
|
133
118
|
const r = spawnSync(cmd, args, { stdio: quiet ? "pipe" : "inherit", cwd: REPO_ROOT })
|
|
134
119
|
if (quiet && r.status !== 0) {
|
|
135
120
|
if (r.stderr) process.stderr.write(r.stderr)
|
|
@@ -144,15 +129,11 @@ function uninstallOpencode({ scope }) {
|
|
|
144
129
|
err(`installer 脚本不存在:${path.join(REPO_ROOT, "install.mjs")}`)
|
|
145
130
|
return 2
|
|
146
131
|
}
|
|
147
|
-
const { script
|
|
148
|
-
|
|
149
|
-
err("Windows 下旧版 install.sh 不支持,请重新安装:npm install -g @andyqiu/codeforge")
|
|
150
|
-
return 2
|
|
151
|
-
}
|
|
152
|
-
const cmd = useNode ? process.execPath : "bash"
|
|
132
|
+
const { script } = resolved
|
|
133
|
+
const cmd = process.execPath
|
|
153
134
|
const args = [script, "--uninstall"]
|
|
154
135
|
if (scope === "global") args.push("--global")
|
|
155
|
-
log(`opencode uninstall:
|
|
136
|
+
log(`opencode uninstall: node ${args.join(" ")}`)
|
|
156
137
|
const r = spawnSync(cmd, args, { stdio: "inherit", cwd: REPO_ROOT })
|
|
157
138
|
return r.status ?? 1
|
|
158
139
|
}
|
|
@@ -175,7 +156,6 @@ function cmdInstall(args) {
|
|
|
175
156
|
const autoSkipBuild = fromNpm
|
|
176
157
|
const verbose = !!args.flags.verbose
|
|
177
158
|
const extraArgs = []
|
|
178
|
-
if (args.flags["enable-legacy-tools"]) extraArgs.push("--enable-legacy-tools")
|
|
179
159
|
if (args.flags["skip-build"] || autoSkipBuild) extraArgs.push("--skip-build")
|
|
180
160
|
|
|
181
161
|
// npm 场景:dist/index.js 必须已经被 prepack 打好;找不到就直接报错并提示
|
|
@@ -222,80 +202,6 @@ function cmdList() {
|
|
|
222
202
|
return 0
|
|
223
203
|
}
|
|
224
204
|
|
|
225
|
-
// ────────────────────────────────────────────────────────────────────
|
|
226
|
-
// 子命令:rollback(ADR-0047)—— 把最近的 .bak.<ver> 文件恢复回 index.js
|
|
227
|
-
// ────────────────────────────────────────────────────────────────────
|
|
228
|
-
function defaultBundlePath() {
|
|
229
|
-
const candidates = [path.join(homedir(), ".config", "opencode", "codeforge", "index.js")]
|
|
230
|
-
if (process.platform === "win32") {
|
|
231
|
-
if (process.env.APPDATA) candidates.push(path.join(process.env.APPDATA, "opencode", "codeforge", "index.js"))
|
|
232
|
-
if (process.env.LOCALAPPDATA) candidates.push(path.join(process.env.LOCALAPPDATA, "opencode", "codeforge", "index.js"))
|
|
233
|
-
}
|
|
234
|
-
for (const c of candidates) {
|
|
235
|
-
if (existsSync(c)) return c
|
|
236
|
-
}
|
|
237
|
-
return candidates[0]
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function cmdRollback(args) {
|
|
241
|
-
const target = typeof args.flags.target === "string" ? args.flags.target : defaultBundlePath()
|
|
242
|
-
const dryRun = !!args.flags["dry-run"] || !!args.flags.dryRun
|
|
243
|
-
|
|
244
|
-
log(`CodeForge rollback —— 恢复到上一次 backup`)
|
|
245
|
-
log(` target : ${target}`)
|
|
246
|
-
log(` dry-run : ${dryRun}`)
|
|
247
|
-
hr()
|
|
248
|
-
|
|
249
|
-
const dir = path.dirname(target)
|
|
250
|
-
const base = path.basename(target)
|
|
251
|
-
if (!existsSync(dir)) {
|
|
252
|
-
err(`目标目录不存在:${dir}`)
|
|
253
|
-
err(`提示:先跑 codeforge install 安装一次,自动更新失败时再 rollback。`)
|
|
254
|
-
return 1
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
let entries
|
|
258
|
-
try {
|
|
259
|
-
entries = readdirSync(dir)
|
|
260
|
-
} catch (e) {
|
|
261
|
-
err(`读取目录失败:${e.message}`)
|
|
262
|
-
return 1
|
|
263
|
-
}
|
|
264
|
-
const prefix = `${base}.bak.`
|
|
265
|
-
const backups = entries
|
|
266
|
-
.filter((f) => f.startsWith(prefix))
|
|
267
|
-
.map((f) => {
|
|
268
|
-
const full = path.join(dir, f)
|
|
269
|
-
let mtimeMs = 0
|
|
270
|
-
try { mtimeMs = statSync(full).mtimeMs } catch {}
|
|
271
|
-
return { full, name: f, mtimeMs, ver: f.substring(prefix.length) }
|
|
272
|
-
})
|
|
273
|
-
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
274
|
-
|
|
275
|
-
if (backups.length === 0) {
|
|
276
|
-
err(`找不到任何 backup(pattern: ${prefix}*)`)
|
|
277
|
-
err(`提示:自动更新尚未执行过 / backup 已被清理。可跑 npm install -g @andyqiu/codeforge 重装。`)
|
|
278
|
-
return 1
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const latest = backups[0]
|
|
282
|
-
log(`找到 ${backups.length} 个 backup,最新:${latest.name}`)
|
|
283
|
-
if (dryRun) {
|
|
284
|
-
ok(`[dry-run] 会执行:cp ${latest.full} → ${target}`)
|
|
285
|
-
return 0
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
try {
|
|
289
|
-
copyFileSync(latest.full, target)
|
|
290
|
-
} catch (e) {
|
|
291
|
-
err(`恢复失败:${e.message}`)
|
|
292
|
-
return 1
|
|
293
|
-
}
|
|
294
|
-
ok(`已恢复 ${latest.name} → ${base}(版本:${latest.ver})`)
|
|
295
|
-
ok(`下次启动 opencode 即可生效`)
|
|
296
|
-
return 0
|
|
297
|
-
}
|
|
298
|
-
|
|
299
205
|
// ────────────────────────────────────────────────────────────────────
|
|
300
206
|
// 子命令:upgrade —— 升级到 npm latest 并重新 install --global
|
|
301
207
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -366,17 +272,6 @@ async function cmdUpgrade(args) {
|
|
|
366
272
|
return 1
|
|
367
273
|
}
|
|
368
274
|
|
|
369
|
-
// npm install 成功后刷新 opencode plugin 缓存,确保重启后加载新版本
|
|
370
|
-
try {
|
|
371
|
-
const cacheDir = path.join(homedir(), ".cache", "opencode", "packages", "@andyqiu", "teamkit@latest")
|
|
372
|
-
if (existsSync(cacheDir)) {
|
|
373
|
-
rmSync(cacheDir, { recursive: true, force: true })
|
|
374
|
-
log("已清除 opencode plugin 缓存,重启 opencode 后生效")
|
|
375
|
-
}
|
|
376
|
-
} catch {
|
|
377
|
-
warn("清除 opencode 缓存失败(非致命,可手动重启 opencode)")
|
|
378
|
-
}
|
|
379
|
-
|
|
380
275
|
const code = installOpencode({ scope: "global", dryRun: false, extraArgs: ["--skip-build"], quiet: true })
|
|
381
276
|
if (code !== 0) {
|
|
382
277
|
err(`install --global 失败 (exit=${code})`)
|
|
@@ -512,7 +407,6 @@ function cmdHelp() {
|
|
|
512
407
|
codeforge uninstall
|
|
513
408
|
codeforge list
|
|
514
409
|
codeforge version
|
|
515
|
-
codeforge rollback [--target=<path>] [--dry-run] # 恢复最近 backup(auto_install 失败救场)
|
|
516
410
|
codeforge upgrade|update [--dry-run] # 升级到 npm latest 并重新 install --global
|
|
517
411
|
codeforge doctor [--project] # 诊断 CodeForge 安装健康状态(manifest/disk/source 三方比对)
|
|
518
412
|
codeforge runtime where [<path>] # 打印当前项目的全局运行时目录
|
|
@@ -525,8 +419,6 @@ function cmdHelp() {
|
|
|
525
419
|
参数:
|
|
526
420
|
--dry-run 只打印操作,不真改
|
|
527
421
|
--skip-build 跳过 npm run build(已有 dist/index.js 时增量装)
|
|
528
|
-
--enable-legacy-tools 启用旧版 file-based tools(默认禁用,避免 zod 跨实例 bug)
|
|
529
|
-
--target rollback 子命令:指定 index.js 路径(默认自动探测)
|
|
530
422
|
|
|
531
423
|
参数(adr-init 专用):
|
|
532
424
|
--force 覆盖已存在文件(覆盖前自动 .bak.<ts> 备份)
|
|
@@ -573,8 +465,6 @@ async function main() {
|
|
|
573
465
|
case "-v":
|
|
574
466
|
console.log(getVersion())
|
|
575
467
|
return 0
|
|
576
|
-
case "rollback":
|
|
577
|
-
return cmdRollback(args)
|
|
578
468
|
case "upgrade":
|
|
579
469
|
case "update":
|
|
580
470
|
return cmdUpgrade(args)
|
package/codeforge.config.yaml
CHANGED
package/codeforge.json
CHANGED
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
},
|
|
153
153
|
|
|
154
154
|
"update": {
|
|
155
|
-
"_doc": "自动更新检查(ADR-0047)。opencode 启动时由 plugins/update-checker.ts 后台拉 npm registry latest,auto_install=true 时自动替换 ~/.config/opencode/codeforge/index.js(下次启动生效),失败/离线静默回退到 GitHub Releases
|
|
155
|
+
"_doc": "自动更新检查(ADR-0047)。opencode 启动时由 plugins/update-checker.ts 后台拉 npm registry latest,auto_install=true 时自动替换 ~/.config/opencode/codeforge/index.js(下次启动生效),失败/离线静默回退到 GitHub Releases。手动降级:npm install -g @andyqiu/codeforge@<旧版本>。",
|
|
156
156
|
"auto_check_enabled": true,
|
|
157
157
|
"interval_hours": 6,
|
|
158
158
|
"package": "@andyqiu/codeforge",
|
package/commands/parallel.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -20166,35 +20166,21 @@ var toolPolicyServer = async (ctx) => {
|
|
|
20166
20166
|
var handler19 = toolPolicyServer;
|
|
20167
20167
|
|
|
20168
20168
|
// plugins/update-checker.ts
|
|
20169
|
-
import { existsSync as
|
|
20170
|
-
import { homedir as
|
|
20171
|
-
import { join as join25 } from "node:path";
|
|
20169
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
|
|
20170
|
+
import { homedir as homedir8 } from "node:os";
|
|
20171
|
+
import { dirname as dirname15, join as join25 } from "node:path";
|
|
20172
20172
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
20173
20173
|
|
|
20174
20174
|
// lib/update-checker-impl.ts
|
|
20175
|
-
import {
|
|
20176
|
-
import {
|
|
20177
|
-
copyFileSync,
|
|
20178
|
-
existsSync as existsSync5,
|
|
20179
|
-
mkdirSync as mkdirSync3,
|
|
20180
|
-
mkdtempSync,
|
|
20181
|
-
readFileSync as readFileSync5,
|
|
20182
|
-
readdirSync as readdirSync3,
|
|
20183
|
-
renameSync,
|
|
20184
|
-
statSync as statSync4,
|
|
20185
|
-
unlinkSync,
|
|
20186
|
-
writeFileSync as writeFileSync2
|
|
20187
|
-
} from "node:fs";
|
|
20188
|
-
import { homedir as homedir8, tmpdir } from "node:os";
|
|
20175
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
20189
20176
|
import { dirname as dirname14, join as join24 } from "node:path";
|
|
20190
20177
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
20191
20178
|
import * as https from "node:https";
|
|
20192
|
-
import * as zlib from "node:zlib";
|
|
20193
20179
|
|
|
20194
20180
|
// lib/version-injected.ts
|
|
20195
20181
|
function getInjectedVersion() {
|
|
20196
20182
|
try {
|
|
20197
|
-
const v = "0.
|
|
20183
|
+
const v = "0.7.0";
|
|
20198
20184
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
20199
20185
|
return v;
|
|
20200
20186
|
}
|
|
@@ -20205,48 +20191,6 @@ function getInjectedVersion() {
|
|
|
20205
20191
|
}
|
|
20206
20192
|
|
|
20207
20193
|
// lib/update-checker-impl.ts
|
|
20208
|
-
async function checkUpdateOnce(opts) {
|
|
20209
|
-
const local = opts.localVersion ?? readLocalVersion();
|
|
20210
|
-
const cacheFile = opts.cacheFile ?? defaultCacheFile();
|
|
20211
|
-
const now = (opts.now ?? Date.now)();
|
|
20212
|
-
if (!opts.forceFresh) {
|
|
20213
|
-
const cached = readCache(cacheFile);
|
|
20214
|
-
if (cached && cached.repo === opts.repo && now - cached.checkedAt < opts.intervalMs) {
|
|
20215
|
-
return {
|
|
20216
|
-
hasUpdate: cmpVersion(local, cached.remote) < 0,
|
|
20217
|
-
local,
|
|
20218
|
-
remote: cached.remote,
|
|
20219
|
-
fromCache: true,
|
|
20220
|
-
cacheAgeMs: now - cached.checkedAt
|
|
20221
|
-
};
|
|
20222
|
-
}
|
|
20223
|
-
}
|
|
20224
|
-
const fetcher = opts.fetcher ?? fetchLatestTagFromGitHub;
|
|
20225
|
-
let remote = null;
|
|
20226
|
-
let fetchErr;
|
|
20227
|
-
try {
|
|
20228
|
-
remote = await fetcher(opts.repo);
|
|
20229
|
-
} catch (e) {
|
|
20230
|
-
fetchErr = e instanceof Error ? e.message : String(e);
|
|
20231
|
-
}
|
|
20232
|
-
if (remote === null) {
|
|
20233
|
-
writeCache(cacheFile, { checkedAt: now, remote: local, repo: opts.repo });
|
|
20234
|
-
return {
|
|
20235
|
-
hasUpdate: false,
|
|
20236
|
-
local,
|
|
20237
|
-
remote: "(no release)",
|
|
20238
|
-
fromCache: false,
|
|
20239
|
-
error: fetchErr ?? "no_release"
|
|
20240
|
-
};
|
|
20241
|
-
}
|
|
20242
|
-
writeCache(cacheFile, { checkedAt: now, remote, repo: opts.repo });
|
|
20243
|
-
return {
|
|
20244
|
-
hasUpdate: cmpVersion(local, remote) < 0,
|
|
20245
|
-
local,
|
|
20246
|
-
remote,
|
|
20247
|
-
fromCache: false
|
|
20248
|
-
};
|
|
20249
|
-
}
|
|
20250
20194
|
function cmpVersion(a, b) {
|
|
20251
20195
|
const pa = parseSemver(a);
|
|
20252
20196
|
const pb = parseSemver(b);
|
|
@@ -20290,97 +20234,6 @@ function readLocalVersion() {
|
|
|
20290
20234
|
return "0.0.0";
|
|
20291
20235
|
}
|
|
20292
20236
|
}
|
|
20293
|
-
function defaultCacheDir() {
|
|
20294
|
-
return process.env["CODEFORGE_CACHE_DIR"] ?? join24(homedir8(), ".cache", "codeforge");
|
|
20295
|
-
}
|
|
20296
|
-
function defaultCacheFile() {
|
|
20297
|
-
return join24(defaultCacheDir(), "update-check.json");
|
|
20298
|
-
}
|
|
20299
|
-
function readCache(file) {
|
|
20300
|
-
try {
|
|
20301
|
-
if (!existsSync5(file))
|
|
20302
|
-
return null;
|
|
20303
|
-
const raw = readFileSync5(file, "utf8");
|
|
20304
|
-
const obj = JSON.parse(raw);
|
|
20305
|
-
if (obj && typeof obj === "object" && typeof obj.checkedAt === "number" && typeof obj.remote === "string" && typeof obj.repo === "string") {
|
|
20306
|
-
return obj;
|
|
20307
|
-
}
|
|
20308
|
-
return null;
|
|
20309
|
-
} catch {
|
|
20310
|
-
return null;
|
|
20311
|
-
}
|
|
20312
|
-
}
|
|
20313
|
-
function writeCache(file, entry) {
|
|
20314
|
-
try {
|
|
20315
|
-
mkdirSync3(dirname14(file), { recursive: true });
|
|
20316
|
-
writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
|
|
20317
|
-
} catch {}
|
|
20318
|
-
}
|
|
20319
|
-
var GITHUB_API_HOST = "api.github.com";
|
|
20320
|
-
var MAX_REDIRECTS = 5;
|
|
20321
|
-
function fetchLatestTagFromGitHub(repo) {
|
|
20322
|
-
return getJsonWithRedirect(`https://${GITHUB_API_HOST}/repos/${repo}/releases/latest`, MAX_REDIRECTS).then((body) => {
|
|
20323
|
-
if (body === null)
|
|
20324
|
-
return null;
|
|
20325
|
-
try {
|
|
20326
|
-
const json = JSON.parse(body);
|
|
20327
|
-
return typeof json.tag_name === "string" && json.tag_name.length > 0 ? json.tag_name : null;
|
|
20328
|
-
} catch (e) {
|
|
20329
|
-
throw e instanceof Error ? e : new Error(String(e));
|
|
20330
|
-
}
|
|
20331
|
-
});
|
|
20332
|
-
}
|
|
20333
|
-
function getJsonWithRedirect(url2, hopsLeft) {
|
|
20334
|
-
return new Promise((resolve17, reject) => {
|
|
20335
|
-
const u = new URL(url2);
|
|
20336
|
-
const headers = {
|
|
20337
|
-
"User-Agent": "codeforge-update-checker",
|
|
20338
|
-
Accept: "application/vnd.github+json"
|
|
20339
|
-
};
|
|
20340
|
-
if (process.env["GITHUB_TOKEN"]) {
|
|
20341
|
-
headers["Authorization"] = `Bearer ${process.env["GITHUB_TOKEN"]}`;
|
|
20342
|
-
}
|
|
20343
|
-
const req = https.request({
|
|
20344
|
-
host: u.hostname,
|
|
20345
|
-
path: u.pathname + (u.search ?? ""),
|
|
20346
|
-
method: "GET",
|
|
20347
|
-
headers,
|
|
20348
|
-
timeout: 5000
|
|
20349
|
-
}, (res) => {
|
|
20350
|
-
const status = res.statusCode ?? 0;
|
|
20351
|
-
if (status >= 300 && status < 400 && res.headers.location) {
|
|
20352
|
-
res.resume();
|
|
20353
|
-
if (hopsLeft <= 0) {
|
|
20354
|
-
reject(new Error("too_many_redirects"));
|
|
20355
|
-
return;
|
|
20356
|
-
}
|
|
20357
|
-
const next = new URL(res.headers.location, url2).toString();
|
|
20358
|
-
getJsonWithRedirect(next, hopsLeft - 1).then(resolve17, reject);
|
|
20359
|
-
return;
|
|
20360
|
-
}
|
|
20361
|
-
if (status === 404) {
|
|
20362
|
-
res.resume();
|
|
20363
|
-
resolve17(null);
|
|
20364
|
-
return;
|
|
20365
|
-
}
|
|
20366
|
-
if (status >= 400) {
|
|
20367
|
-
res.resume();
|
|
20368
|
-
reject(new Error(`http_${status}`));
|
|
20369
|
-
return;
|
|
20370
|
-
}
|
|
20371
|
-
let body = "";
|
|
20372
|
-
res.setEncoding("utf8");
|
|
20373
|
-
res.on("data", (chunk) => body += chunk);
|
|
20374
|
-
res.on("end", () => resolve17(body));
|
|
20375
|
-
});
|
|
20376
|
-
req.on("timeout", () => {
|
|
20377
|
-
req.destroy();
|
|
20378
|
-
reject(new Error("timeout"));
|
|
20379
|
-
});
|
|
20380
|
-
req.on("error", reject);
|
|
20381
|
-
req.end();
|
|
20382
|
-
});
|
|
20383
|
-
}
|
|
20384
20237
|
async function fetchLatestFromNpm(opts) {
|
|
20385
20238
|
const registry = (opts.registry ?? "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
20386
20239
|
const channel = opts.channel ?? "latest";
|
|
@@ -20449,256 +20302,75 @@ function defaultHttpFetcher(url2, timeoutMs) {
|
|
|
20449
20302
|
req.end();
|
|
20450
20303
|
});
|
|
20451
20304
|
}
|
|
20452
|
-
|
|
20453
|
-
|
|
20454
|
-
|
|
20455
|
-
|
|
20456
|
-
|
|
20457
|
-
|
|
20458
|
-
|
|
20459
|
-
extractTarToDir(tarBuf, tmpRoot);
|
|
20460
|
-
const bundlePath = join24(tmpRoot, "package", "dist", "index.js");
|
|
20461
|
-
if (!existsSync5(bundlePath)) {
|
|
20462
|
-
throw new Error(`bundle_not_found: ${bundlePath}`);
|
|
20463
|
-
}
|
|
20464
|
-
return { bundlePath, extractDir: tmpRoot };
|
|
20465
|
-
}
|
|
20466
|
-
function verifyIntegrity(buf, expected) {
|
|
20467
|
-
const m = /^([a-z0-9]+)-(.+)$/i.exec(expected.trim());
|
|
20468
|
-
if (!m) {
|
|
20469
|
-
throw new Error(`integrity_format_invalid: ${expected}`);
|
|
20470
|
-
}
|
|
20471
|
-
const algo = m[1].toLowerCase();
|
|
20472
|
-
const expectedB64 = m[2];
|
|
20473
|
-
if (algo !== "sha512" && algo !== "sha256" && algo !== "sha384") {
|
|
20474
|
-
throw new Error(`integrity_algo_unsupported: ${algo}`);
|
|
20475
|
-
}
|
|
20476
|
-
const actualB64 = createHash4(algo).update(buf).digest("base64");
|
|
20477
|
-
if (actualB64 !== expectedB64) {
|
|
20478
|
-
throw new Error(`integrity_mismatch: expected ${algo}=${expectedB64.slice(0, 16)}... got ${actualB64.slice(0, 16)}...`);
|
|
20479
|
-
}
|
|
20480
|
-
}
|
|
20481
|
-
function extractTarToDir(tarBuf, destRoot) {
|
|
20482
|
-
let offset = 0;
|
|
20483
|
-
while (offset + 512 <= tarBuf.length) {
|
|
20484
|
-
const header = tarBuf.subarray(offset, offset + 512);
|
|
20485
|
-
if (header.every((b) => b === 0))
|
|
20486
|
-
break;
|
|
20487
|
-
const nameRaw = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
|
|
20488
|
-
if (!nameRaw) {
|
|
20489
|
-
offset += 512;
|
|
20490
|
-
continue;
|
|
20491
|
-
}
|
|
20492
|
-
const sizeOctal = header.subarray(124, 124 + 12).toString("ascii").replace(/\0.*$/, "").trim();
|
|
20493
|
-
const size = sizeOctal ? parseInt(sizeOctal, 8) : 0;
|
|
20494
|
-
const typeFlag = header.subarray(156, 157).toString("ascii");
|
|
20495
|
-
const prefixRaw = header.subarray(345, 345 + 155).toString("utf8").replace(/\0.*$/, "");
|
|
20496
|
-
const fullName = prefixRaw ? `${prefixRaw}/${nameRaw}` : nameRaw;
|
|
20497
|
-
offset += 512;
|
|
20498
|
-
if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
|
|
20499
|
-
const fileBuf = tarBuf.subarray(offset, offset + size);
|
|
20500
|
-
const dest = join24(destRoot, fullName);
|
|
20501
|
-
mkdirSync3(dirname14(dest), { recursive: true });
|
|
20502
|
-
writeFileSync2(dest, fileBuf);
|
|
20503
|
-
} else if (typeFlag === "5") {
|
|
20504
|
-
mkdirSync3(join24(destRoot, fullName), { recursive: true });
|
|
20505
|
-
}
|
|
20506
|
-
offset += Math.ceil(size / 512) * 512;
|
|
20507
|
-
}
|
|
20508
|
-
}
|
|
20509
|
-
function defaultBinaryFetcher(url2) {
|
|
20510
|
-
return downloadBinary(url2, 3);
|
|
20511
|
-
}
|
|
20512
|
-
function downloadBinary(url2, hopsLeft) {
|
|
20513
|
-
return new Promise((resolve17, reject) => {
|
|
20514
|
-
const u = new URL(url2);
|
|
20515
|
-
const req = https.request({
|
|
20516
|
-
host: u.hostname,
|
|
20517
|
-
port: u.port || undefined,
|
|
20518
|
-
path: u.pathname + (u.search ?? ""),
|
|
20519
|
-
method: "GET",
|
|
20520
|
-
headers: { "User-Agent": "codeforge-update-checker" },
|
|
20521
|
-
timeout: 30000
|
|
20522
|
-
}, (res) => {
|
|
20523
|
-
const status = res.statusCode ?? 0;
|
|
20524
|
-
if (status >= 300 && status < 400 && res.headers.location) {
|
|
20525
|
-
res.resume();
|
|
20526
|
-
if (hopsLeft <= 0) {
|
|
20527
|
-
reject(new Error("too_many_redirects"));
|
|
20528
|
-
return;
|
|
20529
|
-
}
|
|
20530
|
-
const next = new URL(res.headers.location, url2).toString();
|
|
20531
|
-
downloadBinary(next, hopsLeft - 1).then(resolve17, reject);
|
|
20532
|
-
return;
|
|
20533
|
-
}
|
|
20534
|
-
if (status >= 400) {
|
|
20535
|
-
res.resume();
|
|
20536
|
-
reject(new Error(`http_${status}`));
|
|
20537
|
-
return;
|
|
20538
|
-
}
|
|
20539
|
-
const chunks = [];
|
|
20540
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
20541
|
-
res.on("end", () => resolve17(Buffer.concat(chunks)));
|
|
20542
|
-
});
|
|
20543
|
-
req.on("timeout", () => {
|
|
20544
|
-
req.destroy();
|
|
20545
|
-
reject(new Error("timeout"));
|
|
20546
|
-
});
|
|
20547
|
-
req.on("error", reject);
|
|
20548
|
-
req.end();
|
|
20549
|
-
});
|
|
20305
|
+
|
|
20306
|
+
// plugins/update-checker.ts
|
|
20307
|
+
var PLUGIN_NAME20 = "update-checker";
|
|
20308
|
+
var PLUGIN_VERSION = "3.0.0";
|
|
20309
|
+
var _updateCheckStarted = false;
|
|
20310
|
+
function getCacheFile() {
|
|
20311
|
+
return join25(process.env["CODEFORGE_CACHE_DIR"] ?? join25(homedir8(), ".cache", "codeforge"), "update-check.json");
|
|
20550
20312
|
}
|
|
20551
|
-
function
|
|
20552
|
-
const { source, target, oldVersion } = opts;
|
|
20553
|
-
const keep = opts.keepBackups ?? 3;
|
|
20554
|
-
if (!existsSync5(source)) {
|
|
20555
|
-
throw new Error(`atomic_source_missing: ${source}`);
|
|
20556
|
-
}
|
|
20557
|
-
mkdirSync3(dirname14(target), { recursive: true });
|
|
20558
|
-
const newPath = `${target}.new`;
|
|
20559
|
-
const backupPath = `${target}.bak.${oldVersion}`;
|
|
20560
|
-
let strategy = "rename";
|
|
20313
|
+
function readLastInstalledVersion() {
|
|
20561
20314
|
try {
|
|
20562
|
-
|
|
20563
|
-
if (existsSync5(
|
|
20564
|
-
|
|
20565
|
-
|
|
20566
|
-
|
|
20567
|
-
|
|
20568
|
-
|
|
20569
|
-
copyFileSync(target, backupPath);
|
|
20570
|
-
copyFileSync(newPath, target);
|
|
20571
|
-
try {
|
|
20572
|
-
unlinkSync(newPath);
|
|
20573
|
-
} catch {}
|
|
20574
|
-
strategy = "copy_fallback";
|
|
20575
|
-
cleanupOldBackups(target, keep);
|
|
20576
|
-
return { backupPath, strategy };
|
|
20577
|
-
}
|
|
20578
|
-
throw e;
|
|
20579
|
-
}
|
|
20580
|
-
}
|
|
20581
|
-
try {
|
|
20582
|
-
renameSync(newPath, target);
|
|
20583
|
-
} catch (e) {
|
|
20584
|
-
const code = e.code;
|
|
20585
|
-
if (code === "EBUSY" || code === "EPERM" || code === "EACCES") {
|
|
20586
|
-
copyFileSync(newPath, target);
|
|
20587
|
-
try {
|
|
20588
|
-
unlinkSync(newPath);
|
|
20589
|
-
} catch {}
|
|
20590
|
-
strategy = "copy_fallback";
|
|
20591
|
-
} else {
|
|
20592
|
-
throw e;
|
|
20593
|
-
}
|
|
20594
|
-
}
|
|
20595
|
-
cleanupOldBackups(target, keep);
|
|
20596
|
-
return { backupPath, strategy };
|
|
20597
|
-
} catch (e) {
|
|
20598
|
-
try {
|
|
20599
|
-
if (existsSync5(newPath))
|
|
20600
|
-
unlinkSync(newPath);
|
|
20601
|
-
} catch {}
|
|
20602
|
-
throw e;
|
|
20315
|
+
const f = getCacheFile();
|
|
20316
|
+
if (!existsSync5(f))
|
|
20317
|
+
return null;
|
|
20318
|
+
const o = JSON.parse(readFileSync6(f, "utf8"));
|
|
20319
|
+
return typeof o.installedVersion === "string" ? o.installedVersion : null;
|
|
20320
|
+
} catch {
|
|
20321
|
+
return null;
|
|
20603
20322
|
}
|
|
20604
20323
|
}
|
|
20605
|
-
function
|
|
20606
|
-
if (keep <= 0)
|
|
20607
|
-
return;
|
|
20324
|
+
function writeLastInstalledVersion(v) {
|
|
20608
20325
|
try {
|
|
20609
|
-
const
|
|
20610
|
-
|
|
20611
|
-
|
|
20612
|
-
const all = readdirSync3(dir).filter((f) => f.startsWith(prefix)).map((f) => {
|
|
20613
|
-
const full = join24(dir, f);
|
|
20614
|
-
let mtimeMs = 0;
|
|
20615
|
-
try {
|
|
20616
|
-
mtimeMs = statSync4(full).mtimeMs;
|
|
20617
|
-
} catch {}
|
|
20618
|
-
return { full, mtimeMs };
|
|
20619
|
-
}).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
20620
|
-
const toRemove = all.slice(keep);
|
|
20621
|
-
for (const item of toRemove) {
|
|
20622
|
-
try {
|
|
20623
|
-
unlinkSync(item.full);
|
|
20624
|
-
} catch {}
|
|
20625
|
-
}
|
|
20326
|
+
const f = getCacheFile();
|
|
20327
|
+
mkdirSync3(dirname15(f), { recursive: true });
|
|
20328
|
+
writeFileSync2(f, JSON.stringify({ installedVersion: v }, null, 2), "utf8");
|
|
20626
20329
|
} catch {}
|
|
20627
20330
|
}
|
|
20628
|
-
function
|
|
20331
|
+
function resolveNodeBin() {
|
|
20332
|
+
const w = process.platform === "win32";
|
|
20629
20333
|
try {
|
|
20630
|
-
|
|
20631
|
-
if (
|
|
20632
|
-
|
|
20633
|
-
|
|
20634
|
-
|
|
20635
|
-
|
|
20636
|
-
|
|
20637
|
-
|
|
20638
|
-
|
|
20639
|
-
const raw = readFileSync5(file, "utf8");
|
|
20640
|
-
const obj = JSON.parse(raw);
|
|
20641
|
-
if (!obj || typeof obj !== "object")
|
|
20642
|
-
return null;
|
|
20643
|
-
const oc = obj["opencode"];
|
|
20644
|
-
if (!oc || typeof oc !== "object")
|
|
20645
|
-
return null;
|
|
20646
|
-
const o = oc;
|
|
20647
|
-
const min_version = typeof o["min_version"] === "string" ? o["min_version"] : "";
|
|
20648
|
-
const max_version = typeof o["max_version"] === "string" ? o["max_version"] : null;
|
|
20649
|
-
const tested_versions = Array.isArray(o["tested_versions"]) ? o["tested_versions"].filter((v) => typeof v === "string") : [];
|
|
20650
|
-
if (!min_version)
|
|
20651
|
-
return null;
|
|
20652
|
-
return { min_version, max_version, tested_versions };
|
|
20653
|
-
} catch {
|
|
20654
|
-
return null;
|
|
20334
|
+
const r = spawnSync2(w ? "where" : "which", ["node"], { encoding: "utf8", stdio: "pipe" });
|
|
20335
|
+
if (r.status === 0 && r.stdout.trim())
|
|
20336
|
+
return r.stdout.trim().split(/\r?\n/)[0].trim();
|
|
20337
|
+
} catch {}
|
|
20338
|
+
for (const c of w ? ["C:\\Program Files\\nodejs\\node.exe"] : ["/usr/local/bin/node", "/usr/bin/node", "/opt/homebrew/bin/node", "/opt/homebrew/opt/node/bin/node"]) {
|
|
20339
|
+
try {
|
|
20340
|
+
if (spawnSync2(c, ["--version"], { stdio: "pipe", timeout: 2000 }).status === 0)
|
|
20341
|
+
return c;
|
|
20342
|
+
} catch {}
|
|
20655
20343
|
}
|
|
20344
|
+
return process.execPath;
|
|
20656
20345
|
}
|
|
20657
|
-
function
|
|
20346
|
+
function resolveNpmBin() {
|
|
20347
|
+
const w = process.platform === "win32";
|
|
20658
20348
|
try {
|
|
20659
|
-
const
|
|
20660
|
-
|
|
20661
|
-
|
|
20662
|
-
|
|
20349
|
+
const r = spawnSync2(w ? "where" : "which", ["npm"], { encoding: "utf8", stdio: "pipe" });
|
|
20350
|
+
if (r.status === 0 && r.stdout.trim())
|
|
20351
|
+
return r.stdout.trim().split(/\r?\n/)[0].trim();
|
|
20352
|
+
} catch {}
|
|
20353
|
+
for (const c of w ? ["C:\\Program Files\\nodejs\\npm.cmd"] : ["/usr/local/bin/npm", "/usr/bin/npm", "/opt/homebrew/bin/npm"]) {
|
|
20354
|
+
try {
|
|
20355
|
+
if (spawnSync2(c, ["--version"], { stdio: "pipe", timeout: 2000 }).status === 0)
|
|
20356
|
+
return c;
|
|
20357
|
+
} catch {}
|
|
20663
20358
|
}
|
|
20359
|
+
return "npm";
|
|
20664
20360
|
}
|
|
20665
|
-
function
|
|
20666
|
-
|
|
20667
|
-
|
|
20668
|
-
|
|
20669
|
-
|
|
20670
|
-
|
|
20671
|
-
|
|
20672
|
-
|
|
20673
|
-
|
|
20674
|
-
|
|
20675
|
-
|
|
20676
|
-
|
|
20677
|
-
message: `opencode ${cur} 在 CodeForge 已测试版本列表内`
|
|
20678
|
-
};
|
|
20679
|
-
}
|
|
20680
|
-
if (cmpVersion(cur, compat.min_version) < 0) {
|
|
20681
|
-
return {
|
|
20682
|
-
status: "below_min",
|
|
20683
|
-
message: `opencode ${cur} 低于 CodeForge 要求的最低版本 ${compat.min_version}`
|
|
20684
|
-
};
|
|
20685
|
-
}
|
|
20686
|
-
if (compat.max_version && cmpVersion(cur, compat.max_version) > 0) {
|
|
20687
|
-
return {
|
|
20688
|
-
status: "above_max",
|
|
20689
|
-
message: `opencode ${cur} 高于 CodeForge 已知兼容上限 ${compat.max_version}`
|
|
20690
|
-
};
|
|
20691
|
-
}
|
|
20692
|
-
const testedDisplay = compat.tested_versions.length > 0 ? compat.tested_versions.join(", ") : "(空)";
|
|
20693
|
-
return {
|
|
20694
|
-
status: "untested",
|
|
20695
|
-
message: `opencode ${cur} 未在 CodeForge 已测试版本列表中(已测:${testedDisplay})`
|
|
20696
|
-
};
|
|
20361
|
+
function getNpmGlobalRoot(npmBin) {
|
|
20362
|
+
try {
|
|
20363
|
+
const r = spawnSync2(npmBin, ["root", "-g"], {
|
|
20364
|
+
encoding: "utf8",
|
|
20365
|
+
stdio: "pipe",
|
|
20366
|
+
timeout: 1e4,
|
|
20367
|
+
shell: process.platform === "win32"
|
|
20368
|
+
});
|
|
20369
|
+
if (r.status === 0 && r.stdout.trim())
|
|
20370
|
+
return r.stdout.trim();
|
|
20371
|
+
} catch {}
|
|
20372
|
+
return null;
|
|
20697
20373
|
}
|
|
20698
|
-
|
|
20699
|
-
// plugins/update-checker.ts
|
|
20700
|
-
var PLUGIN_NAME20 = "update-checker";
|
|
20701
|
-
var PLUGIN_VERSION = "2.0.0";
|
|
20702
20374
|
logLifecycle(PLUGIN_NAME20, "import", { version: PLUGIN_VERSION });
|
|
20703
20375
|
var updateCheckerServer = async (ctx) => {
|
|
20704
20376
|
const yieldResult = shouldYieldToLocalPlugin({ directory: ctx.directory });
|
|
@@ -20722,45 +20394,20 @@ var updateCheckerServer = async (ctx) => {
|
|
|
20722
20394
|
logLifecycle(PLUGIN_NAME20, "activate", {
|
|
20723
20395
|
version: PLUGIN_VERSION,
|
|
20724
20396
|
auto_check_enabled: u.auto_check_enabled,
|
|
20725
|
-
interval_hours: u.interval_hours,
|
|
20726
20397
|
package: u.package,
|
|
20727
20398
|
registry: u.registry,
|
|
20728
20399
|
channel: u.channel,
|
|
20729
|
-
auto_install: u.auto_install
|
|
20730
|
-
backup_keep: u.backup_keep,
|
|
20731
|
-
repo_fallback: u.repo,
|
|
20732
|
-
config_source: "codeforge.json"
|
|
20733
|
-
});
|
|
20734
|
-
await safeAsync(PLUGIN_NAME20, "opencode_version_check", async () => {
|
|
20735
|
-
const compat = loadCompatibility();
|
|
20736
|
-
const opencodeVer = detectOpencodeVersion();
|
|
20737
|
-
const verdict = compareOpencodeVersion({
|
|
20738
|
-
currentOpencodeVer: opencodeVer,
|
|
20739
|
-
compat
|
|
20740
|
-
});
|
|
20741
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20742
|
-
level: "info",
|
|
20743
|
-
msg: "opencode_version_check",
|
|
20744
|
-
opencodeVer,
|
|
20745
|
-
status: verdict.status,
|
|
20746
|
-
verdict_message: verdict.message
|
|
20747
|
-
});
|
|
20748
|
-
if (verdict.status === "untested" || verdict.status === "above_max") {
|
|
20749
|
-
await postToast(ctx, `[CodeForge] ⚠ ${verdict.message}
|
|
20750
|
-
遇到问题请在 issue 中附带本提示`);
|
|
20751
|
-
} else if (verdict.status === "below_min") {
|
|
20752
|
-
const minVer = compat?.min_version ?? "(unknown)";
|
|
20753
|
-
await postToast(ctx, `[CodeForge] ⚠ ${verdict.message}
|
|
20754
|
-
请升级 opencode 到 ${minVer}+`);
|
|
20755
|
-
}
|
|
20400
|
+
auto_install: u.auto_install
|
|
20756
20401
|
});
|
|
20757
20402
|
if (!u.auto_check_enabled) {
|
|
20758
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20759
|
-
|
|
20760
|
-
|
|
20761
|
-
|
|
20403
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "auto_check_disabled" });
|
|
20404
|
+
return {};
|
|
20405
|
+
}
|
|
20406
|
+
if (_updateCheckStarted) {
|
|
20407
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "check_skipped_already_running" });
|
|
20762
20408
|
return {};
|
|
20763
20409
|
}
|
|
20410
|
+
_updateCheckStarted = true;
|
|
20764
20411
|
setImmediate(() => {
|
|
20765
20412
|
safeAsync(PLUGIN_NAME20, "checkAndMaybeUpdate", async () => {
|
|
20766
20413
|
const local = readLocalVersion();
|
|
@@ -20773,224 +20420,72 @@ var updateCheckerServer = async (ctx) => {
|
|
|
20773
20420
|
timeoutMs: 5000
|
|
20774
20421
|
});
|
|
20775
20422
|
} catch (e) {
|
|
20776
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20777
|
-
level: "warn",
|
|
20778
|
-
msg: "npm_fetch_failed",
|
|
20779
|
-
error: e.message
|
|
20780
|
-
});
|
|
20781
|
-
await fallbackToGitHubReleases(ctx, u);
|
|
20423
|
+
safeWriteLog(PLUGIN_NAME20, { level: "warn", msg: "npm_fetch_failed", error: e.message });
|
|
20782
20424
|
return;
|
|
20783
20425
|
}
|
|
20784
|
-
if (!npmResult)
|
|
20785
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20786
|
-
level: "info",
|
|
20787
|
-
msg: "npm_no_release",
|
|
20788
|
-
package: u.package,
|
|
20789
|
-
channel: u.channel
|
|
20790
|
-
});
|
|
20426
|
+
if (!npmResult)
|
|
20791
20427
|
return;
|
|
20792
|
-
|
|
20793
|
-
const hasUpdate = cmpVersion(local,
|
|
20794
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20795
|
-
level: "info",
|
|
20796
|
-
msg: "npm_check_result",
|
|
20797
|
-
local,
|
|
20798
|
-
remote: npmResult.version,
|
|
20799
|
-
hasUpdate,
|
|
20800
|
-
tarballUrl: npmResult.tarballUrl
|
|
20801
|
-
});
|
|
20428
|
+
const remote = npmResult.version;
|
|
20429
|
+
const hasUpdate = cmpVersion(local, remote) < 0;
|
|
20430
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "npm_check_result", local, remote, hasUpdate });
|
|
20802
20431
|
if (!hasUpdate)
|
|
20803
20432
|
return;
|
|
20433
|
+
const lastInstalled = readLastInstalledVersion();
|
|
20434
|
+
if (lastInstalled === remote) {
|
|
20435
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "install_skipped_already_installed", version: remote });
|
|
20436
|
+
return;
|
|
20437
|
+
}
|
|
20804
20438
|
if (!u.auto_install) {
|
|
20805
|
-
await postToast(ctx, `[
|
|
20806
|
-
|
|
20439
|
+
await postToast(ctx, `[codeforge] 有新版本 ${local} → ${remote}
|
|
20440
|
+
运行:codeforge upgrade`);
|
|
20807
20441
|
return;
|
|
20808
20442
|
}
|
|
20809
|
-
await safeAsync(PLUGIN_NAME20, "
|
|
20810
|
-
const
|
|
20811
|
-
|
|
20812
|
-
|
|
20813
|
-
|
|
20814
|
-
|
|
20815
|
-
|
|
20816
|
-
|
|
20817
|
-
|
|
20818
|
-
|
|
20443
|
+
await safeAsync(PLUGIN_NAME20, "auto_install", async () => {
|
|
20444
|
+
const nodeBin = resolveNodeBin();
|
|
20445
|
+
const npmBin = resolveNpmBin();
|
|
20446
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "auto_install_start", local, remote, nodeBin, npmBin });
|
|
20447
|
+
const r1 = spawnSync2(npmBin, ["install", "-g", `${u.package}@${remote}`], {
|
|
20448
|
+
stdio: "pipe",
|
|
20449
|
+
encoding: "utf8",
|
|
20450
|
+
timeout: 120000,
|
|
20451
|
+
shell: process.platform === "win32"
|
|
20452
|
+
});
|
|
20453
|
+
if (r1.status !== 0) {
|
|
20454
|
+
safeWriteLog(PLUGIN_NAME20, { level: "warn", msg: "npm_install_failed", status: r1.status, stderr: (r1.stderr ?? "").slice(0, 300) });
|
|
20455
|
+
await postToast(ctx, `[codeforge] 自动升级失败(${local} → ${remote}),请手动运行:codeforge upgrade`);
|
|
20456
|
+
return;
|
|
20457
|
+
}
|
|
20458
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "npm_install_success", remote });
|
|
20459
|
+
const npmRoot = getNpmGlobalRoot(npmBin);
|
|
20460
|
+
const codeForgeBin = npmRoot ? join25(npmRoot, "@andyqiu", "codeforge", "bin", "codeforge.mjs") : null;
|
|
20461
|
+
if (!codeForgeBin || !existsSync5(codeForgeBin)) {
|
|
20462
|
+
safeWriteLog(PLUGIN_NAME20, { level: "warn", msg: "codeforge_bin_not_found", path: codeForgeBin ?? "null" });
|
|
20463
|
+
await postToast(ctx, `[codeforge] ⚠ npm 包已升级 ${local} → ${remote},但资产部署未完成。下次启动将重试,或手动运行:codeforge upgrade`);
|
|
20819
20464
|
return;
|
|
20820
20465
|
}
|
|
20821
|
-
const
|
|
20822
|
-
|
|
20823
|
-
|
|
20466
|
+
const r2 = spawnSync2(nodeBin, [codeForgeBin, "install", "--global", "--skip-build"], {
|
|
20467
|
+
stdio: "pipe",
|
|
20468
|
+
encoding: "utf8",
|
|
20469
|
+
timeout: 60000,
|
|
20470
|
+
shell: false
|
|
20824
20471
|
});
|
|
20825
|
-
|
|
20826
|
-
|
|
20827
|
-
|
|
20828
|
-
|
|
20829
|
-
const r = spawnSync2(nodeBin, [installMjs, "--global", "--skip-build"], {
|
|
20830
|
-
cwd: join25(extractDir, "package"),
|
|
20831
|
-
stdio: "pipe",
|
|
20832
|
-
encoding: "utf8"
|
|
20833
|
-
});
|
|
20834
|
-
if (r.status === 0) {
|
|
20835
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20836
|
-
level: "info",
|
|
20837
|
-
msg: "auto_install_full_success",
|
|
20838
|
-
local,
|
|
20839
|
-
remote: npmResult.version,
|
|
20840
|
-
strategy: "install.mjs"
|
|
20841
|
-
});
|
|
20842
|
-
await postToast(ctx, `[CodeForge] ✅ 已全量更新 ${local} → ${npmResult.version}(bundle + agents/skills/commands/workflows,重启 opencode 生效)
|
|
20843
|
-
回滚:npx ${u.package} rollback`);
|
|
20844
|
-
return;
|
|
20845
|
-
}
|
|
20846
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20847
|
-
level: "warn",
|
|
20848
|
-
msg: "install_mjs_failed",
|
|
20849
|
-
nodeBin,
|
|
20850
|
-
status: r.status,
|
|
20851
|
-
stderr: r.stderr?.slice(0, 500),
|
|
20852
|
-
stdout: r.stdout?.slice(0, 500)
|
|
20853
|
-
});
|
|
20854
|
-
} else {
|
|
20855
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20856
|
-
level: "info",
|
|
20857
|
-
msg: "install_mjs_absent_fallback_bundle_only",
|
|
20858
|
-
installMjs
|
|
20859
|
-
});
|
|
20860
|
-
}
|
|
20861
|
-
const { backupPath, strategy } = atomicReplaceBundle({
|
|
20862
|
-
source: bundlePath,
|
|
20863
|
-
target,
|
|
20864
|
-
oldVersion: local,
|
|
20865
|
-
keepBackups: u.backup_keep
|
|
20866
|
-
});
|
|
20867
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20868
|
-
level: "info",
|
|
20869
|
-
msg: "auto_install_bundle_only_success",
|
|
20870
|
-
local,
|
|
20871
|
-
remote: npmResult.version,
|
|
20872
|
-
target,
|
|
20873
|
-
backupPath,
|
|
20874
|
-
strategy
|
|
20875
|
-
});
|
|
20876
|
-
await postToast(ctx, `[CodeForge] ✅ 已更新 bundle ${local} → ${npmResult.version}(仅 bundle,agents/skills 未变;重启生效)
|
|
20877
|
-
回滚:npx ${u.package} rollback`);
|
|
20878
|
-
} finally {
|
|
20879
|
-
try {
|
|
20880
|
-
rmSync(extractDir, { recursive: true, force: true });
|
|
20881
|
-
} catch {}
|
|
20472
|
+
if (r2.status !== 0) {
|
|
20473
|
+
safeWriteLog(PLUGIN_NAME20, { level: "warn", msg: "codeforge_install_failed", status: r2.status, stderr: (r2.stderr ?? "").slice(0, 300) });
|
|
20474
|
+
await postToast(ctx, `[codeforge] ⚠ npm 包已升级 ${local} → ${remote},但资产部署失败。下次启动将重试,或手动运行:codeforge upgrade`);
|
|
20475
|
+
return;
|
|
20882
20476
|
}
|
|
20477
|
+
writeLastInstalledVersion(remote);
|
|
20478
|
+
safeWriteLog(PLUGIN_NAME20, { level: "info", msg: "auto_install_full_success", local, remote, nodeBin, npmBin });
|
|
20479
|
+
await postToast(ctx, `[codeforge] ✅ 已升级 ${local} → ${remote}(重启 opencode 生效)
|
|
20480
|
+
回滚:npm install -g ${u.package}@${local}`);
|
|
20883
20481
|
});
|
|
20884
20482
|
});
|
|
20885
20483
|
});
|
|
20886
20484
|
return {};
|
|
20887
20485
|
};
|
|
20888
|
-
function resolveNodeBin() {
|
|
20889
|
-
const IS_WIN = process.platform === "win32";
|
|
20890
|
-
try {
|
|
20891
|
-
const r = spawnSync2(IS_WIN ? "where" : "which", ["node"], {
|
|
20892
|
-
encoding: "utf8",
|
|
20893
|
-
stdio: "pipe"
|
|
20894
|
-
});
|
|
20895
|
-
if (r.status === 0 && r.stdout.trim()) {
|
|
20896
|
-
const first = r.stdout.trim().split(/\r?\n/)[0].trim();
|
|
20897
|
-
if (first)
|
|
20898
|
-
return first;
|
|
20899
|
-
}
|
|
20900
|
-
} catch {}
|
|
20901
|
-
const candidates = IS_WIN ? [
|
|
20902
|
-
"C:\\Program Files\\nodejs\\node.exe",
|
|
20903
|
-
"C:\\Program Files (x86)\\nodejs\\node.exe"
|
|
20904
|
-
] : [
|
|
20905
|
-
"/usr/local/bin/node",
|
|
20906
|
-
"/usr/bin/node",
|
|
20907
|
-
"/opt/homebrew/bin/node",
|
|
20908
|
-
"/opt/homebrew/opt/node/bin/node"
|
|
20909
|
-
];
|
|
20910
|
-
for (const c of candidates) {
|
|
20911
|
-
try {
|
|
20912
|
-
const t = spawnSync2(c, ["--version"], {
|
|
20913
|
-
encoding: "utf8",
|
|
20914
|
-
stdio: "pipe",
|
|
20915
|
-
timeout: 2000
|
|
20916
|
-
});
|
|
20917
|
-
if (t.status === 0)
|
|
20918
|
-
return c;
|
|
20919
|
-
} catch {}
|
|
20920
|
-
}
|
|
20921
|
-
return process.execPath;
|
|
20922
|
-
}
|
|
20923
|
-
function detectOpencodeVersion() {
|
|
20924
|
-
const env = process.env["OPENCODE_VERSION"];
|
|
20925
|
-
if (env && env.trim().length > 0)
|
|
20926
|
-
return env.trim();
|
|
20927
|
-
return "unknown";
|
|
20928
|
-
}
|
|
20929
|
-
function getOpencodeBundlePath() {
|
|
20930
|
-
const candidates = [];
|
|
20931
|
-
candidates.push(join25(homedir9(), ".config", "opencode", "codeforge", "index.js"));
|
|
20932
|
-
if (process.platform === "win32") {
|
|
20933
|
-
const appData = process.env["APPDATA"];
|
|
20934
|
-
if (appData)
|
|
20935
|
-
candidates.push(join25(appData, "opencode", "codeforge", "index.js"));
|
|
20936
|
-
const localAppData = process.env["LOCALAPPDATA"];
|
|
20937
|
-
if (localAppData)
|
|
20938
|
-
candidates.push(join25(localAppData, "opencode", "codeforge", "index.js"));
|
|
20939
|
-
}
|
|
20940
|
-
for (const c of candidates) {
|
|
20941
|
-
if (existsSync6(c))
|
|
20942
|
-
return c;
|
|
20943
|
-
}
|
|
20944
|
-
return candidates[0] ?? null;
|
|
20945
|
-
}
|
|
20946
|
-
async function fallbackToGitHubReleases(ctx, u) {
|
|
20947
|
-
await safeAsync(PLUGIN_NAME20, "github_fallback", async () => {
|
|
20948
|
-
const result = await checkUpdateOnce({
|
|
20949
|
-
repo: u.repo,
|
|
20950
|
-
intervalMs: u.interval_hours * 3600 * 1000
|
|
20951
|
-
});
|
|
20952
|
-
await reportLegacyResult(ctx, result, u.repo);
|
|
20953
|
-
});
|
|
20954
|
-
}
|
|
20955
|
-
async function reportLegacyResult(ctx, result, repo) {
|
|
20956
|
-
if (result.error && !result.fromCache) {
|
|
20957
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20958
|
-
level: "warn",
|
|
20959
|
-
msg: `update check failed: ${result.error}`,
|
|
20960
|
-
...result
|
|
20961
|
-
});
|
|
20962
|
-
return;
|
|
20963
|
-
}
|
|
20964
|
-
if (!result.hasUpdate) {
|
|
20965
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20966
|
-
level: "info",
|
|
20967
|
-
msg: `up-to-date (local=${result.local}, remote=${result.remote}${result.fromCache ? ", from_cache" : ""})`,
|
|
20968
|
-
...result
|
|
20969
|
-
});
|
|
20970
|
-
return;
|
|
20971
|
-
}
|
|
20972
|
-
const updateCmd = `bunx --bun github:${repo} install`;
|
|
20973
|
-
const toast = `[CodeForge] 有新版本:${result.local} → ${result.remote}
|
|
20974
|
-
更新命令:${updateCmd}`;
|
|
20975
|
-
safeWriteLog(PLUGIN_NAME20, {
|
|
20976
|
-
level: "info",
|
|
20977
|
-
msg: "new_version_available_github_fallback",
|
|
20978
|
-
local: result.local,
|
|
20979
|
-
remote: result.remote,
|
|
20980
|
-
update_command: updateCmd,
|
|
20981
|
-
fromCache: result.fromCache
|
|
20982
|
-
});
|
|
20983
|
-
await postToast(ctx, toast);
|
|
20984
|
-
}
|
|
20985
20486
|
async function postToast(ctx, message) {
|
|
20986
20487
|
await safeAsync(PLUGIN_NAME20, "client.app.log", async () => {
|
|
20987
|
-
await ctx.client.app.log({
|
|
20988
|
-
body: {
|
|
20989
|
-
service: PLUGIN_NAME20,
|
|
20990
|
-
level: "info",
|
|
20991
|
-
message
|
|
20992
|
-
}
|
|
20993
|
-
});
|
|
20488
|
+
await ctx.client.app.log({ body: { service: PLUGIN_NAME20, level: "info", message } });
|
|
20994
20489
|
});
|
|
20995
20490
|
}
|
|
20996
20491
|
var handler20 = updateCheckerServer;
|
package/install.mjs
CHANGED
|
@@ -103,7 +103,6 @@ function parseArgs(argv) {
|
|
|
103
103
|
action: "install",
|
|
104
104
|
dryRun: false,
|
|
105
105
|
skipBuild: false,
|
|
106
|
-
enableLegacyTools: false,
|
|
107
106
|
verbose: false,
|
|
108
107
|
help: false,
|
|
109
108
|
global: false,
|
|
@@ -116,22 +115,11 @@ function parseArgs(argv) {
|
|
|
116
115
|
case "--uninstall": out.action = "uninstall"; break
|
|
117
116
|
case "--dry-run": out.dryRun = true; break
|
|
118
117
|
case "--skip-build": out.skipBuild = true; break
|
|
119
|
-
case "--enable-legacy-tools": out.enableLegacyTools = true; break
|
|
120
118
|
case "--verbose": out.verbose = true; break
|
|
121
119
|
case "-h":
|
|
122
120
|
case "--help": out.help = true; break
|
|
123
121
|
default:
|
|
124
|
-
//
|
|
125
|
-
if (a.startsWith("-")) {
|
|
126
|
-
// 兼容 -Global/-DryRun/-Uninstall/-SkipBuild/-EnableLegacyTools
|
|
127
|
-
const low = a.toLowerCase()
|
|
128
|
-
if (low === "-global") out.mode = "global"
|
|
129
|
-
else if (low === "-uninstall") out.action = "uninstall"
|
|
130
|
-
else if (low === "-dryrun") out.dryRun = true
|
|
131
|
-
else if (low === "-skipbuild") out.skipBuild = true
|
|
132
|
-
else if (low === "-enablelegacytools") out.enableLegacyTools = true
|
|
133
|
-
else if (low === "-verbose") out.verbose = true
|
|
134
|
-
}
|
|
122
|
+
// 未知参数静默忽略
|
|
135
123
|
break
|
|
136
124
|
}
|
|
137
125
|
}
|
|
@@ -172,7 +160,7 @@ function resolvePaths({ mode }) {
|
|
|
172
160
|
// v0.1 之前装的目录(卸载时一并清掉)
|
|
173
161
|
const LEGACY_DIRS = ["agent", "command", "tool", "tools", "plugin", "plugins", "lib"]
|
|
174
162
|
// v0.1+ 才有的目录
|
|
175
|
-
const MANAGED_DIRS = ["codeforge", "agents", "commands", "workflows", "
|
|
163
|
+
const MANAGED_DIRS = ["codeforge", "agents", "commands", "workflows", "review-profiles", "agent-templates"]
|
|
176
164
|
// 细粒度卸载:只删 CodeForge 自己的 skill
|
|
177
165
|
const OWNED_SKILLS = ["ambiguity-gate", "devils-advocate", "ears-zh", "example-mapping", "success-criteria", "weighted-dimensions"]
|
|
178
166
|
|
|
@@ -188,7 +176,6 @@ const MD_MANIFEST_REL = "codeforge/installed-md-manifest.json"
|
|
|
188
176
|
// 整目录拷贝
|
|
189
177
|
const COPY_DIRS = [
|
|
190
178
|
["workflows", "workflows"],
|
|
191
|
-
["context-templates", "context-templates"],
|
|
192
179
|
["review-profiles", "review-profiles"],
|
|
193
180
|
["agent-templates", "agent-templates"],
|
|
194
181
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andyqiu/codeforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "CodeForge — opencode 的零侵入扩展包",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -48,16 +48,7 @@
|
|
|
48
48
|
"bench:json": "node --experimental-strip-types --no-warnings ./scripts/bench-repo-map.mjs --json",
|
|
49
49
|
"lint": "tsc --noEmit",
|
|
50
50
|
"check:bun": "node ./scripts/check-bun.mjs",
|
|
51
|
-
"install:local": "node ./install.mjs",
|
|
52
|
-
"install:global": "node ./install.mjs --global",
|
|
53
|
-
"uninstall:local": "node ./install.mjs --uninstall",
|
|
54
|
-
"phase0:check": "node ./scripts/phase0-check.mjs",
|
|
55
51
|
"phase1:check": "node ./scripts/check-dist-built.mjs && node ./scripts/check-layer-deps.mjs && node ./scripts/phase1-check.mjs",
|
|
56
|
-
"phase15:check": "node ./scripts/phase15-check.mjs",
|
|
57
|
-
"v13:check": "node ./scripts/v13-check.mjs",
|
|
58
|
-
"phase2:check": "node ./scripts/phase2-check.mjs",
|
|
59
|
-
"phase3:check": "node ./scripts/phase3-check.mjs",
|
|
60
|
-
"phase4:check": "node ./scripts/phase4-check.mjs",
|
|
61
52
|
"adr-check": "node ./scripts/adr-check.mjs",
|
|
62
53
|
"adr-check:strict": "cross-env ADR_STRICT=1 node ./scripts/adr-check.mjs",
|
|
63
54
|
"adr-index-sync": "node ./scripts/adr-index-sync.mjs",
|
|
@@ -65,7 +56,6 @@
|
|
|
65
56
|
"adr-graph": "node ./scripts/adr-graph-gen.mjs",
|
|
66
57
|
"adr-graph-check": "node ./scripts/adr-graph-gen.mjs --check",
|
|
67
58
|
"adr-init": "node ./bin/codeforge.mjs adr-init",
|
|
68
|
-
"phases:all": "npm run phase0:check && npm run phase1:check && npm run phase15:check && npm run phase2:check && npm run phase3:check && npm run phase4:check",
|
|
69
59
|
"sync:models": "node ./scripts/sync-agent-models.mjs",
|
|
70
60
|
"sync:models:check": "node ./scripts/sync-agent-models.mjs --check",
|
|
71
61
|
"sync": "node ./scripts/sync-agent-models.mjs && node ./scripts/dev-sync.mjs --sync-only",
|
|
@@ -115,7 +105,6 @@
|
|
|
115
105
|
"bin/",
|
|
116
106
|
"commands/",
|
|
117
107
|
"workflows/",
|
|
118
|
-
"context-templates/",
|
|
119
108
|
"skills/",
|
|
120
109
|
"review-profiles/",
|
|
121
110
|
"scripts/check-bun.mjs",
|