@yufengtadian/freedom-cli 1.12.1 → 1.12.13

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 (47) hide show
  1. package/README.md +31 -14
  2. package/bin/freedom.js +5 -1
  3. package/lib/build.js +49 -8
  4. package/lib/cli.js +107 -80
  5. package/lib/config.js +108 -31
  6. package/lib/shell.js +73 -20
  7. package/lib/theme.js +105 -0
  8. package/lib/tui.js +21 -3
  9. package/lib/update.js +109 -0
  10. package/lib/utils.js +14 -1
  11. package/package.json +1 -1
  12. package/shell/win-x64/freedom-shell.exe +0 -0
  13. package/templates/go/go.mod +6 -0
  14. package/templates/go/go.sum +2 -2
  15. package/templates/go/pkg/freedom/assets/freedom.js +6 -0
  16. package/templates/go/pkg/freedom/assets/index.html +1 -1
  17. package/templates/go/pkg/freedom/backend_proc.go +4 -4
  18. package/templates/go/pkg/freedom/configfile.go +12 -7
  19. package/templates/go/pkg/freedom/freedom.go +27 -11
  20. package/templates/go/pkg/freedom/window_other.go +64 -12
  21. package/templates/go/pkg/freedom/window_windows.go +244 -29
  22. package/templates/go/webview_go/.github/workflows/ci.yaml +62 -0
  23. package/templates/go/webview_go/CHANGELOG.md +15 -0
  24. package/templates/go/webview_go/LICENSE +22 -0
  25. package/templates/go/webview_go/README.md +50 -0
  26. package/templates/go/webview_go/examples/basic/main.go +12 -0
  27. package/templates/go/webview_go/examples/bind/main.go +38 -0
  28. package/templates/go/webview_go/glue.c +36 -0
  29. package/templates/go/webview_go/go.mod +3 -0
  30. package/templates/go/webview_go/go.sum +0 -0
  31. package/templates/go/webview_go/libs/mswebview2/LICENSE +27 -0
  32. package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
  33. package/templates/go/webview_go/libs/mswebview2/include/vendor.go +2 -0
  34. package/templates/go/webview_go/libs/mswebview2/vendor.go +2 -0
  35. package/templates/go/webview_go/libs/mswebview2/version.txt +1 -0
  36. package/templates/go/webview_go/libs/webview/LICENSE +22 -0
  37. package/templates/go/webview_go/libs/webview/include/vendor.go +2 -0
  38. package/templates/go/webview_go/libs/webview/include/webview.h +3871 -0
  39. package/templates/go/webview_go/libs/webview/vendor.go +2 -0
  40. package/templates/go/webview_go/libs/webview/version.txt +1 -0
  41. package/templates/go/webview_go/webview.cc +1 -0
  42. package/templates/go/webview_go/webview.go +379 -0
  43. package/templates/go/webview_go/webview_test.go +50 -0
  44. package/templates/project/freedom.config.js +5 -5
  45. package/templates/project/index.html +45 -5
  46. package/templates/project/src/main.js +149 -8
  47. package/tutorial/tutorial.html +4 -5
package/README.md CHANGED
@@ -1,10 +1,27 @@
1
1
  # freedom-cli
2
2
 
3
- Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.12.1)。
3
+ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.12.13)。
4
4
 
5
5
  基于自研 Freedom WebView 壳层(对标 Wails / Tauri):前端完全自由、后端可任意语言、渲染复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK),产物为单个可执行文件 + resources 目录,前端页面内存加载,不占本地端口。
6
6
 
