From aed91dbe4507459f8ac0563ee933e44fd4c9fea6 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Sat, 27 Dec 2025 10:54:04 +0800 Subject: [PATCH] feat(cli): add update command for ESEngine packages (#359) * feat(cli): add update command for ESEngine packages - Add 'update' command to check and update @esengine/* packages - Support --check flag to only show available updates without installing - Support --yes flag to skip confirmation prompt - Display package update status with current vs latest version comparison - Preserve version prefix (^ or ~) when updating - Bump version to 1.4.0 * chore: add changeset for CLI update command * fix(cli): handle 'latest' tag in update command - Treat 'latest' and '*' version tags as needing update - Pin to specific version (^x.x.x) when updating from 'latest' - Show '(pin version)' hint in update status output * fix(cli): minimize file system race condition in update command Re-read package.json immediately before writing to reduce the window for potential race conditions between reading and writing. * fix(cli): use atomic file write to avoid race condition Write to temp file first, then rename for atomic update. --- .changeset/cli-update-command.md | 11 ++ packages/tools/cli/package.json | 2 +- packages/tools/cli/src/cli.ts | 186 ++++++++++++++++++++++++++++++- 3 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 .changeset/cli-update-command.md diff --git a/.changeset/cli-update-command.md b/.changeset/cli-update-command.md new file mode 100644 index 00000000..b2a6f083 --- /dev/null +++ b/.changeset/cli-update-command.md @@ -0,0 +1,11 @@ +--- +"@esengine/cli": minor +--- + +feat(cli): 添加 update 命令用于更新 ESEngine 包 + +- 新增 `esengine update` 命令检查并更新 @esengine/* 包到最新版本 +- 支持 `--check` 参数仅检查可用更新而不安装 +- 支持 `--yes` 参数跳过确认提示 +- 显示包更新状态,对比当前版本与最新版本 +- 更新时保留版本前缀(^ 或 ~) diff --git a/packages/tools/cli/package.json b/packages/tools/cli/package.json index 0b81f8a5..15a5d0a3 100644 --- a/packages/tools/cli/package.json +++ b/packages/tools/cli/package.json @@ -1,6 +1,6 @@ { "name": "@esengine/cli", - "version": "1.3.0", + "version": "1.4.0", "description": "CLI tool for adding ESEngine ECS framework to existing projects", "type": "module", "main": "dist/index.js", diff --git a/packages/tools/cli/src/cli.ts b/packages/tools/cli/src/cli.ts index 008624e5..53ab338d 100644 --- a/packages/tools/cli/src/cli.ts +++ b/packages/tools/cli/src/cli.ts @@ -10,7 +10,7 @@ import { getPlatformChoices, getPlatforms, getAdapter } from './adapters/index.j import type { PlatformType, ProjectConfig } from './adapters/types.js'; import { AVAILABLE_MODULES, getModuleById, getAllModuleIds, type ModuleInfo } from './modules.js'; -const VERSION = '1.1.0'; +const VERSION = '1.4.0'; /** * @zh 打印 Logo @@ -585,6 +585,182 @@ async function removeCommand(moduleIds: string[], options: { yes?: boolean }): P console.log(); } +/** + * @zh 获取 npm 包的最新版本 + * @en Get latest version of npm package + */ +function getLatestVersion(packageName: string): string | null { + try { + const result = execSync(`npm view ${packageName} version`, { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }).trim(); + return result || null; + } catch { + return null; + } +} + +/** + * @zh 比较版本号,返回是否有更新 + * @en Compare versions, return true if newer version available + */ +function isNewerVersion(current: string, latest: string): boolean { + const cleanCurrent = current.replace(/^\^|~/, ''); + + // "latest" 标签视为需要更新(固定到具体版本) + if (cleanCurrent === 'latest' || cleanCurrent === '*') { + return true; + } + + const currentParts = cleanCurrent.split('.').map(Number); + const latestParts = latest.split('.').map(Number); + + for (let i = 0; i < 3; i++) { + const c = currentParts[i] || 0; + const l = latestParts[i] || 0; + if (l > c) return true; + if (l < c) return false; + } + return false; +} + +/** + * @zh 更新项目中的 ESEngine 模块 + * @en Update ESEngine modules in project + */ +async function updateCommand(moduleIds: string[], options: { yes?: boolean; check?: boolean }): Promise { + printLogo(); + + const cwd = process.cwd(); + const packageJsonPath = path.join(cwd, 'package.json'); + + if (!fs.existsSync(packageJsonPath)) { + console.log(chalk.red(' ✗ No package.json found.')); + process.exit(1); + } + + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + const deps = pkg.dependencies || {}; + + // Find installed @esengine packages + const esenginePackages: { name: string; current: string; latest: string | null }[] = []; + + console.log(chalk.gray(' Checking for updates...\n')); + + for (const [name, version] of Object.entries(deps)) { + if (name.startsWith('@esengine/')) { + // If specific modules provided, filter + if (moduleIds.length > 0) { + const mod = getModuleById(moduleIds.find(id => { + const m = getModuleById(id); + return m?.package === name; + }) || ''); + if (!mod || mod.package !== name) continue; + } + + const latest = getLatestVersion(name); + esenginePackages.push({ + name, + current: version as string, + latest + }); + } + } + + if (esenginePackages.length === 0) { + console.log(chalk.yellow(' No ESEngine packages found in dependencies.')); + return; + } + + // Display update status + const updatable: { name: string; current: string; latest: string }[] = []; + + console.log(chalk.bold(' Package Status:\n')); + for (const pkg of esenginePackages) { + const currentClean = pkg.current.replace(/^\^|~/, ''); + const isLatestTag = currentClean === 'latest' || currentClean === '*'; + + if (pkg.latest === null) { + console.log(` ${chalk.gray(pkg.name)}`); + console.log(` ${chalk.red('✗')} Unable to fetch latest version`); + } else if (isNewerVersion(pkg.current, pkg.latest)) { + console.log(` ${chalk.cyan(pkg.name)}`); + if (isLatestTag) { + console.log(` ${chalk.yellow(currentClean)} → ${chalk.green(`^${pkg.latest}`)} ${chalk.gray('(pin version)')}`); + } else { + console.log(` ${chalk.yellow(currentClean)} → ${chalk.green(pkg.latest)}`); + } + updatable.push({ name: pkg.name, current: pkg.current, latest: pkg.latest }); + } else { + console.log(` ${chalk.gray(pkg.name)}`); + console.log(` ${chalk.green('✓')} ${currentClean} (up to date)`); + } + } + + if (updatable.length === 0) { + console.log(chalk.bold('\n All packages are up to date!')); + return; + } + + // Check-only mode + if (options.check) { + console.log(chalk.bold(`\n ${updatable.length} package(s) can be updated.`)); + console.log(chalk.gray(' Run `esengine update` to update.')); + return; + } + + // Confirm update + if (!options.yes) { + console.log(); + const confirm = await prompts({ + type: 'confirm', + name: 'proceed', + message: `Update ${updatable.length} package(s)?`, + initial: true + }); + + if (!confirm.proceed) { + console.log(chalk.yellow('\n Cancelled.')); + return; + } + } + + // Update package.json (re-read to minimize race condition) + console.log(chalk.bold('\n Updating packages...\n')); + + const updates: Record = {}; + for (const upd of updatable) { + const cleanCurrent = upd.current.replace(/^\^|~/, ''); + const isLatestTag = cleanCurrent === 'latest' || cleanCurrent === '*'; + const prefix = isLatestTag ? '^' : (upd.current.startsWith('^') ? '^' : upd.current.startsWith('~') ? '~' : ''); + updates[upd.name] = `${prefix}${upd.latest}`; + console.log(` ${chalk.green('↑')} ${upd.name} → ${prefix}${upd.latest}`); + } + + // Atomic update: write to temp file then rename + const tempPath = `${packageJsonPath}.tmp`; + const freshPkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + freshPkg.dependencies = { ...freshPkg.dependencies, ...updates }; + fs.writeFileSync(tempPath, JSON.stringify(freshPkg, null, 2), 'utf-8'); + fs.renameSync(tempPath, packageJsonPath); + + // Run install + const pm = detectPackageManager(cwd); + const installCmd = pm === 'pnpm' ? 'pnpm install' : pm === 'yarn' ? 'yarn' : 'npm install'; + + console.log(chalk.gray(`\n Running ${installCmd}...`)); + + try { + execSync(installCmd, { cwd, stdio: 'inherit' }); + console.log(chalk.bold('\n Done! All packages updated.')); + } catch { + console.log(chalk.yellow(`\n ⚠ Failed to run install. package.json has been updated.`)); + console.log(chalk.gray(` Run \`${installCmd}\` manually.`)); + } + console.log(); +} + // ========================================================================= // CLI Setup // ========================================================================= @@ -623,6 +799,14 @@ program .option('-y, --yes', 'Skip confirmation') .action(removeCommand); +program + .command('update [modules...]') + .alias('up') + .description('Update ESEngine packages to latest versions') + .option('-y, --yes', 'Skip confirmation') + .option('-c, --check', 'Only check for updates, do not install') + .action(updateCommand); + // Default command: show help program .action(() => {