@yufengtadian/freedom-cli 1.12.12 → 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.
- package/README.md +8 -1
- package/bin/freedom.js +5 -1
- package/lib/build.js +49 -8
- package/lib/cli.js +106 -79
- package/lib/config.js +0 -7
- package/lib/shell.js +73 -20
- package/lib/theme.js +105 -0
- package/lib/tui.js +20 -2
- package/lib/update.js +109 -0
- package/lib/utils.js +14 -1
- package/package.json +1 -1
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/pkg/freedom/backend_proc.go +4 -4
- package/templates/go/pkg/freedom/configfile.go +12 -5
- package/templates/go/pkg/freedom/freedom.go +11 -1
- 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 +8 -0
- 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,14 @@
|
|
|
1
1
|
# freedom-cli
|
|
2
2
|
|
|
3
|
-
Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.13
|
|
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.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
|
+
|
|
7
12
|
**v1.12.12 三平台 frameless 彻底修复**:macOS / Linux 无边框窗口不再残留原生标题栏——
|
|
8
13
|
- 壳层向 webview_go 新增 `set_decorated` / `window_control` 原生窗口控制 API(GTK 走 `gtk_window_set_decorated` / `gtk_window_*`,Cocoa 走隐藏标题栏 + `performMiniaturize:` / `zoom:` / `performClose:` / `isZoomed`),mac/linux 的 frameless 从"空实现回退原生标题栏"修复为真无边框,UI 上方不再残留未清理的原生标题栏;
|
|
9
14
|
- 自绘三按钮(最小化 / 最大化 / 关闭)在 macOS / Linux 上接入原生窗口控制,双击最大化 / 还原、`isMaximized` 状态查询真实可用(此前 mac/linux 窗口控制为静默空转、`isMaximized` 恒 false);
|
|
@@ -171,6 +176,8 @@ freedom icon <path> # 设置应用图标(Windows 用 .ico
|
|
|
171
176
|
freedom config [get|set]
|
|
172
177
|
freedom shell list|download <platform>|build <platform>
|
|
173
178
|
freedom dmg [--platform <plat>] # 在 macOS 上把 .app 打包为 .dmg
|
|
179
|
+
freedom version # 显示版本并检测最新版本
|
|
180
|
+
freedom update # 检查新版本并给出升级命令(同 check-update)
|
|
174
181
|
freedom tutorial
|
|
175
182
|
freedom help
|
|
176
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
|
|
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 进程)
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -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(`[
|
|
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
137
|
if (!['native', 'frameless'].includes(mode)) {
|
|
111
|
-
console.error('
|
|
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(
|
|
116
|
-
console.log('
|
|
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('
|
|
124
|
-
console.error('
|
|
125
|
-
console.error('
|
|
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(
|
|
130
|
-
console.log('
|
|
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('
|
|
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(
|
|
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(
|
|
186
|
+
console.log(`${ok('教程已打开:')}${paint(file, C.fg.cyan)}`);
|
|
160
187
|
return 0;
|
|
161
188
|
}
|
|
162
189
|
|
|
163
190
|
default:
|
|
164
|
-
console.error(
|
|
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('
|
|
216
|
+
console.log(`${dim('本地暂无预编译壳。')}`);
|
|
190
217
|
} else {
|
|
191
|
-
console.log('
|
|
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(
|
|
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('
|
|
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(
|
|
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('
|
|
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(
|
|
215
|
-
console.log('
|
|
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(
|
|
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('
|
|
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(
|
|
248
|
-
console.error('
|
|
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(
|
|
282
|
+
console.log(`${ok('已生成 dmg:')}${paint(dmgPath, C.fg.white)}`);
|
|
256
283
|
return 0;
|
|
257
284
|
} catch (e) {
|
|
258
|
-
console.error(
|
|
285
|
+
console.error(`${err(e.message)}`);
|
|
259
286
|
return 1;
|
|
260
287
|
}
|
|
261
288
|
}
|
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) => {
|
package/lib/shell.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
ALL_PLATFORMS,
|
|
23
23
|
SHELL_EXE_NAME,
|
|
24
24
|
localShellPath,
|
|
25
|
+
nativePlatform,
|
|
25
26
|
} = require('./utils');
|
|
26
27
|
|
|
27
28
|
// GitHub Releases 下载源(可用环境变量覆盖)。
|
|
@@ -49,27 +50,63 @@ function releaseUrl(plat) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
// ---- 壳二进制平台格式校验 ----
|
|
52
|
-
//
|
|
53
|
+
// 读取二进制头识别真实平台(含架构),防止"用 Windows 壳冒充 mac/linux 壳"这类假壳
|
|
53
54
|
// 被静默分发(历史缺陷:shell/<darwin-*>/<linux-*> 曾误填 Windows PE 副本)。
|
|
55
|
+
// 返回精确平台 key(win-x64 / win-arm64 / mac-x64 / mac-arm64 / linux-x64 / linux-arm64)、
|
|
56
|
+
// 仅格式族(win / mac / linux,老壳 / 未知架构)或 'unknown'。
|
|
54
57
|
function detectShellFormat(buf) {
|
|
55
|
-
if (!buf || buf.length <
|
|
56
|
-
// Windows PE:MZ
|
|
57
|
-
if (buf[0] === 0x4d && buf[1] === 0x5a)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
if (!buf || buf.length < 64) return 'unknown';
|
|
59
|
+
// Windows PE:MZ;e_lfanew @0x3C (4 LE) -> "PE\0\0",machine @+4 (2 LE)
|
|
60
|
+
if (buf[0] === 0x4d && buf[1] === 0x5a) {
|
|
61
|
+
const peOff = buf.readUInt32LE(0x3c);
|
|
62
|
+
if (peOff + 6 <= buf.length && buf.readUInt32LE(peOff) === 0x00004550) {
|
|
63
|
+
const machine = buf.readUInt16LE(peOff + 4);
|
|
64
|
+
if (machine === 0x8664) return 'win-x64';
|
|
65
|
+
if (machine === 0xaa64) return 'win-arm64';
|
|
66
|
+
}
|
|
67
|
+
return 'win';
|
|
68
|
+
}
|
|
69
|
+
// Mach-O 64 位:CF FA ED FE(magic 0xfeedfacf 小端);cputype @4 (4 LE)
|
|
70
|
+
if (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) {
|
|
71
|
+
const cpu = buf.readUInt32LE(4);
|
|
72
|
+
if (cpu === 0x01000007) return 'mac-x64';
|
|
73
|
+
if (cpu === 0x0100000c) return 'mac-arm64';
|
|
74
|
+
return 'mac';
|
|
75
|
+
}
|
|
76
|
+
// ELF:7F 45 4C 46;e_machine @18 (2 LE):62=x86_64,183=aarch64
|
|
77
|
+
if (buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46) {
|
|
78
|
+
const machine = buf.readUInt16LE(18);
|
|
79
|
+
if (machine === 62) return 'linux-x64';
|
|
80
|
+
if (machine === 183) return 'linux-arm64';
|
|
81
|
+
return 'linux';
|
|
82
|
+
}
|
|
62
83
|
return 'unknown';
|
|
63
84
|
}
|
|
64
85
|
|
|
65
|
-
// 平台 key ->
|
|
86
|
+
// 平台 key -> 期望的二进制平台 key(含架构)
|
|
66
87
|
function expectedFormat(plat) {
|
|
67
|
-
if (plat.startsWith('win'))
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
if (plat.startsWith('win')) {
|
|
89
|
+
return plat === 'win-x64' || plat === 'win-arm64' ? plat : 'win';
|
|
90
|
+
}
|
|
91
|
+
if (plat.startsWith('darwin')) {
|
|
92
|
+
if (plat === 'darwin-x64') return 'mac-x64';
|
|
93
|
+
if (plat === 'darwin-arm64') return 'mac-arm64';
|
|
94
|
+
return 'mac';
|
|
95
|
+
}
|
|
96
|
+
if (plat.startsWith('linux')) {
|
|
97
|
+
return plat === 'linux-x64' || plat === 'linux-arm64' ? plat : 'linux';
|
|
98
|
+
}
|
|
70
99
|
return 'unknown';
|
|
71
100
|
}
|
|
72
101
|
|
|
102
|
+
// 格式族(win / mac / linux)
|
|
103
|
+
function formatFamily(s) {
|
|
104
|
+
if (s.startsWith('win')) return 'win';
|
|
105
|
+
if (s.startsWith('mac')) return 'mac';
|
|
106
|
+
if (s.startsWith('linux')) return 'linux';
|
|
107
|
+
return s;
|
|
108
|
+
}
|
|
109
|
+
|
|
73
110
|
// 校验本地壳二进制是否为目标平台的真实格式;不匹配返回错误说明,不抛错(供调用方决策)。
|
|
74
111
|
function validateLocalShell(plat) {
|
|
75
112
|
const p = localShellPath(plat);
|
|
@@ -87,12 +124,21 @@ function validateLocalShell(plat) {
|
|
|
87
124
|
if (fmt === 'unknown') {
|
|
88
125
|
return `壳二进制 ${p} 不是可识别的 PE/Mach-O/ELF 格式(detect=${fmt})。`;
|
|
89
126
|
}
|
|
90
|
-
if (fmt !== want) {
|
|
127
|
+
if (formatFamily(fmt) !== formatFamily(want)) {
|
|
91
128
|
return `壳二进制 ${p} 格式与目标平台 ${plat} 不匹配:期望 ${want},实际 ${fmt}。` +
|
|
92
129
|
`这是假壳(历史缺陷曾把 Windows 壳复制到 mac/linux 平台)。` +
|
|
93
130
|
`请运行 freedom shell build ${plat} 在 ${plat} 本机编译真实壳,` +
|
|
94
131
|
`或 freedom shell download ${plat} 拉取 CI 预编译产物。`;
|
|
95
132
|
}
|
|
133
|
+
if (fmt !== want) {
|
|
134
|
+
if (!fmt.includes('-')) {
|
|
135
|
+
// 老壳仅能识别格式族、无架构信息:降级通过并提示(历史二进制)
|
|
136
|
+
console.warn(`[freedom] 警告:壳二进制 ${p} 仅识别为 ${formatFamily(fmt)} 格式(架构未知,detect=${fmt}),按目标 ${plat} 使用。`);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return `壳二进制 ${p} 架构与目标平台 ${plat} 不匹配:期望 ${want},实际 ${fmt}(如 linux-x64 与 linux-arm64 不能混用)。` +
|
|
140
|
+
`请运行 freedom shell download ${plat} 拉取正确的预编译壳。`;
|
|
141
|
+
}
|
|
96
142
|
return null;
|
|
97
143
|
}
|
|
98
144
|
|
|
@@ -145,6 +191,16 @@ async function downloadShell(plat) {
|
|
|
145
191
|
);
|
|
146
192
|
}
|
|
147
193
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
194
|
+
// B13:下载后、落盘前校验格式/架构,防止代理劫持返回错误页或假壳被静默分发。
|
|
195
|
+
const fmt = detectShellFormat(buf);
|
|
196
|
+
const want = expectedFormat(plat);
|
|
197
|
+
if (fmt === 'unknown' || formatFamily(fmt) !== formatFamily(want)) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`下载的壳格式异常:期望 ${want}(${plat}),实际 detect=${fmt}。` +
|
|
200
|
+
`下载地址 ${url} 可能返回了错误页或非本平台假壳,请检查 GitHub Release 资产 ` +
|
|
201
|
+
`${releaseRepo()} 的 ${releaseTag()}。已放弃本次写入。`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
148
204
|
fs.writeFileSync(dest, buf);
|
|
149
205
|
if (process.platform !== 'win32') {
|
|
150
206
|
fs.chmodSync(dest, 0o755);
|
|
@@ -166,7 +222,8 @@ function buildShell(plat) {
|
|
|
166
222
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
167
223
|
|
|
168
224
|
// 本机只能编译本机平台(webview_go 依赖系统 WebView 框架,无法交叉编译)。
|
|
169
|
-
|
|
225
|
+
// 统一走 utils.nativePlatform:Intel Mac 会明确抛"已不支持",避免两套映射语义不一(B44)。
|
|
226
|
+
const native = nativePlatform();
|
|
170
227
|
if (plat !== native) {
|
|
171
228
|
throw new Error(
|
|
172
229
|
`无法在本机(${native})交叉编译 ${plat}:webview_go 依赖系统 WebView 框架。` +
|
|
@@ -188,12 +245,8 @@ function buildShell(plat) {
|
|
|
188
245
|
}
|
|
189
246
|
|
|
190
247
|
function nativePlatformKey() {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (plat === 'win32') return 'win-x64';
|
|
194
|
-
if (plat === 'darwin') return arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
|
|
195
|
-
if (plat === 'linux') return arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
|
|
196
|
-
return `${plat}-${arch}`;
|
|
248
|
+
// 已废弃:统一使用 utils.nativePlatform(B44),本函数保留仅作内部兜底,勿再调用。
|
|
249
|
+
return nativePlatform();
|
|
197
250
|
}
|
|
198
251
|
|
|
199
252
|
module.exports = {
|
package/lib/theme.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Claude Code 风格终端 UI 主题:零依赖 ANSI 渲染
|
|
4
|
+
// 非 TTY(管道 / 重定向)或 NO_COLOR 环境变量时自动降级为纯文本,不影响脚本输出解析。
|
|
5
|
+
|
|
6
|
+
const ESC = '\x1b';
|
|
7
|
+
|
|
8
|
+
const C = {
|
|
9
|
+
reset: `${ESC}[0m`,
|
|
10
|
+
bold: `${ESC}[1m`,
|
|
11
|
+
dim: `${ESC}[2m`,
|
|
12
|
+
italic: `${ESC}[3m`,
|
|
13
|
+
underline: `${ESC}[4m`,
|
|
14
|
+
fg: {
|
|
15
|
+
black: `${ESC}[30m`,
|
|
16
|
+
red: `${ESC}[31m`,
|
|
17
|
+
green: `${ESC}[32m`,
|
|
18
|
+
yellow: `${ESC}[33m`,
|
|
19
|
+
blue: `${ESC}[34m`,
|
|
20
|
+
magenta: `${ESC}[35m`,
|
|
21
|
+
cyan: `${ESC}[36m`,
|
|
22
|
+
white: `${ESC}[37m`,
|
|
23
|
+
gray: `${ESC}[90m`,
|
|
24
|
+
},
|
|
25
|
+
bg: {
|
|
26
|
+
red: `${ESC}[41m`,
|
|
27
|
+
green: `${ESC}[42m`,
|
|
28
|
+
yellow: `${ESC}[43m`,
|
|
29
|
+
magenta: `${ESC}[45m`,
|
|
30
|
+
cyan: `${ESC}[46m`,
|
|
31
|
+
gray: `${ESC}[100m`,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const COLOR = Boolean(process.stdout.isTTY && process.env.NO_COLOR === undefined);
|
|
36
|
+
|
|
37
|
+
function paint(text, ...codes) {
|
|
38
|
+
if (!COLOR) return text;
|
|
39
|
+
return codes.join('') + text + C.reset;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stripAnsi(s) {
|
|
43
|
+
return String(s).replace(/\x1b\[[0-9;]*m/g, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 符号徽章(Claude Code 风格):✓ / ✗ / ⚠ / ℹ / ➜
|
|
47
|
+
function ok(text) { return paint('✓ ', C.fg.green, C.bold) + text; }
|
|
48
|
+
function err(text) { return paint('✗ ', C.fg.red, C.bold) + text; }
|
|
49
|
+
function warn(text) { return paint('⚠ ', C.fg.yellow, C.bold) + text; }
|
|
50
|
+
function info(text) { return paint('ℹ ', C.fg.cyan, C.bold) + text; }
|
|
51
|
+
function tip(text) { return paint('➜ ', C.fg.magenta, C.bold) + text; }
|
|
52
|
+
function dim(text) { return paint(text, C.fg.gray); }
|
|
53
|
+
function bold(text, color) { return paint(text, C.bold, color ? C.fg[color] : ''); }
|
|
54
|
+
|
|
55
|
+
// 行内标签:[freedom] 加粗
|
|
56
|
+
function tag() { return paint('[freedom]', C.fg.blue, C.bold); }
|
|
57
|
+
|
|
58
|
+
// 水平分隔线
|
|
59
|
+
function rule(ch = '─', color = 'gray') {
|
|
60
|
+
const w = Math.max(8, (process.stdout.columns || 80) - 2);
|
|
61
|
+
return paint(ch.repeat(w), C.fg[color]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 帮助分组标题:── 分组名 ──
|
|
65
|
+
function section(title) {
|
|
66
|
+
return ` ${paint('──', C.fg.gray)} ${paint(title, C.bold, C.fg.cyan)} ${paint('──', C.fg.gray)}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 品牌横幅(ASCII 标识 + 版本徽章)
|
|
70
|
+
function banner(version, extra) {
|
|
71
|
+
const lines = [
|
|
72
|
+
'',
|
|
73
|
+
` ${paint('▚▚', C.fg.cyan, C.bold)} ${paint('F R E E D O M', C.bold, C.fg.white)} ${paint(`v${version}`, C.fg.gray)}`,
|
|
74
|
+
` ${paint('│', C.fg.cyan)} Freedom 桌面壳打包工具 · Web 前端一键出三平台桌面应用`,
|
|
75
|
+
];
|
|
76
|
+
if (extra) lines.push(` ${paint('│', C.fg.cyan)} ${extra}`);
|
|
77
|
+
lines.push('');
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 版本信息卡
|
|
82
|
+
function versionCard(current, latest, hasUpdate) {
|
|
83
|
+
const lines = [];
|
|
84
|
+
lines.push('');
|
|
85
|
+
lines.push(` ${paint('⚡', C.fg.magenta)} ${paint('freedom', C.bold)} ${paint(`v${current}`, C.fg.white, C.bold)}`);
|
|
86
|
+
lines.push(rule('─'));
|
|
87
|
+
if (hasUpdate && latest) {
|
|
88
|
+
lines.push(
|
|
89
|
+
` ${paint('➜ 当前版本:', C.fg.gray)}${paint(current, C.fg.white, C.bold)}` +
|
|
90
|
+
` ${paint('➜ 最新版本:', C.fg.gray)}${paint(latest, C.fg.green, C.bold)} ${paint('(有新版本可升级)', C.fg.yellow)}`
|
|
91
|
+
);
|
|
92
|
+
} else if (latest) {
|
|
93
|
+
lines.push(` ${paint('➜ 当前版本:', C.fg.gray)}${paint(current, C.fg.white, C.bold)} ${paint('(已是最新版本)', C.fg.green)}`);
|
|
94
|
+
} else {
|
|
95
|
+
lines.push(` ${paint('➜ 当前版本:', C.fg.gray)}${paint(current, C.fg.white, C.bold)} ${paint('(离线,未检测到最新版本)', C.fg.gray)}`);
|
|
96
|
+
}
|
|
97
|
+
lines.push('');
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
C, COLOR, paint, stripAnsi,
|
|
103
|
+
ok, err, warn, info, tip, dim, bold, tag,
|
|
104
|
+
rule, section, banner, versionCard,
|
|
105
|
+
};
|
package/lib/tui.js
CHANGED
|
@@ -296,6 +296,23 @@ async function tutorialFlow(tui) {
|
|
|
296
296
|
await tui.message('教程', [{ text: C.fgGreen + ` 已打开:${file}` + C.reset }]);
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
async function updateFlow(tui) {
|
|
300
|
+
const { checkUpdate, PKG_NAME } = require('./update');
|
|
301
|
+
const r = await checkUpdate({ force: true });
|
|
302
|
+
const lines = [
|
|
303
|
+
{ text: C.fgWhite + C.bold + ` 当前版本:v${r.current}` + C.reset },
|
|
304
|
+
];
|
|
305
|
+
if (r.latest) {
|
|
306
|
+
lines.push({ text: C.fgCyan + ` 最新版本:v${r.latest}` + C.reset });
|
|
307
|
+
lines.push({ text: r.hasUpdate
|
|
308
|
+
? C.fgYellow + ` 发现新版本,可执行:npm install -g ${PKG_NAME}@latest` + C.reset
|
|
309
|
+
: C.fgGreen + ` 已是最新版本` + C.reset });
|
|
310
|
+
} else {
|
|
311
|
+
lines.push({ text: C.fgGray + ` 检查失败:网络不可用,请稍后重试` + C.reset });
|
|
312
|
+
}
|
|
313
|
+
await tui.message('检查版本更新', lines);
|
|
314
|
+
}
|
|
315
|
+
|
|
299
316
|
function coerce(value) {
|
|
300
317
|
if (value === 'true') return true;
|
|
301
318
|
if (value === 'false') return false;
|
|
@@ -313,7 +330,7 @@ async function tui(cwd) {
|
|
|
313
330
|
app.enter();
|
|
314
331
|
let keep = true;
|
|
315
332
|
while (keep) {
|
|
316
|
-
const items = ['打包桌面应用', '新建项目', '修改配置', '壳管理', '打开教程', '退出'];
|
|
333
|
+
const items = ['打包桌面应用', '新建项目', '修改配置', '壳管理', '打开教程', '检查更新', '退出'];
|
|
317
334
|
const idx = await app.menu('主菜单', items, {
|
|
318
335
|
footer: `工作目录:${cwd} ↑ ↓ 选择 · Enter 确认 · q 退出`,
|
|
319
336
|
});
|
|
@@ -324,7 +341,8 @@ async function tui(cwd) {
|
|
|
324
341
|
case 2: await configFlow(app, cwd); break;
|
|
325
342
|
case 3: await shellFlow(app); break;
|
|
326
343
|
case 4: await tutorialFlow(app); break;
|
|
327
|
-
case 5:
|
|
344
|
+
case 5: await updateFlow(app); break;
|
|
345
|
+
case 6: keep = false; break;
|
|
328
346
|
}
|
|
329
347
|
}
|
|
330
348
|
app.exit();
|
package/lib/update.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 版本检测:零依赖(https 内置),查询 npm registry 最新版本并对比本地版本。
|
|
4
|
+
// - 结果缓存到 ~/.freedom/update-cache.json,24h 内不重复联网(离线不打扰)
|
|
5
|
+
// - compareVersions 手写 semver 比较(仅处理 x.y.z 数字前缀,满足语义版本场景)
|
|
6
|
+
// - 所有联网失败均静默降级,绝不阻塞主流程
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const https = require('https');
|
|
12
|
+
const { packageRoot } = require('./utils');
|
|
13
|
+
|
|
14
|
+
const PKG_NAME = '@yufengtadian/freedom-cli';
|
|
15
|
+
const REGISTRY = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}/latest`;
|
|
16
|
+
const CACHE_FILE = path.join(os.homedir(), '.freedom', 'update-cache.json');
|
|
17
|
+
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 小时
|
|
18
|
+
const REQUEST_TIMEOUT = 4000;
|
|
19
|
+
|
|
20
|
+
function currentVersion() {
|
|
21
|
+
return require(path.join(packageRoot(), 'package.json')).version;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// semver 简单比较:返回 1 / -1 / 0
|
|
25
|
+
function compareVersions(a, b) {
|
|
26
|
+
const pa = String(a || '').replace(/[^\d.]/g, '').split('.').map((n) => parseInt(n, 10) || 0);
|
|
27
|
+
const pb = String(b || '').replace(/[^\d.]/g, '').split('.').map((n) => parseInt(n, 10) || 0);
|
|
28
|
+
for (let i = 0; i < 3; i += 1) {
|
|
29
|
+
const x = pa[i] || 0;
|
|
30
|
+
const y = pb[i] || 0;
|
|
31
|
+
if (x > y) return 1;
|
|
32
|
+
if (x < y) return -1;
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readCache() {
|
|
38
|
+
try {
|
|
39
|
+
const j = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
|
40
|
+
if (j && j.latest && Date.now() - j.ts < CACHE_TTL) return j;
|
|
41
|
+
} catch (e) { /* 无缓存或损坏,忽略 */ }
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeCache(data) {
|
|
46
|
+
try {
|
|
47
|
+
fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
|
|
48
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify({ ...data, ts: Date.now() }));
|
|
49
|
+
} catch (e) { /* 写缓存失败静默 */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 从 npm registry 拉取 latest 版本;失败 / 超时返回 null
|
|
53
|
+
function fetchLatest(timeout = REQUEST_TIMEOUT) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const req = https.get(REGISTRY, {
|
|
56
|
+
headers: { 'user-agent': 'freedom-cli', accept: 'application/json' },
|
|
57
|
+
timeout,
|
|
58
|
+
}, (res) => {
|
|
59
|
+
if (res.statusCode !== 200) {
|
|
60
|
+
res.resume();
|
|
61
|
+
return resolve(null);
|
|
62
|
+
}
|
|
63
|
+
let body = '';
|
|
64
|
+
res.setEncoding('utf8');
|
|
65
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
66
|
+
res.on('end', () => {
|
|
67
|
+
try {
|
|
68
|
+
resolve(JSON.parse(body).version || null);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
resolve(null);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
req.on('timeout', () => req.destroy());
|
|
75
|
+
req.on('error', () => resolve(null));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 检查更新:force=true 强制联网(忽略缓存);否则优先读缓存
|
|
80
|
+
async function checkUpdate({ force = false } = {}) {
|
|
81
|
+
const current = currentVersion();
|
|
82
|
+
const cache = force ? null : readCache();
|
|
83
|
+
let latest = cache ? cache.latest : null;
|
|
84
|
+
if (!latest) {
|
|
85
|
+
latest = await fetchLatest();
|
|
86
|
+
if (latest) writeCache({ latest });
|
|
87
|
+
}
|
|
88
|
+
const hasUpdate = Boolean(latest && compareVersions(latest, current) > 0);
|
|
89
|
+
return { current, latest, hasUpdate };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 静默异步通知:仅在检测到新版本时打印一行升级提示,不阻塞调用方
|
|
93
|
+
async function maybeNotifyUpdate() {
|
|
94
|
+
try {
|
|
95
|
+
const r = await checkUpdate();
|
|
96
|
+
if (r.hasUpdate && r.latest) {
|
|
97
|
+
const theme = require('./theme');
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(` ${theme.paint('➜', theme.C.fg.magenta, theme.C.bold)} ${theme.paint(`新版本可用 ${r.latest}(当前 ${r.current})`, theme.C.fg.yellow)}`);
|
|
100
|
+
console.log(` ${theme.dim('运行')} ${theme.paint(`npm install -g ${PKG_NAME}@latest`, theme.C.fg.cyan)} ${theme.dim('升级,或')} ${theme.paint('freedom update', theme.C.fg.cyan)} ${theme.dim('查看详情')}`);
|
|
101
|
+
console.log('');
|
|
102
|
+
}
|
|
103
|
+
} catch (e) { /* 检测失败静默 */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
PKG_NAME, REGISTRY, CACHE_FILE, CACHE_TTL,
|
|
108
|
+
currentVersion, compareVersions, checkUpdate, maybeNotifyUpdate,
|
|
109
|
+
};
|
package/lib/utils.js
CHANGED
|
@@ -28,8 +28,14 @@ function shellDir() {
|
|
|
28
28
|
// 平台矩阵:key -> { exe 文件名(壳二进制名) }
|
|
29
29
|
// 通用壳为三平台预编译二进制,应用内容通过 exe 同目录 resources/ 外部加载,
|
|
30
30
|
// 因此同一壳可复用于任意应用,打包时无需任何语言工具链。
|
|
31
|
+
// 全部平台 key(含暂无预编译资产来源的 linux-arm64,仅支持显式指定 / 本地编译)。
|
|
31
32
|
const ALL_PLATFORMS = ['win-x64', 'darwin-arm64', 'linux-x64', 'linux-arm64'];
|
|
32
33
|
|
|
34
|
+
// 可分发平台(有预编译壳资产,CI 已产出):--platform all 的取值集合。
|
|
35
|
+
// linux-arm64 无 CI runner 与 GitHub 资产,若列入 all 会在 build 时 404 拖垮整个
|
|
36
|
+
// 全量构建(历史 bug B41);故 all 仅包含以下三平台,linux-arm64 需显式指定。
|
|
37
|
+
const DIST_PLATFORMS = ['win-x64', 'darwin-arm64', 'linux-x64'];
|
|
38
|
+
|
|
33
39
|
const SHELL_EXE_NAME = {
|
|
34
40
|
'win-x64': 'freedom-shell.exe',
|
|
35
41
|
'darwin-arm64': 'freedom-shell',
|
|
@@ -55,7 +61,13 @@ function platformExeName(plat, appName) {
|
|
|
55
61
|
function nativePlatform() {
|
|
56
62
|
const plat = process.platform;
|
|
57
63
|
const arch = process.arch;
|
|
58
|
-
if (plat === 'win32')
|
|
64
|
+
if (plat === 'win32') {
|
|
65
|
+
if (arch !== 'x64') {
|
|
66
|
+
// ARM64 Windows 没有独立的 win-arm64 壳,x64 壳经系统仿真可运行;给出提示而非静默。
|
|
67
|
+
console.error(`[freedom] 注意:当前为 ${arch} 架构 Windows,将使用 win-x64 壳(x64 仿真运行)。`);
|
|
68
|
+
}
|
|
69
|
+
return 'win-x64';
|
|
70
|
+
}
|
|
59
71
|
if (plat === 'darwin') {
|
|
60
72
|
if (arch !== 'arm64') {
|
|
61
73
|
throw new Error('已不支持 Intel Mac(darwin-x64):请改用 Apple Silicon Mac 构建 darwin-arm64,或直接使用 darwin-arm64 产物。');
|
|
@@ -113,6 +125,7 @@ module.exports = {
|
|
|
113
125
|
goTemplateDir,
|
|
114
126
|
shellDir,
|
|
115
127
|
ALL_PLATFORMS,
|
|
128
|
+
DIST_PLATFORMS,
|
|
116
129
|
SHELL_EXE_NAME,
|
|
117
130
|
isWinPlat,
|
|
118
131
|
isMacPlat,
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -134,12 +134,12 @@ func (p *ProcBackend) Handle(method string, params []json.RawMessage) (interface
|
|
|
134
134
|
id := p.nextID
|
|
135
135
|
ch := make(chan procResp, 1)
|
|
136
136
|
p.pending[id] = ch
|
|
137
|
-
|
|
138
|
-
line, _ := json.Marshal(msg)
|
|
139
|
-
_, err = p.stdin.Write(append(line, '\n'))
|
|
137
|
+
w := p.stdin // 锁内取引用,锁外写,避免持锁阻塞(后端不消费 stdin 时 Write 可能挂起)
|
|
140
138
|
p.mu.Unlock()
|
|
141
139
|
|
|
142
|
-
|
|
140
|
+
msg := procMessage{ID: id, Method: method, Params: paramsJSON}
|
|
141
|
+
line, _ := json.Marshal(msg)
|
|
142
|
+
if _, err := w.Write(append(line, '\n')); err != nil {
|
|
143
143
|
p.cancel(id, fmt.Errorf("freedom: proc backend write: %w", err))
|
|
144
144
|
return nil, err
|
|
145
145
|
}
|
|
@@ -12,6 +12,8 @@ package freedom
|
|
|
12
12
|
|
|
13
13
|
import (
|
|
14
14
|
"encoding/json"
|
|
15
|
+
"errors"
|
|
16
|
+
"fmt"
|
|
15
17
|
"os"
|
|
16
18
|
"path/filepath"
|
|
17
19
|
)
|
|
@@ -47,19 +49,24 @@ func resourcesDir() (string, error) {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
// loadRuntimeConfig 读取 exe 同目录 resources/config.json,将命中的字段覆盖到应用配置。
|
|
50
|
-
//
|
|
52
|
+
// 文件不存在时视为未配置(返回 nil,保持编译期/默认配置不变);
|
|
53
|
+
// 文件存在但读取/解析失败时返回具体错误,由调用方打印告警,避免用户手改配置出错时无感知。
|
|
51
54
|
func (a *App) loadRuntimeConfig() error {
|
|
52
55
|
dir, err := resourcesDir()
|
|
53
56
|
if err != nil {
|
|
54
|
-
return
|
|
57
|
+
return err
|
|
55
58
|
}
|
|
56
|
-
|
|
59
|
+
cfgPath := filepath.Join(dir, "config.json")
|
|
60
|
+
data, err := os.ReadFile(cfgPath)
|
|
57
61
|
if err != nil {
|
|
58
|
-
|
|
62
|
+
if errors.Is(err, os.ErrNotExist) {
|
|
63
|
+
return nil // 未配置:正常回退默认
|
|
64
|
+
}
|
|
65
|
+
return fmt.Errorf("read %s: %w", cfgPath, err)
|
|
59
66
|
}
|
|
60
67
|
var rc runtimeConfigFile
|
|
61
68
|
if err := json.Unmarshal(data, &rc); err != nil {
|
|
62
|
-
return
|
|
69
|
+
return fmt.Errorf("parse %s: %w", cfgPath, err)
|
|
63
70
|
}
|
|
64
71
|
if rc.Title != "" {
|
|
65
72
|
a.cfg.Title = rc.Title
|
|
@@ -122,7 +122,10 @@ func (a *App) Unbind(name string) {
|
|
|
122
122
|
func (a *App) Run() {
|
|
123
123
|
// 通用壳:先加载 exe 同目录 resources/config.json 覆盖窗口与后端配置
|
|
124
124
|
//(CLI build 时写入;缺失则使用编译期/默认配置)。
|
|
125
|
-
|
|
125
|
+
// 配置存在但非法时打印告警(不中断启动),避免用户手改配置出错时静默无感。
|
|
126
|
+
if err := a.loadRuntimeConfig(); err != nil {
|
|
127
|
+
fmt.Printf("freedom: warning: %v\n", err)
|
|
128
|
+
}
|
|
126
129
|
|
|
127
130
|
html, err := a.resolveHTML()
|
|
128
131
|
if err != nil {
|
|
@@ -209,6 +212,13 @@ func (a *App) bridge(method string, paramsJSON string) (result json.RawMessage,
|
|
|
209
212
|
}
|
|
210
213
|
}
|
|
211
214
|
|
|
215
|
+
// 框架内置方法特判:__freedom__ping 由 webview Bind 注册为全局函数
|
|
216
|
+
// window.__freedom__ping(),同时兼容经 freedom.call()/__freedom_bridge 路由的旧写法,
|
|
217
|
+
// 避免模板"测试桥接"自检报"method not bound"。
|
|
218
|
+
if method == "__freedom__ping" {
|
|
219
|
+
return json.RawMessage(`"pong"`), nil
|
|
220
|
+
}
|
|
221
|
+
|
|
212
222
|
raw, err := a.backend.Handle(method, params)
|
|
213
223
|
if err != nil {
|
|
214
224
|
return nil, err
|
|
@@ -57,8 +57,12 @@ func windowControl(a *App, action string) (interface{}, error) {
|
|
|
57
57
|
// macOS / Linux 暂不提供 exe 内嵌图标提取,前端隐藏标题栏图标。
|
|
58
58
|
return "", nil
|
|
59
59
|
case "startDrag":
|
|
60
|
-
//
|
|
61
|
-
//
|
|
60
|
+
// 无边框窗口拖动:GTK 走 gtk_window_begin_move_drag(root_x/y=-1 用当前指针),
|
|
61
|
+
// Cocoa 走 performWindowDragWithEvent:(用当前鼠标位置合成事件)。
|
|
62
|
+
// 与 Windows 的 WM_NCLBUTTONDOWN+HTCAPTION 语义等价,保留页面双击/右键事件。
|
|
63
|
+
if a.view.BeginMoveDrag() != 0 {
|
|
64
|
+
return nil, fmt.Errorf("window drag is not supported on this platform")
|
|
65
|
+
}
|
|
62
66
|
return nil, nil
|
|
63
67
|
default:
|
|
64
68
|
return nil, fmt.Errorf("unknown window action %q", action)
|
|
@@ -293,6 +293,7 @@ typedef enum webview_window_action {
|
|
|
293
293
|
* @param action The action to execute (see webview_window_action_t).
|
|
294
294
|
*/
|
|
295
295
|
WEBVIEW_API int webview_window_control(webview_t w, webview_window_action_t action);
|
|
296
|
+
WEBVIEW_API int webview_window_begin_move_drag(webview_t w);
|
|
296
297
|
|
|
297
298
|
/**
|
|
298
299
|
* Navigates webview to the given URL. URL may be a properly encoded data URI.
|
|
@@ -1041,6 +1042,7 @@ if (status === 0) {\
|
|
|
1041
1042
|
// 供前端自绘标题栏三按钮使用。action 取值见 webview_window_action_t。
|
|
1042
1043
|
// 返回 0 表示成功;-1 表示该平台/动作不支持;isMaximized 返回 1/0。
|
|
1043
1044
|
int window_control(int action) { return window_control_impl(action); }
|
|
1045
|
+
int begin_move_drag() { return begin_move_drag_impl(); }
|
|
1044
1046
|
|
|
1045
1047
|
void set_size(int width, int height, webview_hint_t hints) {
|
|
1046
1048
|
set_size_impl(width, height, hints);
|
|
@@ -1062,6 +1064,7 @@ protected:
|
|
|
1062
1064
|
virtual void set_size_impl(int width, int height, webview_hint_t hints) = 0;
|
|
1063
1065
|
virtual void set_decorated_impl(bool decorated) = 0;
|
|
1064
1066
|
virtual int window_control_impl(int action) = 0;
|
|
1067
|
+
virtual int begin_move_drag_impl() = 0;
|
|
1065
1068
|
virtual void set_html_impl(const std::string &html) = 0;
|
|
1066
1069
|
virtual void init_impl(const std::string &js) = 0;
|
|
1067
1070
|
virtual void eval_impl(const std::string &js) = 0;
|
|
@@ -1418,6 +1421,14 @@ public:
|
|
|
1418
1421
|
}
|
|
1419
1422
|
}
|
|
1420
1423
|
|
|
1424
|
+
// 无边框窗口拖动:root_x/root_y=-1 表示用当前指针位置,由窗口管理器接管拖动。
|
|
1425
|
+
// 与 Windows 的 WM_NCLBUTTONDOWN+HTCAPTION 语义等价,保留页面双击/右键事件。
|
|
1426
|
+
int begin_move_drag_impl() override {
|
|
1427
|
+
gtk_window_begin_move_drag(GTK_WINDOW(m_window), GDK_BUTTON_PRESS, -1, -1,
|
|
1428
|
+
gtk_get_current_event_time());
|
|
1429
|
+
return 0;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1421
1432
|
void set_size_impl(int width, int height, webview_hint_t hints) override {
|
|
1422
1433
|
gtk_window_set_resizable(GTK_WINDOW(m_window), hints != WEBVIEW_HINT_FIXED);
|
|
1423
1434
|
if (hints == WEBVIEW_HINT_NONE) {
|
|
@@ -1824,6 +1835,21 @@ public:
|
|
|
1824
1835
|
return -1;
|
|
1825
1836
|
}
|
|
1826
1837
|
}
|
|
1838
|
+
|
|
1839
|
+
// 无边框窗口拖动:用当前鼠标屏幕位置合成 NSLeftMouseDragged 事件,
|
|
1840
|
+
// 调 performWindowDragWithEvent: 进入 AppKit 原生窗口拖动循环。
|
|
1841
|
+
int begin_move_drag_impl() override {
|
|
1842
|
+
objc::autoreleasepool arp;
|
|
1843
|
+
auto loc = objc::msg_send<CGPoint>("NSEvent"_cls, "mouseLocation"_sel);
|
|
1844
|
+
auto wnum = objc::msg_send<NSInteger>(m_window, "windowNumber"_sel);
|
|
1845
|
+
auto evt = objc::msg_send<id>(
|
|
1846
|
+
"NSEvent"_cls,
|
|
1847
|
+
"mouseEventWithType:location:modifierFlags:timestamp:windowNumber:context:eventNumber:clickCount:pressure:"_sel,
|
|
1848
|
+
(NSUInteger)1 /*NSLeftMouseDragged*/, loc, (NSUInteger)0, (double)0.0,
|
|
1849
|
+
wnum, (id)nullptr, (NSInteger)0, (NSInteger)1, (float)1.0);
|
|
1850
|
+
objc::msg_send<void>(m_window, "performWindowDragWithEvent:"_sel, evt);
|
|
1851
|
+
return 0;
|
|
1852
|
+
}
|
|
1827
1853
|
void navigate_impl(const std::string &url) override {
|
|
1828
1854
|
objc::autoreleasepool arp;
|
|
1829
1855
|
|
|
@@ -3495,6 +3521,7 @@ public:
|
|
|
3495
3521
|
// Windows 窗口控制(min/max/close)由 freedom 壳层 window_windows.go
|
|
3496
3522
|
// 直接调 user32 处理,webview 层不参与。
|
|
3497
3523
|
int window_control_impl(int action) override { (void)action; return -1; }
|
|
3524
|
+
int begin_move_drag_impl() override { return -1; }
|
|
3498
3525
|
|
|
3499
3526
|
void set_size_impl(int width, int height, webview_hint_t hints) override {
|
|
3500
3527
|
auto style = GetWindowLong(m_window, GWL_STYLE);
|
|
@@ -3794,6 +3821,10 @@ WEBVIEW_API int webview_window_control(webview_t w, webview_window_action_t acti
|
|
|
3794
3821
|
static_cast<int>(action));
|
|
3795
3822
|
}
|
|
3796
3823
|
|
|
3824
|
+
WEBVIEW_API int webview_window_begin_move_drag(webview_t w) {
|
|
3825
|
+
return static_cast<webview::webview *>(w)->begin_move_drag();
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3797
3828
|
WEBVIEW_API void webview_navigate(webview_t w, const char *url) {
|
|
3798
3829
|
static_cast<webview::webview *>(w)->navigate(url);
|
|
3799
3830
|
}
|
|
@@ -118,6 +118,10 @@ type WebView interface {
|
|
|
118
118
|
// 供前端自绘标题栏三按钮调用。Windows 上由 freedom 壳层处理,此处返回 -1。
|
|
119
119
|
WindowControl(action WindowAction) int
|
|
120
120
|
|
|
121
|
+
// BeginMoveDrag 发起无边框窗口的原生拖动(macOS/Linux)。
|
|
122
|
+
// Windows 由 freedom 壳层用 WM_NCLBUTTONDOWN 处理,此处返回 -1。
|
|
123
|
+
BeginMoveDrag() int
|
|
124
|
+
|
|
121
125
|
// SetSize updates native window size. See Hint constants.
|
|
122
126
|
SetSize(w int, h int, hint Hint)
|
|
123
127
|
|
|
@@ -232,6 +236,10 @@ func (w *webview) WindowControl(action WindowAction) int {
|
|
|
232
236
|
return int(C.webview_window_control(w.w, C.webview_window_action_t(action)))
|
|
233
237
|
}
|
|
234
238
|
|
|
239
|
+
func (w *webview) BeginMoveDrag() int {
|
|
240
|
+
return int(C.webview_window_begin_move_drag(w.w))
|
|
241
|
+
}
|
|
242
|
+
|
|
235
243
|
func (w *webview) SetSize(width int, height int, hint Hint) {
|
|
236
244
|
C.webview_set_size(w.w, C.int(width), C.int(height), C.webview_hint_t(hint))
|
|
237
245
|
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
// 无边框模式:显示自绘标题栏并绑定按钮。
|
|
8
8
|
// 壳桥接(__freedom_window)注入时机与页面脚本加载先后不定,早期调用可能被 reject 吞掉导致标题栏不显示,
|
|
9
|
-
// 因此先轮询等待桥接就绪(最多
|
|
10
|
-
function waitForBridge(tries =
|
|
9
|
+
// 因此先轮询等待桥接就绪(最多 150 次 × 100ms = 15s)再初始化;超时后明确告警而非静默失效。
|
|
10
|
+
function waitForBridge(tries = 150, interval = 100) {
|
|
11
11
|
return new Promise((resolve) => {
|
|
12
12
|
const check = () => {
|
|
13
13
|
if (window.freedom && window.freedom.window && typeof window.__freedom_window === 'function') {
|
|
@@ -47,7 +47,11 @@ async function syncMaxState(w) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async function initFrameless() {
|
|
50
|
-
|
|
50
|
+
const ready = await waitForBridge();
|
|
51
|
+
if (!ready) {
|
|
52
|
+
console.warn('[freedom] 桥接 15s 未就绪,无边框标题栏未初始化(窗口仍可正常使用)。');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
51
55
|
try {
|
|
52
56
|
const frameless = await window.freedom.window.isFrameless();
|
|
53
57
|
if (!frameless) return;
|
|
@@ -148,11 +152,14 @@ function initTitlebarMenu() {
|
|
|
148
152
|
initTitlebarMenu();
|
|
149
153
|
initFrameless();
|
|
150
154
|
|
|
151
|
-
//
|
|
155
|
+
// 桥接自检:__freedom__ping 是壳注入的全局函数,优先直调;
|
|
156
|
+
// 低版本壳不存在时回退经 bridge 路由(壳层 bridge 对 ping 特判兼容)。
|
|
152
157
|
document.getElementById('pingBtn').addEventListener('click', async () => {
|
|
153
158
|
const el = document.getElementById('result');
|
|
154
159
|
try {
|
|
155
|
-
const r =
|
|
160
|
+
const r = (typeof window.__freedom__ping === 'function')
|
|
161
|
+
? await window.__freedom__ping()
|
|
162
|
+
: await window.freedom.call('__freedom__ping');
|
|
156
163
|
el.textContent = '桥接正常:' + r;
|
|
157
164
|
} catch (e) {
|
|
158
165
|
el.textContent = '桥接异常:' + e.message;
|
|
File without changes
|
package/templates/go/err.txt
DELETED
package/templates/go/err10.txt
DELETED
|
File without changes
|
package/templates/go/err11.txt
DELETED
|
File without changes
|
package/templates/go/err2.txt
DELETED
|
File without changes
|
package/templates/go/err3.txt
DELETED
|
File without changes
|
package/templates/go/err4.txt
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
github.com/webview/webview_go: build constraints exclude all Go files in D:\dev\自研desktop exe打包框架\freedom-cli\templates\go\webview_go
|
package/templates/go/err5.txt
DELETED
|
File without changes
|
package/templates/go/err6.txt
DELETED
|
File without changes
|
package/templates/go/err7.txt
DELETED
|
File without changes
|
package/templates/go/err8.txt
DELETED
|
File without changes
|
package/templates/go/err9.txt
DELETED
|
File without changes
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
module freedom-cli-shell
|
|
2
|
-
|
|
3
|
-
go 1.22
|
|
4
|
-
|
|
5
|
-
require github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
|
|
6
|
-
|
|
7
|
-
require golang.org/x/sys v0.28.0
|
|
8
|
-
|
|
9
|
-
// Freedom fork: 使用随包分发的本地 webview_go(标题栏图标与 exe 图标一致等定制),
|
|
10
|
-
// 避免拉取上游被覆盖。
|
|
11
|
-
replace github.com/webview/webview_go => ./webview_go
|
package/templates/go/tidy.txt
DELETED
|
File without changes
|
package/templates/go/vet_out.txt
DELETED
|
File without changes
|