@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.
Files changed (76) hide show
  1. package/LICENSE +40 -0
  2. package/README.md +119 -15
  3. package/lib/agents.js +448 -0
  4. package/lib/build.js +189 -45
  5. package/lib/cli.js +258 -9
  6. package/lib/config.js +20 -1
  7. package/lib/desktop.js +133 -0
  8. package/lib/dev.js +149 -0
  9. package/lib/mcp.js +217 -0
  10. package/lib/release.js +90 -0
  11. package/lib/security.js +113 -54
  12. package/lib/shell.js +170 -63
  13. package/lib/toolchain.js +339 -0
  14. package/lib/tui.js +37 -1
  15. package/lib/utils.js +51 -0
  16. package/lib/verify.js +48 -12
  17. package/lib/webkit.js +45 -0
  18. package/package.json +3 -2
  19. package/shell/darwin-arm64/freedom-shell +0 -0
  20. package/shell/linux-x64/freedom-shell +0 -0
  21. package/shell/win-x64/freedom-shell.exe +0 -0
  22. package/skill/freedom/SKILL.md +159 -0
  23. package/templates/desktop/app.html +559 -0
  24. package/templates/desktop/backend/desktop.mjs +327 -0
  25. package/templates/desktop/freedom.config.js +32 -0
  26. package/templates/desktop/icon.ico +0 -0
  27. package/templates/go/go.mod +3 -0
  28. package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
  29. package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
  30. package/templates/go/pkg/freedom/anti_debug_windows.go +238 -11
  31. package/templates/go/pkg/freedom/assets/freedom.js +3 -0
  32. package/templates/go/pkg/freedom/assets_embed.go +1 -0
  33. package/templates/go/pkg/freedom/backend_proc.go +5 -5
  34. package/templates/go/pkg/freedom/events_windows.go +5 -5
  35. package/templates/go/pkg/freedom/freedom.go +64 -10
  36. package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
  37. package/templates/go/pkg/freedom/resources.go +119 -14
  38. package/templates/go/pkg/freedom/securetemp.go +61 -0
  39. package/templates/go/pkg/freedom/securetemp_other.go +20 -0
  40. package/templates/go/pkg/freedom/securetemp_windows.go +25 -0
  41. package/templates/go/pkg/freedom/security.go +293 -64
  42. package/templates/go/pkg/freedom/shutdown.go +55 -0
  43. package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
  44. package/templates/go/pkg/freedom/store.go +11 -3
  45. package/templates/go/pkg/freedom/syscap_windows.go +33 -33
  46. package/templates/go/pkg/freedom/sysint_windows.go +2 -2
  47. package/templates/go/pkg/freedom/tray_windows.go +18 -18
  48. package/templates/go/pkg/freedom/updater.go +3 -3
  49. package/templates/go/pkg/freedom/window_other.go +7 -1
  50. package/templates/go/pkg/freedom/window_windows.go +185 -17
  51. package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
  52. package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
  53. package/templates/go/third_party/webview_go/LICENSE +22 -0
  54. package/templates/go/third_party/webview_go/README.md +50 -0
  55. package/templates/go/third_party/webview_go/glue.c +36 -0
  56. package/templates/go/third_party/webview_go/go.mod +3 -0
  57. package/templates/go/third_party/webview_go/go.sum +0 -0
  58. package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
  59. package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
  60. package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
  61. package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
  62. package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
  63. package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
  64. package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
  65. package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
  66. package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
  67. package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
  68. package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
  69. package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
  70. package/templates/go/third_party/webview_go/webview.cc +1 -0
  71. package/templates/go/third_party/webview_go/webview.go +332 -0
  72. package/templates/installer/app.nsi +43 -0
  73. package/templates/project/freedom.config.js +14 -0
  74. package/templates/project/freedom.d.ts +77 -0
  75. package/templates/project-minimal/freedom.config.js +14 -0
  76. package/templates/project-minimal/freedom.d.ts +77 -0
