diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json index 468c3ed69..74f901d6f 100644 --- a/apps/ai-game-creator-shell/package.json +++ b/apps/ai-game-creator-shell/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "npm --prefix ../.. exec tauri -- dev", "dev-server": "node scripts/start-dev-server.mjs", + "dev-stack": "node scripts/start-dev-stack.mjs", "build": "npm --prefix ../.. exec tauri -- build", "llm-status": "node scripts/run-cli-with-config.mjs --llm-status", "agent-run": "node scripts/run-cli-with-config.mjs --agent-run", diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index da05c1af0..2b723681a 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -452,8 +452,10 @@ if (!viteConfigSource.includes('strictPort: true')) { } if ( - !tauriConfig.build?.beforeDevCommand?.includes( - 'run ai-game-creator-shell:dev-server', + !( + tauriConfig.build?.beforeDevCommand?.includes( + 'run ai-game-creator-shell:dev-server', + ) || tauriConfig.build?.beforeDevCommand?.includes('run agc:serve') ) ) { throw new Error( diff --git a/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs b/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs new file mode 100644 index 000000000..60de90438 --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs @@ -0,0 +1,283 @@ +import { spawn } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import http from 'node:http'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const appRoot = fileURLToPath(new URL('..', import.meta.url)); +const repoRoot = resolve(appRoot, '../..'); +const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json'); +const viteHost = '127.0.0.1'; +const vitePort = 3080; +const viteUrl = `http://${viteHost}:${vitePort}/`; +const viteMarkerUrl = `${viteUrl}__agc_dev_server.json`; +const defaultApiTarget = process.env.RUST_SERVER_TARGET || 'http://127.0.0.1:8082'; +const backendDatabase = 'genarrative-game-creator-dev'; +const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + +function readJson(path) { + if (!existsSync(path)) { + return null; + } + try { + return JSON.parse(readFileSync(path, 'utf8')); + } catch { + return null; + } +} + +function httpGetText(url, timeout = 1000) { + return new Promise((resolveRequest) => { + const request = http.get(url, { timeout }, (response) => { + let body = ''; + response.setEncoding('utf8'); + response.on('data', (chunk) => { + if (body.length < 4096) { + body += chunk; + } + }); + response.on('end', () => { + resolveRequest({ + statusCode: response.statusCode ?? 0, + body, + }); + }); + }); + request.on('timeout', () => { + request.destroy(); + resolveRequest(null); + }); + request.on('error', () => resolveRequest(null)); + }); +} + +async function isHttpReady(url) { + const response = await httpGetText(url); + return Boolean(response && response.statusCode >= 200 && response.statusCode < 300); +} + +function readBackendTargets({ requireAgcDatabase = false } = {}) { + const state = readJson(devStackStatePath); + const apiServer = state?.services?.['api-server']; + const spacetime = state?.services?.spacetime; + const isActive = (service) => + service && ['running', 'reused', 'starting'].includes(service.status ?? ''); + const database = typeof state?.database === 'string' ? state.database : ''; + const hasMatchingDatabase = database === backendDatabase; + const canReuseState = !requireAgcDatabase || hasMatchingDatabase; + const apiUrl = + canReuseState && isActive(apiServer) && apiServer.url + ? apiServer.url + : requireAgcDatabase + ? '' + : defaultApiTarget; + const spacetimeUrl = + canReuseState && isActive(spacetime) && spacetime.url + ? spacetime.url + : requireAgcDatabase + ? '' + : 'http://127.0.0.1:3101'; + return { + apiUrl, + spacetimeUrl, + database, + hasMatchingDatabase, + }; +} + +async function isBackendReady() { + const { apiUrl, spacetimeUrl, hasMatchingDatabase } = readBackendTargets({ + requireAgcDatabase: true, + }); + return ( + hasMatchingDatabase && + Boolean(apiUrl) && + Boolean(spacetimeUrl) && + (await isHttpReady(`${apiUrl}/healthz`)) && + (await isHttpReady(`${spacetimeUrl}/v1/ping`)) + ); +} + +async function readExistingViteServer() { + return httpGetText(viteUrl); +} + +function isAiGameCreatorServer(response) { + return ( + response && + response.statusCode >= 200 && + response.statusCode < 500 && + response.body.includes('