@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 +1 -1
- package/src/command-executor.mjs +60 -34
- package/src/config.mjs +7 -10
- package/src/constants.mjs +2 -3
- package/src/debug.mjs +27 -6
- package/src/index.mjs +50 -1
- package/src/project-initializer.mjs +46 -6
package/package.json
CHANGED
package/src/command-executor.mjs
CHANGED
|
@@ -20,46 +20,72 @@ export class CommandExecutor {
|
|
|
20
20
|
const displayCommand = showCommand ? command : description;
|
|
21
21
|
const spinner = ora(displayCommand).start();
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 (
|
|
55
|
-
process.
|
|
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.
|
|
17
|
-
name: '
|
|
18
|
-
tempPath: '
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
99
|
-
Logger.info('正在关闭调试服务器...');
|
|
97
|
+
// 优雅关闭 - 移除全局处理,添加专用的服务器关闭处理
|
|
98
|
+
const handleShutdown = () => {
|
|
99
|
+
Logger.info('\n正在关闭调试服务器...');
|
|
100
|
+
let isClosed = false;
|
|
101
|
+
|
|
100
102
|
server.close(() => {
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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('正在初始化项目...')
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// 克隆仓库
|