7
- **v1.12.0 核心升级**:标题栏默认改为完全无边框(`frameless`)——标题栏不存在,关闭 / 最大化 / 最小化按钮由前端自绘融入 UI(模板已内置自绘标题栏示例),不再依赖 Windows 原生按钮。`freedom titlebar` 仍可随时切回 `native` / `hidden`。
7
+ **v1.12.13 CLI 界面升级与版本检测**:
8
+ - CLI 交互界面升级为 Claude Code 风格:彩色分组帮助菜单、徽章化命令反馈(✓ / ✗ / ⚠ / ➜)、品牌横幅与版本信息卡;非 TTY(管道 / 重定向)或 `NO_COLOR` 下自动降级为纯文本,脚本调用与 CI 输出不受影响;
9
+ - 新增版本检测:`freedom version` / `freedom update` 实时查询 npm registry 对比最新版本,发现新版本即给出 `npm install -g @yufengtadian/freedom-cli@latest` 升级命令;每次命令执行后静默检测一次(24 小时缓存,离线不打扰、不阻塞),有新版本自动提示升级;
10
+ - TUI 主菜单新增「检查更新」入口,可随时在界面内查看当前版本与最新版本。
11
+
12
+ **v1.12.12 三平台 frameless 彻底修复**:macOS / Linux 无边框窗口不再残留原生标题栏——
13
+ - 壳层向 webview_go 新增 `set_decorated` / `window_control` 原生窗口控制 API(GTK 走 `gtk_window_set_decorated` / `gtk_window_*`,Cocoa 走隐藏标题栏 + `performMiniaturize:` / `zoom:` / `performClose:` / `isZoomed`),mac/linux 的 frameless 从"空实现回退原生标题栏"修复为真无边框,UI 上方不再残留未清理的原生标题栏;
14
+ - 自绘三按钮(最小化 / 最大化 / 关闭)在 macOS / Linux 上接入原生窗口控制,双击最大化 / 还原、`isMaximized` 状态查询真实可用(此前 mac/linux 窗口控制为静默空转、`isMaximized` 恒 false);
15
+ - 最大化语义统一为"铺满工作区"的正常窗口最大化:Windows 经 `WM_GETMINMAXINFO` 限定到监视器工作区(rcWork),macOS 走 `zoom:`、Linux 走 `gtk_window_maximize`,均非 F11 式整屏全屏;
16
+ - 打包流程全程仍不依赖 Go 工具链(`freedom build` 直接复制预编译壳,详见 v1.1.11 说明)。
17
+
18
+ **v1.12.11 标题栏优化**:自绘标题栏(frameless)增强——
19
+ - 标题栏左侧显示应用图标,图标直接从 exe 内嵌图标(PE 资源 ID=1)提取为 PNG data URL,**不再依赖 resources 资源文件夹**里的任何图标文件,跨平台无 resources 依赖;
20
+ - 双击标题栏空白区最大化 / 还原;
21
+ - 右键标题栏弹出系统菜单(还原 / 最小化 / 最大化 / 关闭),并按窗口状态自动置灰;
22
+ - 标题栏拖动改由 `window.freedom.window.startDrag()` 原生发起,保留双击与右键事件(`-webkit-app-region: drag` 会吞掉页面事件)。
23
+
24
+ **v1.12.0 核心升级**:标题栏默认改为完全无边框(`frameless`)——标题栏不存在,关闭 / 最大化 / 最小化按钮由前端自绘融入 UI(模板已内置自绘标题栏示例),不再依赖 Windows 原生按钮。`freedom titlebar` 仍可随时切回 `native`。
8
25
 
9
26
  **v1.12.x 修复与加固**:
10
27
  - 修复 close 按钮缺陷:窗口关闭改走 `PostMessage(WM_CLOSE)`(原 `CloseWindow` 语义为最小化,导致点关闭只最小化);
@@ -13,7 +30,7 @@ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌
13
30
  - 壳进程启用 DPI 感知,高 DPI 屏幕下窗口坐标 / 渲染 / 鼠标自动化定位一致,内容更清晰;
14
31
  - `freedom config set` 兼容双引号配置写法;依赖变更检测改用 `package-lock.json` 作基准。
15
32
 
