feat(tools): 添加 CLI 模块管理命令和文档验证 demos

- CLI 新增 list/add/remove 命令管理项目模块
- 创建 demos 包验证模块文档正确性
- 包含 Timer/FSM/Pathfinding/Procgen/Spatial 5个模块的完整测试
This commit is contained in:
yhh
2025-12-26 22:09:01 +08:00
parent 4a16e30794
commit 881ffad3bc
11 changed files with 1460 additions and 4 deletions

View File

@@ -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();

View 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);
}