From c4f7a13b74e523eb4257a883e2e35c7b329522d4 Mon Sep 17 00:00:00 2001
From: YHH <359807859@qq.com>
Date: Fri, 26 Dec 2025 16:18:59 +0800
Subject: [PATCH] feat(cli): add CLI tool for adding ECS framework to existing
projects (#339)
* feat(cli): add CLI tool for adding ECS framework to existing projects
- Support Cocos Creator 2.x/3.x, LayaAir 3.x, and Node.js platforms
- Auto-detect project type based on directory structure
- Generate ECSManager with full configuration (debug, remote debug, WebSocket URL)
- Auto-install dependencies with npm/yarn/pnpm detection
- Platform-specific decorators and lifecycle methods
* chore: add changeset for @esengine/cli
* fix(ci): fix YAML syntax error in ai-issue-helper workflow
* fix(cli): resolve file system race conditions (CodeQL)
* chore(ci): remove unused and broken workflows
* fix(ci): fix YAML encoding in release.yml
---
.changeset/clever-dragons-smile.md | 11 +
.github/labeler.yml | 32 --
.github/workflows/ai-batch-analyze-issues.yml | 73 ----
.github/workflows/ai-helper-tip.yml | 61 ---
.github/workflows/ai-issue-helper.yml | 80 ----
.github/workflows/ai-issue-moderator.yml | 56 ---
.github/workflows/batch-label-issues.yml | 160 --------
.github/workflows/cleanup-dependabot.yml | 146 --------
.github/workflows/issue-labeler.yml | 23 --
.github/workflows/issue-translator.yml | 28 --
.github/workflows/release.yml | 41 +--
.github/workflows/size-limit.yml | 46 ---
.github/workflows/welcome.yml | 58 ---
packages/tools/cli/package.json | 51 +++
packages/tools/cli/src/adapters/cocos.ts | 254 +++++++++++++
packages/tools/cli/src/adapters/cocos2.ts | 259 +++++++++++++
packages/tools/cli/src/adapters/index.ts | 54 +++
packages/tools/cli/src/adapters/laya.ts | 245 ++++++++++++
packages/tools/cli/src/adapters/nodejs.ts | 348 ++++++++++++++++++
packages/tools/cli/src/adapters/types.ts | 77 ++++
packages/tools/cli/src/cli.ts | 320 ++++++++++++++++
packages/tools/cli/src/index.ts | 8 +
packages/tools/cli/tsconfig.json | 19 +
pnpm-lock.yaml | 39 ++
24 files changed, 1703 insertions(+), 786 deletions(-)
create mode 100644 .changeset/clever-dragons-smile.md
delete mode 100644 .github/labeler.yml
delete mode 100644 .github/workflows/ai-batch-analyze-issues.yml
delete mode 100644 .github/workflows/ai-helper-tip.yml
delete mode 100644 .github/workflows/ai-issue-helper.yml
delete mode 100644 .github/workflows/ai-issue-moderator.yml
delete mode 100644 .github/workflows/batch-label-issues.yml
delete mode 100644 .github/workflows/cleanup-dependabot.yml
delete mode 100644 .github/workflows/issue-labeler.yml
delete mode 100644 .github/workflows/issue-translator.yml
delete mode 100644 .github/workflows/size-limit.yml
delete mode 100644 .github/workflows/welcome.yml
create mode 100644 packages/tools/cli/package.json
create mode 100644 packages/tools/cli/src/adapters/cocos.ts
create mode 100644 packages/tools/cli/src/adapters/cocos2.ts
create mode 100644 packages/tools/cli/src/adapters/index.ts
create mode 100644 packages/tools/cli/src/adapters/laya.ts
create mode 100644 packages/tools/cli/src/adapters/nodejs.ts
create mode 100644 packages/tools/cli/src/adapters/types.ts
create mode 100644 packages/tools/cli/src/cli.ts
create mode 100644 packages/tools/cli/src/index.ts
create mode 100644 packages/tools/cli/tsconfig.json
diff --git a/.changeset/clever-dragons-smile.md b/.changeset/clever-dragons-smile.md
new file mode 100644
index 00000000..b96c54eb
--- /dev/null
+++ b/.changeset/clever-dragons-smile.md
@@ -0,0 +1,11 @@
+---
+"@esengine/cli": minor
+---
+
+feat(cli): 添加 CLI 工具用于将 ECS 框架集成到现有项目
+
+- 支持 Cocos Creator 2.x/3.x、LayaAir 3.x、Node.js 平台
+- 自动检测项目类型
+- 生成完整配置的 ECSManager(调试模式、远程调试、WebSocket URL)
+- 自动安装依赖(支持 npm/yarn/pnpm)
+- 针对不同平台生成正确的装饰器和生命周期方法
diff --git a/.github/labeler.yml b/.github/labeler.yml
deleted file mode 100644
index 54aaab71..00000000
--- a/.github/labeler.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-# 自动标签配置
-# 根据 issue/PR 内容自动打标签
-
-'bug':
- - '/(bug|错误|崩溃|crash|error|exception|问题)/i'
-
-'enhancement':
- - '/(feature|功能|enhancement|improve|优化|建议)/i'
-
-'documentation':
- - '/(doc|文档|readme|guide|tutorial|教程)/i'
-
-'question':
- - '/(question|疑问|how to|如何|怎么)/i'
-
-'performance':
- - '/(performance|性能|slow|慢|lag|卡顿|optimize)/i'
-
-'core':
- - '/(@esengine\/ecs-framework|packages\/core|core package)/i'
-
-'editor':
- - '/(editor|编辑器|tauri)/i'
-
-'network':
- - '/(network|网络|multiplayer|多人)/i'
-
-'help wanted':
- - '/(help wanted|需要帮助|求助)/i'
-
-'good first issue':
- - '/(good first issue|新手友好|beginner)/i'
diff --git a/.github/workflows/ai-batch-analyze-issues.yml b/.github/workflows/ai-batch-analyze-issues.yml
deleted file mode 100644
index c81c0c08..00000000
--- a/.github/workflows/ai-batch-analyze-issues.yml
+++ /dev/null
@@ -1,73 +0,0 @@
-name: AI Batch Analyze Issues
-
-on:
- workflow_dispatch:
- inputs:
- mode:
- description: '分析模式'
- required: true
- type: choice
- options:
- - 'recent' # 最近 10 个 issue
- - 'open' # 所有打开的 issue
- - 'all' # 所有 issue(慎用)
- default: 'recent'
-
-permissions:
- issues: write
- contents: read
-
-jobs:
- analyze:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20.x'
-
- - name: Install GitHub CLI
- run: |
- gh --version || (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
- sudo apt update
- sudo apt install gh)
-
- - name: Batch Analyze Issues
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- MODE="${{ github.event.inputs.mode }}"
-
- # 获取 issue 列表
- if [ "$MODE" = "recent" ]; then
- echo "📊 分析最近 10 个 issue..."
- ISSUES=$(gh issue list --limit 10 --json number --jq '.[].number')
- elif [ "$MODE" = "open" ]; then
- echo "📊 分析所有打开的 issue..."
- ISSUES=$(gh issue list --state open --json number --jq '.[].number')
- else
- echo "📊 分析所有 issue(这可能需要很长时间)..."
- ISSUES=$(gh issue list --state all --limit 100 --json number --jq '.[].number')
- fi
-
- # 为每个 issue 添加 AI 分析评论
- for issue_num in $ISSUES; do
- echo "🤖 分析 Issue #$issue_num..."
-
- # 获取 issue 内容
- ISSUE_BODY=$(gh issue view $issue_num --json body --jq '.body')
- ISSUE_TITLE=$(gh issue view $issue_num --json title --jq '.title')
-
- # 添加触发评论
- gh issue comment $issue_num --body "@ai-helper 请帮我分析这个 issue" || true
-
- # 避免 API 限制
- sleep 2
- done
-
- echo "✅ 批量分析完成!"
- echo "查看结果:https://github.com/${{ github.repository }}/issues"
diff --git a/.github/workflows/ai-helper-tip.yml b/.github/workflows/ai-helper-tip.yml
deleted file mode 100644
index b6915000..00000000
--- a/.github/workflows/ai-helper-tip.yml
+++ /dev/null
@@ -1,61 +0,0 @@
-name: AI Helper Tip
-
-# 对所有新创建的 issue 自动回复 AI 助手使用说明(新老用户都适用)
-on:
- issues:
- types: [opened]
-
-permissions:
- issues: write
-
-jobs:
- tip:
- runs-on: ubuntu-latest
- steps:
- - name: Post AI Helper Usage Tip
- uses: actions/github-script@v7
- with:
- script: |
- const message = [
- "## 🤖 AI 助手可用 | AI Helper Available",
- "",
- "**中文说明:**",
- "",
- "本项目配备了 AI 智能助手,可以帮助你快速获得解答!",
- "",
- "**使用方法:** 在评论中提及 `@ai-helper`,AI 会自动搜索项目代码并提供解决方案。",
- "",
- "**示例:**",
- "```",
- "@ai-helper 如何创建一个新的 System?",
- "@ai-helper 这个报错是什么原因?",
- "```",
- "",
- "---",
- "",
- "**English:**",
- "",
- "This project has an AI assistant to help you get answers quickly!",
- "",
- "**How to use:** Mention `@ai-helper` in a comment, and AI will automatically search the codebase and provide solutions.",
- "",
- "**Examples:**",
- "```",
- "@ai-helper How do I create a new System?",
- "@ai-helper What causes this error?",
- "```",
- "",
- "---",
- "",
- "💡 *AI 助手基于代码库提供建议,复杂问题建议等待维护者回复*",
- "💡 *AI suggestions are based on the codebase. For complex issues, please wait for maintainer responses*"
- ].join('\n');
-
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- body: message
- });
-
- console.log('✅ AI helper tip posted successfully');
diff --git a/.github/workflows/ai-issue-helper.yml b/.github/workflows/ai-issue-helper.yml
deleted file mode 100644
index bc628304..00000000
--- a/.github/workflows/ai-issue-helper.yml
+++ /dev/null
@@ -1,80 +0,0 @@
-name: AI Issue Helper
-
-on:
- issue_comment:
- types: [created]
-
-permissions:
- issues: write
- contents: read
- models: read
-
-jobs:
- ai-helper:
- runs-on: ubuntu-latest
- # 只在真实用户提到 @ai-helper 时触发,忽略机器人评? if: |
- contains(github.event.comment.body, '@ai-helper') &&
- github.event.comment.user.type != 'Bot'
- steps:
- - name: Checkout Repository
- uses: actions/checkout@v4
-
- - name: Get Issue Details
- id: issue
- uses: actions/github-script@v7
- with:
- script: |
- const issue = await github.rest.issues.get({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number
- });
-
- // 限制长度,避免超?token 限制
- const maxLength = 1000;
- const truncate = (str, max) => {
- if (!str) return '';
- return str.length > max ? str.substring(0, max) + '...[内容过长已截断]' : str;
- };
-
- core.exportVariable('ISSUE_TITLE', truncate(issue.data.title || '', 200));
- core.exportVariable('ISSUE_BODY', truncate(issue.data.body || '', maxLength));
- core.exportVariable('COMMENT_BODY', truncate(context.payload.comment.body || '', 500));
- core.exportVariable('ISSUE_NUMBER', context.issue.number);
-
- - name: Create Prompt
- id: prompt
- run: |
- cat > prompt.txt << 'PROMPT_EOF'
- Issue #${{ env.ISSUE_NUMBER }}
-
- 标题: ${{ env.ISSUE_TITLE }}
-
- 内容: ${{ env.ISSUE_BODY }}
-
- 评论: ${{ env.COMMENT_BODY }}
-
- 请搜索项目代码并提供解决方案? PROMPT_EOF
-
- - name: AI Analysis
- uses: actions/ai-inference@v1
- id: ai
- with:
- model: 'gpt-4o'
- enable-github-mcp: true
- max-tokens: 1500
- system-prompt: |
- 你是 ECS Framework (TypeScript ECS 框架) ?AI 助手? 主要代码?packages/framework/core/src? 搜索相关代码后,用中文简洁回答问题,包含问题分析、解决方案和代码引用? prompt-file: prompt.txt
-
- - name: Post AI Response
- env:
- AI_RESPONSE: ${{ steps.ai.outputs.response }}
- uses: actions/github-script@v7
- with:
- script: |
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- body: process.env.AI_RESPONSE
- });
diff --git a/.github/workflows/ai-issue-moderator.yml b/.github/workflows/ai-issue-moderator.yml
deleted file mode 100644
index a50bf6f5..00000000
--- a/.github/workflows/ai-issue-moderator.yml
+++ /dev/null
@@ -1,56 +0,0 @@
-name: AI Issue Moderator
-
-on:
- issues:
- types: [opened]
- issue_comment:
- types: [created]
-
-permissions:
- issues: write
- contents: read
- models: read
-
-jobs:
- moderate:
- runs-on: ubuntu-latest
- steps:
- - name: Check Content
- uses: actions/ai-inference@v1
- id: check
- with:
- model: 'gpt-4o-mini'
- system-prompt: |
- 你是一个内容审查助手。
- 检查内容是否包含:
- 1. 垃圾信息或广告
- 2. 恶意或攻击性内容
- 3. 与项目完全无关的内容
-
- 只返回 "SPAM" 或 "OK",不要其他内容。
- prompt: |
- 标题:${{ github.event.issue.title || github.event.comment.body }}
-
- 内容:
- ${{ github.event.issue.body || github.event.comment.body }}
-
- - name: Mark as Spam
- if: contains(steps.check.outputs.response, 'SPAM')
- uses: actions/github-script@v7
- with:
- script: |
- // 添加 spam 标签
- github.rest.issues.addLabels({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- labels: ['spam']
- });
-
- // 添加评论
- github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- body: '🤖 这个内容被 AI 检测为可能的垃圾内容。如果这是误判,请联系维护者。\n\n🤖 This content was detected as potential spam by AI. If this is a false positive, please contact the maintainers.'
- });
diff --git a/.github/workflows/batch-label-issues.yml b/.github/workflows/batch-label-issues.yml
deleted file mode 100644
index 0593ad24..00000000
--- a/.github/workflows/batch-label-issues.yml
+++ /dev/null
@@ -1,160 +0,0 @@
-name: Batch Label Issues
-
-on:
- workflow_dispatch:
- inputs:
- mode:
- description: '标签模式'
- required: true
- type: choice
- options:
- - 'recent' # 最?20 ?issue
- - 'open' # 所有打开?issue
- - 'unlabeled' # 只处理没有标签的 issue
- - 'all' # 所?issue(慎用)
- default: 'recent'
-
- skip_labeled:
- description: '跳过已有标签?issue'
- required: false
- type: boolean
- default: true
-
-permissions:
- issues: write
- contents: read
-
-jobs:
- batch-label:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20.x'
-
- - name: Batch Label Issues
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- MODE="${{ github.event.inputs.mode }}"
- SKIP_LABELED="${{ github.event.inputs.skip_labeled }}"
-
- echo "📊 开始批量打标签..."
- echo "模式: $MODE"
- echo "跳过已标? $SKIP_LABELED"
-
- # 获取 issue 列表
- if [ "$MODE" = "recent" ]; then
- echo "📋 获取最?20 ?issue..."
- ISSUES=$(gh issue list --limit 20 --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
- elif [ "$MODE" = "open" ]; then
- echo "📋 获取所有打开?issue..."
- ISSUES=$(gh issue list --state open --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
- elif [ "$MODE" = "unlabeled" ]; then
- echo "📋 获取没有标签?issue..."
- ISSUES=$(gh issue list --state all --json number,labels,title,body --jq '.[] | select(.labels | length == 0) | {number, labels: [.labels[].name], title, body}')
- else
- echo "📋 获取所?issue(限?100 个)..."
- ISSUES=$(gh issue list --state all --limit 100 --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
- fi
-
- # 临时文件
- echo "$ISSUES" > /tmp/issues.json
-
- # 处理每个 issue
- cat /tmp/issues.json | jq -c '.' | while read -r issue; do
- ISSUE_NUM=$(echo "$issue" | jq -r '.number')
- EXISTING_LABELS=$(echo "$issue" | jq -r '.labels | join(",")')
- TITLE=$(echo "$issue" | jq -r '.title')
- BODY=$(echo "$issue" | jq -r '.body')
-
- echo ""
- echo "🔍 处理 Issue #$ISSUE_NUM: $TITLE"
- echo " 现有标签: $EXISTING_LABELS"
-
- # 跳过已有标签?issue
- if [ "$SKIP_LABELED" = "true" ] && [ ! -z "$EXISTING_LABELS" ]; then
- echo " ⏭️ 跳过(已有标签)"
- continue
- fi
-
- # 分析内容并打标签
- LABELS_TO_ADD=""
-
- # 检?bug
- if echo "$TITLE $BODY" | grep -iE "(bug|错误|崩溃|crash|error|exception|问题|fix)" > /dev/null; then
- LABELS_TO_ADD="$LABELS_TO_ADD bug"
- echo " 🐛 检测到: bug"
- fi
-
- # 检?feature request
- if echo "$TITLE $BODY" | grep -iE "(feature|功能|enhancement|improve|优化|建议|新增|添加|add)" > /dev/null; then
- LABELS_TO_ADD="$LABELS_TO_ADD enhancement"
- echo " ?检测到: enhancement"
- fi
-
- # 检?question
- if echo "$TITLE $BODY" | grep -iE "(question|疑问|how to|如何|怎么|为什么|why|咨询|\?|?" > /dev/null; then
- LABELS_TO_ADD="$LABELS_TO_ADD question"
- echo " ?检测到: question"
- fi
-
- # 检?documentation
- if echo "$TITLE $BODY" | grep -iE "(doc|文档|readme|guide|tutorial|教程|说明)" > /dev/null; then
- LABELS_TO_ADD="$LABELS_TO_ADD documentation"
- echo " 📖 检测到: documentation"
- fi
-
- # 检?performance
- if echo "$TITLE $BODY" | grep -iE "(performance|性能|slow|慢|lag|卡顿|optimize|优化)" > /dev/null; then
- LABELS_TO_ADD="$LABELS_TO_ADD performance"
- echo " ?检测到: performance"
- fi
-
- # 检?core
- if echo "$TITLE $BODY" | grep -iE "(@esengine/ecs-framework|packages/framework/core|core package|核心?" > /dev/null; then
- LABELS_TO_ADD="$LABELS_TO_ADD core"
- echo " 🎯 检测到: core"
- fi
-
- # 检?editor
- if echo "$TITLE $BODY" | grep -iE "(editor|编辑器|tauri)" > /dev/null; then
- LABELS_TO_ADD="$LABELS_TO_ADD editor"
- echo " 🎨 检测到: editor"
- fi
-
- # 检?network
- if echo "$TITLE $BODY" | grep -iE "(network|网络|multiplayer|多人|同步)" > /dev/null; then
- LABELS_TO_ADD="$LABELS_TO_ADD network"
- echo " 🌐 检测到: network"
- fi
-
- # 检?help wanted
- if echo "$TITLE $BODY" | grep -iE "(help wanted|需要帮助|求助)" > /dev/null; then
- LABELS_TO_ADD="$LABELS_TO_ADD help wanted"
- echo " 🆘 检测到: help wanted"
- fi
-
- # 添加标签
- if [ ! -z "$LABELS_TO_ADD" ]; then
- echo " ?添加标签: $LABELS_TO_ADD"
- for label in $LABELS_TO_ADD; do
- gh issue edit $ISSUE_NUM --add-label "$label" 2>&1 | grep -v "already exists" || true
- done
- echo " 💬 添加说明评论..."
- gh issue comment $ISSUE_NUM --body $'🤖 自动标签系统检测到?issue 并添加了相关标签。如有误判,请告知维护者。\n\n🤖 Auto-labeling system detected and labeled this issue. Please let maintainers know if this is incorrect.' || true
- else
- echo " ℹ️ 未检测到明确类型"
- fi
-
- # 避免 API 限制
- sleep 1
- done
-
- echo ""
- echo "?批量标签完成?
- echo "查看结果: https://github.com/${{ github.repository }}/issues"
diff --git a/.github/workflows/cleanup-dependabot.yml b/.github/workflows/cleanup-dependabot.yml
deleted file mode 100644
index 1010e9e4..00000000
--- a/.github/workflows/cleanup-dependabot.yml
+++ /dev/null
@@ -1,146 +0,0 @@
-name: Cleanup Old Dependabot PRs
-
-# 手动触发的 workflow,用于清理堆积的 Dependabot PR
-on:
- workflow_dispatch:
- inputs:
- days_old:
- description: '关闭多少天前创建的 PR(默认 7 天)'
- required: false
- default: '7'
- dry_run:
- description: '试运行模式(true=仅显示,不关闭)'
- required: false
- default: 'true'
- type: choice
- options:
- - 'true'
- - 'false'
-
-jobs:
- cleanup:
- runs-on: ubuntu-latest
- permissions:
- pull-requests: write
- issues: write
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: List and Close Old Dependabot PRs
- uses: actions/github-script@v7
- with:
- script: |
- const daysOld = parseInt('${{ github.event.inputs.days_old }}') || 7;
- const dryRun = '${{ github.event.inputs.dry_run }}' === 'true';
- const cutoffDate = new Date();
- cutoffDate.setDate(cutoffDate.getDate() - daysOld);
-
- console.log(`🔍 查找超过 ${daysOld} 天的 Dependabot PR...`);
- console.log(`📅 截止日期: ${cutoffDate.toISOString()}`);
- console.log(`🏃 模式: ${dryRun ? '试运行(不会实际关闭)' : '实际执行'}`);
- console.log('---');
-
- // 获取所有 Dependabot PR
- const { data: pulls } = await github.rest.pulls.list({
- owner: context.repo.owner,
- repo: context.repo.repo,
- state: 'open',
- per_page: 100
- });
-
- const dependabotPRs = pulls.filter(pr =>
- pr.user.login === 'dependabot[bot]' &&
- new Date(pr.created_at) < cutoffDate
- );
-
- console.log(`📊 找到 ${dependabotPRs.length} 个符合条件的 Dependabot PR`);
- console.log('');
-
- if (dependabotPRs.length === 0) {
- console.log('✅ 没有需要清理的 PR');
- return;
- }
-
- // 按类型分组
- const byType = {
- dev: [],
- prod: [],
- actions: [],
- other: []
- };
-
- for (const pr of dependabotPRs) {
- const title = pr.title.toLowerCase();
- const labels = pr.labels.map(l => l.name);
-
- let type = 'other';
- if (title.includes('dev-dependencies') || title.includes('development')) {
- type = 'dev';
- } else if (title.includes('production-dependencies')) {
- type = 'prod';
- } else if (labels.includes('github-actions')) {
- type = 'actions';
- }
-
- byType[type].push(pr);
- }
-
- console.log('📋 PR 分类统计:');
- console.log(` 🔧 开发依赖: ${byType.dev.length} 个`);
- console.log(` 📦 生产依赖: ${byType.prod.length} 个`);
- console.log(` ⚙️ GitHub Actions: ${byType.actions.length} 个`);
- console.log(` ❓ 其他: ${byType.other.length} 个`);
- console.log('');
-
- // 处理每个 PR
- for (const pr of dependabotPRs) {
- const age = Math.floor((Date.now() - new Date(pr.created_at)) / (1000 * 60 * 60 * 24));
-
- console.log(`${dryRun ? '🔍' : '🗑️ '} #${pr.number}: ${pr.title}`);
- console.log(` 创建时间: ${pr.created_at} (${age} 天前)`);
- console.log(` 链接: ${pr.html_url}`);
-
- if (!dryRun) {
- await github.rest.pulls.update({
- owner: context.repo.owner,
- repo: context.repo.repo,
- pull_number: pr.number,
- state: 'closed'
- });
-
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: pr.number,
- body: `🤖 **自动关闭旧的 Dependabot PR**
-
-此 PR 已超过 ${daysOld} 天未合并,已被自动关闭以清理积压。
-
-📌 **下一步:**
-- Dependabot 已配置为月度运行,届时会创建新的分组更新
-- 新的 Mergify 规则会智能处理不同类型的依赖更新
-- 开发依赖和 GitHub Actions 会自动合并(即使 CI 失败)
-- 生产依赖需要 CI 通过才会自动合并
-
-如果需要立即应用此更新,请手动更新依赖。
-
----
-*此操作由仓库维护者手动触发的清理工作流执行*`
- });
-
- console.log(' ✅ 已关闭并添加说明');
- } else {
- console.log(' ℹ️ 试运行模式 - 未执行操作');
- }
- console.log('');
- }
-
- console.log('---');
- if (dryRun) {
- console.log(`✨ 试运行完成!共发现 ${dependabotPRs.length} 个待清理的 PR`);
- console.log('💡 要实际执行清理,请将 dry_run 参数设为 false 重新运行');
- } else {
- console.log(`✅ 清理完成!已关闭 ${dependabotPRs.length} 个 Dependabot PR`);
- }
diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml
deleted file mode 100644
index 1db79962..00000000
--- a/.github/workflows/issue-labeler.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-name: Issue Labeler
-
-on:
- issues:
- types: [opened, edited]
-
-permissions:
- issues: write
- contents: read
-
-jobs:
- label:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Label Issues
- uses: github/issue-labeler@v3.4
- with:
- repo-token: ${{ secrets.GITHUB_TOKEN }}
- configuration-path: .github/labeler.yml
- enable-versioned-regex: 1
diff --git a/.github/workflows/issue-translator.yml b/.github/workflows/issue-translator.yml
deleted file mode 100644
index 3b783e93..00000000
--- a/.github/workflows/issue-translator.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-name: Issue Translator
-
-on:
- issue_comment:
- types: [created]
- issues:
- types: [opened]
-
-permissions:
- issues: write
-
-jobs:
- translate:
- runs-on: ubuntu-latest
- steps:
- - name: Translate Issues
- uses: tomsun28/issues-translate-action@v2.7
- with:
- IS_MODIFY_TITLE: false
- # 设置为 true 会修改标题,false 只在评论中添加翻译
- CUSTOM_BOT_NOTE: |
-
- 🌏 Translation / 翻译
-
- Bot detected the issue body's language is not English, translate it automatically.
- 机器人检测到 issue 内容非英文,自动翻译。
-
-
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e144780d..7719f05a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,7 +1,6 @@
name: Release NPM Packages
on:
- # 标签触发:支?v* ?{package}-v* 格式
# Tag trigger: supports v* and {package}-v* formats
push:
tags:
@@ -15,12 +14,11 @@ on:
- 'physics-rapier2d-v*'
- 'worker-generator-v*'
- # 保留手动触发选项
- # Keep manual trigger option
+ # Manual trigger option
workflow_dispatch:
inputs:
package:
- description: '选择要发布的?| Select package to publish'
+ description: 'Select package to publish'
required: true
type: choice
options:
@@ -33,7 +31,7 @@ on:
- physics-rapier2d
- worker-generator
version_type:
- description: '版本更新类型 | Version bump type'
+ description: 'Version bump type'
required: true
type: choice
options:
@@ -61,11 +59,10 @@ jobs:
id: parse
run: |
if [ "${{ github.event_name }}" = "push" ]; then
- # 从标签解析包名和版本 | Parse package and version from tag
+ # Parse package and version from tag
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
- # 解析格式:v1.0.0 ?package-v1.0.0
# Parse format: v1.0.0 or package-v1.0.0
if [[ "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
PACKAGE="core"
@@ -82,10 +79,9 @@ jobs:
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "mode=tag" >> $GITHUB_OUTPUT
- echo "📦 Package: $PACKAGE"
- echo "📌 Version: $VERSION"
+ echo "Package: $PACKAGE"
+ echo "Version: $VERSION"
else
- # 手动触发:从 package.json 读取?bump 版本
# Manual trigger: read from package.json and bump version
PACKAGE="${{ github.event.inputs.package }}"
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
@@ -112,7 +108,6 @@ jobs:
PACKAGE="${{ steps.parse.outputs.package }}"
EXPECTED_VERSION="${{ steps.parse.outputs.version }}"
- # 获取 package.json 中的版本
# Get version from package.json
ACTUAL_VERSION=$(node -p "require('./packages/$PACKAGE/package.json').version")
@@ -125,7 +120,7 @@ jobs:
exit 1
fi
- echo "?Version verified: $EXPECTED_VERSION"
+ echo "Version verified: $EXPECTED_VERSION"
- name: Bump version (manual mode)
if: steps.parse.outputs.mode == 'manual'
@@ -147,7 +142,7 @@ jobs:
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
- echo "📌 Bumped version: $CURRENT ?$NEW_VERSION"
+ echo "Bumped version: $CURRENT -> $NEW_VERSION"
- name: Set final version
id: version
@@ -188,19 +183,18 @@ jobs:
with:
tag_name: ${{ steps.parse.outputs.tag }}
name: "${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}"
- # 不设置为 latest,latest 保留给编辑器热更新使? # Don't mark as latest, reserve latest for editor auto-update
make_latest: false
body: |
- ## 🚀 @esengine/${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}
+ ## @esengine/${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}
- 📦 **NPM**: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
+ **NPM**: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
```bash
npm install @esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}
```
---
- *自动发布 | Auto-released by GitHub Actions*
+ *Auto-released by GitHub Actions*
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -215,15 +209,16 @@ jobs:
delete-branch: true
title: "chore(${{ steps.parse.outputs.package }}): Release v${{ steps.version.outputs.value }}"
body: |
- ## 🚀 Release v${{ steps.version.outputs.value }}
+ ## Release v${{ steps.version.outputs.value }}
- ?PR 更新 `@esengine/${{ steps.parse.outputs.package }}` 包的版本?
- ### 变更
- - ?已发布到 npm: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
- - ?更新 `packages/${{ steps.parse.outputs.package }}/package.json` ?`${{ steps.version.outputs.value }}`
+ This PR updates `@esengine/${{ steps.parse.outputs.package }}` package version.
+
+ ### Changes
+ - Published to npm: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
+ - Updated `packages/${{ steps.parse.outputs.package }}/package.json` to `${{ steps.version.outputs.value }}`
---
- *?PR 由发布工作流自动创建*
+ *This PR was automatically created by the release workflow*
labels: |
release
${{ steps.parse.outputs.package }}
diff --git a/.github/workflows/size-limit.yml b/.github/workflows/size-limit.yml
deleted file mode 100644
index f8a1c7b1..00000000
--- a/.github/workflows/size-limit.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-name: Size Limit
-
-on:
- pull_request:
- branches:
- - master
- - main
- paths:
- - 'packages/framework/core/src/**'
- - 'packages/framework/core/package.json'
- - '.size-limit.json'
-
-permissions:
- contents: read
- pull-requests: write
- issues: write
-
-jobs:
- size:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20.x'
- cache: 'pnpm'
-
- - name: Install dependencies
- run: pnpm install
-
- - name: Build core package
- run: |
- cd packages/framework/core
- pnpm run build:npm
-
- - name: Check bundle size
- uses: andresz1/size-limit-action@v1
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- skip_step: install
diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml
deleted file mode 100644
index 2079048e..00000000
--- a/.github/workflows/welcome.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-name: Welcome
-
-on:
- issues:
- types: [opened]
- pull_request_target:
- types: [opened]
-
-permissions:
- issues: write
- pull-requests: write
-
-jobs:
- welcome:
- runs-on: ubuntu-latest
- steps:
- - name: Welcome new contributors
- uses: actions/first-interaction@v1.3.0
- with:
- repo-token: ${{ secrets.GITHUB_TOKEN }}
- issue-message: |
- 👋 你好!感谢你提交第一个 issue!
-
- 我们会尽快查看并回复。同时,建议你:
- - 📚 查看[文档](https://esengine.github.io/ecs-framework/)
- - 🤖 使用 [AI 文档助手](https://deepwiki.com/esengine/esengine)
- - 💬 加入 [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
-
- ---
-
- 👋 Hello! Thanks for opening your first issue!
-
- We'll review it as soon as possible. Meanwhile, you might want to:
- - 📚 Check the [documentation](https://esengine.github.io/ecs-framework/)
- - 🤖 Use [AI documentation assistant](https://deepwiki.com/esengine/esengine)
-
- pr-message: |
- 👋 你好!感谢你提交第一个 Pull Request!
-
- 在我们 Review 之前,请确保:
- - ✅ 代码遵循项目规范
- - ✅ 通过所有测试
- - ✅ 更新了相关文档
- - ✅ Commit 遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范
-
- 查看完整的[贡献指南](https://github.com/esengine/esengine/blob/master/CONTRIBUTING.md)。
-
- ---
-
- 👋 Hello! Thanks for your first Pull Request!
-
- Before we review, please ensure:
- - ✅ Code follows project conventions
- - ✅ All tests pass
- - ✅ Documentation is updated
- - ✅ Commits follow [Conventional Commits](https://www.conventionalcommits.org/)
-
- See the full [Contributing Guide](https://github.com/esengine/esengine/blob/master/CONTRIBUTING.md).
diff --git a/packages/tools/cli/package.json b/packages/tools/cli/package.json
new file mode 100644
index 00000000..710748e8
--- /dev/null
+++ b/packages/tools/cli/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@esengine/cli",
+ "version": "1.0.0",
+ "description": "CLI tool for adding ESEngine ECS framework to existing projects",
+ "type": "module",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "bin": {
+ "esengine": "./dist/cli.js"
+ },
+ "exports": {
+ ".": {
+ "import": "./dist/index.js",
+ "types": "./dist/index.d.ts"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "build:watch": "tsc --watch",
+ "dev": "ts-node src/cli.ts",
+ "clean": "rimraf dist",
+ "prepublishOnly": "pnpm run build"
+ },
+ "keywords": [
+ "esengine",
+ "ecs",
+ "cli",
+ "cocos",
+ "laya",
+ "game-framework"
+ ],
+ "author": "yhh",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "commander": "^12.1.0",
+ "prompts": "^2.4.2"
+ },
+ "devDependencies": {
+ "@types/node": "^20.19.0",
+ "@types/prompts": "^2.4.9",
+ "rimraf": "^5.0.0",
+ "typescript": "^5.8.3"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/tools/cli/src/adapters/cocos.ts b/packages/tools/cli/src/adapters/cocos.ts
new file mode 100644
index 00000000..cfbe9369
--- /dev/null
+++ b/packages/tools/cli/src/adapters/cocos.ts
@@ -0,0 +1,254 @@
+import type { FileEntry, PlatformAdapter, ProjectConfig } from './types.js';
+
+/**
+ * @zh Cocos Creator 3.x 平台适配器
+ * @en Cocos Creator 3.x platform adapter
+ */
+export const cocosAdapter: PlatformAdapter = {
+ id: 'cocos',
+ name: 'Cocos Creator 3.x',
+ description: 'Generate ECS integration for Cocos Creator 3.x projects',
+
+ getDependencies() {
+ return {
+ '@esengine/ecs-framework': 'latest'
+ };
+ },
+
+ getDevDependencies() {
+ return {};
+ },
+
+ getScripts() {
+ return {};
+ },
+
+ generateFiles(config: ProjectConfig): FileEntry[] {
+ const files: FileEntry[] = [];
+
+ files.push({
+ path: 'assets/scripts/ecs/ECSManager.ts',
+ content: generateECSManager(config)
+ });
+
+ files.push({
+ path: 'assets/scripts/ecs/components/PositionComponent.ts',
+ content: generatePositionComponent()
+ });
+
+ files.push({
+ path: 'assets/scripts/ecs/systems/MovementSystem.ts',
+ content: generateMovementSystem()
+ });
+
+ files.push({
+ path: 'assets/scripts/ecs/README.md',
+ content: generateReadme(config)
+ });
+
+ return files;
+ }
+};
+
+function generateECSManager(config: ProjectConfig): string {
+ return `import { _decorator, Component, director } from 'cc';
+import { Core, Scene, type ICoreConfig } from '@esengine/ecs-framework';
+import { MovementSystem } from './systems/MovementSystem';
+
+const { ccclass, property } = _decorator;
+
+/**
+ * Game Scene - Define your game systems here
+ */
+class GameScene extends Scene {
+ initialize(): void {
+ this.name = '${config.name}';
+ this.addSystem(new MovementSystem());
+ // Add more systems here...
+ }
+
+ onStart(): void {
+ // Create your initial entities here
+ }
+}
+
+/**
+ * ECS Manager - Bridge between Cocos Creator and ESEngine ECS
+ *
+ * Attach this component to a node in your scene.
+ * All game logic should be implemented in ECS Systems.
+ */
+@ccclass('ECSManager')
+export class ECSManager extends Component {
+ /** @zh 调试模式 @en Debug mode */
+ @property({ tooltip: 'Enable debug mode for ECS framework' })
+ debug = false;
+
+ /** @zh 跨场景保持 @en Keep across scenes */
+ @property({ tooltip: 'Keep this node alive across scenes' })
+ persistent = true;
+
+ /** @zh 启用远程调试 @en Enable remote debugging */
+ @property({ tooltip: 'Connect to ECS debugger via WebSocket' })
+ remoteDebug = false;
+
+ /** @zh WebSocket调试地址 @en WebSocket debug URL */
+ @property({ tooltip: 'WebSocket URL for remote debugging' })
+ debugUrl = 'ws://localhost:9229';
+
+ /** @zh 自动重连 @en Auto reconnect */
+ @property({ tooltip: 'Auto reconnect when connection lost' })
+ autoReconnect = true;
+
+ private static _instance: ECSManager | null = null;
+ private _scene!: GameScene;
+
+ static get instance() { return ECSManager._instance; }
+ get scene() { return this._scene; }
+
+ onLoad() {
+ if (ECSManager._instance) {
+ this.destroy();
+ return;
+ }
+ ECSManager._instance = this;
+
+ if (this.persistent) {
+ director.addPersistRootNode(this.node);
+ }
+
+ const config: ICoreConfig = {
+ debug: this.debug
+ };
+
+ // 配置远程调试
+ if (this.remoteDebug && this.debugUrl) {
+ config.debugConfig = {
+ enabled: true,
+ websocketUrl: this.debugUrl,
+ autoReconnect: this.autoReconnect,
+ channels: {
+ entities: true,
+ systems: true,
+ performance: true,
+ components: true,
+ scenes: true
+ }
+ };
+ }
+
+ Core.create(config);
+ this._scene = new GameScene();
+ Core.setScene(this._scene);
+ }
+
+ update(dt: number) {
+ Core.update(dt);
+ }
+
+ onDestroy() {
+ if (ECSManager._instance === this) {
+ ECSManager._instance = null;
+ Core.destroy();
+ }
+ }
+}
+`;
+}
+
+function generatePositionComponent(): string {
+ return `import { Component, ECSComponent } from '@esengine/ecs-framework';
+
+/**
+ * Position component - stores entity position
+ */
+@ECSComponent('Position')
+export class PositionComponent extends Component {
+ x = 0;
+ y = 0;
+
+ constructor(x = 0, y = 0) {
+ super();
+ this.x = x;
+ this.y = y;
+ }
+}
+`;
+}
+
+function generateMovementSystem(): string {
+ return `import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework';
+import { PositionComponent } from '../components/PositionComponent';
+
+/**
+ * Movement system - processes entities with PositionComponent
+ *
+ * Customize this system for your game logic.
+ */
+@ECSSystem('MovementSystem')
+export class MovementSystem extends EntitySystem {
+ constructor() {
+ super(Matcher.empty().all(PositionComponent));
+ }
+
+ protected process(entities: readonly Entity[]): void {
+ for (const entity of entities) {
+ const position = entity.getComponent(PositionComponent)!;
+ // Update position using Time.deltaTime
+ // position.x += velocity.dx * Time.deltaTime;
+ }
+ }
+}
+`;
+}
+
+function generateReadme(config: ProjectConfig): string {
+ return `# ${config.name} - ECS Module
+
+This module integrates ESEngine ECS framework with Cocos Creator.
+
+## Quick Start
+
+1. Attach \`ECSManager\` component to a node in your scene
+2. Create your own components in \`components/\` folder
+3. Create your systems in \`systems/\` folder
+4. Register systems in \`ECSManager.start()\`
+
+## Creating Components
+
+\`\`\`typescript
+import { Component } from '@esengine/ecs-framework';
+
+export class MyComponent extends Component {
+ // Your data here
+ health: number = 100;
+
+ reset() {
+ this.health = 100;
+ }
+}
+\`\`\`
+
+## Creating Systems
+
+\`\`\`typescript
+import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
+import { MyComponent } from '../components/MyComponent';
+
+export class MySystem extends EntitySystem {
+ constructor() {
+ super(Matcher.all(MyComponent));
+ }
+
+ protected processEntity(entity: Entity, dt: number): void {
+ const comp = entity.getComponent(MyComponent)!;
+ // Process entity
+ }
+}
+\`\`\`
+
+## Documentation
+
+- [ESEngine ECS Framework](https://github.com/esengine/esengine)
+`;
+}
diff --git a/packages/tools/cli/src/adapters/cocos2.ts b/packages/tools/cli/src/adapters/cocos2.ts
new file mode 100644
index 00000000..842ea24d
--- /dev/null
+++ b/packages/tools/cli/src/adapters/cocos2.ts
@@ -0,0 +1,259 @@
+import type { FileEntry, PlatformAdapter, ProjectConfig } from './types.js';
+
+/**
+ * @zh Cocos Creator 2.x 平台适配器
+ * @en Cocos Creator 2.x platform adapter
+ */
+export const cocos2Adapter: PlatformAdapter = {
+ id: 'cocos2',
+ name: 'Cocos Creator 2.x',
+ description: 'Generate ECS integration for Cocos Creator 2.x projects',
+
+ getDependencies() {
+ return {
+ '@esengine/ecs-framework': 'latest'
+ };
+ },
+
+ getDevDependencies() {
+ return {};
+ },
+
+ getScripts() {
+ return {};
+ },
+
+ generateFiles(config: ProjectConfig): FileEntry[] {
+ const files: FileEntry[] = [];
+
+ files.push({
+ path: 'assets/scripts/ecs/ECSManager.ts',
+ content: generateECSManager(config)
+ });
+
+ files.push({
+ path: 'assets/scripts/ecs/components/PositionComponent.ts',
+ content: generatePositionComponent()
+ });
+
+ files.push({
+ path: 'assets/scripts/ecs/systems/MovementSystem.ts',
+ content: generateMovementSystem()
+ });
+
+ files.push({
+ path: 'assets/scripts/ecs/README.md',
+ content: generateReadme(config)
+ });
+
+ return files;
+ }
+};
+
+function generateECSManager(config: ProjectConfig): string {
+ return `import { Core, Scene, ICoreConfig } from '@esengine/ecs-framework';
+import { MovementSystem } from './systems/MovementSystem';
+
+const { ccclass, property } = cc._decorator;
+
+/**
+ * Game Scene - Define your game systems here
+ */
+class GameScene extends Scene {
+ initialize(): void {
+ this.name = '${config.name}';
+ this.addSystem(new MovementSystem());
+ // Add more systems here...
+ }
+
+ onStart(): void {
+ // Create your initial entities here
+ }
+}
+
+/**
+ * ECS Manager - Bridge between Cocos Creator 2.x and ESEngine ECS
+ *
+ * Attach this component to a node in your scene.
+ * All game logic should be implemented in ECS Systems.
+ */
+@ccclass
+export default class ECSManager extends cc.Component {
+ /** @zh 调试模式 @en Debug mode */
+ @property({ tooltip: 'Enable debug mode for ECS framework' })
+ debug: boolean = false;
+
+ /** @zh 跨场景保持 @en Keep across scenes */
+ @property({ tooltip: 'Keep this node alive across scenes' })
+ persistent: boolean = true;
+
+ /** @zh 启用远程调试 @en Enable remote debugging */
+ @property({ tooltip: 'Connect to ECS debugger via WebSocket' })
+ remoteDebug: boolean = false;
+
+ /** @zh WebSocket调试地址 @en WebSocket debug URL */
+ @property({ tooltip: 'WebSocket URL for remote debugging' })
+ debugUrl: string = 'ws://localhost:9229';
+
+ /** @zh 自动重连 @en Auto reconnect */
+ @property({ tooltip: 'Auto reconnect when connection lost' })
+ autoReconnect: boolean = true;
+
+ private static _instance: ECSManager | null = null;
+ private _scene!: GameScene;
+
+ static get instance() { return ECSManager._instance; }
+ get scene() { return this._scene; }
+
+ onLoad() {
+ if (ECSManager._instance) {
+ this.node.destroy();
+ return;
+ }
+ ECSManager._instance = this;
+
+ if (this.persistent) {
+ cc.game.addPersistRootNode(this.node);
+ }
+
+ const config: ICoreConfig = {
+ debug: this.debug
+ };
+
+ // 配置远程调试
+ if (this.remoteDebug && this.debugUrl) {
+ config.debugConfig = {
+ enabled: true,
+ websocketUrl: this.debugUrl,
+ autoReconnect: this.autoReconnect,
+ channels: {
+ entities: true,
+ systems: true,
+ performance: true,
+ components: true,
+ scenes: true
+ }
+ };
+ }
+
+ Core.create(config);
+ this._scene = new GameScene();
+ Core.setScene(this._scene);
+ }
+
+ update(dt: number) {
+ Core.update(dt);
+ }
+
+ onDestroy() {
+ if (ECSManager._instance === this) {
+ ECSManager._instance = null;
+ Core.destroy();
+ }
+ }
+}
+`;
+}
+
+function generatePositionComponent(): string {
+ return `import { Component, ECSComponent } from '@esengine/ecs-framework';
+
+/**
+ * Position component - stores entity position
+ */
+@ECSComponent('Position')
+export class PositionComponent extends Component {
+ x = 0;
+ y = 0;
+
+ constructor(x = 0, y = 0) {
+ super();
+ this.x = x;
+ this.y = y;
+ }
+}
+`;
+}
+
+function generateMovementSystem(): string {
+ return `import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework';
+import { PositionComponent } from '../components/PositionComponent';
+
+/**
+ * Movement system - processes entities with PositionComponent
+ *
+ * Customize this system for your game logic.
+ */
+@ECSSystem('MovementSystem')
+export class MovementSystem extends EntitySystem {
+ constructor() {
+ super(Matcher.empty().all(PositionComponent));
+ }
+
+ protected process(entities: readonly Entity[]): void {
+ for (const entity of entities) {
+ const position = entity.getComponent(PositionComponent)!;
+ // Update position using Time.deltaTime
+ // position.x += velocity.dx * Time.deltaTime;
+ }
+ }
+}
+`;
+}
+
+function generateReadme(config: ProjectConfig): string {
+ return `# ${config.name} - ECS Module
+
+This module integrates ESEngine ECS framework with Cocos Creator 2.x.
+
+## Quick Start
+
+1. Attach \`ECSManager\` component to a node in your scene
+2. Create your own components in \`components/\` folder
+3. Create your systems in \`systems/\` folder
+4. Register systems in \`ECSManager.onLoad()\`
+
+## Cocos Creator 2.x Notes
+
+- Use \`cc._decorator\` for decorators
+- Use \`cc.Component\` as base class
+- Use \`cc.game.addPersistRootNode()\` for persistent nodes
+
+## Creating Components
+
+\`\`\`typescript
+import { Component } from '@esengine/ecs-framework';
+
+export class MyComponent extends Component {
+ health: number = 100;
+
+ reset() {
+ this.health = 100;
+ }
+}
+\`\`\`
+
+## Creating Systems
+
+\`\`\`typescript
+import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
+import { MyComponent } from '../components/MyComponent';
+
+export class MySystem extends EntitySystem {
+ constructor() {
+ super(Matcher.all(MyComponent));
+ }
+
+ protected processEntity(entity: Entity, dt: number): void {
+ const comp = entity.getComponent(MyComponent)!;
+ // Process entity
+ }
+}
+\`\`\`
+
+## Documentation
+
+- [ESEngine ECS Framework](https://github.com/esengine/esengine)
+- [Cocos Creator 2.x Docs](https://docs.cocos.com/creator/2.4/manual/)
+`;
+}
diff --git a/packages/tools/cli/src/adapters/index.ts b/packages/tools/cli/src/adapters/index.ts
new file mode 100644
index 00000000..4833376a
--- /dev/null
+++ b/packages/tools/cli/src/adapters/index.ts
@@ -0,0 +1,54 @@
+import { cocosAdapter } from './cocos.js';
+import { cocos2Adapter } from './cocos2.js';
+import { layaAdapter } from './laya.js';
+import { nodejsAdapter } from './nodejs.js';
+import type { AdapterRegistry, PlatformAdapter, PlatformType } from './types.js';
+
+export * from './types.js';
+export { cocosAdapter } from './cocos.js';
+export { cocos2Adapter } from './cocos2.js';
+export { layaAdapter } from './laya.js';
+export { nodejsAdapter } from './nodejs.js';
+
+/**
+ * @zh 平台适配器注册表
+ * @en Platform adapter registry
+ */
+export const adapters: AdapterRegistry = {
+ cocos: cocosAdapter,
+ cocos2: cocos2Adapter,
+ laya: layaAdapter,
+ nodejs: nodejsAdapter
+};
+
+/**
+ * @zh 获取平台适配器
+ * @en Get platform adapter
+ */
+export function getAdapter(platform: PlatformType): PlatformAdapter {
+ const adapter = adapters[platform];
+ if (!adapter) {
+ throw new Error(`Unknown platform: ${platform}`);
+ }
+ return adapter;
+}
+
+/**
+ * @zh 获取所有可用平台
+ * @en Get all available platforms
+ */
+export function getPlatforms(): PlatformType[] {
+ return Object.keys(adapters) as PlatformType[];
+}
+
+/**
+ * @zh 获取平台选项(用于交互式提示)
+ * @en Get platform choices (for interactive prompts)
+ */
+export function getPlatformChoices(): Array<{ title: string; value: PlatformType; description: string }> {
+ return Object.values(adapters).map((adapter) => ({
+ title: adapter.name,
+ value: adapter.id,
+ description: adapter.description
+ }));
+}
diff --git a/packages/tools/cli/src/adapters/laya.ts b/packages/tools/cli/src/adapters/laya.ts
new file mode 100644
index 00000000..b58d0ca4
--- /dev/null
+++ b/packages/tools/cli/src/adapters/laya.ts
@@ -0,0 +1,245 @@
+import type { FileEntry, PlatformAdapter, ProjectConfig } from './types.js';
+
+/**
+ * @zh Laya 3.x 平台适配器
+ * @en Laya 3.x platform adapter
+ */
+export const layaAdapter: PlatformAdapter = {
+ id: 'laya',
+ name: 'Laya 3.x',
+ description: 'Generate ECS integration for LayaAir 3.x projects',
+
+ getDependencies() {
+ return {
+ '@esengine/ecs-framework': 'latest'
+ };
+ },
+
+ getDevDependencies() {
+ return {};
+ },
+
+ getScripts() {
+ return {};
+ },
+
+ generateFiles(config: ProjectConfig): FileEntry[] {
+ const files: FileEntry[] = [];
+
+ files.push({
+ path: 'src/ecs/ECSManager.ts',
+ content: generateECSManager(config)
+ });
+
+ files.push({
+ path: 'src/ecs/components/PositionComponent.ts',
+ content: generatePositionComponent()
+ });
+
+ files.push({
+ path: 'src/ecs/systems/MovementSystem.ts',
+ content: generateMovementSystem()
+ });
+
+ files.push({
+ path: 'src/ecs/README.md',
+ content: generateReadme(config)
+ });
+
+ return files;
+ }
+};
+
+function generateECSManager(config: ProjectConfig): string {
+ return `import { Core, Scene, type ICoreConfig } from '@esengine/ecs-framework';
+import { MovementSystem } from './systems/MovementSystem';
+
+const { regClass, property } = Laya;
+
+/**
+ * Game Scene - Define your game systems here
+ */
+class GameScene extends Scene {
+ initialize(): void {
+ this.name = '${config.name}';
+ this.addSystem(new MovementSystem());
+ // Add more systems here...
+ }
+
+ onStart(): void {
+ // Create your initial entities here
+ }
+}
+
+/**
+ * ECS Manager - Bridge between LayaAir and ESEngine ECS
+ *
+ * Attach this script to a node in your scene via Laya IDE.
+ * All game logic should be implemented in ECS Systems.
+ */
+@regClass()
+export class ECSManager extends Laya.Script {
+ /** @zh 调试模式 @en Debug mode */
+ @property({ type: Boolean, caption: 'Debug', tips: 'Enable debug mode for ECS framework' })
+ debug = false;
+
+ /** @zh 启用远程调试 @en Enable remote debugging */
+ @property({ type: Boolean, caption: 'Remote Debug', tips: 'Connect to ECS debugger via WebSocket' })
+ remoteDebug = false;
+
+ /** @zh WebSocket调试地址 @en WebSocket debug URL */
+ @property({ type: String, caption: 'Debug URL', tips: 'WebSocket URL for remote debugging (e.g., ws://localhost:9229)' })
+ debugUrl = 'ws://localhost:9229';
+
+ /** @zh 自动重连 @en Auto reconnect */
+ @property({ type: Boolean, caption: 'Auto Reconnect', tips: 'Auto reconnect when connection lost' })
+ autoReconnect = true;
+
+ private static _instance: ECSManager | null = null;
+ private _scene!: GameScene;
+
+ static get instance() { return ECSManager._instance; }
+ get scene() { return this._scene; }
+
+ onAwake(): void {
+ if (ECSManager._instance) {
+ this.destroy();
+ return;
+ }
+ ECSManager._instance = this;
+
+ const config: ICoreConfig = {
+ debug: this.debug
+ };
+
+ // 配置远程调试
+ if (this.remoteDebug && this.debugUrl) {
+ config.debugConfig = {
+ enabled: true,
+ websocketUrl: this.debugUrl,
+ autoReconnect: this.autoReconnect,
+ channels: {
+ entities: true,
+ systems: true,
+ performance: true,
+ components: true,
+ scenes: true
+ }
+ };
+ }
+
+ Core.create(config);
+ this._scene = new GameScene();
+ Core.setScene(this._scene);
+ }
+
+ onUpdate(): void {
+ Core.update(Laya.timer.delta / 1000);
+ }
+
+ onDestroy(): void {
+ if (ECSManager._instance === this) {
+ ECSManager._instance = null;
+ Core.destroy();
+ }
+ }
+}
+`;
+}
+
+function generatePositionComponent(): string {
+ return `import { Component, ECSComponent } from '@esengine/ecs-framework';
+
+/**
+ * Position component - stores entity position
+ */
+@ECSComponent('Position')
+export class PositionComponent extends Component {
+ x = 0;
+ y = 0;
+
+ constructor(x = 0, y = 0) {
+ super();
+ this.x = x;
+ this.y = y;
+ }
+}
+`;
+}
+
+function generateMovementSystem(): string {
+ return `import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework';
+import { PositionComponent } from '../components/PositionComponent';
+
+/**
+ * Movement system - processes entities with PositionComponent
+ *
+ * Customize this system for your game logic.
+ */
+@ECSSystem('MovementSystem')
+export class MovementSystem extends EntitySystem {
+ constructor() {
+ super(Matcher.empty().all(PositionComponent));
+ }
+
+ protected process(entities: readonly Entity[]): void {
+ for (const entity of entities) {
+ const position = entity.getComponent(PositionComponent)!;
+ // Update position using Time.deltaTime
+ // position.x += velocity.dx * Time.deltaTime;
+ }
+ }
+}
+`;
+}
+
+function generateReadme(config: ProjectConfig): string {
+ return `# ${config.name} - ECS Module
+
+This module integrates ESEngine ECS framework with LayaAir 3.x.
+
+## Quick Start
+
+1. In Laya IDE, attach \`ECSManager\` script to a node in your scene
+2. Create your own components in \`components/\` folder
+3. Create your systems in \`systems/\` folder
+4. Register systems in \`ECSManager.onAwake()\`
+
+## Creating Components
+
+\`\`\`typescript
+import { Component } from '@esengine/ecs-framework';
+
+export class MyComponent extends Component {
+ health: number = 100;
+
+ reset() {
+ this.health = 100;
+ }
+}
+\`\`\`
+
+## Creating Systems
+
+\`\`\`typescript
+import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
+import { MyComponent } from '../components/MyComponent';
+
+export class MySystem extends EntitySystem {
+ constructor() {
+ super(Matcher.all(MyComponent));
+ }
+
+ protected processEntity(entity: Entity, dt: number): void {
+ const comp = entity.getComponent(MyComponent)!;
+ // Process entity
+ }
+}
+\`\`\`
+
+## Documentation
+
+- [ESEngine ECS Framework](https://github.com/esengine/esengine)
+- [LayaAir Documentation](https://layaair.com/)
+`;
+}
diff --git a/packages/tools/cli/src/adapters/nodejs.ts b/packages/tools/cli/src/adapters/nodejs.ts
new file mode 100644
index 00000000..8537e491
--- /dev/null
+++ b/packages/tools/cli/src/adapters/nodejs.ts
@@ -0,0 +1,348 @@
+import type { FileEntry, PlatformAdapter, ProjectConfig } from './types.js';
+
+/**
+ * @zh Node.js 平台适配器
+ * @en Node.js platform adapter
+ */
+export const nodejsAdapter: PlatformAdapter = {
+ id: 'nodejs',
+ name: 'Node.js',
+ description: 'Generate standalone Node.js project with ECS (for servers, CLI tools, simulations)',
+
+ getDependencies() {
+ return {
+ '@esengine/ecs-framework': 'latest'
+ };
+ },
+
+ getDevDependencies() {
+ return {
+ '@types/node': '^20.0.0',
+ 'tsx': '^4.0.0',
+ 'typescript': '^5.0.0'
+ };
+ },
+
+ getScripts() {
+ return {
+ 'dev': 'tsx watch src/index.ts',
+ 'start': 'tsx src/index.ts',
+ 'build': 'tsc',
+ 'build:start': 'tsc && node dist/index.js'
+ };
+ },
+
+ generateFiles(config: ProjectConfig): FileEntry[] {
+ const files: FileEntry[] = [];
+
+ files.push({
+ path: 'src/index.ts',
+ content: generateIndex(config)
+ });
+
+ files.push({
+ path: 'src/Game.ts',
+ content: generateGame(config)
+ });
+
+ files.push({
+ path: 'src/components/PositionComponent.ts',
+ content: generatePositionComponent()
+ });
+
+ files.push({
+ path: 'src/systems/MovementSystem.ts',
+ content: generateMovementSystem()
+ });
+
+ files.push({
+ path: 'tsconfig.json',
+ content: generateTsConfig()
+ });
+
+ files.push({
+ path: 'README.md',
+ content: generateReadme(config)
+ });
+
+ return files;
+ }
+};
+
+function generateIndex(config: ProjectConfig): string {
+ return `import { Game } from './Game.js';
+
+const game = new Game();
+
+// Handle graceful shutdown
+process.on('SIGINT', () => {
+ console.log('\\nShutting down...');
+ game.stop();
+ process.exit(0);
+});
+
+process.on('SIGTERM', () => {
+ game.stop();
+ process.exit(0);
+});
+
+// Start the game
+game.start();
+
+console.log('[${config.name}] Game started. Press Ctrl+C to stop.');
+`;
+}
+
+function generateGame(config: ProjectConfig): string {
+ return `import { Core, Scene, type ICoreConfig } from '@esengine/ecs-framework';
+import { MovementSystem } from './systems/MovementSystem.js';
+
+/**
+ * Game configuration options
+ */
+export interface GameOptions {
+ /** @zh 调试模式 @en Debug mode */
+ debug?: boolean;
+ /** @zh 目标帧率 @en Target FPS */
+ targetFPS?: number;
+ /** @zh 远程调试配置 @en Remote debug configuration */
+ remoteDebug?: {
+ /** @zh 启用远程调试 @en Enable remote debugging */
+ enabled: boolean;
+ /** @zh WebSocket地址 @en WebSocket URL */
+ url: string;
+ /** @zh 自动重连 @en Auto reconnect */
+ autoReconnect?: boolean;
+ };
+}
+
+/**
+ * Game Scene - Define your game systems here
+ */
+class GameScene extends Scene {
+ initialize(): void {
+ this.name = '${config.name}';
+ this.addSystem(new MovementSystem());
+ // Add more systems here...
+ }
+
+ onStart(): void {
+ // Create your initial entities here
+ }
+}
+
+/**
+ * Main game class with ECS game loop
+ *
+ * Features:
+ * - Configurable debug mode and FPS
+ * - Remote debugging via WebSocket
+ * - Fixed timestep game loop
+ * - Graceful start/stop
+ */
+export class Game {
+ private readonly _scene: GameScene;
+ private readonly _targetFPS: number;
+ private _running = false;
+ private _tickInterval: ReturnType | null = null;
+ private _lastTime = 0;
+
+ get scene() { return this._scene; }
+ get running() { return this._running; }
+
+ constructor(options: GameOptions = {}) {
+ const { debug = false, targetFPS = 60, remoteDebug } = options;
+ this._targetFPS = targetFPS;
+
+ const config: ICoreConfig = { debug };
+
+ // 配置远程调试
+ if (remoteDebug?.enabled && remoteDebug.url) {
+ config.debugConfig = {
+ enabled: true,
+ websocketUrl: remoteDebug.url,
+ autoReconnect: remoteDebug.autoReconnect ?? true,
+ channels: {
+ entities: true,
+ systems: true,
+ performance: true,
+ components: true,
+ scenes: true
+ }
+ };
+ }
+
+ Core.create(config);
+ this._scene = new GameScene();
+ Core.setScene(this._scene);
+ }
+
+ start(): void {
+ if (this._running) return;
+ this._running = true;
+ this._lastTime = performance.now();
+
+ this._tickInterval = setInterval(() => {
+ const now = performance.now();
+ Core.update((now - this._lastTime) / 1000);
+ this._lastTime = now;
+ }, 1000 / this._targetFPS);
+ }
+
+ stop(): void {
+ if (!this._running) return;
+ this._running = false;
+
+ if (this._tickInterval) {
+ clearInterval(this._tickInterval);
+ this._tickInterval = null;
+ }
+ Core.destroy();
+ }
+}
+`;
+}
+
+function generatePositionComponent(): string {
+ return `import { Component, ECSComponent } from '@esengine/ecs-framework';
+
+/**
+ * Position component - stores entity position
+ */
+@ECSComponent('Position')
+export class PositionComponent extends Component {
+ x = 0;
+ y = 0;
+
+ constructor(x = 0, y = 0) {
+ super();
+ this.x = x;
+ this.y = y;
+ }
+}
+`;
+}
+
+function generateMovementSystem(): string {
+ return `import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework';
+import { PositionComponent } from '../components/PositionComponent.js';
+
+/**
+ * Movement system - processes entities with PositionComponent
+ *
+ * Customize this system for your game logic.
+ */
+@ECSSystem('MovementSystem')
+export class MovementSystem extends EntitySystem {
+ constructor() {
+ super(Matcher.empty().all(PositionComponent));
+ }
+
+ protected process(entities: readonly Entity[]): void {
+ for (const entity of entities) {
+ const position = entity.getComponent(PositionComponent)!;
+ // Update position using Time.deltaTime
+ // position.x += velocity.dx * Time.deltaTime;
+ }
+ }
+}
+`;
+}
+
+function generateTsConfig(): string {
+ return `{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ES2022"],
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "declaration": true,
+ "sourceMap": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
+`;
+}
+
+function generateReadme(config: ProjectConfig): string {
+ return `# ${config.name}
+
+A Node.js project using ESEngine ECS framework.
+
+## Quick Start
+
+\`\`\`bash
+# Install dependencies
+npm install
+
+# Run in development mode (with hot reload)
+npm run dev
+
+# Build and run
+npm run build:start
+\`\`\`
+
+## Project Structure
+
+\`\`\`
+src/
+├── index.ts # Entry point
+├── Game.ts # Game loop and ECS setup
+├── components/ # ECS components (data)
+│ └── PositionComponent.ts
+└── systems/ # ECS systems (logic)
+ └── MovementSystem.ts
+\`\`\`
+
+## Creating Components
+
+\`\`\`typescript
+import { Component } from '@esengine/ecs-framework';
+
+export class HealthComponent extends Component {
+ current = 100;
+ max = 100;
+
+ reset(): void {
+ this.current = this.max;
+ }
+}
+\`\`\`
+
+## Creating Systems
+
+\`\`\`typescript
+import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
+import { HealthComponent } from '../components/HealthComponent.js';
+
+export class HealthSystem extends EntitySystem {
+ constructor() {
+ super(Matcher.all(HealthComponent));
+ }
+
+ protected processEntity(entity: Entity, dt: number): void {
+ const health = entity.getComponent(HealthComponent)!;
+ // Your logic here
+ }
+}
+\`\`\`
+
+## Use Cases
+
+- Game servers
+- CLI tools with complex logic
+- Simulations
+- Automated testing
+
+## Documentation
+
+- [ESEngine ECS Framework](https://github.com/esengine/esengine)
+`;
+}
diff --git a/packages/tools/cli/src/adapters/types.ts b/packages/tools/cli/src/adapters/types.ts
new file mode 100644
index 00000000..6dea7ac1
--- /dev/null
+++ b/packages/tools/cli/src/adapters/types.ts
@@ -0,0 +1,77 @@
+/**
+ * @zh 项目配置
+ * @en Project configuration
+ */
+export interface ProjectConfig {
+ /** @zh 项目名称 @en Project name */
+ name: string;
+ /** @zh 目标平台 @en Target platform */
+ platform: PlatformType;
+ /** @zh 项目路径 @en Project path */
+ path: string;
+}
+
+/**
+ * @zh 支持的平台类型
+ * @en Supported platform types
+ */
+export type PlatformType = 'cocos' | 'cocos2' | 'laya' | 'nodejs';
+
+/**
+ * @zh 文件入口
+ * @en File entry
+ */
+export interface FileEntry {
+ /** @zh 相对路径 @en Relative path */
+ path: string;
+ /** @zh 文件内容 @en File content */
+ content: string;
+}
+
+/**
+ * @zh 平台适配器接口
+ * @en Platform adapter interface
+ *
+ * @zh 每个平台只需实现这个接口,即可支持项目生成
+ * @en Each platform only needs to implement this interface to support project generation
+ */
+export interface PlatformAdapter {
+ /** @zh 平台标识 @en Platform identifier */
+ readonly id: PlatformType;
+
+ /** @zh 平台显示名称 @en Platform display name */
+ readonly name: string;
+
+ /** @zh 平台描述 @en Platform description */
+ readonly description: string;
+
+ /**
+ * @zh 获取平台特定的依赖
+ * @en Get platform-specific dependencies
+ */
+ getDependencies(): Record;
+
+ /**
+ * @zh 获取平台特定的开发依赖
+ * @en Get platform-specific dev dependencies
+ */
+ getDevDependencies(): Record;
+
+ /**
+ * @zh 生成平台特定的文件
+ * @en Generate platform-specific files
+ */
+ generateFiles(config: ProjectConfig): FileEntry[];
+
+ /**
+ * @zh 获取 package.json 的 scripts
+ * @en Get package.json scripts
+ */
+ getScripts(): Record;
+}
+
+/**
+ * @zh 平台适配器注册表类型
+ * @en Platform adapter registry type
+ */
+export type AdapterRegistry = Record;
diff --git a/packages/tools/cli/src/cli.ts b/packages/tools/cli/src/cli.ts
new file mode 100644
index 00000000..10ef5ac7
--- /dev/null
+++ b/packages/tools/cli/src/cli.ts
@@ -0,0 +1,320 @@
+#!/usr/bin/env node
+
+import { Command } from 'commander';
+import prompts from 'prompts';
+import chalk from 'chalk';
+import * as fs from 'node:fs';
+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';
+
+const VERSION = '1.0.0';
+
+/**
+ * @zh 打印 Logo
+ * @en Print logo
+ */
+function printLogo(): void {
+ console.log();
+ console.log(chalk.cyan(' ╭──────────────────────────────────────╮'));
+ console.log(chalk.cyan(' │ │'));
+ console.log(chalk.cyan(' │ ') + chalk.bold.white('ESEngine CLI') + chalk.gray(` v${VERSION}`) + chalk.cyan(' │'));
+ console.log(chalk.cyan(' │ │'));
+ console.log(chalk.cyan(' ╰──────────────────────────────────────╯'));
+ console.log();
+}
+
+/**
+ * @zh 检测是否存在 *.laya 文件
+ * @en Check if *.laya file exists
+ */
+function hasLayaProjectFile(cwd: string): boolean {
+ try {
+ const files = fs.readdirSync(cwd);
+ return files.some(f => f.endsWith('.laya'));
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * @zh 检测 Cocos Creator 版本
+ * @en Detect Cocos Creator version
+ */
+function detectCocosVersion(cwd: string): 'cocos' | 'cocos2' | null {
+ // Cocos 3.x: 检查 cc.config.json 或 extensions 目录
+ if (fs.existsSync(path.join(cwd, 'cc.config.json')) ||
+ fs.existsSync(path.join(cwd, 'extensions'))) {
+ return 'cocos';
+ }
+
+ // 检查 project.json 中的版本号
+ const projectJsonPath = path.join(cwd, 'project.json');
+ if (fs.existsSync(projectJsonPath)) {
+ try {
+ const project = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
+ // Cocos 2.x project.json 有 engine-version 字段
+ if (project['engine-version'] || project.engine) {
+ const version = project['engine-version'] || project.engine || '';
+ // 2.x 版本格式: "cocos-creator-js-2.4.x" 或 "2.4.x"
+ if (version.includes('2.') || version.startsWith('2')) {
+ return 'cocos2';
+ }
+ }
+ // 有 project.json 但没有版本信息,假设是 3.x
+ return 'cocos';
+ } catch {
+ // 解析失败,假设是 3.x
+ return 'cocos';
+ }
+ }
+
+ return null;
+}
+
+/**
+ * @zh 检测项目类型
+ * @en Detect project type
+ */
+function detectProjectType(cwd: string): PlatformType | null {
+ // Laya: 检查 *.laya 文件 或 .laya 目录 或 laya.json
+ if (hasLayaProjectFile(cwd) ||
+ fs.existsSync(path.join(cwd, '.laya')) ||
+ fs.existsSync(path.join(cwd, 'laya.json'))) {
+ return 'laya';
+ }
+
+ // Cocos Creator: 检查 assets 目录
+ if (fs.existsSync(path.join(cwd, 'assets'))) {
+ const cocosVersion = detectCocosVersion(cwd);
+ if (cocosVersion) {
+ return cocosVersion;
+ }
+ }
+
+ // Node.js: 检查 package.json
+ if (fs.existsSync(path.join(cwd, 'package.json'))) {
+ return 'nodejs';
+ }
+
+ return null;
+}
+
+/**
+ * @zh 检测包管理器
+ * @en Detect package manager
+ */
+function detectPackageManager(cwd: string): 'pnpm' | 'yarn' | 'npm' {
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
+ if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn';
+ return 'npm';
+}
+
+/**
+ * @zh 读取或创建 package.json
+ * @en Read or create package.json
+ */
+function readOrCreatePackageJson(packageJsonPath: string, projectName: string): Record {
+ try {
+ return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
+ } catch (err) {
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
+ const pkg = {
+ name: projectName,
+ version: '1.0.0',
+ private: true,
+ dependencies: {}
+ };
+ fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), 'utf-8');
+ console.log(chalk.green(' ✓ Created package.json'));
+ return pkg;
+ }
+ throw err;
+ }
+}
+
+/**
+ * @zh 安装依赖
+ * @en Install dependencies
+ */
+function installDependencies(cwd: string, deps: Record): boolean {
+ const pm = detectPackageManager(cwd);
+ const packageJsonPath = path.join(cwd, 'package.json');
+
+ // 读取或创建 package.json(原子操作,避免竞态条件)
+ const pkg = readOrCreatePackageJson(packageJsonPath, path.basename(cwd));
+ const pkgDeps = (pkg.dependencies || {}) as Record;
+
+ let needsInstall = false;
+ for (const [name, version] of Object.entries(deps)) {
+ if (!pkgDeps[name]) {
+ pkgDeps[name] = version;
+ needsInstall = true;
+ }
+ }
+
+ if (!needsInstall) {
+ console.log(chalk.gray(' Dependencies already configured.'));
+ return true;
+ }
+
+ pkg.dependencies = pkgDeps;
+ fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), 'utf-8');
+
+ // 运行安装命令
+ const installCmd = pm === 'pnpm' ? 'pnpm install' : pm === 'yarn' ? 'yarn' : 'npm install';
+ console.log(chalk.gray(` Running ${installCmd}...`));
+
+ try {
+ execSync(installCmd, { cwd, stdio: 'inherit' });
+ return true;
+ } catch {
+ console.log(chalk.yellow(` ⚠ Failed to run ${installCmd}. Please run it manually.`));
+ return false;
+ }
+}
+
+/**
+ * @zh 获取项目名称
+ * @en Get project name
+ */
+function getProjectName(cwd: string): string {
+ const packageJsonPath = path.join(cwd, 'package.json');
+ if (fs.existsSync(packageJsonPath)) {
+ try {
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
+ return pkg.name || path.basename(cwd);
+ } catch {
+ // ignore
+ }
+ }
+ return path.basename(cwd);
+}
+
+/**
+ * @zh 初始化 ECS 到现有项目
+ * @en Initialize ECS into existing project
+ */
+async function initCommand(options: { platform?: string }): Promise {
+ printLogo();
+
+ const cwd = process.cwd();
+ let platform = options.platform as PlatformType | undefined;
+
+ // 尝试自动检测项目类型
+ const detected = detectProjectType(cwd);
+
+ if (!platform) {
+ if (detected) {
+ console.log(chalk.gray(` Detected: ${detected} project`));
+ platform = detected;
+ } else {
+ // 交互式选择
+ const response = await prompts({
+ type: 'select',
+ name: 'platform',
+ message: 'Select platform:',
+ choices: getPlatformChoices(),
+ initial: 0
+ }, {
+ onCancel: () => {
+ console.log(chalk.yellow('\n Cancelled.'));
+ process.exit(0);
+ }
+ });
+ platform = response.platform;
+ }
+ }
+
+ // 验证平台
+ const validPlatforms = getPlatforms();
+ if (!platform || !validPlatforms.includes(platform)) {
+ console.log(chalk.red(`\n ✗ Invalid platform. Choose from: ${validPlatforms.join(', ')}`));
+ process.exit(1);
+ }
+
+ const projectName = getProjectName(cwd);
+ const adapter = getAdapter(platform);
+
+ const config: ProjectConfig = {
+ name: projectName,
+ platform,
+ path: cwd
+ };
+
+ console.log();
+ console.log(chalk.bold('Adding ECS to your project...'));
+
+ // 生成文件
+ const files = adapter.generateFiles(config);
+ const createdFiles: string[] = [];
+
+ for (const file of files) {
+ const filePath = path.join(cwd, file.path);
+ const dir = path.dirname(filePath);
+
+ // 创建目录(recursive: true 不会因目录存在而失败)
+ fs.mkdirSync(dir, { recursive: true });
+
+ // 尝试写入文件(wx 模式:如果文件存在则失败,避免竞态条件)
+ try {
+ fs.writeFileSync(filePath, file.content, { encoding: 'utf-8', flag: 'wx' });
+ createdFiles.push(file.path);
+ console.log(chalk.green(` ✓ Created ${file.path}`));
+ } catch (err) {
+ if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
+ console.log(chalk.yellow(` ⚠ Skipped ${file.path} (already exists)`));
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ if (createdFiles.length === 0) {
+ console.log(chalk.yellow('\n No files created. ECS may already be set up.'));
+ return;
+ }
+
+ // 安装依赖
+ console.log();
+ console.log(chalk.bold('Installing dependencies...'));
+ const deps = adapter.getDependencies();
+ installDependencies(cwd, deps);
+
+ // 打印下一步
+ console.log();
+ console.log(chalk.bold('Done!'));
+ console.log();
+
+ if (platform === 'cocos' || platform === 'cocos2') {
+ console.log(chalk.gray(' Attach ECSManager to a node in your scene to start.'));
+ } else if (platform === 'laya') {
+ console.log(chalk.gray(' Attach ECSManager script to a node in Laya IDE to start.'));
+ } else {
+ console.log(chalk.gray(' Run `npm run dev` to start your game.'));
+ }
+ console.log();
+}
+
+// Setup CLI
+const program = new Command();
+
+program
+ .name('esengine')
+ .description('CLI tool for adding ESEngine ECS to your project')
+ .version(VERSION);
+
+program
+ .command('init')
+ .description('Add ECS framework to your existing project')
+ .option('-p, --platform ', 'Target platform (cocos, cocos2, laya, nodejs)')
+ .action(initCommand);
+
+// Default command: run init
+program
+ .action(() => {
+ initCommand({});
+ });
+
+program.parse();
diff --git a/packages/tools/cli/src/index.ts b/packages/tools/cli/src/index.ts
new file mode 100644
index 00000000..eed42e03
--- /dev/null
+++ b/packages/tools/cli/src/index.ts
@@ -0,0 +1,8 @@
+/**
+ * @zh ESEngine CLI - 为现有项目添加 ECS 框架
+ * @en ESEngine CLI - Add ECS framework to existing projects
+ *
+ * @packageDocumentation
+ */
+
+export * from './adapters/index.js';
diff --git a/packages/tools/cli/tsconfig.json b/packages/tools/cli/tsconfig.json
new file mode 100644
index 00000000..1721d73b
--- /dev/null
+++ b/packages/tools/cli/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ES2020"],
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 350cd40f..f728c061 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2032,6 +2032,31 @@ importers:
specifier: ^4.5.4
version: 4.5.4(@types/node@20.19.27)(rollup@4.54.0)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.27)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
+ packages/tools/cli:
+ dependencies:
+ chalk:
+ specifier: ^4.1.2
+ version: 4.1.2
+ commander:
+ specifier: ^12.1.0
+ version: 12.1.0
+ prompts:
+ specifier: ^2.4.2
+ version: 2.4.2
+ devDependencies:
+ '@types/node':
+ specifier: ^20.19.0
+ version: 20.19.27
+ '@types/prompts':
+ specifier: ^2.4.9
+ version: 2.4.9
+ rimraf:
+ specifier: ^5.0.0
+ version: 5.0.10
+ typescript:
+ specifier: ^5.8.3
+ version: 5.9.3
+
packages/tools/sdk:
dependencies:
'@esengine/asset-system':
@@ -4793,6 +4818,9 @@ packages:
'@types/pako@2.0.4':
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
+ '@types/prompts@2.4.9':
+ resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==}
+
'@types/prop-types@15.7.15':
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
@@ -5557,6 +5585,10 @@ packages:
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
engines: {node: '>=16'}
+ commander@12.1.0:
+ resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
+ engines: {node: '>=18'}
+
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -13067,6 +13099,11 @@ snapshots:
'@types/pako@2.0.4': {}
+ '@types/prompts@2.4.9':
+ dependencies:
+ '@types/node': 20.19.27
+ kleur: 3.0.3
+
'@types/prop-types@15.7.15': {}
'@types/qs@6.14.0': {}
@@ -13936,6 +13973,8 @@ snapshots:
commander@11.1.0: {}
+ commander@12.1.0: {}
+
commander@2.20.3: {}
commander@4.1.1: {}