@andyqiu/codeforge 0.5.0 → 0.5.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.
- package/agents/codeforge.md +3 -2
- package/commands/diff.md +62 -0
- package/dist/index.js +67 -3
- package/install.ps1 +556 -556
- package/install.sh +0 -0
- package/package.json +1 -1
package/agents/codeforge.md
CHANGED
|
@@ -66,12 +66,13 @@ fallback_models:
|
|
|
66
66
|
| 场景 | 该做什么 | MUST NOT |
|
|
67
67
|
|---|---|---|
|
|
68
68
|
| 用户问简单问题 / 寻求解释 / 对比方案讨论(≤ 800 字能答完) | **自己直接答**,不派任何 agent | ❌ 派 planner 或 coder |
|
|
69
|
-
| **小改动 short-circuit**:用户指明确切文件位置 + 改动 ≤ 1 文件 + 估算 < 5 行 + 用户已给出修改思路 | 跳过 planner,**直接派 coder**,prompt 自包含改动需求(无 plan_id
|
|
69
|
+
| **小改动 short-circuit**:用户指明确切文件位置 + 改动 ≤ 1 文件 + 估算 < 5 行 + 用户已给出修改思路 | 跳过 planner,**直接派 coder**,prompt 自包含改动需求(无 plan_id 路径);coder 完成后同样走「coder 回报正常完成」行的自动 reviewer 流程 | ❌ 派 planner 再让它派 coder |
|
|
70
70
|
| 复杂多步任务(含设计 / 涉及多文件 / 不确定改哪 / 需要查历史经验) | **派 planner 出方案** → 等 planner 回 boomerang(含 `plan_id: plan-xxx`)→ **自动派 reviewer 审方案**(`review_target=plan_only` + plan_id):APPROVE → 派 coder 执行(带 plan_id + sessionId);REQUEST_CHANGES (`plan_review_loop_count` < 3) → 自动派 planner 改方案,loop +1;REQUEST_CHANGES (loop = 3) → 转告用户三选一;BLOCK → 转告用户 + 建议派 planner 重设计 | ❌ 派完 planner 直接派 coder;❌ 绕过方案 BLOCK |
|
|
71
71
|
| **决策 review(Q3-a 范围)** | 先派 reviewer 审决策合理性(`review_target=decision_only`):APPROVE → 按决策派;REQUEST_CHANGES → reviewer 给替代方案,loop 1 次后转告用户;BLOCK → 转告用户 | ❌ 审用户业务选择本身;❌ 把所有 question 都套 review |
|
|
72
72
|
| 用户要"独立交付物" | 派 coder 子 session 写并直接落到 session worktree;prompt 明示"final response 不要粘回长内容" | ❌ 自己在父对话吐长文档 |
|
|
73
73
|
| 用户要查项目结构 / 历史经验 | 自己调 `smart_search` / `repo_map` / `read` / `plan_read` | ❌ 为此派 subagent |
|
|
74
|
-
| **
|
|
74
|
+
| **coder 回报正常完成** | 自动派 reviewer 审代码改动(`review_target` 按文件类型自动检测);后续按下方「reviewer 报三档」独立行处理 | ❌ 跳过 reviewer 直接问用户是否 merge;❌ 重新审查代码 |
|
|
75
|
+
| **其他 subagent 回报正常完成**(planner / reviewer 等) | 按上下文决定下一棒:派 reviewer / 派下个 phase / 收尾 | ❌ 默认 subagent 会自派下一棒 |
|
|
75
76
|
| **subagent 报错 / 中断 / 摘要为空** | **立刻停下**,把错误首行原文 + 子 session id 转告用户,问「重试 / 改方案 / 跳过」三选一 | ❌ 盲目"再派一次试试" |
|
|
76
77
|
| **subagent 长时间无回报** | 提醒用户「子 session 仍在跑,按 `Ctrl+→` 可切过去看进度」 | ❌ 主动 Esc 取消;❌ 重派 |
|
|
77
78
|
| **reviewer 报 REQUEST_CHANGES(代码 review,`code_review_loop_count` < 3)** | **自动派 coder 修**(带具体到行的 reviewer 意见 + 原 plan_id + sessionId),loop +1 | ❌ 同时派多个 coder;❌ 不带 reviewer 意见 |
|
package/commands/diff.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 查看当前 session worktree 的改动(文件列表+统计,或完整 diff)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<!--
|
|
6
|
+
codeforge 元数据(opencode 不读,由 plugins / workflow-engine 解析):
|
|
7
|
+
name: diff
|
|
8
|
+
version: 1.0.0
|
|
9
|
+
requires_human_approval: false
|
|
10
|
+
说明:底层调用 session_merge 工具的 action=diff
|
|
11
|
+
默认显示改动文件列表 + 统计(stat=true)
|
|
12
|
+
--full 显示完整 diff 内容(stat=false)
|
|
13
|
+
没有 worktree 时友好提示
|
|
14
|
+
-->
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# /diff — 查看当前 session worktree 改动
|
|
18
|
+
|
|
19
|
+
**ADR: worktree-session-isolation Phase 3**
|
|
20
|
+
|
|
21
|
+
快速查看当前 session worktree 中的代码改动,无需触发 merge 或 review 流程。
|
|
22
|
+
|
|
23
|
+
## 输入
|
|
24
|
+
|
|
25
|
+
用户参数:$ARGUMENTS
|
|
26
|
+
|
|
27
|
+
## 用法
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
/diff # 显示改动文件列表 + 统计(git diff --stat 风格)
|
|
31
|
+
/diff --full # 显示完整 diff 内容
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 行为说明
|
|
35
|
+
|
|
36
|
+
| 模式 | 触发条件 | 输出内容 |
|
|
37
|
+
|------|----------|----------|
|
|
38
|
+
| **stat 模式**(默认) | `/diff` | 改动文件列表 + 增删行数统计 |
|
|
39
|
+
| **full 模式** | `/diff --full` | 完整 diff 内容(含每行改动) |
|
|
40
|
+
| **无 worktree** | session 无写操作 | 友好提示:当前 session 没有写操作,无 worktree 改动 |
|
|
41
|
+
|
|
42
|
+
## 底层调用
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
/diff → session_merge(action="diff", stat=true)
|
|
46
|
+
/diff --full → session_merge(action="diff", stat=false)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 何时使用
|
|
50
|
+
|
|
51
|
+
- 在 `/merge` 之前确认改动范围
|
|
52
|
+
- 向用户或 reviewer 展示本次 session 做了什么
|
|
53
|
+
- 调试时快速确认某文件是否已被修改
|
|
54
|
+
|
|
55
|
+
## 与其他命令的关系
|
|
56
|
+
|
|
57
|
+
| 命令 | 用途 |
|
|
58
|
+
|------|------|
|
|
59
|
+
| `/diff` | 本命令:只读查看 worktree 改动,不触发任何合并 |
|
|
60
|
+
| `/merge` | 触发 review-fix-review 审批闭环后 squash merge 入主仓 |
|
|
61
|
+
| `/discard-session` | 放弃当前 session 所有改动(不可恢复) |
|
|
62
|
+
| `session_merge action=status` | 查看 worktree 绑定状态(非 diff 内容) |
|
package/dist/index.js
CHANGED
|
@@ -13763,6 +13763,7 @@ var description26 = [
|
|
|
13763
13763
|
"- action=status:查询当前 session worktree 状态(任意 agent 可调)",
|
|
13764
13764
|
"- action=merge:codeforge orchestrator 在用户触发 /merge 后调(**subagent 禁止**)",
|
|
13765
13765
|
"- action=discard:用户决定放弃当前 session 改动时调",
|
|
13766
|
+
"- action=diff:查看当前 session worktree 相对主仓(baseSha)的改动;stat=true 只看文件列表",
|
|
13766
13767
|
"**注意**:",
|
|
13767
13768
|
"- merge 走 review-fix-review 闭环(默认 3 轮);force=true 跳过 review 直接 squash",
|
|
13768
13769
|
"- subagent (coder/reviewer/planner) 调 action=merge 会被 Phase 2 guard plugin 拒绝",
|
|
@@ -13784,6 +13785,11 @@ var ArgsSchema26 = z27.discriminatedUnion("action", [
|
|
|
13784
13785
|
z27.object({
|
|
13785
13786
|
action: z27.literal("discard"),
|
|
13786
13787
|
session_id: z27.string().optional().describe("默认当前 session")
|
|
13788
|
+
}),
|
|
13789
|
+
z27.object({
|
|
13790
|
+
action: z27.literal("diff"),
|
|
13791
|
+
session_id: z27.string().optional().describe("默认当前 session"),
|
|
13792
|
+
stat: z27.boolean().optional().describe("true=只显示文件列表+统计,false=完整 diff(默认 false)")
|
|
13787
13793
|
})
|
|
13788
13794
|
]);
|
|
13789
13795
|
var _ctx = {};
|
|
@@ -13833,6 +13839,63 @@ async function execute26(input) {
|
|
|
13833
13839
|
data: { discarded: true, sessionId }
|
|
13834
13840
|
};
|
|
13835
13841
|
}
|
|
13842
|
+
if (args.action === "diff") {
|
|
13843
|
+
const entry = await getSessionWorktree(sessionId, mainRoot);
|
|
13844
|
+
if (!entry) {
|
|
13845
|
+
return { ok: false, action: "diff", error: `session ${sessionId} 没有绑定 worktree` };
|
|
13846
|
+
}
|
|
13847
|
+
const { execFile: execFile4 } = await import("node:child_process");
|
|
13848
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
13849
|
+
const execFileAsync = promisify2(execFile4);
|
|
13850
|
+
const diffArgs = args.stat ? ["diff", "--stat", entry.baseSha] : ["diff", entry.baseSha];
|
|
13851
|
+
let diffOutput = "";
|
|
13852
|
+
let changedFiles = [];
|
|
13853
|
+
try {
|
|
13854
|
+
const { stdout } = await execFileAsync("git", diffArgs, {
|
|
13855
|
+
cwd: entry.worktreePath,
|
|
13856
|
+
maxBuffer: 5242880
|
|
13857
|
+
});
|
|
13858
|
+
diffOutput = stdout;
|
|
13859
|
+
const { stdout: nameOnly } = await execFileAsync("git", ["diff", "--name-only", entry.baseSha], { cwd: entry.worktreePath, maxBuffer: 1048576 });
|
|
13860
|
+
changedFiles = nameOnly.trim().split(`
|
|
13861
|
+
`).filter(Boolean);
|
|
13862
|
+
} catch (e) {
|
|
13863
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
13864
|
+
if (!msg.includes("unknown revision") && !msg.includes("bad object")) {
|
|
13865
|
+
return { ok: false, action: "diff", error: `git diff 失败: ${msg}` };
|
|
13866
|
+
}
|
|
13867
|
+
try {
|
|
13868
|
+
const fallbackArgs = args.stat ? ["diff", "--stat", "HEAD"] : ["diff", "HEAD"];
|
|
13869
|
+
const { stdout } = await execFileAsync("git", fallbackArgs, {
|
|
13870
|
+
cwd: entry.worktreePath,
|
|
13871
|
+
maxBuffer: 5242880
|
|
13872
|
+
});
|
|
13873
|
+
diffOutput = stdout;
|
|
13874
|
+
const { stdout: nameOnly } = await execFileAsync("git", ["diff", "--name-only", "HEAD"], { cwd: entry.worktreePath, maxBuffer: 1048576 });
|
|
13875
|
+
changedFiles = nameOnly.trim().split(`
|
|
13876
|
+
`).filter(Boolean);
|
|
13877
|
+
} catch (e2) {
|
|
13878
|
+
return {
|
|
13879
|
+
ok: false,
|
|
13880
|
+
action: "diff",
|
|
13881
|
+
error: `git diff 失败: ${e2 instanceof Error ? e2.message : String(e2)}`
|
|
13882
|
+
};
|
|
13883
|
+
}
|
|
13884
|
+
}
|
|
13885
|
+
return {
|
|
13886
|
+
ok: true,
|
|
13887
|
+
action: "diff",
|
|
13888
|
+
data: {
|
|
13889
|
+
sessionId,
|
|
13890
|
+
worktreePath: entry.worktreePath,
|
|
13891
|
+
branch: entry.branch,
|
|
13892
|
+
baseSha: entry.baseSha,
|
|
13893
|
+
changedFiles,
|
|
13894
|
+
fileCount: changedFiles.length,
|
|
13895
|
+
diff: diffOutput
|
|
13896
|
+
}
|
|
13897
|
+
};
|
|
13898
|
+
}
|
|
13836
13899
|
if (!_ctx.spawner) {
|
|
13837
13900
|
return {
|
|
13838
13901
|
ok: false,
|
|
@@ -13840,11 +13903,12 @@ async function execute26(input) {
|
|
|
13840
13903
|
error: "session_merge: SubagentSpawner 未注入(Phase 1 runtime 暂未接 wire-up;" + "Phase 3 commands/merge.md 落地后会通过 __setContext 注入)"
|
|
13841
13904
|
};
|
|
13842
13905
|
}
|
|
13906
|
+
const mergeArgs = args;
|
|
13843
13907
|
const result = await runMergeLoop({
|
|
13844
13908
|
sessionId,
|
|
13845
13909
|
mainRoot,
|
|
13846
|
-
...
|
|
13847
|
-
...
|
|
13910
|
+
...mergeArgs.plan_id ? { planId: mergeArgs.plan_id } : {},
|
|
13911
|
+
...mergeArgs.force ? { force: true } : {},
|
|
13848
13912
|
spawner: _ctx.spawner
|
|
13849
13913
|
});
|
|
13850
13914
|
return { ok: true, action: "merge", data: result };
|
|
@@ -20596,7 +20660,7 @@ import * as zlib from "node:zlib";
|
|
|
20596
20660
|
// lib/version-injected.ts
|
|
20597
20661
|
function getInjectedVersion() {
|
|
20598
20662
|
try {
|
|
20599
|
-
const v = "0.5.
|
|
20663
|
+
const v = "0.5.1";
|
|
20600
20664
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
20601
20665
|
return v;
|
|
20602
20666
|
}
|
package/install.ps1
CHANGED
|
@@ -1,556 +1,556 @@
|
|
|
1
|
-
<#
|
|
2
|
-
.SYNOPSIS
|
|
3
|
-
CodeForge installer (PowerShell) — 零侵入安装到 opencode(v2 单 bundle 架构)
|
|
4
|
-
|
|
5
|
-
.DESCRIPTION
|
|
6
|
-
CodeForge v0.1+ 改为 OMO 风格的单 plugin bundle 架构:
|
|
7
|
-
1. 17 个能力被 bun build 编译成 dist/index.js 一个 ESM bundle
|
|
8
|
-
2. 在 opencode.json 里只占 1 行 plugin entry
|
|
9
|
-
3. 永久避免 opencode 1.14+ 早期版本的 zod 跨实例 bug(issue #12336/#21155)
|
|
10
|
-
|
|
11
|
-
本脚本职责:
|
|
12
|
-
1. 检测 opencode CLI 与 KH MCP
|
|
13
|
-
2. 把 dist/index.js 复制到 ~/.config/opencode/codeforge/index.js
|
|
14
|
-
3. 在 ~/.config/opencode/opencode.json 的 "plugin" 数组追加 file:// URL
|
|
15
|
-
4. 把 agents/commands 用 file-by-file copy 注入(带白名单)
|
|
16
|
-
5. 把 workflows/context-templates/review-profiles 拷贝过去
|
|
17
|
-
5b. 把 skills/ 目录拷贝到 $TargetRoot/skills/(与 install.sh 对称)
|
|
18
|
-
6. 智能合并 AGENTS.md:
|
|
19
|
-
- 项目模式 + 已有 AGENTS.md → 替换 <!-- knowledge-hub:start -->...<!-- knowledge-hub:end --> 块(带 .bak 备份)
|
|
20
|
-
- 项目模式 + 无 AGENTS.md → 生成含 marker 块的短版骨架
|
|
21
|
-
- 全局模式 → 跳过(context-templates 已经放进 ~/.config/opencode/context-templates/ 给所有项目复用)
|
|
22
|
-
7. -Global 时生成 ~/.config/codeforge/kh.json 模板(不含 token,硬约束 #2)
|
|
23
|
-
8. 输出验证清单
|
|
24
|
-
|
|
25
|
-
.PARAMETER Global
|
|
26
|
-
装到全局 (~/.config/opencode/),默认装到当前项目 (.opencode/)
|
|
27
|
-
|
|
28
|
-
.PARAMETER Uninstall
|
|
29
|
-
卸载 CodeForge 注入物(不动 opencode 自身和 AGENTS.md,会清掉 opencode.json 里的 plugin entry)
|
|
30
|
-
|
|
31
|
-
.PARAMETER DryRun
|
|
32
|
-
仅打印将要执行的操作,不真正执行
|
|
33
|
-
|
|
34
|
-
.PARAMETER SkipBuild
|
|
35
|
-
跳过 npm run build。默认每次安装前会先 build 确保 dist 是最新的,加 -SkipBuild 跳过
|
|
36
|
-
|
|
37
|
-
.EXAMPLE
|
|
38
|
-
.\install.ps1
|
|
39
|
-
.\install.ps1 -Global
|
|
40
|
-
.\install.ps1 -Uninstall
|
|
41
|
-
.\install.ps1 -DryRun
|
|
42
|
-
.\install.ps1 -SkipBuild
|
|
43
|
-
#>
|
|
44
|
-
[CmdletBinding()]
|
|
45
|
-
param(
|
|
46
|
-
[switch]$Global,
|
|
47
|
-
[switch]$Uninstall,
|
|
48
|
-
[switch]$DryRun,
|
|
49
|
-
[switch]$SkipBuild
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
$ErrorActionPreference = 'Stop'
|
|
53
|
-
|
|
54
|
-
# ────────────── 工具函数 ──────────────
|
|
55
|
-
function Write-Section([string]$Text) {
|
|
56
|
-
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
57
|
-
Write-Host "[codeforge] $Text" -ForegroundColor Cyan
|
|
58
|
-
}
|
|
59
|
-
function Write-Ok([string]$Text) { Write-Host "$([char]0x2713) $Text" -ForegroundColor Green }
|
|
60
|
-
function Write-Warn2([string]$Text) { Write-Host "$([char]0x26A0) $Text" -ForegroundColor Yellow }
|
|
61
|
-
function Write-Err2([string]$Text) { Write-Host "$([char]0x2717) $Text" -ForegroundColor Red }
|
|
62
|
-
|
|
63
|
-
function Invoke-Step([string]$Description, [scriptblock]$Action) {
|
|
64
|
-
if ($DryRun) {
|
|
65
|
-
Write-Host " [dry-run] $Description" -ForegroundColor Blue
|
|
66
|
-
} else {
|
|
67
|
-
& $Action
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
# ────────────── 路径解析 ──────────────
|
|
72
|
-
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
73
|
-
$SourceRoot = $ScriptDir
|
|
74
|
-
|
|
75
|
-
if ($Global) {
|
|
76
|
-
$XdgConfig = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' }
|
|
77
|
-
$TargetRoot = Join-Path $XdgConfig 'opencode'
|
|
78
|
-
$Mode = 'global'
|
|
79
|
-
} else {
|
|
80
|
-
$TargetRoot = Join-Path (Get-Location).Path '.opencode'
|
|
81
|
-
$Mode = 'project'
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# CodeForge 自身的全局配置目录(与 opencode 配置目录同级)
|
|
85
|
-
$XdgConfigBase = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' }
|
|
86
|
-
$CodeforgeCfgDir = Join-Path $XdgConfigBase 'codeforge'
|
|
87
|
-
|
|
88
|
-
# v0.1 之前 install.ps1 装的目录(卸载时一并清掉)
|
|
89
|
-
$LegacyDirs = @('agent', 'command', 'tool', 'tools', 'plugin', 'plugins', 'lib')
|
|
90
|
-
# v0.1+ 才有的目录(review-profiles 由 ADR:reviewer-multi-profile 引入)
|
|
91
|
-
$ManagedDirs = @('codeforge', 'agents', 'commands', 'workflows', 'context-templates', 'review-profiles')
|
|
92
|
-
|
|
93
|
-
# v0.1+ 文件分发计划
|
|
94
|
-
$BundleSrcRel = 'dist/index.js'
|
|
95
|
-
$BundleDstRel = 'codeforge/index.js' # 相对 $TargetRoot
|
|
96
|
-
|
|
97
|
-
# B3 KH 行为规范模板(智能合并的输入)
|
|
98
|
-
$KhTemplateRel = 'context-templates/kh-instructions.md'
|
|
99
|
-
|
|
100
|
-
# .md file-by-file copy(白名单 *.md,排除 README/_*/.bak)
|
|
101
|
-
$MdCopyMap = @(
|
|
102
|
-
@{ Src='agents'; Dst='agents' },
|
|
103
|
-
@{ Src='commands'; Dst='commands' }
|
|
104
|
-
)
|
|
105
|
-
# 普通整目录 copy(review-profiles 由 ADR:reviewer-multi-profile 引入)
|
|
106
|
-
$CopyMap = @(
|
|
107
|
-
@{ Src='workflows'; Dst='workflows' },
|
|
108
|
-
@{ Src='context-templates'; Dst='context-templates' },
|
|
109
|
-
@{ Src='review-profiles'; Dst='review-profiles' }
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
# ────────────── opencode.json 管理 ──────────────
|
|
113
|
-
function Get-OpencodeConfigPath {
|
|
114
|
-
Join-Path $TargetRoot 'opencode.json'
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function Get-PluginUri {
|
|
118
|
-
# opencode 接受 file:// URL 也接受裸绝对路径,统一用 file:// 更明确
|
|
119
|
-
$abs = Join-Path $TargetRoot $BundleDstRel
|
|
120
|
-
$abs = $abs -replace '\\', '/'
|
|
121
|
-
if ($abs -notmatch '^[A-Za-z]:/') {
|
|
122
|
-
return "file://$abs"
|
|
123
|
-
}
|
|
124
|
-
return "file:///$abs"
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function Add-PluginEntry {
|
|
128
|
-
$cfgPath = Get-OpencodeConfigPath
|
|
129
|
-
$uri = Get-PluginUri
|
|
130
|
-
|
|
131
|
-
if (-not (Test-Path -LiteralPath $TargetRoot)) {
|
|
132
|
-
Invoke-Step "mkdir $TargetRoot" {
|
|
133
|
-
New-Item -ItemType Directory -Path $TargetRoot -Force | Out-Null
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
|
138
|
-
Write-Err2 "需要 node 才能改写 opencode.json,请安装 Node.js >= 20"
|
|
139
|
-
exit 1
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
Invoke-Step "write $cfgPath plugin entry: $uri" {
|
|
143
|
-
$env:CODEFORGE_CFG = $cfgPath
|
|
144
|
-
$env:CODEFORGE_URI = $uri
|
|
145
|
-
$nodeScript = @'
|
|
146
|
-
const fs = require("node:fs");
|
|
147
|
-
const path = process.env.CODEFORGE_CFG;
|
|
148
|
-
const uri = process.env.CODEFORGE_URI;
|
|
149
|
-
let cfg = {};
|
|
150
|
-
if (fs.existsSync(path)) {
|
|
151
|
-
try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); }
|
|
152
|
-
catch { fs.copyFileSync(path, path + ".bak." + Date.now()); cfg = {}; }
|
|
153
|
-
}
|
|
154
|
-
if (!cfg.$schema) cfg.$schema = "https://opencode.ai/config.json";
|
|
155
|
-
if (!Array.isArray(cfg.plugin)) cfg.plugin = [];
|
|
156
|
-
const cleaned = [];
|
|
157
|
-
for (const e of cfg.plugin) {
|
|
158
|
-
const s = String(e);
|
|
159
|
-
if (/\/codeforge\/index\.js$/.test(s)) continue;
|
|
160
|
-
if (/\/plugins\/[^/]+\.ts$/.test(s) && /opencode/.test(s)) continue;
|
|
161
|
-
if (/\/\.opencode\/plugins\//.test(s)) continue;
|
|
162
|
-
cleaned.push(e);
|
|
163
|
-
}
|
|
164
|
-
cleaned.push(uri);
|
|
165
|
-
cfg.plugin = cleaned;
|
|
166
|
-
fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
167
|
-
'@
|
|
168
|
-
& node -e $nodeScript
|
|
169
|
-
Remove-Item Env:\CODEFORGE_CFG -ErrorAction SilentlyContinue
|
|
170
|
-
Remove-Item Env:\CODEFORGE_URI -ErrorAction SilentlyContinue
|
|
171
|
-
}
|
|
172
|
-
Write-Ok "opencode.json 已写入 plugin entry: $uri"
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function Remove-PluginEntry {
|
|
176
|
-
$cfgPath = Get-OpencodeConfigPath
|
|
177
|
-
if (-not (Test-Path -LiteralPath $cfgPath)) { return }
|
|
178
|
-
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
|
179
|
-
Write-Warn2 "未找到 node,跳过 opencode.json plugin entry 清理"
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
Invoke-Step "rewrite $cfgPath without codeforge plugin entry" {
|
|
184
|
-
$env:CODEFORGE_CFG = $cfgPath
|
|
185
|
-
$nodeScript = @'
|
|
186
|
-
const fs = require("node:fs");
|
|
187
|
-
const path = process.env.CODEFORGE_CFG;
|
|
188
|
-
let cfg;
|
|
189
|
-
try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); }
|
|
190
|
-
catch { return; }
|
|
191
|
-
if (!Array.isArray(cfg.plugin)) return;
|
|
192
|
-
cfg.plugin = cfg.plugin.filter(e => {
|
|
193
|
-
const s = String(e);
|
|
194
|
-
if (/\/codeforge\/index\.js$/.test(s)) return false;
|
|
195
|
-
if (/\/plugins\/[^/]+\.ts$/.test(s)) return false;
|
|
196
|
-
if (/\/\.opencode\/plugins\//.test(s)) return false;
|
|
197
|
-
return true;
|
|
198
|
-
});
|
|
199
|
-
fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
200
|
-
'@
|
|
201
|
-
& node -e $nodeScript
|
|
202
|
-
Remove-Item Env:\CODEFORGE_CFG -ErrorAction SilentlyContinue
|
|
203
|
-
}
|
|
204
|
-
Write-Ok "opencode.json 已移除 codeforge plugin entry"
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
# ────────────── KH 全局模板(仅 -Global) ──────────────
|
|
208
|
-
#
|
|
209
|
-
# 硬约束 #2:API key 绝不能落盘,模板里**不写** token / apiKey 字段。
|
|
210
|
-
# 用户必须通过环境变量 KNOWLEDGE_API_KEY 提供。
|
|
211
|
-
# 已存在则跳过,避免覆盖用户自定义。
|
|
212
|
-
function Write-KhTemplate {
|
|
213
|
-
$khFile = Join-Path $CodeforgeCfgDir 'kh.json'
|
|
214
|
-
if (-not (Test-Path -LiteralPath $CodeforgeCfgDir)) {
|
|
215
|
-
Invoke-Step "mkdir $CodeforgeCfgDir" {
|
|
216
|
-
New-Item -ItemType Directory -Path $CodeforgeCfgDir -Force | Out-Null
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (Test-Path -LiteralPath $khFile) {
|
|
220
|
-
Write-Ok "已存在 $khFile,跳过覆盖"
|
|
221
|
-
return
|
|
222
|
-
}
|
|
223
|
-
Invoke-Step "write $khFile (no token)" {
|
|
224
|
-
$body = @"
|
|
225
|
-
{
|
|
226
|
-
"url": "http://10.5.60.26:8900/mcp",
|
|
227
|
-
"timeoutMs": 5000,
|
|
228
|
-
"maxRetries": 1
|
|
229
|
-
}
|
|
230
|
-
"@
|
|
231
|
-
# 用 .NET WriteAllText 强制无 BOM 的 UTF8(PowerShell 5.1 默认 BOM 会被 KhConfig 容错,但保持干净)
|
|
232
|
-
[System.IO.File]::WriteAllText($khFile, $body, (New-Object System.Text.UTF8Encoding $false))
|
|
233
|
-
}
|
|
234
|
-
Write-Ok "已生成 $khFile(API key 请通过环境变量 KNOWLEDGE_API_KEY 提供,禁止写入文件)"
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
# ────────────── AGENTS.md 智能合并(B3 新增) ──────────────
|
|
238
|
-
#
|
|
239
|
-
# 实现委托给 scripts/merge-agents-md.mjs CLI(C.4 单一入口),
|
|
240
|
-
# 算法源自 lib/agents-merge.ts,sh/ps1 共用同一份 Node 实现,
|
|
241
|
-
# 避免 install 脚本各自维护算法副本造成漂移。
|
|
242
|
-
function Merge-ProjectAgentsMd {
|
|
243
|
-
param(
|
|
244
|
-
[Parameter(Mandatory=$true)] [string] $AgentsTarget,
|
|
245
|
-
[Parameter(Mandatory=$true)] [string] $TemplatePath
|
|
246
|
-
)
|
|
247
|
-
$cliPath = Join-Path $SourceRoot 'scripts/merge-agents-md.mjs'
|
|
248
|
-
if (-not (Test-Path -LiteralPath $TemplatePath)) {
|
|
249
|
-
Write-Warn2 "模板不存在,跳过 AGENTS.md 合并: $TemplatePath"
|
|
250
|
-
return
|
|
251
|
-
}
|
|
252
|
-
if (-not (Test-Path -LiteralPath $cliPath)) {
|
|
253
|
-
Write-Warn2 "merge CLI 不存在,跳过 AGENTS.md 智能合并: $cliPath"
|
|
254
|
-
return
|
|
255
|
-
}
|
|
256
|
-
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
|
257
|
-
Write-Warn2 "未找到 node,跳过 AGENTS.md 智能合并(请安装 Node.js >= 20)"
|
|
258
|
-
return
|
|
259
|
-
}
|
|
260
|
-
$cliArgs = @('--target', $AgentsTarget, '--template', $TemplatePath)
|
|
261
|
-
if ($DryRun) { $cliArgs += '--dry-run' }
|
|
262
|
-
try {
|
|
263
|
-
& node $cliPath @cliArgs
|
|
264
|
-
} catch {
|
|
265
|
-
Write-Warn2 "AGENTS.md 合并失败(不阻塞 install 主流程): $_"
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
# ────────────── 卸载 ──────────────
|
|
270
|
-
function Invoke-Uninstall {
|
|
271
|
-
Write-Section "卸载 CodeForge from: $TargetRoot"
|
|
272
|
-
Remove-PluginEntry
|
|
273
|
-
$candidates = $LegacyDirs + $ManagedDirs | Select-Object -Unique
|
|
274
|
-
foreach ($name in $candidates) {
|
|
275
|
-
$path = Join-Path $TargetRoot $name
|
|
276
|
-
if (Test-Path -LiteralPath $path) {
|
|
277
|
-
Invoke-Step "remove $path" {
|
|
278
|
-
# Junction 不能用 Remove-Item 直接删(会删源),需要先 rmdir
|
|
279
|
-
$item = Get-Item -LiteralPath $path -Force
|
|
280
|
-
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
|
281
|
-
[IO.Directory]::Delete($path, $false)
|
|
282
|
-
} else {
|
|
283
|
-
Remove-Item -LiteralPath $path -Recurse -Force
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
Write-Ok "已删除 $path"
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
# 细粒度删除 skills:只删 CodeForge 自己的 skill,不删用户自装的
|
|
290
|
-
$ownedSkills = @('ambiguity-gate', 'devils-advocate', 'ears-zh', 'example-mapping', 'success-criteria', 'weighted-dimensions')
|
|
291
|
-
foreach ($skillName in $ownedSkills) {
|
|
292
|
-
$skillPath = Join-Path $TargetRoot "skills\$skillName"
|
|
293
|
-
if (Test-Path -LiteralPath $skillPath) {
|
|
294
|
-
Invoke-Step "remove skill $skillName" {
|
|
295
|
-
Remove-Item -LiteralPath $skillPath -Recurse -Force
|
|
296
|
-
}
|
|
297
|
-
Write-Ok "已删除 skill: $skillPath"
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
301
|
-
Write-Ok "卸载完成(opencode 自身、AGENTS.md、~/.config/codeforge/kh.json 不会被动)"
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
# ────────────── 环境检测 ──────────────
|
|
305
|
-
function Test-Opencode {
|
|
306
|
-
$cmd = Get-Command opencode -ErrorAction SilentlyContinue
|
|
307
|
-
if ($cmd) {
|
|
308
|
-
try {
|
|
309
|
-
$v = & opencode --version 2>$null
|
|
310
|
-
Write-Ok "检测到 opencode: $v"
|
|
311
|
-
} catch {
|
|
312
|
-
Write-Ok "检测到 opencode (版本未知)"
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
Write-Warn2 "未检测到 opencode CLI"
|
|
316
|
-
Write-Warn2 " 安装方式:https://github.com/sst/opencode#installation"
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function Test-KhMcp {
|
|
321
|
-
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' }
|
|
322
|
-
$cfg = Join-Path (Join-Path $xdg 'opencode') 'opencode.json'
|
|
323
|
-
if ((Test-Path -LiteralPath $cfg) -and ((Get-Content -LiteralPath $cfg -Raw -Encoding UTF8) -match 'knowledge-hub|code-forge-knowledge-hub')) {
|
|
324
|
-
Write-Ok "检测到 Knowledge Hub MCP 已注册"
|
|
325
|
-
} else {
|
|
326
|
-
Write-Warn2 "未在 $cfg 中找到 Knowledge Hub MCP"
|
|
327
|
-
Write-Warn2 " 配置示例见 docs/PRD.md §6.2"
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
# ────────────── 主流程 ──────────────
|
|
332
|
-
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
333
|
-
Write-Host "[codeforge] CodeForge installer (mode=$Mode, uninstall=$Uninstall, dry-run=$DryRun)" -ForegroundColor Cyan
|
|
334
|
-
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
335
|
-
Write-Host "Source : $SourceRoot"
|
|
336
|
-
Write-Host "Target : $TargetRoot"
|
|
337
|
-
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
338
|
-
|
|
339
|
-
if ($Uninstall) {
|
|
340
|
-
Invoke-Uninstall
|
|
341
|
-
exit 0
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
# Step 1/7: 环境检测
|
|
345
|
-
Write-Section 'Step 1/7: 环境检测'
|
|
346
|
-
Test-Opencode
|
|
347
|
-
Test-KhMcp
|
|
348
|
-
|
|
349
|
-
# Step 2/7: build dist bundle
|
|
350
|
-
Write-Section 'Step 2/7: 构建 dist/index.js 单 bundle'
|
|
351
|
-
$bundleSrc = Join-Path $SourceRoot $BundleSrcRel
|
|
352
|
-
if ($SkipBuild) {
|
|
353
|
-
Write-Warn2 '已跳过 build(-SkipBuild),使用现有 dist/index.js'
|
|
354
|
-
} else {
|
|
355
|
-
Invoke-Step "npm run build" {
|
|
356
|
-
Push-Location $SourceRoot
|
|
357
|
-
try {
|
|
358
|
-
$out = npm run build 2>&1 | Out-String
|
|
359
|
-
if ($LASTEXITCODE -ne 0) {
|
|
360
|
-
Write-Err2 'npm run build 失败'
|
|
361
|
-
Write-Host $out
|
|
362
|
-
exit 1
|
|
363
|
-
}
|
|
364
|
-
} finally {
|
|
365
|
-
Pop-Location
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (-not (Test-Path -LiteralPath $bundleSrc)) {
|
|
370
|
-
if ($SkipBuild) {
|
|
371
|
-
Write-Err2 "找不到 $bundleSrc(npm 包可能损坏)"
|
|
372
|
-
Write-Err2 " 请尝试重装:npx @andyqiu/codeforge install [--global]"
|
|
373
|
-
} else {
|
|
374
|
-
Write-Err2 "找不到 $bundleSrc,请先成功执行 npm run build"
|
|
375
|
-
}
|
|
376
|
-
exit 1
|
|
377
|
-
}
|
|
378
|
-
$bundleSize = (Get-Item -LiteralPath $bundleSrc).Length
|
|
379
|
-
Write-Ok "bundle 已就绪: $bundleSrc ($([math]::Round($bundleSize/1024,1)) KB)"
|
|
380
|
-
|
|
381
|
-
# Step 3/7: 准备目标目录
|
|
382
|
-
Write-Section 'Step 3/7: 准备目标目录'
|
|
383
|
-
if (-not (Test-Path -LiteralPath $TargetRoot)) {
|
|
384
|
-
Invoke-Step "mkdir $TargetRoot" {
|
|
385
|
-
New-Item -ItemType Directory -Path $TargetRoot -Force | Out-Null
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
# 清理 legacy 单数目录 + 老的 17 个 plugin / lib / tools junction
|
|
389
|
-
foreach ($legacy in $LegacyDirs) {
|
|
390
|
-
$dst = Join-Path $TargetRoot $legacy
|
|
391
|
-
if (Test-Path -LiteralPath $dst) {
|
|
392
|
-
Invoke-Step "cleanup legacy $dst" {
|
|
393
|
-
$item = Get-Item -LiteralPath $dst -Force
|
|
394
|
-
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
|
395
|
-
[IO.Directory]::Delete($dst, $false)
|
|
396
|
-
} else {
|
|
397
|
-
Remove-Item -LiteralPath $dst -Recurse -Force
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
Write-Warn2 "已清理 legacy 目录: $dst"
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
# Step 4/7: 装 bundle 到 codeforge/index.js
|
|
405
|
-
Write-Section 'Step 4/7: 装入 dist/index.js bundle'
|
|
406
|
-
$bundleDst = Join-Path $TargetRoot $BundleDstRel
|
|
407
|
-
$bundleDstDir = Split-Path -Parent $bundleDst
|
|
408
|
-
if (-not (Test-Path -LiteralPath $bundleDstDir)) {
|
|
409
|
-
Invoke-Step "mkdir $bundleDstDir" {
|
|
410
|
-
New-Item -ItemType Directory -Path $bundleDstDir -Force | Out-Null
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
Invoke-Step "copy $BundleSrcRel -> $BundleDstRel" {
|
|
414
|
-
Copy-Item -LiteralPath $bundleSrc -Destination $bundleDst -Force
|
|
415
|
-
}
|
|
416
|
-
Write-Ok "bundle -> $bundleDst"
|
|
417
|
-
Add-PluginEntry
|
|
418
|
-
|
|
419
|
-
# 写 VERSION marker 文件(用户 cat 一行查版本,不依赖 grep bundle)
|
|
420
|
-
$cfVersion = "unknown"
|
|
421
|
-
try {
|
|
422
|
-
$pkgPath = Join-Path $SourceRoot "package.json"
|
|
423
|
-
if (Test-Path -LiteralPath $pkgPath) {
|
|
424
|
-
$pkg = Get-Content -LiteralPath $pkgPath -Raw | ConvertFrom-Json
|
|
425
|
-
if ($pkg.version) { $cfVersion = $pkg.version }
|
|
426
|
-
}
|
|
427
|
-
} catch { $cfVersion = "unknown" }
|
|
428
|
-
$versionFile = Join-Path $TargetRoot "codeforge/VERSION"
|
|
429
|
-
if (-not $DryRun) {
|
|
430
|
-
Set-Content -LiteralPath $versionFile -Value $cfVersion -NoNewline:$false -Encoding utf8
|
|
431
|
-
}
|
|
432
|
-
Write-Ok "VERSION -> $versionFile ($cfVersion)"
|
|
433
|
-
|
|
434
|
-
# Step 5/7: 装 agents / commands / workflows / context-templates / review-profiles
|
|
435
|
-
Write-Section 'Step 5/7: 装 agents / commands / workflows / context-templates / review-profiles'
|
|
436
|
-
foreach ($entry in $MdCopyMap) {
|
|
437
|
-
$src = Join-Path $SourceRoot $entry.Src
|
|
438
|
-
$dst = Join-Path $TargetRoot $entry.Dst
|
|
439
|
-
if (-not (Test-Path -LiteralPath $src)) {
|
|
440
|
-
Write-Warn2 "源目录不存在,跳过: $src"
|
|
441
|
-
continue
|
|
442
|
-
}
|
|
443
|
-
if (Test-Path -LiteralPath $dst) {
|
|
444
|
-
Invoke-Step "cleanup old $dst" {
|
|
445
|
-
$item = Get-Item -LiteralPath $dst -Force
|
|
446
|
-
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
|
447
|
-
[IO.Directory]::Delete($dst, $false)
|
|
448
|
-
} else {
|
|
449
|
-
Remove-Item -LiteralPath $dst -Recurse -Force
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
Invoke-Step "mkdir $dst" {
|
|
454
|
-
New-Item -ItemType Directory -Path $dst -Force | Out-Null
|
|
455
|
-
}
|
|
456
|
-
$files = Get-ChildItem -LiteralPath $src -Filter '*.md' -File | Where-Object {
|
|
457
|
-
$_.Name -ne 'README.md' -and
|
|
458
|
-
$_.Name -notmatch '^_' -and
|
|
459
|
-
$_.Name -notmatch '\.bak$' -and
|
|
460
|
-
$_.Name -notmatch '^\.'
|
|
461
|
-
}
|
|
462
|
-
foreach ($f in $files) {
|
|
463
|
-
Invoke-Step "copy $($entry.Src)/$($f.Name)" {
|
|
464
|
-
Copy-Item -LiteralPath $f.FullName -Destination (Join-Path $dst $f.Name) -Force
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
Write-Ok "$($entry.Src)/ -> $dst ($($files.Count) 个 .md)"
|
|
468
|
-
}
|
|
469
|
-
foreach ($entry in $CopyMap) {
|
|
470
|
-
$src = Join-Path $SourceRoot $entry.Src
|
|
471
|
-
$dst = Join-Path $TargetRoot $entry.Dst
|
|
472
|
-
if (-not (Test-Path -LiteralPath $src)) {
|
|
473
|
-
Write-Warn2 "源目录不存在,跳过: $src"
|
|
474
|
-
continue
|
|
475
|
-
}
|
|
476
|
-
Invoke-Step "copy $($entry.Src)/ -> $dst" {
|
|
477
|
-
if (-not (Test-Path -LiteralPath $dst)) {
|
|
478
|
-
New-Item -ItemType Directory -Path $dst -Force | Out-Null
|
|
479
|
-
}
|
|
480
|
-
Copy-Item -Path (Join-Path $src '*') -Destination $dst -Recurse -Force
|
|
481
|
-
}
|
|
482
|
-
Write-Ok "$($entry.Src)/ -> $dst (整目录拷贝)"
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
# Step 5b/7: 装 skills/(opencode skill 目录)
|
|
486
|
-
Write-Section 'Step 5b/7: 装 skills/'
|
|
487
|
-
$skillsSrc = Join-Path $SourceRoot 'skills'
|
|
488
|
-
$skillsDst = Join-Path $TargetRoot 'skills'
|
|
489
|
-
if (Test-Path -LiteralPath $skillsSrc) {
|
|
490
|
-
if (-not (Test-Path -LiteralPath $skillsDst)) {
|
|
491
|
-
New-Item -ItemType Directory -Path $skillsDst -Force | Out-Null
|
|
492
|
-
}
|
|
493
|
-
$skillDirs = Get-ChildItem -LiteralPath $skillsSrc -Directory
|
|
494
|
-
$skillCount = 0
|
|
495
|
-
foreach ($skillDir in $skillDirs) {
|
|
496
|
-
$dstSkill = Join-Path $skillsDst $skillDir.Name
|
|
497
|
-
if (Test-Path -LiteralPath $dstSkill) {
|
|
498
|
-
Invoke-Step "remove old skill $($skillDir.Name)" {
|
|
499
|
-
Remove-Item -LiteralPath $dstSkill -Recurse -Force
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
Invoke-Step "copy skill $($skillDir.Name)" {
|
|
503
|
-
Copy-Item -LiteralPath $skillDir.FullName -Destination $dstSkill -Recurse -Force
|
|
504
|
-
}
|
|
505
|
-
$skillCount++
|
|
506
|
-
}
|
|
507
|
-
Write-Ok "skills/ -> $skillsDst ($skillCount 个 skill)"
|
|
508
|
-
} else {
|
|
509
|
-
Write-Warn2 "skills/ 目录不存在,跳过(发布包未含 skills)"
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
# Step 6/7: AGENTS.md 智能合并(仅项目模式)
|
|
513
|
-
Write-Section 'Step 6/7: AGENTS.md 智能合并'
|
|
514
|
-
if ($Mode -eq 'project') {
|
|
515
|
-
$agentsTarget = Join-Path (Get-Location).Path 'AGENTS.md'
|
|
516
|
-
$templatePath = Join-Path $SourceRoot $KhTemplateRel
|
|
517
|
-
Merge-ProjectAgentsMd -AgentsTarget $agentsTarget -TemplatePath $templatePath
|
|
518
|
-
} else {
|
|
519
|
-
Write-Ok "全局模式:跳过项目 AGENTS.md 合并(context-templates 已装到 $TargetRoot\context-templates)"
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
# Step 7/7: KH 全局配置模板(仅 -Global)
|
|
523
|
-
Write-Section 'Step 7/7: KH 全局配置模板'
|
|
524
|
-
if ($Global) {
|
|
525
|
-
Write-KhTemplate
|
|
526
|
-
} else {
|
|
527
|
-
Write-Ok "项目级安装,跳过 KH 全局模板(要装请加 -Global)"
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
# 验证清单
|
|
531
|
-
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
532
|
-
Write-Ok "CodeForge v$cfVersion 安装完成"
|
|
533
|
-
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
534
|
-
Write-Host @"
|
|
535
|
-
验证清单:
|
|
536
|
-
1. 列出注入的扩展点:
|
|
537
|
-
PS> Get-ChildItem '$TargetRoot' -Force
|
|
538
|
-
2. 看 opencode.json 里 plugin entry:
|
|
539
|
-
PS> Get-Content '$TargetRoot\opencode.json'
|
|
540
|
-
3. 让 opencode 校验配置:
|
|
541
|
-
PS> opencode debug config
|
|
542
|
-
-> 期望看到 1 个 codeforge plugin entry(不是 17 个)
|
|
543
|
-
4. 跑一次最小 dogfood,看 plugin import + activation 日志:
|
|
544
|
-
PS> opencode run "hello"
|
|
545
|
-
PS> Get-Content `$HOME\.cache\codeforge\plugins.log -Tail 50
|
|
546
|
-
5. KH 配置(仅 -Global 安装时生成):
|
|
547
|
-
PS> Get-Content '$CodeforgeCfgDir\kh.json'
|
|
548
|
-
PS> `$env:KNOWLEDGE_API_KEY = '<your-token>' # ← API key 必须走环境变量
|
|
549
|
-
6. AGENTS.md 智能合并(仅项目级):
|
|
550
|
-
- 已有 AGENTS.md → KH 块被替换成模板内容,原文件备份成 *.bak.<timestamp>
|
|
551
|
-
- 模板源:$SourceRoot\$KhTemplateRel(修改后请重新 install)
|
|
552
|
-
|
|
553
|
-
卸载:
|
|
554
|
-
PS> .\install.ps1 -Uninstall$(if ($Global) { ' -Global' })
|
|
555
|
-
"@
|
|
556
|
-
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
CodeForge installer (PowerShell) — 零侵入安装到 opencode(v2 单 bundle 架构)
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
CodeForge v0.1+ 改为 OMO 风格的单 plugin bundle 架构:
|
|
7
|
+
1. 17 个能力被 bun build 编译成 dist/index.js 一个 ESM bundle
|
|
8
|
+
2. 在 opencode.json 里只占 1 行 plugin entry
|
|
9
|
+
3. 永久避免 opencode 1.14+ 早期版本的 zod 跨实例 bug(issue #12336/#21155)
|
|
10
|
+
|
|
11
|
+
本脚本职责:
|
|
12
|
+
1. 检测 opencode CLI 与 KH MCP
|
|
13
|
+
2. 把 dist/index.js 复制到 ~/.config/opencode/codeforge/index.js
|
|
14
|
+
3. 在 ~/.config/opencode/opencode.json 的 "plugin" 数组追加 file:// URL
|
|
15
|
+
4. 把 agents/commands 用 file-by-file copy 注入(带白名单)
|
|
16
|
+
5. 把 workflows/context-templates/review-profiles 拷贝过去
|
|
17
|
+
5b. 把 skills/ 目录拷贝到 $TargetRoot/skills/(与 install.sh 对称)
|
|
18
|
+
6. 智能合并 AGENTS.md:
|
|
19
|
+
- 项目模式 + 已有 AGENTS.md → 替换 <!-- knowledge-hub:start -->...<!-- knowledge-hub:end --> 块(带 .bak 备份)
|
|
20
|
+
- 项目模式 + 无 AGENTS.md → 生成含 marker 块的短版骨架
|
|
21
|
+
- 全局模式 → 跳过(context-templates 已经放进 ~/.config/opencode/context-templates/ 给所有项目复用)
|
|
22
|
+
7. -Global 时生成 ~/.config/codeforge/kh.json 模板(不含 token,硬约束 #2)
|
|
23
|
+
8. 输出验证清单
|
|
24
|
+
|
|
25
|
+
.PARAMETER Global
|
|
26
|
+
装到全局 (~/.config/opencode/),默认装到当前项目 (.opencode/)
|
|
27
|
+
|
|
28
|
+
.PARAMETER Uninstall
|
|
29
|
+
卸载 CodeForge 注入物(不动 opencode 自身和 AGENTS.md,会清掉 opencode.json 里的 plugin entry)
|
|
30
|
+
|
|
31
|
+
.PARAMETER DryRun
|
|
32
|
+
仅打印将要执行的操作,不真正执行
|
|
33
|
+
|
|
34
|
+
.PARAMETER SkipBuild
|
|
35
|
+
跳过 npm run build。默认每次安装前会先 build 确保 dist 是最新的,加 -SkipBuild 跳过
|
|
36
|
+
|
|
37
|
+
.EXAMPLE
|
|
38
|
+
.\install.ps1
|
|
39
|
+
.\install.ps1 -Global
|
|
40
|
+
.\install.ps1 -Uninstall
|
|
41
|
+
.\install.ps1 -DryRun
|
|
42
|
+
.\install.ps1 -SkipBuild
|
|
43
|
+
#>
|
|
44
|
+
[CmdletBinding()]
|
|
45
|
+
param(
|
|
46
|
+
[switch]$Global,
|
|
47
|
+
[switch]$Uninstall,
|
|
48
|
+
[switch]$DryRun,
|
|
49
|
+
[switch]$SkipBuild
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
$ErrorActionPreference = 'Stop'
|
|
53
|
+
|
|
54
|
+
# ────────────── 工具函数 ──────────────
|
|
55
|
+
function Write-Section([string]$Text) {
|
|
56
|
+
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
57
|
+
Write-Host "[codeforge] $Text" -ForegroundColor Cyan
|
|
58
|
+
}
|
|
59
|
+
function Write-Ok([string]$Text) { Write-Host "$([char]0x2713) $Text" -ForegroundColor Green }
|
|
60
|
+
function Write-Warn2([string]$Text) { Write-Host "$([char]0x26A0) $Text" -ForegroundColor Yellow }
|
|
61
|
+
function Write-Err2([string]$Text) { Write-Host "$([char]0x2717) $Text" -ForegroundColor Red }
|
|
62
|
+
|
|
63
|
+
function Invoke-Step([string]$Description, [scriptblock]$Action) {
|
|
64
|
+
if ($DryRun) {
|
|
65
|
+
Write-Host " [dry-run] $Description" -ForegroundColor Blue
|
|
66
|
+
} else {
|
|
67
|
+
& $Action
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# ────────────── 路径解析 ──────────────
|
|
72
|
+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
73
|
+
$SourceRoot = $ScriptDir
|
|
74
|
+
|
|
75
|
+
if ($Global) {
|
|
76
|
+
$XdgConfig = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' }
|
|
77
|
+
$TargetRoot = Join-Path $XdgConfig 'opencode'
|
|
78
|
+
$Mode = 'global'
|
|
79
|
+
} else {
|
|
80
|
+
$TargetRoot = Join-Path (Get-Location).Path '.opencode'
|
|
81
|
+
$Mode = 'project'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# CodeForge 自身的全局配置目录(与 opencode 配置目录同级)
|
|
85
|
+
$XdgConfigBase = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' }
|
|
86
|
+
$CodeforgeCfgDir = Join-Path $XdgConfigBase 'codeforge'
|
|
87
|
+
|
|
88
|
+
# v0.1 之前 install.ps1 装的目录(卸载时一并清掉)
|
|
89
|
+
$LegacyDirs = @('agent', 'command', 'tool', 'tools', 'plugin', 'plugins', 'lib')
|
|
90
|
+
# v0.1+ 才有的目录(review-profiles 由 ADR:reviewer-multi-profile 引入)
|
|
91
|
+
$ManagedDirs = @('codeforge', 'agents', 'commands', 'workflows', 'context-templates', 'review-profiles')
|
|
92
|
+
|
|
93
|
+
# v0.1+ 文件分发计划
|
|
94
|
+
$BundleSrcRel = 'dist/index.js'
|
|
95
|
+
$BundleDstRel = 'codeforge/index.js' # 相对 $TargetRoot
|
|
96
|
+
|
|
97
|
+
# B3 KH 行为规范模板(智能合并的输入)
|
|
98
|
+
$KhTemplateRel = 'context-templates/kh-instructions.md'
|
|
99
|
+
|
|
100
|
+
# .md file-by-file copy(白名单 *.md,排除 README/_*/.bak)
|
|
101
|
+
$MdCopyMap = @(
|
|
102
|
+
@{ Src='agents'; Dst='agents' },
|
|
103
|
+
@{ Src='commands'; Dst='commands' }
|
|
104
|
+
)
|
|
105
|
+
# 普通整目录 copy(review-profiles 由 ADR:reviewer-multi-profile 引入)
|
|
106
|
+
$CopyMap = @(
|
|
107
|
+
@{ Src='workflows'; Dst='workflows' },
|
|
108
|
+
@{ Src='context-templates'; Dst='context-templates' },
|
|
109
|
+
@{ Src='review-profiles'; Dst='review-profiles' }
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# ────────────── opencode.json 管理 ──────────────
|
|
113
|
+
function Get-OpencodeConfigPath {
|
|
114
|
+
Join-Path $TargetRoot 'opencode.json'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function Get-PluginUri {
|
|
118
|
+
# opencode 接受 file:// URL 也接受裸绝对路径,统一用 file:// 更明确
|
|
119
|
+
$abs = Join-Path $TargetRoot $BundleDstRel
|
|
120
|
+
$abs = $abs -replace '\\', '/'
|
|
121
|
+
if ($abs -notmatch '^[A-Za-z]:/') {
|
|
122
|
+
return "file://$abs"
|
|
123
|
+
}
|
|
124
|
+
return "file:///$abs"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function Add-PluginEntry {
|
|
128
|
+
$cfgPath = Get-OpencodeConfigPath
|
|
129
|
+
$uri = Get-PluginUri
|
|
130
|
+
|
|
131
|
+
if (-not (Test-Path -LiteralPath $TargetRoot)) {
|
|
132
|
+
Invoke-Step "mkdir $TargetRoot" {
|
|
133
|
+
New-Item -ItemType Directory -Path $TargetRoot -Force | Out-Null
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
|
138
|
+
Write-Err2 "需要 node 才能改写 opencode.json,请安装 Node.js >= 20"
|
|
139
|
+
exit 1
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Invoke-Step "write $cfgPath plugin entry: $uri" {
|
|
143
|
+
$env:CODEFORGE_CFG = $cfgPath
|
|
144
|
+
$env:CODEFORGE_URI = $uri
|
|
145
|
+
$nodeScript = @'
|
|
146
|
+
const fs = require("node:fs");
|
|
147
|
+
const path = process.env.CODEFORGE_CFG;
|
|
148
|
+
const uri = process.env.CODEFORGE_URI;
|
|
149
|
+
let cfg = {};
|
|
150
|
+
if (fs.existsSync(path)) {
|
|
151
|
+
try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); }
|
|
152
|
+
catch { fs.copyFileSync(path, path + ".bak." + Date.now()); cfg = {}; }
|
|
153
|
+
}
|
|
154
|
+
if (!cfg.$schema) cfg.$schema = "https://opencode.ai/config.json";
|
|
155
|
+
if (!Array.isArray(cfg.plugin)) cfg.plugin = [];
|
|
156
|
+
const cleaned = [];
|
|
157
|
+
for (const e of cfg.plugin) {
|
|
158
|
+
const s = String(e);
|
|
159
|
+
if (/\/codeforge\/index\.js$/.test(s)) continue;
|
|
160
|
+
if (/\/plugins\/[^/]+\.ts$/.test(s) && /opencode/.test(s)) continue;
|
|
161
|
+
if (/\/\.opencode\/plugins\//.test(s)) continue;
|
|
162
|
+
cleaned.push(e);
|
|
163
|
+
}
|
|
164
|
+
cleaned.push(uri);
|
|
165
|
+
cfg.plugin = cleaned;
|
|
166
|
+
fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
167
|
+
'@
|
|
168
|
+
& node -e $nodeScript
|
|
169
|
+
Remove-Item Env:\CODEFORGE_CFG -ErrorAction SilentlyContinue
|
|
170
|
+
Remove-Item Env:\CODEFORGE_URI -ErrorAction SilentlyContinue
|
|
171
|
+
}
|
|
172
|
+
Write-Ok "opencode.json 已写入 plugin entry: $uri"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function Remove-PluginEntry {
|
|
176
|
+
$cfgPath = Get-OpencodeConfigPath
|
|
177
|
+
if (-not (Test-Path -LiteralPath $cfgPath)) { return }
|
|
178
|
+
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
|
179
|
+
Write-Warn2 "未找到 node,跳过 opencode.json plugin entry 清理"
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
Invoke-Step "rewrite $cfgPath without codeforge plugin entry" {
|
|
184
|
+
$env:CODEFORGE_CFG = $cfgPath
|
|
185
|
+
$nodeScript = @'
|
|
186
|
+
const fs = require("node:fs");
|
|
187
|
+
const path = process.env.CODEFORGE_CFG;
|
|
188
|
+
let cfg;
|
|
189
|
+
try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); }
|
|
190
|
+
catch { return; }
|
|
191
|
+
if (!Array.isArray(cfg.plugin)) return;
|
|
192
|
+
cfg.plugin = cfg.plugin.filter(e => {
|
|
193
|
+
const s = String(e);
|
|
194
|
+
if (/\/codeforge\/index\.js$/.test(s)) return false;
|
|
195
|
+
if (/\/plugins\/[^/]+\.ts$/.test(s)) return false;
|
|
196
|
+
if (/\/\.opencode\/plugins\//.test(s)) return false;
|
|
197
|
+
return true;
|
|
198
|
+
});
|
|
199
|
+
fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
200
|
+
'@
|
|
201
|
+
& node -e $nodeScript
|
|
202
|
+
Remove-Item Env:\CODEFORGE_CFG -ErrorAction SilentlyContinue
|
|
203
|
+
}
|
|
204
|
+
Write-Ok "opencode.json 已移除 codeforge plugin entry"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# ────────────── KH 全局模板(仅 -Global) ──────────────
|
|
208
|
+
#
|
|
209
|
+
# 硬约束 #2:API key 绝不能落盘,模板里**不写** token / apiKey 字段。
|
|
210
|
+
# 用户必须通过环境变量 KNOWLEDGE_API_KEY 提供。
|
|
211
|
+
# 已存在则跳过,避免覆盖用户自定义。
|
|
212
|
+
function Write-KhTemplate {
|
|
213
|
+
$khFile = Join-Path $CodeforgeCfgDir 'kh.json'
|
|
214
|
+
if (-not (Test-Path -LiteralPath $CodeforgeCfgDir)) {
|
|
215
|
+
Invoke-Step "mkdir $CodeforgeCfgDir" {
|
|
216
|
+
New-Item -ItemType Directory -Path $CodeforgeCfgDir -Force | Out-Null
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (Test-Path -LiteralPath $khFile) {
|
|
220
|
+
Write-Ok "已存在 $khFile,跳过覆盖"
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
Invoke-Step "write $khFile (no token)" {
|
|
224
|
+
$body = @"
|
|
225
|
+
{
|
|
226
|
+
"url": "http://10.5.60.26:8900/mcp",
|
|
227
|
+
"timeoutMs": 5000,
|
|
228
|
+
"maxRetries": 1
|
|
229
|
+
}
|
|
230
|
+
"@
|
|
231
|
+
# 用 .NET WriteAllText 强制无 BOM 的 UTF8(PowerShell 5.1 默认 BOM 会被 KhConfig 容错,但保持干净)
|
|
232
|
+
[System.IO.File]::WriteAllText($khFile, $body, (New-Object System.Text.UTF8Encoding $false))
|
|
233
|
+
}
|
|
234
|
+
Write-Ok "已生成 $khFile(API key 请通过环境变量 KNOWLEDGE_API_KEY 提供,禁止写入文件)"
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# ────────────── AGENTS.md 智能合并(B3 新增) ──────────────
|
|
238
|
+
#
|
|
239
|
+
# 实现委托给 scripts/merge-agents-md.mjs CLI(C.4 单一入口),
|
|
240
|
+
# 算法源自 lib/agents-merge.ts,sh/ps1 共用同一份 Node 实现,
|
|
241
|
+
# 避免 install 脚本各自维护算法副本造成漂移。
|
|
242
|
+
function Merge-ProjectAgentsMd {
|
|
243
|
+
param(
|
|
244
|
+
[Parameter(Mandatory=$true)] [string] $AgentsTarget,
|
|
245
|
+
[Parameter(Mandatory=$true)] [string] $TemplatePath
|
|
246
|
+
)
|
|
247
|
+
$cliPath = Join-Path $SourceRoot 'scripts/merge-agents-md.mjs'
|
|
248
|
+
if (-not (Test-Path -LiteralPath $TemplatePath)) {
|
|
249
|
+
Write-Warn2 "模板不存在,跳过 AGENTS.md 合并: $TemplatePath"
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
if (-not (Test-Path -LiteralPath $cliPath)) {
|
|
253
|
+
Write-Warn2 "merge CLI 不存在,跳过 AGENTS.md 智能合并: $cliPath"
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
|
257
|
+
Write-Warn2 "未找到 node,跳过 AGENTS.md 智能合并(请安装 Node.js >= 20)"
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
$cliArgs = @('--target', $AgentsTarget, '--template', $TemplatePath)
|
|
261
|
+
if ($DryRun) { $cliArgs += '--dry-run' }
|
|
262
|
+
try {
|
|
263
|
+
& node $cliPath @cliArgs
|
|
264
|
+
} catch {
|
|
265
|
+
Write-Warn2 "AGENTS.md 合并失败(不阻塞 install 主流程): $_"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# ────────────── 卸载 ──────────────
|
|
270
|
+
function Invoke-Uninstall {
|
|
271
|
+
Write-Section "卸载 CodeForge from: $TargetRoot"
|
|
272
|
+
Remove-PluginEntry
|
|
273
|
+
$candidates = $LegacyDirs + $ManagedDirs | Select-Object -Unique
|
|
274
|
+
foreach ($name in $candidates) {
|
|
275
|
+
$path = Join-Path $TargetRoot $name
|
|
276
|
+
if (Test-Path -LiteralPath $path) {
|
|
277
|
+
Invoke-Step "remove $path" {
|
|
278
|
+
# Junction 不能用 Remove-Item 直接删(会删源),需要先 rmdir
|
|
279
|
+
$item = Get-Item -LiteralPath $path -Force
|
|
280
|
+
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
|
281
|
+
[IO.Directory]::Delete($path, $false)
|
|
282
|
+
} else {
|
|
283
|
+
Remove-Item -LiteralPath $path -Recurse -Force
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
Write-Ok "已删除 $path"
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
# 细粒度删除 skills:只删 CodeForge 自己的 skill,不删用户自装的
|
|
290
|
+
$ownedSkills = @('ambiguity-gate', 'devils-advocate', 'ears-zh', 'example-mapping', 'success-criteria', 'weighted-dimensions')
|
|
291
|
+
foreach ($skillName in $ownedSkills) {
|
|
292
|
+
$skillPath = Join-Path $TargetRoot "skills\$skillName"
|
|
293
|
+
if (Test-Path -LiteralPath $skillPath) {
|
|
294
|
+
Invoke-Step "remove skill $skillName" {
|
|
295
|
+
Remove-Item -LiteralPath $skillPath -Recurse -Force
|
|
296
|
+
}
|
|
297
|
+
Write-Ok "已删除 skill: $skillPath"
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
301
|
+
Write-Ok "卸载完成(opencode 自身、AGENTS.md、~/.config/codeforge/kh.json 不会被动)"
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# ────────────── 环境检测 ──────────────
|
|
305
|
+
function Test-Opencode {
|
|
306
|
+
$cmd = Get-Command opencode -ErrorAction SilentlyContinue
|
|
307
|
+
if ($cmd) {
|
|
308
|
+
try {
|
|
309
|
+
$v = & opencode --version 2>$null
|
|
310
|
+
Write-Ok "检测到 opencode: $v"
|
|
311
|
+
} catch {
|
|
312
|
+
Write-Ok "检测到 opencode (版本未知)"
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
Write-Warn2 "未检测到 opencode CLI"
|
|
316
|
+
Write-Warn2 " 安装方式:https://github.com/sst/opencode#installation"
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function Test-KhMcp {
|
|
321
|
+
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' }
|
|
322
|
+
$cfg = Join-Path (Join-Path $xdg 'opencode') 'opencode.json'
|
|
323
|
+
if ((Test-Path -LiteralPath $cfg) -and ((Get-Content -LiteralPath $cfg -Raw -Encoding UTF8) -match 'knowledge-hub|code-forge-knowledge-hub')) {
|
|
324
|
+
Write-Ok "检测到 Knowledge Hub MCP 已注册"
|
|
325
|
+
} else {
|
|
326
|
+
Write-Warn2 "未在 $cfg 中找到 Knowledge Hub MCP"
|
|
327
|
+
Write-Warn2 " 配置示例见 docs/PRD.md §6.2"
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# ────────────── 主流程 ──────────────
|
|
332
|
+
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
333
|
+
Write-Host "[codeforge] CodeForge installer (mode=$Mode, uninstall=$Uninstall, dry-run=$DryRun)" -ForegroundColor Cyan
|
|
334
|
+
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
335
|
+
Write-Host "Source : $SourceRoot"
|
|
336
|
+
Write-Host "Target : $TargetRoot"
|
|
337
|
+
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
338
|
+
|
|
339
|
+
if ($Uninstall) {
|
|
340
|
+
Invoke-Uninstall
|
|
341
|
+
exit 0
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# Step 1/7: 环境检测
|
|
345
|
+
Write-Section 'Step 1/7: 环境检测'
|
|
346
|
+
Test-Opencode
|
|
347
|
+
Test-KhMcp
|
|
348
|
+
|
|
349
|
+
# Step 2/7: build dist bundle
|
|
350
|
+
Write-Section 'Step 2/7: 构建 dist/index.js 单 bundle'
|
|
351
|
+
$bundleSrc = Join-Path $SourceRoot $BundleSrcRel
|
|
352
|
+
if ($SkipBuild) {
|
|
353
|
+
Write-Warn2 '已跳过 build(-SkipBuild),使用现有 dist/index.js'
|
|
354
|
+
} else {
|
|
355
|
+
Invoke-Step "npm run build" {
|
|
356
|
+
Push-Location $SourceRoot
|
|
357
|
+
try {
|
|
358
|
+
$out = npm run build 2>&1 | Out-String
|
|
359
|
+
if ($LASTEXITCODE -ne 0) {
|
|
360
|
+
Write-Err2 'npm run build 失败'
|
|
361
|
+
Write-Host $out
|
|
362
|
+
exit 1
|
|
363
|
+
}
|
|
364
|
+
} finally {
|
|
365
|
+
Pop-Location
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (-not (Test-Path -LiteralPath $bundleSrc)) {
|
|
370
|
+
if ($SkipBuild) {
|
|
371
|
+
Write-Err2 "找不到 $bundleSrc(npm 包可能损坏)"
|
|
372
|
+
Write-Err2 " 请尝试重装:npx @andyqiu/codeforge install [--global]"
|
|
373
|
+
} else {
|
|
374
|
+
Write-Err2 "找不到 $bundleSrc,请先成功执行 npm run build"
|
|
375
|
+
}
|
|
376
|
+
exit 1
|
|
377
|
+
}
|
|
378
|
+
$bundleSize = (Get-Item -LiteralPath $bundleSrc).Length
|
|
379
|
+
Write-Ok "bundle 已就绪: $bundleSrc ($([math]::Round($bundleSize/1024,1)) KB)"
|
|
380
|
+
|
|
381
|
+
# Step 3/7: 准备目标目录
|
|
382
|
+
Write-Section 'Step 3/7: 准备目标目录'
|
|
383
|
+
if (-not (Test-Path -LiteralPath $TargetRoot)) {
|
|
384
|
+
Invoke-Step "mkdir $TargetRoot" {
|
|
385
|
+
New-Item -ItemType Directory -Path $TargetRoot -Force | Out-Null
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
# 清理 legacy 单数目录 + 老的 17 个 plugin / lib / tools junction
|
|
389
|
+
foreach ($legacy in $LegacyDirs) {
|
|
390
|
+
$dst = Join-Path $TargetRoot $legacy
|
|
391
|
+
if (Test-Path -LiteralPath $dst) {
|
|
392
|
+
Invoke-Step "cleanup legacy $dst" {
|
|
393
|
+
$item = Get-Item -LiteralPath $dst -Force
|
|
394
|
+
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
|
395
|
+
[IO.Directory]::Delete($dst, $false)
|
|
396
|
+
} else {
|
|
397
|
+
Remove-Item -LiteralPath $dst -Recurse -Force
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
Write-Warn2 "已清理 legacy 目录: $dst"
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# Step 4/7: 装 bundle 到 codeforge/index.js
|
|
405
|
+
Write-Section 'Step 4/7: 装入 dist/index.js bundle'
|
|
406
|
+
$bundleDst = Join-Path $TargetRoot $BundleDstRel
|
|
407
|
+
$bundleDstDir = Split-Path -Parent $bundleDst
|
|
408
|
+
if (-not (Test-Path -LiteralPath $bundleDstDir)) {
|
|
409
|
+
Invoke-Step "mkdir $bundleDstDir" {
|
|
410
|
+
New-Item -ItemType Directory -Path $bundleDstDir -Force | Out-Null
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
Invoke-Step "copy $BundleSrcRel -> $BundleDstRel" {
|
|
414
|
+
Copy-Item -LiteralPath $bundleSrc -Destination $bundleDst -Force
|
|
415
|
+
}
|
|
416
|
+
Write-Ok "bundle -> $bundleDst"
|
|
417
|
+
Add-PluginEntry
|
|
418
|
+
|
|
419
|
+
# 写 VERSION marker 文件(用户 cat 一行查版本,不依赖 grep bundle)
|
|
420
|
+
$cfVersion = "unknown"
|
|
421
|
+
try {
|
|
422
|
+
$pkgPath = Join-Path $SourceRoot "package.json"
|
|
423
|
+
if (Test-Path -LiteralPath $pkgPath) {
|
|
424
|
+
$pkg = Get-Content -LiteralPath $pkgPath -Raw | ConvertFrom-Json
|
|
425
|
+
if ($pkg.version) { $cfVersion = $pkg.version }
|
|
426
|
+
}
|
|
427
|
+
} catch { $cfVersion = "unknown" }
|
|
428
|
+
$versionFile = Join-Path $TargetRoot "codeforge/VERSION"
|
|
429
|
+
if (-not $DryRun) {
|
|
430
|
+
Set-Content -LiteralPath $versionFile -Value $cfVersion -NoNewline:$false -Encoding utf8
|
|
431
|
+
}
|
|
432
|
+
Write-Ok "VERSION -> $versionFile ($cfVersion)"
|
|
433
|
+
|
|
434
|
+
# Step 5/7: 装 agents / commands / workflows / context-templates / review-profiles
|
|
435
|
+
Write-Section 'Step 5/7: 装 agents / commands / workflows / context-templates / review-profiles'
|
|
436
|
+
foreach ($entry in $MdCopyMap) {
|
|
437
|
+
$src = Join-Path $SourceRoot $entry.Src
|
|
438
|
+
$dst = Join-Path $TargetRoot $entry.Dst
|
|
439
|
+
if (-not (Test-Path -LiteralPath $src)) {
|
|
440
|
+
Write-Warn2 "源目录不存在,跳过: $src"
|
|
441
|
+
continue
|
|
442
|
+
}
|
|
443
|
+
if (Test-Path -LiteralPath $dst) {
|
|
444
|
+
Invoke-Step "cleanup old $dst" {
|
|
445
|
+
$item = Get-Item -LiteralPath $dst -Force
|
|
446
|
+
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
|
447
|
+
[IO.Directory]::Delete($dst, $false)
|
|
448
|
+
} else {
|
|
449
|
+
Remove-Item -LiteralPath $dst -Recurse -Force
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
Invoke-Step "mkdir $dst" {
|
|
454
|
+
New-Item -ItemType Directory -Path $dst -Force | Out-Null
|
|
455
|
+
}
|
|
456
|
+
$files = Get-ChildItem -LiteralPath $src -Filter '*.md' -File | Where-Object {
|
|
457
|
+
$_.Name -ne 'README.md' -and
|
|
458
|
+
$_.Name -notmatch '^_' -and
|
|
459
|
+
$_.Name -notmatch '\.bak$' -and
|
|
460
|
+
$_.Name -notmatch '^\.'
|
|
461
|
+
}
|
|
462
|
+
foreach ($f in $files) {
|
|
463
|
+
Invoke-Step "copy $($entry.Src)/$($f.Name)" {
|
|
464
|
+
Copy-Item -LiteralPath $f.FullName -Destination (Join-Path $dst $f.Name) -Force
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
Write-Ok "$($entry.Src)/ -> $dst ($($files.Count) 个 .md)"
|
|
468
|
+
}
|
|
469
|
+
foreach ($entry in $CopyMap) {
|
|
470
|
+
$src = Join-Path $SourceRoot $entry.Src
|
|
471
|
+
$dst = Join-Path $TargetRoot $entry.Dst
|
|
472
|
+
if (-not (Test-Path -LiteralPath $src)) {
|
|
473
|
+
Write-Warn2 "源目录不存在,跳过: $src"
|
|
474
|
+
continue
|
|
475
|
+
}
|
|
476
|
+
Invoke-Step "copy $($entry.Src)/ -> $dst" {
|
|
477
|
+
if (-not (Test-Path -LiteralPath $dst)) {
|
|
478
|
+
New-Item -ItemType Directory -Path $dst -Force | Out-Null
|
|
479
|
+
}
|
|
480
|
+
Copy-Item -Path (Join-Path $src '*') -Destination $dst -Recurse -Force
|
|
481
|
+
}
|
|
482
|
+
Write-Ok "$($entry.Src)/ -> $dst (整目录拷贝)"
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# Step 5b/7: 装 skills/(opencode skill 目录)
|
|
486
|
+
Write-Section 'Step 5b/7: 装 skills/'
|
|
487
|
+
$skillsSrc = Join-Path $SourceRoot 'skills'
|
|
488
|
+
$skillsDst = Join-Path $TargetRoot 'skills'
|
|
489
|
+
if (Test-Path -LiteralPath $skillsSrc) {
|
|
490
|
+
if (-not (Test-Path -LiteralPath $skillsDst)) {
|
|
491
|
+
New-Item -ItemType Directory -Path $skillsDst -Force | Out-Null
|
|
492
|
+
}
|
|
493
|
+
$skillDirs = Get-ChildItem -LiteralPath $skillsSrc -Directory
|
|
494
|
+
$skillCount = 0
|
|
495
|
+
foreach ($skillDir in $skillDirs) {
|
|
496
|
+
$dstSkill = Join-Path $skillsDst $skillDir.Name
|
|
497
|
+
if (Test-Path -LiteralPath $dstSkill) {
|
|
498
|
+
Invoke-Step "remove old skill $($skillDir.Name)" {
|
|
499
|
+
Remove-Item -LiteralPath $dstSkill -Recurse -Force
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
Invoke-Step "copy skill $($skillDir.Name)" {
|
|
503
|
+
Copy-Item -LiteralPath $skillDir.FullName -Destination $dstSkill -Recurse -Force
|
|
504
|
+
}
|
|
505
|
+
$skillCount++
|
|
506
|
+
}
|
|
507
|
+
Write-Ok "skills/ -> $skillsDst ($skillCount 个 skill)"
|
|
508
|
+
} else {
|
|
509
|
+
Write-Warn2 "skills/ 目录不存在,跳过(发布包未含 skills)"
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
# Step 6/7: AGENTS.md 智能合并(仅项目模式)
|
|
513
|
+
Write-Section 'Step 6/7: AGENTS.md 智能合并'
|
|
514
|
+
if ($Mode -eq 'project') {
|
|
515
|
+
$agentsTarget = Join-Path (Get-Location).Path 'AGENTS.md'
|
|
516
|
+
$templatePath = Join-Path $SourceRoot $KhTemplateRel
|
|
517
|
+
Merge-ProjectAgentsMd -AgentsTarget $agentsTarget -TemplatePath $templatePath
|
|
518
|
+
} else {
|
|
519
|
+
Write-Ok "全局模式:跳过项目 AGENTS.md 合并(context-templates 已装到 $TargetRoot\context-templates)"
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
# Step 7/7: KH 全局配置模板(仅 -Global)
|
|
523
|
+
Write-Section 'Step 7/7: KH 全局配置模板'
|
|
524
|
+
if ($Global) {
|
|
525
|
+
Write-KhTemplate
|
|
526
|
+
} else {
|
|
527
|
+
Write-Ok "项目级安装,跳过 KH 全局模板(要装请加 -Global)"
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
# 验证清单
|
|
531
|
+
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
532
|
+
Write-Ok "CodeForge v$cfVersion 安装完成"
|
|
533
|
+
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
|
534
|
+
Write-Host @"
|
|
535
|
+
验证清单:
|
|
536
|
+
1. 列出注入的扩展点:
|
|
537
|
+
PS> Get-ChildItem '$TargetRoot' -Force
|
|
538
|
+
2. 看 opencode.json 里 plugin entry:
|
|
539
|
+
PS> Get-Content '$TargetRoot\opencode.json'
|
|
540
|
+
3. 让 opencode 校验配置:
|
|
541
|
+
PS> opencode debug config
|
|
542
|
+
-> 期望看到 1 个 codeforge plugin entry(不是 17 个)
|
|
543
|
+
4. 跑一次最小 dogfood,看 plugin import + activation 日志:
|
|
544
|
+
PS> opencode run "hello"
|
|
545
|
+
PS> Get-Content `$HOME\.cache\codeforge\plugins.log -Tail 50
|
|
546
|
+
5. KH 配置(仅 -Global 安装时生成):
|
|
547
|
+
PS> Get-Content '$CodeforgeCfgDir\kh.json'
|
|
548
|
+
PS> `$env:KNOWLEDGE_API_KEY = '<your-token>' # ← API key 必须走环境变量
|
|
549
|
+
6. AGENTS.md 智能合并(仅项目级):
|
|
550
|
+
- 已有 AGENTS.md → KH 块被替换成模板内容,原文件备份成 *.bak.<timestamp>
|
|
551
|
+
- 模板源:$SourceRoot\$KhTemplateRel(修改后请重新 install)
|
|
552
|
+
|
|
553
|
+
卸载:
|
|
554
|
+
PS> .\install.ps1 -Uninstall$(if ($Global) { ' -Global' })
|
|
555
|
+
"@
|
|
556
|
+
Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
|
package/install.sh
CHANGED
|
File without changes
|