feat(tools): 添加 CLI 模块管理命令和文档验证 demos
- CLI 新增 list/add/remove 命令管理项目模块 - 创建 demos 包验证模块文档正确性 - 包含 Timer/FSM/Pathfinding/Procgen/Spatial 5个模块的完整测试
This commit is contained in:
@@ -8,8 +8,9 @@ import * as path from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { getPlatformChoices, getPlatforms, getAdapter } from './adapters/index.js';
|
||||
import type { PlatformType, ProjectConfig } from './adapters/types.js';
|
||||
import { AVAILABLE_MODULES, getModuleById, getAllModuleIds, type ModuleInfo } from './modules.js';
|
||||
|
||||
const VERSION = '1.0.0';
|
||||
const VERSION = '1.1.0';
|
||||
|
||||
/**
|
||||
* @zh 打印 Logo
|
||||
@@ -297,12 +298,275 @@ async function initCommand(options: { platform?: string }): Promise<void> {
|
||||
console.log();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Module Management Commands
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 列出可用模块
|
||||
* @en List available modules
|
||||
*/
|
||||
function listCommand(options: { category?: string }): void {
|
||||
printLogo();
|
||||
|
||||
console.log(chalk.bold(' Available Modules:\n'));
|
||||
|
||||
const categories = ['core', 'ai', 'utility', 'physics', 'rendering', 'network'] as const;
|
||||
const categoryNames: Record<string, string> = {
|
||||
core: '核心 | Core',
|
||||
ai: 'AI',
|
||||
utility: '工具 | Utility',
|
||||
physics: '物理 | Physics',
|
||||
rendering: '渲染 | Rendering',
|
||||
network: '网络 | Network'
|
||||
};
|
||||
|
||||
for (const category of categories) {
|
||||
const modules = AVAILABLE_MODULES.filter(m => m.category === category);
|
||||
if (modules.length === 0) continue;
|
||||
if (options.category && options.category !== category) continue;
|
||||
|
||||
console.log(chalk.cyan(` ─── ${categoryNames[category]} ───`));
|
||||
for (const mod of modules) {
|
||||
console.log(` ${chalk.green(mod.id.padEnd(15))} ${chalk.gray(mod.package)}`);
|
||||
console.log(` ${' '.repeat(15)} ${chalk.dim(mod.description)}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(chalk.gray(' Use `esengine add <module>` to add a module to your project.'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加模块到项目
|
||||
* @en Add module to project
|
||||
*/
|
||||
async function addCommand(moduleIds: string[], options: { yes?: boolean }): Promise<void> {
|
||||
printLogo();
|
||||
|
||||
const cwd = process.cwd();
|
||||
const packageJsonPath = path.join(cwd, 'package.json');
|
||||
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
console.log(chalk.red(' ✗ No package.json found. Run `npm init` first.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate modules
|
||||
const validModules: ModuleInfo[] = [];
|
||||
const invalidIds: string[] = [];
|
||||
|
||||
for (const id of moduleIds) {
|
||||
const mod = getModuleById(id);
|
||||
if (mod) {
|
||||
validModules.push(mod);
|
||||
} else {
|
||||
invalidIds.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidIds.length > 0) {
|
||||
console.log(chalk.red(` ✗ Unknown module(s): ${invalidIds.join(', ')}`));
|
||||
console.log(chalk.gray(` Available: ${getAllModuleIds().join(', ')}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (validModules.length === 0) {
|
||||
// Interactive selection
|
||||
const response = await prompts({
|
||||
type: 'multiselect',
|
||||
name: 'modules',
|
||||
message: 'Select modules to add:',
|
||||
choices: AVAILABLE_MODULES.map(m => ({
|
||||
title: `${m.id} - ${m.description}`,
|
||||
value: m.id,
|
||||
selected: false
|
||||
})),
|
||||
min: 1
|
||||
}, {
|
||||
onCancel: () => {
|
||||
console.log(chalk.yellow('\n Cancelled.'));
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
for (const id of response.modules) {
|
||||
const mod = getModuleById(id);
|
||||
if (mod) validModules.push(mod);
|
||||
}
|
||||
}
|
||||
|
||||
if (validModules.length === 0) {
|
||||
console.log(chalk.yellow(' No modules selected.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\n Adding modules:\n'));
|
||||
for (const mod of validModules) {
|
||||
console.log(` ${chalk.green('+')} ${mod.package}`);
|
||||
}
|
||||
|
||||
// Confirm
|
||||
if (!options.yes) {
|
||||
const confirm = await prompts({
|
||||
type: 'confirm',
|
||||
name: 'proceed',
|
||||
message: 'Proceed with installation?',
|
||||
initial: true
|
||||
});
|
||||
|
||||
if (!confirm.proceed) {
|
||||
console.log(chalk.yellow('\n Cancelled.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Install
|
||||
console.log();
|
||||
const deps: Record<string, string> = {};
|
||||
for (const mod of validModules) {
|
||||
deps[mod.package] = mod.version;
|
||||
}
|
||||
|
||||
const success = installDependencies(cwd, deps);
|
||||
|
||||
if (success) {
|
||||
console.log(chalk.bold('\n Done!'));
|
||||
console.log(chalk.gray('\n Import modules in your code:'));
|
||||
for (const mod of validModules) {
|
||||
console.log(chalk.cyan(` import { ... } from '${mod.package}';`));
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从项目移除模块
|
||||
* @en Remove module from project
|
||||
*/
|
||||
async function removeCommand(moduleIds: string[], options: { yes?: boolean }): Promise<void> {
|
||||
printLogo();
|
||||
|
||||
const cwd = process.cwd();
|
||||
const packageJsonPath = path.join(cwd, 'package.json');
|
||||
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
console.log(chalk.red(' ✗ No package.json found.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
const deps = pkg.dependencies || {};
|
||||
|
||||
// Find installed modules
|
||||
const installed = AVAILABLE_MODULES.filter(m => deps[m.package]);
|
||||
|
||||
if (installed.length === 0) {
|
||||
console.log(chalk.yellow(' No ESEngine modules installed.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate modules to remove
|
||||
let toRemove: ModuleInfo[] = [];
|
||||
|
||||
if (moduleIds.length === 0) {
|
||||
// Interactive selection
|
||||
const response = await prompts({
|
||||
type: 'multiselect',
|
||||
name: 'modules',
|
||||
message: 'Select modules to remove:',
|
||||
choices: installed.map(m => ({
|
||||
title: `${m.id} - ${m.package}`,
|
||||
value: m.id
|
||||
})),
|
||||
min: 1
|
||||
}, {
|
||||
onCancel: () => {
|
||||
console.log(chalk.yellow('\n Cancelled.'));
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
for (const id of response.modules) {
|
||||
const mod = getModuleById(id);
|
||||
if (mod) toRemove.push(mod);
|
||||
}
|
||||
} else {
|
||||
for (const id of moduleIds) {
|
||||
const mod = getModuleById(id);
|
||||
if (mod && deps[mod.package]) {
|
||||
toRemove.push(mod);
|
||||
} else if (!mod) {
|
||||
console.log(chalk.yellow(` ⚠ Unknown module: ${id}`));
|
||||
} else {
|
||||
console.log(chalk.yellow(` ⚠ Module not installed: ${id}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove.length === 0) {
|
||||
console.log(chalk.yellow(' No modules to remove.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\n Removing modules:\n'));
|
||||
for (const mod of toRemove) {
|
||||
console.log(` ${chalk.red('-')} ${mod.package}`);
|
||||
}
|
||||
|
||||
// Confirm
|
||||
if (!options.yes) {
|
||||
const confirm = await prompts({
|
||||
type: 'confirm',
|
||||
name: 'proceed',
|
||||
message: 'Proceed with removal?',
|
||||
initial: true
|
||||
});
|
||||
|
||||
if (!confirm.proceed) {
|
||||
console.log(chalk.yellow('\n Cancelled.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from package.json
|
||||
for (const mod of toRemove) {
|
||||
delete deps[mod.package];
|
||||
}
|
||||
pkg.dependencies = deps;
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), 'utf-8');
|
||||
|
||||
// Run uninstall
|
||||
const pm = detectPackageManager(cwd);
|
||||
const packages = toRemove.map(m => m.package).join(' ');
|
||||
const uninstallCmd = pm === 'pnpm'
|
||||
? `pnpm remove ${packages}`
|
||||
: pm === 'yarn'
|
||||
? `yarn remove ${packages}`
|
||||
: `npm uninstall ${packages}`;
|
||||
|
||||
console.log(chalk.gray(`\n Running ${uninstallCmd}...`));
|
||||
|
||||
try {
|
||||
execSync(uninstallCmd, { cwd, stdio: 'inherit' });
|
||||
console.log(chalk.bold('\n Done!'));
|
||||
} catch {
|
||||
console.log(chalk.yellow(`\n ⚠ Failed to run uninstall. Modules removed from package.json.`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CLI Setup
|
||||
// =========================================================================
|
||||
|
||||
// Setup CLI
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('esengine')
|
||||
.description('CLI tool for adding ESEngine ECS to your project')
|
||||
.description('CLI tool for ESEngine ECS framework')
|
||||
.version(VERSION);
|
||||
|
||||
program
|
||||
@@ -311,10 +575,30 @@ program
|
||||
.option('-p, --platform <platform>', 'Target platform (cocos, cocos2, laya, nodejs)')
|
||||
.action(initCommand);
|
||||
|
||||
// Default command: run init
|
||||
program
|
||||
.command('list')
|
||||
.alias('ls')
|
||||
.description('List available modules')
|
||||
.option('-c, --category <category>', 'Filter by category (core, ai, utility, physics, rendering, network)')
|
||||
.action(listCommand);
|
||||
|
||||
program
|
||||
.command('add [modules...]')
|
||||
.description('Add modules to your project')
|
||||
.option('-y, --yes', 'Skip confirmation')
|
||||
.action(addCommand);
|
||||
|
||||
program
|
||||
.command('remove [modules...]')
|
||||
.alias('rm')
|
||||
.description('Remove modules from your project')
|
||||
.option('-y, --yes', 'Skip confirmation')
|
||||
.action(removeCommand);
|
||||
|
||||
// Default command: show help
|
||||
program
|
||||
.action(() => {
|
||||
initCommand({});
|
||||
program.help();
|
||||
});
|
||||
|
||||
program.parse();
|
||||
|
||||
122
packages/tools/cli/src/modules.ts
Normal file
122
packages/tools/cli/src/modules.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @zh ESEngine 可用模块定义
|
||||
* @en ESEngine Available Modules Definition
|
||||
*/
|
||||
|
||||
export interface ModuleInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
package: string;
|
||||
version: string;
|
||||
description: string;
|
||||
category: 'core' | 'ai' | 'physics' | 'rendering' | 'network' | 'utility';
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 可用模块列表
|
||||
* @en Available modules list
|
||||
*/
|
||||
export const AVAILABLE_MODULES: ModuleInfo[] = [
|
||||
// Core
|
||||
{
|
||||
id: 'core',
|
||||
name: 'ECS Core',
|
||||
package: '@esengine/ecs-framework',
|
||||
version: 'latest',
|
||||
description: 'ECS 核心框架 | Core ECS framework',
|
||||
category: 'core'
|
||||
},
|
||||
{
|
||||
id: 'math',
|
||||
name: 'Math',
|
||||
package: '@esengine/ecs-framework-math',
|
||||
version: 'latest',
|
||||
description: '数学库 (向量、矩阵) | Math library (vectors, matrices)',
|
||||
category: 'core'
|
||||
},
|
||||
|
||||
// AI
|
||||
{
|
||||
id: 'fsm',
|
||||
name: 'FSM',
|
||||
package: '@esengine/fsm',
|
||||
version: 'latest',
|
||||
description: '有限状态机 | Finite State Machine',
|
||||
category: 'ai'
|
||||
},
|
||||
{
|
||||
id: 'behavior-tree',
|
||||
name: 'Behavior Tree',
|
||||
package: '@esengine/behavior-tree',
|
||||
version: 'latest',
|
||||
description: '行为树 AI 系统 | Behavior Tree AI system',
|
||||
category: 'ai'
|
||||
},
|
||||
{
|
||||
id: 'pathfinding',
|
||||
name: 'Pathfinding',
|
||||
package: '@esengine/pathfinding',
|
||||
version: 'latest',
|
||||
description: '寻路系统 (A*, NavMesh) | Pathfinding (A*, NavMesh)',
|
||||
category: 'ai'
|
||||
},
|
||||
|
||||
// Utility
|
||||
{
|
||||
id: 'timer',
|
||||
name: 'Timer',
|
||||
package: '@esengine/timer',
|
||||
version: 'latest',
|
||||
description: '定时器和冷却系统 | Timer and cooldown system',
|
||||
category: 'utility'
|
||||
},
|
||||
{
|
||||
id: 'spatial',
|
||||
name: 'Spatial',
|
||||
package: '@esengine/spatial',
|
||||
version: 'latest',
|
||||
description: '空间索引和 AOI 系统 | Spatial index and AOI system',
|
||||
category: 'utility'
|
||||
},
|
||||
{
|
||||
id: 'procgen',
|
||||
name: 'Procgen',
|
||||
package: '@esengine/procgen',
|
||||
version: 'latest',
|
||||
description: '程序化生成 (噪声、随机) | Procedural generation',
|
||||
category: 'utility'
|
||||
},
|
||||
{
|
||||
id: 'blueprint',
|
||||
name: 'Blueprint',
|
||||
package: '@esengine/blueprint',
|
||||
version: 'latest',
|
||||
description: '可视化脚本系统 | Visual scripting system',
|
||||
category: 'utility'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* @zh 获取模块信息
|
||||
* @en Get module info by id
|
||||
*/
|
||||
export function getModuleById(id: string): ModuleInfo | undefined {
|
||||
return AVAILABLE_MODULES.find(m => m.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 按分类获取模块
|
||||
* @en Get modules by category
|
||||
*/
|
||||
export function getModulesByCategory(category: ModuleInfo['category']): ModuleInfo[] {
|
||||
return AVAILABLE_MODULES.filter(m => m.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有模块 ID
|
||||
* @en Get all module IDs
|
||||
*/
|
||||
export function getAllModuleIds(): string[] {
|
||||
return AVAILABLE_MODULES.map(m => m.id);
|
||||
}
|
||||
Reference in New Issue
Block a user