AGC 版本号以仓库为准,新增 bump-version 提升版本命令 #298

Closed
suzmii wants to merge 8 commits from fix/agc-release-version-sync into master
12 changed files with 316 additions and 53 deletions
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.12",
"version": "0.1.19",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
@@ -9,6 +9,7 @@
"dev-stack": "node scripts/start-dev-stack.mjs",
"build": "node scripts/build-release.mjs",
"release:upload": "node scripts/release-upload.mjs",
"bump-version": "node scripts/bump-version.mjs",
"skill-pack:check": "node scripts/check-skill-pack.mjs",
"skill-pack:sync": "node scripts/check-skill-pack.mjs --write",
"skill-pack:test": "node --test scripts/check-skill-pack.test.mjs",
@@ -5,6 +5,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = path.resolve(appRoot, '../..');
const defaultReleaseTarget = 'x86_64-pc-windows-msvc';
const releaseTarget =
process.env.AGC_BUILD_TARGET?.trim() || defaultReleaseTarget;
@@ -49,16 +50,23 @@ function parseVersion(value, label) {
return value;
}
export function nextPatchVersion(localVersion, remoteVersion) {
const local = parseVersion(localVersion, '本地版本');
const remote =
remoteVersion == null ? null : parseVersion(remoteVersion, 'OSS版本');
const base = remote && compareVersions(remote, local) > 0 ? remote : local;
const [major, minor, patch] = base.split('.').map(Number);
if (patch === Number.MAX_SAFE_INTEGER) {
throw new Error(`版本号 patch 已达到上限:${base}`);
export function bumpVersion(current, target = 'patch') {
const [major, minor, patch] =
parseVersion(current, '当前版本').split('.').map(Number);
if (target === 'major') return `${major + 1}.0.0`;
if (target === 'minor') return `${major}.${minor + 1}.0`;
if (target === 'patch' || target == null) {
if (patch === Number.MAX_SAFE_INTEGER) {
throw new Error(`版本号 patch 已达到上限:${current}`);
}
return `${major}.${minor}.${patch + 1}`;
}
return `${major}.${minor}.${patch + 1}`;
if (typeof target === 'string' && /^\d+\.\d+\.\d+$/u.test(target)) {
return parseVersion(target, '指定版本');
}
throw new Error(
`无法识别的版本目标:${String(target)}(支持 patch / minor / major / 明确的三段版本号)`,
);
}
async function readRemoteVersion() {
@@ -88,14 +96,52 @@ function replaceVersionLine(source, version, pattern, label) {
return source.replace(pattern, `$1${version}$3`);
}
export async function prepareReleaseVersion() {
const localVersion = parseVersion(readPackageJson().version, '本地版本');
const remoteVersion = await readRemoteVersion();
const requestedVersion = process.env.AGC_RELEASE_VERSION?.trim();
const nextVersion = requestedVersion
? parseVersion(requestedVersion, '指定版本')
: nextPatchVersion(localVersion, remoteVersion);
const versionFileSources = [
{
file: packageJsonPath,
pattern: /("version"\s*:\s*")([^"]+)(")/u,
label: 'package.json',
},
{
file: rootPackageLockPath,
pattern: /("apps\/ai-game-creator-shell"\s*:\s*\{\s*\n\s*"name"\s*:\s*"@genarrative\/ai-game-creator-shell"\s*,\s*\n\s*"version"\s*:\s*")([^"]+)(")/u,
label: 'package-lock.json',
},
{
file: tauriConfigPath,
pattern: /("version"\s*:\s*")([^"]+)(")/u,
label: 'tauri.conf.json',
},
{
file: cargoManifestPath,
pattern: /(^\[package\][\s\S]*?^version\s*=\s*")([^"]+)(")/mu,
label: 'Cargo.toml',
},
{
file: cargoLockPath,
pattern: /(^name\s*=\s*"genarrative-ai-game-creator-shell"\s*\nversion\s*=\s*")([^"]+)(")/mu,
label: 'Cargo.lock',
},
];
export function readLocalVersion() {
return parseVersion(readPackageJson().version, '本地版本');
}
export function validateVersionConsistency(expectedVersion) {
for (const source of versionFileSources) {
const match = fs.readFileSync(source.file, 'utf8').match(source.pattern);
const actual = match ? match[2].trim() : '<未找到>';
if (actual !== expectedVersion) {
throw new Error(
`AGC 版本号不一致:${source.label} 应为 ${expectedVersion},实际 ${actual}`,
);
}
}
}
export function writeVersionFiles(version) {
const nextVersion = parseVersion(version, '目标版本');
const packageSource = fs.readFileSync(packageJsonPath, 'utf8');
fs.writeFileSync(
packageJsonPath,
@@ -151,14 +197,47 @@ export async function prepareReleaseVersion() {
),
);
console.log(
requestedVersion
? `[ai-game-creator-shell] 使用指定版本 ${nextVersion}(本地 ${localVersion} / OSS ${remoteVersion ?? '不存在'}`
: `[ai-game-creator-shell] 版本 ${localVersion} / OSS ${remoteVersion ?? '不存在'} -> ${nextVersion}`,
);
return nextVersion;
}
export function assertVersionCommitted() {
const repoPaths = versionFileSources.map((source) =>
path.relative(repoRoot, source.file).replaceAll('\\', '/'),
);
const changed =
spawnSync(
'git',
['diff', '--quiet', 'HEAD', '--', ...repoPaths],
{ cwd: repoRoot },
).status === 1;
if (changed) {
throw new Error(
'版本文件相对 HEAD 存在未提交改动,请先提交后再发布:运行 npm --prefix apps/ai-game-creator-shell run bump-version -- --commit',
);
}
}
export function prepareReleaseVersion() {
const localVersion = readLocalVersion();
validateVersionConsistency(localVersion);
console.log(
`[ai-game-creator-shell] 使用仓库版本 ${localVersion}(不再自动递增或自由指定版本)`,
);
return localVersion;
}
export async function assertVersionNotBelowOss(version) {
const remoteVersion = await readRemoteVersion();
if (remoteVersion && compareVersions(version, remoteVersion) < 0) {
throw new Error(
`仓库版本 ${version} 低于线上 OSS 版本 ${remoteVersion}。请先运行 npm --prefix apps/ai-game-creator-shell run bump-version 提升版本后再发布。`,
);
}
console.log(
`[ai-game-creator-shell] 线上 OSS 版本 ${remoteVersion ?? '不存在'},发布版本 ${version}`,
);
}
export function runTauriBuild(args = []) {
const noBundle = args.includes('--no-bundle');
const hasTarget = args.includes('--target');
@@ -1,16 +1,19 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { test } from 'node:test';
import { fileURLToPath } from 'node:url';
import {
bumpVersion,
compareVersions,
createUpdateManifest,
nextPatchVersion,
selectReleaseArtifact,
} from './build-release.mjs';
test('selects an explicit release artifact when configured', () => {
const artifactPath = new URL('../package.json', import.meta.url).pathname;
const artifactPath = fileURLToPath(
new URL('../package.json', import.meta.url),
);
const previous = process.env.AGC_UPDATE_ARTIFACT;
process.env.AGC_UPDATE_ARTIFACT = artifactPath;
try {
@@ -30,7 +33,7 @@ test('does not select unsupported files', () => {
test('manifest contains version, download URL and integrity fields', () => {
const manifest = createUpdateManifest(
new URL('../package.json', import.meta.url).pathname,
fileURLToPath(new URL('../package.json', import.meta.url)),
);
assert.match(manifest.version, /^\d+\.\d+\.\d+$/u);
assert.match(
@@ -46,7 +49,7 @@ test('manifest preserves multiline release notes', () => {
process.env.AGC_UPDATE_RELEASE_NOTES = '第一行\n第二行\r\n第三行';
try {
const manifest = createUpdateManifest(
new URL('../package.json', import.meta.url).pathname,
fileURLToPath(new URL('../package.json', import.meta.url)),
);
assert.equal(manifest.releaseNotes, '第一行\n第二行\r\n第三行');
} finally {
@@ -55,11 +58,14 @@ test('manifest preserves multiline release notes', () => {
}
});
test('next release version follows the higher local or OSS version', () => {
test('bump version follows the requested target', () => {
assert.equal(compareVersions('0.1.15', '0.1.12'), 1);
assert.equal(nextPatchVersion('0.1.12', '0.1.15'), '0.1.16');
assert.equal(nextPatchVersion('0.1.18', '0.1.15'), '0.1.19');
assert.equal(nextPatchVersion('0.1.12', null), '0.1.13');
assert.equal(compareVersions('0.1.12', '0.1.12'), 0);
assert.equal(bumpVersion('0.1.19', 'patch'), '0.1.20');
assert.equal(bumpVersion('0.1.19'), '0.1.20');
assert.equal(bumpVersion('0.1.19', 'minor'), '0.2.0');
assert.equal(bumpVersion('0.1.19', 'major'), '1.0.0');
assert.equal(bumpVersion('0.1.12', '0.1.25'), '0.1.25');
});
test('release upload forces overwrite for versioned artifact and latest pointer', () => {
@@ -72,3 +78,12 @@ test('release upload forces overwrite for versioned artifact and latest pointer'
2,
);
});
test('release upload verifies the version is committed and not below OSS', () => {
const source = readFileSync(
new URL('./release-upload.mjs', import.meta.url),
'utf8',
);
assert.match(source, /assertVersionCommitted\(\)/u);
assert.match(source, /assertVersionNotBelowOss\(/u);
});
@@ -0,0 +1,121 @@
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = path.resolve(appRoot, '../..');
const {
bumpVersion,
readLocalVersion,
writeVersionFiles,
} = await import('./build-release.mjs');
const versionFiles = [
'apps/ai-game-creator-shell/package.json',
'package-lock.json',
'apps/ai-game-creator-shell/src-tauri/tauri.conf.json',
'apps/ai-game-creator-shell/src-tauri/Cargo.toml',
'apps/ai-game-creator-shell/src-tauri/Cargo.lock',
];
function parseArgs(argv) {
const args = argv.slice(2);
const explicitIndex = args.indexOf('--version');
const explicit = explicitIndex >= 0 ? args[explicitIndex + 1] : null;
const target =
explicit ??
args.find((arg) => arg === 'patch' || arg === 'minor' || arg === 'major') ??
'patch';
return { target, commit: args.includes('--commit') };
}
function runGit(args) {
const result = spawnSync('git', args, { cwd: repoRoot, stdio: 'inherit' });
if (result.error) throw result.error;
return result.status ?? 1;
}
function hasUncommittedVersionChanges() {
return (
spawnSync('git', ['diff', '--quiet', '--', ...versionFiles], {
cwd: repoRoot,
}).status === 1
);
}
function otherStagedFiles() {
const result = spawnSync(
'git',
['diff', '--cached', '--name-only'],
{ cwd: repoRoot, encoding: 'utf8' },
);
if (result.status !== 0) {
throw new Error('无法读取已暂存文件列表');
}
const versionSet = new Set(versionFiles);
return result.stdout
.split('\n')
.map((file) => file.trim())
.filter(Boolean)
.filter((file) => !versionSet.has(file));
}
const { target, commit } = parseArgs(process.argv);
// 提交前先确认没有其他已暂存改动,避免把无关改动一并提交;若存在则在改动任何文件前中止。
if (commit) {
const others = otherStagedFiles();
if (others.length > 0) {
console.error(
`[bump-version] 检测到其他已暂存改动,为避免误提交,请先单独处理后再运行 --commit:\n ${others.join('\n ')}`,
);
console.error(
'提示:用 git restore --staged <file> 取消暂存,或先提交这些改动。',
);
process.exit(1);
}
}
const current = readLocalVersion();
// 若版本文件已带着一次未提交的提升(例如先默认跑过一次),再 --commit 时不再重复递增,直接提交现有改动。
const next =
commit && hasUncommittedVersionChanges()
? current
: bumpVersion(current, target);
writeVersionFiles(next);
if (!commit) {
console.log(
`[bump-version] 版本 ${current} -> ${next}(已写入版本文件,未创建提交;如需提交加 --commit)`,
);
process.exit(0);
}
const addStatus = runGit(['add', ...versionFiles]);
if (addStatus !== 0) process.exit(addStatus);
const hasDiff =
spawnSync('git', ['diff', '--cached', '--quiet'], {
cwd: repoRoot,
}).status === 1;
if (!hasDiff) {
console.log(`[bump-version] 版本未变化(已是 ${current}),未创建提交`);
process.exit(0);
}
const commitStatus = runGit([
'commit',
'-m',
`提升 AGC 版本至 ${next}`,
'-m',
`- AGC 客户端版本提升至 ${next}`,
'-m',
'- 同步更新 package.json、根 package-lock.json、tauri.conf.json、Cargo.toml、Cargo.lock 中的 AGC 包条目',
]);
if (commitStatus !== 0) process.exit(commitStatus);
console.log(`[bump-version] 已提交版本 ${next}(本地提交,未推送)`);
@@ -9,8 +9,13 @@ if (!/^[a-z0-9][a-z0-9.-]{1,62}$/u.test(bucket) || /[\r\n\0]/u.test(endpoint)) {
}
process.env.AGC_UPDATE_OSS_BASE_URL ||= `https://${bucket}.${endpoint}/agc`;
const { generateUpdateManifest, prepareReleaseVersion, runTauriBuild } =
await import('./build-release.mjs');
const {
assertVersionCommitted,
assertVersionNotBelowOss,
generateUpdateManifest,
prepareReleaseVersion,
runTauriBuild,
} = await import('./build-release.mjs');
function runOssutil(args) {
const binary = process.env.OSSUTIL_BIN?.trim() || 'ossutil';
@@ -36,7 +41,9 @@ function runOssutil(args) {
if (result.status !== 0) process.exit(result.status ?? 1);
}
await prepareReleaseVersion();
const releaseVersion = await prepareReleaseVersion();
assertVersionCommitted();
await assertVersionNotBelowOss(releaseVersion);
runTauriBuild([]);
const { artifact, manifestPath, manifest } = generateUpdateManifest();
const artifactKey = `agc/${manifest.version}/${path.basename(artifact)}`;
+1 -1
View File
@@ -1703,7 +1703,7 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
version = "0.1.12"
version = "0.1.19"
dependencies = [
"agent-runtime-core",
"axum",
@@ -1,6 +1,6 @@
[package]
name = "genarrative-ai-game-creator-shell"
version = "0.1.12"
version = "0.1.19"
edition = "2021"
publish = false
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Genarrative AI Game Creator",
"version": "0.1.12",
"version": "0.1.19",
"identifier": "world.genarrative.ai-game-creator",
"build": {
"beforeDevCommand": "npm --prefix ../.. run agc:serve",
@@ -29,21 +29,62 @@ AGC 每次启动时由根窗口检查一次公开 OSS 更新清单。清单默
## 发布约定
当前发布目标固定为 Windows x64 NSIS。执行 `npm run ai-game-creator-shell:build` 会先读取
`VITE_AGC_UPDATE_MANIFEST_URL`(默认 `https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/latest.json`)的
`latest.json`,取本地与 OSS 的较高版本并递增一个 patch,然后同步更新 package、Tauri 和 Cargo
版本后再向 Tauri 传入 `--target x86_64-pc-windows-msvc` 构建。OSS 清单首次不存在时按本地版本递增;
OSS 请求失败、清单格式错误或版本无效会终止发布,避免覆盖线上版本。构建完成后自动扫描 `.exe`
安装包,并在 `apps/ai-game-creator-shell/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/latest.json`
生成包含版本、下载地址、大小和 SHA-256 的清单。可通过 `AGC_BUILD_TARGET` 显式覆盖目标(发布仍应使用
Windows x64),通过 `AGC_UPDATE_ARTIFACT` 指定要发布的安装包,通过 `AGC_UPDATE_OSS_BASE_URL` 指定
OSS 前缀,通过 `AGC_RELEASE_VERSION` 指定三段版本号(仅在明确需要复现指定版本时使用),通过
`AGC_UPDATE_RELEASE_NOTES` 写入发布说明,支持多行文本且保留内部换行;`--no-bundle` smoke 构建不会读取 OSS、修改版本或生成清单。
> ⚠️ 发布新版本时,版本号提升只发生在构建/上传工作区内,**不会自动回写仓库**。
> 🔑 版本号以仓库为准。发布流程不再自动递增版本,也不再支持 `AGC_RELEASE_VERSION` 自由指定版本。
> 发布前必须先运行版本提升命令,把目标版本写回仓库并提交,发布只读取这份已提交的版本。
**发布前先提升版本(开发者执行)**
执行以下任一命令,会把 AGC 版本写入 `package.json`、根 `package-lock.json`
`tauri.conf.json``Cargo.toml``Cargo.lock` 五处。**默认只写入版本文件、不会创建提交**;
需要创建提交时在命令末尾加 `--commit`(只做本地提交,push 由开发者自行确认):
```bash
# 提升一个 patch(默认),只写入版本文件,例如 0.1.19 -> 0.1.20
npm run ai-game-creator-shell:bump-version
npm run ai-game-creator-shell:bump-version -- patch
# 提升版本并本地提交
npm run ai-game-creator-shell:bump-version -- --commit
# 提升 minor / major
npm run ai-game-creator-shell:bump-version -- minor
npm run ai-game-creator-shell:bump-version -- major
# 指定明确的三段版本(一般只用于复现/回填)
npm run ai-game-creator-shell:bump-version -- --version 0.1.25
# 指定版本并提交
npm run ai-game-creator-shell:bump-version -- --version 0.1.25 --commit
```
若先用默认方式写入版本(未提交),再补 `--commit`,命令会识别到工作区已提升、直接提交现有改动,
**不会重复递增**。提交前若检测到**除版本文件外的其他已暂存改动**,会中止并提示先处理,避免误提交无关内容。
提交标题固定为“提升 AGC 版本至 x.y.z”。
**构建/发布只读仓库版本(`npm run ai-game-creator-shell:build` 与 `release:upload`**
构建与发布都不再改写版本文件,而是读取仓库版本。前者只校验五个版本来源一致(不一致会中止,
防止漂移);后者(`release:upload`)在上传前还会做两道校验:
1. 校验版本相对 `HEAD` 已提交(`assertVersionCommitted`):若版本文件存在未提交改动会中止,
确保本地发布与 Jenkins 检出的是同一个版本。
2. 通过 `assertVersionNotBelowOss` 读取 OSS `latest.json`:若仓库版本**低于**线上版本会中止,
并提示先运行 `bump-version`,避免回退线上版本;OSS 读取失败或清单格式错误同样会中止。
仓库版本等于或高于线上版本才放行。
当前发布目标固定为 Windows x64 NSIS,会向 Tauri 传入 `--target x86_64-pc-windows-msvc` 构建。
构建完成后自动扫描 `.exe` 安装包,并在
`apps/ai-game-creator-shell/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/latest.json`
生成包含版本、下载地址、大小和 SHA-256 的清单,版本取仓库 `package.json``version`
可通过 `AGC_BUILD_TARGET` 显式覆盖目标(发布仍应使用 Windows x64),通过 `AGC_UPDATE_ARTIFACT`
指定要发布的安装包,通过 `AGC_UPDATE_OSS_BASE_URL` 指定 OSS 前缀,通过 `AGC_UPDATE_RELEASE_NOTES`
写入发布说明(支持多行文本且保留内部换行);`--no-bundle` smoke 构建不会读取 OSS、修改版本或生成清单。
每次发布安装包上传完成后,再使用 ossutil 的 `--force` 覆盖上传同一目录生成的 `latest.json`,确保固定的 latest 指针和 `downloadUrl` 指向已存在的 OSS 对象;未显式强制覆盖时,ossutil 在目标已存在时会交互询问并按默认值跳过,不能作为 Jenkins 非交互发布方式。清单和安装包均使用公开可读对象,不在清单中保存凭据、签名或本地路径。构建脚本本身不负责上传 OSS,发布流水线通过 `release:upload` 完成上传。
如需一键构建并上传,可执行 `npm run ai-game-creator-shell:release:upload`。该命令要求本机已安装并配置 `ossutil`
先按上述规则比较 OSS 版本、递增 patch、构建 Windows x64 NSIS,再上传安装包和 `latest.json`。默认上传到
先按上述规则取仓库版本并校验、构建 Windows x64 NSIS,再上传安装包和 `latest.json`。默认上传到
`agc-dev` / `oss-rg-china-mainland.aliyuncs.com`,也可用 `AGC_OSS_BUCKET``AGC_OSS_ENDPOINT``OSSUTIL_BIN`
覆盖;本机执行时凭据由 ossutil 本机配置读取,不能写入仓库或命令行参数。
@@ -63,6 +104,6 @@ Jenkins Checkout 的 `git clean -fdx` 会清理该构建目录,因此每次全
Jenkins Job 在“Build and upload”阶段通过受保护凭据 ID `AliyunAccessKeyId`
`AliyunaccessKeySecret` 注入 AccessKey,仅在当前进程运行时传给 ossutil,不写入仓库、workspace 或构建日志;
本机运行仍使用 ossutil 配置。凭据必须具备 `PutObject` 权限;OSS 对客户端保持公共读即可,公共读本身不授予
Jenkins 上传权限。由于版本号取决于 OSS 当前清单,Job 已关闭并发构建;若 Jenkins
上存在多个 AGC 发布 Job,还应使用同一个 Lockable Resource 串行化发布。Job 参数
`AGC_RELEASE_VERSION` 留空时自动递增,填写后会使用指定版本并更新对应的 `latest.json`,因此回滚或测试旧版本前应确认不会覆盖线上更新入口
Jenkins 上传权限。由于版本号以仓库为准、发布只读仓库版本且禁止回退线上版本,Job 已关闭并发构建;若 Jenkins
上存在多个 AGC 发布 Job,还应使用同一个 Lockable Resource 串行化发布。发布前请先在仓库上运行
`npm run ai-game-creator-shell:bump-version` 提升并提交版本,再触发发布流水线
@@ -21,7 +21,6 @@ pipeline {
parameters {
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支')
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit')
string(name: 'AGC_RELEASE_VERSION', defaultValue: '', description: '可选,指定三段版本号;留空则按 OSS 与本地版本自动递增 patch')
text(name: 'AGC_UPDATE_RELEASE_NOTES', defaultValue: '', description: '可选,支持多行文本,写入 latest.json 的发布说明')
string(name: 'OSSUTIL_BIN', defaultValue: 'ossutil', description: 'ossutil 或 ossutil.exe 的绝对路径/命令名')
}
@@ -127,7 +126,6 @@ pipeline {
withEnv([
"PATH=${env.AGC_WINDOWS_PATH}",
"OSSUTIL_BIN=${params.OSSUTIL_BIN}",
"AGC_RELEASE_VERSION=${params.AGC_RELEASE_VERSION}",
"AGC_UPDATE_RELEASE_NOTES=${params.AGC_UPDATE_RELEASE_NOTES}",
]) {
powershell '''
+1 -1
View File
@@ -93,7 +93,7 @@
},
"apps/ai-game-creator-shell": {
"name": "@genarrative/ai-game-creator-shell",
"version": "0.1.12",
"version": "0.1.19",
"dependencies": {
"@cubone/react-file-manager": "^1.35.0",
"@genarrative/image-canvas-core": "0.1.0",
+1
View File
@@ -172,6 +172,7 @@
"ai-game-creator-shell:dev-server": "npm --prefix apps/ai-game-creator-shell run dev-server",
"ai-game-creator-shell:build": "npm --prefix apps/ai-game-creator-shell run build --",
"ai-game-creator-shell:release:upload": "npm --prefix apps/ai-game-creator-shell run release:upload",
"ai-game-creator-shell:bump-version": "npm --prefix apps/ai-game-creator-shell run bump-version --",
"ai-game-creator-shell:llm-status": "npm --prefix apps/ai-game-creator-shell run llm-status --",
"ai-game-creator-shell:agent-task": "npm --prefix apps/ai-game-creator-shell run agent-task --",
"agc:chat": "npm --prefix apps/ai-game-creator-shell run chat --",