@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.
Files changed (39) hide show
  1. package/README.md +17 -3
  2. package/bin/freedom.js +8 -3
  3. package/lib/build.js +64 -13
  4. package/lib/cli.js +129 -82
  5. package/lib/config.js +0 -7
  6. package/lib/shell.js +170 -33
  7. package/lib/theme.js +105 -0
  8. package/lib/tui.js +27 -3
  9. package/lib/update.js +109 -0
  10. package/lib/utils.js +14 -1
  11. package/package.json +1 -1
  12. package/shell/darwin-arm64/freedom-shell +0 -0
  13. package/shell/linux-x64/freedom-shell +0 -0
  14. package/shell/win-x64/freedom-shell.exe +0 -0
  15. package/templates/go/main.go +2 -1
  16. package/templates/go/pkg/freedom/assets/freedom.js +10 -4
  17. package/templates/go/pkg/freedom/backend_proc.go +7 -4
  18. package/templates/go/pkg/freedom/center_windows.go +44 -5
  19. package/templates/go/pkg/freedom/configfile.go +12 -5
  20. package/templates/go/pkg/freedom/freedom.go +303 -273
  21. package/templates/go/pkg/freedom/window_other.go +6 -2
  22. package/templates/go/webview_go/libs/webview/include/webview.h +31 -0
  23. package/templates/go/webview_go/webview.go +32 -6
  24. package/templates/project/src/main.js +12 -5
  25. package/templates/go/build_out.txt +0 -0
  26. package/templates/go/err.txt +0 -2
  27. package/templates/go/err10.txt +0 -0
  28. package/templates/go/err11.txt +0 -0
  29. package/templates/go/err2.txt +0 -0
  30. package/templates/go/err3.txt +0 -0
  31. package/templates/go/err4.txt +0 -1
  32. package/templates/go/err5.txt +0 -0
  33. package/templates/go/err6.txt +0 -0
  34. package/templates/go/err7.txt +0 -0
  35. package/templates/go/err8.txt +0 -0
  36. package/templates/go/err9.txt +0 -0
  37. package/templates/go/go.mod.bak_gbk +0 -11
  38. package/templates/go/tidy.txt +0 -0
  39. 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.13.0)。
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` | 完全无边框,标题栏不存在,客户区铺满窗口,关闭 / 最大化 / 最小化按钮由前端自绘(默认,模板已内置示例)。当前仅 Windows 完整实现;macOS / Linux 由系统窗口管理器托管,回退为原生标题栏且窗口控制动作返回明确错误 |
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` 模式下(Windows),前端可通过注入的 `window.freedom.window` API 控制窗口(`minimize` / `maximize` / `toggleMaximize` / `close` / `isMaximized` / `isFrameless`),模板已内置自绘标题栏示例;macOS / Linux 上这些动作会 reject,前端应捕获并提示。
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
- process.exit(code || 0);
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.exit(1);
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 解析:win|mac|linux|all 或平台 key(win-x64 等)
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
- if (keyMap[v]) return [keyMap[v]];
62
- if (ALL_PLATFORMS.includes(v)) return [v];
63
- throw new Error(
64
- `未知平台:${raw}。可选:win / mac / linux / all,或 ${ALL_PLATFORMS.join(' / ')}`
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
- // 用系统 tar(Windows bsdtar)把目录压成 zip:跨平台可用,无需额外依赖
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
- const res = spawnSync('tar', ['-a', '-c', '-f', zipPath, base], { cwd: parent, encoding: 'utf8' });
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
- throw new Error(
300
- `打包 ${base} zip 失败:${(res.stderr || res.stdout || '').trim() || res.error.message}`
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
- return `
14
- freedom - Freedom 桌面壳打包工具 v${VERSION}
15
-
16
- 用法:
17
- freedom tui 进入交互式 TUI 界面(新建/打包/配置/壳管理)
18
- freedom init [目录] [--force] 在当前/指定目录新建项目模板
19
- freedom build [--platform <p>] 前端打包并分发桌面应用(默认当前平台)
20
- --platform win|mac|linux|all 指定目标平台(all = 三平台全量)
21
- --no-cache 忽略前端构建缓存,强制重新打包
22
- freedom titlebar <native|frameless>
23
- 一键切换标题栏策略
24
- freedom icon <path> 设置应用图标(Windows .ico,macOS 需 .icns)
25
- freedom shell list 列出本地已就绪的预编译壳
26
- freedom shell download <plat> 从 GitHub Releases 下载预编译壳(无需 Go)
27
- freedom shell build <plat> 本地用 Go 编译壳(可选,壳已预编译一般无需)
28
- freedom dmg [--platform <plat>] 将已构建的 .app 打包为 .dmg(需在 macOS 上执行)
29
- freedom config 查看当前配置
30
- freedom config get <key> 读取单个配置项
31
- freedom config set <key> <value> 修改单个配置项
32
- freedom tutorial 再次打开安装教程
33
- freedom help 显示本帮助
34
- freedom version 显示版本
35
-
36
- 平台(<p>/<plat>):win-x64 / darwin-arm64 / linux-x64 / linux-arm64,
37
- 快捷别名:win / mac / linux / all。
38
-
39
- macOS 产物说明:
40
- freedom build --platform mac 直接产出 <app>.app.zip(解压即得 .app,
41
- 拖入 /Applications 即可使用,无需安装任何语言运行时);如需 .dmg,
42
- 在 macOS 上执行 freedom dmg 用系统 hdiutil 生成。
43
-
44
- 标题栏策略说明:
45
- native 保留系统原生标题栏,标题栏图标与 exe 图标一致
46
- frameless 完全无边框,标题栏与 Windows 原生最小化/最大化/关闭按钮均不存在,
47
- 关闭/最大化/最小化按钮由前端自绘(默认,模板已内置示例)
48
-
49
- 图标说明:
50
- Windows exe 图标:在 freedom.config.js 配置 icon(.ico 路径),构建时自动注入;
51
- macOS .app 图标:icon 配置 .icns 路径即可;未配置则使用壳默认图标。
52
- `.trim();
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
- console.log(VERSION);
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(`[freedom] 项目已创建:${dir}`);
80
- console.log(' 下一步:');
81
- console.log(` cd ${dir}`);
82
- console.log(' npm install');
83
- console.log(' freedom build');
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
- platform = platArg.includes('=') ? platArg.split('=')[1] : rest[rest.indexOf(platArg) + 1];
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(`[freedom] [${r.plat}] 构建完成:${r.outFile}`);
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('[freedom] 用法:freedom titlebar <native|frameless>');
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(`[freedom] titlebar 已切换为:${mode}`);
116
- console.log(' 运行 freedom build 重新打包生效。');
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('[freedom] 用法:freedom icon <path>');
124
- console.error(' 示例:freedom icon icon.ico (Windows exe 图标,.ico 格式)');
125
- console.error(' freedom icon icon.icns (macOS .app 图标,.icns 格式)');
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(`[freedom] icon 已设置为:${iconPath}`);
130
- console.log(' 运行 freedom build 重新打包生效。');
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 cfg = await showConfig(process.cwd());
138
- console.log(cfg);
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('[freedom] 用法:freedom config set <key> <value>');
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(`[freedom] ${key} = ${coerce(value)}`);
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(`[freedom] 教程已打开:${file}`);
206
+ console.log(`${ok('教程已打开:')}${paint(file, C.fg.cyan)}`);
160
207
  return 0;
161
208
  }
162
209
 
163
210
  default:
164
- console.error(`[freedom] 未知命令:${cmd}\n`);
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('[freedom] 本地暂无预编译壳。');
236
+ console.log(`${dim('本地暂无预编译壳。')}`);
190
237
  } else {
191
- console.log('[freedom] 本地已就绪的壳平台:');
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(`[freedom] 可选平台:${ALL_PLATFORMS.join(' / ')}`);
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('[freedom] 用法:freedom shell download <win-x64|darwin-arm64|linux-x64|linux-arm64>');
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(`[freedom] 已下载 ${plat} 壳:${dest}`);
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('[freedom] 用法:freedom shell build <win-x64|darwin-arm64|linux-x64|linux-arm64>');
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(`[freedom] 已编译 ${plat} 壳:${dest}`);
215
- console.log('[freedom] 提示:壳已预编译随包分发,一般无需本地编译。');
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(`[freedom] 未知 shell 子命令:${sub}`);
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('[freedom] dmg 仅支持 macOS 平台(darwin-arm64)。');
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(`[freedom] 未找到 ${name}.app(已检查 ${candidates.join(' / ')})。`);
248
- console.error(' 请先在 macOS 上运行 freedom build --platform mac 生成 .app。');
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(`[freedom] 已生成 dmg:${dmgPath}`);
302
+ console.log(`${ok('已生成 dmg:')}${paint(dmgPath, C.fg.white)}`);
256
303
  return 0;
257
304
  } catch (e) {
258
- console.error(`[freedom] ${e.message}`);
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) => {