@htyf-mp/cli 0.7.0 → 0.7.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/README.md CHANGED
@@ -21,11 +21,11 @@ npx @htyf-mp/cli
21
21
  按照交互提示完成:
22
22
 
23
23
  1. **输入应用目录名**(例如 `my-htyf-mp`,用于创建项目文件夹)。
24
- 2. **输入应用显示名称**(2–4 个中文/字母/数字)。
24
+ 2. **输入应用显示名称**(2–10 个中文/字母/数字)。
25
25
  3. **选择模板类型**:
26
26
  - `app-template`:React Native 应用小程序(对应 `type: 'app'`)。
27
27
  - `game-template`:Godot 游戏小程序(对应 `type: 'game'`)。
28
- - `web-template`:Web/H5 小程序(对应 `type: 'web'`)。
28
+ - `taro-template`:Taro 小程序模板。
29
29
  4. 选择模板仓库镜像(当前默认使用 GitHub)。
30
30
 
31
31
  CLI 会自动:
@@ -35,7 +35,42 @@ CLI 会自动:
35
35
  - 生成 `app.json` 中的 `htyf` 配置(包含 `appid`、`name`、`zipUrl`、`appUrlConfig` 等)。
36
36
  - 将模板拷贝到你指定的项目目录中。
37
37
 
38
- > 各模板的具体用法,请参考生成项目中 `_apps_temp_` / `_game_temp_` / `_web_temp_` 目录下的 `README.md`。
38
+ > 各模板的具体用法,请参考生成项目根目录的 `README.md`。RN/Godot 模板在 htyf-cli 中维护;。
39
+
40
+ AI 全量/增量迁移规则单独安装:[htyf-skills](https://github.com/htyf-mp-community/htyf-skills)。当前 CLI 的非交互命令和版本说明见 [仓库指南](https://github.com/htyf-mp-community/htyf-cli)。
41
+
42
+ ## AI / 脚本直接调用
43
+
44
+ 不带子命令时保留交互菜单;以下子命令均为非交互模式,不需要模拟按键或逐步回答问题。`--non-interactive` 可显式标注自动化调用。无终端且未指定命令时立即报错,不会挂起等待输入。
45
+
46
+ ```bash
47
+ # 创建项目,--project 指定父目录;不会自动安装依赖
48
+ htyf init --name my-app --display-name 我的应用 --template taro --project ./workspace
49
+
50
+ # 构建,显式设置并写入 app.json 的 htyf.version
51
+ htyf build --project ./workspace/my-app --version 1.2.3 --platform ios
52
+
53
+ # 省略 --version 时使用现有版本,不自动递增
54
+ htyf build --project ./workspace/my-app
55
+ htyf debug --project ./workspace/my-app
56
+
57
+ # Godot 项目(项目根目录包含 project.godot)
58
+ htyf build --project ./my-game --version 1.0.0 --platform android \
59
+ --godot-bin /path/to/godot --godot-preset Android
60
+
61
+ # 指定清理范围;省略范围为 all,直接执行清理
62
+ htyf clean build --project ./workspace/my-app
63
+ htyf sync-deps --project ./workspace/my-app
64
+ htyf --help
65
+ ```
66
+
67
+ 可将 `htyf` 替换为 `npx @htyf-mp/cli` 或 `htyf-mp`。初始化必须提供 `--name`、`--display-name`、`--template`;模板支持 `taro`、`app`、`game` 及对应的 `*-template` 全名。目标目录已存在时失败,不覆盖项目。
68
+
69
+ `--project` 默认当前目录,其他相对路径基于此目录解析。Godot 支持 `--godot-project` 指定内部项目目录;可执行文件依次取 `--godot-bin`、`GODOT_EDITOR`、缓存、内置默认路径,不可执行时立即失败。预设默认随平台选择 `iOS` 或 `Android`。
70
+
71
+ 成功退出码为 `0`,参数或执行失败为 `1`;输出为人类可读日志,构建完成输出产物路径。`debug` 为持续运行的调试服务,使用 Ctrl+C 停止。人工菜单的构建版本询问和 Godot 选项询问保留。`--debug` 表示详细日志,与 `debug` 子命令不同。
72
+
73
+ 旧命令 `--clean [类型]`、`--sync-deps` 继续可用,`mp-build` / `mp-debug` 分别为 `build` / `debug` 的别名。使用 `--help` 时仅显示帮助,不执行操作。
39
74
 
40
75
  ## 项目操作命令
41
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@htyf-mp/cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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": "echo \"Error: no test specified\" && exit 0"
16
+ "test": "node --test tests/*.test.mjs"
17
17
  },
