@yufengtadian/freedom-cli 1.13.1 → 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 (76) hide show
  1. package/LICENSE +40 -0
  2. package/README.md +119 -15
  3. package/lib/agents.js +448 -0
  4. package/lib/build.js +189 -45
  5. package/lib/cli.js +258 -9
  6. package/lib/config.js +20 -1
  7. package/lib/desktop.js +133 -0
  8. package/lib/dev.js +149 -0
  9. package/lib/mcp.js +217 -0
  10. package/lib/release.js +90 -0
  11. package/lib/security.js +113 -54
  12. package/lib/shell.js +170 -63
  13. package/lib/toolchain.js +339 -0
  14. package/lib/tui.js +37 -1
  15. package/lib/utils.js +51 -0
  16. package/lib/verify.js +48 -12
  17. package/lib/webkit.js +45 -0
  18. package/package.json +3 -2
  19. package/shell/darwin-arm64/freedom-shell +0 -0
  20. package/shell/linux-x64/freedom-shell +0 -0
  21. package/shell/win-x64/freedom-shell.exe +0 -0
  22. package/skill/freedom/SKILL.md +159 -0
  23. package/templates/desktop/app.html +559 -0
  24. package/templates/desktop/backend/desktop.mjs +327 -0
  25. package/templates/desktop/freedom.config.js +32 -0
  26. package/templates/desktop/icon.ico +0 -0
  27. package/templates/go/go.mod +3 -0
  28. package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
  29. package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
  30. package/templates/go/pkg/freedom/anti_debug_windows.go +238 -11
  31. package/templates/go/pkg/freedom/assets/freedom.js +3 -0
  32. package/templates/go/pkg/freedom/assets_embed.go +1 -0
  33. package/templates/go/pkg/freedom/backend_proc.go +5 -5
  34. package/templates/go/pkg/freedom/events_windows.go +5 -5
  35. package/templates/go/pkg/freedom/freedom.go +64 -10
  36. package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
  37. package/templates/go/pkg/freedom/resources.go +119 -14
  38. package/templates/go/pkg/freedom/securetemp.go +61 -0
  39. package/templates/go/pkg/freedom/securetemp_other.go +20 -0
  40. package/templates/go/pkg/freedom/securetemp_windows.go +25 -0
  41. package/templates/go/pkg/freedom/security.go +293 -64
  42. package/templates/go/pkg/freedom/shutdown.go +55 -0
  43. package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
  44. package/templates/go/pkg/freedom/store.go +11 -3
  45. package/templates/go/pkg/freedom/syscap_windows.go +33 -33
  46. package/templates/go/pkg/freedom/sysint_windows.go +2 -2
  47. package/templates/go/pkg/freedom/tray_windows.go +18 -18
  48. package/templates/go/pkg/freedom/updater.go +3 -3
  49. package/templates/go/pkg/freedom/window_other.go +7 -1
  50. package/templates/go/pkg/freedom/window_windows.go +185 -17
  51. package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
  52. package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
  53. package/templates/go/third_party/webview_go/LICENSE +22 -0
  54. package/templates/go/third_party/webview_go/README.md +50 -0
  55. package/templates/go/third_party/webview_go/glue.c +36 -0
  56. package/templates/go/third_party/webview_go/go.mod +3 -0
  57. package/templates/go/third_party/webview_go/go.sum +0 -0
  58. package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
  59. package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
  60. package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
  61. package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
  62. package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
  63. package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
  64. package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
  65. package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
  66. package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
  67. package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
  68. package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
  69. package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
  70. package/templates/go/third_party/webview_go/webview.cc +1 -0
  71. package/templates/go/third_party/webview_go/webview.go +332 -0
  72. package/templates/installer/app.nsi +43 -0
  73. package/templates/project/freedom.config.js +14 -0
  74. package/templates/project/freedom.d.ts +77 -0
  75. package/templates/project-minimal/freedom.config.js +14 -0
  76. package/templates/project-minimal/freedom.d.ts +77 -0