16
- **v1.11.0 核心升级**:支持自定义应用图标 + 默认隐藏 Windows 原生标题栏。配置 `freedom.config.js` 的 `icon`(Windows 用 `.ico`,macOS 用 `.icns`),`freedom build` 时自动把图标注入到 exe 的 PE 资源(RT_ICON / RT_GROUP_ICON)或 `.app/Contents/Resources`,无需任何资源编译器;`freedom icon <path>` 可一键设置。标题栏默认值由 `native` 调整为 `hidden`——保留 Windows 原生最小化 / 最大化 / 关闭按钮,隐藏标题栏视觉(DWM 扩展实现),默认不再占用窗口顶部空间。
33
+ **v1.11.0 核心升级**:支持自定义应用图标 + 无边框标题栏。配置 `freedom.config.js` 的 `icon`(Windows 用 `.ico`,macOS 用 `.icns`),`freedom build` 时自动把图标注入到 exe 的 PE 资源(RT_ICON / RT_GROUP_ICON)或 `.app/Contents/Resources`,无需任何资源编译器;`freedom icon <path>` 可一键设置。标题栏默认使用无边框(`frameless`),窗口控制按钮由前端自绘,不再占用窗口顶部空间。
17
34
 
18
35
  **v1.10.12 核心升级**:macOS 产物升级为标准 .app 应用包,直接产出 `.app.zip`(mac 用户解压即得 `.app`,拖入 /Applications 即可使用);在 macOS 上执行 `freedom dmg` 可再用系统 hdiutil 生成 `.dmg`。全程仍不依赖 Go 工具链。
19
36
 
@@ -21,7 +38,7 @@ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌
21
38
 
22
39
  **v1.11.0 新特性**:
23
40
  - 自定义应用图标:`icon` 配置 + `freedom icon <path>` 命令,Windows 构建时用 rcedit 注入 `.ico` 到 exe 资源(支持多尺寸,资源管理器 / 任务栏 / 快捷方式统一显示),macOS 构建时把 `.icns` 放入 `.app` 并写入 Info.plist;
24
- - 标题栏默认改为 `hidden`:默认隐藏标题栏视觉,仅保留 Windows 原生最小化 / 最大化 / 关闭按钮;仍可用 `freedom titlebar` 随时切回 `native` / `frameless`。
41
+ - 标题栏默认无边框(`frameless`):标题栏与 Windows 原生最小化 / 最大化 / 关闭按钮均不存在,由前端自绘;仍可用 `freedom titlebar` 随时切回 `native`。
25
42
 
26
43
  **v1.10.12 新特性**:
27
44
  - macOS 产物打包为标准 `.app` bundle + `.app.zip`:壳复制进 `Contents/MacOS/`,resources 与前端/后端随包,解压即用,无需任何语言运行时;
@@ -54,7 +71,6 @@ freedom tui
54
71
 
55
72
  # 3. 调整标题栏(可选,随时可改;默认 frameless:标题栏不存在,关闭/最大化/最小化按钮由前端自绘融入 UI)
56
73
  freedom titlebar native # 保留系统原生标题栏
57
- freedom titlebar hidden # 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
58
74
  freedom titlebar frameless # 完全无边框,标题栏不存在,三按钮由前端自绘(默认,模板已内置示例)
59
75
 
60
76
  # 3.5 设置应用图标(可选;Windows 用 .ico,macOS 用 .icns)
@@ -64,7 +80,7 @@ freedom icon icon.ico
64
80
  freedom build # 默认当前平台
65
81
  freedom build --platform all # 三平台全量(win + mac + linux)
66
82
  freedom build --platform win-x64 # 仅 Windows
67
- freedom build --platform mac-arm64 # 仅 macOS Apple Silicon
83
+ freedom build --platform darwin-arm64 # 仅 macOS Apple Silicon
68
84
  freedom build --platform linux-x64 # 仅 Linux
69
85
  freedom build --no-cache # 忽略前端构建缓存,强制重建
