Compare commits

...

2 Commits

Author SHA1 Message Date
github-actions[bot]
fac4bc19c5 chore: release packages (#360)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-27 10:57:16 +08:00
YHH
aed91dbe45 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.
2025-12-27 10:54:04 +08:00
3 changed files with 197 additions and 2 deletions

View File

@@ -1,5 +1,16 @@
# @esengine/cli
## 1.5.0
### Minor Changes
- [#359](https://github.com/esengine/esengine/pull/359) [`aed91db`](https://github.com/esengine/esengine/commit/aed91dbe4507459f8ac0563ee933e44fd4c9fea6) Thanks [@esengine](https://github.com/esengine)! - feat(cli): 添加 update 命令用于更新 ESEngine 包
- 新增 `esengine update` 命令检查并更新 @esengine/\* 包到最新版本
- 支持 `--check` 参数仅检查可用更新而不安装
- 支持 `--yes` 参数跳过确认提示
- 显示包更新状态,对比当前版本与最新版本
- 更新时保留版本前缀(^ 或 ~
## 1.3.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/cli",
"version": "1.3.0",
"version": "1.5.0",
"description": "CLI tool for adding ESEngine ECS framework to existing projects",
"type": "module",
"main": "dist/index.js",

View File

@@ -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<void> {
printLogo();
const cwd = process.cwd();
const packageJsonPath = path.join(cwd, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
console.log(chalk.red(' ✗ No package.json found.'));
process.exit(1);
}
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const deps = pkg.dependencies || {};
// Find installed @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<string, string> = {};
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(() => {