@yufengtadian/freedom-cli 1.12.12 → 1.12.14
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 +17 -3
- package/bin/freedom.js +8 -3
- package/lib/build.js +64 -13
- package/lib/cli.js +129 -82
- package/lib/config.js +0 -7
- package/lib/shell.js +170 -33
- package/lib/theme.js +105 -0
- package/lib/tui.js +27 -3
- package/lib/update.js +109 -0
- package/lib/utils.js +14 -1
- package/package.json +1 -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/templates/go/main.go +2 -1
- package/templates/go/pkg/freedom/assets/freedom.js +10 -4
- package/templates/go/pkg/freedom/backend_proc.go +7 -4
- package/templates/go/pkg/freedom/center_windows.go +44 -5
- package/templates/go/pkg/freedom/configfile.go +12 -5
- package/templates/go/pkg/freedom/freedom.go +303 -273
- package/templates/go/pkg/freedom/window_other.go +6 -2
- package/templates/go/webview_go/libs/webview/include/webview.h +31 -0
- package/templates/go/webview_go/webview.go +32 -6
- package/templates/project/src/main.js +12 -5
- package/templates/go/build_out.txt +0 -0
- package/templates/go/err.txt +0 -2
- package/templates/go/err10.txt +0 -0
- package/templates/go/err11.txt +0 -0
- package/templates/go/err2.txt +0 -0
- package/templates/go/err3.txt +0 -0
- package/templates/go/err4.txt +0 -1
- package/templates/go/err5.txt +0 -0
- package/templates/go/err6.txt +0 -0
- package/templates/go/err7.txt +0 -0
- package/templates/go/err8.txt +0 -0
- package/templates/go/err9.txt +0 -0
- package/templates/go/go.mod.bak_gbk +0 -11
- package/templates/go/tidy.txt +0 -0
- package/templates/go/vet_out.txt +0 -0
package/README.md
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
# freedom-cli
|
|
2
2
|
|
|
3
|
-
Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.
|
|
3
|
+
Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.12.14)。
|
|
4
4
|
|
|
5
5
|
基于自研 Freedom WebView 壳层(对标 Wails / Tauri):前端完全自由、后端可任意语言、渲染复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK),产物为单个可执行文件 + resources 目录,前端页面内存加载,不占本地端口。
|
|
6
6
|
|
|
7
|
+
**v1.12.14 稳定性 / 兼容性 / 打包链路修复**:
|
|
8
|
+
- **Go 壳层并发安全**:`Dispatch` 修复解锁后读共享 index 的数据竞争(消除 UI 事件丢失 / 偶发 panic);`App.view` 增加互斥锁,`Emit / Quit / WindowHandle` 可在任意 goroutine 安全调用(后端事件推送不再与 Run 竞态);
|
|
9
|
+
- **多屏适配**:窗口居中改为按窗口所在监视器工作区(rcWork)居中,副屏 / 负坐标 / 任务栏遮挡下均正确;实现改用纯 syscall,不再依赖 `x/sys/windows` 新版 API(老版本 Go 亦可编译);
|
|
10
|
+
- **窗口与后端健壮性**:`MinWidth / MinHeight` 任一 >0 即生效;后端进程启动前拦截已关闭状态,杜绝孤儿进程 / 二次 Run;前端自绘三按钮回调全部加 `.catch` 兜底,桥接 reject 不再静默失效;`Unbind` 清理 Go 侧绑定表消除泄漏;
|
|
11
|
+
- **Linux 打包根因修复**:壳下载支持代理(`FREEDOM_SHELL_PROXY / ALL_PROXY / HTTPS_PROXY`),直连 GitHub Releases 被墙超时不再导致 linux 产物打包失败;`--platform` 缺值明确报错、zipDir 跨平台(Windows 用 `tar -a`,Linux/macOS 用 `zip`)、`config get` 校验 key、CLI 改用 `process.exitCode` 不截断管道输出;
|
|
12
|
+
- **兼容性**:ARM Windows 识别并提示 x64 仿真运行;DPI 初始化惰性加载,Win7/8 不再 panic。
|
|
13
|
+
|
|
14
|
+
**v1.12.13 CLI 界面升级与版本检测**:
|
|
15
|
+
- CLI 交互界面升级为 Claude Code 风格:彩色分组帮助菜单、徽章化命令反馈(✓ / ✗ / ⚠ / ➜)、品牌横幅与版本信息卡;非 TTY(管道 / 重定向)或 `NO_COLOR` 下自动降级为纯文本,脚本调用与 CI 输出不受影响;
|
|
16
|
+
- 新增版本检测:`freedom version` / `freedom update` 实时查询 npm registry 对比最新版本,发现新版本即给出 `npm install -g @yufengtadian/freedom-cli@latest` 升级命令;每次命令执行后静默检测一次(24 小时缓存,离线不打扰、不阻塞),有新版本自动提示升级;
|
|
17
|
+
- TUI 主菜单新增「检查更新」入口,可随时在界面内查看当前版本与最新版本。
|
|
18
|
+
|
|
7
19
|
**v1.12.12 三平台 frameless 彻底修复**:macOS / Linux 无边框窗口不再残留原生标题栏——
|
|
8
20
|
- 壳层向 webview_go 新增 `set_decorated` / `window_control` 原生窗口控制 API(GTK 走 `gtk_window_set_decorated` / `gtk_window_*`,Cocoa 走隐藏标题栏 + `performMiniaturize:` / `zoom:` / `performClose:` / `isZoomed`),mac/linux 的 frameless 从"空实现回退原生标题栏"修复为真无边框,UI 上方不再残留未清理的原生标题栏;
|
|
9
21
|
- 自绘三按钮(最小化 / 最大化 / 关闭)在 macOS / Linux 上接入原生窗口控制,双击最大化 / 还原、`isMaximized` 状态查询真实可用(此前 mac/linux 窗口控制为静默空转、`isMaximized` 恒 false);
|
|
@@ -116,9 +128,9 @@ freedom config set outDir dist # 恢复默认 dist/
|
|
|
116
128
|
| 模式 | 说明 |
|
|
117
129
|
| --- | --- |
|
|
118
130
|
| `native` | 保留系统原生标题栏,标题栏图标与 exe 图标一致 |
|
|
119
|
-
| `frameless` | 完全无边框,标题栏不存在,客户区铺满窗口,关闭 / 最大化 /
|
|
131
|
+
| `frameless` | 完全无边框,标题栏不存在,客户区铺满窗口,关闭 / 最大化 / 最小化按钮由前端自绘(默认,模板已内置示例)。三平台完整实现:Windows 经 `WM_NCCALCSIZE` / `WM_NCHITTEST` / `WM_GETMINMAXINFO` 原生层处理,macOS / Linux 经 `set_decorated` + 原生窗口控制(GTK `gtk_window_*` / Cocoa `performMiniaturize:` 等),行为一致(v1.12.12 起) |
|
|
120
132
|
|
|
121
|
-
`frameless`
|
|
133
|
+
`frameless` 模式下,前端可通过注入的 `window.freedom.window` API 控制窗口(`minimize` / `maximize` / `toggleMaximize` / `close` / `isMaximized` / `isFrameless`),模板已内置自绘标题栏示例;三平台窗口控制动作均已接入原生实现(Windows `WM_NCLBUTTONDOWN`、GTK `gtk_window_begin_move_drag`、Cocoa 原生拖动),拖动标题栏 / 双击最大化 / 右键菜单行为一致。
|
|
122
134
|
|
|
123
135
|
## 配置(freedom.config.js)
|
|
124
136
|
|
|
@@ -171,6 +183,8 @@ freedom icon <path> # 设置应用图标(Windows 用 .ico
|
|
|
171
183
|
freedom config [get|set]
|
|
172
184
|
freedom shell list|download <platform>|build <platform>
|
|
173
185
|
freedom dmg [--platform <plat>] # 在 macOS 上把 .app 打包为 .dmg
|
|
186
|
+
freedom version # 显示版本并检测最新版本
|
|
187
|
+
freedom update # 检查新版本并给出升级命令(同 check-update)
|
|
174
188
|
freedom tutorial
|
|
175
189
|
freedom help
|
|
176
190
|
```
|
package/bin/freedom.js
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const { run } = require('../lib/cli');
|
|
5
|
+
const { maybeNotifyUpdate } = require('../lib/update');
|
|
5
6
|
|
|
6
|
-
run(process.argv.slice(2)).then((code) => {
|
|
7
|
-
|
|
7
|
+
run(process.argv.slice(2)).then(async (code) => {
|
|
8
|
+
if (code === 0 && process.argv[2] !== 'tui') {
|
|
9
|
+
try { await maybeNotifyUpdate(); } catch (e) { /* 检测失败静默 */ }
|
|
10
|
+
}
|
|
11
|
+
// 用 exitCode 让 Node 自然刷新 stdout 后退出,避免 process.exit 截断管道输出(历史 bug B28)
|
|
12
|
+
process.exitCode = code || 0;
|
|
8
13
|
}).catch((err) => {
|
|
9
14
|
console.error('[freedom] 执行失败:', err && err.message ? err.message : err);
|
|
10
|
-
process.
|
|
15
|
+
process.exitCode = 1;
|
|
11
16
|
});
|
package/lib/build.js
CHANGED
|
@@ -27,6 +27,7 @@ const { spawnSync } = require('child_process');
|
|
|
27
27
|
const { copyDir } = require('./utils');
|
|
28
28
|
const {
|
|
29
29
|
ALL_PLATFORMS,
|
|
30
|
+
DIST_PLATFORMS,
|
|
30
31
|
isWinPlat,
|
|
31
32
|
isMacPlat,
|
|
32
33
|
platformExeName,
|
|
@@ -52,17 +53,34 @@ function run(cmd, args, opts = {}) {
|
|
|
52
53
|
return res;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
// --platform
|
|
56
|
+
// --platform 解析:支持逗号 / 中英文逗号 / 空白分隔多平台;win|mac|linux|all 或平台 key(win-x64 等)。
|
|
57
|
+
// 多平台去重保留顺序。all 仅取可分发平台(DIST_PLATFORMS):linux-arm64 无 CI 资产,
|
|
58
|
+
// 若列入 all 会在 build 时 404 拖垮整个全量构建(历史 bug B41)。
|
|
56
59
|
function parsePlatforms(raw) {
|
|
57
60
|
if (!raw) return [nativePlatform()];
|
|
58
|
-
const v = String(raw).toLowerCase();
|
|
59
|
-
if (v === 'all') return ALL_PLATFORMS;
|
|
60
61
|
const keyMap = { win: 'win-x64', mac: 'darwin-arm64', linux: 'linux-x64' };
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
const seen = [];
|
|
63
|
+
const push = (p) => {
|
|
64
|
+
if (!ALL_PLATFORMS.includes(p)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`未知平台:${p}。可选:win / mac / linux / all,或 ${ALL_PLATFORMS.join(' / ')}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (!seen.includes(p)) seen.push(p);
|
|
70
|
+
};
|
|
71
|
+
for (const seg of String(raw).split(/[,,\s]+/)) {
|
|
72
|
+
const v = String(seg).toLowerCase();
|
|
73
|
+
if (!v) continue;
|
|
74
|
+
if (v === 'all') {
|
|
75
|
+
for (const p of DIST_PLATFORMS) push(p);
|
|
76
|
+
} else {
|
|
77
|
+
push(keyMap[v] || v);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (seen.length === 0) {
|
|
81
|
+
throw new Error(`未知平台:${raw}。可选:win / mac / linux / all,或 ${ALL_PLATFORMS.join(' / ')}`);
|
|
82
|
+
}
|
|
83
|
+
return seen;
|
|
66
84
|
}
|
|
67
85
|
|
|
68
86
|
async function build(projectDir, opts = {}) {
|
|
@@ -98,6 +116,7 @@ async function build(projectDir, opts = {}) {
|
|
|
98
116
|
throw new Error(`前端打包完成但未找到 ${distHtml},请检查 vite 配置(vite-plugin-singlefile)。`);
|
|
99
117
|
}
|
|
100
118
|
const html = fs.readFileSync(distHtml, 'utf8');
|
|
119
|
+
warnIfNotSingleFile(html, distHtml);
|
|
101
120
|
const configJSON = renderConfigJSON(cfg, name);
|
|
102
121
|
|
|
103
122
|
// 2) 后端目录(若配置了 backend 进程)
|
|
@@ -289,16 +308,26 @@ ${iconEntry} <key>CFBundleExecutable</key>
|
|
|
289
308
|
`;
|
|
290
309
|
}
|
|
291
310
|
|
|
292
|
-
//
|
|
311
|
+
// 把目录压成 .zip:跨平台零额外依赖(历史 bug B27)
|
|
312
|
+
// - Windows:系统 tar 为 bsdtar,`-a` 按扩展名自动选 zip 压缩器,可用;
|
|
313
|
+
// - Linux/macOS:GNU tar 的 `-a` 不支持 .zip(仅 gzip/bzip2/xz 等),
|
|
314
|
+
// 改用系统 zip 命令(Linux/macOS 自带或常见,缺失时给出安装提示)。
|
|
293
315
|
function zipDir(zipPath, dir) {
|
|
294
316
|
const parent = path.dirname(dir);
|
|
295
317
|
const base = path.basename(dir);
|
|
296
318
|
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
|
|
297
|
-
|
|
319
|
+
let res;
|
|
320
|
+
if (process.platform === 'win32') {
|
|
321
|
+
res = spawnSync('tar', ['-a', '-c', '-f', zipPath, base], { cwd: parent, encoding: 'utf8' });
|
|
322
|
+
} else {
|
|
323
|
+
res = spawnSync('zip', ['-r', '-q', zipPath, base], { cwd: parent, encoding: 'utf8' });
|
|
324
|
+
}
|
|
298
325
|
if (res.error || res.status !== 0) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
326
|
+
const detail = (res.stderr || res.stdout || '').trim() || res.error.message;
|
|
327
|
+
const hint = process.platform !== 'win32'
|
|
328
|
+
? '\n(Linux/macOS 打包 zip 需要 zip 命令:Ubuntu: sudo apt install zip / macOS: brew install zip)'
|
|
329
|
+
: '';
|
|
330
|
+
throw new Error(`打包 ${base} 为 zip 失败:${detail}${hint}`);
|
|
302
331
|
}
|
|
303
332
|
}
|
|
304
333
|
|
|
@@ -421,4 +450,26 @@ function intVal(v, dft) {
|
|
|
421
450
|
return Number.isFinite(n) && n >= 0 ? n : dft;
|
|
422
451
|
}
|
|
423
452
|
|
|
453
|
+
// 非 singlefile 产物检测(历史 bug B47):壳只 SetHtml 单页内存加载,HTML 里引用
|
|
454
|
+
// 的外部 <script src> / <link href>(相对路径或 / 根路径)在无服务器环境下必然失效,
|
|
455
|
+
// 导致页面 JS/CSS 丢失静默空白。检测到此类引用时明确告警,避免用户无感知翻车。
|
|
456
|
+
function warnIfNotSingleFile(html, distHtml) {
|
|
457
|
+
const refs = [];
|
|
458
|
+
const re = /<(?:script|link)\b[^>]*(?:src|href)\s*=\s*["']([^"']+)["']/gi;
|
|
459
|
+
let m;
|
|
460
|
+
while ((m = re.exec(html)) !== null) {
|
|
461
|
+
const url = m[1];
|
|
462
|
+
// data: / blob: / http(s): / file: 可正常工作,跳过;其余(相对、/ 根路径、// 协议相对)均会失效
|
|
463
|
+
if (/^(?:data:|blob:|https?:|file:)/i.test(url)) continue;
|
|
464
|
+
refs.push(url);
|
|
465
|
+
}
|
|
466
|
+
if (refs.length > 0) {
|
|
467
|
+
console.warn(
|
|
468
|
+
`[freedom] 警告:${path.basename(distHtml)} 引用了外部资源(${refs.join(', ')})。` +
|
|
469
|
+
`壳在内存加载单页时这些引用会失效,导致页面空白。` +
|
|
470
|
+
`请在 vite.config.js 启用 vite-plugin-singlefile 将 JS/CSS 内联进 HTML。`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
424
475
|
module.exports = { build, parsePlatforms };
|
package/lib/cli.js
CHANGED
|
@@ -6,50 +6,57 @@ const { init } = require('./init');
|
|
|
6
6
|
const { build } = require('./build');
|
|
7
7
|
const { setConfig, showConfig } = require('./config');
|
|
8
8
|
const { packageRoot, tutorialFile } = require('./utils');
|
|
9
|
+
const theme = require('./theme');
|
|
10
|
+
const update = require('./update');
|
|
9
11
|
|
|
10
12
|
const VERSION = require(path.join(packageRoot(), 'package.json')).version;
|
|
13
|
+
const { paint, ok, err, warn, info, tip, dim, bold, section, banner, versionCard, C } = theme;
|
|
11
14
|
|
|
12
15
|
function help() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
用法:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
freedom
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
freedom
|
|
23
|
-
|
|
24
|
-
freedom
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
freedom
|
|
28
|
-
|
|
29
|
-
freedom
|
|
30
|
-
freedom
|
|
31
|
-
|
|
32
|
-
freedom
|
|
33
|
-
freedom
|
|
34
|
-
freedom
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
freedom
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
16
|
+
const L = [];
|
|
17
|
+
L.push(banner(VERSION));
|
|
18
|
+
L.push('');
|
|
19
|
+
L.push(` ${paint('用法:', C.bold, C.fg.white)} ${paint('freedom <command> [options]', C.fg.cyan, C.bold)}`);
|
|
20
|
+
L.push('');
|
|
21
|
+
L.push(section('交互式界面'));
|
|
22
|
+
L.push(` ${paint('freedom tui', C.fg.cyan, C.bold)} ${dim('进入交互式 TUI(新建 / 打包 / 配置 / 壳管理)')}`);
|
|
23
|
+
L.push(section('项目'));
|
|
24
|
+
L.push(` ${paint('freedom init [目录] [--force]', C.fg.cyan)} ${dim('在当前 / 指定目录新建项目模板')}`);
|
|
25
|
+
L.push(` ${paint('freedom tutorial', C.fg.cyan)} ${dim('再次打开安装教程')}`);
|
|
26
|
+
L.push(section('打包'));
|
|
27
|
+
L.push(` ${paint('freedom build [--platform <p>]', C.fg.cyan)} ${dim('前端打包并分发桌面应用(默认当前平台)')}`);
|
|
28
|
+
L.push(` ${dim('--platform win|mac|linux|all')} ${dim('指定目标平台(all = 三平台全量)')}`);
|
|
29
|
+
L.push(` ${dim('--no-cache')} ${dim('忽略前端构建缓存,强制重新打包')}`);
|
|
30
|
+
L.push(` ${paint('freedom dmg [--platform <plat>]', C.fg.cyan)} ${dim('将已构建的 .app 打包为 .dmg(需 macOS)')}`);
|
|
31
|
+
L.push(section('外观'));
|
|
32
|
+
L.push(` ${paint('freedom titlebar <native|frameless>', C.fg.cyan)} ${dim('一键切换标题栏策略')}`);
|
|
33
|
+
L.push(` ${paint('freedom icon <path>', C.fg.cyan)} ${dim('设置应用图标(Win .ico / mac .icns)')}`);
|
|
34
|
+
L.push(section('壳管理'));
|
|
35
|
+
L.push(` ${paint('freedom shell list', C.fg.cyan)} ${dim('列出本地已就绪的预编译壳')}`);
|
|
36
|
+
L.push(` ${paint('freedom shell download <plat>', C.fg.cyan)} ${dim('从 GitHub Releases 下载预编译壳')}`);
|
|
37
|
+
L.push(` ${paint('freedom shell build <plat>', C.fg.cyan)} ${dim('本地用 Go 编译壳(可选,一般无需)')}`);
|
|
38
|
+
L.push(section('配置'));
|
|
39
|
+
L.push(` ${paint('freedom config', C.fg.cyan)} ${dim('查看当前配置')}`);
|
|
40
|
+
L.push(` ${paint('freedom config get <key>', C.fg.cyan)} ${dim('读取单个配置项')}`);
|
|
41
|
+
L.push(` ${paint('freedom config set <key> <value>', C.fg.cyan)} ${dim('修改单个配置项')}`);
|
|
42
|
+
L.push(section('版本'));
|
|
43
|
+
L.push(` ${paint('freedom version', C.fg.cyan)} ${dim('显示版本并检测最新版本')}`);
|
|
44
|
+
L.push(` ${paint('freedom update', C.fg.cyan)} ${dim('检查新版本并给出升级命令')}`);
|
|
45
|
+
L.push(` ${paint('freedom help', C.fg.cyan)} ${dim('显示本帮助')}`);
|
|
46
|
+
L.push('');
|
|
47
|
+
L.push(section('平台'));
|
|
48
|
+
L.push(` ${dim('<p> / <plat>:win-x64 / darwin-arm64 / linux-x64 / linux-arm64,快捷别名:win / mac / linux / all。')}`);
|
|
49
|
+
L.push(section('macOS 产物'));
|
|
50
|
+
L.push(` ${dim('freedom build --platform mac 直接产出 <app>.app.zip(解压即得 .app,拖入 /Applications 即可,无需语言运行时);')}`);
|
|
51
|
+
L.push(` ${dim('如需 .dmg,在 macOS 上执行 freedom dmg 用系统 hdiutil 生成。')}`);
|
|
52
|
+
L.push(section('标题栏策略'));
|
|
53
|
+
L.push(` ${paint('native', C.fg.white)} ${dim('保留系统原生标题栏,标题栏图标与 exe 图标一致')}`);
|
|
54
|
+
L.push(` ${paint('frameless', C.fg.white)} ${dim('完全无边框,关闭 / 最大化 / 最小化按钮由前端自绘(默认,模板已内置示例)')}`);
|
|
55
|
+
L.push(section('图标'));
|
|
56
|
+
L.push(` ${dim('Windows exe 图标:freedom.config.js 配置 icon(.ico 路径),构建时自动注入;')}`);
|
|
57
|
+
L.push(` ${dim('macOS .app 图标:icon 配置 .icns 路径即可;未配置则使用壳默认图标。')}`);
|
|
58
|
+
L.push('');
|
|
59
|
+
return L.join('\n');
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
async function run(argv) {
|
|
@@ -65,9 +72,31 @@ async function run(argv) {
|
|
|
65
72
|
|
|
66
73
|
case 'version':
|
|
67
74
|
case '--version':
|
|
68
|
-
case '-v':
|
|
69
|
-
|
|
75
|
+
case '-v': {
|
|
76
|
+
const r = await update.checkUpdate({ force: true });
|
|
77
|
+
console.log(versionCard(r.current, r.latest, r.hasUpdate));
|
|
78
|
+
if (r.hasUpdate && r.latest) {
|
|
79
|
+
console.log(` ${tip('升级命令:')}${paint(`npm install -g ${update.PKG_NAME}@latest`, C.fg.cyan, C.bold)}`);
|
|
80
|
+
console.log('');
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'update':
|
|
86
|
+
case 'check-update': {
|
|
87
|
+
const r = await update.checkUpdate({ force: true });
|
|
88
|
+
console.log(versionCard(r.current, r.latest, r.hasUpdate));
|
|
89
|
+
if (r.hasUpdate && r.latest) {
|
|
90
|
+
console.log(` ${tip('检测到新版本,执行以下命令升级:')}`);
|
|
91
|
+
console.log(` ${paint(`npm install -g ${update.PKG_NAME}@latest`, C.fg.cyan, C.bold)}`);
|
|
92
|
+
console.log('');
|
|
93
|
+
} else if (r.latest) {
|
|
94
|
+
console.log(` ${ok('当前已是最新版本。')}`);
|
|
95
|
+
} else {
|
|
96
|
+
console.log(` ${warn('检查失败:网络不可用或 npm registry 未响应,请稍后重试。')}`);
|
|
97
|
+
}
|
|
70
98
|
return 0;
|
|
99
|
+
}
|
|
71
100
|
|
|
72
101
|
case 'tui':
|
|
73
102
|
return await require('./tui').tui(process.cwd());
|
|
@@ -76,11 +105,11 @@ async function run(argv) {
|
|
|
76
105
|
const force = rest.includes('--force');
|
|
77
106
|
const dirArg = rest.filter((a) => a !== '--force')[0];
|
|
78
107
|
const dir = init(dirArg || '.', { force });
|
|
79
|
-
console.log(
|
|
80
|
-
console.log('
|
|
81
|
-
console.log(` cd ${dir}`);
|
|
82
|
-
console.log('
|
|
83
|
-
console.log('
|
|
108
|
+
console.log(`${ok('项目已创建:')}${paint(dir, C.fg.cyan, C.bold)}`);
|
|
109
|
+
console.log(` ${dim('下一步:')}`);
|
|
110
|
+
console.log(` ${paint(`cd ${dir}`, C.fg.white)}`);
|
|
111
|
+
console.log(` ${paint('npm install', C.fg.white)}`);
|
|
112
|
+
console.log(` ${paint('freedom build', C.fg.white)}`);
|
|
84
113
|
return 0;
|
|
85
114
|
}
|
|
86
115
|
|
|
@@ -88,65 +117,83 @@ async function run(argv) {
|
|
|
88
117
|
const platArg = rest.find((a) => a.startsWith('--platform') || a.startsWith('-p'));
|
|
89
118
|
let platform;
|
|
90
119
|
if (platArg) {
|
|
91
|
-
|
|
120
|
+
const inline = platArg.includes('=') ? platArg.split('=')[1] : undefined;
|
|
121
|
+
if (inline) {
|
|
122
|
+
platform = inline;
|
|
123
|
+
} else {
|
|
124
|
+
platform = rest[rest.indexOf(platArg) + 1];
|
|
125
|
+
}
|
|
126
|
+
// 缺值(--platform 后无值 / 紧跟着另一个选项)时明确报错,禁止静默回退当前平台(历史 bug B26)
|
|
127
|
+
if (!platform || platform.startsWith('--')) {
|
|
128
|
+
console.error(`${err('--platform 缺少取值。')} ${dim('用法:')}${paint('freedom build --platform <win|mac|linux|all>', C.fg.cyan)}`);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
92
131
|
}
|
|
93
132
|
const { results } = await build(process.cwd(), { platform, noCache: rest.includes('--no-cache') });
|
|
94
133
|
for (const r of results) {
|
|
95
|
-
console.log(`[
|
|
134
|
+
console.log(`${ok('构建完成')} ${paint(`[${r.plat}]`, C.fg.magenta, C.bold)} ${paint(r.outFile, C.fg.white)}`);
|
|
96
135
|
}
|
|
97
136
|
return 0;
|
|
98
137
|
}
|
|
99
138
|
|
|
100
|
-
case 'shell':
|
|
139
|
+
case 'shell':
|
|
101
140
|
return await runShell(rest);
|
|
102
|
-
}
|
|
103
141
|
|
|
104
|
-
case 'dmg':
|
|
142
|
+
case 'dmg':
|
|
105
143
|
return await runDmg(rest);
|
|
106
|
-
}
|
|
107
144
|
|
|
108
145
|
case 'titlebar': {
|
|
109
146
|
const mode = rest[0];
|
|
110
147
|
if (!['native', 'frameless'].includes(mode)) {
|
|
111
|
-
console.error('
|
|
148
|
+
console.error(`${err('用法:')}${paint('freedom titlebar <native|frameless>', C.fg.cyan)}`);
|
|
112
149
|
return 1;
|
|
113
150
|
}
|
|
114
151
|
setConfig(process.cwd(), 'titlebar', mode);
|
|
115
|
-
console.log(
|
|
116
|
-
console.log('
|
|
152
|
+
console.log(`${ok('titlebar 已切换为:')}${paint(mode, C.fg.cyan, C.bold)}`);
|
|
153
|
+
console.log(` ${dim('运行')} ${paint('freedom build', C.fg.cyan)} ${dim('重新打包生效。')}`);
|
|
117
154
|
return 0;
|
|
118
155
|
}
|
|
119
156
|
|
|
120
157
|
case 'icon': {
|
|
121
158
|
const iconPath = rest[0];
|
|
122
159
|
if (!iconPath) {
|
|
123
|
-
console.error('
|
|
124
|
-
console.error('
|
|
125
|
-
console.error('
|
|
160
|
+
console.error(`${err('用法:')}${paint('freedom icon <path>', C.fg.cyan)}`);
|
|
161
|
+
console.error(` ${dim('示例:freedom icon icon.ico (Windows exe 图标,.ico 格式)')}`);
|
|
162
|
+
console.error(` ${dim('freedom icon icon.icns (macOS .app 图标,.icns 格式)')}`);
|
|
126
163
|
return 1;
|
|
127
164
|
}
|
|
128
165
|
setConfig(process.cwd(), 'icon', iconPath);
|
|
129
|
-
console.log(
|
|
130
|
-
console.log('
|
|
166
|
+
console.log(`${ok('icon 已设置为:')}${paint(iconPath, C.fg.cyan, C.bold)}`);
|
|
167
|
+
console.log(` ${dim('运行')} ${paint('freedom build', C.fg.cyan)} ${dim('重新打包生效。')}`);
|
|
131
168
|
return 0;
|
|
132
169
|
}
|
|
133
170
|
|
|
134
171
|
case 'config': {
|
|
135
172
|
const sub = rest[0];
|
|
136
173
|
if (sub === 'get') {
|
|
137
|
-
const
|
|
138
|
-
|
|
174
|
+
const key = rest[1];
|
|
175
|
+
if (!key) {
|
|
176
|
+
console.error(`${err('用法:')}${paint('freedom config get <key>', C.fg.cyan)}`);
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
const { getConfig } = require('./config');
|
|
180
|
+
const cfg = await getConfig(process.cwd());
|
|
181
|
+
if (!(key in cfg)) {
|
|
182
|
+
console.error(`${err(`配置项 ${key} 不存在。可用键:`)}${paint(Object.keys(cfg).join(' / '), C.fg.cyan)}`);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
console.log(JSON.stringify(cfg[key], null, 2));
|
|
139
186
|
return 0;
|
|
140
187
|
}
|
|
141
188
|
if (sub === 'set') {
|
|
142
189
|
const key = rest[1];
|
|
143
190
|
const value = rest[2];
|
|
144
191
|
if (!key || value === undefined) {
|
|
145
|
-
console.error('
|
|
192
|
+
console.error(`${err('用法:')}${paint('freedom config set <key> <value>', C.fg.cyan)}`);
|
|
146
193
|
return 1;
|
|
147
194
|
}
|
|
148
195
|
setConfig(process.cwd(), key, coerce(value));
|
|
149
|
-
console.log(
|
|
196
|
+
console.log(`${ok('已设置')} ${paint(key, C.fg.cyan, C.bold)} = ${paint(JSON.stringify(coerce(value)), C.fg.white)}`);
|
|
150
197
|
return 0;
|
|
151
198
|
}
|
|
152
199
|
console.log(await showConfig(process.cwd()));
|
|
@@ -156,12 +203,12 @@ async function run(argv) {
|
|
|
156
203
|
case 'tutorial': {
|
|
157
204
|
const file = tutorialFile();
|
|
158
205
|
openBrowser(file);
|
|
159
|
-
console.log(
|
|
206
|
+
console.log(`${ok('教程已打开:')}${paint(file, C.fg.cyan)}`);
|
|
160
207
|
return 0;
|
|
161
208
|
}
|
|
162
209
|
|
|
163
210
|
default:
|
|
164
|
-
console.error(
|
|
211
|
+
console.error(`${err('未知命令:')}${paint(cmd, C.fg.red, C.bold)}\n`);
|
|
165
212
|
console.log(help());
|
|
166
213
|
return 1;
|
|
167
214
|
}
|
|
@@ -186,37 +233,37 @@ async function runShell(rest) {
|
|
|
186
233
|
case 'list': {
|
|
187
234
|
const ready = listLocal();
|
|
188
235
|
if (ready.length === 0) {
|
|
189
|
-
console.log('
|
|
236
|
+
console.log(`${dim('本地暂无预编译壳。')}`);
|
|
190
237
|
} else {
|
|
191
|
-
console.log('
|
|
192
|
-
for (const p of ready) console.log(` ${p}`);
|
|
238
|
+
console.log(`${ok('本地已就绪的壳平台:')}`);
|
|
239
|
+
for (const p of ready) console.log(` ${paint('✓', C.fg.green)} ${paint(p, C.fg.cyan, C.bold)}`);
|
|
193
240
|
}
|
|
194
|
-
console.log(
|
|
241
|
+
console.log(`${dim('可选平台:')}${paint(ALL_PLATFORMS.join(' / '), C.fg.gray)}`);
|
|
195
242
|
return 0;
|
|
196
243
|
}
|
|
197
244
|
case 'download': {
|
|
198
245
|
const plat = rest[1];
|
|
199
246
|
if (!plat) {
|
|
200
|
-
console.error('
|
|
247
|
+
console.error(`${err('用法:')}${paint('freedom shell download <win-x64|darwin-arm64|linux-x64|linux-arm64>', C.fg.cyan)}`);
|
|
201
248
|
return 1;
|
|
202
249
|
}
|
|
203
250
|
const dest = await downloadShell(plat);
|
|
204
|
-
console.log(
|
|
251
|
+
console.log(`${ok('已下载')} ${paint(plat, C.fg.magenta, C.bold)} ${dim('壳:')}${paint(dest, C.fg.white)}`);
|
|
205
252
|
return 0;
|
|
206
253
|
}
|
|
207
254
|
case 'build': {
|
|
208
255
|
const plat = rest[1];
|
|
209
256
|
if (!plat) {
|
|
210
|
-
console.error('
|
|
257
|
+
console.error(`${err('用法:')}${paint('freedom shell build <win-x64|darwin-arm64|linux-x64|linux-arm64>', C.fg.cyan)}`);
|
|
211
258
|
return 1;
|
|
212
259
|
}
|
|
213
260
|
const dest = buildShell(plat);
|
|
214
|
-
console.log(
|
|
215
|
-
console.log('
|
|
261
|
+
console.log(`${ok('已编译')} ${paint(plat, C.fg.magenta, C.bold)} ${dim('壳:')}${paint(dest, C.fg.white)}`);
|
|
262
|
+
console.log(` ${dim('提示:壳已预编译随包分发,一般无需本地编译。')}`);
|
|
216
263
|
return 0;
|
|
217
264
|
}
|
|
218
265
|
default:
|
|
219
|
-
console.error(
|
|
266
|
+
console.error(`${err('未知 shell 子命令:')}${paint(sub, C.fg.red, C.bold)}`);
|
|
220
267
|
return 1;
|
|
221
268
|
}
|
|
222
269
|
}
|
|
@@ -235,7 +282,7 @@ async function runDmg(rest) {
|
|
|
235
282
|
? (platArg.includes('=') ? platArg.split('=')[1] : rest[rest.indexOf(platArg) + 1])
|
|
236
283
|
: nativePlatform();
|
|
237
284
|
if (!plat || !plat.startsWith('darwin')) {
|
|
238
|
-
console.error('
|
|
285
|
+
console.error(`${err('dmg 仅支持 macOS 平台(darwin-arm64)。')}`);
|
|
239
286
|
return 1;
|
|
240
287
|
}
|
|
241
288
|
|
|
@@ -244,18 +291,18 @@ async function runDmg(rest) {
|
|
|
244
291
|
const candidates = [path.join(baseDir, `${name}.app`), path.join(baseDir, plat, `${name}.app`)];
|
|
245
292
|
const appDir = candidates.find((p) => fs.existsSync(p));
|
|
246
293
|
if (!appDir) {
|
|
247
|
-
console.error(
|
|
248
|
-
console.error('
|
|
294
|
+
console.error(`${err('未找到')} ${paint(`${name}.app`, C.fg.cyan)} ${dim(`(已检查 ${candidates.join(' / ')})。`)}`);
|
|
295
|
+
console.error(` ${dim('请先在 macOS 上运行')} ${paint('freedom build --platform mac', C.fg.cyan)} ${dim('生成 .app。')}`);
|
|
249
296
|
return 1;
|
|
250
297
|
}
|
|
251
298
|
|
|
252
299
|
const outPath = path.join(path.dirname(appDir), `${name}-${plat}.dmg`);
|
|
253
300
|
try {
|
|
254
301
|
const dmgPath = await makeDmg(appDir, outPath, name);
|
|
255
|
-
console.log(
|
|
302
|
+
console.log(`${ok('已生成 dmg:')}${paint(dmgPath, C.fg.white)}`);
|
|
256
303
|
return 0;
|
|
257
304
|
} catch (e) {
|
|
258
|
-
console.error(
|
|
305
|
+
console.error(`${err(e.message)}`);
|
|
259
306
|
return 1;
|
|
260
307
|
}
|
|
261
308
|
}
|
package/lib/config.js
CHANGED
|
@@ -134,13 +134,6 @@ const KNOWN_KEYS = {
|
|
|
134
134
|
outDir: 'dist',
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
-
async function getConfig(dir) {
|
|
138
|
-
if (!hasConfig(dir)) {
|
|
139
|
-
throw new Error('当前目录不是 Freedom 项目(缺少 freedom.config.js)。');
|
|
140
|
-
}
|
|
141
|
-
return loadConfig(dir);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
137
|
async function showConfig(dir) {
|
|
145
138
|
const cfg = await loadConfig(dir);
|
|
146
139
|
const lines = Object.keys(KNOWN_KEYS).map((k) => {
|