@htyf-mp/cli 0.2.0-alpha.3 → 0.2.0-alpha.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@htyf-mp/cli",
3
- "version": "0.2.0-alpha.3",
3
+ "version": "0.2.0-alpha.4",
4
4
  "description": "红糖云服app小程序模板-cli",
5
5
  "type": "module",
6
6
  "bin": "src/index.mjs",
@@ -20,46 +20,72 @@ export class CommandExecutor {
20
20
  const displayCommand = showCommand ? command : description;
21
21
  const spinner = ora(displayCommand).start();
22
22
 
23
- for (let attempt = 1; attempt <= retryCount; attempt++) {
24
- try {
25
- Logger.debug(`执行命令 (尝试 ${attempt}/${retryCount}): ${command}`);
26
-
27
- const result = await new Promise((resolve, reject) => {
28
- const timeoutId = setTimeout(() => {
29
- reject(new Error(`命令执行超时 (${timeout}ms)`));
30
- }, timeout);
31
-
32
- shell.exec(command, {
33
- silent: silent,
34
- fatal: false,
35
- async: false
36
- }, (code, stdout, stderr) => {
37
- clearTimeout(timeoutId);
38
- resolve({ code, stdout, stderr });
23
+ // 信号处理:当收到退出信号时,停止 spinner
24
+ const cleanup = () => {
25
+ spinner.stop();
26
+ };
27
+ process.once('SIGINT', cleanup);
28
+ process.once('SIGTERM', cleanup);
29
+
30
+ try {
31
+ for (let attempt = 1; attempt <= retryCount; attempt++) {
32
+ try {
33
+ Logger.debug(`执行命令 (尝试 ${attempt}/${retryCount}): ${command}`);
34
+
35
+ const result = await new Promise((resolve, reject) => {
36
+ const timeoutId = setTimeout(() => {
37
+ reject(new Error(`命令执行超时 (${timeout}ms)`));
38
+ }, timeout);
39
+
40
+ shell.exec(command, {
41
+ silent: silent,
42
+ fatal: false,
43
+ async: false
44
+ }, (code, stdout, stderr) => {
45
+ clearTimeout(timeoutId);
46
+ resolve({ code, stdout, stderr });
47
+ });
39
48
  });
40
- });
41
49
 
42
- if (result.code === 0) {
43
- spinner.succeed(`${description} 成功`);
44
- Logger.debug(`命令输出: ${result.stdout}`);
45
- return result;
46
- } else {
47
- throw new Error(`命令执行失败,退出码: ${result.code}, 错误: ${result.stderr}`);
48
- }
49
- } catch (error) {
50
- if (attempt === retryCount) {
51
- spinner.fail(`${description} 失败 (${retryCount} 次尝试后)`);
52
- Logger.error(`命令执行失败: ${error.message}`);
50
+ if (result.code === 0) {
51
+ process.removeListener('SIGINT', cleanup);
52
+ process.removeListener('SIGTERM', cleanup);
53
+ spinner.succeed(`${description} 成功`);
54
+ Logger.debug(`命令输出: ${result.stdout}`);
55
+ return result;
56
+ } else {
57
+ throw new Error(`命令执行失败,退出码: ${result.code}, 错误: ${result.stderr}`);
58
+ }
59
+ } catch (error) {
60
+ // 如果是用户中断,直接退出
61
+ if (error.message && (error.message.includes('SIGINT') || error.message.includes('SIGTERM'))) {
62
+ process.removeListener('SIGINT', cleanup);
63
+ process.removeListener('SIGTERM', cleanup);
64
+ spinner.stop();
65
+ throw error;
66
+ }
53
67
 
54
- if (fatal) {
55
- process.exit(1);
68
+ if (attempt === retryCount) {
69
+ process.removeListener('SIGINT', cleanup);
70
+ process.removeListener('SIGTERM', cleanup);
71
+ spinner.fail(`${description} 失败 (${retryCount} 次尝试后)`);
72
+ Logger.error(`命令执行失败: ${error.message}`);
73
+
74
+ if (fatal) {
75
+ process.exit(1);
76
+ }
77
+ throw error;
78
+ } else {
79
+ Logger.warn(`命令执行失败,准备重试 (${attempt}/${retryCount}): ${error.message}`);
80
+ await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // 递增延迟
56
81
  }
57
- throw error;
58
- } else {
59
- Logger.warn(`命令执行失败,准备重试 (${attempt}/${retryCount}): ${error.message}`);
60
- await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // 递增延迟
61
82
  }
62
83
  }
84
+ } catch (error) {
85
+ process.removeListener('SIGINT', cleanup);
86
+ process.removeListener('SIGTERM', cleanup);
87
+ spinner.stop();
88
+ throw error;
63
89
  }
64
90
  }
65
91
 
package/src/config.mjs CHANGED
@@ -13,17 +13,14 @@ export class ProjectConfig {
13
13
  };
14
14
 
15
15
  this.templates = {
16
- [CONSTANTS.TEMPLATE_TYPES.EXPO]: {
17
- name: 'Expo (https://docs.expo.dev/)',
18
- tempPath: 'mini-apps-template',
19
- files: {
20
- src: 'expo_src',
21
- app: 'expo.App.tsx',
22
- index: 'expo.index.js',
23
- babel: 'expo.babel.config.js',
24
- config: 'expo.project.dgz.json'
25
- }
16
+ [CONSTANTS.TEMPLATE_TYPES.APP_TEMPLATE]: {
17
+ name: 'app-template',
18
+ tempPath: '_apps_temp_'
26
19
  },
20
+ [CONSTANTS.TEMPLATE_TYPES.GAME_TEMPLATE]: {
21
+ name: 'game-template',
22
+ tempPath: '_games_temp_'
23
+ }
27
24
  };
28
25
  }
29
26
 
package/src/constants.mjs CHANGED
@@ -25,9 +25,8 @@ export const CONSTANTS = {
25
25
  CODING: 'https://e.coding.net/dagouzhi/hongtangyun_mobile/htyf-mp.git'
26
26
  },
27
27
  TEMPLATE_TYPES: {
28
- EXPO: 'expo',
29
- TARO: 'taro',
30
- GAME_COCOS: 'game-cocos',
28
+ APP_TEMPLATE: 'app-template',
29
+ GAME_TEMPLATE: 'game-template',
31
30
  }
32
31
  };
33
32
 
package/src/debug.mjs CHANGED
@@ -94,14 +94,35 @@ export async function mpDebugShell(newAppInfo) {
94
94
  Logger.info('调试服务器已启动,按 Ctrl+C 停止');
95
95
  });
96
96
 
97
- // 优雅关闭
98
- process.on('SIGINT', () => {
99
- Logger.info('正在关闭调试服务器...');
97
+ // 优雅关闭 - 移除全局处理,添加专用的服务器关闭处理
98
+ const handleShutdown = () => {
99
+ Logger.info('\n正在关闭调试服务器...');
100
+ let isClosed = false;
101
+
100
102
  server.close(() => {
101
- Logger.success('调试服务器已关闭');
102
- process.exit(0);
103
+ if (!isClosed) {
104
+ isClosed = true;
105
+ Logger.success('调试服务器已关闭');
106
+ process.exit(0);
107
+ }
103
108
  });
104
- });
109
+
110
+ // 如果服务器在 3 秒内没有关闭,强制退出
111
+ setTimeout(() => {
112
+ if (!isClosed) {
113
+ isClosed = true;
114
+ Logger.warn('强制退出...');
115
+ process.exit(0);
116
+ }
117
+ }, 3000);
118
+ };
119
+
120
+ // 移除全局的信号处理,添加专用的服务器关闭处理
121
+ // 这样可以确保服务器能够正确关闭
122
+ process.removeAllListeners('SIGINT');
123
+ process.removeAllListeners('SIGTERM');
124
+ process.once('SIGINT', handleShutdown);
125
+ process.once('SIGTERM', handleShutdown);
105
126
 
106
127
  } catch (error) {
107
128
  Logger.error(`小程序调试失败: ${error.message}`);
package/src/index.mjs CHANGED
@@ -27,6 +27,48 @@ import { incrementVersion, updateAppConfig } from './utils-functions.mjs';
27
27
 
28
28
  const projectPath = getProjectRoot();
29
29
 
30
+ // 全局状态管理,用于优雅退出
31
+ let isExiting = false;
32
+ let currentProcess = null;
33
+
34
+ /**
35
+ * 优雅退出处理
36
+ */
37
+ function handleGracefulExit() {
38
+ if (isExiting) {
39
+ // 如果已经在退出中,强制退出
40
+ Logger.warn('强制退出...');
41
+ process.exit(1);
42
+ }
43
+
44
+ isExiting = true;
45
+ Logger.info('\n正在退出...');
46
+
47
+ // 如果有正在运行的进程,尝试关闭
48
+ if (currentProcess) {
49
+ try {
50
+ if (typeof currentProcess.close === 'function') {
51
+ currentProcess.close();
52
+ } else if (typeof currentProcess.kill === 'function') {
53
+ currentProcess.kill('SIGTERM');
54
+ }
55
+ } catch (error) {
56
+ Logger.debug(`关闭进程时出错: ${error.message}`);
57
+ }
58
+ }
59
+
60
+ // 延迟退出,给清理操作一些时间
61
+ setTimeout(() => {
62
+ Logger.info('再见!');
63
+ process.exit(0);
64
+ }, 100);
65
+ }
66
+
67
+ // 注册全局信号处理(使用 once 确保只注册一次)
68
+ // 注意:某些模块(如 debug.mjs)可能会移除这些监听器并添加自己的处理
69
+ process.on('SIGINT', handleGracefulExit);
70
+ process.on('SIGTERM', handleGracefulExit);
71
+
30
72
  /**
31
73
  * 主启动函数
32
74
  * @param {string} action - 操作类型
@@ -250,6 +292,13 @@ inquirer
250
292
  }
251
293
  })
252
294
  .catch((error) => {
295
+ // 如果是用户取消(Ctrl+C),不显示错误
296
+ if (error.isTtyError || error.name === 'AbortError') {
297
+ handleGracefulExit();
298
+ return;
299
+ }
253
300
  Logger.error(`程序启动失败: ${error.message}`);
254
- process.exit(1);
301
+ if (!isExiting) {
302
+ process.exit(1);
303
+ }
255
304
  });
@@ -26,7 +26,28 @@ export class ProjectInitializer {
26
26
  }
27
27
 
28
28
  async initialize() {
29
- const spinner = ora('正在初始化项目...').start();
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);
30
51
 
31
52
  try {
32
53
  // 1. 获取用户输入
@@ -35,26 +56,41 @@ export class ProjectInitializer {
35
56
  // 2. 验证输入
36
57
  const validation = this.validateInputs(userInputs);
37
58
  if (!validation.isValid) {
59
+ process.removeListener('SIGINT', handleExit);
60
+ process.removeListener('SIGTERM', handleExit);
38
61
  spinner.fail('输入验证失败');
39
62
  Logger.error(validation.error);
40
63
  return;
41
64
  }
42
65
 
43
66
  // 3. 检查目录
44
- const rootPath = path.join(process.cwd(), userInputs.appName);
67
+ rootPath = path.join(process.cwd(), userInputs.appName);
45
68
  if (fs.existsSync(rootPath)) {
69
+ process.removeListener('SIGINT', handleExit);
70
+ process.removeListener('SIGTERM', handleExit);
46
71
  spinner.fail('目录已存在');
47
72
  Logger.error(`目录已存在: ${rootPath}`);
48
73
  return;
49
74
  }
50
75
 
51
- // 4. 创建项目
52
- await this.createProject(userInputs, rootPath, spinner);
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);
53
87
 
54
88
  // 5. 显示成功信息
55
89
  this.showSuccessInfo(userInputs.appName, rootPath);
56
90
 
57
91
  } catch (error) {
92
+ process.removeListener('SIGINT', handleExit);
93
+ process.removeListener('SIGTERM', handleExit);
58
94
  spinner.fail('项目初始化失败');
59
95
  Logger.error('初始化失败:', error.message);
60
96
  if (error.exitCode === 128) {
@@ -106,7 +142,6 @@ export class ProjectInitializer {
106
142
  message: '请选择模板镜像?',
107
143
  choices: [
108
144
  { name: 'GitHub (最新)', value: CONSTANTS.TEMPLATE_REPOS.GITHUB },
109
- { name: 'Coding (最快)', value: CONSTANTS.TEMPLATE_REPOS.CODING }
110
145
  ]
111
146
  });
112
147
 
@@ -122,13 +157,18 @@ export class ProjectInitializer {
122
157
  return { isValid: true };
123
158
  }
124
159
 
125
- async createProject(userInputs, rootPath, spinner) {
160
+ async createProject(userInputs, rootPath, spinner, setTmpdirCallback = null) {
126
161
  const { appName, displayName, templateType, repoType } = userInputs;
127
162
 
128
163
  // 创建临时目录
129
164
  spinner.text = '正在准备临时目录...';
130
165
  const tmpdir = path.join(os.tmpdir(), md5('__HTYF__'), appName);
131
166
  fse.emptyDirSync(tmpdir);
167
+
168
+ // 设置临时目录引用,以便在退出时清理
169
+ if (setTmpdirCallback) {
170
+ setTmpdirCallback(tmpdir);
171
+ }
132
172
 
133
173
  try {
134
174
  // 克隆仓库