18
18
  "keywords": [
19
19
  "hongtangyun",
@@ -31,7 +31,7 @@
31
31
  "repository": {
32
32
  "type": "git",
33
33
  "url": "https://github.com/htyf-mp-community/htyf-cli.git",
34
- "directory": "htyf-cli"
34
+ "directory": "packages/cli"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=18.0.0",
@@ -82,5 +82,5 @@
82
82
  "publishConfig": {
83
83
  "access": "public"
84
84
  },
85
- "gitHead": "0bb5ec21167c668e13d0a072a90edc153e4111e8"
85
+ "gitHead": "e19498f682a2b6c7d1c9ac07616ef7460ed52e14"
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(godotDefaults);
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
@@ -74,6 +74,8 @@ export async function cleanShell(cleanType = 'all') {
74
74
  for (const cleanPath of pathsToClean) {
75
75
  if (FileSystemUtils.cleanDirectory(cleanPath)) {
76
76
  cleanedCount++;
77
+ } else {
78
+ throw new Error(`清理目录失败: ${cleanPath}`);
77
79
  }
78
80
  }
79
81
 
@@ -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/config.mjs CHANGED
@@ -15,15 +15,15 @@ export class ProjectConfig {
15
15
  this.templates = {
16
16
  [CONSTANTS.TEMPLATE_TYPES.TARO_TEMPLATE]: {
17
17
  name: 'taro-template',
18
- tempPath: '_taro_temp_'
18
+ tempPath: 'packages/cli/_taro_temp_'
19
19
  },
20
20
  [CONSTANTS.TEMPLATE_TYPES.APP_TEMPLATE]: {
21
21
  name: 'app-template',
22
- tempPath: '_apps_temp_'
22
+ tempPath: 'packages/cli/_apps_temp_'
23
23
  },
24
24
  [CONSTANTS.TEMPLATE_TYPES.GAME_TEMPLATE]: {
25
25
  name: 'game-template',
26
- tempPath: '_game_temp_'
26
+ tempPath: 'packages/cli/_game_temp_'
27
27
  },
28
28
  };
29
29
  }
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
 
@@ -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
- let defaultPath = override || cached || DEFAULT_GODOT_BIN;
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
- * 红糖云服应用 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, 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
- 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
- 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 = await this.getUserInputs();
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
- Logger.error(validation.error);
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
- Logger.error(`目录已存在: ${rootPath}`);
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(), md5('__HTYF__'), appName);
168
- fse.emptyDirSync(tmpdir);
172
+ const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'htyf-init-'));
169
173
 
170
174
  // 设置临时目录引用,以便在退出时清理
171
175
  if (setTmpdirCallback) {
@@ -175,11 +179,12 @@ export class ProjectInitializer {
175
179
  try {
176
180
  // 克隆仓库
177
181
  spinner.text = '正在克隆模板仓库...';
178
- await this.processor.cloneRepository(repoType, tmpdir);
182
+ const template = this.config.templates[templateType];
183
+ await this.processor.cloneRepository(template.repository || repoType, tmpdir, { nonInteractive: userInputs.nonInteractive });
179
184
 
180
185
  // 确定应用根路径
181
- const template = this.config.templates[templateType];
182
- const appRootPath = path.join(tmpdir, 'packages/cli', template.tempPath);
186
+ const appRootPath = path.join(tmpdir, template.tempPath);
187
+ if (!fs.existsSync(appRootPath)) throw new Error(`模板目录不存在: ${template.tempPath}`);
183
188
 
184
189
  // 清理不需要的文件
185
190
  spinner.text = '正在清理模板文件...';
@@ -241,18 +246,9 @@ export class ProjectInitializer {
241
246
  // 移动到目标目录
242
247
  spinner.text = '正在创建项目目录...';
243
248
 
244
- if (fs.existsSync(rootPath)) {
245
- fse.ensureDirSync(rootPath);
246
- const items = fs.readdirSync(appRootPath);
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
- }
249
+ // 创建过程中目录可能被其他进程占用;同样拒绝覆盖。
250
+ if (fs.existsSync(rootPath)) throw new Error(`目录已存在: ${rootPath}`);
251
+ fse.moveSync(appRootPath, rootPath, { overwrite: false });
256
252
 
257
253
  // 清理临时目录
258
254
  fse.removeSync(tmpdir);
@@ -262,7 +258,7 @@ export class ProjectInitializer {
262
258
  } catch (error) {
263
259
  // 清理临时目录
264
260
  if (fs.existsSync(tmpdir)) {
265
- // fse.removeSync(tmpdir);
261
+ fse.removeSync(tmpdir);
266
262
  }
267
263
  throw error;
268
264
  }
@@ -299,7 +295,9 @@ export class ProjectInitializer {
299
295
  return commands.join('\n');
300
296
  })() + '\n\n' +
301
297
  chalk.blue('常用命令:') + '\n' +
302
- chalk.white(' npm run htyf # 构建红糖云服小程序') + '\n' +
298
+ chalk.white(templateType === CONSTANTS.TEMPLATE_TYPES.TARO_TEMPLATE
299
+ ? ' npm run build:htyf # 进入 Taro 平台菜单,选择打包或调试'
300
+ : ' npx @htyf-mp/cli # 打开构建与调试菜单') + '\n' +
303
301
  chalk.gray('💡 提示: 使用 --debug 参数可以查看详细的调试信息'),
304
302
  {
305
303
  padding: 1,
@@ -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
- Logger.error(`未找到 package.json: ${packageJsonPath}`);
76
- return;
75
+ throw new Error(`未找到 package.json: ${packageJsonPath}`);
77
76
  }
78
77
 
79
78
  if (!fse.pathExistsSync(sharedOutputPath)) {
80
- Logger.error(`未找到 shared-output.json: ${sharedOutputPath}`);
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) {
@@ -47,12 +48,11 @@ export class TemplateProcessor {
47
48
  path.join(tmpdir, 'README.md')
48
49
  ];
49
50
 
50
- const cliTemplatesDir = path.join(tmpdir, 'packages/cli');
51
51
  const selectedTemplate = this.config.templates[templateType];
52
52
  if (selectedTemplate) {
53
- const selectedPath = path.join(cliTemplatesDir, selectedTemplate.tempPath);
53
+ const selectedPath = path.join(tmpdir, selectedTemplate.tempPath);
54
54
  Object.values(this.config.templates).forEach((template) => {
55
- const templatePath = path.join(cliTemplatesDir, template.tempPath);
55
+ const templatePath = path.join(tmpdir, template.tempPath);
56
56
  if (templatePath !== selectedPath) {
57
57
  pathsToRemove.push(templatePath);
58
58
  }