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
This commit is contained in:
YHH
2025-12-26 16:18:59 +08:00
committed by GitHub
parent 155411e743
commit c4f7a13b74
24 changed files with 1703 additions and 786 deletions

View File

@@ -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
- 针对不同平台生成正确的装饰器和生命周期方法

32
.github/labeler.yml vendored
View File

@@ -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'

View File

@@ -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"

View File

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

View File

@@ -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
# å<>ªåœ¨çœŸå®žç”¨æˆ·æ<C2B7><C3A6>到 @ai-helper 时触å<C2A6>,忽略机器人评è®? 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
});
// é™<C3A9>制长度,é<C592>¿å…<C3A5>è¶…è¿?token é™<C3A9>åˆ
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 }}
请æ<C2B7>œç´¢é¡¹ç®ä»£ç <C3A7>å¹¶æ<C2B6><C3A6>ä¾è§£å†³æ¹æ¡ˆã€? 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 助手ã€? 主è¦<C3A8>代ç <C3A7>åœ?packages/framework/core/srcã€? æ<>œç´¢ç¸å…³ä»£ç <C3A7>å<EFBFBD>Žï¼Œç”¨ä¸­æ‡ç®€æ´<C3A6>åžç­”问题,包å<E280A6>«é—®é¢˜åˆ†æž<C3A6>ã€<C3A3>è§£å†³æ¹æ¡ˆåŒä»£ç <C3A7>引用ã€? 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
});

View File

@@ -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.'
});

View File

@@ -1,160 +0,0 @@
name: Batch Label Issues
on:
workflow_dispatch:
inputs:
mode:
description:  ‡ç­¾æ¨¡å¼<C3A5>'
required: true
type: choice
options:
- 'recent' # 最�20 �issue
- 'open' # 所有打开�issue
- 'unlabeled' # å<>ªå¤„ç<E2809E>†æ²¡æœ‰æ ‡ç­¾çš„ 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 "📊 开姿‰¹é‡<C3A9>打标签..."
echo "模å¼<C3A5>: $MODE"
echo "跳过已标� $SKIP_LABELED"
# 获å<C2B7> issue 列表
if [ "$MODE" = "recent" ]; then
echo "📋 获å<C2B7>最è¿?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 "📋 获å<C2B7>所有打开çš?issue..."
ISSUES=$(gh issue list --state open --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
elif [ "$MODE" = "unlabeled" ]; then
echo "📋 获å<C2B7>没有标签çš?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 "📋 获å<C2B7>所æœ?issue(é™<C3A9>åˆ?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
# 处ç<E2809E>†æ¯<C3A6>个 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 "ðŸ”<C5B8> 处ç<E2809E>† Issue #$ISSUE_NUM: $TITLE"
echo " 现有标签: $EXISTING_LABELS"
# 跳过已有标签�issue
if [ "$SKIP_LABELED" = "true" ] && [ ! -z "$EXISTING_LABELS" ]; then
echo " â<>­ï¸<C3AF> 跳过(已有标签)"
continue
fi
# 分æž<C3A6>内容并打标签
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 " ðŸ<C3B0> 检测到: 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 " ðŸŒ<C5B8> 检测到: network"
fi
# 检�help wanted
if echo "$TITLE $BODY" | grep -iE "(help wanted|需è¦<C3A8>帮助|求助)" > /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 " ℹï¸<C3AF> 未检测到明确类型"
fi
# é<>¿å…<C3A5> API é™<C3A9>åˆ
sleep 1
done
echo ""
echo "âœ?批é‡<C3A9>标签完æˆ<C3A6>ï¼?
echo "查看结果: https://github.com/${{ github.repository }}/issues"

View File

@@ -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`);
}

View File

@@ -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

View File

@@ -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: |
<details>
<summary>🌏 Translation / 翻译</summary>
Bot detected the issue body's language is not English, translate it automatically.
机器人检测到 issue 内容非英文,自动翻译。
</details>

View File

@@ -1,7 +1,6 @@
name: Release NPM Packages
on:
# 标签触å<C2A6>:支æŒ?v* å’?{package}-v* æ ¼å¼<C3A5>
# Tag trigger: supports v* and {package}-v* formats
push:
tags:
@@ -15,12 +14,11 @@ on:
- 'physics-rapier2d-v*'
- 'worker-generator-v*'
# ä¿<EFBFBD>ç•™æ‰åŠ¨è§¦å<EFBFBD>选项
# Keep manual trigger option
# Manual trigger option
workflow_dispatch:
inputs:
package:
description: '选æ©è¦<EFBFBD>å<EFBFBD>布的åŒ?| 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
# 从标签解æž<C3A6>包å<E280A6><C3A5>åŒç‰ˆæœ¬ | Parse package and version from tag
# Parse package and version from tag
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
# è§£æž<C3A6>æ ¼å¼<C3A5>: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
# æ‰åŠ¨è§¦å<C2A6>:从 package.json 读å<C2BB>å¹?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 }}"
# 获å<C2B7> 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 }}"
# ä¸<C3A4>设置为 latest,latest ä¿<C3A4>留给ç¼è¾å™¨çƒ­æ´æ°ä½¿ç”? # 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 }}
```
---
*自动å<EFBFBD>布 | 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 }}` 包的版本å<EFBFBD>?
### å<>˜æ´
- âœ?å·²å<C2B2>布到 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 ç”±å<C2B1>布工作æµ<C3A6>自动åˆå»º*
*This PR was automatically created by the release workflow*
labels: |
release
${{ steps.parse.outputs.package }}

View File

@@ -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

View File

@@ -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).

View File

@@ -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"
}
}

View File

@@ -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)
`;
}

View File

@@ -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/)
`;
}

View File

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

View File

@@ -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/)
`;
}

View File

@@ -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<typeof setInterval> | 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)
`;
}

View File

@@ -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<string, string>;
/**
* @zh 获取平台特定的开发依赖
* @en Get platform-specific dev dependencies
*/
getDevDependencies(): Record<string, string>;
/**
* @zh 生成平台特定的文件
* @en Generate platform-specific files
*/
generateFiles(config: ProjectConfig): FileEntry[];
/**
* @zh 获取 package.json 的 scripts
* @en Get package.json scripts
*/
getScripts(): Record<string, string>;
}
/**
* @zh 平台适配器注册表类型
* @en Platform adapter registry type
*/
export type AdapterRegistry = Record<PlatformType, PlatformAdapter>;

View File

@@ -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<string, unknown> {
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<string, string>): 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<string, string>;
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<void> {
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 <platform>', 'Target platform (cocos, cocos2, laya, nodejs)')
.action(initCommand);
// Default command: run init
program
.action(() => {
initCommand({});
});
program.parse();

View File

@@ -0,0 +1,8 @@
/**
* @zh ESEngine CLI - 为现有项目添加 ECS 框架
* @en ESEngine CLI - Add ECS framework to existing projects
*
* @packageDocumentation
*/
export * from './adapters/index.js';

View File

@@ -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"]
}

39
pnpm-lock.yaml generated
View File

@@ -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: {}