@yufengtadian/freedom-cli 1.13.2 → 1.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +40 -0
  2. package/README.md +44 -14
  3. package/lib/agents.js +182 -52
  4. package/lib/build.js +22 -7
  5. package/lib/cli.js +100 -7
  6. package/lib/shell.js +165 -61
  7. package/lib/toolchain.js +339 -0
  8. package/lib/utils.js +51 -0
  9. package/lib/verify.js +5 -4
  10. package/lib/webkit.js +45 -0
  11. package/package.json +2 -2
  12. package/shell/linux-x64/freedom-shell +0 -0
  13. package/shell/win-x64/freedom-shell.exe +0 -0
  14. package/skill/freedom/SKILL.md +3 -3
  15. package/templates/desktop/backend/desktop.mjs +16 -6
  16. package/templates/desktop/freedom.config.js +4 -1
  17. package/templates/go/go.mod +3 -0
  18. package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
  19. package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
  20. package/templates/go/pkg/freedom/anti_debug_windows.go +136 -12
  21. package/templates/go/pkg/freedom/assets_embed.go +1 -0
  22. package/templates/go/pkg/freedom/backend_proc.go +5 -5
  23. package/templates/go/pkg/freedom/events_windows.go +5 -5
  24. package/templates/go/pkg/freedom/freedom.go +13 -2
  25. package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
  26. package/templates/go/pkg/freedom/resources.go +52 -1
  27. package/templates/go/pkg/freedom/security.go +57 -7
  28. package/templates/go/pkg/freedom/shutdown.go +55 -0
  29. package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
  30. package/templates/go/pkg/freedom/store.go +3 -3
  31. package/templates/go/pkg/freedom/syscap_windows.go +33 -33
  32. package/templates/go/pkg/freedom/sysint_windows.go +2 -2
  33. package/templates/go/pkg/freedom/tray_windows.go +18 -18
  34. package/templates/go/pkg/freedom/updater.go +3 -3
  35. package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
  36. package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
  37. package/templates/go/third_party/webview_go/LICENSE +22 -0
  38. package/templates/go/third_party/webview_go/README.md +50 -0
  39. package/templates/go/third_party/webview_go/glue.c +36 -0
  40. package/templates/go/third_party/webview_go/go.mod +3 -0
  41. package/templates/go/third_party/webview_go/go.sum +0 -0
  42. package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
  43. package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
  44. package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
  45. package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
  46. package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
  47. package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
  48. package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
  49. package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
  50. package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
  51. package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
  52. package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
  53. package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
  54. package/templates/go/third_party/webview_go/webview.cc +1 -0
  55. package/templates/go/third_party/webview_go/webview.go +332 -0
package/LICENSE ADDED
@@ -0,0 +1,40 @@
1
+ Freedom —— 专有软件许可(Proprietary License)
2
+
3
+ Copyright (c) 2026 YUfeng12TA(邮箱与 GitHub 账号见仓库 https://github.com/YUfeng12TA/freedom )
4
+
5
+ 本仓库(下称「本软件」)为闭源专有软件:**不适用 MIT、Apache、GPL 等任何开源许可**。
6
+ 本文件同时随 npm 包 `@yufengtadian/freedom-cli` 分发(包目录内 `LICENSE` 与本文件同文)。
7
+
8
+ ## 1. 授权范围(你可以做什么)
9
+
10
+ - 安装与使用本软件(CLI、Freedom Desktop、预编译壳)于任意数量的个人 / 团队 / 商用设备。
11
+ - 用本软件打包、签名、分发、销售**你自己的应用**:你的前端代码、后端代码、`resources/`
12
+ 内容与应用数据归你所有,无需就产物向许可方付费、开源或署名。
13
+ - 出于自身部署、审计与安全研究目的**阅读**随包分发的源码(见第 4 条的边界)。
14
+
15
+ ## 2. 限制(未经许可方书面同意不得做什么)
16
+
17
+ - 再分发本软件源码、预编译壳、npm 包内容物,或任何衍生 / 修改版本(第 4 条的第三方组件除外)。
18
+ - 移除、遮盖或改动版权声明、许可文本与完整性校验信息。
19
+ - 将本软件或其副本作为独立产品对外销售、托管、转许可或用于构建竞争性打包工具。
20
+ - 绕过 FRDM2 加密容器与反调试机制,对他人分发的产物提取源码,或将该成果对外发布
21
+ (安全研究请走 `SECURITY.md` 的披露通道)。
22
+
23
+ ## 3. 第三方组件不受本文件改写
24
+
25
+ - `third_party/webview_go/`(及其在 `freedom-cli/templates/go/` 下的镜像副本)为
26
+ [webview_go](https://github.com/webview/webview_go) 的上游补丁副本,沿用其 **MIT** 许可
27
+ (原文见该目录 `LICENSE`),改动记录见同目录 `FREEDOM-PATCH.md`。
28
+ - Go、Node.js、WebView2 SDK、WebKitGTK/GTK、NSIS、rcedit 等各自按其许可证使用。
29
+ - 本文件不对这些组件施加任何额外限制。
30
+
31
+ ## 4. 免责与无担保
32
+
33
+ 本软件按「现状」提供,不含任何明示或暗示的担保。FRDM2 容器、完整性清单与反调试检测的设计
34
+ 目标是**提高逆向成本**,不是可信执行环境,不构成「不可逆向」的承诺(详见 `SECURITY.md`)。
35
+ 在适用法律允许的最大范围内,许可方不对因使用或无法使用本软件造成的任何损害承担责任。
36
+
37
+ ## 5. 条款变更与联系
38
+
39
+ 许可条款可能在新版本中调整,以你所获得版本随附的本文件为准。商用授权、白标或源码买断等
40
+ 需求,请通过仓库 GitHub Issues 与许可方联系。
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # freedom-cli
2
2
 
3
- Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.13.2)。
3
+ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.13.3)。
4
4
 
5
5
  基于自研 Freedom WebView 壳层(对标 Wails / Tauri):前端完全自由、后端可任意语言、渲染复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK),产物为单个可执行文件 + resources 目录,前端页面内存加载,不占本地端口。
6
6
 
@@ -12,6 +12,7 @@ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌
12
12
  - **销毁竞态收口**:修复 webview2 在 `Destroy` 中泵出滞留 dispatch 回调导致的随机崩溃(0xc0000005,多窗口 / 快速关闭场景),回收后所有排队回调按拆除旗标自我作废,并配套红绿回归测试;
13
13
  - **v1.13.1**:文档同步(本 README 更新至 v1.13.x 真实现状、壳 CI 章节勘误),无功能与壳二进制变更。
