@yufengtadian/freedom-cli 1.13.0 → 1.13.2
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/README.md +96 -14
- package/lib/agents.js +318 -0
- package/lib/build.js +168 -39
- package/lib/cli.js +160 -4
- 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 +5 -2
- package/lib/tui.js +37 -1
- package/lib/verify.js +43 -8
- package/package.json +2 -1
- 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 +317 -0
- package/templates/desktop/freedom.config.js +29 -0
- package/templates/desktop/icon.ico +0 -0
- package/templates/go/pkg/freedom/anti_debug_windows.go +114 -11
- package/templates/go/pkg/freedom/assets/freedom.js +3 -0
- package/templates/go/pkg/freedom/freedom.go +52 -9
- package/templates/go/pkg/freedom/resources.go +68 -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 +240 -61
- package/templates/go/pkg/freedom/store.go +8 -0
- package/templates/go/pkg/freedom/window_other.go +7 -1
- package/templates/go/pkg/freedom/window_windows.go +185 -17
- 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/build.js
CHANGED
|
@@ -107,27 +107,39 @@ async function build(projectDir, opts = {}) {
|
|
|
107
107
|
const autoDownload = opts.autoDownload !== false;
|
|
108
108
|
|
|
109
109
|
// 1) 前端打包(带缓存:源码未变更时复用上次 vite 产物,跳过 npm run build)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
// cfg.staticHtml:静态单文件页面直通——不跑 npm install / vite,用于 freedom 自己的
|
|
111
|
+
// Desktop 界面自举打包(以及纯静态页项目),零网络、零依赖。
|
|
112
|
+
let html;
|
|
113
|
+
if (cfg.staticHtml) {
|
|
114
|
+
const staticPath = path.resolve(dir, String(cfg.staticHtml));
|
|
115
|
+
if (!fs.existsSync(staticPath)) {
|
|
116
|
+
throw new Error(`staticHtml 指向的文件不存在:${staticPath}(freedom.config.js 配置项)`);
|
|
117
|
+
}
|
|
118
|
+
html = fs.readFileSync(staticPath, 'utf8');
|
|
119
|
+
warnIfNotSingleFile(html, staticPath);
|
|
114
120
|
} else {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
ensureNodeModules(dir);
|
|
122
|
+
const useCache = opts.noCache !== true;
|
|
123
|
+
if (useCache && !viteNeedsRebuild(dir)) {
|
|
124
|
+
process.stdout.write('[freedom] 前端源码无改动,复用构建缓存...\n');
|
|
125
|
+
} else {
|
|
126
|
+
process.stdout.write('[freedom] 前端打包中(npm run build)...\n');
|
|
127
|
+
// 全量构建时 npm/vite 输出必须实时透传(stdio: inherit),
|
|
128
|
+
// 此前 pipe 缓冲会吞掉构建全程输出,Node 主线程被 spawnSync 同步阻塞,
|
|
129
|
+
// 大前端全量构建时终端长时间零输出,表现如"未响应/卡死"。
|
|
130
|
+
const vite = run('npm', ['run', 'build'], { cwd: dir, stdio: 'inherit' });
|
|
131
|
+
if (vite.status !== 0) {
|
|
132
|
+
throw new Error(`前端打包失败(退出码 ${vite.status}),详见上方构建输出。`);
|
|
133
|
+
}
|
|
134
|
+
writeViteCacheMarker(dir);
|
|
122
135
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
const distHtml = path.join(dir, '.freedom', 'vite-dist', 'index.html');
|
|
137
|
+
if (!fs.existsSync(distHtml)) {
|
|
138
|
+
throw new Error(`前端打包完成但未找到 ${distHtml},请检查 vite 配置(vite-plugin-singlefile)。`);
|
|
139
|
+
}
|
|
140
|
+
html = fs.readFileSync(distHtml, 'utf8');
|
|
141
|
+
warnIfNotSingleFile(html, distHtml);
|
|
128
142
|
}
|
|
129
|
-
const html = fs.readFileSync(distHtml, 'utf8');
|
|
130
|
-
warnIfNotSingleFile(html, distHtml);
|
|
131
143
|
const configJSON = renderConfigJSON(cfg, name);
|
|
132
144
|
// 安全模式:--security 优先于 freedom.config.js 的 security 字段,默认 none。
|
|
133
145
|
// none = 明文资源(默认,兼容历史产物)
|
|
@@ -149,8 +161,8 @@ async function build(projectDir, opts = {}) {
|
|
|
149
161
|
const results = await Promise.all(
|
|
150
162
|
platforms.map(async (plat) => {
|
|
151
163
|
const targetDir = multi ? path.join(outDirPath, plat) : outDirPath;
|
|
152
|
-
const
|
|
153
|
-
return { plat,
|
|
164
|
+
const emitted = await emitPlatform({ plat, name, version, targetDir, html, configJSON, backendDir, hasBackend, autoDownload, icon, security, installer: opts.installer === true });
|
|
165
|
+
return { plat, ...emitted };
|
|
154
166
|
})
|
|
155
167
|
);
|
|
156
168
|
|
|
@@ -184,7 +196,7 @@ async function build(projectDir, opts = {}) {
|
|
|
184
196
|
return { results };
|
|
185
197
|
}
|
|
186
198
|
|
|
187
|
-
async function emitPlatform({ plat, name, version, targetDir, html, configJSON, backendDir, hasBackend, autoDownload, icon, security }) {
|
|
199
|
+
async function emitPlatform({ plat, name, version, targetDir, html, configJSON, backendDir, hasBackend, autoDownload, icon, security, installer }) {
|
|
188
200
|
// 取预编译壳二进制
|
|
189
201
|
const shell = localShellPath(plat);
|
|
190
202
|
if (!fs.existsSync(shell)) {
|
|
@@ -233,14 +245,24 @@ async function emitPlatform({ plat, name, version, targetDir, html, configJSON,
|
|
|
233
245
|
const p = path.join(resDir, legacy);
|
|
234
246
|
if (fs.existsSync(p)) await fsp.rm(p, { force: true });
|
|
235
247
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
248
|
+
// 后端源码同样进容器:high 模式磁盘上不留 resources/backend 明文目录,
|
|
249
|
+
// 壳启动时把容器内 backend/* 解密到一次性临时目录(退出即删)。
|
|
250
|
+
const legacyBackend = path.join(resDir, 'backend');
|
|
251
|
+
if (fs.existsSync(legacyBackend)) await fsp.rm(legacyBackend, { recursive: true, force: true });
|
|
252
|
+
const backendFiles = {};
|
|
239
253
|
if (hasBackend && fs.existsSync(backendDir)) {
|
|
240
|
-
collectDirFiles(backendDir, '',
|
|
254
|
+
for (const [rel, buf] of Object.entries(collectDirFiles(backendDir, '', {}))) {
|
|
255
|
+
backendFiles['backend/' + rel] = { data: buf, mode: fileMode(path.join(backendDir, rel)) };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// 加密前先抹掉 source map 引用:内联 sourceMappingURL 里往往直接嵌着前端原始源码,
|
|
259
|
+
// 容器解密即还原,等于给 high 模式留了个明文后门。
|
|
260
|
+
const { html: secureHtml, stripped } = stripSourceMapRefs(html);
|
|
261
|
+
if (stripped > 0) {
|
|
262
|
+
process.stdout.write(`[freedom] high 模式:已抹去前端页内 ${stripped} 处 source map 引用(可能含原始源码)。\n`);
|
|
241
263
|
}
|
|
242
|
-
const
|
|
243
|
-
await fsp.writeFile(path.join(resDir, '.integrity'),
|
|
264
|
+
const appBin = encryptApp(name, secureHtml, configJSON, backendFiles); await fsp.writeFile(path.join(resDir, 'app.bin'), appBin);
|
|
265
|
+
await fsp.writeFile(path.join(resDir, '.integrity'), renderIntegrity(buildIntegrity(name, appBin)), 'utf8');
|
|
244
266
|
} else {
|
|
245
267
|
for (const legacy of ['app.bin', '.integrity']) {
|
|
246
268
|
const p = path.join(resDir, legacy);
|
|
@@ -256,10 +278,14 @@ async function emitPlatform({ plat, name, version, targetDir, html, configJSON,
|
|
|
256
278
|
);
|
|
257
279
|
}
|
|
258
280
|
}
|
|
259
|
-
if (hasBackend) {
|
|
281
|
+
if (hasBackend && security !== 'high') {
|
|
260
282
|
copyDir(backendDir, path.join(resDir, 'backend'));
|
|
261
283
|
}
|
|
262
284
|
|
|
285
|
+
// --installer:便携 zip + Windows NSIS 安装器。先于 mac 的 .app 生成,
|
|
286
|
+
// 使 zip 内容只含「exe + resources」,不把 .app 与 .app.zip 二次打包进去。
|
|
287
|
+
const installers = installer ? await buildInstaller({ plat, name, version, targetDir, exeName }) : [];
|
|
288
|
+
|
|
263
289
|
// macOS:额外生成 .app bundle + .app.zip(供 mac 用户解压即用)
|
|
264
290
|
// 壳加载 exe 同目录 resources/,故把 resources 放进 Contents/MacOS/ 即可运行,无需改壳。
|
|
265
291
|
if (isMacPlat(plat)) {
|
|
@@ -269,7 +295,7 @@ async function emitPlatform({ plat, name, version, targetDir, html, configJSON,
|
|
|
269
295
|
);
|
|
270
296
|
}
|
|
271
297
|
|
|
272
|
-
return outFile;
|
|
298
|
+
return { outFile, installers };
|
|
273
299
|
}
|
|
274
300
|
|
|
275
301
|
// 把 .ico 图标注入到 Windows exe 的 PE 资源(RT_ICON + RT_GROUP_ICON),
|
|
@@ -290,7 +316,16 @@ async function applyWindowsIcon(exePath, iconPath, name, version) {
|
|
|
290
316
|
);
|
|
291
317
|
return;
|
|
292
318
|
}
|
|
293
|
-
|
|
319
|
+
let rcedit;
|
|
320
|
+
try {
|
|
321
|
+
rcedit = require('rcedit'); // 惰性加载,避免未配置 icon 时增加启动开销
|
|
322
|
+
} catch (e) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
'exe 图标注入需要依赖 rcedit,但未能加载(npm 安装不完整)。'
|
|
325
|
+
+ '请在 freedom-cli 安装目录执行 npm install 后重试;'
|
|
326
|
+
+ '或临时移除 freedom.config.js 的 icon 配置以跳过注入。'
|
|
327
|
+
);
|
|
328
|
+
}
|
|
294
329
|
const info = { icon: iconPath };
|
|
295
330
|
// rcedit 4.x 参数结构:FileDescription/ProductName 等字符串必须走
|
|
296
331
|
// version-string(--set-version-string 键值对),file-version/product-version
|
|
@@ -402,21 +437,25 @@ ${iconEntry} <key>CFBundleExecutable</key>
|
|
|
402
437
|
}
|
|
403
438
|
|
|
404
439
|
// 把目录压成 .zip:跨平台零额外依赖(历史 bug B27)
|
|
405
|
-
// - Windows
|
|
406
|
-
//
|
|
407
|
-
//
|
|
440
|
+
// - Windows:bsdtar 的 `-a` 按扩展名自动选 zip 压缩器,可用;但必须绝对路径锁定系统
|
|
441
|
+
// tar.exe —— Git Bash / MSYS 会把 GNU tar 塞进 PATH 前面,GNU tar 不认 `-a` 且把
|
|
442
|
+
// `D:\...` 当远程主机(`Cannot connect to D: resolve failed`)。
|
|
443
|
+
// - Linux/macOS:GNU tar 的 `-a` 不支持 .zip,改用系统 zip 命令(缺失时给出安装提示)。
|
|
408
444
|
function zipDir(zipPath, dir) {
|
|
409
445
|
const parent = path.dirname(dir);
|
|
410
446
|
const base = path.basename(dir);
|
|
411
447
|
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
|
|
412
|
-
let
|
|
448
|
+
let cmd = 'zip';
|
|
449
|
+
let args = ['-r', '-q', zipPath, base];
|
|
413
450
|
if (process.platform === 'win32') {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
451
|
+
const sysRoot = process.env.SystemRoot || 'C:\\Windows';
|
|
452
|
+
const bsdtar = path.join(sysRoot, 'System32', 'tar.exe');
|
|
453
|
+
cmd = fs.existsSync(bsdtar) ? bsdtar : 'tar';
|
|
454
|
+
args = ['-a', '-c', '-f', zipPath, base];
|
|
417
455
|
}
|
|
456
|
+
const res = spawnSync(cmd, args, { cwd: parent, encoding: 'utf8', windowsHide: true });
|
|
418
457
|
if (res.error || res.status !== 0) {
|
|
419
|
-
const detail = (res.stderr || res.stdout ||
|
|
458
|
+
const detail = (res.stderr || res.stdout || (res.error && res.error.message) || '').trim();
|
|
420
459
|
const hint = process.platform !== 'win32'
|
|
421
460
|
? '\n(Linux/macOS 打包 zip 需要 zip 命令:Ubuntu: sudo apt install zip / macOS: brew install zip)'
|
|
422
461
|
: '';
|
|
@@ -424,6 +463,62 @@ function zipDir(zipPath, dir) {
|
|
|
424
463
|
}
|
|
425
464
|
}
|
|
426
465
|
|
|
466
|
+
// --installer 产物:便携 zip 恒产出;Windows 额外填充 NSIS 模板,本机有 makensis
|
|
467
|
+
// 时直接编译 setup.exe,无则产出已填充的 .nsi(缺 makensis 属环境能力而非代码缺陷)。
|
|
468
|
+
// 顺序即排除策略:先压 zip 再写 .nsi/setup.exe,免维护"排除安装器中间物"清单。
|
|
469
|
+
async function buildInstaller({ plat, name, version, targetDir, exeName }) {
|
|
470
|
+
const artifacts = [];
|
|
471
|
+
|
|
472
|
+
const zipPath = path.join(targetDir, `${name}-${plat}-portable.zip`);
|
|
473
|
+
const tmpPath = path.join(path.dirname(targetDir), `.freedom-portable-${process.pid}.zip`);
|
|
474
|
+
zipDir(tmpPath, targetDir);
|
|
475
|
+
await fsp.rename(tmpPath, zipPath);
|
|
476
|
+
artifacts.push(zipPath);
|
|
477
|
+
|
|
478
|
+
if (!isWinPlat(plat)) return artifacts;
|
|
479
|
+
|
|
480
|
+
const tplPath = path.join(__dirname, '..', 'templates', 'installer', 'app.nsi');
|
|
481
|
+
if (!fs.existsSync(tplPath)) {
|
|
482
|
+
process.stdout.write(`[freedom] 提示:缺少安装包模板 ${tplPath},已跳过 NSIS。\n`);
|
|
483
|
+
return artifacts;
|
|
484
|
+
}
|
|
485
|
+
const fill = (s) =>
|
|
486
|
+
s.replace(/@NAME@/g, name)
|
|
487
|
+
.replace(/@VERSION@/g, version)
|
|
488
|
+
.replace(/@SRC@/g, targetDir)
|
|
489
|
+
.replace(/@OUT@/g, targetDir)
|
|
490
|
+
.replace(/@EXE@/g, exeName);
|
|
491
|
+
// NSIS 3 对无 BOM 的 UTF-8 脚本按代码页解释,模板含中文注释与 SimpChinese 文案,
|
|
492
|
+
// 故补 BOM(与 build.ps1 的 [Text.Encoding]::UTF8 写出行为一致)。
|
|
493
|
+
const body = fill(fs.readFileSync(tplPath, 'utf8'));
|
|
494
|
+
const nsiPath = path.join(targetDir, `${name}-setup-${version}.nsi`);
|
|
495
|
+
await fsp.writeFile(nsiPath, Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(body, 'utf8')]));
|
|
496
|
+
artifacts.push(nsiPath);
|
|
497
|
+
|
|
498
|
+
if (!hasTool('makensis')) {
|
|
499
|
+
process.stdout.write(
|
|
500
|
+
'[freedom] 未检测到 makensis(NSIS):已产出填充好的 .nsi,在装有 NSIS 的机器上执行\n' +
|
|
501
|
+
` makensis "${nsiPath}" 即可编译安装器。\n`
|
|
502
|
+
);
|
|
503
|
+
return artifacts;
|
|
504
|
+
}
|
|
505
|
+
const res = spawnSync('makensis', ['-V2', nsiPath], { encoding: 'utf8' });
|
|
506
|
+
if (res.status !== 0) {
|
|
507
|
+
const detail = (res.stderr || res.stdout || '').trim().slice(0, 600);
|
|
508
|
+
throw new Error(`makensis 编译失败(退出码 ${res.status}):${detail}`);
|
|
509
|
+
}
|
|
510
|
+
const setupPath = path.join(targetDir, `${name}-setup-${version}.exe`);
|
|
511
|
+
process.stdout.write(`[freedom] NSIS 安装器已生成:${setupPath}\n`);
|
|
512
|
+
artifacts.push(setupPath);
|
|
513
|
+
return artifacts;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 外部命令可用性探测(不影响主流程,仅决定是否尝试调用)。
|
|
517
|
+
function hasTool(cmd) {
|
|
518
|
+
const res = spawnSync(cmd, ['-VERSION'], { encoding: 'utf8', windowsHide: true });
|
|
519
|
+
return !res.error;
|
|
520
|
+
}
|
|
521
|
+
|
|
427
522
|
// 从项目 package.json 取版本号(cfg.version 优先)
|
|
428
523
|
function appVersionFromPkg(dir) {
|
|
429
524
|
try {
|
|
@@ -456,6 +551,17 @@ function renderConfigJSON(cfg, name) {
|
|
|
456
551
|
args: Array.isArray(cfg.backend.args) ? cfg.backend.args : [],
|
|
457
552
|
};
|
|
458
553
|
}
|
|
554
|
+
// 能力透传(与壳 resources.go 对齐):URL 直载 / 单实例 / 自动更新。
|
|
555
|
+
if (typeof cfg.url === 'string' && cfg.url) obj.url = cfg.url;
|
|
556
|
+
if (cfg.singleInstance === true) obj.singleInstance = true;
|
|
557
|
+
if (cfg.updater && typeof cfg.updater.manifestURL === 'string' && typeof cfg.updater.publicKey === 'string'
|
|
558
|
+
&& cfg.updater.manifestURL && cfg.updater.publicKey) {
|
|
559
|
+
obj.updater = {
|
|
560
|
+
manifestURL: cfg.updater.manifestURL,
|
|
561
|
+
publicKey: cfg.updater.publicKey,
|
|
562
|
+
requireSignature: cfg.updater.requireSignature === true ? true : undefined,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
459
565
|
return JSON.stringify(obj, null, 2);
|
|
460
566
|
}
|
|
461
567
|
|
|
@@ -579,7 +685,7 @@ function warnIfNotSingleFile(html, distHtml) {
|
|
|
579
685
|
}
|
|
580
686
|
}
|
|
581
687
|
|
|
582
|
-
// 递归收集目录内所有文件:relPath(POSIX 相对路径)-> Buffer 内容,供
|
|
688
|
+
// 递归收集目录内所有文件:relPath(POSIX 相对路径)-> Buffer 内容,供 high 模式入容器。
|
|
583
689
|
function collectDirFiles(dir, prefix, out) {
|
|
584
690
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
585
691
|
const abs = path.join(dir, entry.name);
|
|
@@ -592,4 +698,27 @@ function collectDirFiles(dir, prefix, out) {
|
|
|
592
698
|
return out;
|
|
593
699
|
}
|
|
594
700
|
|
|
701
|
+
// 文件权限位(POSIX mode 低 12 位):进容器后由壳在临时目录还原(执行位决定后端能否直接跑)。
|
|
702
|
+
function fileMode(abs) {
|
|
703
|
+
try {
|
|
704
|
+
return fs.statSync(abs).mode & 0o7777;
|
|
705
|
+
} catch (e) {
|
|
706
|
+
return 0o644;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// 抹掉前端页内的 source map 引用(JS 的 //# sourceMappingURL=…、CSS 的 /*# … */)。
|
|
711
|
+
// 返回 { html, stripped }。
|
|
712
|
+
function stripSourceMapRefs(html) {
|
|
713
|
+
let stripped = 0;
|
|
714
|
+
const out = String(html).replace(
|
|
715
|
+
/\/\/[#@]\s*sourceMappingURL=[^\r\n]*|\/\*#\s*sourceMappingURL=[\s\S]*?\*\//g,
|
|
716
|
+
() => {
|
|
717
|
+
stripped += 1;
|
|
718
|
+
return '';
|
|
719
|
+
},
|
|
720
|
+
);
|
|
721
|
+
return { html: out, stripped };
|
|
722
|
+
}
|
|
723
|
+
|
|
595
724
|
module.exports = { build, parsePlatforms };
|
package/lib/cli.js
CHANGED
|
@@ -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,12 @@ 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 自动拉起,无需手敲)')}`);
|
|
44
59
|
L.push(section('版本'));
|
|
45
60
|
L.push(` ${paint('freedom version', C.fg.cyan)} ${dim('显示版本并检测最新版本')}`);
|
|
46
61
|
L.push(` ${paint('freedom update', C.fg.cyan)} ${dim('检查新版本并立即自动更新')}`);
|
|
@@ -66,9 +81,10 @@ async function run(argv) {
|
|
|
66
81
|
|
|
67
82
|
switch (cmd) {
|
|
68
83
|
case undefined: {
|
|
69
|
-
//
|
|
84
|
+
// 无参数:交互式终端先选择显示方式(终端 TUI / Freedom Desktop);
|
|
85
|
+
// 管道 / 脚本环境打印帮助,避免卡死
|
|
70
86
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
71
|
-
return await require('./tui').
|
|
87
|
+
return await require('./tui').pickDisplay(process.cwd());
|
|
72
88
|
}
|
|
73
89
|
console.log(help());
|
|
74
90
|
return 0;
|
|
@@ -112,6 +128,48 @@ async function run(argv) {
|
|
|
112
128
|
case 'tui':
|
|
113
129
|
return await require('./tui').tui(process.cwd());
|
|
114
130
|
|
|
131
|
+
case 'desktop': {
|
|
132
|
+
const { desktop } = require('./desktop');
|
|
133
|
+
try {
|
|
134
|
+
const r = await desktop({
|
|
135
|
+
rebuild: rest.includes('--rebuild'),
|
|
136
|
+
noLaunch: rest.includes('--no-launch'),
|
|
137
|
+
});
|
|
138
|
+
console.log(`${ok(r.rebuilt ? '已重新打包 Freedom Desktop:' : '复用已有产物:')}${paint(r.exe, C.fg.cyan, C.bold)}`);
|
|
139
|
+
console.log(` ${dim(`项目目录:${r.dir} · CLI v${r.version}`)}`);
|
|
140
|
+
if (r.launched) console.log(` ${ok(`窗口已拉起(pid ${r.pid})`)}`);
|
|
141
|
+
else console.log(` ${dim('未拉起窗口(--no-launch);执行 freedom desktop 即可打开。')}`);
|
|
142
|
+
return 0;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.error(`${err(e.message)}`);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
case 'agents': {
|
|
150
|
+
const sub = rest[0] && !rest[0].startsWith('--') ? rest[0] : undefined;
|
|
151
|
+
const flags = sub ? rest.slice(1) : rest;
|
|
152
|
+
const hi = flags.indexOf('--home');
|
|
153
|
+
const home = flags.find((a) => a.startsWith('--home=')) || (hi >= 0 ? flags[hi + 1] : undefined);
|
|
154
|
+
if (home && !home.startsWith('--')) process.env.FREEDOM_AGENT_HOME = path.resolve(home.replace(/^--home=/, ''));
|
|
155
|
+
if (!sub || sub === 'list') return require('./agents').printMatrix();
|
|
156
|
+
if (sub !== 'install') {
|
|
157
|
+
console.error(`${err(`未知 agents 子命令:${sub}`)} ${dim('可用:list / install --what <mcp|skill> --agent <key|all> [--dry-run]')}`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
const wi = flags.indexOf('--what');
|
|
161
|
+
const what = (flags.find((a) => a.startsWith('--what=')) || '').split('=')[1] || flags[wi + 1];
|
|
162
|
+
if (what && what !== 'mcp' && what !== 'skill') {
|
|
163
|
+
console.error(`${err(`不支持的 --what:${what}`)} ${dim('(可选 mcp|skill)')}`);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
return await runAgents(what === 'skill' ? 'skill' : 'mcp', ['install', ...flags]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case 'skill':
|
|
170
|
+
case 'mcp':
|
|
171
|
+
return await runAgents(cmd, rest);
|
|
172
|
+
|
|
115
173
|
case 'init': {
|
|
116
174
|
const force = rest.includes('--force');
|
|
117
175
|
const tmplArg = rest.find((a) => a.startsWith('--template'));
|
|
@@ -140,6 +198,62 @@ async function run(argv) {
|
|
|
140
198
|
return 0;
|
|
141
199
|
}
|
|
142
200
|
|
|
201
|
+
case 'dev': {
|
|
202
|
+
const optVal = (name) => {
|
|
203
|
+
const a = rest.find((x) => x.startsWith(`--${name}=`));
|
|
204
|
+
if (a) return a.split('=')[1];
|
|
205
|
+
const i = rest.indexOf(`--${name}`);
|
|
206
|
+
return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[i + 1] : undefined;
|
|
207
|
+
};
|
|
208
|
+
const url = optVal('url');
|
|
209
|
+
const port = optVal('port');
|
|
210
|
+
const { dev: runDev } = require('./dev');
|
|
211
|
+
try {
|
|
212
|
+
const done = await runDev({ dir: process.cwd(), url, port: port ? Number(port) : undefined, command: optVal('command') });
|
|
213
|
+
console.log(`${ok(String(done))}`);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
console.error(`${err(e.message)}`);
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
case 'keygen': {
|
|
222
|
+
const { keygen } = require('./release');
|
|
223
|
+
try {
|
|
224
|
+
console.log(await keygen({ dir: process.cwd(), force: rest.includes('--force') }));
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.error(`${err(e.message)}`);
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case 'manifest': {
|
|
233
|
+
const optVal = (name) => {
|
|
234
|
+
const a = rest.find((x) => x.startsWith(`--${name}=`));
|
|
235
|
+
if (a) return a.split('=')[1];
|
|
236
|
+
const i = rest.indexOf(`--${name}`);
|
|
237
|
+
return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[i + 1] : undefined;
|
|
238
|
+
};
|
|
239
|
+
const { manifest } = require('./release');
|
|
240
|
+
try {
|
|
241
|
+
console.log(await manifest({
|
|
242
|
+
dir: process.cwd(),
|
|
243
|
+
artifact: optVal('artifact'),
|
|
244
|
+
url: optVal('url'),
|
|
245
|
+
version: optVal('version'),
|
|
246
|
+
notes: optVal('notes'),
|
|
247
|
+
key: optVal('key'),
|
|
248
|
+
out: optVal('out'),
|
|
249
|
+
}));
|
|
250
|
+
} catch (e) {
|
|
251
|
+
console.error(`${err(e.message)}`);
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
143
257
|
case 'build': {
|
|
144
258
|
const platArg = rest.find((a) => a.startsWith('--platform') || a.startsWith('-p'));
|
|
145
259
|
let platform;
|
|
@@ -171,9 +285,13 @@ async function run(argv) {
|
|
|
171
285
|
platform,
|
|
172
286
|
noCache: rest.includes('--no-cache'),
|
|
173
287
|
security,
|
|
288
|
+
installer: rest.includes('--installer'),
|
|
174
289
|
});
|
|
175
290
|
for (const r of results) {
|
|
176
291
|
console.log(`${ok('构建完成')} ${paint(`[${r.plat}]`, C.fg.magenta, C.bold)} ${paint(r.outFile, C.fg.white)}`);
|
|
292
|
+
for (const p of r.installers || []) {
|
|
293
|
+
console.log(` ${dim('安装包产物:')}${paint(p, C.fg.white)}`);
|
|
294
|
+
}
|
|
177
295
|
}
|
|
178
296
|
return 0;
|
|
179
297
|
}
|
|
@@ -368,6 +486,44 @@ async function runShell(rest) {
|
|
|
368
486
|
}
|
|
369
487
|
}
|
|
370
488
|
|
|
489
|
+
// freedom skill / mcp —— Agent 集成安装与 MCP 服务
|
|
490
|
+
async function runAgents(cmd, rest) {
|
|
491
|
+
const sub = rest[0];
|
|
492
|
+
const optVal = (name) => {
|
|
493
|
+
const a = rest.find((x) => x.startsWith(`--${name}=`));
|
|
494
|
+
if (a) return a.split('=')[1];
|
|
495
|
+
const i = rest.indexOf(`--${name}`);
|
|
496
|
+
return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[i + 1] : undefined;
|
|
497
|
+
};
|
|
498
|
+
if (cmd === 'mcp' && sub === 'serve') {
|
|
499
|
+
return await require('./mcp').serve();
|
|
500
|
+
}
|
|
501
|
+
if (sub !== 'install') {
|
|
502
|
+
console.error(`${err('用法:')}${paint(`freedom ${cmd} install --agent <key|all> [--dry-run]`, C.fg.cyan)}`);
|
|
503
|
+
if (cmd === 'mcp') {
|
|
504
|
+
console.error(` ${paint('freedom mcp serve', C.fg.cyan)} ${dim('(stdio MCP 服务,通常由 agent 配置自动拉起)')}`);
|
|
505
|
+
}
|
|
506
|
+
console.error(` ${dim('可选 agent 与本机取证状态见:')}${paint('freedom agents', C.fg.white)}`);
|
|
507
|
+
return 1;
|
|
508
|
+
}
|
|
509
|
+
const { install } = require('./agents');
|
|
510
|
+
try {
|
|
511
|
+
const r = await install({
|
|
512
|
+
what: cmd === 'skill' ? 'skill' : 'mcp',
|
|
513
|
+
agent: optVal('agent') || 'all',
|
|
514
|
+
dryRun: rest.includes('--dry-run'),
|
|
515
|
+
config: optVal('config'),
|
|
516
|
+
format: optVal('format'),
|
|
517
|
+
skillsDir: optVal('skills-dir'),
|
|
518
|
+
home: optVal('home'),
|
|
519
|
+
});
|
|
520
|
+
return r.code;
|
|
521
|
+
} catch (e) {
|
|
522
|
+
console.error(`${err(e.message)}`);
|
|
523
|
+
return 1;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
371
527
|
// freedom dmg:在 macOS 上把已构建的 .app 打包为 .dmg(依赖系统 hdiutil)
|
|
372
528
|
async function runDmg(rest) {
|
|
373
529
|
const { loadConfig, nativePlatform } = require('./utils');
|
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
|
|