@yufengtadian/freedom-cli 1.0.0

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 ADDED
@@ -0,0 +1,100 @@
1
+ # freedom-cli
2
+
3
+ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用。
4
+
5
+ 基于自研 Freedom WebView 壳层(对标 Wails / Tauri):前端完全自由、后端可任意语言、渲染复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK),产物为单个可执行文件,前端页面内存加载,不占本地端口。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm install -g @yufengtadian/freedom-cli
11
+ ```
12
+
13
+ 安装完成后会自动弹出使用教程;之后随时可用 `freedom tutorial` 重新查看。
14
+
15
+ ## 快速开始
16
+
17
+ ```bash
18
+ # 1. 新建项目
19
+ freedom init my-app
20
+ cd my-app
21
+ npm install
22
+
23
+ # 2. 调整标题栏(可选,随时可改)
24
+ freedom titlebar native # 保留系统原生标题栏(默认)
25
+ freedom titlebar hidden # 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
26
+ freedom titlebar frameless # 完全无边框,按钮由前端自绘(模板已内置示例)
27
+
28
+ # 3. 打包
29
+ freedom build
30
+ ```
31
+
32
+ 构建产物默认输出到 `dist/`,Windows 下为 `dist/<应用名>.exe`。产物为 GUI 子系统程序,运行时不会弹出 cmd 黑窗。
33
+
34
+ 产物目录可在 `freedom.config.js` 的 `outDir` 中调整:默认 `'dist'`,设为 `'.'` 则直接输出到项目根目录(dist 的上级),设为任意相对 / 绝对路径亦可。输出到项目根目录时会自动跳过 `index.html` 副本,避免覆盖项目源文件。
35
+
36
+ ```bash
37
+ freedom config set outDir . # 产物直接输出到项目根目录
38
+ freedom config set outDir dist # 恢复默认 dist/
39
+ ```
40
+
41
+ ## 标题栏策略
42
+
43
+ | 模式 | 说明 |
44
+ | --- | --- |
45
+ | `native` | 保留系统原生标题栏(默认) |
46
+ | `hidden` | 隐藏标题栏视觉,仅保留 Windows 原生最小化 / 最大化 / 关闭按钮(DWM 扩展实现) |
47
+ | `frameless` | 完全无边框,客户区铺满窗口,最小化 / 最大化 / 关闭按钮由前端自绘 |
48
+
49
+ `hidden` / `frameless` 模式下,前端可通过注入的 `window.freedom.window` API 控制窗口(`minimize` / `maximize` / `toggleMaximize` / `close` / `isMaximized` / `isFrameless`),模板已内置自绘标题栏示例。
50
+
51
+ ## 配置(freedom.config.js)
52
+
53
+ ```js
54
+ export default {
55
+ name: 'my-app', // 应用名 / 窗口标题 / exe 文件名
56
+ width: 1024,
57
+ height: 720,
58
+ minWidth: 400,
59
+ minHeight: 300,
60
+ center: true, // 启动居中
61
+ debug: false, // 开发者工具
62
+ titlebar: 'native', // native | hidden | frameless
63
+ outDir: 'dist', // 产物目录:'dist'(默认)| '.'(项目根目录)| 任意路径
64
+ // backend: { command: 'node', args: ['backend/main.mjs'] }, // 任意语言后端进程
65
+ };
66
+ ```
67
+
68
+ ## 前端
69
+
70
+ 前端是标准 Vite 项目,`vite build` 时通过 `vite-plugin-singlefile` 内联为单个 HTML。壳层注入全局对象 `window.freedom`:
71
+
72
+ ```js
73
+ const r = await window.freedom.call('__freedom__ping'); // 调用后端方法
74
+ window.freedom.on('event', (data) => {}); // 订阅后端事件
75
+ window.freedom.window.minimize(); // 窗口控制
76
+ ```
77
+
78
+ ## 构建要求
79
+
80
+ - Node.js >= 18
81
+ - Go 工具链(编译壳层,需支持 CGO;Windows 下需要 MSVC 编译环境与 WebView2 运行时,Win10/11 自带)
82
+
83
+ ## 命令
84
+
85
+ ```
86
+ freedom init <目录> [--force]
87
+ freedom build
88
+ freedom titlebar <native|hidden|frameless>
89
+ freedom config [get|set]
90
+ freedom tutorial
91
+ freedom help
92
+ ```
93
+
94
+ ## 关于生成产物
95
+
96
+ 本工具生成的所有文件(含模板与构建产物)均干净整洁,无任何冗余尾注,可直接作为交付物使用。
97
+
98
+ ## License
99
+
100
+ MIT
package/bin/freedom.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { run } = require('../lib/cli');
5
+
6
+ run(process.argv.slice(2)).then((code) => {
7
+ process.exit(code || 0);
8
+ }).catch((err) => {
9
+ console.error('[freedom] 执行失败:', err && err.message ? err.message : err);
10
+ process.exit(1);
11
+ });
package/lib/build.js ADDED
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { spawnSync } = require('child_process');
7
+ const { goTemplateDir, copyDir, goJSONString } = require('./utils');
8
+
9
+ const TITLEBAR_MAP = {
10
+ native: 'freedom.TitleBarNative',
11
+ hidden: 'freedom.TitleBarHidden',
12
+ frameless: 'freedom.TitleBarFrameless',
13
+ };
14
+
15
+ function run(cmd, args, opts = {}) {
16
+ // Windows 下 npm 是 .cmd 批处理,必须经 shell 执行
17
+ const isNpmWin = process.platform === 'win32' && cmd === 'npm';
18
+ const realCmd = isNpmWin ? 'npm.cmd' : cmd;
19
+ const res = spawnSync(realCmd, args, {
20
+ stdio: opts.stdio === 'inherit' ? 'inherit' : 'pipe',
21
+ encoding: 'utf8',
22
+ env: process.env,
23
+ shell: isNpmWin,
24
+ ...opts,
25
+ });
26
+ if (res.error) {
27
+ throw new Error(`执行 ${cmd} 失败:${res.error.message}`);
28
+ }
29
+ return res;
30
+ }
31
+
32
+ function checkGo() {
33
+ const res = run('go', ['version']);
34
+ if (res.status !== 0) {
35
+ throw new Error('未检测到 Go 工具链。请先安装 Go(https://go.dev/dl/),并确保 go 命令在 PATH 中。');
36
+ }
37
+ return res.stdout.trim();
38
+ }
39
+
40
+ async function build(projectDir, opts = {}) {
41
+ const dir = path.resolve(projectDir || '.');
42
+ const cfgPath = path.join(dir, 'freedom.config.js');
43
+ if (!fs.existsSync(cfgPath)) {
44
+ throw new Error(`未找到 ${cfgPath},请先运行 freedom init 初始化项目。`);
45
+ }
46
+ const { loadConfig } = require('./utils');
47
+ const cfg = await loadConfig(dir);
48
+
49
+ const name = (cfg.name || 'freedom-app').replace(/[^a-zA-Z0-9_.-]/g, '-');
50
+ const titlebar = TITLEBAR_MAP[cfg.titlebar] || 'freedom.TitleBarNative';
51
+
52
+ // 产物目录:默认 dist,可配置为 '.'(项目根目录,即 dist 的上级)或任意相对/绝对目录。
53
+ const outDir = String(cfg.outDir || 'dist').trim() || 'dist';
54
+ const outDirPath = path.resolve(dir, outDir);
55
+ const exeName = os.platform() === 'win32' ? `${name}.exe` : name;
56
+ const outFile = path.join(outDirPath, exeName);
57
+
58
+ checkGo();
59
+
60
+ // 1) 前端打包:npm install(如缺依赖)+ vite build -> .freedom/vite-dist/index.html
61
+ ensureNodeModules(dir);
62
+ const vite = run('npm', ['run', 'build'], { cwd: dir });
63
+ if (vite.status !== 0) {
64
+ throw new Error(`前端打包失败:\n${vite.stdout}\n${vite.stderr}`);
65
+ }
66
+ const distHtml = path.join(dir, '.freedom', 'vite-dist', 'index.html');
67
+ if (!fs.existsSync(distHtml)) {
68
+ throw new Error(`前端打包完成但未找到 ${distHtml},请检查 vite 配置(vite-plugin-singlefile)。`);
69
+ }
70
+
71
+ // 2) 组装壳层构建目录
72
+ const buildDir = path.join(dir, '.freedom', 'build');
73
+ fs.rmSync(buildDir, { recursive: true, force: true });
74
+ fs.mkdirSync(buildDir, { recursive: true });
75
+ copyDir(goTemplateDir(), buildDir);
76
+
77
+ // 3) 写入前端产物与后端
78
+ fs.copyFileSync(distHtml, path.join(buildDir, 'pkg', 'freedom', 'assets', 'index.html'));
79
+ const backendDir = path.join(dir, cfg.backendDir || 'backend');
80
+ if (cfg.backend && fs.existsSync(backendDir)) {
81
+ copyDir(backendDir, path.join(buildDir, 'backend'));
82
+ }
83
+
84
+ // 4) 生成 gen_config.go
85
+ const genConfig = renderGenConfig(cfg, name, titlebar);
86
+ fs.writeFileSync(path.join(buildDir, 'gen_config.go'), genConfig, 'utf8');
87
+
88
+ // 5) 编译壳层
89
+ fs.mkdirSync(outDirPath, { recursive: true });
90
+ if (process.env.FREEDOM_SKIP_CGO) {
91
+ console.log(`[freedom] FREEDOM_SKIP_CGO=1,跳过 CGO 编译,壳层源码保留在 .freedom/build。`);
92
+ return { outFile: null, buildDir };
93
+ }
94
+ const tidy = run('go', ['mod', 'tidy'], { cwd: buildDir });
95
+ if (tidy.status !== 0) {
96
+ throw new Error(`go mod tidy 失败:\n${tidy.stdout}\n${tidy.stderr}`);
97
+ }
98
+ // Windows 用 GUI 子系统编译(-H windowsgui),运行时不弹出 cmd 黑窗。
99
+ // 注意:go 的 flag 必须在位置参数('.')之前,否则会被当作 import path。
100
+ const ldflags = process.platform === 'win32' ? ['-ldflags', '-H windowsgui'] : [];
101
+ const buildRes = run('go', ['build', ...ldflags, '-o', outFile, '.'], {
102
+ cwd: buildDir,
103
+ env: { ...process.env, CGO_ENABLED: '1' },
104
+ });
105
+ if (buildRes.status !== 0) {
106
+ throw new Error(`Go 编译失败:\n${buildRes.stdout}\n${buildRes.stderr}`);
107
+ }
108
+
109
+ // 6) 产物分发到 outDir:
110
+ // - exe:已输出到 outDir;
111
+ // - 单文件 index.html:outDir 非项目根目录时才写入(避免覆盖项目源文件);
112
+ // - backend:随应用一起分发。
113
+ if (path.resolve(outDirPath) !== dir) {
114
+ fs.copyFileSync(distHtml, path.join(outDirPath, 'index.html'));
115
+ }
116
+ if (cfg.backend && fs.existsSync(backendDir)) {
117
+ fs.mkdirSync(path.join(outDirPath, 'backend'), { recursive: true });
118
+ copyDir(backendDir, path.join(outDirPath, 'backend'));
119
+ }
120
+
121
+ return { outFile, buildDir };
122
+ }
123
+
124
+ function ensureNodeModules(dir) {
125
+ if (fs.existsSync(path.join(dir, 'node_modules'))) return;
126
+ const res = run('npm', ['install'], { cwd: dir, stdio: 'inherit' });
127
+ if (res.status !== 0) {
128
+ throw new Error('npm install 失败。');
129
+ }
130
+ }
131
+
132
+ function renderGenConfig(cfg, name, titlebar) {
133
+ const backendBlock = renderBackend(cfg);
134
+ const lines = [];
135
+ lines.push('package main');
136
+ lines.push('');
137
+ lines.push('import "freedom-cli-shell/pkg/freedom"');
138
+ lines.push('');
139
+ lines.push('// appConfig 返回应用窗口配置。');
140
+ lines.push('// 本文件由 freedom CLI 在 build 阶段根据 freedom.config.js 生成,');
141
+ lines.push('// 不要手动修改(手动修改会在下次 build 时被覆盖)。');
142
+ lines.push('func appConfig() freedom.Config {');
143
+ lines.push(' return freedom.Config{');
144
+ lines.push(` Title: ${goJSONString(cfg.name || name)},`);
145
+ lines.push(` TitleBar: ${titlebar},`);
146
+ lines.push(` Width: ${intVal(cfg.width, 1024)},`);
147
+ lines.push(` Height: ${intVal(cfg.height, 720)},`);
148
+ lines.push(` MinWidth: ${intVal(cfg.minWidth, 0)},`);
149
+ lines.push(` MinHeight: ${intVal(cfg.minHeight, 0)},`);
150
+ lines.push(` Center: ${boolVal(cfg.center, true)},`);
151
+ lines.push(` Debug: ${boolVal(cfg.debug, false)},`);
152
+ lines.push(' }');
153
+ lines.push('}');
154
+ lines.push('');
155
+ return lines.join('\n');
156
+ }
157
+
158
+ function renderBackend(cfg) {
159
+ // 当前版本:后端进程方式由用户在 main 模板内手动接线;
160
+ // gen_config 仅承载窗口配置。此处预留注释说明。
161
+ return '';
162
+ }
163
+
164
+ function intVal(v, dft) {
165
+ const n = parseInt(v, 10);
166
+ return Number.isFinite(n) && n >= 0 ? n : dft;
167
+ }
168
+
169
+ function boolVal(v, dft) {
170
+ return typeof v === 'boolean' ? v : dft;
171
+ }
172
+
173
+ module.exports = { build };
package/lib/cli.js ADDED
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { init } = require('./init');
6
+ const { build } = require('./build');
7
+ const { setConfig, showConfig } = require('./config');
8
+ const { packageRoot, tutorialFile } = require('./utils');
9
+
10
+ const VERSION = require(path.join(packageRoot(), 'package.json')).version;
11
+
12
+ function help() {
13
+ return `
14
+ freedom - Freedom 桌面壳打包工具 v${VERSION}
15
+
16
+ 用法:
17
+ freedom init [目录] [--force] 在当前/指定目录新建项目模板
18
+ freedom build 前端打包并编译出桌面应用(产物在 dist/)
19
+ freedom titlebar <native|hidden|frameless>
20
+ 一键切换标题栏策略
21
+ freedom config 查看当前配置
22
+ freedom config get <key> 读取单个配置项
23
+ freedom config set <key> <value> 修改单个配置项
24
+ freedom tutorial 再次打开安装教程
25
+ freedom help 显示本帮助
26
+ freedom version 显示版本
27
+
28
+ 标题栏策略说明:
29
+ native 保留系统原生标题栏(默认)
30
+ hidden 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
31
+ frameless 完全无边框,按钮由前端自绘(模板已内置示例)
32
+ `.trim();
33
+ }
34
+
35
+ async function run(argv) {
36
+ const [cmd, ...rest] = argv;
37
+
38
+ switch (cmd) {
39
+ case undefined:
40
+ case 'help':
41
+ case '--help':
42
+ case '-h':
43
+ console.log(help());
44
+ return 0;
45
+
46
+ case 'version':
47
+ case '--version':
48
+ case '-v':
49
+ console.log(VERSION);
50
+ return 0;
51
+
52
+ case 'init': {
53
+ const force = rest.includes('--force');
54
+ const dirArg = rest.filter((a) => a !== '--force')[0];
55
+ const dir = init(dirArg || '.', { force });
56
+ console.log(`[freedom] 项目已创建:${dir}`);
57
+ console.log(' 下一步:');
58
+ console.log(` cd ${dir}`);
59
+ console.log(' npm install');
60
+ console.log(' freedom build');
61
+ return 0;
62
+ }
63
+
64
+ case 'build': {
65
+ const { outFile } = await build(process.cwd());
66
+ if (outFile) {
67
+ console.log(`[freedom] 构建完成:${outFile}`);
68
+ }
69
+ return 0;
70
+ }
71
+
72
+ case 'titlebar': {
73
+ const mode = rest[0];
74
+ if (!['native', 'hidden', 'frameless'].includes(mode)) {
75
+ console.error('[freedom] 用法:freedom titlebar <native|hidden|frameless>');
76
+ return 1;
77
+ }
78
+ setConfig(process.cwd(), 'titlebar', mode);
79
+ console.log(`[freedom] titlebar 已切换为:${mode}`);
80
+ console.log(' 运行 freedom build 重新打包生效。');
81
+ return 0;
82
+ }
83
+
84
+ case 'config': {
85
+ const sub = rest[0];
86
+ if (sub === 'get') {
87
+ const cfg = await showConfig(process.cwd());
88
+ console.log(cfg);
89
+ return 0;
90
+ }
91
+ if (sub === 'set') {
92
+ const key = rest[1];
93
+ const value = rest[2];
94
+ if (!key || value === undefined) {
95
+ console.error('[freedom] 用法:freedom config set <key> <value>');
96
+ return 1;
97
+ }
98
+ setConfig(process.cwd(), key, coerce(value));
99
+ console.log(`[freedom] ${key} = ${coerce(value)}`);
100
+ return 0;
101
+ }
102
+ console.log(await showConfig(process.cwd()));
103
+ return 0;
104
+ }
105
+
106
+ case 'tutorial': {
107
+ const file = tutorialFile();
108
+ openBrowser(file);
109
+ console.log(`[freedom] 教程已打开:${file}`);
110
+ return 0;
111
+ }
112
+
113
+ default:
114
+ console.error(`[freedom] 未知命令:${cmd}\n`);
115
+ console.log(help());
116
+ return 1;
117
+ }
118
+ }
119
+
120
+ function coerce(value) {
121
+ if (value === 'true') return true;
122
+ if (value === 'false') return false;
123
+ const num = Number(value);
124
+ if (value !== '' && Number.isFinite(num) && String(num) === value.trim()) {
125
+ return num;
126
+ }
127
+ return value;
128
+ }
129
+
130
+ function openBrowser(file) {
131
+ const { spawn } = require('child_process');
132
+ const plat = process.platform;
133
+ const url = `file://${file.replace(/\\/g, '/')}`;
134
+ try {
135
+ if (plat === 'win32') {
136
+ spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
137
+ } else if (plat === 'darwin') {
138
+ spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
139
+ } else {
140
+ spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
141
+ }
142
+ } catch (e) { /* 打开失败静默 */ }
143
+ }
144
+
145
+ module.exports = { run };
package/lib/config.js ADDED
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { loadConfig, hasConfig } = require('./utils');
6
+
7
+ // 配置文件中可安全写入的字段及其默认值(用于渲染或回填)。
8
+ const KNOWN_KEYS = {
9
+ name: 'freedom-app',
10
+ width: 1024,
11
+ height: 720,
12
+ minWidth: 400,
13
+ minHeight: 300,
14
+ center: true,
15
+ debug: false,
16
+ titlebar: 'native',
17
+ outDir: 'dist',
18
+ };
19
+
20
+ // 把配置写回 freedom.config.js(保留注释,只替换已知键的取值)。
21
+ function setConfig(dir, key, value) {
22
+ const cfgPath = path.join(dir, 'freedom.config.js');
23
+ if (!fs.existsSync(cfgPath)) {
24
+ throw new Error('未找到 freedom.config.js。');
25
+ }
26
+ const text = fs.readFileSync(cfgPath, 'utf8');
27
+
28
+ if (key === 'titlebar') {
29
+ const modes = ['native', 'hidden', 'frameless'];
30
+ if (!modes.includes(value)) {
31
+ throw new Error(`titlebar 取值必须为:${modes.join(' / ')}。`);
32
+ }
33
+ }
34
+
35
+ const regex = new RegExp(`(${key}\\s*:\\s*)('[^']*'|\\d+|true|false|undefined|null)`, 'g');
36
+ if (!regex.test(text)) {
37
+ throw new Error(`配置项 ${key} 未在 freedom.config.js 中找到,请手动添加。`);
38
+ }
39
+
40
+ let rendered = String(value);
41
+ if (typeof value === 'string') {
42
+ rendered = `'${value.replace(/'/g, "\\'")}'`;
43
+ }
44
+
45
+ const updated = text.replace(regex, `$1${rendered}`);
46
+ fs.writeFileSync(cfgPath, updated, 'utf8');
47
+ return value;
48
+ }
49
+
50
+ async function getConfig(dir) {
51
+ if (!hasConfig(dir)) {
52
+ throw new Error('当前目录不是 Freedom 项目(缺少 freedom.config.js)。');
53
+ }
54
+ const cfg = await loadConfig(dir);
55
+ return cfg;
56
+ }
57
+
58
+ async function showConfig(dir) {
59
+ const cfg = await loadConfig(dir);
60
+ const lines = Object.keys(KNOWN_KEYS).map((k) => {
61
+ const v = cfg[k] === undefined ? KNOWN_KEYS[k] : cfg[k];
62
+ return ` ${k}: ${JSON.stringify(v)}`;
63
+ });
64
+ lines.push(` backend: ${cfg.backend ? JSON.stringify(cfg.backend) : 'undefined'}`);
65
+ return lines.join('\n');
66
+ }
67
+
68
+ module.exports = { setConfig, getConfig, showConfig, KNOWN_KEYS };
package/lib/init.js ADDED
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { projectTemplateDir, copyDir } = require('./utils');
6
+
7
+ function init(targetDir, opts = {}) {
8
+ const dir = targetDir || '.';
9
+ const abs = path.resolve(dir);
10
+
11
+ if (fs.existsSync(abs) && fs.readdirSync(abs).length > 0 && !opts.force) {
12
+ throw new Error(`目标目录 ${abs} 非空,请使用空目录,或加 --force 覆盖。`);
13
+ }
14
+
15
+ fs.mkdirSync(abs, { recursive: true });
16
+ copyDir(projectTemplateDir(), abs);
17
+
18
+ // 项目名替换到 package.json 与 freedom.config.js
19
+ const name = opts.name || path.basename(abs);
20
+ patchName(abs, name);
21
+
22
+ return abs;
23
+ }
24
+
25
+ function patchName(dir, name) {
26
+ const safeName = String(name).replace(/[^a-zA-Z0-9_.-]/g, '-').toLowerCase();
27
+
28
+ const pkgPath = path.join(dir, 'package.json');
29
+ if (fs.existsSync(pkgPath)) {
30
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
31
+ pkg.name = safeName || 'freedom-app';
32
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
33
+ }
34
+
35
+ const cfgPath = path.join(dir, 'freedom.config.js');
36
+ if (fs.existsSync(cfgPath)) {
37
+ let text = fs.readFileSync(cfgPath, 'utf8');
38
+ text = text.replace(/name:\s*'[^']*'/, `name: '${safeName || 'freedom-app'}'`);
39
+ fs.writeFileSync(cfgPath, text, 'utf8');
40
+ }
41
+ }
42
+
43
+ module.exports = { init };
package/lib/utils.js ADDED
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const PKG_ROOT = path.resolve(__dirname, '..');
7
+
8
+ function packageRoot() {
9
+ return PKG_ROOT;
10
+ }
11
+
12
+ function templateDir() {
13
+ return path.join(PKG_ROOT, 'templates');
14
+ }
15
+
16
+ function projectTemplateDir() {
17
+ return path.join(templateDir(), 'project');
18
+ }
19
+
20
+ function goTemplateDir() {
21
+ return path.join(templateDir(), 'go');
22
+ }
23
+
24
+ function tutorialFile() {
25
+ return path.join(PKG_ROOT, 'tutorial', 'tutorial.html');
26
+ }
27
+
28
+ async function loadConfig(dir) {
29
+ const cfgPath = path.join(dir, 'freedom.config.js');
30
+ if (!fs.existsSync(cfgPath)) {
31
+ throw new Error('未找到 freedom.config.js,请先在项目根目录运行 freedom init 或创建该文件。');
32
+ }
33
+ // 动态 import 以兼容 ESM / CJS 两种书写方式,带时间戳防缓存
34
+ const url = require('url').pathToFileURL(cfgPath).href + '?t=' + Date.now();
35
+ const mod = await import(url);
36
+ return mod.default || mod;
37
+ }
38
+
39
+ function hasConfig(dir) {
40
+ return fs.existsSync(path.join(dir, 'freedom.config.js'));
41
+ }
42
+
43
+ function copyDir(src, dest) {
44
+ fs.mkdirSync(dest, { recursive: true });
45
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
46
+ const s = path.join(src, entry.name);
47
+ const d = path.join(dest, entry.name);
48
+ if (entry.isDirectory()) {
49
+ copyDir(s, d);
50
+ } else {
51
+ fs.copyFileSync(s, d);
52
+ }
53
+ }
54
+ }
55
+
56
+ function goJSONString(v) {
57
+ return JSON.stringify(String(v));
58
+ }
59
+
60
+ module.exports = {
61
+ packageRoot,
62
+ templateDir,
63
+ projectTemplateDir,
64
+ goTemplateDir,
65
+ tutorialFile,
66
+ loadConfig,
67
+ hasConfig,
68
+ copyDir,
69
+ goJSONString,
70
+ };
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@yufengtadian/freedom-cli",
3
+ "version": "1.0.0",
4
+ "description": "Freedom WebView desktop shell packaging tool - embed any web frontend into a cross-platform desktop app",
5
+ "keywords": ["desktop", "webview", "electron-alternative", "wails", "tauri", "go", "frontend"],
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "bin": {
11
+ "freedom": "bin/freedom.js"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "lib",
16
+ "templates",
17
+ "tutorial",
18
+ "postinstall.js",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "postinstall": "node postinstall.js"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ }
27
+ }
package/postinstall.js ADDED
@@ -0,0 +1,36 @@
1
+ // 安装后脚本:首次安装自动弹出 Freedom CLI 使用教程。
2
+ // 环境变量 FREEDOM_NO_TUTORIAL=1 可跳过(CI 或脚本化安装场景)。
3
+ 'use strict';
4
+
5
+ const path = require('path');
6
+ const { spawn } = require('child_process');
7
+
8
+ if (process.env.FREEDOM_NO_TUTORIAL === '1') {
9
+ process.exit(0);
10
+ }
11
+
12
+ const file = path.join(__dirname, 'tutorial', 'tutorial.html');
13
+ const url = 'file://' + file.replace(/\\/g, '/');
14
+ const plat = process.platform;
15
+
16
+ function tryOpen(cmd, args) {
17
+ try {
18
+ const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
19
+ child.on('error', () => {});
20
+ child.unref();
21
+ } catch (e) { /* 打开失败静默 */ }
22
+ }
23
+
24
+ if (plat === 'win32') {
25
+ tryOpen('cmd', ['/c', 'start', '', url]);
26
+ } else if (plat === 'darwin') {
27
+ tryOpen('open', [url]);
28
+ } else {
29
+ tryOpen('xdg-open', [url]);
30
+ }
31
+
32
+ console.log('');
33
+ console.log('Freedom CLI 已安装。教程窗口已打开,也可以随时运行 freedom tutorial 重新查看。');
34
+ console.log('快速开始:');
35
+ console.log(' freedom init my-app && cd my-app && npm install && freedom build');
36
+ console.log('');