14
14
  - **v1.13.2 反逆向加固(FRDM2)**:容器升 FRDM2(构建期随机盐 + PBKDF2 60 万次 + enc/mac 域分离 + MAC 覆盖容器头部),主密钥改为掩码表运行时组装(壳二进制 `strings` 直取不到),`backend/**` 后端源码一并入容器并在运行期解密到私有临时目录(退出即删、崩溃残留按 PID 回收),前端产物 `sourceMappingURL` 构建期抹除,anti-debug 扩到六道信号,本地与 CI 壳统一 `-trimpath -s -w`。**旧版本 CLI 产出的 FRDM1 资源包在新壳上明确拒绝运行(不静默降级),须重新 `freedom build`**;三平台壳随本版本经 CI 重编发布。
15
+ - **v1.13.3 内置工具链扩展 + 加固与健壮性收口**:新增 `freedom toolchain status|install|optimize|clear`,探测 / 安装 / 优化 C++(MSVC / MinGW / clang)、Rust、Go 三条工具链并按平台映射到 winget / brew / apt;探测结论缓存(绑 PATH 指纹 + 7 天 TTL)**只缓存成功**,即「检测到配置好就不再实探」,刚装好的工具当场可识别,`install` 默认只打印命令、`--apply` 才执行。同轮收口:Windows 第七道反调试信号(`DR0–DR3` 硬件断点)、SIGTERM / `CTRL_CLOSE_EVENT` 退出清扫通道(临时目录不再等下次启动回收)、`.integrity` 缺失即拒绝启动、Linux WebKitGTK 4.1 构建路径自动注入 `-tags=webkit2_41`、`shell build` / `shell download` 成功却非零退出的退出码修正、产物自检不再把 `.integrity` 大小说成 0 B。许可改为闭源专有许可(详见根 `LICENSE`)。
15
16
 
16
17
  **v1.12.17 安全模式全面落地**:三档安全模式 `freedom security <none|basic|high>` 正式随包分发——high 档把 resources 加密为 `app.bin`(AES-256-CTR + HMAC-SHA256 + PBKDF2 密钥派生),配合 `.integrity` 完整性校验、anti-debug 与进程隐藏,磁盘无明文、篡改即拒运行;三平台预编译壳经 CI 重建分发,补齐 v1.12.16 仅重编 win-x64 壳的缺口。
17
18
 
@@ -138,6 +139,13 @@ mac 用户解压 `.app.zip` 即得 `.app`,拖入 `/Applications` 即可直接
138
139
 
139
140
  **预编译壳的来源与 CI**:`webview_go` 依赖各系统自带 WebView 框架,**无法交叉编译**,三平台壳必须在对应平台本机编译。自 v1.13.0 起由仓库 `.github/workflows/build.yml` 统一承担:推送 `vX.Y.Z` tag → 三平台 runner 编译通用壳 → `release` job 自动创建 GitHub Release 并上传资产 `freedom-shell-<plat>`,供 `freedom shell download` 按「`v<包版本>` tag + 同名资产」拉取(Intel Mac 已不支持,见 `nativePlatform()` 的明确报错)。发版顺序:先推 GitHub tag、等 Release 资产就绪,再 `npm publish` 同版本——保证 shell 下载默认源始终可用(紧急时可设 `FREEDOM_SHELL_TAG` 指向既有 tag)。
140
141
 
142
+ **平台名写法与下载回退**:`--platform` 与 `freedom shell download/build` 接受别名与架构后缀——
143
+ `win` / `windows` / `mac` / `macos` / `osx` / `ubuntu` / `linux-x86_64` / `mac-arm64` 等统一归一到
144
+ `win-x64` / `darwin-arm64` / `linux-x64`;无法支持的组合(`win-x86`、`darwin-x64`、`plan9`)直接报「未知平台」并给出可选值,不做猜测。
145
+ 下载有两条路径:先试直连 `releases/download`,不通(超时 / 代理 / 网络策略)时自动回退 GitHub Release API 资产端点;
146
+ 两条都不通则错误信息分别给出两端点与手工放置目录(`FREEDOM_SHELL_DIR` 可覆写壳目录,私有镜像另见 `FREEDOM_SHELL_REPO` / `FREEDOM_SHELL_BASE` / `FREEDOM_GITHUB_API`),
147
+ GitHub 限流时可带 `FREEDOM_GITHUB_TOKEN`。
148
+
141
149
  产物目录可在 `freedom.config.js` 的 `outDir` 中调整:默认 `'dist'`,设为 `'.'` 则直接输出到项目根目录(dist 的上级),设为任意相对 / 绝对路径亦可。输出到项目根目录时会自动跳过 `index.html` 副本,避免覆盖项目源文件。
142
150
 
