@htyf-mp/cli 0.2.0-beta.1 → 0.2.2

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/index.mjs CHANGED
@@ -1,677 +1,101 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * React Native 应用 CLI 工具
5
- * 用于管理多应用项目的开发、构建和部署
4
+ * 红糖云服应用 CLI 工具主入口
5
+ *
6
+ * 功能:
7
+ * - 初始化新小程序项目
8
+ * - 小程序打包构建
9
+ * - 小程序真机调试
10
+ * - 清理临时文件
11
+ *
12
+ * 支持项目类型:
13
+ * - 普通小程序项目(使用 webpack 构建)
14
+ * - Godot 游戏项目(使用 Godot 导出工具)
6
15
  *
7
16
  * @author CLI Team
8
- * @version 2.0.0
17
+ * @version 2.2.0
18
+ * @module index
9
19
  */
10
20
 
11
21
  import inquirer from 'inquirer';
12
- import shell from 'shelljs';
13
- import fs from 'fs';
14
- import { fileURLToPath } from 'node:url';
15
22
  import path from 'path';
16
23
  import lodash from 'lodash';
17
24
  import fse from 'fs-extra';
18
- import superstatic from 'superstatic';
19
- import AdmZip from 'adm-zip';
20
- import QRCode from 'qrcode';
21
- import portfinder from 'portfinder';
22
- import si from 'systeminformation';
23
25
  import chalk from 'chalk';
24
- import ora from 'ora';
25
26
  import boxen from 'boxen';
26
27
  import gradient from 'gradient-string';
27
-
28
- // 获取当前文件的绝对路径和目录
29
- const __filename = fileURLToPath(import.meta.url);
30
- const __dirname = path.dirname(__filename);
31
-
32
- const projectPath = path.resolve(__dirname, '../../../../');
33
-
34
- // 常量定义
35
- const CONSTANTS = {
36
- SRC_DIR: '../src',
37
- TEMP_DIR: './.temp',
38
- MP_OUTPUT_DIR: './.mp-output',
39
- LOGS_DIR: './.logs',
40
- DEFAULT_VERSION_NAME: '1.0.0',
41
- DEFAULT_VERSION_CODE: '1',
42
- VERSION_NAME_REGEX: /^\d+(\.\d+){2}$/g,
43
- VERSION_CODE_REGEX: /^\d+$/g,
44
- WEBPACK_PORT: 8081,
45
- BUNDLE_ENTRY_FILE: 'index.js',
46
- BUNDLE_PLATFORM: 'ios',
47
- MAX_RETRY_COUNT: 3,
48
- DEFAULT_TIMEOUT: 30000
49
- };
50
-
51
- // 操作类型枚举
52
- const ACTION_TYPES = {
53
- START: 'start',
54
- MP_BUILD: 'mp-build',
55
- MP_DEBUG: 'mp-debug',
56
- CLEAN: 'clean',
57
- QUIT: 'quit'
58
- };
59
-
60
- // 日志级别枚举
61
- const LOG_LEVELS = {
62
- DEBUG: 0,
63
- INFO: 1,
64
- WARN: 2,
65
- ERROR: 3
66
- };
67
-
68
- // 当前日志级别
69
- let currentLogLevel = LOG_LEVELS.INFO;
70
-
71
- /**
72
- * 日志工具类
73
- */
74
- class Logger {
75
- static debug(message, ...args) {
76
- if (currentLogLevel <= LOG_LEVELS.DEBUG) {
77
- console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
78
- }
79
- }
80
-
81
- static info(message, ...args) {
82
- if (currentLogLevel <= LOG_LEVELS.INFO) {
83
- console.log(chalk.blue(`[INFO] ${message}`), ...args);
84
- }
85
- }
86
-
87
- static warn(message, ...args) {
88
- if (currentLogLevel <= LOG_LEVELS.WARN) {
89
- console.log(chalk.yellow(`[WARN] ${message}`), ...args);
90
- }
91
- }
92
-
93
- static error(message, ...args) {
94
- if (currentLogLevel <= LOG_LEVELS.ERROR) {
95
- console.log(chalk.red(`[ERROR] ${message}`), ...args);
96
- }
97
- }
98
-
99
- static success(message, ...args) {
100
- console.log(chalk.green(`[SUCCESS] ${message}`), ...args);
101
- }
102
-
103
- static writeToFile(message, level = 'INFO') {
104
- const logDir = path.join(__dirname, CONSTANTS.LOGS_DIR);
105
- const logFile = path.join(logDir, `cli-${new Date().toISOString().split('T')[0]}.log`);
106
-
107
- try {
108
- fse.ensureDirSync(logDir);
109
- const timestamp = new Date().toISOString();
110
- const logEntry = `[${timestamp}] [${level}] ${message}\n`;
111
- fs.appendFileSync(logFile, logEntry);
112
- } catch (error) {
113
- console.error('写入日志文件失败:', error.message);
114
- }
115
- }
116
- }
28
+ import { confirm } from '@inquirer/prompts';
29
+ import { getProjectRoot } from './utils.mjs';
30
+ import { Logger, setLogLevel } from './logger.mjs';
31
+ import { CONSTANTS, ACTION_TYPES, LOG_LEVELS } from './constants.mjs';
32
+ import { ProjectInitializer } from './project-initializer.mjs';
33
+ import { mpBuildShell } from './build.mjs';
34
+ import { mpDebugShell } from './debug.mjs';
35
+ import { cleanShell } from './clean.mjs';
36
+ import { incrementVersion, updateAppConfig } from './utils-functions.mjs';
37
+
38
+ const projectPath = getProjectRoot();
39
+
40
+ // ========== 全局状态管理 ==========
41
+ // 用于优雅退出,避免重复退出操作
42
+ let isExiting = false;
43
+ let currentProcess = null;
117
44
 