package/lib/agents.js ADDED
@@ -0,0 +1,448 @@
1
+ 'use strict';
2
+
3
+ // freedom agents / skill / mcp —— 把 Freedom 使用技能与 MCP 服务装进各家 Agent
4
+ //
5
+ // 设计纪律(铁律 19):注册表只登记「候选路径 + 写入格式」,是否可写一律在安装时按磁盘
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
+ // 目录不存在则按需创建后再合并。
14
+ //
15
+ // 幂等:同一 server 名 / 同一 skill 目录重复安装为原地替换,不产生重复条目。
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const { packageRoot, copyDir } = require('./utils');
21
+ const theme = require('./theme');
22
+ const { paint, ok, err, warn, dim, bold, section, C } = theme;
23
+
24
+ const SERVER_NAME = 'freedom';
25
+
26
+ // mcp: { file: 相对 HOME 的配置文件, format: json|toml|yaml|toml-aot, path: JSON/YAML 容器键路径,
27
+ // section: toml-aot 的数组表名, key: toml-aot 用于定位条目的字段 }
28
+ // skills: 相对 HOME 的 skill 根目录(每个 agent 一个)
29
+ // home / evidence: 该 agent 的专属目录与额外安装足迹(可选,安装检测门的证据)
30
+ const AGENTS = [
31
+ { key: 'claude-code', name: 'Claude Code', mcp: { file: '.claude.json', format: 'json', path: ['mcpServers'] }, skills: '.claude/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' },
34
+ { key: 'qoder', name: 'Qoder', mcp: { file: '.qoder/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.qoder/skills' },
35
+ { key: 'codebuddy', name: 'CodeBuddy', mcp: { file: '.codebuddy/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.codebuddy/skills' },
36
+ { key: 'zcode', name: 'Zcode', home: '.zcode', mcp: { file: '.zcode/cli/config.json', format: 'json', path: ['mcp', 'servers'] }, skills: '.zcode/skills' },
37
+ { key: 'cursor', name: 'Cursor', mcp: { file: '.cursor/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.cursor/skills' },
38
+ { key: 'hermes', name: 'Hermes', mcp: { file: '.hermes/config.yaml', format: 'yaml', path: ['mcp_servers'] }, skills: '.hermes/skills' },
39
+ { key: 'tianshu', name: 'Tianshu Harness', mcp: { file: '.tianshu/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.tianshu/skills' },
40
+ { key: 'pi', name: 'Pi Agent', mcp: { file: '.pi/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.pi/skills' },
41
+ { key: 'workbuddy', name: 'WorkBuddy', mcp: { file: '.workbuddy/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.workbuddy/skills' },
42
+ { key: 'opencode', name: 'OpenCode', home: '.config/opencode', mcp: { file: '.config/opencode/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.opencode/skills' },
43
+ { key: 'gemini-cli', name: 'Gemini CLI', mcp: { file: '.gemini/settings.json', format: 'json', path: ['mcpServers'] }, skills: '.gemini/skills' },
44
+ { key: 'deepseek-harness', name: 'DeepSeek Harness', mcp: { file: '.deepseek/mcp.json', format: 'json', path: ['mcpServers'] }, skills: '.deepseek/skills' },
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' },
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
+ },
58
+ ];
59
+
60
+ function homeDir() {
61
+ return process.env.FREEDOM_AGENT_HOME || os.homedir();
62
+ }
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
+
111
+ function skillSource() {
112
+ return path.join(packageRoot(), 'skill', 'freedom');
113
+ }
114
+
115
+ // MCP 服务定义:stdio 拉起 `node <pkg>/bin/freedom.js mcp serve`
116
+ function serverDef() {
117
+ return {
118
+ command: process.execPath,
119
+ args: [path.join(packageRoot(), 'bin', 'freedom.js'), 'mcp', 'serve'],
120
+ };
121
+ }
122
+
123
+ function agentByKey(key) {
124
+ return AGENTS.find((a) => a.key === key) || null;
125
+ }
126
+
127
+ function resolveAgentKeys(raw) {
128
+ const all = AGENTS.map((a) => a.key);
129
+ if (!raw || raw === 'all') return all;
130
+ const out = [];
131
+ for (const seg of String(raw).split(/[,,\s]+/)) {
132
+ if (!seg) continue;
133
+ if (!all.includes(seg)) throw new Error(`未知 agent:${seg}。可选:${all.join(' / ')} 或 all`);
134
+ if (!out.includes(seg)) out.push(seg);
135
+ }
136
+ if (!out.length) throw new Error('--agent 缺少取值。');
137
+ return out;
138
+ }
139
+
140
+ // ---- 状态探测(全部基于本轮磁盘证据) ----
141
+ function probe(agent, which) {
142
+ const home = homeDir();
143
+ const mcpFile = path.join(home, agent.mcp.file);
144
+ const skillsDir = path.join(home, agent.skills);
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) };
149
+ }
150
+
151
+ function matrix() {
152
+ const which = makeWhich();
153
+ return AGENTS.map((a) => ({ key: a.key, name: a.name, ...probe(a, which) }));
154
+ }
155
+
156
+ // ---- JSON 配置:按键路径 upsert,保留其余条目 ----
157
+ function upsertJson(text, containerPath, name, value) {
158
+ let obj;
159
+ try {
160
+ obj = text.trim() ? JSON.parse(text) : {};
161
+ } catch (e) {
162
+ throw new Error(`JSON 配置无法解析(拒绝覆盖以免损坏):${e.message}`);
163
+ }
164
+ let node = obj;
165
+ for (const k of containerPath) {
166
+ if (node[k] === undefined) node[k] = {};
167
+ else if (typeof node[k] !== 'object' || node[k] === null) throw new Error(`配置键 ${k} 已存在且不是对象,拒绝改写。`);
168
+ node = node[k];
169
+ }
170
+ const had = name in node;
171
+ node[name] = value;
172
+ return { text: JSON.stringify(obj, null, 2) + '\n', had };
173
+ }
174
+
175
+ // ---- TOML:[mcp_servers.<name>] 块级替换 / 追加 ----
176
+ function tomlValue(v) {
177
+ if (Array.isArray(v)) return `[${v.map(tomlValue).join(', ')}]`;
178
+ return JSON.stringify(v);
179
+ }
180
+
181
+ function tomlBlock(name, def) {
182
+ const lines = [`[mcp_servers.${name}]`];
183
+ for (const [k, v] of Object.entries(def)) lines.push(`${k} = ${tomlValue(v)}`);
184
+ return lines.join('\n') + '\n';
185
+ }
186
+
187
+ function upsertToml(text, name, def) {
188
+ const lines = text.split(/\r?\n/);
189
+ const header = `[mcp_servers.${name}]`;
190
+ const start = lines.findIndex((l) => l.trim() === header);
191
+ const had = start >= 0;
192
+ let out;
193
+ if (had) {
194
+ let end = start + 1;
195
+ while (end < lines.length && !/^\s*\[/.test(lines[end])) end++;
196
+ out = [...lines.slice(0, start), ...tomlBlock(name, def).trimEnd().split('\n'), ...lines.slice(end)];
197
+ } else {
198
+ out = [...lines, '', ...tomlBlock(name, def).trimEnd().split('\n')];
199
+ }
200
+ return { text: out.join('\n').replace(/\n*$/, '\n'), had };
201
+ }
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
+
244
+ // ---- YAML:mcp_servers: 下按缩进 upsert 一个条目 ----
245
+ function yamlEntry(indent, name, def) {
246
+ const p = ' '.repeat(indent);
247
+ const lines = [`${p}${name}:`];
248
+ lines.push(`${p} type: stdio`);
249
+ lines.push(`${p} command: ${JSON.stringify(def.command)}`);
250
+ if (Array.isArray(def.args) && def.args.length) {
251
+ lines.push(`${p} args:`);
252
+ for (const a of def.args) lines.push(`${p} - ${JSON.stringify(a)}`);
253
+ }
254
+ lines.push(`${p} enabled: true`);
255
+ return lines;
256
+ }
257
+
258
+ function upsertYaml(text, containerPath, name, def) {
259
+ const lines = text.split(/\r?\n/);
260
+ const key = containerPath[containerPath.length - 1];
261
+ const head = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l));
262
+ const entry = (ownIndent) => yamlEntry(ownIndent, name, def);
263
+ if (head < 0) {
264
+ return { text: lines.join('\n').replace(/\n*$/, '\n') + `\n${key}:\n` + entry(2).join('\n') + '\n', had: false };
265
+ }
266
+ // 既有子条目缩进(取 head 之后第一条非空行);无子条目时用 2
267
+ let own = 2;
268
+ for (let i = head + 1; i < lines.length; i++) {
269
+ if (!lines[i].trim()) continue;
270
+ const m = lines[i].match(/^(\s+)/);
271
+ own = m ? m[1].length : 0;
272
+ break;
273
+ }
274
+ const start = lines.findIndex((l, i) => i > head && new RegExp(`^ {${own}}${name}:`).test(l));
275
+ const had = start >= 0;
276
+ let out;
277
+ if (had) {
278
+ let end = start + 1;
279
+ while (end < lines.length && (!lines[end].trim() || indentOf(lines[end]) > own)) end++;
280
+ out = [...lines.slice(0, start), ...entry(own), ...lines.slice(end)];
281
+ } else {
282
+ out = [...lines.slice(0, head + 1), ...entry(own), ...lines.slice(head + 1)];
283
+ }
284
+ return { text: out.join('\n').replace(/\n*$/, '\n'), had };
285
+ }
286
+
287
+ function indentOf(line) {
288
+ const m = line.match(/^(\s+)/);
289
+ return m ? m[1].length : 0;
290
+ }
291
+
292
+ function backup(file) {
293
+ fs.mkdirSync(path.dirname(file), { recursive: true });
294
+ if (!fs.existsSync(file)) return null;
295
+ const bak = `${file}.bak`;
296
+ fs.copyFileSync(file, bak);
297
+ return bak;
298
+ }
299
+
300
+ function snippet(agent, what) {
301
+ if (what === 'mcp') {
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))];
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);
311
+ return [
312
+ ` ${dim('请把以下条目并入你的 MCP 配置(容器键通常是 mcpServers / mcp.servers):')}`,
313
+ ...body.split('\n').map((l) => ` ${dim(l)}`),
314
+ ` ${dim(`或指定路径直接写入:freedom mcp install --agent ${agent.key} --config <path> --format json|toml|yaml`)}`,
315
+ ];
316
+ }
317
+ return [
318
+ ` ${dim('手工复制技能目录:')}`,
319
+ ` ${dim(`${skillSource()} → ${path.join(homeDir(), agent.skills)}${'\\'}freedom`)}`,
320
+ ` ${dim(`或指定目录直接安装:freedom skill install --agent ${agent.key} --skills-dir <path>`)}`,
321
+ ];
322
+ }
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
+
334
+ // ---- 安装主流程 ----
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
+ }
341
+ const file = config || pr.mcpFile;
342
+ const fmt = format || agent.mcp.format;
343
+ const cpath = agent.mcp.path || ['mcpServers'];
344
+
345
+ const text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : (fmt === 'json' ? '{}\n' : '');
346
+ const up = fmt === 'toml' ? upsertToml(text, SERVER_NAME, serverDef())
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)`); })();
352
+
353
+ if (dryRun) {
354
+ return { agent, state: up.had ? 'replace(dry)' : 'add(dry)', lines: [` ${dim(`${file} · ${fmt} · ${up.had ? '替换既有条目' : '新增条目'}`)} ${gate.reason}`] };
355
+ }
356
+ const bak = backup(file);
357
+ fs.writeFileSync(file, up.text, 'utf8');
358
+ return {
359
+ agent,
360
+ state: up.had ? 'replaced' : 'added',
361
+ lines: [` ${ok(`${file}`)} ${dim(`· ${fmt} · ${up.had ? '已替换既有 freedom 条目' : '已写入'}${bak ? ` · 备份 ${path.basename(bak)}` : ''}`)}`,
362
+ ...(gate.reason ? [` ${gate.reason}`] : []),
363
+ ` ${dim(`重启 ${agent.name} 后生效;未自动信任前请在该 agent 内确认。`)}`],
364
+ };
365
+ }
366
+
367
+ function installSkillOne(agent, { skillsDir, dryRun, force, which }) {
368
+ const pr = probe(agent, which);
369
+ const src = skillSource();
370
+ if (!fs.existsSync(src)) throw new Error(`技能源缺失:${src}`);
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')] };
374
+ }
375
+ const destBase = skillsDir || pr.skillsDir;
376
+ const dest = path.join(destBase, 'freedom');
377
+ if (dryRun) return { agent, state: 'dry', lines: [` ${dim(`${src} → ${dest}`)} ${gate.reason}`] };
378
+ if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
379
+ copyDir(src, dest);
380
+ return {
381
+ agent,
382
+ state: 'installed',
383
+ lines: [` ${ok(dest)}`,
384
+ ...(gate.reason ? [` ${gate.reason}`] : [])],
385
+ };
386
+ }
387
+
388
+ async function install({ what = 'mcp', agent = 'all', dryRun = false, config = null, format = null, skillsDir = null, force = false, home = null } = {}) {
389
+ const prevHome = process.env.FREEDOM_AGENT_HOME;
390
+ if (home) process.env.FREEDOM_AGENT_HOME = path.resolve(home);
391
+ let out;
392
+ try {
393
+ out = await installRun({ what, agent, dryRun, config, format, skillsDir, force });
394
+ } finally {
395
+ if (prevHome === undefined) delete process.env.FREEDOM_AGENT_HOME;
396
+ else process.env.FREEDOM_AGENT_HOME = prevHome;
397
+ }
398
+ return out;
399
+ }
400
+
401
+ async function installRun({ what, agent, dryRun, config, format, skillsDir, force }) {
402
+ if (config) config = path.resolve(config);
403
+ if (skillsDir) skillsDir = path.resolve(skillsDir);
404
+ const keys = resolveAgentKeys(agent);
405
+ if (config && keys.length > 1) throw new Error('--config 指向单一目标文件,只能配合 --agent <单个 key> 使用。');
406
+ const results = [];
407
+ console.log(section(what === 'skill' ? '安装 Freedom 使用技能' : '注册 Freedom MCP 服务'));
408
+ if (dryRun) console.log(` ${warn('--dry-run 预览模式:不写任何文件')}`);
409
+ const which = makeWhich();
410
+ for (const key of keys) {
411
+ const a = agentByKey(key);
412
+ const fn = what === 'skill' ? installSkillOne : installMcpOne;
413
+ let r;
414
+ try {
415
+ r = fn(a, { config, format, dryRun, skillsDir, force, which });
416
+ } catch (e) {
417
+ r = { agent: a, state: 'failed', lines: [` ${err(`${key}: ${e.message}`)}`] };
418
+ }
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})`)}`);
421
+ for (const l of r.lines) console.log(l);
422
+ }
423
+ const bad = results.filter((x) => x.state === 'failed').length;
424
+ const skipped = results.filter((x) => x.state === 'not-installed').length;
425
+ console.log('');
426
+ console.log(` ${dim('汇总:')}${results.length} 个 agent${skipped ? `,${paint(`${skipped} 个未检出安装(未写入)`, C.fg.yellow)}` : ''}${bad ? `,${err(`${bad} 个失败`)}` : ''}${dryRun ? ` ${dim('(预览,未落盘)')}` : ''}`);
427
+ return { results, code: bad ? 1 : 0 };
428
+ }
429
+
430
+ // 打印支持矩阵(freedom agents)
431
+ function printMatrix() {
432
+ console.log(section('Agent 集成支持矩阵'));
433
+ console.log(` ${dim('安装门=本机是否检出该 agent 的足迹(配置文件/声明目录/PATH 可执行);未检出只打印片段、不写入(可 --force 覆写)')}`);
434
+ console.log(` ${dim('MCP/SKILL 列:ready=目标已存在(合并写入) create=安装时新建目录后写入')}`);
435
+ console.log('');
436
+ for (const r of matrix()) {
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)}`);
442
+ }
443
+ console.log('');
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)}`);
445
+ return 0;
446
+ }
447
+
448
+ module.exports = { install, printMatrix, upsertJson, upsertToml, upsertTomlAoT, upsertYaml, detect, makeWhich };