@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.
- package/LICENSE +40 -0
- package/README.md +119 -15
- package/lib/agents.js +448 -0
- package/lib/build.js +189 -45
- package/lib/cli.js +258 -9
- package/lib/config.js +20 -1
- package/lib/desktop.js +133 -0
- package/lib/dev.js +149 -0
- package/lib/mcp.js +217 -0
- package/lib/release.js +90 -0
- package/lib/security.js +113 -54
- package/lib/shell.js +170 -63
- package/lib/toolchain.js +339 -0
- package/lib/tui.js +37 -1
- package/lib/utils.js +51 -0
- package/lib/verify.js +48 -12
- package/lib/webkit.js +45 -0
- package/package.json +3 -2
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/skill/freedom/SKILL.md +159 -0
- package/templates/desktop/app.html +559 -0
- package/templates/desktop/backend/desktop.mjs +327 -0
- package/templates/desktop/freedom.config.js +32 -0
- package/templates/desktop/icon.ico +0 -0
- package/templates/go/go.mod +3 -0
- package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
- package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +238 -11
- package/templates/go/pkg/freedom/assets/freedom.js +3 -0
- package/templates/go/pkg/freedom/assets_embed.go +1 -0
- package/templates/go/pkg/freedom/backend_proc.go +5 -5
- package/templates/go/pkg/freedom/events_windows.go +5 -5
- package/templates/go/pkg/freedom/freedom.go +64 -10
- package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
- package/templates/go/pkg/freedom/resources.go +119 -14
- package/templates/go/pkg/freedom/securetemp.go +61 -0
- package/templates/go/pkg/freedom/securetemp_other.go +20 -0
- package/templates/go/pkg/freedom/securetemp_windows.go +25 -0
- package/templates/go/pkg/freedom/security.go +293 -64
- package/templates/go/pkg/freedom/shutdown.go +55 -0
- package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
- package/templates/go/pkg/freedom/store.go +11 -3
- package/templates/go/pkg/freedom/syscap_windows.go +33 -33
- package/templates/go/pkg/freedom/sysint_windows.go +2 -2
- package/templates/go/pkg/freedom/tray_windows.go +18 -18
- package/templates/go/pkg/freedom/updater.go +3 -3
- package/templates/go/pkg/freedom/window_other.go +7 -1
- package/templates/go/pkg/freedom/window_windows.go +185 -17
- package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
- package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
- package/templates/go/third_party/webview_go/LICENSE +22 -0
- package/templates/go/third_party/webview_go/README.md +50 -0
- package/templates/go/third_party/webview_go/glue.c +36 -0
- package/templates/go/third_party/webview_go/go.mod +3 -0
- package/templates/go/third_party/webview_go/go.sum +0 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
- package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
- package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
- package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
- package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
- package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
- package/templates/go/third_party/webview_go/webview.cc +1 -0
- package/templates/go/third_party/webview_go/webview.go +332 -0
- package/templates/installer/app.nsi +43 -0
- package/templates/project/freedom.config.js +14 -0
- package/templates/project/freedom.d.ts +77 -0
- package/templates/project-minimal/freedom.config.js +14 -0
- package/templates/project-minimal/freedom.d.ts +77 -0
package/lib/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const { init } = require('./init');
|
|
6
6
|
const { build } = require('./build');
|
|
7
7
|
const { setConfig, showConfig } = require('./config');
|
|
8
|
-
const { packageRoot, tutorialFile } = require('./utils');
|
|
8
|
+
const { packageRoot, tutorialFile, normalizePlatform } = require('./utils');
|
|
9
9
|
const theme = require('./theme');
|
|
10
10
|
const update = require('./update');
|
|
11
11
|
|
|
@@ -19,8 +19,11 @@ function help() {
|
|
|
19
19
|
L.push(` ${paint('用法:', C.bold, C.fg.white)} ${paint('freedom <command> [options]', C.fg.cyan, C.bold)}`);
|
|
20
20
|
L.push('');
|
|
21
21
|
L.push(section('交互式界面'));
|
|
22
|
-
L.push(` ${paint('freedom', C.fg.cyan, C.bold)} ${dim('
|
|
23
|
-
L.push(` ${paint('freedom tui', C.fg.cyan, C.bold)} ${dim('
|
|
22
|
+
L.push(` ${paint('freedom', C.fg.cyan, C.bold)} ${dim('选择显示方式:终端 TUI / Freedom Desktop(图形界面)')}`);
|
|
23
|
+
L.push(` ${paint('freedom tui', C.fg.cyan, C.bold)} ${dim('直达终端 TUI(交互式菜单,新建 / 打包 / 配置 / 壳管理)')}`);
|
|
24
|
+
L.push(` ${paint('freedom desktop', C.fg.cyan, C.bold)} ${dim('直达 Freedom Desktop:图形界面由 freedom 自己打包产出,模板或版本变更时自动重打包')}`);
|
|
25
|
+
L.push(` ${dim('--rebuild')} ${dim('强制重新打包 Desktop 并拉起(需先关闭已开窗口)')}`);
|
|
26
|
+
L.push(` ${dim('--no-launch')} ${dim('只准备 / 重打包产物,不拉起窗口')}`);
|
|
24
27
|
L.push(section('项目'));
|
|
25
28
|
L.push(` ${paint('freedom init [目录] [--force] [--template full|minimal]', C.fg.cyan)} ${dim('新建项目模板(默认 full:含自绘标题栏示例;minimal:极简无自绘标题栏)')}`);
|
|
26
29
|
L.push(` ${paint('freedom tutorial', C.fg.cyan)} ${dim('再次打开安装教程')}`);
|
|
@@ -28,8 +31,14 @@ function help() {
|
|
|
28
31
|
L.push(` ${paint('freedom build [--platform <p>]', C.fg.cyan)} ${dim('前端打包并分发桌面应用(默认当前平台)')}`);
|
|
29
32
|
L.push(` ${dim('--platform win|mac|linux|all')} ${dim('指定目标平台(all = 三平台全量)')}`);
|
|
30
33
|
L.push(` ${dim('--no-cache')} ${dim('忽略前端构建缓存,强制重新打包')}`);
|
|
34
|
+
L.push(` ${dim('--security <none|basic|high>')} ${dim('资源安全模式(high = resources 加密为 app.bin)')}`);
|
|
35
|
+
L.push(` ${dim('--installer')} ${dim('附带安装包产物:便携 zip + Windows NSIS 安装器(有 makensis 时编译 setup.exe,否则产出已填充的 .nsi)')}`);
|
|
31
36
|
L.push(` ${paint('freedom verify [--platform <p>]', C.fg.cyan)} ${dim('校验已构建产物完整性并打印产物结构(CI 可依赖退出码)')}`);
|
|
32
37
|
L.push(` ${paint('freedom dmg [--platform <plat>]', C.fg.cyan)} ${dim('将已构建的 .app 打包为 .dmg(需 macOS)')}`);
|
|
38
|
+
L.push(section('开发与更新'));
|
|
39
|
+
L.push(` ${paint('freedom dev [--port <n>|--url <u>] [--command <cmd>]', C.fg.cyan)} ${dim('dev server 联调壳窗口:HMR 热更,改码免重打包')}`);
|
|
40
|
+
L.push(` ${paint('freedom keygen', C.fg.cyan)} ${dim('生成应用自更新 ed25519 密钥对(公钥进配置,私钥发布方保管)')}`);
|
|
41
|
+
L.push(` ${paint('freedom manifest --artifact <产物> --url <下载地址> [--version x]', C.fg.cyan)} ${dim('产出签名的更新清单 latest.json')}`);
|
|
33
42
|
L.push(section('外观'));
|
|
34
43
|
L.push(` ${paint('freedom titlebar <native|frameless>', C.fg.cyan)} ${dim('一键切换标题栏策略')}`);
|
|
35
44
|
L.push(` ${paint('freedom icon <path>', C.fg.cyan)} ${dim('设置应用图标(Win .ico / mac .icns)')}`);
|
|
@@ -41,6 +50,17 @@ function help() {
|
|
|
41
50
|
L.push(` ${paint('freedom config', C.fg.cyan)} ${dim('查看当前配置')}`);
|
|
42
51
|
L.push(` ${paint('freedom config get <key>', C.fg.cyan)} ${dim('读取单个配置项')}`);
|
|
43
52
|
L.push(` ${paint('freedom config set <key> <value>', C.fg.cyan)} ${dim('修改单个配置项')}`);
|
|
53
|
+
L.push(section('Agent 集成'));
|
|
54
|
+
L.push(` ${paint('freedom agents [--home <dir>]', C.fg.cyan)} ${dim('打印 Agent 支持矩阵(按本机磁盘证据判定 ready / convention / unknown)')}`);
|
|
55
|
+
L.push(` ${paint('freedom agents install --what <mcp|skill> --agent <key|all>', C.fg.cyan)} ${dim('等价于下面两条的合并入口')}`);
|
|
56
|
+
L.push(` ${paint('freedom skill install --agent <key|all> [--dry-run] [--skills-dir <path>]', C.fg.cyan)} ${dim('把 Freedom 使用技能装入 agent 的 skills 目录')}`);
|
|
57
|
+
L.push(` ${paint('freedom mcp install --agent <key|all> [--dry-run] [--config <path> --format json|toml|yaml]', C.fg.cyan)} ${dim('把 Freedom MCP 服务写入 agent 的 MCP 配置(幂等合并、写前 .bak 备份)')}`);
|
|
58
|
+
L.push(` ${paint('freedom mcp serve', C.fg.cyan)} ${dim('stdio MCP 服务本体(一般由 agent 自动拉起,无需手敲)')}`);
|
|
59
|
+
L.push(section('工具链'));
|
|
60
|
+
L.push(` ${paint('freedom toolchain', C.fg.cyan)} ${dim('探测 Go / Rust / C++ 工具链是否就位(已配置好则走缓存、不重复探测)')}`);
|
|
61
|
+
L.push(` ${paint('freedom toolchain install [go,rust,cpp|missing] [--apply]', C.fg.cyan)} ${dim('缺失时自动安装;缺省只打印命令,--apply 才真跑(联网 + 提权)')}`);
|
|
62
|
+
L.push(` ${paint('freedom toolchain optimize [--apply]', C.fg.cyan)} ${dim('自动优化配置:Go GOPROXY 镜像、cargo 源换 rsproxy(幂等 + .bak)')}`);
|
|
63
|
+
L.push(` ${paint('freedom toolchain status --refresh', C.fg.cyan)} ${dim('强制重新探测并刷新缓存(默认缓存 7 天,PATH 变更即失效)')}`);
|
|
44
64
|
L.push(section('版本'));
|
|
45
65
|
L.push(` ${paint('freedom version', C.fg.cyan)} ${dim('显示版本并检测最新版本')}`);
|
|
46
66
|
L.push(` ${paint('freedom update', C.fg.cyan)} ${dim('检查新版本并立即自动更新')}`);
|
|
@@ -66,9 +86,10 @@ async function run(argv) {
|
|
|
66
86
|
|
|
67
87
|
switch (cmd) {
|
|
68
88
|
case undefined: {
|
|
69
|
-
//
|
|
89
|
+
// 无参数:交互式终端先选择显示方式(终端 TUI / Freedom Desktop);
|
|
90
|
+
// 管道 / 脚本环境打印帮助,避免卡死
|
|
70
91
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
71
|
-
return await require('./tui').
|
|
92
|
+
return await require('./tui').pickDisplay(process.cwd());
|
|
72
93
|
}
|
|
73
94
|
console.log(help());
|
|
74
95
|
return 0;
|
|
@@ -112,6 +133,52 @@ async function run(argv) {
|
|
|
112
133
|
case 'tui':
|
|
113
134
|
return await require('./tui').tui(process.cwd());
|
|
114
135
|
|
|
136
|
+
case 'desktop': {
|
|
137
|
+
const { desktop } = require('./desktop');
|
|
138
|
+
try {
|
|
139
|
+
const r = await desktop({
|
|
140
|
+
rebuild: rest.includes('--rebuild'),
|
|
141
|
+
noLaunch: rest.includes('--no-launch'),
|
|
142
|
+
});
|
|
143
|
+
console.log(`${ok(r.rebuilt ? '已重新打包 Freedom Desktop:' : '复用已有产物:')}${paint(r.exe, C.fg.cyan, C.bold)}`);
|
|
144
|
+
console.log(` ${dim(`项目目录:${r.dir} · CLI v${r.version}`)}`);
|
|
145
|
+
if (r.launched) console.log(` ${ok(`窗口已拉起(pid ${r.pid})`)}`);
|
|
146
|
+
else console.log(` ${dim('未拉起窗口(--no-launch);执行 freedom desktop 即可打开。')}`);
|
|
147
|
+
return 0;
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.error(`${err(e.message)}`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
case 'agents': {
|
|
155
|
+
const sub = rest[0] && !rest[0].startsWith('--') ? rest[0] : undefined;
|
|
156
|
+
const flags = sub ? rest.slice(1) : rest;
|
|
157
|
+
const hi = flags.indexOf('--home');
|
|
158
|
+
const home = flags.find((a) => a.startsWith('--home=')) || (hi >= 0 ? flags[hi + 1] : undefined);
|
|
159
|
+
if (home && !home.startsWith('--')) process.env.FREEDOM_AGENT_HOME = path.resolve(home.replace(/^--home=/, ''));
|
|
160
|
+
if (!sub || sub === 'list') return require('./agents').printMatrix();
|
|
161
|
+
if (sub !== 'install') {
|
|
162
|
+
console.error(`${err(`未知 agents 子命令:${sub}`)} ${dim('可用:list / install --what <mcp|skill> --agent <key|all> [--dry-run] [--force]')}`);
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
const wi = flags.indexOf('--what');
|
|
166
|
+
const what = (flags.find((a) => a.startsWith('--what=')) || '').split('=')[1] || flags[wi + 1];
|
|
167
|
+
if (what && what !== 'mcp' && what !== 'skill') {
|
|
168
|
+
console.error(`${err(`不支持的 --what:${what}`)} ${dim('(可选 mcp|skill)')}`);
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
return await runAgents(what === 'skill' ? 'skill' : 'mcp', ['install', ...flags]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case 'toolchain':
|
|
175
|
+
case 'tc':
|
|
176
|
+
return toolchainCommand(rest);
|
|
177
|
+
|
|
178
|
+
case 'skill':
|
|
179
|
+
case 'mcp':
|
|
180
|
+
return await runAgents(cmd, rest);
|
|
181
|
+
|
|
115
182
|
case 'init': {
|
|
116
183
|
const force = rest.includes('--force');
|
|
117
184
|
const tmplArg = rest.find((a) => a.startsWith('--template'));
|
|
@@ -140,6 +207,62 @@ async function run(argv) {
|
|
|
140
207
|
return 0;
|
|
141
208
|
}
|
|
142
209
|
|
|
210
|
+
case 'dev': {
|
|
211
|
+
const optVal = (name) => {
|
|
212
|
+
const a = rest.find((x) => x.startsWith(`--${name}=`));
|
|
213
|
+
if (a) return a.split('=')[1];
|
|
214
|
+
const i = rest.indexOf(`--${name}`);
|
|
215
|
+
return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[i + 1] : undefined;
|
|
216
|
+
};
|
|
217
|
+
const url = optVal('url');
|
|
218
|
+
const port = optVal('port');
|
|
219
|
+
const { dev: runDev } = require('./dev');
|
|
220
|
+
try {
|
|
221
|
+
const done = await runDev({ dir: process.cwd(), url, port: port ? Number(port) : undefined, command: optVal('command') });
|
|
222
|
+
console.log(`${ok(String(done))}`);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
console.error(`${err(e.message)}`);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
case 'keygen': {
|
|
231
|
+
const { keygen } = require('./release');
|
|
232
|
+
try {
|
|
233
|
+
console.log(await keygen({ dir: process.cwd(), force: rest.includes('--force') }));
|
|
234
|
+
} catch (e) {
|
|
235
|
+
console.error(`${err(e.message)}`);
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case 'manifest': {
|
|
242
|
+
const optVal = (name) => {
|
|
243
|
+
const a = rest.find((x) => x.startsWith(`--${name}=`));
|
|
244
|
+
if (a) return a.split('=')[1];
|
|
245
|
+
const i = rest.indexOf(`--${name}`);
|
|
246
|
+
return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[i + 1] : undefined;
|
|
247
|
+
};
|
|
248
|
+
const { manifest } = require('./release');
|
|
249
|
+
try {
|
|
250
|
+
console.log(await manifest({
|
|
251
|
+
dir: process.cwd(),
|
|
252
|
+
artifact: optVal('artifact'),
|
|
253
|
+
url: optVal('url'),
|
|
254
|
+
version: optVal('version'),
|
|
255
|
+
notes: optVal('notes'),
|
|
256
|
+
key: optVal('key'),
|
|
257
|
+
out: optVal('out'),
|
|
258
|
+
}));
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.error(`${err(e.message)}`);
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
143
266
|
case 'build': {
|
|
144
267
|
const platArg = rest.find((a) => a.startsWith('--platform') || a.startsWith('-p'));
|
|
145
268
|
let platform;
|
|
@@ -171,9 +294,13 @@ async function run(argv) {
|
|
|
171
294
|
platform,
|
|
172
295
|
noCache: rest.includes('--no-cache'),
|
|
173
296
|
security,
|
|
297
|
+
installer: rest.includes('--installer'),
|
|
174
298
|
});
|
|
175
299
|
for (const r of results) {
|
|
176
300
|
console.log(`${ok('构建完成')} ${paint(`[${r.plat}]`, C.fg.magenta, C.bold)} ${paint(r.outFile, C.fg.white)}`);
|
|
301
|
+
for (const p of r.installers || []) {
|
|
302
|
+
console.log(` ${dim('安装包产物:')}${paint(p, C.fg.white)}`);
|
|
303
|
+
}
|
|
177
304
|
}
|
|
178
305
|
return 0;
|
|
179
306
|
}
|
|
@@ -344,21 +471,21 @@ async function runShell(rest) {
|
|
|
344
471
|
case 'download': {
|
|
345
472
|
const plat = rest[1];
|
|
346
473
|
if (!plat) {
|
|
347
|
-
console.error(`${err('用法:')}${paint('freedom shell download <win-x64
|
|
474
|
+
console.error(`${err('用法:')}${paint('freedom shell download <win|mac|linux|all 或 win-x64 / darwin-arm64 / linux-x64 / linux-arm64>', C.fg.cyan)}`);
|
|
348
475
|
return 1;
|
|
349
476
|
}
|
|
350
477
|
const dest = await downloadShell(plat);
|
|
351
|
-
console.log(`${ok('已下载')} ${paint(plat, C.fg.magenta, C.bold)} ${dim('壳:')}${paint(dest, C.fg.white)}`);
|
|
478
|
+
console.log(`${ok('已下载')} ${paint(normalizePlatform(plat) || plat, C.fg.magenta, C.bold)} ${dim('壳:')}${paint(dest, C.fg.white)}`);
|
|
352
479
|
return 0;
|
|
353
480
|
}
|
|
354
481
|
case 'build': {
|
|
355
482
|
const plat = rest[1];
|
|
356
483
|
if (!plat) {
|
|
357
|
-
console.error(`${err('用法:')}${paint('freedom shell build <win-x64
|
|
484
|
+
console.error(`${err('用法:')}${paint('freedom shell build <win|mac|linux 或 win-x64 / darwin-arm64 / linux-x64 / linux-arm64>', C.fg.cyan)}`);
|
|
358
485
|
return 1;
|
|
359
486
|
}
|
|
360
487
|
const dest = buildShell(plat);
|
|
361
|
-
console.log(`${ok('已编译')} ${paint(plat, C.fg.magenta, C.bold)} ${dim('壳:')}${paint(dest, C.fg.white)}`);
|
|
488
|
+
console.log(`${ok('已编译')} ${paint(normalizePlatform(plat) || plat, C.fg.magenta, C.bold)} ${dim('壳:')}${paint(dest, C.fg.white)}`);
|
|
362
489
|
console.log(` ${dim('提示:壳已预编译随包分发,一般无需本地编译。')}`);
|
|
363
490
|
return 0;
|
|
364
491
|
}
|
|
@@ -368,6 +495,45 @@ async function runShell(rest) {
|
|
|
368
495
|
}
|
|
369
496
|
}
|
|
370
497
|
|
|
498
|
+
// freedom skill / mcp —— Agent 集成安装与 MCP 服务
|
|
499
|
+
async function runAgents(cmd, rest) {
|
|
500
|
+
const sub = rest[0];
|
|
501
|
+
const optVal = (name) => {
|
|
502
|
+
const a = rest.find((x) => x.startsWith(`--${name}=`));
|
|
503
|
+
if (a) return a.split('=')[1];
|
|
504
|
+
const i = rest.indexOf(`--${name}`);
|
|
505
|
+
return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[i + 1] : undefined;
|
|
506
|
+
};
|
|
507
|
+
if (cmd === 'mcp' && sub === 'serve') {
|
|
508
|
+
return await require('./mcp').serve();
|
|
509
|
+
}
|
|
510
|
+
if (sub !== 'install') {
|
|
511
|
+
console.error(`${err('用法:')}${paint(`freedom ${cmd} install --agent <key|all> [--dry-run] [--force]`, C.fg.cyan)}`);
|
|
512
|
+
if (cmd === 'mcp') {
|
|
513
|
+
console.error(` ${paint('freedom mcp serve', C.fg.cyan)} ${dim('(stdio MCP 服务,通常由 agent 配置自动拉起)')}`);
|
|
514
|
+
}
|
|
515
|
+
console.error(` ${dim('可选 agent 与本机取证状态见:')}${paint('freedom agents', C.fg.white)}`);
|
|
516
|
+
return 1;
|
|
517
|
+
}
|
|
518
|
+
const { install } = require('./agents');
|
|
519
|
+
try {
|
|
520
|
+
const r = await install({
|
|
521
|
+
what: cmd === 'skill' ? 'skill' : 'mcp',
|
|
522
|
+
agent: optVal('agent') || 'all',
|
|
523
|
+
dryRun: rest.includes('--dry-run'),
|
|
524
|
+
config: optVal('config'),
|
|
525
|
+
format: optVal('format'),
|
|
526
|
+
skillsDir: optVal('skills-dir'),
|
|
527
|
+
force: rest.includes('--force'),
|
|
528
|
+
home: optVal('home'),
|
|
529
|
+
});
|
|
530
|
+
return r.code;
|
|
531
|
+
} catch (e) {
|
|
532
|
+
console.error(`${err(e.message)}`);
|
|
533
|
+
return 1;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
371
537
|
// freedom dmg:在 macOS 上把已构建的 .app 打包为 .dmg(依赖系统 hdiutil)
|
|
372
538
|
async function runDmg(rest) {
|
|
373
539
|
const { loadConfig, nativePlatform } = require('./utils');
|
|
@@ -407,6 +573,89 @@ async function runDmg(rest) {
|
|
|
407
573
|
}
|
|
408
574
|
}
|
|
409
575
|
|
|
576
|
+
// freedom toolchain —— 工具链自动装配:探测(带缓存)/ 安装(默认只打印)/ 配置优化。
|
|
577
|
+
function toolchainCommand(rest) {
|
|
578
|
+
const tc = require('./toolchain');
|
|
579
|
+
const flags = new Set();
|
|
580
|
+
const pos = [];
|
|
581
|
+
let homeArg;
|
|
582
|
+
for (let i = 0; i < rest.length; i++) {
|
|
583
|
+
const a = rest[i];
|
|
584
|
+
if (a === '--home') { homeArg = rest[++i]; continue; }
|
|
585
|
+
if (a.startsWith('--home=')) { homeArg = a.slice('--home='.length); continue; }
|
|
586
|
+
if (a.startsWith('--')) { flags.add(a); continue; }
|
|
587
|
+
pos.push(a);
|
|
588
|
+
}
|
|
589
|
+
const sub = pos[0];
|
|
590
|
+
const opts = {
|
|
591
|
+
refresh: flags.has('--refresh'),
|
|
592
|
+
apply: flags.has('--apply'),
|
|
593
|
+
home: homeArg ? path.resolve(homeArg) : undefined,
|
|
594
|
+
};
|
|
595
|
+
const out = [];
|
|
596
|
+
switch (sub) {
|
|
597
|
+
case undefined:
|
|
598
|
+
case 'status': {
|
|
599
|
+
const { rows, skipped } = tc.status(opts);
|
|
600
|
+
for (const r of rows) {
|
|
601
|
+
if (r.ok) out.push(` ${ok('已就位')} ${paint(r.key, C.fg.cyan, C.bold)} ${dim(r.version + ' ' + r.exe)} ${r.cached ? dim('[缓存]') : dim('[探测]')}`);
|
|
602
|
+
else out.push(` ${err('缺失')} ${paint(r.key, C.fg.cyan, C.bold)} ${dim(r.reason || '未检测到')} ${dim('· ' + r.why)}`);
|
|
603
|
+
}
|
|
604
|
+
out.push('');
|
|
605
|
+
const cacheNote = skipped ? `本轮跳过 ${skipped} 次探测(缓存命中,PATH 变更或 --refresh 即失效)` : '本轮全部实探';
|
|
606
|
+
out.push([' ', dim(cacheNote) + ';', dim('安装:'), paint('freedom toolchain install missing', C.fg.cyan),
|
|
607
|
+
dim('(默认只打印命令,加 --apply 才执行)')].join(' '));
|
|
608
|
+
console.log(out.join('\n'));
|
|
609
|
+
return 0;
|
|
610
|
+
}
|
|
611
|
+
case 'install': {
|
|
612
|
+
const res = tc.install(pos[1] || 'missing', opts);
|
|
613
|
+
for (const r of res.rows) {
|
|
614
|
+
if (r.action === 'skip') console.log(` ${ok(`[${r.key}] 已就位`)} ${dim(r.version || '')}`);
|
|
615
|
+
else if (r.action === 'manual') console.log(` ${warn(`[${r.key}] 本平台无自动安装方式`)} ${dim(r.note)}`);
|
|
616
|
+
else if (r.action === 'plan') {
|
|
617
|
+
console.log(` ${warn(`[${r.key}] 待安装`)} ${dim('以下为将执行的命令:')}`);
|
|
618
|
+
for (const c of r.commands) console.log(` ${paint(c, C.fg.white)}`);
|
|
619
|
+
} else if (r.action === 'applied') {
|
|
620
|
+
for (const s of r.ran) {
|
|
621
|
+
console.log(` ${s.status === 0 ? ok('[完成]') : err('[失败]')} ${dim(s.command)} ${s.status === 0 ? '' : err(`退出码 ${s.status}`)}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (res.dryRun) {
|
|
626
|
+
console.log('');
|
|
627
|
+
console.log(` ${tip('确认无误后加 --apply 真正执行安装(联网、可能需要管理员权限)。')}`);
|
|
628
|
+
} else {
|
|
629
|
+
console.log(` ${dim('安装后若 PATH 未刷新,重新打开终端或执行 freedom toolchain status --refresh。')}`);
|
|
630
|
+
}
|
|
631
|
+
return res.rows.some((r) => r.action === 'applied' && r.ran.some((s) => s.status !== 0)) ? 1 : 0;
|
|
632
|
+
}
|
|
633
|
+
case 'optimize': {
|
|
634
|
+
const res = tc.optimize(opts);
|
|
635
|
+
for (const r of res.rows) {
|
|
636
|
+
if (r.action === 'skip') console.log(` ${ok(`[${r.key}] 无需优化`)} ${dim(r.note)}`);
|
|
637
|
+
else if (r.action === 'blocked') console.log(` ${warn(`[${r.key}] 跳过`)} ${dim(r.note)}`);
|
|
638
|
+
else if (r.action === 'report') console.log(` ${r.ok ? ok(`[${r.key}] 已就位`) : warn(`[${r.key}] 未就位`)} ${dim(r.note)}`);
|
|
639
|
+
else if (r.action === 'plan') {
|
|
640
|
+
console.log(` ${warn(`[${r.key}] 将优化`)} ${dim(r.from ? `当前:${r.from}` : '')}`);
|
|
641
|
+
for (const c of (r.commands || [])) console.log(` ${paint(c, C.fg.white)}`);
|
|
642
|
+
if (r.file) console.log(` ${dim(r.file)} ${dim('(幂等合并,写前 .bak)')}`);
|
|
643
|
+
} else if (r.action === 'applied') {
|
|
644
|
+
console.log(` ${ok(`[${r.key}] 已优化`)} ${dim(r.to || r.file || '')}${r.backup ? dim(' 备份 ' + r.backup) : ''}${r.status ? err(` 退出码 ${r.status}`) : ''}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (res.dryRun) console.log(`\n ${tip('以上为拟写入内容,未改动任何文件;加 --apply 生效(写入前自动 .bak 备份)。')}`);
|
|
648
|
+
return 0;
|
|
649
|
+
}
|
|
650
|
+
case 'clear':
|
|
651
|
+
console.log(tc.clearCache(opts) ? `${ok('缓存已清除,下一次 status 将重新探测。')}` : `${dim('无缓存可清除。')}`);
|
|
652
|
+
return 0;
|
|
653
|
+
default:
|
|
654
|
+
console.error(`${err(`未知 toolchain 子命令:${sub}`)} ${dim('可用:status / install [go,rust,cpp|missing] / optimize / clear')}`);
|
|
655
|
+
return 1;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
410
659
|
function openBrowser(file) {
|
|
411
660
|
const { spawn } = require('child_process');
|
|
412
661
|
const plat = process.platform;
|
package/lib/config.js
CHANGED
|
@@ -17,6 +17,8 @@ const KEY_TYPES = {
|
|
|
17
17
|
security: 'security',
|
|
18
18
|
icon: 'string',
|
|
19
19
|
outDir: 'string',
|
|
20
|
+
url: 'string',
|
|
21
|
+
singleInstance: 'bool',
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
// 把 CLI/TUI 传入的值规范化为配置语义上的目标值;非法即抛错。
|
|
@@ -115,7 +117,21 @@ function setConfig(dir, key, value) {
|
|
|
115
117
|
break;
|
|
116
118
|
}
|
|
117
119
|
if (!replaced) {
|
|
118
|
-
|
|
120
|
+
// 模板里的新能力键(url / singleInstance)默认是注释示例,用户不必先手动取消注释:
|
|
121
|
+
// 找不到未注释的键就在结尾 `};` 前插入一行,仍保持 `freedom config set` 一条命令可用。
|
|
122
|
+
let endIdx = -1;
|
|
123
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
124
|
+
if (/^\s*\}\s*;?\s*$/.test(lines[i])) {
|
|
125
|
+
endIdx = i;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (endIdx < 0) {
|
|
130
|
+
throw new Error(`配置项 ${key} 未在 freedom.config.js 中找到(注释行不参与修改),请手动添加。`);
|
|
131
|
+
}
|
|
132
|
+
lines.splice(endIdx, 0, ` ${key}: ${rendered},`);
|
|
133
|
+
fs.writeFileSync(cfgPath, lines.join('\n'), 'utf8');
|
|
134
|
+
return norm;
|
|
119
135
|
}
|
|
120
136
|
fs.writeFileSync(cfgPath, lines.join('\n'), 'utf8');
|
|
121
137
|
return norm;
|
|
@@ -143,6 +159,8 @@ const KNOWN_KEYS = {
|
|
|
143
159
|
security: 'none',
|
|
144
160
|
icon: undefined,
|
|
145
161
|
outDir: 'dist',
|
|
162
|
+
url: undefined,
|
|
163
|
+
singleInstance: false,
|
|
146
164
|
};
|
|
147
165
|
|
|
148
166
|
async function showConfig(dir) {
|
|
@@ -152,6 +170,7 @@ async function showConfig(dir) {
|
|
|
152
170
|
return ` ${k}: ${JSON.stringify(v)}`;
|
|
153
171
|
});
|
|
154
172
|
lines.push(` backend: ${cfg.backend ? JSON.stringify(cfg.backend) : 'undefined'}`);
|
|
173
|
+
lines.push(` updater: ${cfg.updater ? JSON.stringify(cfg.updater) : 'undefined'}`);
|
|
155
174
|
return lines.join('\n');
|
|
156
175
|
}
|
|
157
176
|
|
package/lib/desktop.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// freedom desktop —— Freedom Desktop:用 freedom 自己打包出来的图形界面(自举)
|
|
4
|
+
//
|
|
5
|
+
// 链路:templates/desktop/* 同步到 ~/.freedom/desktop/ → 经与用户项目同一条 build()
|
|
6
|
+
// 代码路径产出(staticHtml 直通,免 npm / 免 vite / 免网络)→ 拉起该壳。
|
|
7
|
+
// 壳内前端通过 NDJSON 进程后端调起 freedom CLI 子进程,界面能力即 CLI 能力。
|
|
8
|
+
//
|
|
9
|
+
// stamp = CLI 版本 + 模板内容哈希;不匹配(或产物缺失、--rebuild)即自动重打包,幂等。
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const { spawn } = require('child_process');
|
|
16
|
+
const { packageRoot, templateDir, nativePlatform, platformExeName } = require('./utils');
|
|
17
|
+
|
|
18
|
+
const APP_NAME = 'freedom-desktop';
|
|
19
|
+
const STAMP_FILE = '.freedom-desktop.json';
|
|
20
|
+
|
|
21
|
+
function cliVersion() {
|
|
22
|
+
return require(path.join(packageRoot(), 'package.json')).version;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function desktopDir() {
|
|
26
|
+
return path.join(os.homedir(), '.freedom', 'desktop');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function templateRoot() {
|
|
30
|
+
return path.join(templateDir(), 'desktop');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function walk(root, rel = '', out = []) {
|
|
34
|
+
for (const e of fs.readdirSync(path.join(root, rel), { withFileTypes: true })) {
|
|
35
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
36
|
+
if (e.isDirectory()) walk(root, r, out);
|
|
37
|
+
else out.push(r);
|
|
38
|
+
}
|
|
39
|
+
return out.sort();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 把模板文件拷入工作目录,并补齐两份运行时元数据:
|
|
43
|
+
// backend/cli-entry.json —— Desktop 后端据此以子进程方式定位 freedom CLI 入口
|
|
44
|
+
// package.json —— 声明 type:module(freedom.config.js 用 ESM 书写)与产物版本
|
|
45
|
+
function sync(dir) {
|
|
46
|
+
const src = templateRoot();
|
|
47
|
+
if (!fs.existsSync(src)) throw new Error(`缺少 Desktop 模板目录:${src}`);
|
|
48
|
+
const files = walk(src);
|
|
49
|
+
for (const rel of files) {
|
|
50
|
+
const dest = path.join(dir, rel);
|
|
51
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
52
|
+
fs.copyFileSync(path.join(src, rel), dest);
|
|
53
|
+
}
|
|
54
|
+
const entry = path.join(packageRoot(), 'bin', 'freedom.js');
|
|
55
|
+
writeIfChanged(path.join(dir, 'backend', 'cli-entry.json'),
|
|
56
|
+
JSON.stringify({ entry, root: packageRoot(), version: cliVersion() }, null, 2) + '\n');
|
|
57
|
+
writeIfChanged(path.join(dir, 'package.json'),
|
|
58
|
+
JSON.stringify({ name: APP_NAME, private: true, version: cliVersion(), type: 'module' }, null, 2) + '\n');
|
|
59
|
+
return files;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeIfChanged(file, content) {
|
|
63
|
+
let cur = null;
|
|
64
|
+
try { cur = fs.readFileSync(file, 'utf8'); } catch (e) { /* 首次写入 */ }
|
|
65
|
+
if (cur !== content) fs.writeFileSync(file, content, 'utf8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function stampOf(files) {
|
|
69
|
+
const h = crypto.createHash('sha256');
|
|
70
|
+
h.update(`cli:${cliVersion()}\n`);
|
|
71
|
+
for (const rel of files) {
|
|
72
|
+
h.update(`${rel}\0`);
|
|
73
|
+
h.update(fs.readFileSync(path.join(templateRoot(), rel)));
|
|
74
|
+
h.update('\0');
|
|
75
|
+
}
|
|
76
|
+
return h.digest('hex').slice(0, 16);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveExe(dir, plat) {
|
|
80
|
+
if (plat.startsWith('darwin')) {
|
|
81
|
+
// mac 产物会升级为 .app bundle,可执行文件在 Contents/MacOS 下
|
|
82
|
+
return [path.join(dir, 'dist', `${APP_NAME}.app`, 'Contents', 'MacOS', APP_NAME),
|
|
83
|
+
path.join(dir, 'dist', APP_NAME)].find((p) => fs.existsSync(p)) || path.join(dir, 'dist', APP_NAME);
|
|
84
|
+
}
|
|
85
|
+
return path.join(dir, 'dist', platformExeName(plat, APP_NAME));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 打包(幂等):产物缺失 / stamp 变更 / rebuild 时执行,其余情况直接复用上次产物。
|
|
89
|
+
async function ensure({ rebuild = false } = {}) {
|
|
90
|
+
const dir = desktopDir();
|
|
91
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
92
|
+
const files = sync(dir);
|
|
93
|
+
const stamp = stampOf(files);
|
|
94
|
+
const plat = nativePlatform();
|
|
95
|
+
const exe = resolveExe(dir, plat);
|
|
96
|
+
let prev = null;
|
|
97
|
+
try { prev = JSON.parse(fs.readFileSync(path.join(dir, STAMP_FILE), 'utf8')); } catch (e) { /* 未构建过 */ }
|
|
98
|
+
|
|
99
|
+
const stale = rebuild || !fs.existsSync(exe) || !prev || prev.stamp !== stamp;
|
|
100
|
+
if (stale) {
|
|
101
|
+
if (fs.existsSync(exe)) {
|
|
102
|
+
// Windows 下运行中的 exe 被系统锁定;先探测并给出可执行的指示,而不是抛底层 EBUSY/EPERM
|
|
103
|
+
try { fs.unlinkSync(exe); } catch (e) {
|
|
104
|
+
throw new Error(`正在运行的 Freedom Desktop 锁定了自身可执行文件(${exe}):请先关闭其窗口,再执行 freedom desktop${rebuild ? ' --rebuild' : ''}。`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
process.stdout.write(`[freedom] ${fs.existsSync(exe) ? 'Desktop 模板或 CLI 版本已变更,重新打包中…' : '首次运行:正在用 freedom 打包 Freedom Desktop…'}\n`);
|
|
108
|
+
const { build } = require('./build');
|
|
109
|
+
await build(dir, { platform: plat, noCache: true });
|
|
110
|
+
fs.writeFileSync(path.join(dir, STAMP_FILE),
|
|
111
|
+
JSON.stringify({ stamp, version: cliVersion(), plat, builtAt: new Date().toISOString() }, null, 2) + '\n', 'utf8');
|
|
112
|
+
}
|
|
113
|
+
return { dir, exe, plat, rebuilt: stale, version: cliVersion() };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function launch(exe, dir) {
|
|
117
|
+
const child = spawn(exe, [], {
|
|
118
|
+
cwd: path.dirname(exe),
|
|
119
|
+
detached: true,
|
|
120
|
+
stdio: 'ignore',
|
|
121
|
+
env: { ...process.env, FREEDOM_DESKTOP_DIR: dir },
|
|
122
|
+
});
|
|
123
|
+
child.unref();
|
|
124
|
+
return child.pid;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function desktop({ rebuild = false, noLaunch = false } = {}) {
|
|
128
|
+
const r = await ensure({ rebuild });
|
|
129
|
+
if (noLaunch) return { ...r, launched: false };
|
|
130
|
+
return { ...r, launched: true, pid: launch(r.exe, r.dir) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { desktop };
|