118
45
  /**
119
- * 配置验证工具类
46
+ * 优雅退出处理函数
47
+ *
48
+ * 当收到退出信号(如 Ctrl+C)时:
49
+ * 1. 尝试关闭正在运行的进程
50
+ * 2. 延迟退出,给清理操作一些时间
51
+ * 3. 如果已经在退出中,强制退出
120
52
  */
121
- class ConfigValidator {
122
- static validateAppConfig(config) {
123
- const errors = [];
124
- const warnings = [];
125
-
126
- // 必需字段检查
127
- const requiredFields = ['productName', 'versionName', 'versionCode'];
128
- requiredFields.forEach(field => {
129
- if (!config[field]) {
130
- errors.push(`缺少必需字段: ${field}`);
131
- }
132
- });
133
-
134
- // 版本格式检查
135
- // if (config.versionName && !CONSTANTS.VERSION_NAME_REGEX.test(`${config.versionName}`)) {
136
- // errors.push(`版本名称格式错误: ${config.versionName},应为 x.y.z 格式`);
137
- // }
138
- // if (config.versionCode && !CONSTANTS.VERSION_CODE_REGEX.test(`${config.versionCode}`)) {
139
- // errors.push(`版本代码格式错误: ${config.versionCode},应为数字格式`);
140
- // }
141
-
142
- // 平台配置检查
143
- if (config.iosBuild) {
144
- if (!config.iosBuild.appId) {
145
- warnings.push('iOS配置缺少appId');
146
- }
147
- }
148
-
149
- if (config.andBuild) {
150
- if (!config.andBuild.appId) {
151
- warnings.push('Android配置缺少appId');
152
- }
153
- }
154
-
155
- return { errors, warnings, isValid: errors.length === 0 };
156
- }
157
-
158
- static validateNetworkConfig() {
159
- return new Promise(async (resolve) => {
160
- try {
161
- const networkInfo = await si.networkInterfaces();
162
- const defaultNet = (lodash.isArray(networkInfo) ? networkInfo : [networkInfo])
163
- .find(i => lodash.get(i, 'default', false));
164
-
165
- if (!defaultNet || !defaultNet.ip4) {
166
- resolve({ isValid: false, error: '无法获取网络配置' });
167
- return;
168
- }
169
-
170
- resolve({
171
- isValid: true,
172
- host: defaultNet.ip4,
173
- interface: defaultNet.iface
174
- });
175
- } catch (error) {
176
- resolve({ isValid: false, error: error.message });
177
- }
178
- });
179
- }
180
- }
181
-
182
- /**
183
- * 文件系统工具类
184
- */
185
- class FileSystemUtils {
186
- static ensureDirectory(dirPath) {
187
- try {
188
- fse.ensureDirSync(dirPath);
189
- Logger.debug(`目录已确保存在: ${dirPath}`);
190
- return true;
191
- } catch (error) {
192
- Logger.error(`创建目录失败: ${dirPath}`, error.message);
193
- return false;
194
- }
53
+ function handleGracefulExit() {
54
+ if (isExiting) {
55
+ // 如果已经在退出中,强制退出
56
+ Logger.warn('强制退出...');
57
+ process.exit(1);
195
58
  }
196
59
 
197
- static cleanDirectory(dirPath) {
198
- try {
199
- if (fs.existsSync(dirPath)) {
200
- fse.removeSync(dirPath);
201
- Logger.info(`目录已清理: ${dirPath}`);
202
- }
203
- return true;
204
- } catch (error) {
205
- Logger.error(`清理目录失败: ${dirPath}`, error.message);
206
- return false;
207
- }
208
- }
60
+ isExiting = true;
61
+ Logger.info('\n正在退出...');
209
62
 
210
- static backupFile(filePath) {
63
+ // 如果有正在运行的进程,尝试关闭
64
+ if (currentProcess) {
211
65
  try {
212
- if (fs.existsSync(filePath)) {
213
- const backupPath = `${filePath}.backup.${Date.now()}`;
214
- fse.copySync(filePath, backupPath);
215
- Logger.info(`文件已备份: ${backupPath}`);
216
- return backupPath;
66
+ if (typeof currentProcess.close === 'function') {
67
+ currentProcess.close();
68
+ } else if (typeof currentProcess.kill === 'function') {
69
+ currentProcess.kill('SIGTERM');
217
70
  }
218
- return null;
219
- } catch (error) {
220
- Logger.error(`备份文件失败: ${filePath}`, error.message);
221
- return null;
222
- }
223
- }
224
-
225
- static getFileSize(filePath) {
226
- try {
227
- const stats = fs.statSync(filePath);
228
- return stats.size;
229
71
  } catch (error) {
230
- return 0;
231
- }
232
- }
233
-
234
- static formatFileSize(bytes) {
235
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
236
- if (bytes === 0) return '0 Bytes';
237
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
238
- return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
239
- }
240
- }
241
-
242
- /**
243
- * 命令执行工具类
244
- */
245
- class CommandExecutor {
246
- static async execute(command, options = {}) {
247
- const {
248
- silent = false,
249
- fatal = true,
250
- timeout = CONSTANTS.DEFAULT_TIMEOUT,
251
- retryCount = CONSTANTS.MAX_RETRY_COUNT,
252
- description = '执行命令'
253
- } = options;
254
-
255
- const spinner = ora(`${description}: ${command}`).start();
256
-
257
- for (let attempt = 1; attempt <= retryCount; attempt++) {
258
- try {
259
- Logger.debug(`执行命令 (尝试 ${attempt}/${retryCount}): ${command}`);
260
-
261
- const result = await new Promise((resolve, reject) => {
262
- const timeoutId = setTimeout(() => {
263
- reject(new Error(`命令执行超时 (${timeout}ms)`));
264
- }, timeout);
265
-
266
- shell.exec(command, {
267
- silent: silent,
268
- fatal: false,
269
- async: false
270
- }, (code, stdout, stderr) => {
271
- clearTimeout(timeoutId);
272
- resolve({ code, stdout, stderr });
273
- });
274
- });
275
-
276
- if (result.code === 0) {
277
- spinner.succeed(`${description} 成功`);
278
- Logger.debug(`命令输出: ${result.stdout}`);
279
- return result;
280
- } else {
281
- throw new Error(`命令执行失败,退出码: ${result.code}, 错误: ${result.stderr}`);
282
- }
283
- } catch (error) {
284
- if (attempt === retryCount) {
285
- spinner.fail(`${description} 失败 (${retryCount} 次尝试后)`);
286
- Logger.error(`命令执行失败: ${error.message}`);
287
-
288
- if (fatal) {
289
- process.exit(1);
290
- }
291
- throw error;
292
- } else {
293
- Logger.warn(`命令执行失败,准备重试 (${attempt}/${retryCount}): ${error.message}`);
294
- await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // 递增延迟
295
- }
296
- }
72
+ Logger.debug(`关闭进程时出错: ${error.message}`);
297
73
  }
298
74
  }
299
75
 
300
- static async executeWithProgress(command, options = {}) {
301
- const { description = '执行命令', progressCallback } = options;
302
-
303
- return new Promise((resolve, reject) => {
304
- const child = shell.exec(command, {
305
- silent: false,
306
- fatal: false,
307
- async: true
308
- });
309
-
310
- let output = '';
311
-
312
- child.stdout?.on('data', (data) => {
313
- output += data;
314
- if (progressCallback) {
315
- progressCallback(data.toString());
316
- }
317
- });
318
-
319
- child.stderr?.on('data', (data) => {
320
- output += data;
321
- Logger.warn(`命令输出: ${data}`);
322
- });
323
-
324
- child.on('close', (code) => {
325
- if (code === 0) {
326
- Logger.success(`${description} 完成`);
327
- resolve({ code, output });
328
- } else {
329
- Logger.error(`${description} 失败,退出码: ${code}`);
330
- reject(new Error(`命令执行失败,退出码: ${code}`));
331
- }
332
- });
333
-
334
- child.on('error', (error) => {
335
- Logger.error(`${description} 出错: ${error.message}`);
336
- reject(error);
337
- });
338
- });
339
- }
340
- }
341
-
342
- /**
343
- * 验证版本号格式
344
- *
345
- * @param {string} versionName - 版本名称 (格式: x.y.z)
346
- * @param {string} versionCode - 版本代码 (格式: 数字)
347
- * @returns {boolean} 验证结果
348
- */
349
- function validateVersion(versionName, versionCode) {
350
- const isVersionNameValid = CONSTANTS.VERSION_NAME_REGEX.test(versionName);
351
- const isVersionCodeValid = CONSTANTS.VERSION_CODE_REGEX.test(versionCode);
352
-
353
- if (!isVersionNameValid) {
354
- Logger.error(`版本名称格式错误: ${versionName},应为 x.y.z 格式`);
355
- }
356
-
357
- if (!isVersionCodeValid) {
358
- Logger.error(`版本代码格式错误: ${versionCode},应为数字格式`);
359
- }
360
-
361
- return isVersionNameValid && isVersionCodeValid;
362
- }
363
-
364
- /**
365
- * 更新应用配置
366
- *
367
- * @param {string} appName - 应用名称
368
- * @param {Object} newConfig - 新配置
369
- * @returns {boolean} 更新是否成功
370
- */
371
- function updateAppConfig(newConfig) {
372
- try {
373
- const configPath = path.join(projectPath, 'app.json');
374
- const localConfig = fse.readJsonSync(configPath);
375
- const _newConfig = {
376
- ...localConfig,
377
- 'htyf': {
378
- ...localConfig.htyf,
379
- ...newConfig
380
- }
381
- }
382
- // 备份原配置文件
383
- // FileSystemUtils.backupFile(configPath);
384
-
385
- // 写入新配置
386
- fs.writeFileSync(configPath, JSON.stringify(_newConfig, undefined, 2), 'utf-8');
387
- Logger.success(`配置文件已更新: ${configPath}`);
388
-
389
- return true;
390
- } catch (error) {
391
- Logger.error(`更新配置文件失败: ${error.message}`);
392
- return false;
393
- }
394
- }
395
-
396
- /**
397
- * 构建小程序包
398
- *
399
- * @param {string} appName - 应用名称
400
- */
401
- async function mpBuildShell(newAppInfo) {
402
- Logger.info(`开始构建小程序包...`);
403
- Logger.info(`应用: ${newAppInfo.name}`);
404
- Logger.info(`平台: ${CONSTANTS.BUNDLE_PLATFORM}`);
405
-
406
- // 创建临时目录
407
- const tempPath = path.join(__dirname, CONSTANTS.TEMP_DIR, newAppInfo.name);
408
- if (!FileSystemUtils.ensureDirectory(tempPath)) {
409
- return;
410
- }
411
-
412
- const mpOutputPath = path.join(__dirname, CONSTANTS.MP_OUTPUT_DIR, newAppInfo.name, 'dist');
413
- if (!FileSystemUtils.ensureDirectory(mpOutputPath)) {
414
- return;
415
- }
416
- const version = newAppInfo.version;
417
- const appid = newAppInfo.appid;
418
- const scriptName = getMiniAppScriptId(appid, version);
419
-
420
- const appJson = {
421
- type: 'app',
422
- engines: '2.0.0',
423
- name: newAppInfo.name,
424
- appid: appid,
425
- version: version,
426
- appUrlConfig: newAppInfo.appUrlConfig || `${newAppInfo.host}/app.json`,
427
- zipUrl: newAppInfo.zipUrl || `${newAppInfo.host}/dist.dgz`
428
- }
429
-
430
- const mpOptions = {
431
- name: scriptName,
432
- filename: `${scriptName}.bundle`,
433
- exposes: {
434
- 'App': `./src/${newAppInfo.name}/index.tsx`,
435
- },
436
- outputPath: path.join(mpOutputPath, '../build'),
437
- extraChunksPath: path.join(mpOutputPath),
438
- }
439
-
440
-
441
- const appJsonPath = path.join(mpOutputPath, '../', `./app.json`);
442
- fs.writeFileSync(appJsonPath, JSON.stringify(appJson, undefined, 2));
443
- const webpackConfigPath = path.join(__dirname, 'webpack.config.mjs');
444
- const command = `npx cross-env APP_USE_ENTRY_DEV=${newAppInfo.name} APP_EXPOSES_OPTIONS=${encodeURI(JSON.stringify(mpOptions))} APP_EXPOSES_FILENAME=${mpOptions.filename} APP_EXPOSES_EXPOSES=${JSON.stringify(mpOptions.exposes)} npx webpack --config ${webpackConfigPath}`;
445
-
446
- try {
447
- await CommandExecutor.execute(command, {
448
- description: '构建小程序包',
449
- timeout: 120000
450
- });
451
-
452
- fs.copyFileSync(appJsonPath, path.join(mpOutputPath, '../dist', `./app.json`));
453
- const zipPath = await handleZip(mpOutputPath, path.join(mpOutputPath, '../', `./dist.dgz`));
454
- Logger.success(`压缩包已创建: ${zipPath}`);
455
- return zipPath;
456
- } catch (error) {
457
- Logger.error(`小程序构建失败: ${error.message}`);
458
- throw error;
459
- }
460
- }
461
-
462
- /**
463
- * 压缩文件夹为zip文件
464
- *
465
- * @param {string} inputPath - 输入文件夹路径
466
- * @param {string} outputPath - 输出zip文件路径
467
- * @returns {Promise<string>} zip文件路径
468
- */
469
- const handleZip = async (inputPath, outputPath) => {
470
- Logger.info(`开始压缩文件夹: ${inputPath}`);
471
- Logger.info(`输出文件: ${outputPath}`);
472
-
473
- // 检查输入路径是否存在
474
- if (!fs.existsSync(inputPath)) {
475
- throw new Error(`输入路径不存在: ${inputPath}`);
476
- }
477
-
478
- // 检查输入路径是否为目录
479
- const stats = fs.statSync(inputPath);
480
- if (!stats.isDirectory()) {
481
- throw new Error(`输入路径不是目录: ${inputPath}`);
482
- }
483
-
484
- // 列出目录内容
485
- const files = fs.readdirSync(inputPath);
486
- Logger.debug(`目录内容: ${files.length} 个文件/文件夹`);
487
- files.forEach(file => {
488
- const filePath = path.join(inputPath, file);
489
- const fileStats = fs.statSync(filePath);
490
- Logger.debug(` - ${file} (${fileStats.isDirectory() ? '目录' : '文件'})`);
491
- });
492
-
493
- if (files.length === 0) {
494
- Logger.warn(`警告: 目录为空,将创建空的zip文件`);
495
- }
496
-
497
- const zipPath = outputPath;
498
- const admzip = new AdmZip();
499
-
500
- try {
501
- await admzip.addLocalFolderPromise(inputPath, {
502
- filter: (filename) => {
503
- // 过滤掉 .map 文件
504
- if (filename.endsWith('.map')) {
505
- Logger.debug(`过滤文件: ${filename}`);
506
- return false;
507
- }
508
- Logger.debug(`添加文件: ${filename}`);
509
- return true;
510
- }
511
- });
512
-
513
- await admzip.writeZipPromise(zipPath);
514
- Logger.success(`压缩完成: ${zipPath}`);
515
-
516
- // 检查生成的zip文件大小
517
- const zipStats = fs.statSync(zipPath);
518
- Logger.info(`ZIP文件大小: ${FileSystemUtils.formatFileSize(zipStats.size)}`);
519
-
520
- return zipPath;
521
- } catch (error) {
522
- Logger.error(`压缩失败: ${error.message}`);
523
- throw error;
524
- }
525
- };
526
-
527
- /**
528
- * 小程序真机调试
529
- *
530
- * @param {string} appName - 应用名称
531
- */
532
- async function mpDebugShell(newAppInfo) {
533
- try {
534
- const zipPath = await mpBuildShell(newAppInfo);
535
- const assetsPath = path.dirname(zipPath);
536
- const appJsonPath = path.join(assetsPath, 'app.json');
537
-
538
- Logger.info(`资源路径: ${assetsPath}`);
539
- const applocaljson = fse.readJsonSync(appJsonPath);
540
-
541
- // 验证网络配置
542
- const networkValidation = await ConfigValidator.validateNetworkConfig();
543
- if (!networkValidation.isValid) {
544
- throw new Error(`网络配置验证失败: ${networkValidation.error}`);
545
- }
546
-
547
- const host = networkValidation.host;
548
- const port = await portfinder.getPortPromise();
549
-
550
- const app = superstatic.server({
551
- port: port,
552
- address: host,
553
- cwd: assetsPath
554
- });
555
-
556
- const server = app.listen(function () {
557
- const address = server.address();
558
-
559
- const debugerAppUrlConfig = `http://${host}:${address.port}/app.json`;
560
- const debugerZip = `http://${host}:${address.port}/dist.dgz`;
561
- const args = {
562
- ...applocaljson,
563
- development: true,
564
- "name": applocaljson.name,
565
- "appid": applocaljson.appid,
566
- "version": applocaljson.version,
567
- appUrlConfig: debugerAppUrlConfig,
568
- zipUrl: debugerZip,
569
- }
570
-
571
- fse.writeJSONSync(appJsonPath, {
572
- ...fse.readJSONSync(appJsonPath),
573
- appUrlConfig: debugerAppUrlConfig,
574
- zipUrl: debugerZip,
575
- });
576
-
577
- const qrcodeUrl = `https://share.dagouzhi.com/#/pages/index/index?data=${encodeURIComponent(JSON.stringify(args))}`;
578
-
579
- // 显示调试信息
580
- console.log('\n');
581
- console.log(boxen(
582
- gradient.rainbow('小程序真机调试') + '\n\n' +
583
- chalk.cyan('服务器地址: ') + chalk.white(`${host}:${address.port}`) + '\n' +
584
- chalk.cyan('应用名称: ') + chalk.white(applocaljson.name) + '\n' +
585
- chalk.cyan('应用ID: ') + chalk.white(applocaljson.appid) + '\n' +
586
- chalk.cyan('版本: ') + chalk.white(applocaljson.version),
587
- {
588
- padding: 1,
589
- margin: 1,
590
- borderStyle: 'round',
591
- borderColor: 'cyan'
592
- }
593
- ));
594
-
595
- console.log('\n' + chalk.yellow('调试配置:'));
596
- console.log(JSON.stringify(args, undefined, 2));
597
-
598
- console.log('\n' + chalk.green('调试链接:'));
599
- console.log(qrcodeUrl);
600
-
601
- console.log('\n' + chalk.blue('二维码:'));
602
- printQrcode(qrcodeUrl);
603
-
604
- Logger.info('调试服务器已启动,按 Ctrl+C 停止');
605
- });
606
-
607
- // 优雅关闭
608
- process.on('SIGINT', () => {
609
- Logger.info('正在关闭调试服务器...');
610
- server.close(() => {
611
- Logger.success('调试服务器已关闭');
612
- process.exit(0);
613
- });
614
- });
615
-
616
- } catch (error) {
617
- Logger.error(`小程序调试失败: ${error.message}`);
618
- process.exit(1);
619
- }
620
- }
621
-
622
- /**
623
- * 清理临时文件
624
- *
625
- * @param {string} appName - 应用名称(可选)
626
- */
627
- async function cleanShell(appName = null) {
628
- Logger.info('开始清理临时文件...');
629
-
630
- const pathsToClean = [];
631
-
632
- if (appName) {
633
- // 清理指定应用的临时文件
634
- pathsToClean.push(
635
- path.join(__dirname, CONSTANTS.TEMP_DIR, appName),
636
- path.join(__dirname, CONSTANTS.MP_OUTPUT_DIR, appName)
637
- );
638
- } else {
639
- // 清理所有临时文件
640
- pathsToClean.push(
641
- path.join(__dirname, CONSTANTS.TEMP_DIR),
642
- path.join(__dirname, CONSTANTS.MP_OUTPUT_DIR)
643
- );
644
- }
645
-
646
- let cleanedCount = 0;
647
- for (const path of pathsToClean) {
648
- if (FileSystemUtils.cleanDirectory(path)) {
649
- cleanedCount++;
650
- }
651
- }
652
-
653
- Logger.success(`清理完成,已清理 ${cleanedCount} 个目录`);
76
+ // 延迟退出,给清理操作一些时间
77
+ setTimeout(() => {
78
+ Logger.info('再见!');
79
+ process.exit(0);
80
+ }, 100);
654
81
  }
655
82
 
656
- /**
657
- * 打印二维码
658
- *
659
- * @param {string} url - 要生成二维码的URL
660
- */
661
- const printQrcode = async (url) => {
662
- try {
663
- let terminalStr = await QRCode.toString(url, { type: 'terminal', small: true });
664
- console.log(terminalStr);
665
- } catch (error) {
666
- Logger.error(`生成二维码失败: ${error.message}`);
667
- }
668
- };
83
+ // ========== 注册全局信号处理 ==========
84
+ // 使用 once 确保只注册一次
85
+ // 注意:某些模块(如 debug.mjs)可能会移除这些监听器并添加自己的处理
86
+ process.on('SIGINT', handleGracefulExit); // Ctrl+C
87
+ process.on('SIGTERM', handleGracefulExit); // 终止信号
669
88
 
670
89
  /**
671
90
  * 主启动函数
672
- * 处理用户选择的操作类型
673
- *
674
- * @param {string} action - 操作类型
91
+ *
92
+ * 根据操作类型执行相应的功能:
93
+ * - MP_BUILD: 构建小程序包
94
+ * - MP_DEBUG: 启动真机调试服务器
95
+ * - CLEAN: 清理临时文件
96
+ * - QUIT: 退出程序
97
+ *
98
+ * @param {string} action - 操作类型(ACTION_TYPES 中的值)
675
99
  */
676
100
  async function Start(action) {
677
101
  const actionNames = {
@@ -683,45 +107,87 @@ async function Start(action) {
683
107
 
684
108
  Logger.info(`当前操作: ${actionNames[action] || '未知操作'}`);
685
109
 
686
- // 如果是退出操作,直接返回
110
+ // ========== 退出操作 ==========
687
111
  if (action === ACTION_TYPES.QUIT) {
688
112
  Logger.info('再见!');
689
113
  return;
690
114
  }
691
115
 
692
- // 如果是清理或状态操作,不需要选择应用
116
+ // ========== 清理操作 ==========
693
117
  if (action === ACTION_TYPES.CLEAN) {
694
- const { cleanAll } = await inquirer.prompt([
118
+ const { cleanType } = await inquirer.prompt([
695
119
  {
696
- type: 'confirm',
697
- name: 'cleanAll',
698
- message: '是否清理所有应用的临时文件?',
699
- default: false
120
+ type: 'list',
121
+ name: 'cleanType',
122
+ message: '请选择清理类型:',
123
+ choices: [
124
+ { name: '🧹 清理所有临时文件', value: 'all' },
125
+ { name: '📁 清理构建输出', value: 'build' },
126
+ { name: '🗂️ 清理临时目录', value: 'temp' },
127
+ { name: '📝 清理日志文件', value: 'logs' },
128
+ { name: '💾 清理缓存文件', value: 'cache' },
129
+ { name: '❌ 取消', value: 'cancel' }
130
+ ]
700
131
  }
701
132
  ]);
702
133
 
703
- if (cleanAll) {
704
- await cleanShell();
134
+ if (cleanType === 'cancel') {
135
+ Logger.info('已取消清理操作');
136
+ return;
137
+ }
138
+
139
+ const getCleanTypeName = (type) => {
140
+ switch (type) {
141
+ case 'all': return '所有';
142
+ case 'build': return '构建输出';
143
+ case 'temp': return '临时';
144
+ case 'logs': return '日志';
145
+ case 'cache': return '缓存';
146
+ default: return type;
147
+ }
148
+ };
149
+
150
+ const confirmClean = await confirm({
151
+ message: `确定要清理${getCleanTypeName(cleanType)}文件吗?`,
152
+ default: false
153
+ });
154
+
155
+ if (confirmClean) {
156
+ await cleanShell(cleanType);
157
+ } else {
158
+ Logger.info('已取消清理操作');
705
159
  }
706
160
  return;
707
161
  }
708
162
 
709
-
710
- // 显示应用选择界面
163
+ // ========== 构建/调试操作 ==========
711
164
  try {
712
- const appInfo = fse.readJsonSync(path.join(projectPath, 'app.json')).htyf;
165
+ // 读取应用配置
166
+ const appConfigPath = path.join(projectPath, 'app.json');
167
+ const appInfo = fse.readJsonSync(appConfigPath).htyf;
168
+
169
+ // 检测是否为 Godot 项目(通过检查是否存在 project.godot 文件)
170
+ const isGodot = fse.existsSync(path.join(projectPath, 'project.godot'));
171
+ Logger.info(`项目类型: ${isGodot ? 'Godot 游戏项目' : '普通小程序项目'}`);
172
+
713
173
  if (!appInfo) {
714
174
  Logger.error('应用配置不存在,请先在app.json中配置htyf');
715
175
  return;
716
176
  }
177
+
178
+ // ========== 版本处理 ==========
179
+ // 自动递增版本号作为默认值
180
+ const version = appInfo?.version;
181
+ const incrementedVersion = version ? incrementVersion(version) : '1.0.0';
717
182
  const { versionName } = await inquirer.prompt([
718
183
  {
719
184
  type: 'input',
720
185
  name: 'versionName',
721
186
  message: '请输入应用版本名称 (格式: x.y.z):',
722
- default: appInfo?.version,
187
+ default: incrementedVersion,
723
188
  validate: (input) => {
724
- return CONSTANTS.VERSION_NAME_REGEX.test(input) || '请输入正确的版本名称格式 (x.y.z)';
189
+ const _ipt = input || incrementedVersion;
190
+ return (!!_ipt && /^\d+(\.\d+){2}$/g.test(_ipt)) || '请输入正确的版本名称格式 (x.y.z)';
725
191
  }
726
192
  },
727
193
  ]);
@@ -729,25 +195,22 @@ async function Start(action) {
729
195
  Logger.info(`用户选择:`);
730
196
  Logger.info(`版本名称: ${versionName}`);
731
197
 
732
- // 验证版本格式
733
- // if (!validateVersion(versionName, versionCode)) {
734
- // return;
735
- // }
736
-
737
- // 合并新配置
198
+ // ========== 更新应用配置 ==========
199
+ // 合并用户输入的版本号到应用信息中
738
200
  const newAppInfo = lodash.merge({}, appInfo, {
739
201
  version: versionName,
740
202
  });
741
203
 
742
- // 更新配置文件
204
+ // 更新 app.json 文件
743
205
  if (!updateAppConfig(newAppInfo)) {
744
206
  return;
745
207
  }
746
208
 
747
- // 显示操作信息
209
+ // ========== 显示应用信息 ==========
748
210
  console.log('\n' + boxen(
749
211
  chalk.cyan('应用信息') + '\n\n' +
750
- chalk.white('名称: ') + chalk.yellow(`${newAppInfo.name})`) + '\n' +
212
+ chalk.white('名称: ') + chalk.yellow(`${newAppInfo.name}`) + '\n' +
213
+ chalk.white('版本: ') + chalk.yellow(`${newAppInfo.version}`),
751
214
  {
752
215
  padding: 1,
753
216
  margin: 1,
@@ -756,16 +219,17 @@ async function Start(action) {
756
219
  }
757
220
  ));
758
221
 
759
- // 执行初始化
760
- Logger.info('执行初始化...');
222
+ Logger.info('开始执行操作...');
761
223
 
762
- // 根据操作类型执行相应命令
224
+ // ========== 执行相应操作 ==========
763
225
  switch (action) {
764
226
  case ACTION_TYPES.MP_BUILD:
765
- await mpBuildShell(newAppInfo);
227
+ // 构建小程序包
228
+ await mpBuildShell(newAppInfo, isGodot);
766
229
  break;
767
230
  case ACTION_TYPES.MP_DEBUG:
768
- await mpDebugShell(newAppInfo);
231
+ // 启动真机调试服务器
232
+ await mpDebugShell(newAppInfo, isGodot);
769
233
  break;
770
234
  default:
771
235
  Logger.error(`未知的操作类型: ${action}`);
@@ -777,10 +241,13 @@ async function Start(action) {
777
241
  }
778
242
  }
779
243
 
780
- // 主程序入口
244
+ // ========== 主程序入口 ==========
245
+ // 显示欢迎信息
781
246
  console.log('\n' + boxen(
782
- gradient.rainbow('🎯 红糖云服 应用 CLI 工具 v2.0.0') + '\n' +
783
- chalk.gray('用于管理多应用项目的开发、构建和部署'),
247
+ gradient.rainbow('🎯 红糖云服 应用 CLI 工具 v2.2.0') + '\n' +
248
+ chalk.gray('用于管理多应用项目的开发、构建和部署') + '\n' +
249
+ chalk.gray(`项目目录:`) + '\n\n' +
250
+ chalk.gray(`${projectPath}`) + '\n',
784
251
  {
785
252
  padding: 1,
786
253
  margin: 1,
@@ -789,15 +256,62 @@ console.log('\n' + boxen(
789
256
  }
790
257
  ));
791
258
 
792
- // 设置日志级别
259
+ // ========== 命令行参数处理 ==========
260
+
261
+ // 处理调试模式参数
793
262
  if (process.argv.includes('--debug')) {
794
- currentLogLevel = LOG_LEVELS.DEBUG;
263
+ setLogLevel(LOG_LEVELS.DEBUG);
795
264
  Logger.debug('调试模式已启用');
796
265
  }
797
266
 
798
267
  // 记录启动日志
799
268
  Logger.writeToFile('CLI 工具启动', 'INFO');
800
269
 
270
+ // 处理清理参数(命令行直接清理,不进入交互界面)
271
+ if (process.argv.includes('--clean')) {
272
+ const cleanIndex = process.argv.indexOf('--clean');
273
+ const cleanType = process.argv[cleanIndex + 1] || 'all';
274
+
275
+ if (['all', 'build', 'temp', 'logs', 'cache'].includes(cleanType)) {
276
+ Logger.info(`执行命令行清理: ${cleanType}`);
277
+ await cleanShell(cleanType);
278
+ process.exit(0);
279
+ } else {
280
+ Logger.error(`无效的清理类型: ${cleanType}`);
281
+ Logger.error('支持的清理类型: all, build, temp, logs, cache');
282
+ process.exit(1);
283
+ }
284
+ }
285
+
286
+ // 显示帮助信息
287
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
288
+ console.log('\n' + boxen(
289
+ chalk.cyan('CLI 工具使用说明') + '\n\n' +
290
+ chalk.white('基本用法:') + '\n' +
291
+ chalk.gray(' node index.mjs # 启动交互式界面') + '\n' +
292
+ chalk.gray(' node index.mjs --debug # 启用调试模式') + '\n' +
293
+ chalk.gray(' node index.mjs --help # 显示帮助信息') + '\n\n' +
294
+ chalk.white('清理命令:') + '\n' +
295
+ chalk.gray(' node index.mjs --clean all # 清理所有临时文件') + '\n' +
296
+ chalk.gray(' node index.mjs --clean build # 清理构建输出') + '\n' +
297
+ chalk.gray(' node index.mjs --clean temp # 清理临时目录') + '\n' +
298
+ chalk.gray(' node index.mjs --clean logs # 清理日志文件') + '\n' +
299
+ chalk.gray(' node index.mjs --clean cache # 清理缓存文件') + '\n\n' +
300
+ chalk.white('功能说明:') + '\n' +
301
+ chalk.gray(' 🆕 初始化新小程序项目 - 创建新的小程序项目') + '\n' +
302
+ chalk.gray(' 🔍 小程序打包 - 构建小程序包') + '\n' +
303
+ chalk.gray(' 📦 小程序调试 - 启动真机调试服务') + '\n' +
304
+ chalk.gray(' 🧹 清理模式 - 清理临时文件') + '\n',
305
+ {
306
+ padding: 1,
307
+ margin: 1,
308
+ borderStyle: 'round',
309
+ borderColor: 'blue'
310
+ }
311
+ ));
312
+ process.exit(0);
313
+ }
314
+
801
315
  inquirer
802
316
  .prompt([
803
317
  {
@@ -805,6 +319,7 @@ inquirer
805
319
  name: 'index',
806
320
  message: '请选择你想要执行的操作:',
807
321
  choices: [
322
+ { name: '🆕 初始化新小程序项目', value: ACTION_TYPES.INIT },
808
323
  { name: '🔍 小程序 - 打包小程序', value: ACTION_TYPES.MP_BUILD },
809
324
  { name: '📦 小程序 - 真机调试', value: ACTION_TYPES.MP_DEBUG },
810
325
  { name: '🧹 清理模式 - 清理临时文件', value: ACTION_TYPES.CLEAN },
@@ -812,14 +327,22 @@ inquirer
812
327
  ],
813
328
  },
814
329
  ])
815
- .then(({ index }) => {
816
- Start(index);
330
+ .then(async ({ index }) => {
331
+ if (index === ACTION_TYPES.INIT) {
332
+ const initializer = new ProjectInitializer();
333
+ await initializer.initialize();
334
+ } else {
335
+ await Start(index);
336
+ }
817
337
  })
818
338
  .catch((error) => {
339
+ // 如果是用户取消(Ctrl+C),不显示错误
340
+ if (error.isTtyError || error.name === 'AbortError') {
341
+ handleGracefulExit();
342
+ return;
343
+ }
819
344
  Logger.error(`程序启动失败: ${error.message}`);
820
- process.exit(1);
345
+ if (!isExiting) {
346
+ process.exit(1);
347
+ }
821
348
  });
822
-
823
- export const getMiniAppScriptId = (appid, version) => {
824
- return `${appid}_v${version?.replace(/\./gi, '_')}`
825
- }