@yufengtadian/freedom-cli 1.0.0 → 1.1.11

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 CHANGED
@@ -1,8 +1,15 @@
1
1
  # freedom-cli
2
2
 
3
- Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用。
3
+ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.1.11)。
4
4
 
5
- 基于自研 Freedom WebView 壳层(对标 Wails / Tauri):前端完全自由、后端可任意语言、渲染复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK),产物为单个可执行文件,前端页面内存加载,不占本地端口。
5
+ 基于自研 Freedom WebView 壳层(对标 Wails / Tauri):前端完全自由、后端可任意语言、渲染复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK),产物为单个可执行文件 + resources 目录,前端页面内存加载,不占本地端口。
6
+
7
+ **v1.1.11 核心升级**:不再依赖 Go 工具链。壳层以预编译二进制随 npm 包分发(`shell/<platform>/`),`freedom build` 直接复制壳 + 写入前端与配置即可出包,Windows / macOS / Linux 三平台一次全出,任意前后端语言自由组合。
8
+
9
+ **v1.1.11 新特性**:
10
+ - `freedom tui` 交互式终端界面:菜单化完成打包 / 新建项目 / 修改配置 / 壳管理 / 教程,零依赖、方向键 + 回车导航、支持搜索过滤与多选;
11
+ - 前端构建缓存:源码无改动时跳过重复 `vite build`(秒级复用,典型加速 10 倍+),`--no-cache` 强制全量重建;
12
+ - 多平台分发并行化:`--platform all` 各平台同时复制分发,全平台打包耗时显著缩短。
6
13
 
7
14
  ## 安装
8
15
 
@@ -20,16 +27,28 @@ freedom init my-app
20
27
  cd my-app
21
28
  npm install
22
29
 
23
- # 2. 调整标题栏(可选,随时可改)
30
+ # 2. 交互式终端界面(方向键 + 回车选择,q 返回)
31
+ freedom tui
32
+
33
+ # 3. 调整标题栏(可选,随时可改)
24
34
  freedom titlebar native # 保留系统原生标题栏(默认)
25
35
  freedom titlebar hidden # 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
26
36
  freedom titlebar frameless # 完全无边框,按钮由前端自绘(模板已内置示例)
27
37
 
28
- # 3. 打包
29
- freedom build
38
+ # 4. 打包
39
+ freedom build # 默认当前平台
40
+ freedom build --platform all # 三平台全量(win + mac + linux)
41
+ freedom build --platform win-x64 # 仅 Windows
42
+ freedom build --platform mac-arm64 # 仅 macOS Apple Silicon
43
+ freedom build --platform linux-x64 # 仅 Linux
44
+ freedom build --no-cache # 忽略前端构建缓存,强制重建
30
45
  ```
31
46
 
32
- 构建产物默认输出到 `dist/`,Windows 下为 `dist/<应用名>.exe`。产物为 GUI 子系统程序,运行时不会弹出 cmd 黑窗。
47
+ 构建产物默认输出到 `dist/`。`dist/<应用名>.exe`(或对应平台可执行文件)为壳层程序,运行时从 `dist/resources/` 加载前端页面与配置,不弹 cmd 黑窗。
48
+
49
+ **构建缓存**:CLI 会在 `.freedom/vite-dist/` 记录前端源码指纹,源码无改动时复用上次构建产物直接进入分发阶段(秒级完成)。修改了 `src/` / `index.html` / `vite.config.js` 等任一处会触发自动重建;需要强制全量时可加 `--no-cache`。
50
+
51
+ **三平台打包**:壳层二进制随包分发,无需本机安装 Go 或交叉编译工具链。首次构建某平台时若本地缺少对应壳二进制,CLI 自动从 GitHub Releases 下载(`freedom shell download <platform>` 可手动补齐);仅当你想用本机 Go 工具链亲自编译壳时才需要 `freedom shell build`(只产出本机平台)。多平台分发为并行执行,全平台打包耗时大幅缩短。
33
52
 
34
53
  产物目录可在 `freedom.config.js` 的 `outDir` 中调整:默认 `'dist'`,设为 `'.'` 则直接输出到项目根目录(dist 的上级),设为任意相对 / 绝对路径亦可。输出到项目根目录时会自动跳过 `index.html` 副本,避免覆盖项目源文件。
35
54
 
@@ -78,19 +97,37 @@ window.freedom.window.minimize(); // 窗口控制
78
97
  ## 构建要求
79
98
 
80
99
  - Node.js >= 18
81
- - Go 工具链(编译壳层,需支持 CGO;Windows 下需要 MSVC 编译环境与 WebView2 运行时,Win10/11 自带)
100
+ - 不需要 Go 工具链(壳层为随包分发的预编译二进制)
101
+ - 可选:`freedom shell build` 需本机 Go(含 CGO,Windows 需 MSVC 编译环境),仅用于亲自编译壳
82
102
 
83
103
  ## 命令
84
104
 
85
105
  ```