70
86
  ```
@@ -91,7 +107,7 @@ mac 用户解压 `.app.zip` 即得 `.app`,拖入 `/Applications` 即可直接
91
107
 
92
108
  **壳二进制平台校验**:`freedom build` 在分发前会读取壳二进制文件头,校验其确为目标平台的真实格式(Windows PE / macOS Mach-O / Linux ELF)。若某平台壳缺失或格式不匹配(例如误用其它平台的二进制顶替),会**明确报错并给出修复指引**,绝不静默产出无法运行的假产物。
93
109
 
94
- **预编译壳的来源与 CI**:`webview_go` 依赖各系统自带 WebView 框架,**无法交叉编译**,三平台壳必须在对应平台本机编译。仓库已提供 `.github/workflows/build-shell.yml`:在 win / mac(Intel / Apple Silicon)/ linux runner 上分别编译真实壳并上传到 GitHub Release,供 `freedom shell download` 拉取。发布新版本时:先 `npm publish`,再创建同名 GitHub Release,手动触发 `build-shell` workflow(或直接发 Release 自动触发)即自动补齐各平台壳资产。
110
+ **预编译壳的来源与 CI**:`webview_go` 依赖各系统自带 WebView 框架,**无法交叉编译**,三平台壳必须在对应平台本机编译。仓库已提供 `.github/workflows/build-shell.yml`:在 win / mac(Apple Silicon)/ linux runner 上分别编译真实壳并上传到 GitHub Release,供 `freedom shell download` 拉取(Intel Mac 已不支持,见 `nativePlatform()` 的明确报错)。发布新版本时:先 `npm publish`,再创建同名 GitHub Release,手动触发 `build-shell` workflow(或直接发 Release 自动触发)即自动补齐各平台壳资产。
95
111
 
96
112
  产物目录可在 `freedom.config.js` 的 `outDir` 中调整:默认 `'dist'`,设为 `'.'` 则直接输出到项目根目录(dist 的上级),设为任意相对 / 绝对路径亦可。输出到项目根目录时会自动跳过 `index.html` 副本,避免覆盖项目源文件。
97
113
 
@@ -104,11 +120,10 @@ freedom config set outDir dist # 恢复默认 dist/
104
120
 
105
121
  | 模式 | 说明 |
106
122
  | --- | --- |
107
- | `native` | 保留系统原生标题栏 |
108
- | `hidden` | 隐藏标题栏视觉,仅保留 Windows 原生最小化 / 最大化 / 关闭按钮(DWM 扩展实现) |
109
- | `frameless` | 完全无边框,标题栏不存在,客户区铺满窗口,关闭 / 最大化 / 最小化按钮由前端自绘(默认,模板已内置示例) |
123
+ | `native` | 保留系统原生标题栏,标题栏图标与 exe 图标一致 |
124
+ | `frameless` | 完全无边框,标题栏不存在,客户区铺满窗口,关闭 / 最大化 / 最小化按钮由前端自绘(默认,模板已内置示例)。当前仅 Windows 完整实现;macOS / Linux 由系统窗口管理器托管,回退为原生标题栏且窗口控制动作返回明确错误 |
110
125
 
111
- `hidden` / `frameless` 模式下,前端可通过注入的 `window.freedom.window` API 控制窗口(`minimize` / `maximize` / `toggleMaximize` / `close` / `isMaximized` / `isFrameless`),模板已内置自绘标题栏示例。
126
+ `frameless` 模式下(Windows),前端可通过注入的 `window.freedom.window` API 控制窗口(`minimize` / `maximize` / `toggleMaximize` / `close` / `isMaximized` / `isFrameless`),模板已内置自绘标题栏示例;macOS / Linux 上这些动作会 reject,前端应捕获并提示。
112
127
 
113
128
  ## 配置(freedom.config.js)
114
129
 
@@ -121,7 +136,7 @@ export default {
121
136
  minHeight: 300,
122
137
  center: true, // 启动居中
123
138
  debug: false, // 开发者工具
124
- titlebar: 'frameless', // frameless(默认,标题栏不存在,三按钮由前端自绘)| native | hidden
139
+ titlebar: 'frameless', // frameless(默认,标题栏不存在,三按钮由前端自绘)| native
125
140
  icon: undefined, // 应用图标:Windows 用 .ico(推荐多尺寸),macOS 用 .icns
126
141
  outDir: 'dist', // 产物目录:'dist'(默认)| '.'(项目根目录)| 任意路径
127
142
  // backend: { command: 'node', args: ['backend/main.mjs'] }, // 任意语言后端进程
@@ -155,12 +170,14 @@ window.freedom.window.minimize(); // 窗口控制
155
170
  ```
156
171
  freedom tui # 交互式终端界面
157
172
  freedom init <目录> [--force]
158
- freedom build [--platform win-x64|mac-arm64|linux-x64|all] [--no-cache]
159
- freedom titlebar <native|hidden|frameless>
173
+ freedom build [--platform win-x64|darwin-arm64|linux-x64|all] [--no-cache]
174
+ freedom titlebar <native|frameless>
160
175
  freedom icon <path> # 设置应用图标(Windows 用 .ico,macOS 用 .icns)
