@andyqiu/codeforge 0.3.12 → 0.3.13

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/install.ps1 CHANGED
@@ -1,516 +1,555 @@
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 拷贝过去
17
- 6. 智能合并 AGENTS.md:
18
- - 项目模式 + 已有 AGENTS.md → 替换 <!-- knowledge-hub:start -->...<!-- knowledge-hub:end --> 块(带 .bak 备份)
19
- - 项目模式 + AGENTS.md 生成含 marker 块的短版骨架
20
- - 全局模式 跳过(context-templates 已经放进 ~/.config/opencode/context-templates/ 给所有项目复用)
21
- 7. -Global 时生成 ~/.config/codeforge/kh.json 模板(不含 token,硬约束 #2)
22
- 8. 输出验证清单
23
-
24
- .PARAMETER Global
25
- 装到全局 (~/.config/opencode/),默认装到当前项目 (.opencode/)
26
-
27
- .PARAMETER Uninstall
28
- 卸载 CodeForge 注入物(不动 opencode 自身和 AGENTS.md,会清掉 opencode.json 里的 plugin entry)
29
-
30
- .PARAMETER DryRun
31
- 仅打印将要执行的操作,不真正执行
32
-
33
- .PARAMETER SkipBuild
34
- 跳过 npm run build。默认每次安装前会先 build 确保 dist 是最新的,加 -SkipBuild 跳过
35
-
36
- .EXAMPLE
37
- .\install.ps1
38
- .\install.ps1 -Global
39
- .\install.ps1 -Uninstall
40
- .\install.ps1 -DryRun
41
- .\install.ps1 -SkipBuild
42
- #>
43
- [CmdletBinding()]
44
- param(
45
- [switch]$Global,
46
- [switch]$Uninstall,
47
- [switch]$DryRun,
48
- [switch]$SkipBuild
49
- )
50
-
51
- $ErrorActionPreference = 'Stop'
52
-
53
- # ────────────── 工具函数 ──────────────
54
- function Write-Section([string]$Text) {
55
- Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
56
- Write-Host "[codeforge] $Text" -ForegroundColor Cyan
57
- }
58
- function Write-Ok([string]$Text) { Write-Host "$([char]0x2713) $Text" -ForegroundColor Green }
59
- function Write-Warn2([string]$Text) { Write-Host "$([char]0x26A0) $Text" -ForegroundColor Yellow }
60
- function Write-Err2([string]$Text) { Write-Host "$([char]0x2717) $Text" -ForegroundColor Red }
61
-
62
- function Invoke-Step([string]$Description, [scriptblock]$Action) {
63
- if ($DryRun) {
64
- Write-Host " [dry-run] $Description" -ForegroundColor Blue
65
- } else {
66
- & $Action
67
- }
68
- }
69
-
70
- # ────────────── 路径解析 ──────────────
71
- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
72
- $SourceRoot = $ScriptDir
73
-
74
- if ($Global) {
75
- $XdgConfig = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' }
76
- $TargetRoot = Join-Path $XdgConfig 'opencode'
77
- $Mode = 'global'
78
- } else {
79
- $TargetRoot = Join-Path (Get-Location).Path '.opencode'
80
- $Mode = 'project'
81
- }
82
-
83
- # CodeForge 自身的全局配置目录(与 opencode 配置目录同级)
84
- $XdgConfigBase = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' }
85
- $CodeforgeCfgDir = Join-Path $XdgConfigBase 'codeforge'
86
-
87
- # v0.1 之前 install.ps1 装的目录(卸载时一并清掉)
88
- $LegacyDirs = @('agent', 'command', 'tool', 'tools', 'plugin', 'plugins', 'lib')
89
- # v0.1+ 才有的目录
90
- $ManagedDirs = @('codeforge', 'agents', 'commands', 'workflows', 'context-templates')
91
-
92
- # v0.1+ 文件分发计划
93
- $BundleSrcRel = 'dist/index.js'
94
- $BundleDstRel = 'codeforge/index.js' # 相对 $TargetRoot
95
-
96
- # B3 KH 行为规范模板(智能合并的输入)
97
- $KhTemplateRel = 'context-templates/kh-instructions.md'
98
-
99
- # .md file-by-file copy(白名单 *.md,排除 README/_*/.bak)
100
- $MdCopyMap = @(
101
- @{ Src='agents'; Dst='agents' },
102
- @{ Src='commands'; Dst='commands' }
103
- )
104
- # 普通整目录 copy
105
- $CopyMap = @(
106
- @{ Src='workflows'; Dst='workflows' },
107
- @{ Src='context-templates'; Dst='context-templates' }
108
- )
109
-
110
- # ────────────── opencode.json 管理 ──────────────
111
- function Get-OpencodeConfigPath {
112
- Join-Path $TargetRoot 'opencode.json'
113
- }
114
-
115
- function Get-PluginUri {
116
- # opencode 接受 file:// URL 也接受裸绝对路径,统一用 file:// 更明确
117
- $abs = Join-Path $TargetRoot $BundleDstRel
118
- $abs = $abs -replace '\\', '/'
119
- if ($abs -notmatch '^[A-Za-z]:/') {
120
- return "file://$abs"
121
- }
122
- return "file:///$abs"
123
- }
124
-
125
- function Add-PluginEntry {
126
- $cfgPath = Get-OpencodeConfigPath
127
- $uri = Get-PluginUri
128
-
129
- if (-not (Test-Path -LiteralPath $TargetRoot)) {
130
- Invoke-Step "mkdir $TargetRoot" {
131
- New-Item -ItemType Directory -Path $TargetRoot -Force | Out-Null
132
- }
133
- }
134
-
135
- if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
136
- Write-Err2 "需要 node 才能改写 opencode.json,请安装 Node.js >= 20"
137
- exit 1
138
- }
139
-
140
- Invoke-Step "write $cfgPath plugin entry: $uri" {
141
- $env:CODEFORGE_CFG = $cfgPath
142
- $env:CODEFORGE_URI = $uri
143
- $nodeScript = @'
144
- const fs = require("node:fs");
145
- const path = process.env.CODEFORGE_CFG;
146
- const uri = process.env.CODEFORGE_URI;
147
- let cfg = {};
148
- if (fs.existsSync(path)) {
149
- try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); }
150
- catch { fs.copyFileSync(path, path + ".bak." + Date.now()); cfg = {}; }
151
- }
152
- if (!cfg.$schema) cfg.$schema = "https://opencode.ai/config.json";
153
- if (!Array.isArray(cfg.plugin)) cfg.plugin = [];
154
- const cleaned = [];
155
- for (const e of cfg.plugin) {
156
- const s = String(e);
157
- if (/\/codeforge\/index\.js$/.test(s)) continue;
158
- if (/\/plugins\/[^/]+\.ts$/.test(s) && /opencode/.test(s)) continue;
159
- if (/\/\.opencode\/plugins\//.test(s)) continue;
160
- cleaned.push(e);
161
- }
162
- cleaned.push(uri);
163
- cfg.plugin = cleaned;
164
- fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
165
- '@
166
- & node -e $nodeScript
167
- Remove-Item Env:\CODEFORGE_CFG -ErrorAction SilentlyContinue
168
- Remove-Item Env:\CODEFORGE_URI -ErrorAction SilentlyContinue
169
- }
170
- Write-Ok "opencode.json 已写入 plugin entry: $uri"
171
- }
172
-
173
- function Remove-PluginEntry {
174
- $cfgPath = Get-OpencodeConfigPath
175
- if (-not (Test-Path -LiteralPath $cfgPath)) { return }
176
- if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
177
- Write-Warn2 "未找到 node,跳过 opencode.json plugin entry 清理"
178
- return
179
- }
180
-
181
- Invoke-Step "rewrite $cfgPath without codeforge plugin entry" {
182
- $env:CODEFORGE_CFG = $cfgPath
183
- $nodeScript = @'
184
- const fs = require("node:fs");
185
- const path = process.env.CODEFORGE_CFG;
186
- let cfg;
187
- try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); }
188
- catch { return; }
189
- if (!Array.isArray(cfg.plugin)) return;
190
- cfg.plugin = cfg.plugin.filter(e => {
191
- const s = String(e);
192
- if (/\/codeforge\/index\.js$/.test(s)) return false;
193
- if (/\/plugins\/[^/]+\.ts$/.test(s)) return false;
194
- if (/\/\.opencode\/plugins\//.test(s)) return false;
195
- return true;
196
- });
197
- fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
198
- '@
199
- & node -e $nodeScript
200
- Remove-Item Env:\CODEFORGE_CFG -ErrorAction SilentlyContinue
201
- }
202
- Write-Ok "opencode.json 已移除 codeforge plugin entry"
203
- }
204
-
205
- # ────────────── KH 全局模板(仅 -Global) ──────────────
206
- #
207
- # 硬约束 #2:API key 绝不能落盘,模板里**不写** token / apiKey 字段。
208
- # 用户必须通过环境变量 KNOWLEDGE_API_KEY 提供。
209
- # 已存在则跳过,避免覆盖用户自定义。
210
- function Write-KhTemplate {
211
- $khFile = Join-Path $CodeforgeCfgDir 'kh.json'
212
- if (-not (Test-Path -LiteralPath $CodeforgeCfgDir)) {
213
- Invoke-Step "mkdir $CodeforgeCfgDir" {
214
- New-Item -ItemType Directory -Path $CodeforgeCfgDir -Force | Out-Null
215
- }
216
- }
217
- if (Test-Path -LiteralPath $khFile) {
218
- Write-Ok "已存在 $khFile,跳过覆盖"
219
- return
220
- }
221
- Invoke-Step "write $khFile (no token)" {
222
- $body = @"
223
- {
224
- "url": "http://10.5.60.26:8900/mcp",
225
- "timeoutMs": 5000,
226
- "maxRetries": 1
227
- }
228
- "@
229
- # 用 .NET WriteAllText 强制无 BOM 的 UTF8(PowerShell 5.1 默认 BOM 会被 KhConfig 容错,但保持干净)
230
- [System.IO.File]::WriteAllText($khFile, $body, (New-Object System.Text.UTF8Encoding $false))
231
- }
232
- Write-Ok "已生成 $khFile(API key 请通过环境变量 KNOWLEDGE_API_KEY 提供,禁止写入文件)"
233
- }
234
-
235
- # ────────────── AGENTS.md 智能合并(B3 新增) ──────────────
236
- #
237
- # 实现委托给 scripts/merge-agents-md.mjs CLI(C.4 单一入口),
238
- # 算法源自 lib/agents-merge.ts,sh/ps1 共用同一份 Node 实现,
239
- # 避免 install 脚本各自维护算法副本造成漂移。
240
- function Merge-ProjectAgentsMd {
241
- param(
242
- [Parameter(Mandatory=$true)] [string] $AgentsTarget,
243
- [Parameter(Mandatory=$true)] [string] $TemplatePath
244
- )
245
- $cliPath = Join-Path $SourceRoot 'scripts/merge-agents-md.mjs'
246
- if (-not (Test-Path -LiteralPath $TemplatePath)) {
247
- Write-Warn2 "模板不存在,跳过 AGENTS.md 合并: $TemplatePath"
248
- return
249
- }
250
- if (-not (Test-Path -LiteralPath $cliPath)) {
251
- Write-Warn2 "merge CLI 不存在,跳过 AGENTS.md 智能合并: $cliPath"
252
- return
253
- }
254
- if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
255
- Write-Warn2 "未找到 node,跳过 AGENTS.md 智能合并(请安装 Node.js >= 20)"
256
- return
257
- }
258
- $cliArgs = @('--target', $AgentsTarget, '--template', $TemplatePath)
259
- if ($DryRun) { $cliArgs += '--dry-run' }
260
- try {
261
- & node $cliPath @cliArgs
262
- } catch {
263
- Write-Warn2 "AGENTS.md 合并失败(不阻塞 install 主流程): $_"
264
- }
265
- }
266
-
267
- # ────────────── 卸载 ──────────────
268
- function Invoke-Uninstall {
269
- Write-Section "卸载 CodeForge from: $TargetRoot"
270
- Remove-PluginEntry
271
- $candidates = $LegacyDirs + $ManagedDirs | Select-Object -Unique
272
- foreach ($name in $candidates) {
273
- $path = Join-Path $TargetRoot $name
274
- if (Test-Path -LiteralPath $path) {
275
- Invoke-Step "remove $path" {
276
- # Junction 不能用 Remove-Item 直接删(会删源),需要先 rmdir
277
- $item = Get-Item -LiteralPath $path -Force
278
- if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
279
- [IO.Directory]::Delete($path, $false)
280
- } else {
281
- Remove-Item -LiteralPath $path -Recurse -Force
282
- }
283
- }
284
- Write-Ok "已删除 $path"
285
- }
286
- }
287
- Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
288
- Write-Ok "卸载完成(opencode 自身、AGENTS.md、~/.config/codeforge/kh.json 不会被动)"
289
- }
290
-
291
- # ────────────── 环境检测 ──────────────
292
- function Test-Opencode {
293
- $cmd = Get-Command opencode -ErrorAction SilentlyContinue
294
- if ($cmd) {
295
- try {
296
- $v = & opencode --version 2>$null
297
- Write-Ok "检测到 opencode: $v"
298
- } catch {
299
- Write-Ok "检测到 opencode (版本未知)"
300
- }
301
- } else {
302
- Write-Warn2 "未检测到 opencode CLI"
303
- Write-Warn2 " 安装方式:https://github.com/sst/opencode#installation"
304
- }
305
- }
306
-
307
- function Test-KhMcp {
308
- $xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' }
309
- $cfg = Join-Path (Join-Path $xdg 'opencode') 'opencode.json'
310
- if ((Test-Path -LiteralPath $cfg) -and ((Get-Content -LiteralPath $cfg -Raw -Encoding UTF8) -match 'knowledge-hub|code-forge-knowledge-hub')) {
311
- Write-Ok "检测到 Knowledge Hub MCP 已注册"
312
- } else {
313
- Write-Warn2 "未在 $cfg 中找到 Knowledge Hub MCP"
314
- Write-Warn2 " 配置示例见 docs/PRD.md §6.2"
315
- }
316
- }
317
-
318
- # ────────────── 主流程 ──────────────
319
- Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
320
- Write-Host "[codeforge] CodeForge installer (mode=$Mode, uninstall=$Uninstall, dry-run=$DryRun)" -ForegroundColor Cyan
321
- Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
322
- Write-Host "Source : $SourceRoot"
323
- Write-Host "Target : $TargetRoot"
324
- Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
325
-
326
- if ($Uninstall) {
327
- Invoke-Uninstall
328
- exit 0
329
- }
330
-
331
- # Step 1/7: 环境检测
332
- Write-Section 'Step 1/7: 环境检测'
333
- Test-Opencode
334
- Test-KhMcp
335
-
336
- # Step 2/7: build dist bundle
337
- Write-Section 'Step 2/7: 构建 dist/index.js 单 bundle'
338
- $bundleSrc = Join-Path $SourceRoot $BundleSrcRel
339
- if ($SkipBuild) {
340
- Write-Warn2 '已跳过 build(-SkipBuild),使用现有 dist/index.js'
341
- } else {
342
- Invoke-Step "npm run build" {
343
- Push-Location $SourceRoot
344
- try {
345
- $out = npm run build 2>&1 | Out-String
346
- if ($LASTEXITCODE -ne 0) {
347
- Write-Err2 'npm run build 失败'
348
- Write-Host $out
349
- exit 1
350
- }
351
- } finally {
352
- Pop-Location
353
- }
354
- }
355
- }
356
- if (-not (Test-Path -LiteralPath $bundleSrc)) {
357
- if ($SkipBuild) {
358
- Write-Err2 "找不到 $bundleSrc(npm 包可能损坏)"
359
- Write-Err2 " 请尝试重装:npx @andyqiu/codeforge install [--global]"
360
- } else {
361
- Write-Err2 "找不到 $bundleSrc,请先成功执行 npm run build"
362
- }
363
- exit 1
364
- }
365
- $bundleSize = (Get-Item -LiteralPath $bundleSrc).Length
366
- Write-Ok "bundle 已就绪: $bundleSrc ($([math]::Round($bundleSize/1024,1)) KB)"
367
-
368
- # Step 3/7: 准备目标目录
369
- Write-Section 'Step 3/7: 准备目标目录'
370
- if (-not (Test-Path -LiteralPath $TargetRoot)) {
371
- Invoke-Step "mkdir $TargetRoot" {
372
- New-Item -ItemType Directory -Path $TargetRoot -Force | Out-Null
373
- }
374
- }
375
- # 清理 legacy 单数目录 + 老的 17 个 plugin / lib / tools junction
376
- foreach ($legacy in $LegacyDirs) {
377
- $dst = Join-Path $TargetRoot $legacy
378
- if (Test-Path -LiteralPath $dst) {
379
- Invoke-Step "cleanup legacy $dst" {
380
- $item = Get-Item -LiteralPath $dst -Force
381
- if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
382
- [IO.Directory]::Delete($dst, $false)
383
- } else {
384
- Remove-Item -LiteralPath $dst -Recurse -Force
385
- }
386
- }
387
- Write-Warn2 "已清理 legacy 目录: $dst"
388
- }
389
- }
390
-
391
- # Step 4/7: bundle 到 codeforge/index.js
392
- Write-Section 'Step 4/7: 装入 dist/index.js bundle'
393
- $bundleDst = Join-Path $TargetRoot $BundleDstRel
394
- $bundleDstDir = Split-Path -Parent $bundleDst
395
- if (-not (Test-Path -LiteralPath $bundleDstDir)) {
396
- Invoke-Step "mkdir $bundleDstDir" {
397
- New-Item -ItemType Directory -Path $bundleDstDir -Force | Out-Null
398
- }
399
- }
400
- Invoke-Step "copy $BundleSrcRel -> $BundleDstRel" {
401
- Copy-Item -LiteralPath $bundleSrc -Destination $bundleDst -Force
402
- }
403
- Write-Ok "bundle -> $bundleDst"
404
- Add-PluginEntry
405
-
406
- # VERSION marker 文件(用户 cat 一行查版本,不依赖 grep bundle)
407
- $cfVersion = "unknown"
408
- try {
409
- $pkgPath = Join-Path $SourceRoot "package.json"
410
- if (Test-Path -LiteralPath $pkgPath) {
411
- $pkg = Get-Content -LiteralPath $pkgPath -Raw | ConvertFrom-Json
412
- if ($pkg.version) { $cfVersion = $pkg.version }
413
- }
414
- } catch { $cfVersion = "unknown" }
415
- $versionFile = Join-Path $TargetRoot "codeforge/VERSION"
416
- if (-not $DryRun) {
417
- Set-Content -LiteralPath $versionFile -Value $cfVersion -NoNewline:$false -Encoding utf8
418
- }
419
- Write-Ok "VERSION -> $versionFile ($cfVersion)"
420
-
421
- # Step 5/7: agents / commands / workflows / context-templates
422
- Write-Section 'Step 5/7: 装 agents / commands / workflows / context-templates'
423
- foreach ($entry in $MdCopyMap) {
424
- $src = Join-Path $SourceRoot $entry.Src
425
- $dst = Join-Path $TargetRoot $entry.Dst
426
- if (-not (Test-Path -LiteralPath $src)) {
427
- Write-Warn2 "源目录不存在,跳过: $src"
428
- continue
429
- }
430
- if (Test-Path -LiteralPath $dst) {
431
- Invoke-Step "cleanup old $dst" {
432
- $item = Get-Item -LiteralPath $dst -Force
433
- if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
434
- [IO.Directory]::Delete($dst, $false)
435
- } else {
436
- Remove-Item -LiteralPath $dst -Recurse -Force
437
- }
438
- }
439
- }
440
- Invoke-Step "mkdir $dst" {
441
- New-Item -ItemType Directory -Path $dst -Force | Out-Null
442
- }
443
- $files = Get-ChildItem -LiteralPath $src -Filter '*.md' -File | Where-Object {
444
- $_.Name -ne 'README.md' -and
445
- $_.Name -notmatch '^_' -and
446
- $_.Name -notmatch '\.bak$' -and
447
- $_.Name -notmatch '^\.'
448
- }
449
- foreach ($f in $files) {
450
- Invoke-Step "copy $($entry.Src)/$($f.Name)" {
451
- Copy-Item -LiteralPath $f.FullName -Destination (Join-Path $dst $f.Name) -Force
452
- }
453
- }
454
- Write-Ok "$($entry.Src)/ -> $dst ($($files.Count) 个 .md)"
455
- }
456
- foreach ($entry in $CopyMap) {
457
- $src = Join-Path $SourceRoot $entry.Src
458
- $dst = Join-Path $TargetRoot $entry.Dst
459
- if (-not (Test-Path -LiteralPath $src)) {
460
- Write-Warn2 "源目录不存在,跳过: $src"
461
- continue
462
- }
463
- Invoke-Step "copy $($entry.Src)/ -> $dst" {
464
- if (-not (Test-Path -LiteralPath $dst)) {
465
- New-Item -ItemType Directory -Path $dst -Force | Out-Null
466
- }
467
- Copy-Item -Path (Join-Path $src '*') -Destination $dst -Recurse -Force
468
- }
469
- Write-Ok "$($entry.Src)/ -> $dst (整目录拷贝)"
470
- }
471
-
472
- # Step 6/7: AGENTS.md 智能合并(仅项目模式)
473
- Write-Section 'Step 6/7: AGENTS.md 智能合并'
474
- if ($Mode -eq 'project') {
475
- $agentsTarget = Join-Path (Get-Location).Path 'AGENTS.md'
476
- $templatePath = Join-Path $SourceRoot $KhTemplateRel
477
- Merge-ProjectAgentsMd -AgentsTarget $agentsTarget -TemplatePath $templatePath
478
- } else {
479
- Write-Ok "全局模式:跳过项目 AGENTS.md 合并(context-templates 已装到 $TargetRoot\context-templates)"
480
- }
481
-
482
- # Step 7/7: KH 全局配置模板(仅 -Global)
483
- Write-Section 'Step 7/7: KH 全局配置模板'
484
- if ($Global) {
485
- Write-KhTemplate
486
- } else {
487
- Write-Ok "项目级安装,跳过 KH 全局模板(要装请加 -Global)"
488
- }
489
-
490
- # 验证清单
491
- Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
492
- Write-Ok "CodeForge v$cfVersion 安装完成"
493
- Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
494
- Write-Host @"
495
- 验证清单:
496
- 1. 列出注入的扩展点:
497
- PS> Get-ChildItem '$TargetRoot' -Force
498
- 2. opencode.json plugin entry:
499
- PS> Get-Content '$TargetRoot\opencode.json'
500
- 3. 让 opencode 校验配置:
501
- PS> opencode debug config
502
- -> 期望看到 1 codeforge plugin entry(不是 17 个)
503
- 4. 跑一次最小 dogfood,看 plugin import + activation 日志:
504
- PS> opencode run "hello"
505
- PS> Get-Content `$HOME\.cache\codeforge\plugins.log -Tail 50
506
- 5. KH 配置(仅 -Global 安装时生成):
507
- PS> Get-Content '$CodeforgeCfgDir\kh.json'
508
- PS> `$env:KNOWLEDGE_API_KEY = '<your-token>' # API key 必须走环境变量
509
- 6. AGENTS.md 智能合并(仅项目级):
510
- - 已有 AGENTS.md → KH 块被替换成模板内容,原文件备份成 *.bak.<timestamp>
511
- - 模板源:$SourceRoot\$KhTemplateRel(修改后请重新 install)
512
-
513
- 卸载:
514
- PS> .\install.ps1 -Uninstall$(if ($Global) { ' -Global' })
515
- "@
516
- 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 拷贝过去
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+ 才有的目录
91
+ $ManagedDirs = @('codeforge', 'agents', 'commands', 'workflows', 'context-templates')
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
106
+ $CopyMap = @(
107
+ @{ Src='workflows'; Dst='workflows' },
108
+ @{ Src='context-templates'; Dst='context-templates' }
109
+ )
110
+
111
+ # ────────────── opencode.json 管理 ──────────────
112
+ function Get-OpencodeConfigPath {
113
+ Join-Path $TargetRoot 'opencode.json'
114
+ }
115
+
116
+ function Get-PluginUri {
117
+ # opencode 接受 file:// URL 也接受裸绝对路径,统一用 file:// 更明确
118
+ $abs = Join-Path $TargetRoot $BundleDstRel
119
+ $abs = $abs -replace '\\', '/'
120
+ if ($abs -notmatch '^[A-Za-z]:/') {
121
+ return "file://$abs"
122
+ }
123
+ return "file:///$abs"
124
+ }
125
+
126
+ function Add-PluginEntry {
127
+ $cfgPath = Get-OpencodeConfigPath
128
+ $uri = Get-PluginUri
129
+
130
+ if (-not (Test-Path -LiteralPath $TargetRoot)) {
131
+ Invoke-Step "mkdir $TargetRoot" {
132
+ New-Item -ItemType Directory -Path $TargetRoot -Force | Out-Null
133
+ }
134
+ }
135
+
136
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
137
+ Write-Err2 "需要 node 才能改写 opencode.json,请安装 Node.js >= 20"
138
+ exit 1
139
+ }
140
+
141
+ Invoke-Step "write $cfgPath plugin entry: $uri" {
142
+ $env:CODEFORGE_CFG = $cfgPath
143
+ $env:CODEFORGE_URI = $uri
144
+ $nodeScript = @'
145
+ const fs = require("node:fs");
146
+ const path = process.env.CODEFORGE_CFG;
147
+ const uri = process.env.CODEFORGE_URI;
148
+ let cfg = {};
149
+ if (fs.existsSync(path)) {
150
+ try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); }
151
+ catch { fs.copyFileSync(path, path + ".bak." + Date.now()); cfg = {}; }
152
+ }
153
+ if (!cfg.$schema) cfg.$schema = "https://opencode.ai/config.json";
154
+ if (!Array.isArray(cfg.plugin)) cfg.plugin = [];
155
+ const cleaned = [];
156
+ for (const e of cfg.plugin) {
157
+ const s = String(e);
158
+ if (/\/codeforge\/index\.js$/.test(s)) continue;
159
+ if (/\/plugins\/[^/]+\.ts$/.test(s) && /opencode/.test(s)) continue;
160
+ if (/\/\.opencode\/plugins\//.test(s)) continue;
161
+ cleaned.push(e);
162
+ }
163
+ cleaned.push(uri);
164
+ cfg.plugin = cleaned;
165
+ fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
166
+ '@
167
+ & node -e $nodeScript
168
+ Remove-Item Env:\CODEFORGE_CFG -ErrorAction SilentlyContinue
169
+ Remove-Item Env:\CODEFORGE_URI -ErrorAction SilentlyContinue
170
+ }
171
+ Write-Ok "opencode.json 已写入 plugin entry: $uri"
172
+ }
173
+
174
+ function Remove-PluginEntry {
175
+ $cfgPath = Get-OpencodeConfigPath
176
+ if (-not (Test-Path -LiteralPath $cfgPath)) { return }
177
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
178
+ Write-Warn2 "未找到 node,跳过 opencode.json plugin entry 清理"
179
+ return
180
+ }
181
+
182
+ Invoke-Step "rewrite $cfgPath without codeforge plugin entry" {
183
+ $env:CODEFORGE_CFG = $cfgPath
184
+ $nodeScript = @'
185
+ const fs = require("node:fs");
186
+ const path = process.env.CODEFORGE_CFG;
187
+ let cfg;
188
+ try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); }
189
+ catch { return; }
190
+ if (!Array.isArray(cfg.plugin)) return;
191
+ cfg.plugin = cfg.plugin.filter(e => {
192
+ const s = String(e);
193
+ if (/\/codeforge\/index\.js$/.test(s)) return false;
194
+ if (/\/plugins\/[^/]+\.ts$/.test(s)) return false;
195
+ if (/\/\.opencode\/plugins\//.test(s)) return false;
196
+ return true;
197
+ });
198
+ fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
199
+ '@
200
+ & node -e $nodeScript
201
+ Remove-Item Env:\CODEFORGE_CFG -ErrorAction SilentlyContinue
202
+ }
203
+ Write-Ok "opencode.json 已移除 codeforge plugin entry"
204
+ }
205
+
206
+ # ────────────── KH 全局模板(仅 -Global) ──────────────
207
+ #
208
+ # 硬约束 #2:API key 绝不能落盘,模板里**不写** token / apiKey 字段。
209
+ # 用户必须通过环境变量 KNOWLEDGE_API_KEY 提供。
210
+ # 已存在则跳过,避免覆盖用户自定义。
211
+ function Write-KhTemplate {
212
+ $khFile = Join-Path $CodeforgeCfgDir 'kh.json'
213
+ if (-not (Test-Path -LiteralPath $CodeforgeCfgDir)) {
214
+ Invoke-Step "mkdir $CodeforgeCfgDir" {
215
+ New-Item -ItemType Directory -Path $CodeforgeCfgDir -Force | Out-Null
216
+ }
217
+ }
218
+ if (Test-Path -LiteralPath $khFile) {
219
+ Write-Ok "已存在 $khFile,跳过覆盖"
220
+ return
221
+ }
222
+ Invoke-Step "write $khFile (no token)" {
223
+ $body = @"
224
+ {
225
+ "url": "http://10.5.60.26:8900/mcp",
226
+ "timeoutMs": 5000,
227
+ "maxRetries": 1
228
+ }
229
+ "@
230
+ # 用 .NET WriteAllText 强制无 BOM 的 UTF8(PowerShell 5.1 默认 BOM 会被 KhConfig 容错,但保持干净)
231
+ [System.IO.File]::WriteAllText($khFile, $body, (New-Object System.Text.UTF8Encoding $false))
232
+ }
233
+ Write-Ok "已生成 $khFile(API key 请通过环境变量 KNOWLEDGE_API_KEY 提供,禁止写入文件)"
234
+ }
235
+
236
+ # ────────────── AGENTS.md 智能合并(B3 新增) ──────────────
237
+ #
238
+ # 实现委托给 scripts/merge-agents-md.mjs CLI(C.4 单一入口),
239
+ # 算法源自 lib/agents-merge.ts,sh/ps1 共用同一份 Node 实现,
240
+ # 避免 install 脚本各自维护算法副本造成漂移。
241
+ function Merge-ProjectAgentsMd {
242
+ param(
243
+ [Parameter(Mandatory=$true)] [string] $AgentsTarget,
244
+ [Parameter(Mandatory=$true)] [string] $TemplatePath
245
+ )
246
+ $cliPath = Join-Path $SourceRoot 'scripts/merge-agents-md.mjs'
247
+ if (-not (Test-Path -LiteralPath $TemplatePath)) {
248
+ Write-Warn2 "模板不存在,跳过 AGENTS.md 合并: $TemplatePath"
249
+ return
250
+ }
251
+ if (-not (Test-Path -LiteralPath $cliPath)) {
252
+ Write-Warn2 "merge CLI 不存在,跳过 AGENTS.md 智能合并: $cliPath"
253
+ return
254
+ }
255
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
256
+ Write-Warn2 "未找到 node,跳过 AGENTS.md 智能合并(请安装 Node.js >= 20)"
257
+ return
258
+ }
259
+ $cliArgs = @('--target', $AgentsTarget, '--template', $TemplatePath)
260
+ if ($DryRun) { $cliArgs += '--dry-run' }
261
+ try {
262
+ & node $cliPath @cliArgs
263
+ } catch {
264
+ Write-Warn2 "AGENTS.md 合并失败(不阻塞 install 主流程): $_"
265
+ }
266
+ }
267
+
268
+ # ────────────── 卸载 ──────────────
269
+ function Invoke-Uninstall {
270
+ Write-Section "卸载 CodeForge from: $TargetRoot"
271
+ Remove-PluginEntry
272
+ $candidates = $LegacyDirs + $ManagedDirs | Select-Object -Unique
273
+ foreach ($name in $candidates) {
274
+ $path = Join-Path $TargetRoot $name
275
+ if (Test-Path -LiteralPath $path) {
276
+ Invoke-Step "remove $path" {
277
+ # Junction 不能用 Remove-Item 直接删(会删源),需要先 rmdir
278
+ $item = Get-Item -LiteralPath $path -Force
279
+ if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
280
+ [IO.Directory]::Delete($path, $false)
281
+ } else {
282
+ Remove-Item -LiteralPath $path -Recurse -Force
283
+ }
284
+ }
285
+ Write-Ok "已删除 $path"
286
+ }
287
+ }
288
+ # 细粒度删除 skills:只删 CodeForge 自己的 skill,不删用户自装的
289
+ $ownedSkills = @('ambiguity-gate', 'devils-advocate', 'ears-zh', 'example-mapping', 'success-criteria', 'weighted-dimensions')
290
+ foreach ($skillName in $ownedSkills) {
291
+ $skillPath = Join-Path $TargetRoot "skills\$skillName"
292
+ if (Test-Path -LiteralPath $skillPath) {
293
+ Invoke-Step "remove skill $skillName" {
294
+ Remove-Item -LiteralPath $skillPath -Recurse -Force
295
+ }
296
+ Write-Ok "已删除 skill: $skillPath"
297
+ }
298
+ }
299
+ Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
300
+ Write-Ok "卸载完成(opencode 自身、AGENTS.md、~/.config/codeforge/kh.json 不会被动)"
301
+ }
302
+
303
+ # ────────────── 环境检测 ──────────────
304
+ function Test-Opencode {
305
+ $cmd = Get-Command opencode -ErrorAction SilentlyContinue
306
+ if ($cmd) {
307
+ try {
308
+ $v = & opencode --version 2>$null
309
+ Write-Ok "检测到 opencode: $v"
310
+ } catch {
311
+ Write-Ok "检测到 opencode (版本未知)"
312
+ }
313
+ } else {
314
+ Write-Warn2 "未检测到 opencode CLI"
315
+ Write-Warn2 " 安装方式:https://github.com/sst/opencode#installation"
316
+ }
317
+ }
318
+
319
+ function Test-KhMcp {
320
+ $xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' }
321
+ $cfg = Join-Path (Join-Path $xdg 'opencode') 'opencode.json'
322
+ if ((Test-Path -LiteralPath $cfg) -and ((Get-Content -LiteralPath $cfg -Raw -Encoding UTF8) -match 'knowledge-hub|code-forge-knowledge-hub')) {
323
+ Write-Ok "检测到 Knowledge Hub MCP 已注册"
324
+ } else {
325
+ Write-Warn2 "未在 $cfg 中找到 Knowledge Hub MCP"
326
+ Write-Warn2 " 配置示例见 docs/PRD.md §6.2"
327
+ }
328
+ }
329
+
330
+ # ────────────── 主流程 ──────────────
331
+ Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
332
+ Write-Host "[codeforge] CodeForge installer (mode=$Mode, uninstall=$Uninstall, dry-run=$DryRun)" -ForegroundColor Cyan
333
+ Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
334
+ Write-Host "Source : $SourceRoot"
335
+ Write-Host "Target : $TargetRoot"
336
+ Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
337
+
338
+ if ($Uninstall) {
339
+ Invoke-Uninstall
340
+ exit 0
341
+ }
342
+
343
+ # Step 1/7: 环境检测
344
+ Write-Section 'Step 1/7: 环境检测'
345
+ Test-Opencode
346
+ Test-KhMcp
347
+
348
+ # Step 2/7: build dist bundle
349
+ Write-Section 'Step 2/7: 构建 dist/index.js 单 bundle'
350
+ $bundleSrc = Join-Path $SourceRoot $BundleSrcRel
351
+ if ($SkipBuild) {
352
+ Write-Warn2 '已跳过 build(-SkipBuild),使用现有 dist/index.js'
353
+ } else {
354
+ Invoke-Step "npm run build" {
355
+ Push-Location $SourceRoot
356
+ try {
357
+ $out = npm run build 2>&1 | Out-String
358
+ if ($LASTEXITCODE -ne 0) {
359
+ Write-Err2 'npm run build 失败'
360
+ Write-Host $out
361
+ exit 1
362
+ }
363
+ } finally {
364
+ Pop-Location
365
+ }
366
+ }
367
+ }
368
+ if (-not (Test-Path -LiteralPath $bundleSrc)) {
369
+ if ($SkipBuild) {
370
+ Write-Err2 "找不到 $bundleSrc(npm 包可能损坏)"
371
+ Write-Err2 " 请尝试重装:npx @andyqiu/codeforge install [--global]"
372
+ } else {
373
+ Write-Err2 "找不到 $bundleSrc,请先成功执行 npm run build"
374
+ }
375
+ exit 1
376
+ }
377
+ $bundleSize = (Get-Item -LiteralPath $bundleSrc).Length
378
+ Write-Ok "bundle 已就绪: $bundleSrc ($([math]::Round($bundleSize/1024,1)) KB)"
379
+
380
+ # Step 3/7: 准备目标目录
381
+ Write-Section 'Step 3/7: 准备目标目录'
382
+ if (-not (Test-Path -LiteralPath $TargetRoot)) {
383
+ Invoke-Step "mkdir $TargetRoot" {
384
+ New-Item -ItemType Directory -Path $TargetRoot -Force | Out-Null
385
+ }
386
+ }
387
+ # 清理 legacy 单数目录 + 老的 17 个 plugin / lib / tools junction
388
+ foreach ($legacy in $LegacyDirs) {
389
+ $dst = Join-Path $TargetRoot $legacy
390
+ if (Test-Path -LiteralPath $dst) {
391
+ Invoke-Step "cleanup legacy $dst" {
392
+ $item = Get-Item -LiteralPath $dst -Force
393
+ if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
394
+ [IO.Directory]::Delete($dst, $false)
395
+ } else {
396
+ Remove-Item -LiteralPath $dst -Recurse -Force
397
+ }
398
+ }
399
+ Write-Warn2 "已清理 legacy 目录: $dst"
400
+ }
401
+ }
402
+
403
+ # Step 4/7: 装 bundle codeforge/index.js
404
+ Write-Section 'Step 4/7: 装入 dist/index.js bundle'
405
+ $bundleDst = Join-Path $TargetRoot $BundleDstRel
406
+ $bundleDstDir = Split-Path -Parent $bundleDst
407
+ if (-not (Test-Path -LiteralPath $bundleDstDir)) {
408
+ Invoke-Step "mkdir $bundleDstDir" {
409
+ New-Item -ItemType Directory -Path $bundleDstDir -Force | Out-Null
410
+ }
411
+ }
412
+ Invoke-Step "copy $BundleSrcRel -> $BundleDstRel" {
413
+ Copy-Item -LiteralPath $bundleSrc -Destination $bundleDst -Force
414
+ }
415
+ Write-Ok "bundle -> $bundleDst"
416
+ Add-PluginEntry
417
+
418
+ # 写 VERSION marker 文件(用户 cat 一行查版本,不依赖 grep bundle)
419
+ $cfVersion = "unknown"
420
+ try {
421
+ $pkgPath = Join-Path $SourceRoot "package.json"
422
+ if (Test-Path -LiteralPath $pkgPath) {
423
+ $pkg = Get-Content -LiteralPath $pkgPath -Raw | ConvertFrom-Json
424
+ if ($pkg.version) { $cfVersion = $pkg.version }
425
+ }
426
+ } catch { $cfVersion = "unknown" }
427
+ $versionFile = Join-Path $TargetRoot "codeforge/VERSION"
428
+ if (-not $DryRun) {
429
+ Set-Content -LiteralPath $versionFile -Value $cfVersion -NoNewline:$false -Encoding utf8
430
+ }
431
+ Write-Ok "VERSION -> $versionFile ($cfVersion)"
432
+
433
+ # Step 5/7: agents / commands / workflows / context-templates
434
+ Write-Section 'Step 5/7: 装 agents / commands / workflows / context-templates'
435
+ foreach ($entry in $MdCopyMap) {
436
+ $src = Join-Path $SourceRoot $entry.Src
437
+ $dst = Join-Path $TargetRoot $entry.Dst
438
+ if (-not (Test-Path -LiteralPath $src)) {
439
+ Write-Warn2 "源目录不存在,跳过: $src"
440
+ continue
441
+ }
442
+ if (Test-Path -LiteralPath $dst) {
443
+ Invoke-Step "cleanup old $dst" {
444
+ $item = Get-Item -LiteralPath $dst -Force
445
+ if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
446
+ [IO.Directory]::Delete($dst, $false)
447
+ } else {
448
+ Remove-Item -LiteralPath $dst -Recurse -Force
449
+ }
450
+ }
451
+ }
452
+ Invoke-Step "mkdir $dst" {
453
+ New-Item -ItemType Directory -Path $dst -Force | Out-Null
454
+ }
455
+ $files = Get-ChildItem -LiteralPath $src -Filter '*.md' -File | Where-Object {
456
+ $_.Name -ne 'README.md' -and
457
+ $_.Name -notmatch '^_' -and
458
+ $_.Name -notmatch '\.bak$' -and
459
+ $_.Name -notmatch '^\.'
460
+ }
461
+ foreach ($f in $files) {
462
+ Invoke-Step "copy $($entry.Src)/$($f.Name)" {
463
+ Copy-Item -LiteralPath $f.FullName -Destination (Join-Path $dst $f.Name) -Force
464
+ }
465
+ }
466
+ Write-Ok "$($entry.Src)/ -> $dst ($($files.Count) 个 .md)"
467
+ }
468
+ foreach ($entry in $CopyMap) {
469
+ $src = Join-Path $SourceRoot $entry.Src
470
+ $dst = Join-Path $TargetRoot $entry.Dst
471
+ if (-not (Test-Path -LiteralPath $src)) {
472
+ Write-Warn2 "源目录不存在,跳过: $src"
473
+ continue
474
+ }
475
+ Invoke-Step "copy $($entry.Src)/ -> $dst" {
476
+ if (-not (Test-Path -LiteralPath $dst)) {
477
+ New-Item -ItemType Directory -Path $dst -Force | Out-Null
478
+ }
479
+ Copy-Item -Path (Join-Path $src '*') -Destination $dst -Recurse -Force
480
+ }
481
+ Write-Ok "$($entry.Src)/ -> $dst (整目录拷贝)"
482
+ }
483
+
484
+ # Step 5b/7: 装 skills/(opencode skill 目录)
485
+ Write-Section 'Step 5b/7: 装 skills/'
486
+ $skillsSrc = Join-Path $SourceRoot 'skills'
487
+ $skillsDst = Join-Path $TargetRoot 'skills'
488
+ if (Test-Path -LiteralPath $skillsSrc) {
489
+ if (-not (Test-Path -LiteralPath $skillsDst)) {
490
+ New-Item -ItemType Directory -Path $skillsDst -Force | Out-Null
491
+ }
492
+ $skillDirs = Get-ChildItem -LiteralPath $skillsSrc -Directory
493
+ $skillCount = 0
494
+ foreach ($skillDir in $skillDirs) {
495
+ $dstSkill = Join-Path $skillsDst $skillDir.Name
496
+ if (Test-Path -LiteralPath $dstSkill) {
497
+ Invoke-Step "remove old skill $($skillDir.Name)" {
498
+ Remove-Item -LiteralPath $dstSkill -Recurse -Force
499
+ }
500
+ }
501
+ Invoke-Step "copy skill $($skillDir.Name)" {
502
+ Copy-Item -LiteralPath $skillDir.FullName -Destination $dstSkill -Recurse -Force
503
+ }
504
+ $skillCount++
505
+ }
506
+ Write-Ok "skills/ -> $skillsDst ($skillCount 个 skill)"
507
+ } else {
508
+ Write-Warn2 "skills/ 目录不存在,跳过(发布包未含 skills)"
509
+ }
510
+
511
+ # Step 6/7: AGENTS.md 智能合并(仅项目模式)
512
+ Write-Section 'Step 6/7: AGENTS.md 智能合并'
513
+ if ($Mode -eq 'project') {
514
+ $agentsTarget = Join-Path (Get-Location).Path 'AGENTS.md'
515
+ $templatePath = Join-Path $SourceRoot $KhTemplateRel
516
+ Merge-ProjectAgentsMd -AgentsTarget $agentsTarget -TemplatePath $templatePath
517
+ } else {
518
+ Write-Ok "全局模式:跳过项目 AGENTS.md 合并(context-templates 已装到 $TargetRoot\context-templates)"
519
+ }
520
+
521
+ # Step 7/7: KH 全局配置模板(仅 -Global)
522
+ Write-Section 'Step 7/7: KH 全局配置模板'
523
+ if ($Global) {
524
+ Write-KhTemplate
525
+ } else {
526
+ Write-Ok "项目级安装,跳过 KH 全局模板(要装请加 -Global)"
527
+ }
528
+
529
+ # 验证清单
530
+ Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
531
+ Write-Ok "CodeForge v$cfVersion 安装完成"
532
+ Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray
533
+ Write-Host @"
534
+ 验证清单:
535
+ 1. 列出注入的扩展点:
536
+ PS> Get-ChildItem '$TargetRoot' -Force
537
+ 2. 看 opencode.json 里 plugin entry:
538
+ PS> Get-Content '$TargetRoot\opencode.json'
539
+ 3. 让 opencode 校验配置:
540
+ PS> opencode debug config
541
+ -> 期望看到 1 个 codeforge plugin entry(不是 17 个)
542
+ 4. 跑一次最小 dogfood,看 plugin import + activation 日志:
543
+ PS> opencode run "hello"
544
+ PS> Get-Content `$HOME\.cache\codeforge\plugins.log -Tail 50
545
+ 5. KH 配置(仅 -Global 安装时生成):
546
+ PS> Get-Content '$CodeforgeCfgDir\kh.json'
547
+ PS> `$env:KNOWLEDGE_API_KEY = '<your-token>' # ← API key 必须走环境变量
548
+ 6. AGENTS.md 智能合并(仅项目级):
549
+ - 已有 AGENTS.md → KH 块被替换成模板内容,原文件备份成 *.bak.<timestamp>
550
+ - 模板源:$SourceRoot\$KhTemplateRel(修改后请重新 install)
551
+
552
+ 卸载:
553
+ PS> .\install.ps1 -Uninstall$(if ($Global) { ' -Global' })
554
+ "@
555
+ Write-Host '────────────────────────────────────────────────' -ForegroundColor DarkGray