106
+ freedom tui # 交互式终端界面
86
107
  freedom init <目录> [--force]
87
- freedom build
108
+ freedom build [--platform win-x64|mac-arm64|linux-x64|all] [--no-cache]
88
109
  freedom titlebar <native|hidden|frameless>
89
110
  freedom config [get|set]
111
+ freedom shell list|download <platform>|build <platform>
90
112
  freedom tutorial
91
113
  freedom help
92
114
  ```
93
115
 
116
+ ## 任意语言后端
117
+
118
+ 壳与后端进程通过 stdin/stdout 的 NDJSON/JSON-RPC 桥接(协议语言无关),因此后端可用任意语言实现(Node / Python / Rust / Go / C#…)。
119
+
120
+ 在 `freedom.config.js` 中声明后端进程,`freedom build` 会把你的 `backend/` 目录原样分发到 `dist/resources/backend/`,壳启动时自动以 `resources/` 为工作目录拉起后端进程(Windows 下后端黑窗同样隐藏):
121
+
122
+ ```js
123
+ export default {
124
+ ...
125
+ backend: { command: 'node', args: ['backend/main.mjs'] },
126
+ };
127
+ ```
128
+
129
+ 后端只需读写 stdin/stdout,示例见模板注释与 `tutorial` 教程。
130
+
94
131
  ## 关于生成产物
95
132
 
96
133
  本工具生成的所有文件(含模板与构建产物)均干净整洁,无任何冗余尾注,可直接作为交付物使用。
package/lib/build.js CHANGED
@@ -1,16 +1,33 @@
1
1
  'use strict';
2
2
 
3
+ // freedom build:前端打包 + 通用壳分发(v1.1.10,零语言工具链)
4
+ //
5
+ // 流程(不再调用 go build / 不依赖 Go):
6
+ // 1. 前端打包:npm install(如缺依赖)+ vite build -> .freedom/vite-dist/index.html
7
+ // 2. 选择目标平台:--platform <win|mac|linux|all>(默认当前平台)
8
+ // 3. 每个平台:
9
+ // - 取预编译通用壳二进制(包内 shell/<plat>/,缺失自动从 GitHub Releases 下载)
10
+ // - 写 resources/index.html(前端单文件页)
11
+ // - 写 resources/config.json(窗口 + 后端配置,壳运行时读取)
12
+ // - 复制壳为 outDir/<app>[.exe]
13
+ // - 复制 backend/ 到 resources/backend/(配置了 backend 时)
14
+ //
15
+ // 产物不嵌入 HTML,页面与配置均在 exe 同目录 resources/ 下,因此壳可复用、
16
+ // 可跨平台分发、无需在目标机器安装任何语言运行时。
17
+
3
18
  const fs = require('fs');
19
+ const fsp = fs.promises;
4
20
  const path = require('path');
5
- const os = require('os');
6
21
  const { spawnSync } = require('child_process');
7
- const { goTemplateDir, copyDir, goJSONString } = require('./utils');
8
-
9
- const TITLEBAR_MAP = {
10
- native: 'freedom.TitleBarNative',
11
- hidden: 'freedom.TitleBarHidden',
12
- frameless: 'freedom.TitleBarFrameless',
13
- };
22
+ const { copyDir } = require('./utils');
23
+ const {
24
+ ALL_PLATFORMS,
25
+ isWinPlat,
26
+ platformExeName,
27
+ nativePlatform,
28
+ localShellPath,
29
+ } = require('./utils');
30
+ const { hasShell, downloadShell } = require('./shell');
14
31
 
15
32
  function run(cmd, args, opts = {}) {
16
33
  // Windows 下 npm 是 .cmd 批处理,必须经 shell 执行
@@ -29,12 +46,17 @@ function run(cmd, args, opts = {}) {
29
46
  return res;
30
47
  }
31
48
 
32
- function checkGo() {
33
- const res = run('go', ['version']);
34
- if (res.status !== 0) {
35
- throw new Error('未检测到 Go 工具链。请先安装 Go(https://go.dev/dl/),并确保 go 命令在 PATH 中。');
36
- }
37
- return res.stdout.trim();
49
+ // --platform 解析:win|mac|linux|all 或平台 key(win-x64 等)
50
+ function parsePlatforms(raw) {
51
+ if (!raw) return [nativePlatform()];
52
+ const v = String(raw).toLowerCase();
53
+ if (v === 'all') return ALL_PLATFORMS;
54
+ const keyMap = { win: 'win-x64', mac: 'darwin-x64', linux: 'linux-x64' };
55
+ if (keyMap[v]) return [keyMap[v]];
56
+ if (ALL_PLATFORMS.includes(v)) return [v];
57
+ throw new Error(
58
+ `未知平台:${raw}。可选:win / mac / linux / all,或 ${ALL_PLATFORMS.join(' / ')}`
59
+ );
38
60
  }
39
61
 
40
62
  async function build(projectDir, opts = {}) {
@@ -47,118 +69,185 @@ async function build(projectDir, opts = {}) {
47
69
  const cfg = await loadConfig(dir);
48
70
 
49
71
  const name = (cfg.name || 'freedom-app').replace(/[^a-zA-Z0-9_.-]/g, '-');
50
- const titlebar = TITLEBAR_MAP[cfg.titlebar] || 'freedom.TitleBarNative';
51
-
52
- // 产物目录:默认 dist,可配置为 '.'(项目根目录,即 dist 的上级)或任意相对/绝对目录。
53
72
  const outDir = String(cfg.outDir || 'dist').trim() || 'dist';
54
73
  const outDirPath = path.resolve(dir, outDir);
55
- const exeName = os.platform() === 'win32' ? `${name}.exe` : name;
56
- const outFile = path.join(outDirPath, exeName);
74
+ const platforms = parsePlatforms(opts.platform);
75
+ const autoDownload = opts.autoDownload !== false;
57
76
 
58
- checkGo();
59
-
60
- // 1) 前端打包:npm install(如缺依赖)+ vite build -> .freedom/vite-dist/index.html
77
+ // 1) 前端打包(带缓存:源码未变更时复用上次 vite 产物,跳过 npm run build)
61
78
  ensureNodeModules(dir);
62
- const vite = run('npm', ['run', 'build'], { cwd: dir });
63
- if (vite.status !== 0) {
64
- throw new Error(`前端打包失败:\n${vite.stdout}\n${vite.stderr}`);
79
+ const useCache = opts.noCache !== true;
80
+ if (useCache && !viteNeedsRebuild(dir)) {
81
+ process.stdout.write('[freedom] 前端源码无改动,复用构建缓存...\n');
82
+ } else {
83
+ const vite = run('npm', ['run', 'build'], { cwd: dir });
84
+ if (vite.status !== 0) {
85
+ throw new Error(`前端打包失败:\n${vite.stdout}\n${vite.stderr}`);
86
+ }
87
+ writeViteCacheMarker(dir);
65
88
  }
66
89
  const distHtml = path.join(dir, '.freedom', 'vite-dist', 'index.html');
67
90
  if (!fs.existsSync(distHtml)) {
68
91
  throw new Error(`前端打包完成但未找到 ${distHtml},请检查 vite 配置(vite-plugin-singlefile)。`);
69
92
  }
93
+ const html = fs.readFileSync(distHtml, 'utf8');
94
+ const configJSON = renderConfigJSON(cfg, name);
70
95
 
71
- // 2) 组装壳层构建目录
72
- const buildDir = path.join(dir, '.freedom', 'build');
73
- fs.rmSync(buildDir, { recursive: true, force: true });
74
- fs.mkdirSync(buildDir, { recursive: true });
75
- copyDir(goTemplateDir(), buildDir);
76
-
77
- // 3) 写入前端产物与后端
78
- fs.copyFileSync(distHtml, path.join(buildDir, 'pkg', 'freedom', 'assets', 'index.html'));
96
+ // 2) 后端目录(若配置了 backend 进程)
79
97
  const backendDir = path.join(dir, cfg.backendDir || 'backend');
80
- if (cfg.backend && fs.existsSync(backendDir)) {
81
- copyDir(backendDir, path.join(buildDir, 'backend'));
98
+ const hasBackend = !!(cfg.backend && fs.existsSync(backendDir));
99
+
100
+ // 3) 逐平台分发(多平台并行,显著缩短全平台打包耗时)
101
+ // 单平台直接输出到 outDir;多平台各自放到 outDir/<plat>/ 子目录,避免互相覆盖
102
+ const multi = platforms.length > 1;
103
+ const results = await Promise.all(
104
+ platforms.map(async (plat) => {
105
+ const targetDir = multi ? path.join(outDirPath, plat) : outDirPath;
106
+ const outFile = await emitPlatform({ plat, name, targetDir, html, configJSON, backendDir, hasBackend, autoDownload });
107
+ return { plat, outFile };
108
+ })
109
+ );
110
+
111
+ return { results };
112
+ }
113
+
114
+ async function emitPlatform({ plat, name, targetDir, html, configJSON, backendDir, hasBackend, autoDownload }) {
115
+ // 取预编译壳二进制
116
+ const shell = localShellPath(plat);
117
+ if (!fs.existsSync(shell)) {
118
+ if (!autoDownload) {
119
+ throw new Error(
120
+ `缺少平台 ${plat} 的壳二进制:${shell}\n` +
121
+ `可运行 freedom shell download ${plat} 下载,或 freedom shell build ${plat} 本地编译。`
122
+ );
123
+ }
124
+ process.stdout.write(`[freedom] 本地无 ${plat} 壳,尝试自动下载...\n`);
125
+ await downloadShell(plat);
82
126
  }
83
127
 
84
- // 4) 生成 gen_config.go
85
- const genConfig = renderGenConfig(cfg, name, titlebar);
86
- fs.writeFileSync(path.join(buildDir, 'gen_config.go'), genConfig, 'utf8');
128
+ await fsp.mkdir(targetDir, { recursive: true });
87
129
 
88
- // 5) 编译壳层
89
- fs.mkdirSync(outDirPath, { recursive: true });
90
- if (process.env.FREEDOM_SKIP_CGO) {
91
- console.log(`[freedom] FREEDOM_SKIP_CGO=1,跳过 CGO 编译,壳层源码保留在 .freedom/build。`);
92
- return { outFile: null, buildDir };
130
+ // 壳二进制 -> 应用可执行文件
131
+ const exeName = platformExeName(plat, name);
132
+ const outFile = path.join(targetDir, exeName);
133
+ await fsp.copyFile(shell, outFile);
134
+ if (!isWinPlat(plat)) {
135
+ await fsp.chmod(outFile, 0o755);
93
136
  }
94
- const tidy = run('go', ['mod', 'tidy'], { cwd: buildDir });
95
- if (tidy.status !== 0) {
96
- throw new Error(`go mod tidy 失败:\n${tidy.stdout}\n${tidy.stderr}`);
137
+
138
+ // resources:页面 + 配置 + 后端
139
+ const resDir = path.join(targetDir, 'resources');
140
+ await fsp.mkdir(resDir, { recursive: true });
141
+ await fsp.writeFile(path.join(resDir, 'index.html'), html, 'utf8');
142
+ await fsp.writeFile(path.join(resDir, 'config.json'), configJSON, 'utf8');
143
+ if (hasBackend) {
144
+ copyDir(backendDir, path.join(resDir, 'backend'));
97
145
  }
98
- // Windows 用 GUI 子系统编译(-H windowsgui),运行时不弹出 cmd 黑窗。
99
- // 注意:go 的 flag 必须在位置参数('.')之前,否则会被当作 import path。
100
- const ldflags = process.platform === 'win32' ? ['-ldflags', '-H windowsgui'] : [];
101
- const buildRes = run('go', ['build', ...ldflags, '-o', outFile, '.'], {
102
- cwd: buildDir,
103
- env: { ...process.env, CGO_ENABLED: '1' },
104
- });
105
- if (buildRes.status !== 0) {
106
- throw new Error(`Go 编译失败:\n${buildRes.stdout}\n${buildRes.stderr}`);
146
+
147
+ return outFile;
148
+ }
149
+
150
+ // 渲染 resources/config.json(与壳 configfile.go 的 runtimeConfigFile 字段对齐)
151
+ function renderConfigJSON(cfg, name) {
152
+ const obj = {
153
+ name,
154
+ title: cfg.title || cfg.name || name,
155
+ titlebar: cfg.titlebar || 'native',
156
+ width: intVal(cfg.width, 1024),
157
+ height: intVal(cfg.height, 720),
158
+ minWidth: intVal(cfg.minWidth, 400),
159
+ minHeight: intVal(cfg.minHeight, 300),
160
+ center: typeof cfg.center === 'boolean' ? cfg.center : true,
161
+ debug: typeof cfg.debug === 'boolean' ? cfg.debug : false,
162
+ };
163
+ if (cfg.backend && Array.isArray(cfg.backend.command) && cfg.backend.command.length > 0) {
164
+ // 兼容 command 为数组形式:command=[cmd, ...args]
165
+ obj.backend = { command: cfg.backend.command[0], args: cfg.backend.command.slice(1) };
166
+ } else if (cfg.backend && typeof cfg.backend.command === 'string' && cfg.backend.command) {
167
+ obj.backend = {
168
+ command: cfg.backend.command,
169
+ args: Array.isArray(cfg.backend.args) ? cfg.backend.args : [],
170
+ };
107
171
  }
172
+ return JSON.stringify(obj, null, 2);
173
+ }
108
174
 
109
- // 6) 产物分发到 outDir:
110
- // - exe:已输出到 outDir;
111
- // - 单文件 index.html:outDir 非项目根目录时才写入(避免覆盖项目源文件);
112
- // - backend:随应用一起分发。
113
- if (path.resolve(outDirPath) !== dir) {
114
- fs.copyFileSync(distHtml, path.join(outDirPath, 'index.html'));
175
+ // ---- 前端构建缓存 ----
176
+ // 追踪影响前端产物的源文件(index.html / vite.config.* / src/**)的最新 mtime,
177
+ // 与上次 vite build 记录值比较:未变更则跳过 npm run build,直接复用 .freedom/vite-dist 产物。
178
+
179
+ function collectViteSources(dir) {
180
+ const files = [];
181
+ const roots = ['index.html', 'vite.config.js', 'vite.config.mjs', 'vite.config.ts', 'vite.config.cjs'];
182
+ for (const r of roots) {
183
+ const p = path.join(dir, r);
184
+ if (fs.existsSync(p)) files.push(p);
115
185
  }
116
- if (cfg.backend && fs.existsSync(backendDir)) {
117
- fs.mkdirSync(path.join(outDirPath, 'backend'), { recursive: true });
118
- copyDir(backendDir, path.join(outDirPath, 'backend'));
186
+ const src = path.join(dir, 'src');
187
+ if (fs.existsSync(src)) {
188
+ const walk = (d) => {
189
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
190
+ const p = path.join(d, entry.name);
191
+ if (entry.isDirectory()) walk(p);
192
+ else files.push(p);
193
+ }
194
+ };
195
+ walk(src);
119
196
  }
197
+ return files;
198
+ }
120
199
 
121
- return { outFile, buildDir };
200
+ function maxMtime(files) {
201
+ return files.reduce((m, f) => {
202
+ try {
203
+ const t = fs.statSync(f).mtimeMs;
204
+ return t > m ? t : m;
205
+ } catch (e) {
206
+ return m;
207
+ }
208
+ }, 0);
122
209
  }
123
210
 
124
- function ensureNodeModules(dir) {
125
- if (fs.existsSync(path.join(dir, 'node_modules'))) return;
126
- const res = run('npm', ['install'], { cwd: dir, stdio: 'inherit' });
127
- if (res.status !== 0) {
128
- throw new Error('npm install 失败。');
129
- }
211
+ function viteCacheMarker(dir) {
212
+ return path.join(dir, '.freedom', 'vite-dist', '.cache-mtime');
213
+ }
214
+
215
+ function viteNeedsRebuild(dir) {
216
+ const distHtml = path.join(dir, '.freedom', 'vite-dist', 'index.html');
217
+ const marker = viteCacheMarker(dir);
218
+ if (!fs.existsSync(distHtml) || !fs.existsSync(marker)) return true;
219
+ let recorded = 0;
220
+ try {
221
+ recorded = Number(fs.readFileSync(marker, 'utf8'));
222
+ } catch (e) { return true; }
223
+ // 留 1s 容差避免文件系统时间精度抖动
224
+ return maxMtime(collectViteSources(dir)) > recorded + 1000;
130
225
  }
131
226
 
132
- function renderGenConfig(cfg, name, titlebar) {
133
- const backendBlock = renderBackend(cfg);
134
- const lines = [];
135
- lines.push('package main');
136
- lines.push('');
137
- lines.push('import "freedom-cli-shell/pkg/freedom"');
138
- lines.push('');
139
- lines.push('// appConfig 返回应用窗口配置。');
140
- lines.push('// 本文件由 freedom CLI 在 build 阶段根据 freedom.config.js 生成,');
141
- lines.push('// 不要手动修改(手动修改会在下次 build 时被覆盖)。');
142
- lines.push('func appConfig() freedom.Config {');
143
- lines.push(' return freedom.Config{');
144
- lines.push(` Title: ${goJSONString(cfg.name || name)},`);
145
- lines.push(` TitleBar: ${titlebar},`);
146
- lines.push(` Width: ${intVal(cfg.width, 1024)},`);
147
- lines.push(` Height: ${intVal(cfg.height, 720)},`);
148
- lines.push(` MinWidth: ${intVal(cfg.minWidth, 0)},`);
149
- lines.push(` MinHeight: ${intVal(cfg.minHeight, 0)},`);
150
- lines.push(` Center: ${boolVal(cfg.center, true)},`);
151
- lines.push(` Debug: ${boolVal(cfg.debug, false)},`);
152
- lines.push(' }');
153
- lines.push('}');
154
- lines.push('');
155
- return lines.join('\n');
227
+ function writeViteCacheMarker(dir) {
228
+ fs.mkdirSync(path.join(dir, '.freedom', 'vite-dist'), { recursive: true });
229
+ fs.writeFileSync(viteCacheMarker(dir), String(maxMtime(collectViteSources(dir))), 'utf8');
156
230
  }
157
231
 
158
- function renderBackend(cfg) {
159
- // 当前版本:后端进程方式由用户在 main 模板内手动接线;
160
- // gen_config 仅承载窗口配置。此处预留注释说明。
161
- return '';
232
+ function ensureNodeModules(dir) {
233
+ // 缺陷修复:node_modules 存在但 package.json 更新过(依赖变更)时自动重装。
234
+ const pkgFile = path.join(dir, 'package.json');
235
+ const nmDir = path.join(dir, 'node_modules');
236
+ const marker = path.join(nmDir, '.package-lock.json');
237
+ if (!fs.existsSync(nmDir)) {
238
+ const res = run('npm', ['install'], { cwd: dir, stdio: 'inherit' });
239
+ if (res.status !== 0) throw new Error('npm install 失败。');
240
+ return;
241
+ }
242
+ if (fs.existsSync(pkgFile) && fs.existsSync(marker)) {
243
+ const pkgMtime = fs.statSync(pkgFile).mtimeMs;
244
+ const lockMtime = fs.statSync(marker).mtimeMs;
245
+ if (pkgMtime > lockMtime + 1000) {
246
+ process.stdout.write('[freedom] package.json 已更新,重新安装依赖...\n');
247
+ const res = run('npm', ['install'], { cwd: dir, stdio: 'inherit' });
248
+ if (res.status !== 0) throw new Error('npm install 失败。');
249
+ }
250
+ }
162
251
  }
163
252
 
164
253
  function intVal(v, dft) {
@@ -166,8 +255,4 @@ function intVal(v, dft) {
166
255
  return Number.isFinite(n) && n >= 0 ? n : dft;
167
256
  }
168
257
 
169
- function boolVal(v, dft) {
170
- return typeof v === 'boolean' ? v : dft;
171
- }
172
-
173
- module.exports = { build };
258
+ module.exports = { build, parsePlatforms };
package/lib/cli.js CHANGED
@@ -14,10 +14,16 @@ function help() {
14
14
  freedom - Freedom 桌面壳打包工具 v${VERSION}
15
15
 
16
16
  用法:
17
+ freedom tui 进入交互式 TUI 界面(新建/打包/配置/壳管理)
17
18
  freedom init [目录] [--force] 在当前/指定目录新建项目模板
18
- freedom build 前端打包并编译出桌面应用(产物在 dist/)
19
+ freedom build [--platform <p>] 前端打包并分发桌面应用(默认当前平台)
20
+ --platform win|mac|linux|all 指定目标平台(all = 三平台全量)
21
+ --no-cache 忽略前端构建缓存,强制重新打包
19
22
  freedom titlebar <native|hidden|frameless>
20
23
  一键切换标题栏策略
24
+ freedom shell list 列出本地已就绪的预编译壳
25
+ freedom shell download <plat> 从 GitHub Releases 下载预编译壳
26
+ freedom shell build <plat> 本地用 Go 编译壳(需要 Go)
21
27
  freedom config 查看当前配置
22
28
  freedom config get <key> 读取单个配置项
23
29
  freedom config set <key> <value> 修改单个配置项
@@ -25,6 +31,9 @@ freedom - Freedom 桌面壳打包工具 v${VERSION}
25
31
  freedom help 显示本帮助
26
32
  freedom version 显示版本
27
33
 
34
+ 平台(<p>/<plat>):win-x64 / darwin-x64 / darwin-arm64 / linux-x64 / linux-arm64,
35
+ 快捷别名:win / mac / linux / all。
36
+
28
37
  标题栏策略说明:
29
38
  native 保留系统原生标题栏(默认)
30
39
  hidden 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
@@ -49,6 +58,9 @@ async function run(argv) {
49
58
  console.log(VERSION);
50
59
  return 0;
51
60
 
61
+ case 'tui':
62
+ return await require('./tui').tui(process.cwd());
63
+
52
64
  case 'init': {
53
65
  const force = rest.includes('--force');
54
66
  const dirArg = rest.filter((a) => a !== '--force')[0];
@@ -62,13 +74,22 @@ async function run(argv) {
62
74
  }
63
75
 
64
76
  case 'build': {
65
- const { outFile } = await build(process.cwd());
66
- if (outFile) {
67
- console.log(`[freedom] 构建完成:${outFile}`);
77
+ const platArg = rest.find((a) => a.startsWith('--platform') || a.startsWith('-p'));
78
+ let platform;
79
+ if (platArg) {
80
+ platform = platArg.includes('=') ? platArg.split('=')[1] : rest[rest.indexOf(platArg) + 1];
81
+ }
82
+ const { results } = await build(process.cwd(), { platform, noCache: rest.includes('--no-cache') });
83
+ for (const r of results) {
84
+ console.log(`[freedom] [${r.plat}] 构建完成:${r.outFile}`);
68
85
  }
69
86
  return 0;
70
87
  }
71
88
 
89
+ case 'shell': {
90
+ return await runShell(rest);
91
+ }
92
+
72
93
  case 'titlebar': {
73
94
  const mode = rest[0];
74
95
  if (!['native', 'hidden', 'frameless'].includes(mode)) {
@@ -127,6 +148,49 @@ function coerce(value) {
127
148
  return value;
128
149
  }
129
150
 
151
+ async function runShell(rest) {
152
+ const { listLocal, downloadShell, buildShell } = require('./shell');
153
+ const { ALL_PLATFORMS } = require('./utils');
154
+ const sub = rest[0];
155
+ switch (sub) {
156
+ case undefined:
157
+ case 'list': {
158
+ const ready = listLocal();
159
+ if (ready.length === 0) {
160
+ console.log('[freedom] 本地暂无预编译壳。');
161
+ } else {
162
+ console.log('[freedom] 本地已就绪的壳平台:');
163
+ for (const p of ready) console.log(` ${p}`);
164
+ }
165
+ console.log(`[freedom] 可选平台:${ALL_PLATFORMS.join(' / ')}`);
166
+ return 0;
167
+ }
168
+ case 'download': {
169
+ const plat = rest[1];
170
+ if (!plat) {
171
+ console.error('[freedom] 用法:freedom shell download <win-x64|darwin-x64|darwin-arm64|linux-x64|linux-arm64>');
172
+ return 1;
173
+ }
174
+ const dest = await downloadShell(plat);
175
+ console.log(`[freedom] 已下载 ${plat} 壳:${dest}`);
176
+ return 0;
177
+ }
178
+ case 'build': {
179
+ const plat = rest[1];
180
+ if (!plat) {
181
+ console.error('[freedom] 用法:freedom shell build <win-x64|darwin-x64|darwin-arm64|linux-x64|linux-arm64>');
182
+ return 1;
183
+ }
184
+ const dest = buildShell(plat);
185
+ console.log(`[freedom] 已编译 ${plat} 壳:${dest}`);
186
+ return 0;
187
+ }
188
+ default:
189
+ console.error(`[freedom] 未知 shell 子命令:${sub}`);
190
+ return 1;
191
+ }
192
+ }
193
+
130
194
  function openBrowser(file) {
131
195
  const { spawn } = require('child_process');
132
196
  const plat = process.platform;