161
176
  freedom config [get|set]
162
177
  freedom shell list|download <platform>|build <platform>
163
178
  freedom dmg [--platform <plat>] # 在 macOS 上把 .app 打包为 .dmg
179
+ freedom version # 显示版本并检测最新版本
180
+ freedom update # 检查新版本并给出升级命令(同 check-update)
164
181
  freedom tutorial
165
182
  freedom help
166
183
  ```
package/bin/freedom.js CHANGED
@@ -2,8 +2,12 @@
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
+ run(process.argv.slice(2)).then(async (code) => {
8
+ if (code === 0 && process.argv[2] !== 'tui') {
9
+ try { await maybeNotifyUpdate(); } catch (e) { /* 检测失败静默 */ }
10
+ }
7
11
  process.exit(code || 0);
8
12
  }).catch((err) => {
9
13
  console.error('[freedom] 执行失败:', err && err.message ? err.message : err);
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 进程)
@@ -421,4 +440,26 @@ function intVal(v, dft) {
421
440
  return Number.isFinite(n) && n >= 0 ? n : dft;
422
441
  }
423
442
 
443
+ // 非 singlefile 产物检测(历史 bug B47):壳只 SetHtml 单页内存加载,HTML 里引用
444
+ // 的外部 <script src> / <link href>(相对路径或 / 根路径)在无服务器环境下必然失效,
445
+ // 导致页面 JS/CSS 丢失静默空白。检测到此类引用时明确告警,避免用户无感知翻车。
446
+ function warnIfNotSingleFile(html, distHtml) {
447
+ const refs = [];
448
+ const re = /<(?:script|link)\b[^>]*(?:src|href)\s*=\s*["']([^"']+)["']/gi;
449
+ let m;
450
+ while ((m = re.exec(html)) !== null) {
451
+ const url = m[1];
452
+ // data: / blob: / http(s): / file: 可正常工作,跳过;其余(相对、/ 根路径、// 协议相对)均会失效
453
+ if (/^(?:data:|blob:|https?:|file:)/i.test(url)) continue;
454
+ refs.push(url);
455
+ }
456
+ if (refs.length > 0) {
457
+ console.warn(
458
+ `[freedom] 警告:${path.basename(distHtml)} 引用了外部资源(${refs.join(', ')})。` +
459
+ `壳在内存加载单页时这些引用会失效,导致页面空白。` +
460
+ `请在 vite.config.js 启用 vite-plugin-singlefile 将 JS/CSS 内联进 HTML。`
461
+ );
462
+ }
463
+ }
464
+
424
465
  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|hidden|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 保留系统原生标题栏
46
- hidden 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
47
- frameless 完全无边框,标题栏不存在,关闭/最大化/最小化按钮由前端自绘(默认,模板已内置示例)
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
 
@@ -92,42 +121,40 @@ async function run(argv) {
92
121
  }
93
122
  const { results } = await build(process.cwd(), { platform, noCache: rest.includes('--no-cache') });
94
123
  for (const r of results) {
95
- console.log(`[freedom] [${r.plat}] 构建完成:${r.outFile}`);
124
+ console.log(`${ok('构建完成')} ${paint(`[${r.plat}]`, C.fg.magenta, C.bold)} ${paint(r.outFile, C.fg.white)}`);
96
125
  }
97
126
  return 0;
98
127
  }
99
128
 
100
- case 'shell': {
129
+ case 'shell':
101
130
  return await runShell(rest);
102
- }
103
131
 
104
- case 'dmg': {
132
+ case 'dmg':
105
133
  return await runDmg(rest);
106
- }
107
134
 
108
135
  case 'titlebar': {
109
136
  const mode = rest[0];
110
- if (!['native', 'hidden', 'frameless'].includes(mode)) {
111
- console.error('[freedom] 用法:freedom titlebar <native|hidden|frameless>');
137
+ if (!['native', 'frameless'].includes(mode)) {
138
+ console.error(`${err('用法:')}${paint('freedom titlebar <native|frameless>', C.fg.cyan)}`);
112
139
  return 1;
113
140
  }
114
141
  setConfig(process.cwd(), 'titlebar', mode);
115
- console.log(`[freedom] titlebar 已切换为:${mode}`);
116
- console.log(' 运行 freedom build 重新打包生效。');
142
+ console.log(`${ok('titlebar 已切换为:')}${paint(mode, C.fg.cyan, C.bold)}`);
143
+ console.log(` ${dim('运行')} ${paint('freedom build', C.fg.cyan)} ${dim('重新打包生效。')}`);
117
144
  return 0;
118
145
  }
119
146
 
120
147
  case 'icon': {
121
148
  const iconPath = rest[0];
122
149
  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 格式)');
150
+ console.error(`${err('用法:')}${paint('freedom icon <path>', C.fg.cyan)}`);
151
+ console.error(` ${dim('示例:freedom icon icon.ico (Windows exe 图标,.ico 格式)')}`);
152
+ console.error(` ${dim('freedom icon icon.icns (macOS .app 图标,.icns 格式)')}`);
126
153
  return 1;
127
154
  }
128
155
  setConfig(process.cwd(), 'icon', iconPath);
129
- console.log(`[freedom] icon 已设置为:${iconPath}`);
130
- console.log(' 运行 freedom build 重新打包生效。');
156
+ console.log(`${ok('icon 已设置为:')}${paint(iconPath, C.fg.cyan, C.bold)}`);
157
+ console.log(` ${dim('运行')} ${paint('freedom build', C.fg.cyan)} ${dim('重新打包生效。')}`);
131
158
  return 0;
132
159
  }
133
160
 
@@ -142,11 +169,11 @@ async function run(argv) {
142
169
  const key = rest[1];
143
170
  const value = rest[2];
144
171
  if (!key || value === undefined) {
145
- console.error('[freedom] 用法:freedom config set <key> <value>');
172
+ console.error(`${err('用法:')}${paint('freedom config set <key> <value>', C.fg.cyan)}`);
146
173
  return 1;
147
174
  }
148
175
  setConfig(process.cwd(), key, coerce(value));
149
- console.log(`[freedom] ${key} = ${coerce(value)}`);
176
+ console.log(`${ok('已设置')} ${paint(key, C.fg.cyan, C.bold)} = ${paint(JSON.stringify(coerce(value)), C.fg.white)}`);
150
177
  return 0;
151
178
  }
152
179
  console.log(await showConfig(process.cwd()));
@@ -156,12 +183,12 @@ async function run(argv) {
156
183
  case 'tutorial': {
157
184
  const file = tutorialFile();
158
185
  openBrowser(file);
159
- console.log(`[freedom] 教程已打开:${file}`);
186
+ console.log(`${ok('教程已打开:')}${paint(file, C.fg.cyan)}`);
160
187
  return 0;
161
188
  }
162
189
 
163
190
  default:
164
- console.error(`[freedom] 未知命令:${cmd}\n`);
191
+ console.error(`${err('未知命令:')}${paint(cmd, C.fg.red, C.bold)}\n`);
165
192
  console.log(help());
166
193
  return 1;
167
194
  }
@@ -186,37 +213,37 @@ async function runShell(rest) {
186
213
  case 'list': {
187
214
  const ready = listLocal();
188
215
  if (ready.length === 0) {
189
- console.log('[freedom] 本地暂无预编译壳。');
216
+ console.log(`${dim('本地暂无预编译壳。')}`);
190
217
  } else {
191
- console.log('[freedom] 本地已就绪的壳平台:');
192
- for (const p of ready) console.log(` ${p}`);
218
+ console.log(`${ok('本地已就绪的壳平台:')}`);
219
+ for (const p of ready) console.log(` ${paint('✓', C.fg.green)} ${paint(p, C.fg.cyan, C.bold)}`);
193
220
  }
194
- console.log(`[freedom] 可选平台:${ALL_PLATFORMS.join(' / ')}`);
221
+ console.log(`${dim('可选平台:')}${paint(ALL_PLATFORMS.join(' / '), C.fg.gray)}`);
195
222
  return 0;
196
223
  }
197
224
  case 'download': {
198
225
  const plat = rest[1];
199
226
  if (!plat) {
200
- console.error('[freedom] 用法:freedom shell download <win-x64|darwin-arm64|linux-x64|linux-arm64>');
227
+ console.error(`${err('用法:')}${paint('freedom shell download <win-x64|darwin-arm64|linux-x64|linux-arm64>', C.fg.cyan)}`);
201
228
  return 1;
202
229
  }
203
230
  const dest = await downloadShell(plat);
204
- console.log(`[freedom] 已下载 ${plat} 壳:${dest}`);
231
+ console.log(`${ok('已下载')} ${paint(plat, C.fg.magenta, C.bold)} ${dim('壳:')}${paint(dest, C.fg.white)}`);
205
232
  return 0;
206
233
  }
207
234
  case 'build': {
208
235
  const plat = rest[1];
209
236
  if (!plat) {
210
- console.error('[freedom] 用法:freedom shell build <win-x64|darwin-arm64|linux-x64|linux-arm64>');
237
+ console.error(`${err('用法:')}${paint('freedom shell build <win-x64|darwin-arm64|linux-x64|linux-arm64>', C.fg.cyan)}`);
211
238
  return 1;
212
239
  }
213
240
  const dest = buildShell(plat);
214
- console.log(`[freedom] 已编译 ${plat} 壳:${dest}`);
215
- console.log('[freedom] 提示:壳已预编译随包分发,一般无需本地编译。');
241
+ console.log(`${ok('已编译')} ${paint(plat, C.fg.magenta, C.bold)} ${dim('壳:')}${paint(dest, C.fg.white)}`);
242
+ console.log(` ${dim('提示:壳已预编译随包分发,一般无需本地编译。')}`);
216
243
  return 0;
217
244
  }
218
245
  default:
219
- console.error(`[freedom] 未知 shell 子命令:${sub}`);
246
+ console.error(`${err('未知 shell 子命令:')}${paint(sub, C.fg.red, C.bold)}`);
220
247
  return 1;
221
248
  }
222
249
  }
@@ -235,7 +262,7 @@ async function runDmg(rest) {
235
262
  ? (platArg.includes('=') ? platArg.split('=')[1] : rest[rest.indexOf(platArg) + 1])
236
263
  : nativePlatform();
237
264
  if (!plat || !plat.startsWith('darwin')) {
238
- console.error('[freedom] dmg 仅支持 macOS 平台(darwin-arm64)。');
265
+ console.error(`${err('dmg 仅支持 macOS 平台(darwin-arm64)。')}`);
239
266
  return 1;
240
267
  }
241
268
 
@@ -244,18 +271,18 @@ async function runDmg(rest) {
244
271
  const candidates = [path.join(baseDir, `${name}.app`), path.join(baseDir, plat, `${name}.app`)];
245
272
  const appDir = candidates.find((p) => fs.existsSync(p));
246
273
  if (!appDir) {
247
- console.error(`[freedom] 未找到 ${name}.app(已检查 ${candidates.join(' / ')})。`);
248
- console.error(' 请先在 macOS 上运行 freedom build --platform mac 生成 .app。');
274
+ console.error(`${err('未找到')} ${paint(`${name}.app`, C.fg.cyan)} ${dim(`(已检查 ${candidates.join(' / ')})。`)}`);
275
+ console.error(` ${dim('请先在 macOS 上运行')} ${paint('freedom build --platform mac', C.fg.cyan)} ${dim('生成 .app。')}`);
249
276
  return 1;
250
277
  }
251
278
 
252
279
  const outPath = path.join(path.dirname(appDir), `${name}-${plat}.dmg`);
253
280
  try {
254
281
  const dmgPath = await makeDmg(appDir, outPath, name);
255
- console.log(`[freedom] 已生成 dmg:${dmgPath}`);
282
+ console.log(`${ok('已生成 dmg:')}${paint(dmgPath, C.fg.white)}`);
256
283
  return 0;
257
284
  } catch (e) {
258
- console.error(`[freedom] ${e.message}`);
285
+ console.error(`${err(e.message)}`);
259
286
  return 1;
260
287
  }
261
288
  }