143
151
  ```bash
@@ -213,7 +221,10 @@ freedom build # 读取配置中的 security 值
213
221
  - 另内置 **anti-debug**:`IsDebuggerPresent`、`CheckRemoteDebuggerPresent`、直读 `PEB.BeingDebugged`、`NtQueryInformationProcess` 的 `ProcessDebugPort` / `ProcessDebugObjectHandle` / `ProcessDebugFlags` 共六道独立信号,任一确证即静默退出(退出码 77),在资源解密前后各检测一次;所有探测遵循"取不到即视为未命中",杜绝误杀;
214
222
  - 进程隐藏加固(`-H windowsgui`,无控制台窗口)。
215
223
 
216
- **加固上限说明**:`high` 大幅提高破解门槛,但**任何客户端可执行程序都无法做到绝对不可破解**——密钥最终存在于壳二进制与运行时内存中,反调试只抬升动态分析成本、不是不可绕过的墙。真正敏感的密钥与业务逻辑仍应留在服务端。
224
+ **加固上限说明**:`high` 大幅提高破解门槛,但**任何客户端可执行程序都无法做到绝对不可破解**——密钥最终存在于壳二进制与运行时内存中,反调试只抬升动态分析成本、不是不可绕过的墙。真正敏感的密钥与业务逻辑仍应留在服务端。**误报可关**:反调试在 CI、自动化测试、远程桌面、部分虚拟化环境可能把正常运行判成被附加调试器
225
+ (命中即静默退出,码 77)。这类场景设 `FREEDOM_DISABLE_ANTIDEBUG=1`,或在壳层源码里 `Config{DisableAntiDebug: true}`
226
+ 后 `freedom shell build`——只关探测,容器解密与 `.integrity` 完整性校验照旧生效(那两道才是源码保护的本体)。
227
+ `resources/config.json` 里刻意不提供此开关:打包产物里的配置运行期可被第三方替换,给开关等于递刀。
217
228
 
218
229
  **互斥规则**:切换安全模式重新构建时,CLI 会自动清理另一模式的遗留产物(`app.bin`/`.integrity` 与明文 `index.html`/`config.json`/`backend/` 只能存其一),避免壳误加载旧资源。
219
230
 
@@ -231,7 +242,10 @@ window.freedom.window.minimize(); // 窗口控制
231
242
 
232
243
  - Node.js >= 18
233
244
  - 不需要 Go 工具链(壳层为随包分发的预编译二进制)
234
- - 可选:`freedom shell build` 需本机 Go(含 CGO,Windows 需 MSVC 编译环境),仅用于亲自编译壳
245
+ - 可选:`freedom shell build` 需本机 Go(含 CGO,Windows 需 MSVC 编译环境),仅用于亲自编译壳。
246
+ Linux 侧需 WebKitGTK + GTK3 开发库,`4.0` 与 `4.1` 二选一(Ubuntu 24.04+/Debian 13+ 只有 4.1:
247
+ `sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev`);
248
+ 仅有 4.1 时 `freedom shell build` 会自动为编译加 `-tags webkit2_41`,无需手工干预(探测见 `lib/webkit.js`)。
235
249
  - `.dmg` 打包需在 macOS 上执行(依赖系统 hdiutil);`.app.zip` 在任意平台均可直接产出
236
250
 
237
251
  ## 命令
@@ -248,13 +262,17 @@ freedom security <none|basic|high> # 设置安全模式(写入配置)
248
262
  freedom icon <path> # 设置应用图标(Windows 用 .ico,macOS 用 .icns)
249
263
  freedom config [get|set]
250
264
  freedom shell list|download <platform>|build <platform>
265
+ freedom toolchain status [--refresh] [--home <dir>] # 探测 Go / Rust / C++ 工具链(成功结论才缓存,绑 PATH 指纹)
266
+ freedom toolchain install [go,rust,cpp|missing] [--apply] # 默认只打印安装命令,加 --apply 才执行
267
+ freedom toolchain optimize [--apply] # 国内网络迹象下换 Go/Rust 镜像源(改配置前留 .bak)
268
+ freedom toolchain clear # 清空探测缓存(~/.freedom/toolchain-cache.json)
251
269
  freedom dmg [--platform <plat>] # 在 macOS 上把 .app 打包为 .dmg
252
270
  freedom keygen [--force] # 生成应用自更新 ed25519 密钥对(私钥留 .freedom/keys/,公钥进配置)
253
271
  freedom manifest --artifact <产物> --url <下载地址> [--version x] [--notes txt] # 产出签名更新清单 dist/latest.json
254
- freedom agents [--home <dir>] # Agent 集成支持矩阵(本机磁盘证据判定 ready/convention/unknown)
272
+ freedom agents [--home <dir>] # Agent 集成支持矩阵(本机磁盘证据判定「是否安装 + 目标文件状态」)
255
273
  freedom agents install --what <mcp|skill> --agent <key|all> # 等价于下面两条
256
- freedom skill install --agent <key|all> [--dry-run] [--skills-dir <path>]
257
- freedom mcp install --agent <key|all> [--dry-run] [--config <path> --format json|toml|yaml]
274
+ freedom skill install --agent <key|all> [--dry-run] [--force] [--skills-dir <path>]
275
+ freedom mcp install --agent <key|all> [--dry-run] [--force] [--config <path> --format json|toml|toml-aot|yaml]
258
276
  freedom mcp serve # stdio MCP 服务本体(一般由 agent 自动拉起)
259
277
  freedom version # 显示版本并检测最新版本
260
278
  freedom update # 检查新版本并给出升级命令(同 check-update)
@@ -284,16 +302,22 @@ freedom help
284
302
  NDJSON 协议、CLI 命令、坑清单)复制进各 agent 的 skills 目录。
285
303
  - `freedom mcp install --agent <key|all>`:把 `freedom mcp serve` 注册进各 agent 的 MCP 配置。
286
304
  MCP 工具面:`freedom_build / init / verify / config / shell / release / agents / guide`。
287
- - `freedom agents [--home <dir>]`:打印支持矩阵。**是否可写一律按本机磁盘证据判定**——
288
- `ready`(配置文件已在,合并写入)/ `convention`(仅主目录在,按同族约定新建并明确提示)/
289
- `unknown`(本机无足迹,只输出可粘贴片段,绝不凭记忆造路径)。`--home` 换一棵家目录树预览取证结果。
305
+ - `freedom agents [--home <dir>]`:打印支持矩阵。**安装检测门(v1.13.2 起)**——只有本机检出该
306
+ agent 的**安装足迹**(配置文件在 / 注册表声明的专属目录在 / PATH 里有它的可执行)才允许写入;
307
+ 未检出即 `not-installed`,只输出可粘贴片段,**绝不新建目录、绝不凭记忆造路径**。
308
+ 「父目录存在」不算证据(HOME 与 `AppData/Roaming` 恒在,据此写入=往没装的东西里写)。
309
+ `--home` 换一棵家目录树预览取证结果。
310
+ - 逃生通道两条:`--force`(明知该 agent 装着但足迹未被覆盖,强行写入并显式告警)、
311
+ `--config <真实路径>` / `--skills-dir <真实目录>`(按你指定的路径写,绕开候选路径推断)。
290
312
  - `freedom agents install --what <mcp|skill> --agent <key|all>`:上面两条安装入口的合并写法,
291
313
  不带 `--what` 时默认 `mcp`;`agents <其它子命令>` 直接报错退出,不会静默回落成矩阵。
292
314
  - 写入是**幂等合并**:保留既有其它 server 条目,二次安装原地替换不产生重复;改前留 `<file>.bak` 备份;
293
- `--dry-run` 只预览不落盘。未取证的 agent 用 `--config <真实路径> --format <json|toml|yaml>` 覆写。
294
- - 已按本机取证登记的 agent:Claude Code / Claude Desktop / Codex CLI / Qoder / CodeBuddy / Zcode / Cursor /
295
- Hermes(YAML)等;其余(Trae、OpenCode、Gemini CLI、Pi、Tianshu、WorkBuddy、DeepSeek harness、Oh My Pi)
296
- 在无足迹的机器上一律降级为片段 + 覆写通道。
315
+ `--dry-run` 只预览不落盘。
316
+ - 已登记的 agent(17 个):Claude Code / Claude Desktop / Codex CLI / Qoder / CodeBuddy / Zcode / Cursor /
317
+ Hermes(YAML)/ Tianshu / Pi / WorkBuddy / OpenCode / Gemini CLI / DeepSeek Harness / Oh My Pi / Trae /
318
+ **Reasonix**。其中 **Reasonix** 的 MCP 配置是 TOML **数组表** `[[plugins]]`(按 `name = "freedom"` 定位条目,
319
+ 与既有 `computer-use` 等插件共存),格式取自本机 `%APPDATA%/reasonix/config.toml` 实况;
320
+ 技能装在 `~/.reasonix/skills/freedom`。未被登记或无足迹的布局用 `--format <json|toml|toml-aot|yaml>` 覆写。
297
321
 
298
322
  ## 开发热更(freedom dev)
299
323
 
@@ -336,5 +360,11 @@ export default {
336
360
 
337
361
  ## License
338
362
 
339
- MIT
363
+ 本工具与其源码为**闭源专有软件**,完整条款见包内 `LICENSE`(不适用 MIT)。要点:
364
+
365
+ - 你可以自由安装使用,并用它打包 / 销售你自己的应用——**你的前端、后端与产物归你所有**,无需开源或署名;
366
+ - 未经许可方书面同意,不得再分发本包内容物、源码或衍生版本,也不得移除版权与许可声明;
367
+ - 唯一沿用开源许可的部分是内嵌第三方 `webview_go`(MIT,见 `templates/go/third_party/webview_go/LICENSE`)。
368
+
369
+ 商用授权与定制需求请通过仓库 GitHub Issues 联系。
340
370
 
package/lib/agents.js CHANGED
@@ -3,9 +3,14 @@
3
3
  // freedom agents / skill / mcp —— 把 Freedom 使用技能与 MCP 服务装进各家 Agent
4
4
  //
5
5
  // 设计纪律(铁律 19):注册表只登记「候选路径 + 写入格式」,是否可写一律在安装时按磁盘
6
- // 证据判定——配置文件存在 = ready(合并写入,保留既有条目、写前 .bak 备份);目录存在但
7
- // 配置文件缺失 = create(新建后写入);皆无 = unverified,只打印可粘贴片段,绝不凭记忆
8
- // 造路径。未取证的 agent 与任意私有布局经 --config <path> --format <json|toml|yaml> 覆写。
6
+ // 证据判定。安装检测门(R4 起,用户裁定):**只有检出该 agent 确实装在本机**(配置文件在
7
+ // / agent 家目录在 / PATH 里有它的可执行 / 注册表声明的额外足迹)才允许写入;仅凭「父目录
8
+ // 在」不构成证据——HOME 与 AppData/Roaming 恒在,按同族约定新建等于往没装的东西里写。
9
+ // 未检出的 agent 一律 skipped(not-installed),只打印可粘贴片段,绝不凭记忆造路径。
10
+ // 覆写通道:--force(明知在但足迹未覆盖)、--config / --skills-dir(用户指定真实路径)。
11
+ //
12
+ // 写入档位:配置文件存在 = ready(合并写入,保留既有条目、写前 .bak 备份);
13
+ // 目录不存在则按需创建后再合并。
9
14
  //
10
15
  // 幂等:同一 server 名 / 同一 skill 目录重复安装为原地替换,不产生重复条目。
11
16
 
@@ -18,31 +23,91 @@ const { paint, ok, err, warn, dim, bold, section, C } = theme;
18
23
 
19
24
  const SERVER_NAME = 'freedom';
20
25
 
21
- // mcp: { file: 相对 HOME 的配置文件, format: json|toml|yaml, path: JSON/YAML 容器键路径 }
26
+ // mcp: { file: 相对 HOME 的配置文件, format: json|toml|yaml|toml-aot, path: JSON/YAML 容器键路径,
27
+ // section: toml-aot 的数组表名, key: toml-aot 用于定位条目的字段 }
22
28
  // skills: 相对 HOME 的 skill 根目录(每个 agent 一个)
29
+ // home / evidence: 该 agent 的专属目录与额外安装足迹(可选,安装检测门的证据)
23
30
  const AGENTS = [
24
31
  { key: 'claude-code', name: 'Claude Code', mcp: { file: '.claude.json', format: 'json', path: ['mcpServers'] }, skills: '.claude/skills' },
25
- { key: 'claude-desktop', name: 'Claude Desktop', mcp: { file: 'AppData/Roaming/Claude/claude_desktop_config.json', format: 'json', path: ['mcpServers'] }, skills: 'AppData/Roaming/Claude/skills' },
26
- { key: 'codex', name: 'Codex CLI', mcp: { file: '.codex/config.toml', format: 'toml' }, skills: '.codex/skills' },
32
+ { key: 'claude-desktop', name: 'Claude Desktop', home: 'AppData/Roaming/Claude', mcp: { file: 'AppData/Roaming/Claude/claude_desktop_config.json', format: 'json', path: ['mcpServers'] }, skills: 'AppData/Roaming/Claude/skills' },
33
+ { key: 'codex', name: 'Codex CLI', bin: 'codex', mcp: { file: '.codex/config.toml', format: 'toml' }, skills: '.codex/skills' },
27
34
  { key: 'qoder', name: 'Qoder', mcp: { file: '.qoder/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.qoder/skills' },
28
35
  { key: 'codebuddy', name: 'CodeBuddy', mcp: { file: '.codebuddy/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.codebuddy/skills' },
29
- { key: 'zcode', name: 'Zcode', mcp: { file: '.zcode/cli/config.json', format: 'json', path: ['mcp', 'servers'] }, skills: '.zcode/skills' },
36
+ { key: 'zcode', name: 'Zcode', home: '.zcode', mcp: { file: '.zcode/cli/config.json', format: 'json', path: ['mcp', 'servers'] }, skills: '.zcode/skills' },
30
37
  { key: 'cursor', name: 'Cursor', mcp: { file: '.cursor/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.cursor/skills' },
31
38
  { key: 'hermes', name: 'Hermes', mcp: { file: '.hermes/config.yaml', format: 'yaml', path: ['mcp_servers'] }, skills: '.hermes/skills' },
32
39
  { key: 'tianshu', name: 'Tianshu Harness', mcp: { file: '.tianshu/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.tianshu/skills' },
33
40
  { key: 'pi', name: 'Pi Agent', mcp: { file: '.pi/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.pi/skills' },
34
41
  { key: 'workbuddy', name: 'WorkBuddy', mcp: { file: '.workbuddy/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.workbuddy/skills' },
35
- { key: 'opencode', name: 'OpenCode', mcp: { file: '.config/opencode/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.opencode/skills' },
42
+ { key: 'opencode', name: 'OpenCode', home: '.config/opencode', mcp: { file: '.config/opencode/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.opencode/skills' },
36
43
  { key: 'gemini-cli', name: 'Gemini CLI', mcp: { file: '.gemini/settings.json', format: 'json', path: ['mcpServers'] }, skills: '.gemini/skills' },
37
44
  { key: 'deepseek-harness', name: 'DeepSeek Harness', mcp: { file: '.deepseek/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.deepseek/skills' },
38
45
  { key: 'oh-my-pi', name: 'Oh My Pi', mcp: { file: '.oh-my-pi/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.oh-my-pi/skills' },
39
46
  { key: 'trae', name: 'Trae', mcp: { file: '.trae/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.trae/skills' },
47
+ // Reasonix(本机取证 2026-09-25:%APPDATA%/reasonix/config.toml 第 288 行起
48
+ // 「External MCP servers …」+ [[plugins]] name/type/command 实例;该文件注释明示
49
+ // command/args/env/url/headers 支持 ${VAR} 展开 → args 字段存在)。
50
+ {
51
+ key: 'reasonix',
52
+ name: 'Reasonix',
53
+ home: 'AppData/Roaming/reasonix',
54
+ mcp: { file: 'AppData/Roaming/reasonix/config.toml', format: 'toml-aot', section: 'plugins', key: 'name' },
55
+ skills: '.reasonix/skills',
56
+ evidence: ['AppData/Roaming/reasonix-studio.exe', '.reasonix'],
57
+ },
40
58
  ];
41
59
 
42
60
  function homeDir() {
43
61
  return process.env.FREEDOM_AGENT_HOME || os.homedir();
44
62
  }
45
63
 
64
+ // PATH 探测(每轮安装只扫一次,缓存于 Map):返回命中的可执行全路径或 null
65
+ function makeWhich() {
66
+ const dirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
67
+ const exts = process.platform === 'win32'
68
+ ? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.PS1').split(';').concat([''])
69
+ : [''];
70
+ const cache = new Map();
71
+ return (names) => {
72
+ for (const n of names) {
73
+ if (cache.has(n)) {
74
+ const hit = cache.get(n);
75
+ if (hit) return hit;
76
+ continue;
77
+ }
78
+ let found = null;
79
+ for (const d of dirs) {
80
+ for (const e of exts) {
81
+ const cand = path.join(d, n + e);
82
+ try { if (fs.statSync(cand).isFile()) { found = cand; break; } } catch (err) { /* 不在该目录 */ }
83
+ }
84
+ if (found) break;
85
+ }
86
+ cache.set(n, found);
87
+ if (found) return found;
88
+ }
89
+ return null;
90
+ };
91
+ }
92
+
93
+ // 安装检测门:该 agent 是否真的装在本机(只认真实足迹,不认「父目录恒在」)
94
+ // home / evidence 为显式声明的专属目录与足迹,不做推断——'AppData/Roaming' 这类公共父目录恒在,
95
+ // 推断即假阳性(未安装也被判为已安装)。
96
+ function detect(agent, which) {
97
+ const home = homeDir();
98
+ const ev = [];
99
+ if (fs.existsSync(path.join(home, agent.mcp.file))) ev.push(`配置文件 ${agent.mcp.file}`);
100
+ if (agent.home && fs.existsSync(path.join(home, agent.home))) ev.push(`目录 ${agent.home}`);
101
+ for (const rel of agent.evidence || []) {
102
+ if (fs.existsSync(path.join(home, rel))) ev.push(`足迹 ${rel}`);
103
+ }
104
+ if (agent.bin) {
105
+ const exe = which([agent.bin]);
106
+ if (exe) ev.push(`PATH ${path.basename(exe)}`);
107
+ }
108
+ return { installed: ev.length > 0, evidence: ev };
109
+ }
110
+
46
111
  function skillSource() {
47
112
  return path.join(packageRoot(), 'skill', 'freedom');
48
113
  }
@@ -73,20 +138,19 @@ function resolveAgentKeys(raw) {
73
138
  }
74
139
 
75
140
  // ---- 状态探测(全部基于本轮磁盘证据) ----
76
- function probe(agent) {
141
+ function probe(agent, which) {
77
142
  const home = homeDir();
78
143
  const mcpFile = path.join(home, agent.mcp.file);
79
144
  const skillsDir = path.join(home, agent.skills);
80
- // ready = 路径本机取证;convention = 仅 agent 主目录在(按同族约定新建,标注提示);unknown = 无足迹(只给片段)
81
- const mcpState = fs.existsSync(mcpFile) ? 'ready'
82
- : (fs.existsSync(path.dirname(mcpFile)) ? 'convention' : 'unknown');
83
- const skillState = fs.existsSync(skillsDir) ? 'ready'
84
- : (fs.existsSync(path.dirname(skillsDir)) ? 'convention' : 'unknown');
85
- return { mcpFile, skillsDir, mcpState, skillState };
145
+ // ready = 目标文件本机取证;create = 目录不在(安装时新建后合并写入)
146
+ const mcpState = fs.existsSync(mcpFile) ? 'ready' : 'create';
147
+ const skillState = fs.existsSync(skillsDir) ? 'ready' : 'create';
148
+ return { mcpFile, skillsDir, mcpState, skillState, detected: detect(agent, which) };
86
149
  }
87
150
 
88
151
  function matrix() {
89
- return AGENTS.map((a) => ({ key: a.key, name: a.name, ...probe(a) }));
152
+ const which = makeWhich();
153
+ return AGENTS.map((a) => ({ key: a.key, name: a.name, ...probe(a, which) }));
90
154
  }
91
155
 
92
156
  // ---- JSON 配置:按键路径 upsert,保留其余条目 ----
@@ -136,6 +200,47 @@ function upsertToml(text, name, def) {
136
200
  return { text: out.join('\n').replace(/\n*$/, '\n'), had };
137
201
  }
138
202
 
203
+ // ---- TOML 数组表:[[section]] 条目按 key 字段定位(Reasonix 的 [[plugins]] 用 name) ----
204
+ function aotBlock(section, def) {
205
+ const lines = [`[[${section}]]`];
206
+ for (const [k, v] of Object.entries(def)) lines.push(`${k} = ${tomlValue(v)}`);
207
+ return lines;
208
+ }
209
+
210
+ function upsertTomlAoT(text, section, keyField, keyVal, def) {
211
+ const lines = text.split(/\r?\n/);
212
+ const header = `[[${section}]]`;
213
+ const starts = [];
214
+ for (let i = 0; i < lines.length; i++) if (lines[i].trim() === header) starts.push(i);
215
+ const blockEnd = (i) => {
216
+ let j = i + 1;
217
+ while (j < lines.length && !/^\s*\[/.test(lines[j])) j++;
218
+ return j;
219
+ };
220
+ const keyRe = new RegExp(`^\\s*${keyField}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|(.+?))\\s*$`);
221
+ for (const s of starts) {
222
+ let hit = false;
223
+ for (let i = s + 1; i < blockEnd(s); i++) {
224
+ const m = lines[i].match(keyRe);
225
+ if (!m) continue;
226
+ const raw = m[1] !== undefined ? m[1] : m[2] !== undefined ? m[2] : m[3];
227
+ if (String(raw).trim() === keyVal) hit = true;
228
+ }
229
+ if (!hit) continue;
230
+ const out = [...lines.slice(0, s), ...aotBlock(section, def), ...lines.slice(blockEnd(s))];
231
+ return { text: out.join('\n').replace(/\n*$/, '\n'), had: true };
232
+ }
233
+ let out;
234
+ if (starts.length) {
235
+ const last = starts[starts.length - 1];
236
+ const end = blockEnd(last);
237
+ out = [...lines.slice(0, end), '', ...aotBlock(section, def), ...lines.slice(end)];
238
+ } else {
239
+ out = [...lines, '', ...aotBlock(section, def)];
240
+ }
241
+ return { text: out.join('\n').replace(/\n*$/, '\n'), had: false };
242
+ }
243
+
139
244
  // ---- YAML:mcp_servers: 下按缩进 upsert 一个条目 ----
140
245
  function yamlEntry(indent, name, def) {
141
246
  const p = ' '.repeat(indent);
@@ -194,41 +299,59 @@ function backup(file) {
194
299
 
195
300
  function snippet(agent, what) {
196
301
  if (what === 'mcp') {
197
- const body = JSON.stringify({ [SERVER_NAME]: serverDef() }, null, 2);
198
- if (agent.mcp.format === 'toml') {
199
- return [` ${dim('# 无 mcp_servers 块,手工追加到 ~/.codex/config.toml:')}`, ...tomlBlock(SERVER_NAME, serverDef()).trimEnd().split('\n').map((l) => ' ' + dim(l))];
302
+ const fmt = agent.mcp.format;
303
+ if (fmt === 'toml') {
304
+ return [` ${dim(`手工追加到 ${agent.mcp.file}:`)}`, ...tomlBlock(SERVER_NAME, serverDef()).trimEnd().split('\n').map((l) => ' ' + dim(l))];
200
305
  }
306
+ if (fmt === 'toml-aot') {
307
+ const block = aotBlock(agent.mcp.section || 'plugins', { name: SERVER_NAME, type: 'stdio', ...serverDef() });
308
+ return [` ${dim(`手工追加到 ${agent.mcp.file}(数组表按 ${agent.mcp.key || 'name'} 定位条目):`)}`, ...block.map((l) => ' ' + dim(l))];
309
+ }
310
+ const body = JSON.stringify({ [SERVER_NAME]: serverDef() }, null, 2);
201
311
  return [
202
- ` ${dim('目标配置文件未取证,请把以下条目并入你的 MCP 配置(容器键通常是 mcpServers / mcp.servers):')}`,
312
+ ` ${dim('请把以下条目并入你的 MCP 配置(容器键通常是 mcpServers / mcp.servers):')}`,
203
313
  ...body.split('\n').map((l) => ` ${dim(l)}`),
204
314
  ` ${dim(`或指定路径直接写入:freedom mcp install --agent ${agent.key} --config <path> --format json|toml|yaml`)}`,
205
315
  ];
206
316
  }
207
317
  return [
208
- ` ${dim('未发现该 agent 的 skill 目录,手工复制:')}`,
209
- ` ${dim(`${skillSource()} → <agent skills 根目录>/freedom`)}`,
318
+ ` ${dim('手工复制技能目录:')}`,
319
+ ` ${dim(`${skillSource()} → ${path.join(homeDir(), agent.skills)}${'\\'}freedom`)}`,
210
320
  ` ${dim(`或指定目录直接安装:freedom skill install --agent ${agent.key} --skills-dir <path>`)}`,
211
321
  ];
212
322
  }
213
323
 
324
+ // 安装检测门裁定:显式覆写通道 / --force / 足迹命中,三者皆无则不写
325
+ function gateOf(agent, pr, explicit, force) {
326
+ if (explicit) return { pass: true, reason: '' };
327
+ if (force) return { pass: true, reason: `${warn(`未检出安装足迹(${[
328
+ path.join(homeDir(), agent.mcp.file), path.join(homeDir(), agent.skills),
329
+ ].join(' / ')}),按 --force 强行写入;若该 agent 并未安装,请自行删除。`)}` };
330
+ if (pr.detected.installed) return { pass: true, reason: dim(`已检出:${pr.detected.evidence.join(';')}`) };
331
+ return { pass: false, reason: '' };
332
+ }
333
+
214
334
  // ---- 安装主流程 ----
215
- function installMcpOne(agent, { config, format, dryRun }) {
216
- const pr = probe(agent);
335
+ function installMcpOne(agent, { config, format, dryRun, force, which }) {
336
+ const pr = probe(agent, which);
337
+ const gate = gateOf(agent, pr, config, force);
338
+ if (!gate.pass) {
339
+ return { agent, state: 'not-installed', lines: [` ${warn(`${agent.key}: 本机未检出安装足迹 → 依检测门不写入(只给可粘贴片段)`)}`, ...snippet(agent, 'mcp')] };
340
+ }
217
341
  const file = config || pr.mcpFile;
218
342
  const fmt = format || agent.mcp.format;
219
343
  const cpath = agent.mcp.path || ['mcpServers'];
220
344
 
221
- if (!config && pr.mcpState === 'unknown') {
222
- return { agent, state: 'snippet', lines: [` ${warn(`${agent.key}: 未找到配置目录 ${path.dirname(pr.mcpFile)}(未取证,不写入)`)}`, ...snippet(agent, 'mcp')] };
223
- }
224
- const text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : (fmt === 'json' ? '{}\n' : fmt === 'toml' ? '' : '');
345
+ const text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : (fmt === 'json' ? '{}\n' : '');
225
346
  const up = fmt === 'toml' ? upsertToml(text, SERVER_NAME, serverDef())
226
- : fmt === 'yaml' ? upsertYaml(text, cpath, SERVER_NAME, serverDef())
227
- : fmt === 'json' ? upsertJson(text, cpath, SERVER_NAME, serverDef())
228
- : (() => { throw new Error(`不支持的 --format:${fmt}(可选 json|toml|yaml)`); })();
347
+ : fmt === 'toml-aot' ? upsertTomlAoT(text, agent.mcp.section || 'plugins', agent.mcp.key || 'name', SERVER_NAME,
348
+ { name: SERVER_NAME, type: 'stdio', ...serverDef() })
349
+ : fmt === 'yaml' ? upsertYaml(text, cpath, SERVER_NAME, serverDef())
350
+ : fmt === 'json' ? upsertJson(text, cpath, SERVER_NAME, serverDef())
351
+ : (() => { throw new Error(`不支持的 --format:${fmt}(可选 json|toml|toml-aot|yaml)`); })();
229
352
 
230
353
  if (dryRun) {
231
- return { agent, state: up.had ? 'replace(dry)' : 'add(dry)', lines: [` ${dim(`${file} · ${fmt} · ${up.had ? '替换既有条目' : '新增条目'}`)}`] };
354
+ return { agent, state: up.had ? 'replace(dry)' : 'add(dry)', lines: [` ${dim(`${file} · ${fmt} · ${up.had ? '替换既有条目' : '新增条目'}`)} ${gate.reason}`] };
232
355
  }
233
356
  const bak = backup(file);
234
357
  fs.writeFileSync(file, up.text, 'utf8');
@@ -236,37 +359,38 @@ function installMcpOne(agent, { config, format, dryRun }) {
236
359
  agent,
237
360
  state: up.had ? 'replaced' : 'added',
238
361
  lines: [` ${ok(`${file}`)} ${dim(`· ${fmt} · ${up.had ? '已替换既有 freedom 条目' : '已写入'}${bak ? ` · 备份 ${path.basename(bak)}` : ''}`)}`,
239
- ...(config || pr.mcpState === 'ready' ? [] : [` ${warn('该路径按同族约定新建(本机未取证);若该 agent 不读此文件,请用 --config 指定真实路径。')}`]),
362
+ ...(gate.reason ? [` ${gate.reason}`] : []),
240
363
  ` ${dim(`重启 ${agent.name} 后生效;未自动信任前请在该 agent 内确认。`)}`],
241
364
  };
242
365
  }
243
366
 
244
- function installSkillOne(agent, { skillsDir, dryRun }) {
245
- const pr = probe(agent);
367
+ function installSkillOne(agent, { skillsDir, dryRun, force, which }) {
368
+ const pr = probe(agent, which);
246
369
  const src = skillSource();
247
370
  if (!fs.existsSync(src)) throw new Error(`技能源缺失:${src}`);
248
- const destBase = skillsDir || pr.skillsDir;
249
- if (!skillsDir && pr.skillState === 'unknown') {
250
- return { agent, state: 'snippet', lines: [` ${warn(`${agent.key}: 未找到 skill 根目录 ${destBase}(未取证,不写入)`)}`, ...snippet(agent, 'skill')] };
371
+ const gate = gateOf(agent, pr, skillsDir, force);
372
+ if (!gate.pass) {
373
+ return { agent, state: 'not-installed', lines: [` ${warn(`${agent.key}: 本机未检出安装足迹 → 依检测门不写入(只给可粘贴片段)`)}`, ...snippet(agent, 'skill')] };
251
374
  }
375
+ const destBase = skillsDir || pr.skillsDir;
252
376
  const dest = path.join(destBase, 'freedom');
253
- if (dryRun) return { agent, state: 'dry', lines: [` ${dim(`${src} → ${dest}`)}`] };
377
+ if (dryRun) return { agent, state: 'dry', lines: [` ${dim(`${src} → ${dest}`)} ${gate.reason}`] };
254
378
  if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
255
379
  copyDir(src, dest);
256
380
  return {
257
381
  agent,
258
382
  state: 'installed',
259
383
  lines: [` ${ok(dest)}`,
260
- ...(skillsDir || pr.skillState === 'ready' ? [] : [` ${warn('该 skill 根目录按同族约定新建(本机未取证);若不被读取,请用 --skills-dir 指定真实目录。')}`])],
384
+ ...(gate.reason ? [` ${gate.reason}`] : [])],
261
385
  };
262
386
  }
263
387
 
264
- async function install({ what = 'mcp', agent = 'all', dryRun = false, config = null, format = null, skillsDir = null, home = null } = {}) {
388
+ async function install({ what = 'mcp', agent = 'all', dryRun = false, config = null, format = null, skillsDir = null, force = false, home = null } = {}) {
265
389
  const prevHome = process.env.FREEDOM_AGENT_HOME;
266
390
  if (home) process.env.FREEDOM_AGENT_HOME = path.resolve(home);
267
391
  let out;
268
392
  try {
269
- out = await installRun({ what, agent, dryRun, config, format, skillsDir });
393
+ out = await installRun({ what, agent, dryRun, config, format, skillsDir, force });
270
394
  } finally {
271
395
  if (prevHome === undefined) delete process.env.FREEDOM_AGENT_HOME;
272
396
  else process.env.FREEDOM_AGENT_HOME = prevHome;
@@ -274,7 +398,7 @@ async function install({ what = 'mcp', agent = 'all', dryRun = false, config = n
274
398
  return out;
275
399
  }
276
400
 
277
- async function installRun({ what, agent, dryRun, config, format, skillsDir }) {
401
+ async function installRun({ what, agent, dryRun, config, format, skillsDir, force }) {
278
402
  if (config) config = path.resolve(config);
279
403
  if (skillsDir) skillsDir = path.resolve(skillsDir);
280
404
  const keys = resolveAgentKeys(agent);
@@ -282,37 +406,43 @@ async function installRun({ what, agent, dryRun, config, format, skillsDir }) {
282
406
  const results = [];
283
407
  console.log(section(what === 'skill' ? '安装 Freedom 使用技能' : '注册 Freedom MCP 服务'));
284
408
  if (dryRun) console.log(` ${warn('--dry-run 预览模式:不写任何文件')}`);
409
+ const which = makeWhich();
285
410
  for (const key of keys) {
286
411
  const a = agentByKey(key);
287
412
  const fn = what === 'skill' ? installSkillOne : installMcpOne;
288
413
  let r;
289
414
  try {
290
- r = fn(a, { config, format, dryRun, skillsDir });
415
+ r = fn(a, { config, format, dryRun, skillsDir, force, which });
291
416
  } catch (e) {
292
417
  r = { agent: a, state: 'failed', lines: [` ${err(`${key}: ${e.message}`)}`] };
293
418
  }
294
- results.push({ key, state: r.state });
295
- console.log(`${paint(`${r.state.padEnd(9)}`, C.fg.cyan, C.bold)} ${bold(a.name)} ${dim(`(${key})`)}`);
419
+ results.push({ key, state: r.state, detected: probe(a, which).detected.installed });
420
+ console.log(`${paint(`${r.state.padEnd(13)}`, C.fg.cyan, C.bold)} ${bold(a.name)} ${dim(`(${key})`)}`);
296
421
  for (const l of r.lines) console.log(l);
297
422
  }
298
423
  const bad = results.filter((x) => x.state === 'failed').length;
424
+ const skipped = results.filter((x) => x.state === 'not-installed').length;
299
425
  console.log('');
300
- console.log(` ${dim('汇总:')}${results.length} 个 agent${bad ? `,${err(`${bad} 个失败`)}` : ''}${dryRun ? ` ${dim('(预览,未落盘)')}` : ''}`);
426
+ console.log(` ${dim('汇总:')}${results.length} 个 agent${skipped ? `,${paint(`${skipped} 个未检出安装(未写入)`, C.fg.yellow)}` : ''}${bad ? `,${err(`${bad} 个失败`)}` : ''}${dryRun ? ` ${dim('(预览,未落盘)')}` : ''}`);
301
427
  return { results, code: bad ? 1 : 0 };
302
428
  }
303
429
 
304
430
  // 打印支持矩阵(freedom agents)
305
431
  function printMatrix() {
306
432
  console.log(section('Agent 集成支持矩阵'));
307
- console.log(` ${dim('ready=配置文件/技能目录已存在(合并写入) convention=仅主目录在(按同族约定新建并提示) unknown=本机未取证(仅打印片段)')}`);
433
+ console.log(` ${dim('安装门=本机是否检出该 agent 的足迹(配置文件/声明目录/PATH 可执行);未检出只打印片段、不写入(可 --force 覆写)')}`);
434
+ console.log(` ${dim('MCP/SKILL 列:ready=目标已存在(合并写入) create=安装时新建目录后写入')}`);
308
435
  console.log('');
309
436
  for (const r of matrix()) {
310
- const cell = (s) => (s === 'ready' ? `${ok('ready ')}` : s === 'convention' ? `${paint('conventn ', C.fg.yellow)}` : dim('unknown'));
311
- console.log(` ${paint(r.key.padEnd(17), C.fg.cyan, C.bold)} MCP ${cell(r.mcpState)} ${dim(r.mcpFile)} · SKILL ${cell(r.skillState)} ${dim(r.skillsDir)}`);
437
+ const cell = (s) => (s === 'ready' ? `${ok('ready ')}` : paint('create ', C.fg.yellow));
438
+ const gate = r.detected.installed ? ok('已安装') : dim('未检出');
439
+ const basis = r.detected.installed ? dim(`(${r.detected.evidence.join(';')})`) : dim('(需 --force 或 --config/--skills-dir)');
440
+ console.log(` ${paint(r.key.padEnd(17), C.fg.cyan, C.bold)} ${gate} ${basis}`);
441
+ console.log(` ${' '.repeat(18)}MCP ${cell(r.mcpState)} ${dim(r.mcpFile)} · SKILL ${cell(r.skillState)} ${dim(r.skillsDir)}`);
312
442
  }
313
443
  console.log('');
314
- console.log(` ${dim('用法:')}${paint('freedom skill install --agent all', C.fg.white)} / ${paint('freedom mcp install --agent <key> [--dry-run] [--config <path> --format <json|toml|yaml>]', C.fg.white)}`);
444
+ console.log(` ${dim('用法:')}${paint('freedom skill install --agent all', C.fg.white)} / ${paint('freedom mcp install --agent <key> [--dry-run] [--force] [--config <path> --format <json|toml|toml-aot|yaml>]', C.fg.white)}`);
315
445
  return 0;
316
446
  }
317
447
 
318
- module.exports = { install, printMatrix, upsertJson, upsertToml, upsertYaml };
448
+ module.exports = { install, printMatrix, upsertJson, upsertToml, upsertTomlAoT, upsertYaml, detect, makeWhich };
package/lib/build.js CHANGED
@@ -32,6 +32,7 @@ const {
32
32
  isMacPlat,
33
33
  platformExeName,
34
34
  nativePlatform,
35
+ normalizePlatform,
35
36
  localShellPath,
36
37
  } = require('./utils');
37
38
  const { hasShell, downloadShell, validateLocalShell } = require('./shell');
@@ -60,12 +61,12 @@ function run(cmd, args, opts = {}) {
60
61
  return res;
61
62
  }
62
63
 
63
- // --platform 解析:支持逗号 / 中英文逗号 / 空白分隔多平台;win|mac|linux|all 或平台 key(win-x64 等)。
64
+ // --platform 解析:支持逗号 / 中英文逗号 / 空白分隔多平台;win|mac|linux|all 或平台 key
65
+ // (win-x64 等,别名口径与 freedom shell 子命令统一走 utils.normalizePlatform)。
64
66
  // 多平台去重保留顺序。all 仅取可分发平台(DIST_PLATFORMS):linux-arm64 无 CI 资产,
65
67
  // 若列入 all 会在 build 时 404 拖垮整个全量构建(历史 bug B41)。
66
68
  function parsePlatforms(raw) {
67
69
  if (!raw) return [nativePlatform()];
68
- const keyMap = { win: 'win-x64', mac: 'darwin-arm64', linux: 'linux-x64' };
69
70
  const seen = [];
70
71
  const push = (p) => {
71
72
  if (!ALL_PLATFORMS.includes(p)) {
@@ -81,7 +82,7 @@ function parsePlatforms(raw) {
81
82
  if (v === 'all') {
82
83
  for (const p of DIST_PLATFORMS) push(p);
83
84
  } else {
84
- push(keyMap[v] || v);
85
+ push(normalizePlatform(v) || v);
85
86
  }
86
87
  }
87
88
  if (seen.length === 0) {
@@ -140,7 +141,7 @@ async function build(projectDir, opts = {}) {
140
141
  html = fs.readFileSync(distHtml, 'utf8');
141
142
  warnIfNotSingleFile(html, distHtml);
142
143
  }
143
- const configJSON = renderConfigJSON(cfg, name);
144
+ const configJSON = renderConfigJSON(cfg, name, version);
144
145
  // 安全模式:--security 优先于 freedom.config.js 的 security 字段,默认 none。
145
146
  // none = 明文资源(默认,兼容历史产物)
146
147
  // basic = 明文资源 + 构建期安全提示(剥符号 / 混淆,后续可用高模式加密)
@@ -470,7 +471,10 @@ async function buildInstaller({ plat, name, version, targetDir, exeName }) {
470
471
  const artifacts = [];
471
472
 
472
473
  const zipPath = path.join(targetDir, `${name}-${plat}-portable.zip`);
473
- const tmpPath = path.join(path.dirname(targetDir), `.freedom-portable-${process.pid}.zip`);
474
+ // 中间 zip 必须带平台后缀:多平台打包是并发的(emitPlatform 跑在 Promise.all 里),
475
+ // 而各 plat 的 targetDir 同级,只按 pid 命名会让 A 平台的 zipDir 覆写 B 平台正在 rename 的
476
+ // 临时文件——结果是 A 的 portable.zip 里装着 B 的二进制(静默错平台产物)或 ENOENT 崩在成功提示之后。
477
+ const tmpPath = path.join(path.dirname(targetDir), `.freedom-portable-${plat}-${process.pid}.zip`);
474
478
  zipDir(tmpPath, targetDir);
475
479
  await fsp.rename(tmpPath, zipPath);
476
480
  artifacts.push(zipPath);
@@ -530,9 +534,12 @@ function appVersionFromPkg(dir) {
530
534
  }
531
535
 
532
536
  // 渲染 resources/config.json(与壳 resources.go 的 runtimeConfigFile 字段对齐)
533
- function renderConfigJSON(cfg, name) {
537
+ function renderConfigJSON(cfg, name, version) {
534
538
  const obj = {
535
539
  name,
540
+ // 应用自己的版本必须随产物落盘:通用壳编译期注入的 freedom.Version 是「壳」的版本域,
541
+ // 自更新拿它跟应用清单比,结果是「永远 uncomparable」或「下完更新还是同一个壳」。
542
+ version: version || undefined,
536
543
  title: cfg.title || cfg.name || name,
537
544
  titlebar: cfg.titlebar || 'frameless',
538
545
  width: intVal(cfg.width, 1024),
@@ -562,6 +569,14 @@ function renderConfigJSON(cfg, name) {
562
569
  requireSignature: cfg.updater.requireSignature === true ? true : undefined,
563
570
  };
564
571
  }
572
+ // 能力收口透传:壳在 Config.Capabilities 未编译设置时才采用这里的白/黑名单
573
+ // (capability.go 在 sys/tray/window 三桥派发前判定,拒绝零副作用)。
574
+ const caps = cfg.capabilities;
575
+ if (caps && (Array.isArray(caps.allow) || Array.isArray(caps.deny))) {
576
+ const allow = (caps.allow || []).filter((s) => typeof s === 'string' && s);
577
+ const deny = (caps.deny || []).filter((s) => typeof s === 'string' && s);
578
+ if (allow.length || deny.length) obj.capabilities = { allow, deny };
579
+ }
565
580
  return JSON.stringify(obj, null, 2);
566
581
  }
567
582
 
@@ -721,4 +736,4 @@ function stripSourceMapRefs(html) {
721
736
  return { html: out, stripped };
722
737
  }
723
738
 
724
- module.exports = { build, parsePlatforms };
739
+ module.exports = { build, parsePlatforms, renderConfigJSON };