@yufengtadian/freedom-cli 1.13.1 → 1.13.3
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/LICENSE +40 -0
- package/README.md +119 -15
- package/lib/agents.js +448 -0
- package/lib/build.js +189 -45
- package/lib/cli.js +258 -9
- package/lib/config.js +20 -1
- package/lib/desktop.js +133 -0
- package/lib/dev.js +149 -0
- package/lib/mcp.js +217 -0
- package/lib/release.js +90 -0
- package/lib/security.js +113 -54
- package/lib/shell.js +170 -63
- package/lib/toolchain.js +339 -0
- package/lib/tui.js +37 -1
- package/lib/utils.js +51 -0
- package/lib/verify.js +48 -12
- package/lib/webkit.js +45 -0
- package/package.json +3 -2
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/skill/freedom/SKILL.md +159 -0
- package/templates/desktop/app.html +559 -0
- package/templates/desktop/backend/desktop.mjs +327 -0
- package/templates/desktop/freedom.config.js +32 -0
- package/templates/desktop/icon.ico +0 -0
- package/templates/go/go.mod +3 -0
- package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
- package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +238 -11
- package/templates/go/pkg/freedom/assets/freedom.js +3 -0
- package/templates/go/pkg/freedom/assets_embed.go +1 -0
- package/templates/go/pkg/freedom/backend_proc.go +5 -5
- package/templates/go/pkg/freedom/events_windows.go +5 -5
- package/templates/go/pkg/freedom/freedom.go +64 -10
- package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
- package/templates/go/pkg/freedom/resources.go +119 -14
- package/templates/go/pkg/freedom/securetemp.go +61 -0
- package/templates/go/pkg/freedom/securetemp_other.go +20 -0
- package/templates/go/pkg/freedom/securetemp_windows.go +25 -0
- package/templates/go/pkg/freedom/security.go +293 -64
- package/templates/go/pkg/freedom/shutdown.go +55 -0
- package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
- package/templates/go/pkg/freedom/store.go +11 -3
- package/templates/go/pkg/freedom/syscap_windows.go +33 -33
- package/templates/go/pkg/freedom/sysint_windows.go +2 -2
- package/templates/go/pkg/freedom/tray_windows.go +18 -18
- package/templates/go/pkg/freedom/updater.go +3 -3
- package/templates/go/pkg/freedom/window_other.go +7 -1
- package/templates/go/pkg/freedom/window_windows.go +185 -17
- package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
- package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
- package/templates/go/third_party/webview_go/LICENSE +22 -0
- package/templates/go/third_party/webview_go/README.md +50 -0
- package/templates/go/third_party/webview_go/glue.c +36 -0
- package/templates/go/third_party/webview_go/go.mod +3 -0
- package/templates/go/third_party/webview_go/go.sum +0 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
- package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
- package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
- package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
- package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
- package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
- package/templates/go/third_party/webview_go/webview.cc +1 -0
- package/templates/go/third_party/webview_go/webview.go +332 -0
- package/templates/installer/app.nsi +43 -0
- package/templates/project/freedom.config.js +14 -0
- package/templates/project/freedom.d.ts +77 -0
- package/templates/project-minimal/freedom.config.js +14 -0
- package/templates/project-minimal/freedom.d.ts +77 -0
package/lib/toolchain.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 工具链自动装配扩展:探测 Go / Rust / C++ 是否就位,缺失时给出安装动作(--apply 才真跑),
|
|
4
|
+
// 并把镜像源等编译配置优化写进各工具自己的配置位。
|
|
5
|
+
//
|
|
6
|
+
// 「检测到配置好就不去检测」:探测要 spawn 子进程跑 --version,三平台冷启动累计几百毫秒,
|
|
7
|
+
// 故 ok 结论进缓存(按 PATH 指纹 + TTL 失效)。失败结论**永不入缓存**——刚装好的工具必须
|
|
8
|
+
// 当场被认出来,否则用户装完再跑一次会说「没装」,那是比慢得多的缺陷。
|
|
9
|
+
//
|
|
10
|
+
// 安装动作默认只打印不执行:这些命令要联网、要管理员权限、会改系统,属高危面(见 AGENTS.md
|
|
11
|
+
// 铁律 8 口径),必须 --apply 显式授权。
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const CACHE_VERSION = 1;
|
|
19
|
+
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
const SPECS = [
|
|
22
|
+
{
|
|
23
|
+
key: 'go',
|
|
24
|
+
name: 'Go 工具链',
|
|
25
|
+
why: '编译 Freedom 壳与内嵌 Go 后端(freedom shell build / cmd/freedom build)',
|
|
26
|
+
probes: [{ exe: 'go', args: ['version'], re: /go(\d+\.\d+(?:\.\d+)?)/ }],
|
|
27
|
+
manual: 'https://go.dev/dl/',
|
|
28
|
+
install: {
|
|
29
|
+
win32: [{ cmd: 'winget', args: ['install', '-e', '--id', 'GoLang.Go'] }],
|
|
30
|
+
darwin: [{ cmd: 'brew', args: ['install', 'go'] }],
|
|
31
|
+
linux: [{ cmd: 'sudo', args: ['apt-get', 'install', '-y', 'golang-go'] }],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'rust',
|
|
36
|
+
name: 'Rust 工具链',
|
|
37
|
+
why: '编译 examples/multiproc 的 Rust 后端(rustc -O 单文件,不走 cargo)',
|
|
38
|
+
probes: [
|
|
39
|
+
{ exe: 'rustc', args: ['--version'], re: /rustc\s+(\d+\.\d+(?:\.\d+)?)/ },
|
|
40
|
+
{ exe: 'rustc', args: ['--version'], re: /rustc\s+(\d+\.\d+(?:\.\d+)?)/, homeRelative: true },
|
|
41
|
+
],
|
|
42
|
+
manual: 'https://rustup.rs/',
|
|
43
|
+
install: {
|
|
44
|
+
win32: [{ cmd: 'winget', args: ['install', '-e', '--id', 'Rustlang.Rustup'] }],
|
|
45
|
+
darwin: [{ cmd: 'brew', args: ['install', 'rust'] }],
|
|
46
|
+
linux: [{ cmd: 'sudo', args: ['apt-get', 'install', '-y', 'rustc'] }],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: 'cpp',
|
|
51
|
+
name: 'C/C++ 工具链',
|
|
52
|
+
why: 'cgo 依赖:Linux WebKitGTK 壳与 Windows WebView2 层需要宿主 C 编译器',
|
|
53
|
+
probes: [
|
|
54
|
+
{ exe: 'cl', args: [], re: /Version\s+(\d+\.\d+)/, okNonZero: true },
|
|
55
|
+
{ exe: 'g++', args: ['--version'], re: /(\d+\.\d+(?:\.\d+)?)/ },
|
|
56
|
+
{ exe: 'clang++', args: ['--version'], re: /(\d+\.\d+(?:\.\d+)?)/ },
|
|
57
|
+
],
|
|
58
|
+
manual: 'https://visualstudio.microsoft.com/visual-cpp-build-tools/',
|
|
59
|
+
install: {
|
|
60
|
+
win32: [{
|
|
61
|
+
cmd: 'winget',
|
|
62
|
+
args: ['install', '-e', '--id', 'Microsoft.VisualStudio.2022.BuildTools',
|
|
63
|
+
'--override', '"--quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"'],
|
|
64
|
+
}],
|
|
65
|
+
darwin: [{ cmd: 'xcode-select', args: ['--install'] }],
|
|
66
|
+
linux: [{ cmd: 'sudo', args: ['apt-get', 'install', '-y', 'build-essential', 'pkg-config'] }],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
function toolchainHome(opts = {}) {
|
|
72
|
+
return opts.home || process.env.FREEDOM_TOOLCHAIN_HOME || os.homedir();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cacheFile(opts) {
|
|
76
|
+
return path.join(toolchainHome(opts), '.freedom', 'toolchain-cache.json');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function findInPath(exe, env) {
|
|
80
|
+
const dirs = String(env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
81
|
+
const exts = env.FREEDOM_HOST_PLATFORM === 'win32' || process.platform === 'win32'
|
|
82
|
+
? String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').concat([''])
|
|
83
|
+
: [''];
|
|
84
|
+
for (const dir of dirs) {
|
|
85
|
+
for (const ext of exts) {
|
|
86
|
+
const full = path.join(dir, exe + ext);
|
|
87
|
+
try { if (fs.existsSync(full) && fs.statSync(full).isFile()) return full; } catch (e) { /* 无权访问的 PATH 项:跳过 */ }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// PATH 指纹:工具可见性只取决于 PATH 变了没有,变了就必须重探(新装工具通常在 PATH 尾部)。
|
|
94
|
+
function pathFingerprint(env) {
|
|
95
|
+
let h = 0;
|
|
96
|
+
const s = String(env.PATH || '');
|
|
97
|
+
for (let i = 0; i < s.length; i++) h = (Math.imul(h, 31) + s.charCodeAt(i)) | 0;
|
|
98
|
+
return h.toString(16);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readCache(opts) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = JSON.parse(fs.readFileSync(cacheFile(opts), 'utf8'));
|
|
104
|
+
if (raw && raw.version === CACHE_VERSION && raw.host === hostPlatform(opts)) return raw;
|
|
105
|
+
} catch (e) { /* 无缓存 / 损坏 / 跨平台搬运:一律当未命中 */ }
|
|
106
|
+
return { version: CACHE_VERSION, host: hostPlatform(opts), pathHash: null, tools: {} };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeCache(opts, cache) {
|
|
110
|
+
const file = cacheFile(opts);
|
|
111
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
112
|
+
fs.writeFileSync(file, JSON.stringify(cache, null, 2) + '\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hostPlatform(opts) {
|
|
116
|
+
return (opts.env && opts.env.FREEDOM_HOST_PLATFORM) || process.platform;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 单个工具链探测:返回 { ok, version, exe, reason }。
|
|
120
|
+
function resolveProbe(p, o) {
|
|
121
|
+
if (p.homeRelative) {
|
|
122
|
+
// rustup 的默认安装位不进 PATH 时也要认出来(装完没重开终端是常态)。
|
|
123
|
+
const bin = path.join(o.env.CARGO_HOME || path.join(toolchainHome(o), '.cargo'), 'bin');
|
|
124
|
+
const exts = hostPlatform(o) === 'win32' ? ['.exe', ''] : [''];
|
|
125
|
+
for (const ext of exts) {
|
|
126
|
+
const full = path.join(bin, p.exe + ext);
|
|
127
|
+
try { if (fs.statSync(full).isFile()) return full; } catch (e) { /* 不存在 */ }
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return findInPath(p.exe, o.env);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function probe(spec, opts) {
|
|
135
|
+
const o = Object.assign({ env: process.env, exec: spawnSync }, opts);
|
|
136
|
+
const reasons = [];
|
|
137
|
+
for (const p of spec.probes) {
|
|
138
|
+
const resolved = resolveProbe(p, o);
|
|
139
|
+
if (!resolved) continue;
|
|
140
|
+
let r;
|
|
141
|
+
try { r = o.exec(resolved, p.args, { encoding: 'utf8', timeout: 8000 }); } catch (e) { reasons.push(`${resolved}: 无法启动`); continue; }
|
|
142
|
+
const text = `${(r && r.stdout) || ''}${(r && r.stderr) || ''}`;
|
|
143
|
+
const m = text.match(p.re);
|
|
144
|
+
if (m && (r.status === 0 || p.okNonZero)) return { ok: true, version: m[1], exe: resolved };
|
|
145
|
+
if (r && r.error) { reasons.push(`${resolved}: ${r.error.code || r.error.message}`); continue; }
|
|
146
|
+
// 找得到可执行但跑不通(cl 未加载 vcvarsall 环境是最常见的一种):记原因再试下一个候选。
|
|
147
|
+
reasons.push(`${resolved}: 可执行但输出无法识别(MSVC 需先加载 vcvarsall 环境)`);
|
|
148
|
+
}
|
|
149
|
+
return { ok: false, reason: reasons.length ? reasons[reasons.length - 1] : 'PATH 中未找到可执行' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// status:命中缓存的 ok 记录直接跳过 spawn。
|
|
153
|
+
function status(opts = {}) {
|
|
154
|
+
const o = Object.assign({ env: process.env, refresh: false }, opts);
|
|
155
|
+
const cache = readCache(o);
|
|
156
|
+
const hash = pathFingerprint(o.env);
|
|
157
|
+
const stale = cache.pathHash !== hash;
|
|
158
|
+
const rows = [];
|
|
159
|
+
for (const spec of SPECS) {
|
|
160
|
+
const hit = !o.refresh && !stale && cache.tools[spec.key] && cache.tools[spec.key].ok
|
|
161
|
+
&& (Date.now() - cache.tools[spec.key].at) < CACHE_TTL_MS;
|
|
162
|
+
if (hit) {
|
|
163
|
+
rows.push({ key: spec.key, name: spec.name, why: spec.why, source: 'cache', cached: true, ...cache.tools[spec.key] });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const res = probe(spec, o);
|
|
167
|
+
rows.push({ key: spec.key, name: spec.name, why: spec.why, source: 'probe', cached: false, ...res });
|
|
168
|
+
if (res.ok) cache.tools[spec.key] = { ok: true, version: res.version, exe: res.exe, at: Date.now() };
|
|
169
|
+
else delete cache.tools[spec.key]; // 失败不入缓存:下次仍探
|
|
170
|
+
}
|
|
171
|
+
cache.pathHash = hash;
|
|
172
|
+
writeCache(o, cache);
|
|
173
|
+
return { rows, skipped: rows.filter((r) => r.cached).length };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function installPlan(spec, opts) {
|
|
177
|
+
const list = spec.install[hostPlatform(opts)] || [];
|
|
178
|
+
return list.map((s) => [s.cmd].concat(s.args).join(' '));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// install:默认只打印计划;--apply 才真跑(联网 + 提权 + 改系统,属高危动作)。
|
|
182
|
+
function install(targets, opts = {}) {
|
|
183
|
+
const o = Object.assign({ env: process.env, apply: false, exec: spawnSync }, opts);
|
|
184
|
+
const want = resolveTargets(targets);
|
|
185
|
+
const st = status(Object.assign({}, o, { refresh: o.apply ? true : false }));
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const spec of want) {
|
|
188
|
+
const row = st.rows.find((r) => r.key === spec.key);
|
|
189
|
+
if (row.ok && !o.force) {
|
|
190
|
+
out.push({ key: spec.key, action: 'skip', version: row.version, note: '已就位' });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const cmds = installPlan(spec, o);
|
|
194
|
+
if (!cmds.length) { out.push({ key: spec.key, action: 'manual', note: `本平台无自动安装命令,请手动获取:${spec.manual}` }); continue; }
|
|
195
|
+
if (!o.apply) { out.push({ key: spec.key, action: 'plan', commands: cmds, manual: spec.manual }); continue; }
|
|
196
|
+
const ran = [];
|
|
197
|
+
for (const line of cmds) {
|
|
198
|
+
const [cmd, ...args] = splitCmd(line);
|
|
199
|
+
let r;
|
|
200
|
+
try { r = o.exec(cmd, args, { stdio: 'inherit', env: o.env }); } catch (e) { r = { status: -1, error: e }; }
|
|
201
|
+
ran.push({ command: line, status: r ? r.status : -1 });
|
|
202
|
+
if (!r || r.status !== 0) break; // 前置步骤失败就别往下跑
|
|
203
|
+
}
|
|
204
|
+
out.push({ key: spec.key, action: 'applied', ran });
|
|
205
|
+
}
|
|
206
|
+
return { rows: out, dryRun: !o.apply };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveTargets(raw) {
|
|
210
|
+
const keys = !raw || !raw.length || raw === 'missing' ? SPECS.map((s) => s.key) : String(raw).split(',').map((s) => s.trim()).filter(Boolean);
|
|
211
|
+
return SPECS.filter((s) => keys.includes(s.key));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function splitCmd(line) {
|
|
215
|
+
const m = String(line).match(/(?:[^\s"']+|"[^"]*")+/g) || [];
|
|
216
|
+
return m.map((t) => t.replace(/^"|"$/g, ''));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---- 配置优化:只写各工具自己的配置位,幂等 + .bak,缺省只打印 diff ----
|
|
220
|
+
|
|
221
|
+
// 大陆网络提示:locale 或时区命中才提议换镜像(不误伤海外机器),也可 FREEDOM_CN_MIRROR=1/0 强制。
|
|
222
|
+
function mainlandHint(env) {
|
|
223
|
+
const force = env.FREEDOM_CN_MIRROR;
|
|
224
|
+
if (force === '0' || force === 'false') return false;
|
|
225
|
+
if (force === '1' || force === 'true') return true;
|
|
226
|
+
const loc = `${env.LANG || ''}${env.LC_ALL || ''}${env.LC_CTYPE || ''}`;
|
|
227
|
+
const tz = env.TZ || '';
|
|
228
|
+
return /zh[-_]CN|chinese/i.test(loc) || /Asia\/(Shanghai|Chongqing|Harbin|Urumqi|Beijing|Kashgar)/i.test(tz);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const GO_PROXY = 'https://goproxy.cn,direct';
|
|
232
|
+
const CARGO_SOURCE = {
|
|
233
|
+
'source.crates-io': { 'replace-with': 'rsproxy-sparse' },
|
|
234
|
+
'source.rsproxy-sparse': { registry: 'sparse+https://rsproxy.cn/index/' },
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// cargo 的 config.toml:表名可含点([source.crates-io]),按表头整块替换/追加。
|
|
238
|
+
function upsertTomlTable(text, header, fields) {
|
|
239
|
+
const lines = String(text).split(/\r?\n/);
|
|
240
|
+
const block = [header];
|
|
241
|
+
for (const [k, v] of Object.entries(fields)) block.push(`${k} = ${JSON.stringify(v)}`);
|
|
242
|
+
const start = lines.findIndex((l) => l.trim() === header);
|
|
243
|
+
if (start < 0) {
|
|
244
|
+
while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
|
|
245
|
+
return { text: lines.concat('', block).join('\n') + '\n', had: false };
|
|
246
|
+
}
|
|
247
|
+
let end = start + 1;
|
|
248
|
+
while (end < lines.length && !/^\s*\[/.test(lines[end])) end++;
|
|
249
|
+
const out = lines.slice(0, start).concat(block, lines.slice(end));
|
|
250
|
+
return { text: out.join('\n').replace(/\n*$/, '\n'), had: true };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function cargoConfigFile(env, home) {
|
|
254
|
+
const root = env.CARGO_HOME ? env.CARGO_HOME : path.join(home, '.cargo');
|
|
255
|
+
return path.join(root, 'config.toml');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// optimize:返回每个工具链的配置动作。apply=false 只回报将要写什么。
|
|
259
|
+
function optimize(opts = {}) {
|
|
260
|
+
const o = Object.assign({ env: process.env, apply: false, exec: spawnSync }, opts);
|
|
261
|
+
const home = toolchainHome(o);
|
|
262
|
+
const actions = [];
|
|
263
|
+
const st = status(Object.assign({}, o, { refresh: false }));
|
|
264
|
+
|
|
265
|
+
const go = st.rows.find((r) => r.key === 'go');
|
|
266
|
+
if (!mainlandHint(o.env)) {
|
|
267
|
+
actions.push({ key: 'go', action: 'skip', note: '非大陆网络环境提示,保留官方 GOPROXY' });
|
|
268
|
+
} else if (!go || !go.ok) {
|
|
269
|
+
actions.push({ key: 'go', action: 'blocked', note: '未检测到 Go,先 freedom toolchain install go' });
|
|
270
|
+
} else {
|
|
271
|
+
const cur = runCapture(o.exec, go.exe, ['env', 'GOPROXY']);
|
|
272
|
+
if (cur && cur.includes('goproxy.cn')) {
|
|
273
|
+
actions.push({ key: 'go', action: 'skip', note: `GOPROXY 已是镜像(${cur})` });
|
|
274
|
+
} else {
|
|
275
|
+
const cmdLine = `${go.exe} env -w GOPROXY=${GO_PROXY}`;
|
|
276
|
+
if (!o.apply) { actions.push({ key: 'go', action: 'plan', commands: [cmdLine], from: cur }); }
|
|
277
|
+
else {
|
|
278
|
+
const r = o.exec(go.exe, ['env', '-w', `GOPROXY=${GO_PROXY}`], { env: o.env, encoding: 'utf8' });
|
|
279
|
+
actions.push({ key: 'go', action: 'applied', to: GO_PROXY, status: r ? r.status : -1, error: r && r.error ? String(r.error.message) : null });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const rust = st.rows.find((r) => r.key === 'rust');
|
|
285
|
+
if (!mainlandHint(o.env)) {
|
|
286
|
+
actions.push({ key: 'rust', action: 'skip', note: '非大陆网络环境提示,不改 cargo 源' });
|
|
287
|
+
} else if (!rust || !rust.ok) {
|
|
288
|
+
actions.push({ key: 'rust', action: 'blocked', note: '未检测到 Rust,先 freedom toolchain install rust' });
|
|
289
|
+
} else {
|
|
290
|
+
const file = cargoConfigFile(o.env, home);
|
|
291
|
+
let text = '';
|
|
292
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch (e) { /* 首次创建 */ }
|
|
293
|
+
if (/rsproxy\.cn/.test(text)) {
|
|
294
|
+
actions.push({ key: 'rust', action: 'skip', note: `cargo 源已是 rsproxy(${file})`, file });
|
|
295
|
+
} else {
|
|
296
|
+
let next = text;
|
|
297
|
+
for (const [header, fields] of Object.entries(CARGO_SOURCE)) next = upsertTomlTable(next, `[${header}]`, fields).text;
|
|
298
|
+
if (!o.apply) actions.push({ key: 'rust', action: 'plan', file, writes: next });
|
|
299
|
+
else {
|
|
300
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
301
|
+
if (fs.existsSync(file)) fs.copyFileSync(file, `${file}.bak`);
|
|
302
|
+
fs.writeFileSync(file, next);
|
|
303
|
+
actions.push({ key: 'rust', action: 'applied', file, backup: fs.existsSync(`${file}.bak`) ? `${file}.bak` : null });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const cpp = st.rows.find((r) => r.key === 'cpp');
|
|
309
|
+
actions.push({
|
|
310
|
+
key: 'cpp',
|
|
311
|
+
action: 'report',
|
|
312
|
+
ok: !!(cpp && cpp.ok),
|
|
313
|
+
note: cpp && cpp.ok
|
|
314
|
+
? `已就位:${cpp.exe}(cgo 可直接使用,无用户配置需改)`
|
|
315
|
+
: `${cpp && cpp.reason ? cpp.reason + ';' : ''}Windows 需经 vcvarsall 注入环境(freedom build 已在子进程内处理),或安装 Build Tools`,
|
|
316
|
+
});
|
|
317
|
+
return { rows: actions, dryRun: !o.apply };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function runCapture(exec, exe, args) {
|
|
321
|
+
try {
|
|
322
|
+
const r = exec(exe, args, { encoding: 'utf8', timeout: 8000 });
|
|
323
|
+
if (!r || r.status !== 0) return null;
|
|
324
|
+
return String(r.stdout || '').trim();
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 清理缓存(测试与「重装后重探」用)。
|
|
331
|
+
function clearCache(opts = {}) {
|
|
332
|
+
try { fs.unlinkSync(cacheFile(opts)); return true; } catch (e) { return false; }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = {
|
|
336
|
+
SPECS, status, install, optimize, probe, mainlandHint, upsertTomlTable,
|
|
337
|
+
cacheFile, clearCache, findInPath, pathFingerprint, installPlan, splitCmd, cargoConfigFile,
|
|
338
|
+
GO_PROXY, CACHE_VERSION, CACHE_TTL_MS,
|
|
339
|
+
};
|
package/lib/tui.js
CHANGED
|
@@ -372,4 +372,40 @@ async function tui(cwd) {
|
|
|
372
372
|
return 0;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
|
|
375
|
+
// 裸 `freedom` 的显示方式选择器:终端 TUI 或 Freedom Desktop(图形窗口)。
|
|
376
|
+
// 先退出 raw mode 再拉起窗口 / 再进主菜单,避免两个界面抢同一个终端。
|
|
377
|
+
async function pickDisplay(cwd) {
|
|
378
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
379
|
+
console.error('[freedom] 当前非交互式终端,请改用具体子命令(freedom help 查看全部命令)。');
|
|
380
|
+
return 1;
|
|
381
|
+
}
|
|
382
|
+
const app = new TUI();
|
|
383
|
+
app.enter();
|
|
384
|
+
let idx;
|
|
385
|
+
try {
|
|
386
|
+
idx = await app.menu('选择显示方式', ['终端 TUI(纯文本菜单)', 'Freedom Desktop(图形窗口,由 freedom 自身打包)'], {
|
|
387
|
+
footer: '↑ ↓ 选择 · Enter 确认 · q 退出',
|
|
388
|
+
});
|
|
389
|
+
} finally {
|
|
390
|
+
app.exit();
|
|
391
|
+
}
|
|
392
|
+
if (idx === null) return 0;
|
|
393
|
+
if (idx === 1) return desktopFlow(cwd);
|
|
394
|
+
return await tui(cwd);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function desktopFlow(cwd) {
|
|
398
|
+
const { desktop } = require('./desktop');
|
|
399
|
+
const theme = require('./theme');
|
|
400
|
+
try {
|
|
401
|
+
const r = await desktop({});
|
|
402
|
+
process.stdout.write(`${theme.ok('Freedom Desktop 已拉起:')}${r.exe}\n`);
|
|
403
|
+
if (r.rebuilt) process.stdout.write(`${theme.dim(`(本次已重新打包,CLI v${r.version})`)}\n`);
|
|
404
|
+
return 0;
|
|
405
|
+
} catch (e) {
|
|
406
|
+
process.stdout.write(`\n${theme.err(e.message)}\n`);
|
|
407
|
+
return 1;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
module.exports = { tui, pickDisplay, TUI };
|
package/lib/utils.js
CHANGED
|
@@ -25,6 +25,9 @@ function goTemplateDir() {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function shellDir() {
|
|
28
|
+
// 壳目录可经 env 覆写(离线/测试注入假壳;与 FREEDOM_SHELL_REPO 等既有旋钮同族)。
|
|
29
|
+
const override = process.env.FREEDOM_SHELL_DIR;
|
|
30
|
+
if (override) return path.resolve(override);
|
|
28
31
|
return path.join(PKG_ROOT, 'shell');
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -81,6 +84,53 @@ function nativePlatform() {
|
|
|
81
84
|
return `${plat}-${arch}`;
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
// 平台别名归一化:把口语写法(win / mac / linux / windows / osx / ubuntu)与架构同义词
|
|
88
|
+
// (x86_64 / amd64 / aarch64)统一到 ALL_PLATFORMS 的 key,无法识别返回 null。
|
|
89
|
+
// 缺架构时的默认值与历史 build.js keyMap 一致:win→x64、darwin→arm64(已不支持 Intel Mac)、
|
|
90
|
+
// linux→x64。
|
|
91
|
+
const PLAT_FAMILY = {
|
|
92
|
+
win: 'win', win32: 'win', win64: 'win', windows: 'win', pc: 'win',
|
|
93
|
+
mac: 'darwin', macos: 'darwin', 'mac-os': 'darwin', 'mac-os-x': 'darwin',
|
|
94
|
+
osx: 'darwin', darwin: 'darwin', apple: 'darwin',
|
|
95
|
+
linux: 'linux', lin: 'linux', ubuntu: 'linux', debian: 'linux',
|
|
96
|
+
};
|
|
97
|
+
const PLAT_ARCH = {
|
|
98
|
+
x64: 'x64', x86_64: 'x64', 'x86-64': 'x64', amd64: 'x64',
|
|
99
|
+
arm64: 'arm64', aarch64: 'arm64', 'aarch-64': 'arm64',
|
|
100
|
+
};
|
|
101
|
+
const PLAT_DEFAULT_ARCH = { win: 'x64', darwin: 'arm64', linux: 'x64' };
|
|
102
|
+
|
|
103
|
+
function normalizePlatform(raw) {
|
|
104
|
+
if (typeof raw !== 'string') return null;
|
|
105
|
+
const s = raw.trim().toLowerCase().replace(/\s+/g, '-').replace(/_/g, '-');
|
|
106
|
+
if (!s) return null;
|
|
107
|
+
if (ALL_PLATFORMS.includes(s)) return s;
|
|
108
|
+
// 从右往左试两种切法(架构名自身可能带分隔符,如 x86-64;族名也一样,如 mac-os-x):
|
|
109
|
+
// 先按最后一个分隔符切,切不出架构再按倒数第二个切,还不行就整串当族名。
|
|
110
|
+
let fam = s;
|
|
111
|
+
let arch;
|
|
112
|
+
const cuts = [];
|
|
113
|
+
const last = s.lastIndexOf('-');
|
|
114
|
+
if (last > 0) {
|
|
115
|
+
cuts.push([last, s.slice(last + 1)]);
|
|
116
|
+
const prev = s.lastIndexOf('-', last - 1);
|
|
117
|
+
if (prev > 0) cuts.push([prev, s.slice(prev + 1)]);
|
|
118
|
+
}
|
|
119
|
+
for (const [at, tail] of cuts) {
|
|
120
|
+
if (PLAT_ARCH[tail]) {
|
|
121
|
+
arch = PLAT_ARCH[tail];
|
|
122
|
+
fam = s.slice(0, at);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const family = PLAT_FAMILY[fam];
|
|
127
|
+
if (!family) return null;
|
|
128
|
+
const key = `${family}-${arch || PLAT_DEFAULT_ARCH[family]}`;
|
|
129
|
+
// 归一后仍须落在支持矩阵里:darwin-x64(Intel Mac 已停止支持)之类的组合
|
|
130
|
+
// 不能因为"语法像对的"就放行。
|
|
131
|
+
return ALL_PLATFORMS.includes(key) ? key : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
84
134
|
function localShellPath(plat) {
|
|
85
135
|
return path.join(shellDir(), plat, SHELL_EXE_NAME[plat] || 'freedom-shell');
|
|
86
136
|
}
|
|
@@ -134,6 +184,7 @@ module.exports = {
|
|
|
134
184
|
isMacPlat,
|
|
135
185
|
platformExeName,
|
|
136
186
|
nativePlatform,
|
|
187
|
+
normalizePlatform,
|
|
137
188
|
localShellPath,
|
|
138
189
|
tutorialFile,
|
|
139
190
|
loadConfig,
|
package/lib/verify.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { isWinPlat, platformExeName } = require('./utils');
|
|
13
|
+
const { APP_BIN_MAGIC, decryptApp, buildIntegrity } = require('./security');
|
|
13
14
|
|
|
14
15
|
// ---- 产物定位 ----
|
|
15
16
|
|
|
@@ -107,6 +108,7 @@ function checkPlatformProduct({ targetDir, plat, appName, hasBackend }) {
|
|
|
107
108
|
const hasIntegrity = fs.existsSync(path.join(resDir, '.integrity'));
|
|
108
109
|
const hasHtml = fs.existsSync(path.join(resDir, 'index.html'));
|
|
109
110
|
const hasConfig = fs.existsSync(path.join(resDir, 'config.json'));
|
|
111
|
+
let securePayload = null;
|
|
110
112
|
if (hasBin) {
|
|
111
113
|
if (hasHtml || hasConfig) {
|
|
112
114
|
fail('资源形态', 'high 模式残留明文 index.html/config.json(互斥被破坏)');
|
|
@@ -115,13 +117,35 @@ function checkPlatformProduct({ targetDir, plat, appName, hasBackend }) {
|
|
|
115
117
|
}
|
|
116
118
|
if (!hasIntegrity) fail('完整性清单', '缺失 .integrity');
|
|
117
119
|
else okc('完整性清单', '.integrity 存在');
|
|
118
|
-
// app.bin 容器头校验(
|
|
119
|
-
let
|
|
120
|
+
// app.bin 容器头校验(FRDM2)
|
|
121
|
+
let bin = Buffer.alloc(0);
|
|
120
122
|
try {
|
|
121
|
-
|
|
123
|
+
bin = fs.readFileSync(path.join(resDir, 'app.bin'));
|
|
122
124
|
} catch (e) { /* 读取失败按不通过处理 */ }
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
const head = bin.subarray(0, APP_BIN_MAGIC.length).toString('latin1');
|
|
126
|
+
if (head !== APP_BIN_MAGIC) {
|
|
127
|
+
fail('app.bin 容器', `容器头不是 ${APP_BIN_MAGIC}(实际 "${head}"),文件损坏、非本工具产物或旧版产物`);
|
|
128
|
+
} else {
|
|
129
|
+
okc('app.bin 容器', `${APP_BIN_MAGIC} 头有效(${fmtSize(bin.length)})`);
|
|
130
|
+
// 真解一次容器:认证失败 = 产物被篡改或 name 与容器不匹配;同时拿到 backend 供后端检查。
|
|
131
|
+
try {
|
|
132
|
+
securePayload = decryptApp(appName, bin);
|
|
133
|
+
okc('容器解密', `认证通过,html ${fmtSize(securePayload.html.length)} / config ${fmtSize(securePayload.config.length)}`);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
fail('容器解密', e.message);
|
|
136
|
+
}
|
|
137
|
+
// .integrity 与容器一致(防整体替换:清单值由构建期容器盐派生密钥签名)
|
|
138
|
+
if (hasIntegrity) {
|
|
139
|
+
try {
|
|
140
|
+
const list = JSON.parse(fs.readFileSync(path.join(resDir, '.integrity'), 'utf8'));
|
|
141
|
+
const want = buildIntegrity(appName, bin).appBin;
|
|
142
|
+
if (list.appBin !== want) fail('完整性清单', '.integrity 与 app.bin 不一致(产物可能被替换)');
|
|
143
|
+
else okc('完整性清单', '.integrity 与 app.bin 一致');
|
|
144
|
+
} catch (e) {
|
|
145
|
+
fail('完整性清单', e.message);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
125
149
|
} else {
|
|
126
150
|
if (hasIntegrity) {
|
|
127
151
|
fail('资源形态', '明文模式残留 high 产物(.integrity)');
|
|
@@ -148,10 +172,21 @@ function checkPlatformProduct({ targetDir, plat, appName, hasBackend }) {
|
|
|
148
172
|
}
|
|
149
173
|
}
|
|
150
174
|
|
|
151
|
-
// 4)
|
|
175
|
+
// 4) 后端:明文模式看 resources/backend 目录;high 模式看容器内 backend 条目
|
|
176
|
+
// (容器模式下磁盘不得有明文后端目录,运行时由壳解密到临时目录)。
|
|
152
177
|
if (hasBackend) {
|
|
153
|
-
if (
|
|
154
|
-
|
|
178
|
+
if (hasBin) {
|
|
179
|
+
const n = securePayload && securePayload.backend ? Object.keys(securePayload.backend).length : 0;
|
|
180
|
+
if (n === 0) fail('后端进程', 'high 模式容器内无 backend 条目(后端源码未入容器)');
|
|
181
|
+
else okc('后端进程', `容器内 ${n} 个后端文件(磁盘无明文)`);
|
|
182
|
+
if (fs.existsSync(path.join(resDir, 'backend'))) {
|
|
183
|
+
fail('后端进程', 'high 模式残留明文 resources/backend(与容器互斥被破坏)');
|
|
184
|
+
}
|
|
185
|
+
} else if (fs.existsSync(path.join(resDir, 'backend'))) {
|
|
186
|
+
okc('后端进程', 'resources/backend 存在');
|
|
187
|
+
} else {
|
|
188
|
+
fail('后端进程', '配置了 backend 但 resources/backend 缺失');
|
|
189
|
+
}
|
|
155
190
|
}
|
|
156
191
|
|
|
157
192
|
return checks;
|
|
@@ -191,13 +226,14 @@ function collectTree(dir, depth, out, maxDepth) {
|
|
|
191
226
|
return out;
|
|
192
227
|
}
|
|
193
228
|
for (const e of entries) {
|
|
229
|
+
const abs = path.join(dir, e.name);
|
|
230
|
+
const stat = safeStat(abs);
|
|
194
231
|
if (e.name === '.integrity') {
|
|
195
|
-
//
|
|
196
|
-
|
|
232
|
+
// 清单内容是不对外展开的校验值,但大小必须照实报:写死 0 会让产物自检打印
|
|
233
|
+
// 「.integrity (0 B)」,在高模式安检语境下读起来像清单是空的,反而制造误判。
|
|
234
|
+
out.push({ rel: e.name, size: stat ? stat.size : 0, isDir: false, depth, hidden: true });
|
|
197
235
|
continue;
|
|
198
236
|
}
|
|
199
|
-
const abs = path.join(dir, e.name);
|
|
200
|
-
const stat = safeStat(abs);
|
|
201
237
|
if (e.isDirectory()) {
|
|
202
238
|
out.push({ rel: e.name, size: 0, isDir: true, depth });
|
|
203
239
|
if (depth < maxDepth) collectTree(abs, depth + 1, out, maxDepth);
|
package/lib/webkit.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Linux 构建的 WebKitGTK 依赖名选择。
|
|
4
|
+
//
|
|
5
|
+
// 背景:webview_go 的 cgo 依赖名在本地补丁副本(third_party/webview_go)里拆成了
|
|
6
|
+
// webkit2_40.go / webkit2_41.go 两个互斥标签文件。Ubuntu 24.04 / Debian 13 已移除
|
|
7
|
+
// webkit2gtk-4.0,只保留同一套 C API 的 webkit2gtk-4.1 —— 沿用 4.0 会让 `go build`
|
|
8
|
+
// 在新发行版上直接失败。
|
|
9
|
+
//
|
|
10
|
+
// 这里做的事:探测 pkg-config 实际装了哪个,缺 4.0 且有 4.1 时给 go 命令加
|
|
11
|
+
// `-tags webkit2_41`;否则返回原始环境(老发行版与 CI 产物基线零变化)。
|
|
12
|
+
// webkitgtk-6.0 是另一套 API,不能这样桥接(需上游适配)。
|
|
13
|
+
|
|
14
|
+
const { spawnSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
function pkgConfigExists(mod, exec = spawnSync) {
|
|
17
|
+
try {
|
|
18
|
+
return exec('pkg-config', ['--exists', mod], { stdio: 'ignore' }).status === 0;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 需要 4.1 标签吗:仅 Linux、仅当 4.0 不可用而 4.1 可用。
|
|
25
|
+
function needsWebkit241(env = process.env, exec = spawnSync) {
|
|
26
|
+
if (env.FREEDOM_WEBKIT_FORCE_41 === '1') return true;
|
|
27
|
+
const platform = env.FREEDOM_HOST_PLATFORM || process.platform;
|
|
28
|
+
if (platform !== 'linux') return false;
|
|
29
|
+
if (pkgConfigExists('webkit2gtk-4.0', exec)) return false;
|
|
30
|
+
return pkgConfigExists('webkit2gtk-4.1', exec);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 把标签并入 GOFLAGS(go 命令原生识别 GOFLAGS,故子进程与人工命令行都吃到)。
|
|
34
|
+
// 已有 GOFLAGS 时追加,不覆盖。
|
|
35
|
+
function applyWebkitTags(env = process.env, exec = spawnSync) {
|
|
36
|
+
const out = Object.assign({}, env);
|
|
37
|
+
if (!needsWebkit241(env, exec)) return { env: out, tags: [] };
|
|
38
|
+
const tags = ['webkit2_41'];
|
|
39
|
+
const existing = (env.GOFLAGS || '').trim();
|
|
40
|
+
const merged = existing ? `${existing} -tags=${tags.join(',')}` : `-tags=${tags.join(',')}`;
|
|
41
|
+
out.GOFLAGS = merged;
|
|
42
|
+
return { env: out, tags };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { needsWebkit241, applyWebkitTags, pkgConfigExists };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yufengtadian/freedom-cli",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.3",
|
|
4
4
|
"description": "Freedom WebView desktop shell packaging tool - no Go toolchain required, one command packs three-platform desktop apps",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"frontend",
|
|
16
16
|
"cross-platform"
|
|
17
17
|
],
|
|
18
|
-
"license": "
|
|
18
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"bin",
|
|
27
27
|
"lib",
|
|
28
28
|
"shell",
|
|
29
|
+
"skill",
|
|
29
30
|
"templates",
|
|
30
31
|
"tutorial",
|
|
31
32
|
"postinstall.js",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|