@htyf-mp/cli 0.7.0 → 0.7.1
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 +36 -3
- package/package.json +3 -3
- package/src/build.mjs +4 -1
- package/src/clean.mjs +2 -0
- package/src/cli-options.mjs +77 -0
- package/src/debug.mjs +2 -2
- package/src/export_godot.mjs +13 -9
- package/src/index.mjs +19 -361
- package/src/interactive.mjs +303 -0
- package/src/project-initializer.mjs +18 -23
- package/src/run-command.mjs +46 -0
- package/src/sync-deps.mjs +2 -4
- package/src/template-processor.mjs +3 -2
package/README.md
CHANGED
|
@@ -21,11 +21,11 @@ npx @htyf-mp/cli
|
|
|
21
21
|
按照交互提示完成:
|
|
22
22
|
|
|
23
23
|
1. **输入应用目录名**(例如 `my-htyf-mp`,用于创建项目文件夹)。
|
|
24
|
-
2. **输入应用显示名称**(2–
|
|
24
|
+
2. **输入应用显示名称**(2–10 个中文/字母/数字)。
|
|
25
25
|
3. **选择模板类型**:
|
|
26
26
|
- `app-template`:React Native 应用小程序(对应 `type: 'app'`)。
|
|
27
27
|
- `game-template`:Godot 游戏小程序(对应 `type: 'game'`)。
|
|
28
|
-
- `
|
|
28
|
+
- `taro-template`:Taro 小程序模板。
|
|
29
29
|
4. 选择模板仓库镜像(当前默认使用 GitHub)。
|
|
30
30
|
|
|
31
31
|
CLI 会自动:
|
|
@@ -35,7 +35,40 @@ CLI 会自动:
|
|
|
35
35
|
- 生成 `app.json` 中的 `htyf` 配置(包含 `appid`、`name`、`zipUrl`、`appUrlConfig` 等)。
|
|
36
36
|
- 将模板拷贝到你指定的项目目录中。
|
|
37
37
|
|
|
38
|
-
> 各模板的具体用法,请参考生成项目中 `_apps_temp_` / `_game_temp_` / `
|
|
38
|
+
> 各模板的具体用法,请参考生成项目中 `_apps_temp_` / `_game_temp_` / `_taro_temp_` 目录下的 `README.md`。
|
|
39
|
+
|
|
40
|
+
## AI / 脚本直接调用
|
|
41
|
+
|
|
42
|
+
不带子命令时保留交互菜单;以下子命令均为非交互模式,不需要模拟按键或逐步回答问题。`--non-interactive` 可显式标注自动化调用。无终端且未指定命令时立即报错,不会挂起等待输入。
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 创建项目,--project 指定父目录;不会自动安装依赖
|
|
46
|
+
htyf init --name my-app --display-name 我的应用 --template taro --project ./workspace
|
|
47
|
+
|
|
48
|
+
# 构建,显式设置并写入 app.json 的 htyf.version
|
|
49
|
+
htyf build --project ./workspace/my-app --version 1.2.3 --platform ios
|
|
50
|
+
|
|
51
|
+
# 省略 --version 时使用现有版本,不自动递增
|
|
52
|
+
htyf build --project ./workspace/my-app
|
|
53
|
+
htyf debug --project ./workspace/my-app
|
|
54
|
+
|
|
55
|
+
# Godot 项目(项目根目录包含 project.godot)
|
|
56
|
+
htyf build --project ./my-game --version 1.0.0 --platform android \
|
|
57
|
+
--godot-bin /path/to/godot --godot-preset Android
|
|
58
|
+
|
|
59
|
+
# 指定清理范围;省略范围为 all,直接执行清理
|
|
60
|
+
htyf clean build --project ./workspace/my-app
|
|
61
|
+
htyf sync-deps --project ./workspace/my-app
|
|
62
|
+
htyf --help
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
可将 `htyf` 替换为 `npx @htyf-mp/cli` 或 `htyf-mp`。初始化必须提供 `--name`、`--display-name`、`--template`;模板支持 `taro`、`app`、`game` 及对应的 `*-template` 全名。目标目录已存在时失败,不覆盖项目。
|
|
66
|
+
|
|
67
|
+
`--project` 默认当前目录,其他相对路径基于此目录解析。Godot 支持 `--godot-project` 指定内部项目目录;可执行文件依次取 `--godot-bin`、`GODOT_EDITOR`、缓存、内置默认路径,不可执行时立即失败。预设默认随平台选择 `iOS` 或 `Android`。
|
|
68
|
+
|
|
69
|
+
成功退出码为 `0`,参数或执行失败为 `1`;输出为人类可读日志,构建完成输出产物路径。`debug` 为持续运行的调试服务,使用 Ctrl+C 停止。人工菜单的构建版本询问和 Godot 选项询问保留。`--debug` 表示详细日志,与 `debug` 子命令不同。
|
|
70
|
+
|
|
71
|
+
旧命令 `--clean [类型]`、`--sync-deps` 继续可用,`mp-build` / `mp-debug` 分别为 `build` / `debug` 的别名。使用 `--help` 时仅显示帮助,不执行操作。
|
|
39
72
|
|
|
40
73
|
## 项目操作命令
|
|
41
74
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@htyf-mp/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "红糖云服app小程序模板-cli",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"start": "node src/index.mjs",
|
|
16
|
-
"test": "
|
|
16
|
+
"test": "node --test tests/*.test.mjs"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"hongtangyun",
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
"publishConfig": {
|
|
83
83
|
"access": "public"
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "29d6076f403a5e856e08602ab61d57c887766787"
|
|
86
86
|
}
|
package/src/build.mjs
CHANGED
|
@@ -365,7 +365,10 @@ export async function mpBuildShell(newAppInfo, isGodot = false, buildOptions = {
|
|
|
365
365
|
};
|
|
366
366
|
|
|
367
367
|
// 提示用户输入或确认 Godot 导出选项
|
|
368
|
-
const godotOptions = await promptGodotOptions(
|
|
368
|
+
const godotOptions = await promptGodotOptions({
|
|
369
|
+
...godotDefaults, ...buildOptions.godot,
|
|
370
|
+
nonInteractive: buildOptions.nonInteractive
|
|
371
|
+
});
|
|
369
372
|
outputPath = path.resolve(godotOptions.targetBaseDir);
|
|
370
373
|
await fse.ensureDir(outputPath);
|
|
371
374
|
|
package/src/clean.mjs
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
|
|
3
|
+
export const HELP = `红糖云服 CLI
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
htyf 交互式菜单(需要终端)
|
|
7
|
+
htyf init --name my-app --display-name 我的应用 --template taro
|
|
8
|
+
htyf build --project ./my-app --version 1.2.3 --platform ios
|
|
9
|
+
htyf debug --project ./my-app
|
|
10
|
+
htyf clean [all|build|temp|logs|cache] --project ./my-app
|
|
11
|
+
htyf sync-deps --project ./my-app
|
|
12
|
+
|
|
13
|
+
所有子命令均不询问问题,可由 AI、脚本或 CI 直接调用。
|
|
14
|
+
--project <目录> 项目目录;init 时为父目录,默认当前目录
|
|
15
|
+
--non-interactive 显式要求非交互模式,必须指定子命令
|
|
16
|
+
--name <名称> init 必填,英文目录名称,如 my-app
|
|
17
|
+
--display-name <名称> init 必填,2–10 个中文、字母或数字
|
|
18
|
+
--template <模板> init 必填:taro、app、game(也支持 *-template)
|
|
19
|
+
--version <x.y.z> build/debug 使用的版本;省略则保留现有版本
|
|
20
|
+
--platform <平台> build/debug:ios(默认)或 android
|
|
21
|
+
--godot-bin <文件> Godot 可执行文件;也可使用 GODOT_EDITOR
|
|
22
|
+
--godot-project <目录> Godot 项目目录,默认自动检测
|
|
23
|
+
--godot-preset <名称> 默认 iOS 或 Android
|
|
24
|
+
--debug 详细日志(真机调试使用 debug 子命令)
|
|
25
|
+
--help, -h 帮助
|
|
26
|
+
|
|
27
|
+
兼容:--clean [类型]、--sync-deps、mp-build、mp-debug。
|
|
28
|
+
成功退出码 0,失败 1;debug 为持续运行服务,Ctrl+C 停止。
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export function parseCliArgs(args, isTTY = Boolean(process.stdin.isTTY)) {
|
|
32
|
+
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
|
|
33
|
+
help: { type: 'boolean', short: 'h' }, debug: { type: 'boolean' },
|
|
34
|
+
'non-interactive': { type: 'boolean' }, clean: { type: 'boolean' },
|
|
35
|
+
'sync-deps': { type: 'boolean' },
|
|
36
|
+
...Object.fromEntries(['project', 'name', 'display-name', 'template', 'version', 'platform',
|
|
37
|
+
'godot-bin', 'godot-project', 'godot-preset'].map(key => [key, { type: 'string' }]))
|
|
38
|
+
}});
|
|
39
|
+
if (values.help || positionals[0] === 'help') return { help: true };
|
|
40
|
+
const commands = [...positionals];
|
|
41
|
+
if (values.clean && values['sync-deps']) throw new Error('不能同时指定多个命令');
|
|
42
|
+
if (values.clean) commands.unshift('clean');
|
|
43
|
+
if (values['sync-deps']) commands.unshift('sync-deps');
|
|
44
|
+
let command = commands.shift();
|
|
45
|
+
command = ({ 'mp-build': 'build', 'mp-debug': 'debug' })[command] || command;
|
|
46
|
+
if (!command) {
|
|
47
|
+
if (values['non-interactive'] || !isTTY) throw new Error('非交互环境必须指定命令;运行 htyf --help 查看用法');
|
|
48
|
+
if (Object.keys(values).some(key => !['debug', 'project'].includes(key))) throw new Error('请为参数指定子命令');
|
|
49
|
+
return { command: 'interactive', values };
|
|
50
|
+
}
|
|
51
|
+
const allowed = {
|
|
52
|
+
init: ['name', 'display-name', 'template'],
|
|
53
|
+
build: ['version', 'platform', 'godot-bin', 'godot-project', 'godot-preset'],
|
|
54
|
+
debug: ['version', 'platform', 'godot-bin', 'godot-project', 'godot-preset'],
|
|
55
|
+
clean: ['clean'], 'sync-deps': ['sync-deps']
|
|
56
|
+
};
|
|
57
|
+
if (!Object.hasOwn(allowed, command)) throw new Error(`未知命令: ${command}`);
|
|
58
|
+
for (const key of Object.keys(values)) {
|
|
59
|
+
if (!['project', 'debug', 'non-interactive'].includes(key) && !allowed[command].includes(key)) {
|
|
60
|
+
throw new Error(`${command} 不支持 --${key}`);
|
|
61
|
+
}
|
|
62
|
+
if (typeof values[key] === 'string' && !values[key].trim()) throw new Error(`--${key} 不能为空`);
|
|
63
|
+
}
|
|
64
|
+
const cleanType = command === 'clean' ? commands.shift() || 'all' : undefined;
|
|
65
|
+
if (commands.length) throw new Error(`多余参数: ${commands.join(' ')}`);
|
|
66
|
+
if (cleanType && !['all', 'build', 'temp', 'logs', 'cache'].includes(cleanType)) throw new Error(`无效清理类型: ${cleanType}`);
|
|
67
|
+
if (values.version && !/^\d+\.\d+\.\d+$/.test(values.version)) throw new Error('--version 必须为 x.y.z');
|
|
68
|
+
if (values.platform && !['ios', 'android'].includes(values.platform)) throw new Error('--platform 必须为 ios 或 android');
|
|
69
|
+
if (command === 'init') {
|
|
70
|
+
for (const key of ['name', 'display-name', 'template']) if (!values[key]) throw new Error(`init 缺少 --${key}`);
|
|
71
|
+
values.template = values.template.replace(/-template$/, '') + '-template';
|
|
72
|
+
if (!['taro-template', 'app-template', 'game-template'].includes(values.template)) throw new Error('无效模板类型');
|
|
73
|
+
if (!/^([a-z]+)(-[a-z0-9]+)*$/.test(values.name)) throw new Error('目录名称格式应为 my-app');
|
|
74
|
+
if (!/^[\u4e00-\u9fa5a-zA-Z0-9]{2,10}$/.test(values['display-name'])) throw new Error('应用名称需为 2–10 个中文、字母或数字');
|
|
75
|
+
}
|
|
76
|
+
return { command, values, cleanType };
|
|
77
|
+
}
|
package/src/debug.mjs
CHANGED
|
@@ -73,7 +73,7 @@ async function waitForWebH5Output(options = {}) {
|
|
|
73
73
|
* entry: './src/index.js'
|
|
74
74
|
* }, false);
|
|
75
75
|
*/
|
|
76
|
-
export async function mpDebugShell(newAppInfo, isGodot = false) {
|
|
76
|
+
export async function mpDebugShell(newAppInfo, isGodot = false, buildOptions = {}) {
|
|
77
77
|
try {
|
|
78
78
|
// ========== 参数验证 ==========
|
|
79
79
|
// Godot 项目不需要 entry 字段,Web 类型也不需要;
|
|
@@ -102,7 +102,7 @@ export async function mpDebugShell(newAppInfo, isGodot = false) {
|
|
|
102
102
|
|
|
103
103
|
// ========== 构建小程序包 ==========
|
|
104
104
|
Logger.info('开始构建小程序包...');
|
|
105
|
-
const zipPath = await mpBuildShell(newAppInfo, isGodot, { skipWebBuild: isWeb });
|
|
105
|
+
const zipPath = await mpBuildShell(newAppInfo, isGodot, { ...buildOptions, skipWebBuild: isWeb });
|
|
106
106
|
const assetsPath = path.dirname(zipPath);
|
|
107
107
|
const appJsonPath = path.join(assetsPath, 'app.json');
|
|
108
108
|
|
package/src/export_godot.mjs
CHANGED
|
@@ -265,7 +265,7 @@ export async function promptGodotOptions(defaults = {}) {
|
|
|
265
265
|
if (
|
|
266
266
|
defaults.targetBaseDir &&
|
|
267
267
|
projectDir &&
|
|
268
|
-
defaults.appid &&
|
|
268
|
+
(defaults.appid || (defaults.nonInteractive && defaults.name)) &&
|
|
269
269
|
defaults.preset &&
|
|
270
270
|
defaults.platform
|
|
271
271
|
) {
|
|
@@ -278,12 +278,16 @@ export async function promptGodotOptions(defaults = {}) {
|
|
|
278
278
|
return {
|
|
279
279
|
targetBaseDir: String(defaults.targetBaseDir).trim(),
|
|
280
280
|
projectDir: projectDir,
|
|
281
|
-
name: String(defaults.appid).trim(),
|
|
281
|
+
name: String(defaults.appid || defaults.name).trim(),
|
|
282
|
+
nonInteractive: defaults.nonInteractive,
|
|
283
|
+
godotBin: defaults.godotBin,
|
|
282
284
|
preset: String(defaults.preset).trim(),
|
|
283
285
|
platform: defaults.platform
|
|
284
286
|
};
|
|
285
287
|
}
|
|
286
288
|
|
|
289
|
+
if (defaults.nonInteractive) throw new Error('缺少 Godot 导出参数');
|
|
290
|
+
|
|
287
291
|
// 通过交互式命令行获取用户输入
|
|
288
292
|
const answers = await inquirer.prompt([
|
|
289
293
|
{
|
|
@@ -415,22 +419,22 @@ async function promptForGodotBinaryPath(defaultPath) {
|
|
|
415
419
|
/**
|
|
416
420
|
* 解析并获取 Godot 编辑器可执行文件路径
|
|
417
421
|
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
422
|
+
* 优先级:显式参数 > 环境变量 > 缓存 > 默认路径
|
|
423
|
+
* 交互模式询问用户;非交互模式直接验证并使用已解析路径
|
|
420
424
|
*
|
|
421
425
|
* @returns {Promise<string>} Godot 编辑器可执行文件路径
|
|
422
426
|
* @throws {Error} 当路径不可执行时抛出错误
|
|
423
427
|
*/
|
|
424
|
-
async function resolveGodotBinary() {
|
|
428
|
+
async function resolveGodotBinary(options = {}) {
|
|
425
429
|
// 获取可能的默认值(优先级:环境变量 > 缓存 > 默认路径)
|
|
426
430
|
const override = process.env.GODOT_EDITOR ? process.env.GODOT_EDITOR.trim() : '';
|
|
427
431
|
const cached = readCachedGodotPath();
|
|
428
432
|
|
|
429
433
|
// 确定默认值
|
|
430
|
-
|
|
434
|
+
const defaultPath = options.godotBin || override || cached || DEFAULT_GODOT_BIN;
|
|
431
435
|
|
|
432
|
-
//
|
|
433
|
-
const godotPath = await promptForGodotBinaryPath(defaultPath);
|
|
436
|
+
// 自动化调用不进入输入提示
|
|
437
|
+
const godotPath = options.nonInteractive ? defaultPath : await promptForGodotBinaryPath(defaultPath);
|
|
434
438
|
|
|
435
439
|
// 验证路径是否可执行
|
|
436
440
|
if (!isExecutable(godotPath)) {
|
|
@@ -538,7 +542,7 @@ async function handleIosExport({ godotBin, projectDir, preset, targetBaseDir, na
|
|
|
538
542
|
*/
|
|
539
543
|
export async function exportGodot(options) {
|
|
540
544
|
// 解析并获取 Godot 编辑器路径
|
|
541
|
-
const godotBin = await resolveGodotBinary();
|
|
545
|
+
const godotBin = await resolveGodotBinary(options);
|
|
542
546
|
const { platform, projectDir, preset, targetBaseDir, name } = options;
|
|
543
547
|
|
|
544
548
|
// 执行两次 import 以确保资源正确导入
|
package/src/index.mjs
CHANGED
|
@@ -1,367 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { HELP, parseCliArgs } from './cli-options.mjs';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* @author CLI Team
|
|
17
|
-
* @version 2.2.0
|
|
18
|
-
* @module index
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import inquirer from 'inquirer';
|
|
22
|
-
import path from 'path';
|
|
23
|
-
import chalk from 'chalk';
|
|
24
|
-
import { requireCjs } from './cjs.mjs';
|
|
25
|
-
|
|
26
|
-
const lodash = requireCjs('lodash');
|
|
27
|
-
const fse = requireCjs('fs-extra');
|
|
28
|
-
import boxen from 'boxen';
|
|
29
|
-
import gradient from 'gradient-string';
|
|
30
|
-
import { confirm } from '@inquirer/prompts';
|
|
31
|
-
import { getProjectRoot } from './utils.mjs';
|
|
32
|
-
import { Logger, setLogLevel } from './logger.mjs';
|
|
33
|
-
import { CONSTANTS, ACTION_TYPES, LOG_LEVELS } from './constants.mjs';
|
|
34
|
-
import { ProjectInitializer } from './project-initializer.mjs';
|
|
35
|
-
import { mpBuildShell } from './build.mjs';
|
|
36
|
-
import { mpDebugShell } from './debug.mjs';
|
|
37
|
-
import { cleanShell } from './clean.mjs';
|
|
38
|
-
import { syncDepsShell } from './sync-deps.mjs';
|
|
39
|
-
import { incrementVersion, updateAppConfig } from './utils-functions.mjs';
|
|
40
|
-
|
|
41
|
-
const projectPath = getProjectRoot();
|
|
42
|
-
|
|
43
|
-
// ========== 全局状态管理 ==========
|
|
44
|
-
// 用于优雅退出,避免重复退出操作
|
|
45
|
-
let isExiting = false;
|
|
46
|
-
let currentProcess = null;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* 优雅退出处理函数
|
|
50
|
-
*
|
|
51
|
-
* 当收到退出信号(如 Ctrl+C)时:
|
|
52
|
-
* 1. 尝试关闭正在运行的进程
|
|
53
|
-
* 2. 延迟退出,给清理操作一些时间
|
|
54
|
-
* 3. 如果已经在退出中,强制退出
|
|
55
|
-
*/
|
|
56
|
-
function handleGracefulExit() {
|
|
57
|
-
if (isExiting) {
|
|
58
|
-
// 如果已经在退出中,强制退出
|
|
59
|
-
Logger.warn('强制退出...');
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
isExiting = true;
|
|
64
|
-
Logger.info('\n正在退出...');
|
|
65
|
-
|
|
66
|
-
// 如果有正在运行的进程,尝试关闭
|
|
67
|
-
if (currentProcess) {
|
|
68
|
-
try {
|
|
69
|
-
if (typeof currentProcess.close === 'function') {
|
|
70
|
-
currentProcess.close();
|
|
71
|
-
} else if (typeof currentProcess.kill === 'function') {
|
|
72
|
-
currentProcess.kill('SIGTERM');
|
|
73
|
-
}
|
|
74
|
-
} catch (error) {
|
|
75
|
-
Logger.debug(`关闭进程时出错: ${error.message}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// 延迟退出,给清理操作一些时间
|
|
80
|
-
setTimeout(() => {
|
|
81
|
-
Logger.info('再见!');
|
|
82
|
-
process.exit(0);
|
|
83
|
-
}, 100);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ========== 注册全局信号处理 ==========
|
|
87
|
-
// 使用 once 确保只注册一次
|
|
88
|
-
// 注意:某些模块(如 debug.mjs)可能会移除这些监听器并添加自己的处理
|
|
89
|
-
process.on('SIGINT', handleGracefulExit); // Ctrl+C
|
|
90
|
-
process.on('SIGTERM', handleGracefulExit); // 终止信号
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* 主启动函数
|
|
94
|
-
*
|
|
95
|
-
* 根据操作类型执行相应的功能:
|
|
96
|
-
* - MP_BUILD: 构建小程序包
|
|
97
|
-
* - MP_DEBUG: 启动真机调试服务器
|
|
98
|
-
* - CLEAN: 清理临时文件
|
|
99
|
-
* - QUIT: 退出程序
|
|
100
|
-
*
|
|
101
|
-
* @param {string} action - 操作类型(ACTION_TYPES 中的值)
|
|
102
|
-
*/
|
|
103
|
-
async function Start(action) {
|
|
104
|
-
const actionNames = {
|
|
105
|
-
[ACTION_TYPES.MP_BUILD]: '小程序构建',
|
|
106
|
-
[ACTION_TYPES.MP_DEBUG]: '小程序调试',
|
|
107
|
-
[ACTION_TYPES.SYNC_DEPS]: '同步依赖版本',
|
|
108
|
-
[ACTION_TYPES.CLEAN]: '清理模式',
|
|
109
|
-
[ACTION_TYPES.QUIT]: '退出'
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
Logger.info(`当前操作: ${actionNames[action] || '未知操作'}`);
|
|
113
|
-
|
|
114
|
-
// ========== 退出操作 ==========
|
|
115
|
-
if (action === ACTION_TYPES.QUIT) {
|
|
116
|
-
Logger.info('再见!');
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ========== 同步依赖版本 ==========
|
|
121
|
-
if (action === ACTION_TYPES.SYNC_DEPS) {
|
|
122
|
-
await syncDepsShell();
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ========== 清理操作 ==========
|
|
127
|
-
if (action === ACTION_TYPES.CLEAN) {
|
|
128
|
-
const { cleanType } = await inquirer.prompt([
|
|
129
|
-
{
|
|
130
|
-
type: 'list',
|
|
131
|
-
name: 'cleanType',
|
|
132
|
-
message: '请选择清理类型:',
|
|
133
|
-
choices: [
|
|
134
|
-
{ name: '🧹 清理所有临时文件', value: 'all' },
|
|
135
|
-
{ name: '📁 清理构建输出', value: 'build' },
|
|
136
|
-
{ name: '🗂️ 清理临时目录', value: 'temp' },
|
|
137
|
-
{ name: '📝 清理日志文件', value: 'logs' },
|
|
138
|
-
{ name: '💾 清理缓存文件', value: 'cache' },
|
|
139
|
-
{ name: '❌ 取消', value: 'cancel' }
|
|
140
|
-
]
|
|
141
|
-
}
|
|
142
|
-
]);
|
|
143
|
-
|
|
144
|
-
if (cleanType === 'cancel') {
|
|
145
|
-
Logger.info('已取消清理操作');
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const getCleanTypeName = (type) => {
|
|
150
|
-
switch (type) {
|
|
151
|
-
case 'all': return '所有';
|
|
152
|
-
case 'build': return '构建输出';
|
|
153
|
-
case 'temp': return '临时';
|
|
154
|
-
case 'logs': return '日志';
|
|
155
|
-
case 'cache': return '缓存';
|
|
156
|
-
default: return type;
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const confirmClean = await confirm({
|
|
161
|
-
message: `确定要清理${getCleanTypeName(cleanType)}文件吗?`,
|
|
162
|
-
default: false
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
if (confirmClean) {
|
|
166
|
-
await cleanShell(cleanType);
|
|
4
|
+
try {
|
|
5
|
+
const options = parseCliArgs(process.argv.slice(2));
|
|
6
|
+
if (options.help) {
|
|
7
|
+
console.log(HELP);
|
|
8
|
+
} else {
|
|
9
|
+
// 模块在加载时计算项目路径,必须先切换目录再导入。
|
|
10
|
+
if (options.values.project) process.chdir(options.values.project);
|
|
11
|
+
if (options.values.debug) {
|
|
12
|
+
const { setLogLevel } = await import('./logger.mjs');
|
|
13
|
+
setLogLevel(0);
|
|
14
|
+
}
|
|
15
|
+
if (options.command === 'interactive') {
|
|
16
|
+
await import('./interactive.mjs');
|
|
167
17
|
} else {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ========== 构建/调试操作 ==========
|
|
174
|
-
try {
|
|
175
|
-
// 读取应用配置
|
|
176
|
-
const appConfigPath = path.join(projectPath, 'app.json');
|
|
177
|
-
const appInfo = fse.readJsonSync(appConfigPath).htyf;
|
|
178
|
-
|
|
179
|
-
// 检测是否为 Godot 项目(通过检查是否存在 project.godot 文件)
|
|
180
|
-
const isGodot = fse.existsSync(path.join(projectPath, 'project.godot'));
|
|
181
|
-
Logger.info(`项目类型: ${isGodot ? 'Godot 游戏项目' : '普通小程序项目'}`);
|
|
182
|
-
|
|
183
|
-
if (!appInfo) {
|
|
184
|
-
Logger.error('应用配置不存在,请先在app.json中配置htyf');
|
|
185
|
-
return;
|
|
18
|
+
const { runCommand } = await import('./run-command.mjs');
|
|
19
|
+
await runCommand(options);
|
|
186
20
|
}
|
|
187
|
-
|
|
188
|
-
// ========== 版本处理 ==========
|
|
189
|
-
// 自动递增版本号作为默认值
|
|
190
|
-
const version = appInfo?.version;
|
|
191
|
-
const incrementedVersion = version ? incrementVersion(version) : '1.0.0';
|
|
192
|
-
const { versionName } = await inquirer.prompt([
|
|
193
|
-
{
|
|
194
|
-
type: 'input',
|
|
195
|
-
name: 'versionName',
|
|
196
|
-
message: '请输入应用版本名称 (格式: x.y.z):',
|
|
197
|
-
default: incrementedVersion,
|
|
198
|
-
validate: (input) => {
|
|
199
|
-
const _ipt = input || incrementedVersion;
|
|
200
|
-
return (!!_ipt && /^\d+(\.\d+){2}$/g.test(_ipt)) || '请输入正确的版本名称格式 (x.y.z)';
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
]);
|
|
204
|
-
|
|
205
|
-
Logger.info(`用户选择:`);
|
|
206
|
-
Logger.info(`版本名称: ${versionName}`);
|
|
207
|
-
|
|
208
|
-
// ========== 更新应用配置 ==========
|
|
209
|
-
// 合并用户输入的版本号到应用信息中
|
|
210
|
-
const newAppInfo = lodash.merge({}, appInfo, {
|
|
211
|
-
version: versionName,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// 更新 app.json 文件
|
|
215
|
-
if (!updateAppConfig(newAppInfo)) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// ========== 显示应用信息 ==========
|
|
220
|
-
console.log('\n' + boxen(
|
|
221
|
-
chalk.cyan('应用信息') + '\n\n' +
|
|
222
|
-
chalk.white('名称: ') + chalk.yellow(`${newAppInfo.name}`) + '\n' +
|
|
223
|
-
chalk.white('版本: ') + chalk.yellow(`${newAppInfo.version}`),
|
|
224
|
-
{
|
|
225
|
-
padding: 1,
|
|
226
|
-
margin: 1,
|
|
227
|
-
borderStyle: 'round',
|
|
228
|
-
borderColor: 'yellow'
|
|
229
|
-
}
|
|
230
|
-
));
|
|
231
|
-
|
|
232
|
-
Logger.info('开始执行操作...');
|
|
233
|
-
|
|
234
|
-
// ========== 执行相应操作 ==========
|
|
235
|
-
switch (action) {
|
|
236
|
-
case ACTION_TYPES.MP_BUILD:
|
|
237
|
-
// 构建小程序包
|
|
238
|
-
await mpBuildShell(newAppInfo, isGodot);
|
|
239
|
-
break;
|
|
240
|
-
case ACTION_TYPES.MP_DEBUG:
|
|
241
|
-
// 启动真机调试服务器
|
|
242
|
-
await mpDebugShell(newAppInfo, isGodot);
|
|
243
|
-
break;
|
|
244
|
-
default:
|
|
245
|
-
Logger.error(`未知的操作类型: ${action}`);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
} catch (error) {
|
|
249
|
-
Logger.error(`操作失败: ${error.message}`);
|
|
250
|
-
Logger.error(`错误详情: ${error.stack}`);
|
|
251
21
|
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(`htyf: ${error.message}`);
|
|
24
|
+
process.exitCode = 1;
|
|
252
25
|
}
|
|
253
|
-
|
|
254
|
-
// ========== 主程序入口 ==========
|
|
255
|
-
// 显示欢迎信息
|
|
256
|
-
console.log('\n' + boxen(
|
|
257
|
-
gradient.rainbow('🎯 红糖云服 应用 CLI 工具 v2.2.0') + '\n' +
|
|
258
|
-
chalk.gray('用于管理多应用项目的开发、构建和部署') + '\n' +
|
|
259
|
-
chalk.gray(`项目目录:`) + '\n\n' +
|
|
260
|
-
chalk.gray(`${projectPath}`) + '\n',
|
|
261
|
-
{
|
|
262
|
-
padding: 1,
|
|
263
|
-
margin: 1,
|
|
264
|
-
borderStyle: 'round',
|
|
265
|
-
borderColor: 'cyan'
|
|
266
|
-
}
|
|
267
|
-
));
|
|
268
|
-
|
|
269
|
-
// ========== 命令行参数处理 ==========
|
|
270
|
-
|
|
271
|
-
// 处理调试模式参数
|
|
272
|
-
if (process.argv.includes('--debug')) {
|
|
273
|
-
setLogLevel(LOG_LEVELS.DEBUG);
|
|
274
|
-
Logger.debug('调试模式已启用');
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// 记录启动日志
|
|
278
|
-
Logger.writeToFile('CLI 工具启动', 'INFO');
|
|
279
|
-
|
|
280
|
-
// 处理依赖同步参数(命令行直接同步,不进入交互界面)
|
|
281
|
-
if (process.argv.includes('--sync-deps')) {
|
|
282
|
-
await syncDepsShell({ skipConfirm: true });
|
|
283
|
-
process.exit(0);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// 处理清理参数(命令行直接清理,不进入交互界面)
|
|
287
|
-
if (process.argv.includes('--clean')) {
|
|
288
|
-
const cleanIndex = process.argv.indexOf('--clean');
|
|
289
|
-
const cleanType = process.argv[cleanIndex + 1] || 'all';
|
|
290
|
-
|
|
291
|
-
if (['all', 'build', 'temp', 'logs', 'cache'].includes(cleanType)) {
|
|
292
|
-
Logger.info(`执行命令行清理: ${cleanType}`);
|
|
293
|
-
await cleanShell(cleanType);
|
|
294
|
-
process.exit(0);
|
|
295
|
-
} else {
|
|
296
|
-
Logger.error(`无效的清理类型: ${cleanType}`);
|
|
297
|
-
Logger.error('支持的清理类型: all, build, temp, logs, cache');
|
|
298
|
-
process.exit(1);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// 显示帮助信息
|
|
303
|
-
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
304
|
-
console.log('\n' + boxen(
|
|
305
|
-
chalk.cyan('CLI 工具使用说明') + '\n\n' +
|
|
306
|
-
chalk.white('基本用法:') + '\n' +
|
|
307
|
-
chalk.gray(' node index.mjs # 启动交互式界面') + '\n' +
|
|
308
|
-
chalk.gray(' node index.mjs --debug # 启用调试模式') + '\n' +
|
|
309
|
-
chalk.gray(' node index.mjs --help # 显示帮助信息') + '\n\n' +
|
|
310
|
-
chalk.white('清理命令:') + '\n' +
|
|
311
|
-
chalk.gray(' node index.mjs --clean all # 清理所有临时文件') + '\n' +
|
|
312
|
-
chalk.gray(' node index.mjs --clean build # 清理构建输出') + '\n' +
|
|
313
|
-
chalk.gray(' node index.mjs --clean temp # 清理临时目录') + '\n' +
|
|
314
|
-
chalk.gray(' node index.mjs --clean logs # 清理日志文件') + '\n' +
|
|
315
|
-
chalk.gray(' node index.mjs --clean cache # 清理缓存文件') + '\n\n' +
|
|
316
|
-
chalk.gray(' node index.mjs --sync-deps # 同步依赖版本') + '\n\n' +
|
|
317
|
-
chalk.white('功能说明:') + '\n' +
|
|
318
|
-
chalk.gray(' 🆕 初始化新小程序项目 - 创建新的小程序项目') + '\n' +
|
|
319
|
-
chalk.gray(' 🔍 小程序打包 - 构建小程序包') + '\n' +
|
|
320
|
-
chalk.gray(' 📦 小程序调试 - 启动真机调试服务') + '\n' +
|
|
321
|
-
chalk.gray(' 🔄 同步依赖版本 - 将 package.json 依赖对齐 shared-output.json') + '\n' +
|
|
322
|
-
chalk.gray(' 🧹 清理模式 - 清理临时文件') + '\n',
|
|
323
|
-
{
|
|
324
|
-
padding: 1,
|
|
325
|
-
margin: 1,
|
|
326
|
-
borderStyle: 'round',
|
|
327
|
-
borderColor: 'blue'
|
|
328
|
-
}
|
|
329
|
-
));
|
|
330
|
-
process.exit(0);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
inquirer
|
|
334
|
-
.prompt([
|
|
335
|
-
{
|
|
336
|
-
type: 'rawlist',
|
|
337
|
-
name: 'index',
|
|
338
|
-
message: '请选择你想要执行的操作:',
|
|
339
|
-
choices: [
|
|
340
|
-
{ name: '🆕 初始化新小程序项目', value: ACTION_TYPES.INIT },
|
|
341
|
-
{ name: '🔍 小程序 - 打包小程序', value: ACTION_TYPES.MP_BUILD },
|
|
342
|
-
{ name: '📦 小程序 - 真机调试', value: ACTION_TYPES.MP_DEBUG },
|
|
343
|
-
{ name: '🔄 同步依赖版本 - 对齐 shared-output.json', value: ACTION_TYPES.SYNC_DEPS },
|
|
344
|
-
{ name: '🧹 清理模式 - 清理临时文件', value: ACTION_TYPES.CLEAN },
|
|
345
|
-
{ name: '👋 退出', value: ACTION_TYPES.QUIT },
|
|
346
|
-
],
|
|
347
|
-
},
|
|
348
|
-
])
|
|
349
|
-
.then(async ({ index }) => {
|
|
350
|
-
if (index === ACTION_TYPES.INIT) {
|
|
351
|
-
const initializer = new ProjectInitializer();
|
|
352
|
-
await initializer.initialize();
|
|
353
|
-
} else {
|
|
354
|
-
await Start(index);
|
|
355
|
-
}
|
|
356
|
-
})
|
|
357
|
-
.catch((error) => {
|
|
358
|
-
// 如果是用户取消(Ctrl+C),不显示错误
|
|
359
|
-
if (error.isTtyError || error.name === 'AbortError') {
|
|
360
|
-
handleGracefulExit();
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
Logger.error(`程序启动失败: ${error.message}`);
|
|
364
|
-
if (!isExiting) {
|
|
365
|
-
process.exit(1);
|
|
366
|
-
}
|
|
367
|
-
});
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 红糖云服应用 CLI 工具主入口
|
|
5
|
+
*
|
|
6
|
+
* 功能:
|
|
7
|
+
* - 初始化新小程序项目
|
|
8
|
+
* - 小程序打包构建
|
|
9
|
+
* - 小程序真机调试
|
|
10
|
+
* - 清理临时文件
|
|
11
|
+
*
|
|
12
|
+
* 支持项目类型:
|
|
13
|
+
* - 普通小程序项目(使用 webpack 构建)
|
|
14
|
+
* - Godot 游戏项目(使用 Godot 导出工具)
|
|
15
|
+
*
|
|
16
|
+
* @author CLI Team
|
|
17
|
+
* @version 2.2.0
|
|
18
|
+
* @module index
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import inquirer from 'inquirer';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import chalk from 'chalk';
|
|
24
|
+
import { requireCjs } from './cjs.mjs';
|
|
25
|
+
|
|
26
|
+
const lodash = requireCjs('lodash');
|
|
27
|
+
const fse = requireCjs('fs-extra');
|
|
28
|
+
import boxen from 'boxen';
|
|
29
|
+
import gradient from 'gradient-string';
|
|
30
|
+
import { confirm } from '@inquirer/prompts';
|
|
31
|
+
import { getProjectRoot } from './utils.mjs';
|
|
32
|
+
import { Logger } from './logger.mjs';
|
|
33
|
+
import { ACTION_TYPES } from './constants.mjs';
|
|
34
|
+
import { ProjectInitializer } from './project-initializer.mjs';
|
|
35
|
+
import { mpBuildShell } from './build.mjs';
|
|
36
|
+
import { mpDebugShell } from './debug.mjs';
|
|
37
|
+
import { cleanShell } from './clean.mjs';
|
|
38
|
+
import { syncDepsShell } from './sync-deps.mjs';
|
|
39
|
+
import { incrementVersion, updateAppConfig } from './utils-functions.mjs';
|
|
40
|
+
|
|
41
|
+
const projectPath = getProjectRoot();
|
|
42
|
+
|
|
43
|
+
// ========== 全局状态管理 ==========
|
|
44
|
+
// 用于优雅退出,避免重复退出操作
|
|
45
|
+
let isExiting = false;
|
|
46
|
+
let currentProcess = null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 优雅退出处理函数
|
|
50
|
+
*
|
|
51
|
+
* 当收到退出信号(如 Ctrl+C)时:
|
|
52
|
+
* 1. 尝试关闭正在运行的进程
|
|
53
|
+
* 2. 延迟退出,给清理操作一些时间
|
|
54
|
+
* 3. 如果已经在退出中,强制退出
|
|
55
|
+
*/
|
|
56
|
+
function handleGracefulExit() {
|
|
57
|
+
if (isExiting) {
|
|
58
|
+
// 如果已经在退出中,强制退出
|
|
59
|
+
Logger.warn('强制退出...');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
isExiting = true;
|
|
64
|
+
Logger.info('\n正在退出...');
|
|
65
|
+
|
|
66
|
+
// 如果有正在运行的进程,尝试关闭
|
|
67
|
+
if (currentProcess) {
|
|
68
|
+
try {
|
|
69
|
+
if (typeof currentProcess.close === 'function') {
|
|
70
|
+
currentProcess.close();
|
|
71
|
+
} else if (typeof currentProcess.kill === 'function') {
|
|
72
|
+
currentProcess.kill('SIGTERM');
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
Logger.debug(`关闭进程时出错: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 延迟退出,给清理操作一些时间
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
Logger.info('再见!');
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}, 100);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ========== 注册全局信号处理 ==========
|
|
87
|
+
// 使用 once 确保只注册一次
|
|
88
|
+
// 注意:某些模块(如 debug.mjs)可能会移除这些监听器并添加自己的处理
|
|
89
|
+
process.on('SIGINT', handleGracefulExit); // Ctrl+C
|
|
90
|
+
process.on('SIGTERM', handleGracefulExit); // 终止信号
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 主启动函数
|
|
94
|
+
*
|
|
95
|
+
* 根据操作类型执行相应的功能:
|
|
96
|
+
* - MP_BUILD: 构建小程序包
|
|
97
|
+
* - MP_DEBUG: 启动真机调试服务器
|
|
98
|
+
* - CLEAN: 清理临时文件
|
|
99
|
+
* - QUIT: 退出程序
|
|
100
|
+
*
|
|
101
|
+
* @param {string} action - 操作类型(ACTION_TYPES 中的值)
|
|
102
|
+
*/
|
|
103
|
+
async function Start(action) {
|
|
104
|
+
const actionNames = {
|
|
105
|
+
[ACTION_TYPES.MP_BUILD]: '小程序构建',
|
|
106
|
+
[ACTION_TYPES.MP_DEBUG]: '小程序调试',
|
|
107
|
+
[ACTION_TYPES.SYNC_DEPS]: '同步依赖版本',
|
|
108
|
+
[ACTION_TYPES.CLEAN]: '清理模式',
|
|
109
|
+
[ACTION_TYPES.QUIT]: '退出'
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
Logger.info(`当前操作: ${actionNames[action] || '未知操作'}`);
|
|
113
|
+
|
|
114
|
+
// ========== 退出操作 ==========
|
|
115
|
+
if (action === ACTION_TYPES.QUIT) {
|
|
116
|
+
Logger.info('再见!');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ========== 同步依赖版本 ==========
|
|
121
|
+
if (action === ACTION_TYPES.SYNC_DEPS) {
|
|
122
|
+
await syncDepsShell();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ========== 清理操作 ==========
|
|
127
|
+
if (action === ACTION_TYPES.CLEAN) {
|
|
128
|
+
const { cleanType } = await inquirer.prompt([
|
|
129
|
+
{
|
|
130
|
+
type: 'list',
|
|
131
|
+
name: 'cleanType',
|
|
132
|
+
message: '请选择清理类型:',
|
|
133
|
+
choices: [
|
|
134
|
+
{ name: '🧹 清理所有临时文件', value: 'all' },
|
|
135
|
+
{ name: '📁 清理构建输出', value: 'build' },
|
|
136
|
+
{ name: '🗂️ 清理临时目录', value: 'temp' },
|
|
137
|
+
{ name: '📝 清理日志文件', value: 'logs' },
|
|
138
|
+
{ name: '💾 清理缓存文件', value: 'cache' },
|
|
139
|
+
{ name: '❌ 取消', value: 'cancel' }
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
if (cleanType === 'cancel') {
|
|
145
|
+
Logger.info('已取消清理操作');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const getCleanTypeName = (type) => {
|
|
150
|
+
switch (type) {
|
|
151
|
+
case 'all': return '所有';
|
|
152
|
+
case 'build': return '构建输出';
|
|
153
|
+
case 'temp': return '临时';
|
|
154
|
+
case 'logs': return '日志';
|
|
155
|
+
case 'cache': return '缓存';
|
|
156
|
+
default: return type;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const confirmClean = await confirm({
|
|
161
|
+
message: `确定要清理${getCleanTypeName(cleanType)}文件吗?`,
|
|
162
|
+
default: false
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (confirmClean) {
|
|
166
|
+
await cleanShell(cleanType);
|
|
167
|
+
} else {
|
|
168
|
+
Logger.info('已取消清理操作');
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ========== 构建/调试操作 ==========
|
|
174
|
+
try {
|
|
175
|
+
// 读取应用配置
|
|
176
|
+
const appConfigPath = path.join(projectPath, 'app.json');
|
|
177
|
+
const appInfo = fse.readJsonSync(appConfigPath).htyf;
|
|
178
|
+
|
|
179
|
+
// 检测是否为 Godot 项目(通过检查是否存在 project.godot 文件)
|
|
180
|
+
const isGodot = fse.existsSync(path.join(projectPath, 'project.godot'));
|
|
181
|
+
Logger.info(`项目类型: ${isGodot ? 'Godot 游戏项目' : '普通小程序项目'}`);
|
|
182
|
+
|
|
183
|
+
if (!appInfo) {
|
|
184
|
+
throw new Error('应用配置不存在,请先在app.json中配置htyf');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ========== 版本处理 ==========
|
|
188
|
+
// 自动递增版本号作为默认值
|
|
189
|
+
const version = appInfo?.version;
|
|
190
|
+
const incrementedVersion = version ? incrementVersion(version) : '1.0.0';
|
|
191
|
+
const { versionName } = await inquirer.prompt([
|
|
192
|
+
{
|
|
193
|
+
type: 'input',
|
|
194
|
+
name: 'versionName',
|
|
195
|
+
message: '请输入应用版本名称 (格式: x.y.z):',
|
|
196
|
+
default: incrementedVersion,
|
|
197
|
+
validate: (input) => {
|
|
198
|
+
const _ipt = input || incrementedVersion;
|
|
199
|
+
return (!!_ipt && /^\d+(\.\d+){2}$/g.test(_ipt)) || '请输入正确的版本名称格式 (x.y.z)';
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
Logger.info(`用户选择:`);
|
|
205
|
+
Logger.info(`版本名称: ${versionName}`);
|
|
206
|
+
|
|
207
|
+
// ========== 更新应用配置 ==========
|
|
208
|
+
// 合并用户输入的版本号到应用信息中
|
|
209
|
+
const newAppInfo = lodash.merge({}, appInfo, {
|
|
210
|
+
version: versionName,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// 更新 app.json 文件
|
|
214
|
+
if (!updateAppConfig(newAppInfo)) {
|
|
215
|
+
throw new Error('更新 app.json 失败');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ========== 显示应用信息 ==========
|
|
219
|
+
console.log('\n' + boxen(
|
|
220
|
+
chalk.cyan('应用信息') + '\n\n' +
|
|
221
|
+
chalk.white('名称: ') + chalk.yellow(`${newAppInfo.name}`) + '\n' +
|
|
222
|
+
chalk.white('版本: ') + chalk.yellow(`${newAppInfo.version}`),
|
|
223
|
+
{
|
|
224
|
+
padding: 1,
|
|
225
|
+
margin: 1,
|
|
226
|
+
borderStyle: 'round',
|
|
227
|
+
borderColor: 'yellow'
|
|
228
|
+
}
|
|
229
|
+
));
|
|
230
|
+
|
|
231
|
+
Logger.info('开始执行操作...');
|
|
232
|
+
|
|
233
|
+
// ========== 执行相应操作 ==========
|
|
234
|
+
switch (action) {
|
|
235
|
+
case ACTION_TYPES.MP_BUILD:
|
|
236
|
+
// 构建小程序包
|
|
237
|
+
await mpBuildShell(newAppInfo, isGodot);
|
|
238
|
+
break;
|
|
239
|
+
case ACTION_TYPES.MP_DEBUG:
|
|
240
|
+
// 启动真机调试服务器
|
|
241
|
+
await mpDebugShell(newAppInfo, isGodot);
|
|
242
|
+
break;
|
|
243
|
+
default:
|
|
244
|
+
Logger.error(`未知的操作类型: ${action}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
} catch (error) {
|
|
248
|
+
Logger.error(`操作失败: ${error.message}`);
|
|
249
|
+
Logger.error(`错误详情: ${error.stack}`);
|
|
250
|
+
process.exitCode = 1;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ========== 主程序入口 ==========
|
|
255
|
+
// 显示欢迎信息
|
|
256
|
+
console.log('\n' + boxen(
|
|
257
|
+
gradient.rainbow('🎯 红糖云服 应用 CLI 工具 v2.2.0') + '\n' +
|
|
258
|
+
chalk.gray('用于管理多应用项目的开发、构建和部署') + '\n' +
|
|
259
|
+
chalk.gray(`项目目录:`) + '\n\n' +
|
|
260
|
+
chalk.gray(`${projectPath}`) + '\n',
|
|
261
|
+
{
|
|
262
|
+
padding: 1,
|
|
263
|
+
margin: 1,
|
|
264
|
+
borderStyle: 'round',
|
|
265
|
+
borderColor: 'cyan'
|
|
266
|
+
}
|
|
267
|
+
));
|
|
268
|
+
|
|
269
|
+
inquirer
|
|
270
|
+
.prompt([
|
|
271
|
+
{
|
|
272
|
+
type: 'rawlist',
|
|
273
|
+
name: 'index',
|
|
274
|
+
message: '请选择你想要执行的操作:',
|
|
275
|
+
choices: [
|
|
276
|
+
{ name: '🆕 初始化新小程序项目', value: ACTION_TYPES.INIT },
|
|
277
|
+
{ name: '🔍 小程序 - 打包小程序', value: ACTION_TYPES.MP_BUILD },
|
|
278
|
+
{ name: '📦 小程序 - 真机调试', value: ACTION_TYPES.MP_DEBUG },
|
|
279
|
+
{ name: '🔄 同步依赖版本 - 对齐 shared-output.json', value: ACTION_TYPES.SYNC_DEPS },
|
|
280
|
+
{ name: '🧹 清理模式 - 清理临时文件', value: ACTION_TYPES.CLEAN },
|
|
281
|
+
{ name: '👋 退出', value: ACTION_TYPES.QUIT },
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
])
|
|
285
|
+
.then(async ({ index }) => {
|
|
286
|
+
if (index === ACTION_TYPES.INIT) {
|
|
287
|
+
const initializer = new ProjectInitializer();
|
|
288
|
+
await initializer.initialize();
|
|
289
|
+
} else {
|
|
290
|
+
await Start(index);
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
.catch((error) => {
|
|
294
|
+
// 如果是用户取消(Ctrl+C),不显示错误
|
|
295
|
+
if (error.isTtyError || error.name === 'AbortError') {
|
|
296
|
+
handleGracefulExit();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
Logger.error(`程序启动失败: ${error.message}`);
|
|
300
|
+
if (!isExiting) {
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
import md5 from 'md5';
|
|
5
4
|
import ora from 'ora';
|
|
6
5
|
import { requireCjs } from './cjs.mjs';
|
|
7
6
|
|
|
@@ -27,7 +26,7 @@ export class ProjectInitializer {
|
|
|
27
26
|
this.processor = new TemplateProcessor(this.config);
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
async initialize() {
|
|
29
|
+
async initialize(options = {}) {
|
|
31
30
|
const spinner = ora('正在初始化项目...');
|
|
32
31
|
let tmpdir = null;
|
|
33
32
|
let rootPath = null;
|
|
@@ -53,7 +52,10 @@ export class ProjectInitializer {
|
|
|
53
52
|
|
|
54
53
|
try {
|
|
55
54
|
// 1. 获取用户输入
|
|
56
|
-
const userInputs =
|
|
55
|
+
const userInputs = options.nonInteractive ? {
|
|
56
|
+
appName: options.name, displayName: options.displayName,
|
|
57
|
+
templateType: options.template, nonInteractive: true, repoType: CONSTANTS.TEMPLATE_REPOS.GITHUB
|
|
58
|
+
} : await this.getUserInputs();
|
|
57
59
|
|
|
58
60
|
// 2. 验证输入
|
|
59
61
|
const validation = this.validateInputs(userInputs);
|
|
@@ -61,8 +63,7 @@ export class ProjectInitializer {
|
|
|
61
63
|
process.removeListener('SIGINT', handleExit);
|
|
62
64
|
process.removeListener('SIGTERM', handleExit);
|
|
63
65
|
spinner.fail('输入验证失败');
|
|
64
|
-
|
|
65
|
-
return;
|
|
66
|
+
throw new Error(validation.error);
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
// 3. 检查目录
|
|
@@ -71,8 +72,7 @@ export class ProjectInitializer {
|
|
|
71
72
|
process.removeListener('SIGINT', handleExit);
|
|
72
73
|
process.removeListener('SIGTERM', handleExit);
|
|
73
74
|
spinner.fail('目录已存在');
|
|
74
|
-
|
|
75
|
-
return;
|
|
75
|
+
throw new Error(`目录已存在: ${rootPath}`);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// 4. 创建项目(在此之前启动 spinner)
|
|
@@ -95,6 +95,8 @@ export class ProjectInitializer {
|
|
|
95
95
|
process.removeListener('SIGTERM', handleExit);
|
|
96
96
|
spinner.fail('项目初始化失败');
|
|
97
97
|
Logger.error('初始化失败:', error.message);
|
|
98
|
+
if (options.nonInteractive) throw error;
|
|
99
|
+
process.exitCode = 1;
|
|
98
100
|
if (error.exitCode === 128) {
|
|
99
101
|
Logger.error('Error: 目录已经存在。');
|
|
100
102
|
} else if (error.code === 'ENOTFOUND') {
|
|
@@ -151,6 +153,9 @@ export class ProjectInitializer {
|
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
validateInputs(inputs) {
|
|
156
|
+
if (!this.config.validateAppName(inputs.appName)) return { isValid: false, error: '应用目录名称格式无效' };
|
|
157
|
+
if (!this.config.validateDisplayName(inputs.displayName)) return { isValid: false, error: '应用名称需为 2–10 个中文、字母或数字' };
|
|
158
|
+
if (!Object.hasOwn(this.config.templates, inputs.templateType)) return { isValid: false, error: '模板类型无效' };
|
|
154
159
|
const pathValidation = FileSystemUtils.validatePath(inputs.appName);
|
|
155
160
|
if (!pathValidation.isValid) {
|
|
156
161
|
return { isValid: false, error: pathValidation.error };
|
|
@@ -164,8 +169,7 @@ export class ProjectInitializer {
|
|
|
164
169
|
|
|
165
170
|
// 创建临时目录
|
|
166
171
|
spinner.text = '正在准备临时目录...';
|
|
167
|
-
const tmpdir = path.join(os.tmpdir(),
|
|
168
|
-
fse.emptyDirSync(tmpdir);
|
|
172
|
+
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'htyf-init-'));
|
|
169
173
|
|
|
170
174
|
// 设置临时目录引用,以便在退出时清理
|
|
171
175
|
if (setTmpdirCallback) {
|
|
@@ -175,7 +179,7 @@ export class ProjectInitializer {
|
|
|
175
179
|
try {
|
|
176
180
|
// 克隆仓库
|
|
177
181
|
spinner.text = '正在克隆模板仓库...';
|
|
178
|
-
await this.processor.cloneRepository(repoType, tmpdir);
|
|
182
|
+
await this.processor.cloneRepository(repoType, tmpdir, { nonInteractive: userInputs.nonInteractive });
|
|
179
183
|
|
|
180
184
|
// 确定应用根路径
|
|
181
185
|
const template = this.config.templates[templateType];
|
|
@@ -241,18 +245,9 @@ export class ProjectInitializer {
|
|
|
241
245
|
// 移动到目标目录
|
|
242
246
|
spinner.text = '正在创建项目目录...';
|
|
243
247
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
items.forEach((item) => {
|
|
248
|
-
const source = path.join(appRootPath, item);
|
|
249
|
-
const destination = path.join(rootPath, item);
|
|
250
|
-
fse.moveSync(source, destination, { overwrite: true });
|
|
251
|
-
});
|
|
252
|
-
fse.removeSync(appRootPath);
|
|
253
|
-
} else {
|
|
254
|
-
fse.moveSync(appRootPath, rootPath);
|
|
255
|
-
}
|
|
248
|
+
// 创建过程中目录可能被其他进程占用;同样拒绝覆盖。
|
|
249
|
+
if (fs.existsSync(rootPath)) throw new Error(`目录已存在: ${rootPath}`);
|
|
250
|
+
fse.moveSync(appRootPath, rootPath, { overwrite: false });
|
|
256
251
|
|
|
257
252
|
// 清理临时目录
|
|
258
253
|
fse.removeSync(tmpdir);
|
|
@@ -262,7 +257,7 @@ export class ProjectInitializer {
|
|
|
262
257
|
} catch (error) {
|
|
263
258
|
// 清理临时目录
|
|
264
259
|
if (fs.existsSync(tmpdir)) {
|
|
265
|
-
|
|
260
|
+
fse.removeSync(tmpdir);
|
|
266
261
|
}
|
|
267
262
|
throw error;
|
|
268
263
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function runCommand({ command, values, cleanType }) {
|
|
5
|
+
if (command === 'init') {
|
|
6
|
+
const { ProjectInitializer } = await import('./project-initializer.mjs');
|
|
7
|
+
return new ProjectInitializer().initialize({
|
|
8
|
+
nonInteractive: true, name: values.name,
|
|
9
|
+
displayName: values['display-name'], template: values.template
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
if (command === 'clean') {
|
|
13
|
+
const { cleanShell } = await import('./clean.mjs');
|
|
14
|
+
return cleanShell(cleanType);
|
|
15
|
+
}
|
|
16
|
+
if (command === 'sync-deps') {
|
|
17
|
+
const { syncDepsShell } = await import('./sync-deps.mjs');
|
|
18
|
+
return syncDepsShell({ skipConfirm: true });
|
|
19
|
+
}
|
|
20
|
+
const config = JSON.parse(fs.readFileSync('app.json', 'utf8'));
|
|
21
|
+
if (!config.htyf || typeof config.htyf !== 'object' || Array.isArray(config.htyf)) {
|
|
22
|
+
throw new Error('请先在 app.json 中配置 htyf');
|
|
23
|
+
}
|
|
24
|
+
const appInfo = { ...config.htyf, version: values.version || config.htyf.version };
|
|
25
|
+
if (!/^\d+\.\d+\.\d+$/.test(appInfo.version || '')) throw new Error('缺少有效版本,请指定 --version x.y.z');
|
|
26
|
+
const { CONSTANTS } = await import('./constants.mjs');
|
|
27
|
+
CONSTANTS.BUNDLE_PLATFORM = values.platform || 'ios';
|
|
28
|
+
const isGodot = fs.existsSync('project.godot');
|
|
29
|
+
const godot = {};
|
|
30
|
+
if (values['godot-bin']) godot.godotBin = path.resolve(values['godot-bin']);
|
|
31
|
+
if (values['godot-project']) godot.projectDir = path.resolve(values['godot-project']);
|
|
32
|
+
if (values['godot-preset']) godot.preset = values['godot-preset'];
|
|
33
|
+
const buildOptions = { nonInteractive: true, godot };
|
|
34
|
+
if (values.version) {
|
|
35
|
+
const { updateAppConfig } = await import('./utils-functions.mjs');
|
|
36
|
+
if (!updateAppConfig(appInfo)) throw new Error('更新 app.json 失败');
|
|
37
|
+
}
|
|
38
|
+
if (command === 'build') {
|
|
39
|
+
const { mpBuildShell } = await import('./build.mjs');
|
|
40
|
+
const artifact = await mpBuildShell(appInfo, isGodot, buildOptions);
|
|
41
|
+
console.log(`构建产物: ${artifact}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const { mpDebugShell } = await import('./debug.mjs');
|
|
45
|
+
return mpDebugShell(appInfo, isGodot, buildOptions);
|
|
46
|
+
}
|
package/src/sync-deps.mjs
CHANGED
|
@@ -72,13 +72,11 @@ export async function syncDepsShell(options = {}) {
|
|
|
72
72
|
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
73
73
|
|
|
74
74
|
if (!fse.pathExistsSync(packageJsonPath)) {
|
|
75
|
-
|
|
76
|
-
return;
|
|
75
|
+
throw new Error(`未找到 package.json: ${packageJsonPath}`);
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
if (!fse.pathExistsSync(sharedOutputPath)) {
|
|
80
|
-
|
|
81
|
-
return;
|
|
79
|
+
throw new Error(`未找到 shared-output.json: ${sharedOutputPath}`);
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
const sharedVersions = fse.readJsonSync(sharedOutputPath);
|
|
@@ -14,14 +14,15 @@ export class TemplateProcessor {
|
|
|
14
14
|
this.config = config;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
async cloneRepository(repoUrl, tmpdir) {
|
|
17
|
+
async cloneRepository(repoUrl, tmpdir, options = {}) {
|
|
18
18
|
Logger.info('正在克隆模板仓库...');
|
|
19
19
|
Logger.debug(`仓库地址: ${repoUrl}`);
|
|
20
20
|
Logger.debug(`目标目录: ${tmpdir}`);
|
|
21
21
|
|
|
22
22
|
try {
|
|
23
23
|
await execa('git', ['clone', '--depth', '1', repoUrl, tmpdir], {
|
|
24
|
-
timeout: 60000 // 60秒超时
|
|
24
|
+
timeout: 60000, // 60秒超时
|
|
25
|
+
...(options.nonInteractive ? { env: { GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' } } : {})
|
|
25
26
|
});
|
|
26
27
|
Logger.success('模板仓库克隆完成');
|
|
27
28
|
} catch (error) {
|