package/lib/dev.js ADDED
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ // freedom dev —— 热更新开发流(对标 tauri dev / wails dev):
4
+ // 1) 拉起项目 dev server(默认 `npm run dev`,可用配置 dev.command 或 --command 覆盖);
5
+ // 2) 从其输出解析 http://localhost:<port> 地址(或直接 --port/--url 指定);
6
+ // 3) 把随包预编译壳复制进 .freedom/dev/,写 URL 模式 config.json(debug 开、不加密),
7
+ // 拉起壳窗口——页面热更新走 vite HMR(WebView 内建 WebSocket 支持),改码免重打包。
8
+ // 产物边界:.freedom/dev/ 是开发用临时目录,与正式 dist/ 互不影响;backend/ 会镜像复制,
9
+ // 让带任意语言后端的项目也能在 dev 流里联调。
10
+
11
+ const fs = require('fs');
12
+ const fsp = fs.promises;
13
+ const path = require('path');
14
+ const { spawn } = require('child_process');
15
+
16
+ const {
17
+ loadConfig, nativePlatform, localShellPath, platformExeName, isWinPlat, copyDir,
18
+ } = require('./utils');
19
+
20
+ // vite/webpack 都会打 "Local: http://localhost:5173/";ANSI 色码先剥掉再匹配。
21
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
22
+ const URL_RE = /https?:\/\/(localhost|127\.0\.0\.1):\d+/;
23
+
24
+ function pickUrl(text) {
25
+ const m = text.replace(ANSI_RE, '').match(URL_RE);
26
+ return m ? m[0] : null;
27
+ }
28
+
29
+ function killTree(child) {
30
+ if (!child || child.killed) return;
31
+ try {
32
+ if (process.platform === 'win32' && child.pid) {
33
+ // npm 是树根壳进程,普通 kill 只杀 cmd 包装层,子进程全部留下——必须树杀。
34
+ spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
35
+ } else {
36
+ child.kill('SIGTERM');
37
+ }
38
+ } catch { /* 已退出 */ }
39
+ }
40
+
41
+ function spawnCommand(command, cwd) {
42
+ return spawn(command, { cwd, shell: true, env: { ...process.env, NO_COLOR: '1', BROWSER: 'none' } });
43
+ }
44
+
45
+ function waitForUrl(server, timeoutMs) {
46
+ return new Promise((resolve, reject) => {
47
+ let buf = '';
48
+ const onData = (chunk) => {
49
+ buf += String(chunk);
50
+ const url = pickUrl(buf);
51
+ if (url) {
52
+ clearTimeout(timer);
53
+ resolve(url);
54
+ }
55
+ if (buf.length > 64 * 1024) buf = buf.slice(-8 * 1024); // 只留尾部,防长输出撑爆
56
+ };
57
+ server.stdout.on('data', onData);
58
+ server.stderr.on('data', onData);
59
+ const timer = setTimeout(
60
+ () => reject(new Error(`dev server 在 ${timeoutMs / 1000}s 内未报告本地 URL,请检查 dev 脚本或用 --port/--url 指定`)),
61
+ timeoutMs,
62
+ );
63
+ server.on('exit', (code) => {
64
+ clearTimeout(timer);
65
+ reject(new Error(`dev server 提前退出(code=${code}),见上方其输出`));
66
+ });
67
+ });
68
+ }
69
+
70
+ async function renderDevConfigJSON(dir, cfg, url, name) {
71
+ const obj = {
72
+ name,
73
+ title: cfg.title || cfg.name || name,
74
+ titlebar: cfg.titlebar || 'frameless',
75
+ width: Number(cfg.width) || 1024,
76
+ height: Number(cfg.height) || 720,
77
+ minWidth: Number(cfg.minWidth) || 400,
78
+ minHeight: Number(cfg.minHeight) || 300,
79
+ center: typeof cfg.center === 'boolean' ? cfg.center : true,
80
+ debug: true, // dev 流默认开开发者工具
81
+ url,
82
+ singleInstance: false, // 开发期允许反复起窗
83
+ };
84
+ if (cfg.backend && cfg.backend.command) {
85
+ obj.backend = Array.isArray(cfg.backend.command)
86
+ ? { command: cfg.backend.command[0], args: cfg.backend.command.slice(1) }
87
+ : { command: cfg.backend.command, args: Array.isArray(cfg.backend.args) ? cfg.backend.args : [] };
88
+ }
89
+ await fsp.writeFile(path.join(dir, 'resources', 'config.json'), JSON.stringify(obj, null, 2), 'utf8');
90
+ }
91
+
92
+ async function dev(opts = {}) {
93
+ const dir = path.resolve(opts.dir || '.');
94
+ const cfg = await loadConfig(dir);
95
+ const name = cfg.name || path.basename(dir);
96
+ const plat = nativePlatform();
97
+ const shellSrc = localShellPath(plat);
98
+ if (!fs.existsSync(shellSrc)) {
99
+ throw new Error(`缺少 ${plat} 壳二进制(${shellSrc}),先执行 freedom shell download ${plat}`);
100
+ }
101
+
102
+ const devDir = path.join(dir, '.freedom', 'dev');
103
+ await fsp.rm(devDir, { recursive: true, force: true });
104
+ await fsp.mkdir(path.join(devDir, 'resources'), { recursive: true });
105
+ const exe = platformExeName(plat, name);
106
+ const exePath = path.join(devDir, exe);
107
+ await fsp.copyFile(shellSrc, exePath);
108
+ if (!isWinPlat(plat)) await fsp.chmod(exePath, 0o755);
109
+ const backendSrc = path.join(dir, 'backend');
110
+ if (fs.existsSync(backendSrc)) {
111
+ await copyDir(backendSrc, path.join(devDir, 'resources', 'backend'));
112
+ }
113
+
114
+ const url = opts.url || (opts.port ? `http://localhost:${Number(opts.port)}` : null);
115
+ const command = opts.command || (cfg.dev && cfg.dev.command) || 'npm run dev';
116
+ const server = spawnCommand(command, dir);
117
+ const children = [server];
118
+ let shuttingDown = false;
119
+ const stopAll = () => {
120
+ if (shuttingDown) return;
121
+ shuttingDown = true;
122
+ for (const c of children) killTree(c);
123
+ };
124
+ process.once('SIGINT', () => { stopAll(); process.exitCode = 0; });
125
+
126
+ let resolvedUrl = url;
127
+ try {
128
+ if (!resolvedUrl) resolvedUrl = await waitForUrl(server, opts.timeoutMs || 60000);
129
+ } catch (e) {
130
+ stopAll();
131
+ throw e;
132
+ }
133
+
134
+ await renderDevConfigJSON(devDir, cfg, resolvedUrl, name);
135
+ process.stdout.write(`[freedom] dev server: ${resolvedUrl}(HMR 热更已接壳窗口)\n`);
136
+
137
+ const app = spawn(exePath, [], { cwd: devDir, detached: false, stdio: 'ignore' });
138
+ children.push(app);
139
+ const appExit = new Promise((resolve) => app.on('exit', (code) => resolve(code)));
140
+ const serverExit = new Promise((resolve) => server.on('exit', (code) => resolve({ serverDied: code })));
141
+ const outcome = await Promise.race([appExit.then((code) => ({ appExit: code })), serverExit]);
142
+ stopAll();
143
+ if (outcome.appExit !== undefined) {
144
+ return `DEVDONE shell-exit=${outcome.appExit}`;
145
+ }
146
+ return `DEVDONE dev-server-exited-code=${outcome.serverDied}`;
147
+ }
148
+
149
+ module.exports = { dev, pickUrl };
package/lib/mcp.js ADDED
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ // freedom mcp serve —— 零依赖 stdio MCP 服务:把 freedom CLI 能力暴露为 MCP 工具
4
+ //
5
+ // 协议:newline-delimited JSON-RPC 2.0(initialize / tools/list / tools/call / ping)。
6
+ // 实现纪律:本进程的 stdout 只允许写协议帧,因此所有能力都经"子进程调用 CLI"并捕获其
7
+ // 输出实现(绝不在本进程内 require CLI 模块直接执行——那会把彩色日志打进协议通道)。
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const readline = require('readline');
12
+ const { spawn } = require('child_process');
13
+ const { packageRoot } = require('./utils');
14
+
15
+ const PROTOCOL_VERSION = '2024-11-05';
16
+ const ENTRY = path.join(packageRoot(), 'bin', 'freedom.js');
17
+ const VERSION = (() => {
18
+ try { return require(path.join(packageRoot(), 'package.json')).version; } catch (e) { return '0.0.0'; }
19
+ })();
20
+
21
+ const DIR = { type: 'string', description: 'freedom 项目目录(缺省为服务进程当前目录)' };
22
+ const PLATFORM = { type: 'string', enum: ['win', 'mac', 'linux', 'all', 'win-x64', 'darwin-arm64', 'linux-x64', 'linux-arm64'] };
23
+
24
+ const TOOLS = [
25
+ {
26
+ name: 'freedom_build',
27
+ description: '把前端项目打包为跨平台桌面应用(预编译通用壳 + resources/ 外部资源层)。返回产物路径与自检结论。',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: { dir: DIR, platform: PLATFORM, security: { type: 'string', enum: ['none', 'basic', 'high'] }, installer: { type: 'boolean', description: '附带安装包产物(便携 zip / Windows NSIS)' } },
31
+ additionalProperties: false,
32
+ },
33
+ args: (a) => ['build', ...flags(a, { platform: '--platform', security: '--security' }, { installer: '--installer' })],
34
+ cwd: (a) => a.dir,
35
+ },
36
+ {
37
+ name: 'freedom_init',
38
+ description: '新建 freedom 项目骨架(含 freedom.config.js 与前端最小工程)。dir 为目标目录。',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: { dir: { type: 'string', description: '新项目目录(必填)' }, template: { type: 'string', enum: ['full', 'minimal'] }, force: { type: 'boolean' } },
42
+ required: ['dir'],
43
+ additionalProperties: false,
44
+ },
45
+ args: (a) => ['init', String(a.dir), ...flags(a, { template: '--template' }, { force: '--force' })],
46
+ },
47
+ {
48
+ name: 'freedom_verify',
49
+ description: '校验已构建产物的完整性与结构(退出码非零即失败),用于发布前把关。',
50
+ inputSchema: { type: 'object', properties: { dir: DIR, platform: PLATFORM }, additionalProperties: false },
51
+ args: (a) => ['verify', ...flags(a, { platform: '--platform' })],
52
+ cwd: (a) => a.dir,
53
+ },
54
+ {
55
+ name: 'freedom_config',
56
+ description: '读取(无 key)或修改(key + value)freedom.config.js 配置项。常用 key:titlebar、width、height、url、singleInstance、security、backendDir、staticHtml。',
57
+ inputSchema: { type: 'object', properties: { dir: DIR, key: { type: 'string' }, value: { type: 'string' } }, additionalProperties: false },
58
+ args: (a) => (a.key ? ['config', 'set', String(a.key), String(a.value === undefined ? '' : a.value)] : ['config']),
59
+ cwd: (a) => a.dir,
60
+ },
61
+ {
62
+ name: 'freedom_shell',
63
+ description: '通用壳管理:list 列出本地已就绪壳;download 下载指定平台壳。',
64
+ inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'download'] }, platform: PLATFORM }, required: ['action'], additionalProperties: false },
65
+ args: (a) => ['shell', String(a.action || 'list'), ...(a.platform ? [String(a.platform)] : [])],
66
+ },
67
+ {
68
+ name: 'freedom_release',
69
+ description: '应用自更新发布环:keygen 生成 ed25519 密钥对;manifest 产出签名更新清单 latest.json。',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: { action: { type: 'string', enum: ['keygen', 'manifest'] }, dir: DIR, artifact: { type: 'string' }, url: { type: 'string' }, version: { type: 'string' }, notes: { type: 'string' } },
73
+ required: ['action'],
74
+ additionalProperties: false,
75
+ },
76
+ args: (a) => (a.action === 'manifest'
77
+ ? ['manifest', ...flags(a, { artifact: '--artifact', url: '--url', version: '--version', notes: '--notes' })]
78
+ : ['keygen']),
79
+ cwd: (a) => a.dir,
80
+ },
81
+ {
82
+ name: 'freedom_agents',
83
+ description: 'Agent 集成:list 打印支持矩阵;install_skill / install_mcp 把本技能与 MCP 服务装入指定 agent(agent 可为 all)。',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: { action: { type: 'string', enum: ['list', 'install_skill', 'install_mcp'] }, agent: { type: 'string', description: 'agent key 或 all(逗号分隔支持多个)' }, dryRun: { type: 'boolean' } },
87
+ required: ['action'],
88
+ additionalProperties: false,
89
+ },
90
+ args: (a) => {
91
+ if (a.action === 'install_skill') return ['skill', 'install', ...flags(a, { agent: '--agent' }), ...(a.dryRun ? ['--dry-run'] : [])];
92
+ if (a.action === 'install_mcp') return ['mcp', 'install', ...flags(a, { agent: '--agent' }), ...(a.dryRun ? ['--dry-run'] : [])];
93
+ return ['agents'];
94
+ },
95
+ },
96
+ {
97
+ name: 'freedom_guide',
98
+ description: '返回 Freedom 使用指南全文(框架架构、CLI 命令、前端 SDK、后端 NDJSON 协议、打包与发布),供编码前查阅。',
99
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
100
+ guide: true,
101
+ },
102
+ ];
103
+
104
+ function flags(a, map, bools) {
105
+ const out = [];
106
+ for (const [k, flag] of Object.entries(map || {})) {
107
+ if (a[k] !== undefined && a[k] !== '') out.push(flag, String(a[k]));
108
+ }
109
+ for (const [k, flag] of Object.entries(bools || {})) {
110
+ if (a[k] === true) out.push(flag);
111
+ }
112
+ return out;
113
+ }
114
+
115
+ function runCli(args, cwd) {
116
+ return new Promise((resolve) => {
117
+ const child = spawn(process.execPath, [ENTRY, ...args], {
118
+ cwd: cwd && fs.existsSync(cwd) ? cwd : process.cwd(),
119
+ env: { ...process.env, FREEDOM_AUTO_UPDATE: '0', NO_COLOR: '1', FORCE_COLOR: '0' },
120
+ shell: false,
121
+ stdio: ['ignore', 'pipe', 'pipe'],
122
+ });
123
+ let out = '';
124
+ child.stdout.on('data', (c) => { out += String(c); });
125
+ child.stderr.on('data', (c) => { out += String(c); });
126
+ child.on('error', (e) => resolve({ code: -1, text: `启动 CLI 失败:${e.message}` }));
127
+ child.on('close', (code) => resolve({ code: code === null ? -1 : code, text: out.trim() }));
128
+ });
129
+ }
130
+
131
+ function guideText() {
132
+ const f = path.join(packageRoot(), 'skill', 'freedom', 'SKILL.md');
133
+ try {
134
+ return { code: 0, text: fs.readFileSync(f, 'utf8') };
135
+ } catch (e) {
136
+ return { code: 1, text: `指南文件不可读:${f}(${e.message})` };
137
+ }
138
+ }
139
+
140
+ async function callTool(name, args) {
141
+ const tool = TOOLS.find((t) => t.name === name);
142
+ if (!tool) throw new Error(`unknown tool ${name}`);
143
+ const a = args || {};
144
+ const r = tool.guide ? guideText() : await runCli(tool.args(a), tool.cwd ? tool.cwd(a) : undefined);
145
+ const head = tool.guide ? '' : `$ freedom ${tool.args(a).join(' ')}\n`;
146
+ return {
147
+ content: [{ type: 'text', text: `${head}${r.text || '(无输出)'}\n[exit ${r.code}]` }],
148
+ isError: r.code !== 0,
149
+ };
150
+ }
151
+
152
+ function send(msg) {
153
+ process.stdout.write(JSON.stringify(msg) + '\n');
154
+ }
155
+
156
+ async function handle(req) {
157
+ const isRequest = req.id !== undefined && req.id !== null;
158
+ const reply = (result) => isRequest && send({ jsonrpc: '2.0', id: req.id, result });
159
+ const fail = (message) => isRequest && send({ jsonrpc: '2.0', id: req.id, error: { code: -32602, message } });
160
+
161
+ switch (req.method) {
162
+ case 'initialize':
163
+ return reply({
164
+ protocolVersion: PROTOCOL_VERSION,
165
+ capabilities: { tools: { listChanged: false } },
166
+ serverInfo: { name: 'freedom', version: VERSION },
167
+ });
168
+ case 'ping':
169
+ return reply({});
170
+ case 'tools/list':
171
+ return reply({ tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })) });
172
+ case 'tools/call': {
173
+ try {
174
+ return reply(await callTool(String(req.params && req.params.name), req.params && req.params.arguments));
175
+ } catch (e) {
176
+ return reply({ content: [{ type: 'text', text: String((e && e.message) || e) }], isError: true });
177
+ }
178
+ }
179
+ case 'notifications/initialized':
180
+ case 'notifications/cancelled':
181
+ return undefined;
182
+ default:
183
+ if (isRequest) send({ jsonrpc: '2.0', id: req.id, error: { code: -32601, message: `method not found: ${req.method}` } });
184
+ return undefined;
185
+ }
186
+ }
187
+
188
+ async function serve() {
189
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
190
+ // stdin 关闭(客户端断开)时不能立刻退出:在途 tools/call 的子进程还没回帧,
191
+ // 直接 exit 会让客户端收不到最后一个响应。等在途清零后再走。
192
+ let inflight = 0;
193
+ let ended = false;
194
+ const maybeExit = () => { if (ended && inflight === 0) process.exit(0); };
195
+ rl.on('line', (line) => {
196
+ const t = line.trim();
197
+ if (!t) return;
198
+ let req;
199
+ try {
200
+ req = JSON.parse(t);
201
+ } catch (e) {
202
+ send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error' } });
203
+ return;
204
+ }
205
+ inflight++;
206
+ Promise.resolve(handle(req))
207
+ .catch((e) => {
208
+ send({ jsonrpc: '2.0', id: req.id === undefined ? null : req.id, error: { code: -32603, message: String((e && e.message) || e) } });
209
+ })
210
+ .then(() => { inflight--; maybeExit(); });
211
+ });
212
+ rl.on('close', () => { ended = true; maybeExit(); });
213
+ await new Promise(() => { /* 常驻直到 stdin 关闭且在途请求回帧完毕 */ });
214
+ return 0;
215
+ }
216
+
217
+ module.exports = { serve, TOOLS, callTool };
package/lib/release.js ADDED
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ // 应用自更新发布链(对标 tauri signer + updater manifest):
4
+ // freedom keygen —— 生成 ed25519 密钥对;公钥写进 freedom.config.js 的 updater.publicKey,
5
+ // 私钥是发布方资产,绝不入库。
6
+ // freedom manifest —— 对 dist 产物算 sha256,按壳 updater.go 的规范串签名,
7
+ // 产出 latest.json(version/url/sha256/notes/signature)。
8
+ // 签名载荷格式与 Go 侧 manifestPayload 固定一致(改任一字节两侧同炸,见
9
+ // updater_jsinterop_test.go 的跨语言回归):
10
+ // "freedom-update-v1\n{version}\n{url}\n{sha256小写}"
11
+
12
+ const crypto = require('crypto');
13
+ const fs = require('fs');
14
+ const fsp = fs.promises;
15
+ const path = require('path');
16
+
17
+ const { loadConfig } = require('./utils');
18
+
19
+ function manifestPayload(version, url, sha256hex) {
20
+ return Buffer.from(`freedom-update-v1\n${version}\n${url}\n${String(sha256hex).toLowerCase()}`, 'utf8');
21
+ }
22
+
23
+ function keysPath(dir) {
24
+ return path.join(dir, '.freedom', 'keys', 'update_ed25519');
25
+ }
26
+
27
+ async function keygen(opts = {}) {
28
+ const dir = path.resolve(opts.dir || '.');
29
+ const privPath = opts.out ? path.resolve(opts.out) : keysPath(dir);
30
+ if (fs.existsSync(privPath) && !opts.force) {
31
+ throw new Error(`私钥已存在:${privPath}(重新生成请 --force,注意旧公钥签发的客户端将收不到新签名)`);
32
+ }
33
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
34
+ await fsp.mkdir(path.dirname(privPath), { recursive: true });
35
+ await fsp.writeFile(privPath, privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 });
36
+ const rawPub = publicKey.export({ type: 'spki', format: 'der' }).subarray(-32); // SPKI 尾部 32 字节即原始公钥
37
+ const pubB64 = rawPub.toString('base64');
38
+ const lines = [
39
+ `私钥已生成:${privPath}(发布方资产,勿入库/勿分发)`,
40
+ `公钥(base64):${pubB64}`,
41
+ '用法:freedom.config.js 中设置',
42
+ ` updater: { manifestURL: 'https://your.host/latest.json', publicKey: '${pubB64}' }`,
43
+ '发版时:freedom manifest 生成签名 latest.json。',
44
+ ];
45
+ return lines.join('\n');
46
+ }
47
+
48
+ async function sha256File(p) {
49
+ const h = crypto.createHash('sha256');
50
+ await new Promise((resolve, reject) => {
51
+ const s = fs.createReadStream(p);
52
+ s.on('data', (d) => h.update(d));
53
+ s.on('error', reject);
54
+ s.on('end', resolve);
55
+ });
56
+ return h.digest('hex');
57
+ }
58
+
59
+ async function manifest(opts = {}) {
60
+ const dir = path.resolve(opts.dir || '.');
61
+ const cfg = await loadConfig(dir);
62
+ const name = cfg.name || path.basename(dir);
63
+ const artifact = opts.artifact ? path.resolve(opts.artifact) : null;
64
+ if (!artifact || !fs.existsSync(artifact)) {
65
+ throw new Error(`产物不存在:${artifact || '(未指定 --artifact,如 dist/myapp.exe)'}`);
66
+ }
67
+ if (!opts.url || !/^https?:\/\//.test(opts.url)) {
68
+ throw new Error('必须提供 --url(该产物的最终下载 https 地址,签名直接覆盖它)');
69
+ }
70
+ const version = opts.version || cfg.version
71
+ || (() => { try { return require(path.join(dir, 'package.json')).version; } catch { return null; } })();
72
+ if (!version) throw new Error('版本未定:--version / freedom.config.js 的 version / 项目 package.json 的 version 均缺失');
73
+
74
+ const keyPath = opts.key ? path.resolve(opts.key) : keysPath(dir);
75
+ if (!fs.existsSync(keyPath)) {
76
+ throw new Error(`私钥不存在:${keyPath},先运行 freedom keygen`);
77
+ }
78
+ const privateKey = crypto.createPrivateKey(await fsp.readFile(keyPath));
79
+ const sha256 = await sha256File(artifact);
80
+ const signature = crypto.sign(null, manifestPayload(version, opts.url, sha256), privateKey).toString('base64');
81
+ const out = { version, url: opts.url, sha256 };
82
+ if (opts.notes) out.notes = opts.notes;
83
+ out.signature = signature;
84
+ const outPath = opts.out ? path.resolve(opts.out) : path.join(dir, 'dist', 'latest.json');
85
+ await fsp.mkdir(path.dirname(outPath), { recursive: true });
86
+ await fsp.writeFile(outPath, JSON.stringify(out, null, 2) + '\n', 'utf8');
87
+ return `manifest 已签名:${outPath}\n app=${name} version=${version}\n sha256=${sha256}\n url=${opts.url}`;
88
+ }
89
+
90
+ module.exports = { keygen, manifest, manifestPayload };