@htyf-mp/cli 0.1.3 → 0.2.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/logger.mjs ADDED
@@ -0,0 +1,72 @@
1
+ import chalk from 'chalk';
2
+ import fse from 'fs-extra';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { CONSTANTS, LOG_LEVELS } from './constants.mjs';
6
+
7
+ // 当前日志级别
8
+ let currentLogLevel = LOG_LEVELS.INFO;
9
+
10
+ /**
11
+ * 设置日志级别
12
+ * @param {number} level - 日志级别
13
+ */
14
+ export function setLogLevel(level) {
15
+ currentLogLevel = level;
16
+ }
17
+
18
+ /**
19
+ * 获取当前日志级别
20
+ * @returns {number} 当前日志级别
21
+ */
22
+ export function getLogLevel() {
23
+ return currentLogLevel;
24
+ }
25
+
26
+ /**
27
+ * 日志工具类
28
+ */
29
+ export class Logger {
30
+ static debug(message, ...args) {
31
+ if (currentLogLevel <= LOG_LEVELS.DEBUG) {
32
+ console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
33
+ }
34
+ }
35
+
36
+ static info(message, ...args) {
37
+ if (currentLogLevel <= LOG_LEVELS.INFO) {
38
+ console.log(chalk.blue(`[INFO] ${message}`), ...args);
39
+ }
40
+ }
41
+
42
+ static warn(message, ...args) {
43
+ if (currentLogLevel <= LOG_LEVELS.WARN) {
44
+ console.log(chalk.yellow(`[WARN] ${message}`), ...args);
45
+ }
46
+ }
47
+
48
+ static error(message, ...args) {
49
+ if (currentLogLevel <= LOG_LEVELS.ERROR) {
50
+ console.log(chalk.red(`[ERROR] ${message}`), ...args);
51
+ }
52
+ }
53
+
54
+ static success(message, ...args) {
55
+ console.log(chalk.green(`[SUCCESS] ${message}`), ...args);
56
+ }
57
+
58
+ static writeToFile(message, level = 'INFO') {
59
+ const logDir = CONSTANTS.LOGS_DIR;
60
+ const logFile = path.join(logDir, `cli-${new Date().toISOString().split('T')[0]}.log`);
61
+
62
+ try {
63
+ fse.ensureDirSync(logDir);
64
+ const timestamp = new Date().toISOString();
65
+ const logEntry = `[${timestamp}] [${level}] ${message}\n`;
66
+ fs.appendFileSync(logFile, logEntry);
67
+ } catch (error) {
68
+ console.error('写入日志文件失败:', error.message);
69
+ }
70
+ }
71
+ }
72
+
@@ -0,0 +1,273 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import fse from 'fs-extra';
5
+ import md5 from 'md5';
6
+ import lodash from 'lodash';
7
+ import ora from 'ora';
8
+ import boxen from 'boxen';
9
+ import chalk from 'chalk';
10
+ import gradient from 'gradient-string';
11
+ import { input, confirm } from '@inquirer/prompts';
12
+ import select from '@inquirer/select';
13
+ import { Logger } from './logger.mjs';
14
+ import { CONSTANTS } from './constants.mjs';
15
+ import { ProjectConfig } from './config.mjs';
16
+ import { TemplateProcessor } from './template-processor.mjs';
17
+ import { FileSystemUtils } from './file-system.mjs';
18
+
19
+ /**
20
+ * 项目初始化器类
21
+ */
22
+ export class ProjectInitializer {
23
+ constructor() {
24
+ this.config = new ProjectConfig();
25
+ this.processor = new TemplateProcessor(this.config);
26
+ }
27
+
28
+ async initialize() {
29
+ const spinner = ora('正在初始化项目...');
30
+ let tmpdir = null;
31
+ let rootPath = null;
32
+
33
+ // 信号处理:清理临时文件并停止 spinner
34
+ const handleExit = () => {
35
+ spinner.stop();
36
+ Logger.info('\n正在清理临时文件...');
37
+ if (tmpdir && fs.existsSync(tmpdir)) {
38
+ try {
39
+ fse.removeSync(tmpdir);
40
+ Logger.info('临时文件已清理');
41
+ } catch (error) {
42
+ Logger.warn(`清理临时文件时出错: ${error.message}`);
43
+ }
44
+ }
45
+ Logger.info('已取消项目初始化');
46
+ process.exit(0);
47
+ };
48
+
49
+ process.once('SIGINT', handleExit);
50
+ process.once('SIGTERM', handleExit);
51
+
52
+ try {
53
+ // 1. 获取用户输入
54
+ const userInputs = await this.getUserInputs();
55
+
56
+ // 2. 验证输入
57
+ const validation = this.validateInputs(userInputs);
58
+ if (!validation.isValid) {
59
+ process.removeListener('SIGINT', handleExit);
60
+ process.removeListener('SIGTERM', handleExit);
61
+ spinner.fail('输入验证失败');
62
+ Logger.error(validation.error);
63
+ return;
64
+ }
65
+
66
+ // 3. 检查目录
67
+ rootPath = path.join(process.cwd(), userInputs.appName);
68
+ if (fs.existsSync(rootPath)) {
69
+ process.removeListener('SIGINT', handleExit);
70
+ process.removeListener('SIGTERM', handleExit);
71
+ spinner.fail('目录已存在');
72
+ Logger.error(`目录已存在: ${rootPath}`);
73
+ return;
74
+ }
75
+
76
+ // 4. 创建项目(在此之前启动 spinner)
77
+ spinner.start();
78
+ spinner.text = '正在初始化项目...';
79
+ // 传递 tmpdir 引用以便清理
80
+ await this.createProject(userInputs, rootPath, spinner, (dir) => {
81
+ tmpdir = dir;
82
+ });
83
+
84
+ // 移除信号监听器
85
+ process.removeListener('SIGINT', handleExit);
86
+ process.removeListener('SIGTERM', handleExit);
87
+
88
+ // 5. 显示成功信息
89
+ this.showSuccessInfo(userInputs.appName, rootPath, userInputs.templateType);
90
+
91
+ } catch (error) {
92
+ process.removeListener('SIGINT', handleExit);
93
+ process.removeListener('SIGTERM', handleExit);
94
+ spinner.fail('项目初始化失败');
95
+ Logger.error('初始化失败:', error.message);
96
+ if (error.exitCode === 128) {
97
+ Logger.error('Error: 目录已经存在。');
98
+ } else if (error.code === 'ENOTFOUND') {
99
+ Logger.error('网络连接失败,请检查网络连接或尝试使用其他镜像源。');
100
+ } else if (error.code === 'ECONNREFUSED') {
101
+ Logger.error('连接被拒绝,请检查防火墙设置或网络配置。');
102
+ }
103
+ }
104
+ }
105
+
106
+ async getUserInputs() {
107
+ Logger.info('\n开始收集项目信息...\n');
108
+
109
+ const appName = await input({
110
+ message: '输入应用程序目录名称',
111
+ default: 'my-htyf-mp',
112
+ validate: (input) => {
113
+ if (!this.config.validateAppName(input)) {
114
+ return '请以 my-app-name 格式输入您的应用程序英文目录名称';
115
+ }
116
+ return true;
117
+ }
118
+ });
119
+
120
+ const displayName = await input({
121
+ message: '输入应用程序名称',
122
+ default: '小程序',
123
+ validate: (input) => {
124
+ if (!this.config.validateDisplayName(input)) {
125
+ return '只能中文、字母和数字, 不能包含特殊字符, 限2-4个字符';
126
+ }
127
+ return true;
128
+ }
129
+ });
130
+
131
+ const templateType = await select({
132
+ message: '请选择模板类型?',
133
+ choices: Object.entries(this.config.templates).map(([key, template]) => ({
134
+ name: template.name,
135
+ value: key
136
+ }))
137
+ });
138
+
139
+ Logger.info(`已选择模板: ${this.config.templates[templateType].name}`);
140
+
141
+ const repoType = await select({
142
+ message: '请选择模板镜像?',
143
+ choices: [
144
+ { name: 'GitHub (最新)', value: CONSTANTS.TEMPLATE_REPOS.GITHUB },
145
+ ]
146
+ });
147
+
148
+ return { appName, displayName, templateType, repoType };
149
+ }
150
+
151
+ validateInputs(inputs) {
152
+ const pathValidation = FileSystemUtils.validatePath(inputs.appName);
153
+ if (!pathValidation.isValid) {
154
+ return { isValid: false, error: pathValidation.error };
155
+ }
156
+
157
+ return { isValid: true };
158
+ }
159
+
160
+ async createProject(userInputs, rootPath, spinner, setTmpdirCallback = null) {
161
+ const { appName, displayName, templateType, repoType } = userInputs;
162
+
163
+ // 创建临时目录
164
+ spinner.text = '正在准备临时目录...';
165
+ const tmpdir = path.join(os.tmpdir(), md5('__HTYF__'), appName);
166
+ fse.emptyDirSync(tmpdir);
167
+
168
+ // 设置临时目录引用,以便在退出时清理
169
+ if (setTmpdirCallback) {
170
+ setTmpdirCallback(tmpdir);
171
+ }
172
+
173
+ try {
174
+ // 克隆仓库
175
+ spinner.text = '正在克隆模板仓库...';
176
+ await this.processor.cloneRepository(repoType, tmpdir);
177
+
178
+ // 确定应用根路径
179
+ const template = this.config.templates[templateType];
180
+ const appRootPath = path.join(tmpdir, template.tempPath);
181
+
182
+ // 清理不需要的文件
183
+ spinner.text = '正在清理模板文件...';
184
+ this.processor.cleanupUnusedTemplates(tmpdir, templateType);
185
+
186
+ // 处理模板
187
+ spinner.text = '正在处理模板...';
188
+ this.processor.processTemplate(appRootPath, templateType);
189
+
190
+ // 生成配置
191
+ spinner.text = '正在生成项目配置...';
192
+ const appid = this.config.generateAppId();
193
+ const projectConfig = this.config.createProjectConfig(appName, displayName, appid);
194
+
195
+ // 写入配置文件
196
+ const configPath = path.join(appRootPath, 'htyf.json');
197
+ fs.writeFileSync(configPath, JSON.stringify(projectConfig, undefined, 2));
198
+
199
+ // 清理临时文件
200
+ spinner.text = '正在清理临时文件...';
201
+ const tempPath = path.join(appRootPath, '__TEMP__');
202
+ if (fs.existsSync(tempPath)) {
203
+ fse.removeSync(tempPath);
204
+ }
205
+
206
+ // 移动到目标目录
207
+ spinner.text = '正在创建项目目录...';
208
+
209
+ if (fs.existsSync(rootPath)) {
210
+ fse.ensureDirSync(rootPath);
211
+ const items = fs.readdirSync(appRootPath);
212
+ items.forEach((item) => {
213
+ const source = path.join(appRootPath, item);
214
+ const destination = path.join(rootPath, item);
215
+ fse.moveSync(source, destination, { overwrite: true });
216
+ });
217
+ fse.removeSync(appRootPath);
218
+ } else {
219
+ fse.moveSync(appRootPath, rootPath);
220
+ }
221
+
222
+ // 清理临时目录
223
+ fse.removeSync(tmpdir);
224
+
225
+ spinner.succeed('项目创建完成');
226
+
227
+ } catch (error) {
228
+ // 清理临时目录
229
+ if (fs.existsSync(tmpdir)) {
230
+ fse.removeSync(tmpdir);
231
+ }
232
+ throw error;
233
+ }
234
+ }
235
+
236
+ showSuccessInfo(appName, rootPath, templateType) {
237
+ console.log('\n' + boxen(
238
+ gradient.rainbow('🎉 项目创建成功!') + '\n\n' +
239
+ chalk.cyan('项目名称: ') + chalk.white(appName) + '\n' +
240
+ chalk.cyan('项目路径: ') + chalk.white(rootPath) + '\n\n' +
241
+ chalk.yellow('下一步操作:') + '\n' +
242
+ (() => {
243
+ const relativePath = path.relative(process.cwd(), rootPath);
244
+ const commands = [];
245
+ if (relativePath) {
246
+ commands.push(chalk.white(` cd ${relativePath}`));
247
+ }
248
+
249
+ if (templateType === CONSTANTS.TEMPLATE_TYPES.GAME_TEMPLATE) {
250
+ commands.push(chalk.white(' npm install'));
251
+ } else {
252
+ commands.push(
253
+ chalk.white(' npm install'),
254
+ chalk.white(' npm run ios'),
255
+ chalk.white(' npm run android')
256
+ );
257
+ }
258
+
259
+ return commands.join('\n');
260
+ })() + '\n\n' +
261
+ chalk.blue('常用命令:') + '\n' +
262
+ chalk.white(' npm run htyf # 构建红糖云服小程序') + '\n' +
263
+ chalk.gray('💡 提示: 使用 --debug 参数可以查看详细的调试信息'),
264
+ {
265
+ padding: 1,
266
+ margin: 1,
267
+ borderStyle: 'round',
268
+ borderColor: 'green'
269
+ }
270
+ ));
271
+ }
272
+ }
273
+
@@ -0,0 +1,81 @@
1
+ {
2
+ "@bam.tech/react-native-image-resizer": "3.0.11",
3
+ "@dr.pogodin/react-native-fs": "2.34.0",
4
+ "@dr.pogodin/react-native-static-server": "0.22.0",
5
+ "@react-native-async-storage/async-storage": "2.2.0",
6
+ "@react-native-camera-roll/camera-roll": "7.10.1",
7
+ "@react-native-clipboard/clipboard": "1.16.3",
8
+ "@react-native-community/blur": "4.4.1",
9
+ "@react-native-community/checkbox": "0.5.20",
10
+ "@react-native-community/datetimepicker": "8.4.2",
11
+ "@react-native-community/geolocation": "3.4.0",
12
+ "@react-native-community/netinfo": "11.4.1",
13
+ "@react-native-community/slider": "4.5.7",
14
+ "@react-native-cookies/cookies": "6.2.1",
15
+ "@react-native-documents/picker": "10.1.5",
16
+ "@react-native-masked-view/masked-view": "0.3.2",
17
+ "@react-native-picker/picker": "2.11.1",
18
+ "@react-native-segmented-control/segmented-control": "2.5.7",
19
+ "@shopify/flash-list": "1.8.3",
20
+ "@shopify/react-native-skia": "2.1.1",
21
+ "expo": "53.0.20",
22
+ "expo-asset": "11.1.7",
23
+ "expo-audio": "0.4.8",
24
+ "expo-brightness": "13.1.4",
25
+ "expo-camera": "16.1.11",
26
+ "expo-clipboard": "7.1.5",
27
+ "expo-constants": "17.1.7",
28
+ "expo-crypto": "14.1.5",
29
+ "expo-font": "13.3.2",
30
+ "expo-image": "2.4.0",
31
+ "expo-linking": "7.1.7",
32
+ "expo-router": "5.1.4",
33
+ "expo-system-ui": "5.0.10",
34
+ "expo-video": "2.2.2",
35
+ "jcore-react-native": "2.3.1",
36
+ "jpush-react-native": "3.2.0",
37
+ "llama.rn": "0.6.2",
38
+ "lottie-react-native": "7.2.4",
39
+ "open-im-sdk-rn": "3.8.3-patch.3",
40
+ "react-airplay": "1.2.0",
41
+ "react-native-avoid-softinput": "7.0.2",
42
+ "react-native-background-actions": "4.0.1",
43
+ "react-native-ble-manager": "12.1.5",
44
+ "react-native-blob-util": "0.22.2",
45
+ "react-native-bootsplash": "6.3.10",
46
+ "react-native-compressor": "1.12.0",
47
+ "react-native-config": "1.5.5",
48
+ "react-native-device-info": "14.0.4",
49
+ "react-native-edge-to-edge": "1.6.2",
50
+ "react-native-fast-image": "8.6.3",
51
+ "react-native-gesture-handler": "2.27.2",
52
+ "react-native-get-random-values": "1.11.0",
53
+ "react-native-google-mobile-ads": "15.4.0",
54
+ "react-native-haptic-feedback": "2.3.3",
55
+ "react-native-iap": "13.0.1",
56
+ "react-native-image-picker": "8.2.1",
57
+ "react-native-keyboard-controller": "1.18.0",
58
+ "react-native-linear-gradient": "2.8.3",
59
+ "react-native-localize": "3.5.1",
60
+ "react-native-mmkv": "3.3.0",
61
+ "react-native-orientation-locker": "1.7.0",
62
+ "react-native-permissions": "5.4.2",
63
+ "react-native-quick-base64": "2.2.1",
64
+ "react-native-quick-crypto": "0.7.15",
65
+ "react-native-reanimated": "3.19.0",
66
+ "react-native-restart": "0.0.27",
67
+ "react-native-safe-area-context": "5.5.2",
68
+ "react-native-screens": "4.13.1",
69
+ "react-native-share": "12.1.0",
70
+ "react-native-svg": "15.13.0",
71
+ "react-native-track-player": "4.1.1",
72
+ "react-native-vector-icons": "10.2.0",
73
+ "react-native-video": "6.16.1",
74
+ "react-native-view-shot": "4.0.3",
75
+ "react-native-vlc-media-player": "1.0.89",
76
+ "react-native-volume-manager": "2.0.8",
77
+ "react-native-webview": "13.15.0",
78
+ "react-native-wgpu": "0.2.0",
79
+ "react-native-wifi-reborn": "4.13.6",
80
+ "react-native-zip-archive": "7.0.2"
81
+ }
@@ -0,0 +1,137 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import fse from 'fs-extra';
4
+ import { execa } from 'execa';
5
+ import { Logger } from './logger.mjs';
6
+ import { CONSTANTS } from './constants.mjs';
7
+
8
+ /**
9
+ * 模板处理器类
10
+ */
11
+ export class TemplateProcessor {
12
+ constructor(config) {
13
+ this.config = config;
14
+ }
15
+
16
+ async cloneRepository(repoUrl, tmpdir) {
17
+ Logger.info('正在克隆模板仓库...');
18
+ Logger.debug(`仓库地址: ${repoUrl}`);
19
+ Logger.debug(`目标目录: ${tmpdir}`);
20
+
21
+ try {
22
+ await execa('git', ['clone', '--depth', '1', repoUrl, tmpdir], {
23
+ timeout: 60000 // 60秒超时
24
+ });
25
+ Logger.success('模板仓库克隆完成');
26
+ } catch (error) {
27
+ if (error.code === 'ENOTFOUND') {
28
+ throw new Error('网络连接失败,请检查网络连接或尝试使用其他镜像源');
29
+ } else if (error.code === 'ECONNREFUSED') {
30
+ throw new Error('连接被拒绝,请检查防火墙设置或网络配置');
31
+ } else if (error.exitCode === 128) {
32
+ throw new Error('Git仓库访问失败,请检查仓库地址是否正确');
33
+ } else if (error.signal === 'SIGTERM' || error.timedOut) {
34
+ throw new Error('克隆操作超时,请检查网络连接或稍后重试');
35
+ } else {
36
+ throw new Error(`克隆失败: ${error.message}`);
37
+ }
38
+ }
39
+ }
40
+
41
+ cleanupUnusedTemplates(tmpdir, templateType) {
42
+ const pathsToRemove = [
43
+ path.join(tmpdir, 'cli'),
44
+ path.join(tmpdir, '.git'),
45
+ path.join(tmpdir, 'docs'),
46
+ path.join(tmpdir, 'README.md')
47
+ ];
48
+
49
+ // 根据模板类型移除不需要的模板
50
+ if (templateType === CONSTANTS.TEMPLATE_TYPES.TARO || templateType === CONSTANTS.TEMPLATE_TYPES.EXPO) {
51
+ pathsToRemove.push(path.join(tmpdir, 'mini-game-template'));
52
+ } else if (templateType === CONSTANTS.TEMPLATE_TYPES.GAME_COCOS) {
53
+ pathsToRemove.push(path.join(tmpdir, 'mini-apps-template'));
54
+ }
55
+
56
+ pathsToRemove.forEach(pathToRemove => {
57
+ try {
58
+ if (fs.existsSync(pathToRemove)) {
59
+ fse.removeSync(pathToRemove);
60
+ Logger.debug(`已移除: ${pathToRemove}`);
61
+ }
62
+ } catch (err) {
63
+ Logger.warn(`移除失败: ${pathToRemove}`, err.message);
64
+ }
65
+ });
66
+ }
67
+
68
+ processTemplate(appRootPath, templateType) {
69
+ const template = this.config.templates[templateType];
70
+ if (!template) {
71
+ throw new Error(`不支持的模板类型: ${templateType}`);
72
+ }
73
+
74
+ const tempPath = path.join(appRootPath, '__TEMP__');
75
+
76
+ if (templateType === CONSTANTS.TEMPLATE_TYPES.TARO) {
77
+ this.processTaroTemplate(appRootPath, tempPath, template);
78
+ } else if (templateType === CONSTANTS.TEMPLATE_TYPES.EXPO) {
79
+ this.processExpoTemplate(appRootPath, tempPath, template);
80
+ } else if (templateType === CONSTANTS.TEMPLATE_TYPES.GAME_COCOS) {
81
+ // 预留游戏模板处理
82
+ Logger.info('游戏模板处理功能开发中...');
83
+ }
84
+ }
85
+
86
+ processTaroTemplate(appRootPath, tempPath, template) {
87
+ Logger.info('正在处理 Taro 模板...');
88
+
89
+ // 复制模板文件
90
+ Object.entries(template.files).forEach(([key, fileName]) => {
91
+ const srcPath = path.join(tempPath, fileName);
92
+ const destPath = path.join(appRootPath, key === 'src' ? './src' : `./${key === 'app' ? 'App.tsx' : key === 'index' ? 'index.js' : key === 'babel' ? 'babel.config.js' : 'htyf.json'}`);
93
+
94
+ if (fs.existsSync(srcPath)) {
95
+ fse.copySync(srcPath, destPath);
96
+ Logger.debug(`已复制: ${fileName} -> ${destPath}`);
97
+ }
98
+ });
99
+
100
+ // 更新 package.json
101
+ const pkgPath = path.join(appRootPath, './package.json');
102
+ if (fs.existsSync(pkgPath)) {
103
+ const pkgInfo = fse.readJSONSync(pkgPath);
104
+ if (template.scripts) {
105
+ pkgInfo.scripts = {
106
+ ...(pkgInfo.scripts || {}),
107
+ ...template.scripts
108
+ };
109
+ }
110
+
111
+ // 移除不需要的脚本
112
+ if (pkgInfo.scripts) {
113
+ delete pkgInfo.scripts.web;
114
+ delete pkgInfo.scripts['web:dev'];
115
+ }
116
+
117
+ fse.writeJSONSync(pkgPath, pkgInfo, { spaces: 2 });
118
+ Logger.info('package.json 已更新');
119
+ }
120
+ }
121
+
122
+ processExpoTemplate(appRootPath, tempPath, template) {
123
+ Logger.info('正在处理 Expo 模板...');
124
+
125
+ // 复制模板文件
126
+ Object.entries(template.files).forEach(([key, fileName]) => {
127
+ const srcPath = path.join(tempPath, fileName);
128
+ const destPath = path.join(appRootPath, key === 'src' ? './src' : `./${key === 'app' ? 'App.tsx' : key === 'index' ? 'index.js' : key === 'babel' ? 'babel.config.js' : 'htyf.json'}`);
129
+
130
+ if (fs.existsSync(srcPath)) {
131
+ fse.copySync(srcPath, destPath);
132
+ Logger.debug(`已复制: ${fileName} -> ${destPath}`);
133
+ }
134
+ });
135
+ }
136
+ }
137
+
@@ -0,0 +1,76 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import fse from 'fs-extra';
4
+ import { Logger } from './logger.mjs';
5
+ import { CONSTANTS } from './constants.mjs';
6
+ import { getProjectRoot } from './utils.mjs';
7
+
8
+ const projectPath = getProjectRoot();
9
+
10
+ /**
11
+ * 版本号递增函数
12
+ * @param {string} version - 当前版本号 (格式: x.y.z)
13
+ * @returns {string} - 递增后的版本号
14
+ */
15
+ export function incrementVersion(version) {
16
+ if (!version || !CONSTANTS.VERSION_NAME_REGEX.test(version)) {
17
+ return CONSTANTS.DEFAULT_VERSION_NAME;
18
+ }
19
+
20
+ const parts = version.split('.').map(Number);
21
+ parts[2] = (parts[2] || 0) + 1; // 补丁版本号加1
22
+
23
+ return parts.join('.');
24
+ }
25
+
26
+ /**
27
+ * 更新应用配置
28
+ * @param {object} newConfig - 新配置对象
29
+ * @returns {boolean} 是否成功
30
+ */
31
+ export function updateAppConfig(newConfig) {
32
+ try {
33
+ const configPath = path.join(projectPath, 'app.json');
34
+ const localConfig = fse.readJsonSync(configPath);
35
+ const _newConfig = {
36
+ ...localConfig,
37
+ 'htyf': {
38
+ ...localConfig.htyf,
39
+ ...newConfig
40
+ }
41
+ }
42
+
43
+ fs.writeFileSync(configPath, JSON.stringify(_newConfig, undefined, 2), 'utf-8');
44
+ Logger.success(`配置文件已更新: ${configPath}`);
45
+
46
+ return true;
47
+ } catch (error) {
48
+ Logger.error(`更新配置文件失败: ${error.message}`);
49
+ return false;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 获取小程序脚本ID
55
+ * @param {string} appid - 应用ID
56
+ * @param {string} version - 版本号
57
+ * @returns {string} 脚本ID
58
+ */
59
+ export function getMiniAppScriptId(appid, version) {
60
+ return `${appid}_v${version?.replace(/\./gi, '_')}`;
61
+ }
62
+
63
+ /**
64
+ * 打印二维码
65
+ * @param {string} url - 二维码URL
66
+ */
67
+ export async function printQrcode(url) {
68
+ try {
69
+ const QRCode = await import('qrcode');
70
+ let terminalStr = await QRCode.toString(url, { type: 'terminal', small: true });
71
+ console.log(terminalStr);
72
+ } catch (error) {
73
+ Logger.error(`生成二维码失败: ${error.message}`);
74
+ }
75
+ }
76
+
package/src/utils.mjs ADDED
@@ -0,0 +1,37 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ /**
9
+ * 获取项目根目录
10
+ * @returns {string} 项目根目录路径
11
+ */
12
+ export function getProjectRoot() {
13
+ return process.cwd() || __dirname;
14
+ }
15
+
16
+ /**
17
+ * 猜测 Godot 项目目录
18
+ * @param {string} rootDir - 根目录
19
+ * @returns {string} Godot 项目目录路径
20
+ */
21
+ export function guessGodotProjectDir(rootDir = getProjectRoot()) {
22
+ const candidates = [
23
+ rootDir,
24
+ path.join(rootDir, 'project'),
25
+ path.join(rootDir, 'game'),
26
+ path.join(rootDir, 'godot')
27
+ ];
28
+
29
+ for (const dir of candidates) {
30
+ const projectFile = path.join(dir, 'project.godot');
31
+ const gameFile = path.join(dir, 'game.godot');
32
+ if (fs.existsSync(projectFile) || fs.existsSync(gameFile)) {
33
+ return dir;
34
+ }
35
+ }
36
+ return rootDir;
37
+ }