@andyqiu/codeforge 0.6.3 → 0.6.5
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/codeforge.json +1 -1
- package/dist/index.js +60 -20
- package/install.mjs +96 -12
- package/package.json +1 -1
package/codeforge.json
CHANGED
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
"update": {
|
|
155
155
|
"_doc": "自动更新检查(ADR-0047)。opencode 启动时由 plugins/update-checker.ts 后台拉 npm registry latest,auto_install=true 时自动替换 ~/.config/opencode/codeforge/index.js(下次启动生效),失败/离线静默回退到 GitHub Releases。手动回滚:codeforge rollback。",
|
|
156
156
|
"auto_check_enabled": true,
|
|
157
|
-
"interval_hours":
|
|
157
|
+
"interval_hours": 6,
|
|
158
158
|
"package": "@andyqiu/codeforge",
|
|
159
159
|
"registry": "https://registry.npmjs.org",
|
|
160
160
|
"channel": "latest",
|
package/dist/index.js
CHANGED
|
@@ -19732,9 +19732,10 @@ var toolPolicyServer = async (ctx) => {
|
|
|
19732
19732
|
var handler19 = toolPolicyServer;
|
|
19733
19733
|
|
|
19734
19734
|
// plugins/update-checker.ts
|
|
19735
|
-
import { existsSync as existsSync6 } from "node:fs";
|
|
19735
|
+
import { existsSync as existsSync6, rmSync } from "node:fs";
|
|
19736
19736
|
import { homedir as homedir8 } from "node:os";
|
|
19737
19737
|
import { join as join23 } from "node:path";
|
|
19738
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
19738
19739
|
|
|
19739
19740
|
// lib/update-checker-impl.ts
|
|
19740
19741
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -19759,7 +19760,7 @@ import * as zlib from "node:zlib";
|
|
|
19759
19760
|
// lib/version-injected.ts
|
|
19760
19761
|
function getInjectedVersion() {
|
|
19761
19762
|
try {
|
|
19762
|
-
const v = "0.6.
|
|
19763
|
+
const v = "0.6.5";
|
|
19763
19764
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
19764
19765
|
return v;
|
|
19765
19766
|
}
|
|
@@ -20371,7 +20372,7 @@ var updateCheckerServer = async (ctx) => {
|
|
|
20371
20372
|
更新命令:npx ${u.package} install --global`);
|
|
20372
20373
|
return;
|
|
20373
20374
|
}
|
|
20374
|
-
await safeAsync(PLUGIN_NAME20, "
|
|
20375
|
+
await safeAsync(PLUGIN_NAME20, "auto_install_full", async () => {
|
|
20375
20376
|
const target = getOpencodeBundlePath();
|
|
20376
20377
|
if (!target) {
|
|
20377
20378
|
safeWriteLog(PLUGIN_NAME20, {
|
|
@@ -20383,27 +20384,66 @@ var updateCheckerServer = async (ctx) => {
|
|
|
20383
20384
|
自动安装失败:找不到 bundle,请手动 npx ${u.package} install`);
|
|
20384
20385
|
return;
|
|
20385
20386
|
}
|
|
20386
|
-
const { bundlePath } = await downloadAndExtractBundle({
|
|
20387
|
+
const { bundlePath, extractDir } = await downloadAndExtractBundle({
|
|
20387
20388
|
tarballUrl: npmResult.tarballUrl,
|
|
20388
20389
|
expectedIntegrity: npmResult.integrity
|
|
20389
20390
|
});
|
|
20390
|
-
|
|
20391
|
-
|
|
20392
|
-
|
|
20393
|
-
|
|
20394
|
-
|
|
20395
|
-
|
|
20396
|
-
|
|
20397
|
-
|
|
20398
|
-
|
|
20399
|
-
|
|
20400
|
-
|
|
20401
|
-
|
|
20402
|
-
|
|
20403
|
-
|
|
20404
|
-
|
|
20405
|
-
|
|
20391
|
+
try {
|
|
20392
|
+
const installMjs = join23(extractDir, "package", "install.mjs");
|
|
20393
|
+
if (existsSync6(installMjs)) {
|
|
20394
|
+
const r = spawnSync2(process.execPath, [installMjs, "--global", "--skip-build"], {
|
|
20395
|
+
cwd: join23(extractDir, "package"),
|
|
20396
|
+
stdio: "pipe",
|
|
20397
|
+
encoding: "utf8"
|
|
20398
|
+
});
|
|
20399
|
+
if (r.status === 0) {
|
|
20400
|
+
safeWriteLog(PLUGIN_NAME20, {
|
|
20401
|
+
level: "info",
|
|
20402
|
+
msg: "auto_install_full_success",
|
|
20403
|
+
local,
|
|
20404
|
+
remote: npmResult.version,
|
|
20405
|
+
strategy: "install.mjs"
|
|
20406
|
+
});
|
|
20407
|
+
await postToast(ctx, `[CodeForge] ✅ 已全量更新 ${local} → ${npmResult.version}(bundle + agents/skills/commands/workflows,重启 opencode 生效)
|
|
20408
|
+
回滚:npx ${u.package} rollback`);
|
|
20409
|
+
return;
|
|
20410
|
+
}
|
|
20411
|
+
safeWriteLog(PLUGIN_NAME20, {
|
|
20412
|
+
level: "warn",
|
|
20413
|
+
msg: "install_mjs_failed",
|
|
20414
|
+
status: r.status,
|
|
20415
|
+
stderr: r.stderr?.slice(0, 500),
|
|
20416
|
+
stdout: r.stdout?.slice(0, 500)
|
|
20417
|
+
});
|
|
20418
|
+
} else {
|
|
20419
|
+
safeWriteLog(PLUGIN_NAME20, {
|
|
20420
|
+
level: "info",
|
|
20421
|
+
msg: "install_mjs_absent_fallback_bundle_only",
|
|
20422
|
+
installMjs
|
|
20423
|
+
});
|
|
20424
|
+
}
|
|
20425
|
+
const { backupPath, strategy } = atomicReplaceBundle({
|
|
20426
|
+
source: bundlePath,
|
|
20427
|
+
target,
|
|
20428
|
+
oldVersion: local,
|
|
20429
|
+
keepBackups: u.backup_keep
|
|
20430
|
+
});
|
|
20431
|
+
safeWriteLog(PLUGIN_NAME20, {
|
|
20432
|
+
level: "info",
|
|
20433
|
+
msg: "auto_install_bundle_only_success",
|
|
20434
|
+
local,
|
|
20435
|
+
remote: npmResult.version,
|
|
20436
|
+
target,
|
|
20437
|
+
backupPath,
|
|
20438
|
+
strategy
|
|
20439
|
+
});
|
|
20440
|
+
await postToast(ctx, `[CodeForge] ✅ 已更新 bundle ${local} → ${npmResult.version}(仅 bundle,agents/skills 未变;重启生效)
|
|
20406
20441
|
回滚:npx ${u.package} rollback`);
|
|
20442
|
+
} finally {
|
|
20443
|
+
try {
|
|
20444
|
+
rmSync(extractDir, { recursive: true, force: true });
|
|
20445
|
+
} catch {}
|
|
20446
|
+
}
|
|
20407
20447
|
});
|
|
20408
20448
|
});
|
|
20409
20449
|
});
|
package/install.mjs
CHANGED
|
@@ -176,8 +176,15 @@ const MANAGED_DIRS = ["codeforge", "agents", "commands", "workflows", "context-t
|
|
|
176
176
|
// 细粒度卸载:只删 CodeForge 自己的 skill
|
|
177
177
|
const OWNED_SKILLS = ["ambiguity-gate", "devils-advocate", "ears-zh", "example-mapping", "success-criteria", "weighted-dimensions"]
|
|
178
178
|
|
|
179
|
-
// 文件级 copy(.md,排除 README/_*/.bak
|
|
179
|
+
// 文件级 copy(.md,排除 README/_*/.bak/隐藏)— per-file upsert + manifest 差集清理,不 rmrf 整目录
|
|
180
|
+
// ADR:install-whitelist-md-files
|
|
180
181
|
const MD_COPY_DIRS = [["agents", "agents"], ["commands", "commands"]]
|
|
182
|
+
|
|
183
|
+
// codeforge 自维护的 .md 安装清单:记录"上一版 codeforge 在各目标目录装了哪些 .md"。
|
|
184
|
+
// 用途:install 时算差集(manifest − 本次源)精准清理本版已删除的旧 codeforge 文件,
|
|
185
|
+
// 第三方 .md(如 teamkit-git.md)从不写入 manifest → 永不被清理(零副作用)。
|
|
186
|
+
// 位置在 codeforge/ 子目录下,uninstall 的 rmrf(codeforge/) 会自然清掉,无需额外卸载逻辑。
|
|
187
|
+
const MD_MANIFEST_REL = "codeforge/installed-md-manifest.json"
|
|
181
188
|
// 整目录拷贝
|
|
182
189
|
const COPY_DIRS = [
|
|
183
190
|
["workflows", "workflows"],
|
|
@@ -450,31 +457,57 @@ function installBundle({ targetRoot, bundleSrc }) {
|
|
|
450
457
|
vok(`VERSION → ${versionFile} (${version})`)
|
|
451
458
|
}
|
|
452
459
|
|
|
460
|
+
// ADR:install-whitelist-md-files —— manifest 自维护:不再 rmrf 整目录,
|
|
461
|
+
// 读 manifest 算差集精准清理旧 codeforge .md,第三方 .md 零副作用。
|
|
453
462
|
function installMdDirs({ targetRoot }) {
|
|
463
|
+
// 步骤 1:读上一版 manifest(不存在 → 各目录空列表 = 首次安装)
|
|
464
|
+
const prevManifest = readMdManifest(targetRoot)
|
|
465
|
+
const nextManifest = {}
|
|
466
|
+
|
|
454
467
|
for (const [srcName, dstName] of MD_COPY_DIRS) {
|
|
455
468
|
const srcPath = path.join(SOURCE_ROOT, srcName)
|
|
456
469
|
const dstPath = path.join(targetRoot, dstName)
|
|
457
470
|
if (!fs.existsSync(srcPath) || !fs.statSync(srcPath).isDirectory()) {
|
|
458
471
|
vlog(`源目录不存在,跳过: ${srcPath}`)
|
|
472
|
+
// 源目录缺失:本版该目录装 0 个文件,manifest 记空(差集会清掉上一版全部该目录文件)
|
|
473
|
+
nextManifest[dstName] = []
|
|
474
|
+
const prevList = prevManifest[dstName] ?? []
|
|
475
|
+
pruneStale({ dstPath, prevList, sourceSet: new Set() })
|
|
459
476
|
continue
|
|
460
477
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
|
|
478
|
+
ensureDir(dstPath) // 不再 rmrf;幂等建目录
|
|
479
|
+
|
|
480
|
+
// 步骤 2:扫源目录 → sourceSet(本版要装的文件名集合)
|
|
481
|
+
const sourceFiles = []
|
|
482
|
+
const srcEntries = fs.readdirSync(srcPath)
|
|
483
|
+
for (const base of srcEntries) {
|
|
466
484
|
if (!shouldCopyMd(base)) continue
|
|
485
|
+
try { if (!fs.statSync(path.join(srcPath, base)).isFile()) continue } catch { continue }
|
|
486
|
+
sourceFiles.push(base)
|
|
487
|
+
}
|
|
488
|
+
const sourceSet = new Set(sourceFiles)
|
|
489
|
+
|
|
490
|
+
// 步骤 3:差集清理 prevManifest[dir] − sourceSet(仅删确实存在的)
|
|
491
|
+
const prevList = prevManifest[dstName] ?? []
|
|
492
|
+
const pruned = pruneStale({ dstPath, prevList, sourceSet })
|
|
493
|
+
|
|
494
|
+
// 步骤 4:upsert —— 把 sourceSet 文件复制到目标(覆盖同名,不动其他文件)
|
|
495
|
+
let count = 0
|
|
496
|
+
for (const base of sourceFiles) {
|
|
467
497
|
const f = path.join(srcPath, base)
|
|
468
|
-
try {
|
|
469
|
-
if (!fs.statSync(f).isFile()) continue
|
|
470
|
-
} catch {
|
|
471
|
-
continue
|
|
472
|
-
}
|
|
473
498
|
run(`cp ${f} ${dstPath}/`, () => fs.copyFileSync(f, path.join(dstPath, base)))
|
|
474
499
|
count++
|
|
475
500
|
}
|
|
476
|
-
|
|
501
|
+
|
|
502
|
+
// 步骤 5:新 manifest = sourceSet(本版实际安装的文件名)
|
|
503
|
+
nextManifest[dstName] = [...sourceFiles]
|
|
504
|
+
|
|
505
|
+
vok(`${srcName}/ → ${dstPath} (${count} 个 .md upsert, ${pruned} 个过期清理, 第三方文件保留)`)
|
|
477
506
|
}
|
|
507
|
+
|
|
508
|
+
// 步骤 5(落盘):写新 manifest
|
|
509
|
+
ensureDir(path.join(targetRoot, "codeforge")) // 幂等,保证父目录存在
|
|
510
|
+
run(`write md manifest → ${MD_MANIFEST_REL}`, () => writeMdManifest(targetRoot, nextManifest))
|
|
478
511
|
}
|
|
479
512
|
|
|
480
513
|
function safeReaddir(p) {
|
|
@@ -485,6 +518,54 @@ function safeReaddir(p) {
|
|
|
485
518
|
}
|
|
486
519
|
}
|
|
487
520
|
|
|
521
|
+
// 读 manifest:不存在 / 解析失败 → 返回各目录空列表(首次安装走此分支)。
|
|
522
|
+
// 返回结构:{ [dstName]: string[] },含 MD_COPY_DIRS 的所有 dstName 键。
|
|
523
|
+
// 损坏 JSON 保守降级为"首次安装"(差集空 → 不误删)。
|
|
524
|
+
function readMdManifest(targetRoot) {
|
|
525
|
+
const empty = {}
|
|
526
|
+
for (const [, dstName] of MD_COPY_DIRS) empty[dstName] = []
|
|
527
|
+
const p = path.join(targetRoot, MD_MANIFEST_REL)
|
|
528
|
+
let raw
|
|
529
|
+
try {
|
|
530
|
+
raw = fs.readFileSync(p, "utf8")
|
|
531
|
+
} catch {
|
|
532
|
+
return empty // 文件不存在 → 首次安装
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
const parsed = JSON.parse(raw)
|
|
536
|
+
const out = {}
|
|
537
|
+
for (const [, dstName] of MD_COPY_DIRS) {
|
|
538
|
+
const v = parsed?.[dstName]
|
|
539
|
+
out[dstName] = Array.isArray(v) ? v.filter((x) => typeof x === "string") : []
|
|
540
|
+
}
|
|
541
|
+
return out
|
|
542
|
+
} catch {
|
|
543
|
+
return empty // 损坏的 manifest → 视为首次安装(保守:差集为空,不误删)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// 写 manifest:atomicWriteJson(同目录 tmp + rename)。data 形如 { agents: [...], commands: [...] }。
|
|
548
|
+
// 调用方需保证 codeforge/ 目录已存在(installBundle 先于 installMdDirs 跑,已建 codeforge/)。
|
|
549
|
+
function writeMdManifest(targetRoot, data) {
|
|
550
|
+
const p = path.join(targetRoot, MD_MANIFEST_REL)
|
|
551
|
+
atomicWriteJson(p, data)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// 差集清理:prevList − sourceSet,仅删目标目录里确实存在的文件。返回清理计数。
|
|
555
|
+
// 第三方文件(如 teamkit-git.md)从不写入 manifest → 不在 prevList → 永不被删。
|
|
556
|
+
function pruneStale({ dstPath, prevList, sourceSet }) {
|
|
557
|
+
let pruned = 0
|
|
558
|
+
const actual = new Set(safeReaddir(dstPath))
|
|
559
|
+
for (const base of prevList) {
|
|
560
|
+
if (sourceSet.has(base)) continue // 本版仍发布 → 保留
|
|
561
|
+
if (!actual.has(base)) continue // 目标里已不存在 → 无需删
|
|
562
|
+
const stale = path.join(dstPath, base)
|
|
563
|
+
run(`rm ${stale}(过期 codeforge .md)`, () => fs.rmSync(stale, { force: true }))
|
|
564
|
+
pruned++
|
|
565
|
+
}
|
|
566
|
+
return pruned
|
|
567
|
+
}
|
|
568
|
+
|
|
488
569
|
function installCopyDirs({ targetRoot }) {
|
|
489
570
|
for (const [srcName, dstName] of COPY_DIRS) {
|
|
490
571
|
const srcPath = path.join(SOURCE_ROOT, srcName)
|
|
@@ -742,6 +823,9 @@ export {
|
|
|
742
823
|
LEGACY_DIRS,
|
|
743
824
|
MANAGED_DIRS,
|
|
744
825
|
OWNED_SKILLS,
|
|
826
|
+
readMdManifest,
|
|
827
|
+
writeMdManifest,
|
|
828
|
+
MD_MANIFEST_REL,
|
|
745
829
|
}
|
|
746
830
|
|
|
747
831
|
// main 守卫:仅作为脚本直接运行时执行
|