import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { readAgcDevEndpoint, resolveAgcDevEndpoint, withAgcDevEndpointEnv, } from './dev-port.mjs'; import { isAiGameCreatorServer, preflightExistingVite, readChildFailure, readExistingViteServer, spawnChild, stopChild, terminateChildTree, waitForChildTermination, } from './start-dev-stack.mjs'; const appRoot = fileURLToPath(new URL('..', import.meta.url)); const repoRoot = resolve(appRoot, '../..'); const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js'); function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) { const args = [...argv]; const configOverride = JSON.stringify({ build: { devUrl, beforeDevCommand: '' }, }); const separatorIndex = args.indexOf('--'); if (separatorIndex < 0) { return ['dev', ...args, '--config', configOverride]; } const separatedArguments = args.slice(separatorIndex); if (separatedArguments[1] !== '--') { separatedArguments.unshift('--'); } return [ 'dev', ...args.slice(0, separatorIndex), '--config', configOverride, ...separatedArguments, ]; } function spawnTauriCli(argv, { env = process.env } = {}) { return spawnChild(process.execPath, [tauriCliPath, ...argv], { cwd: appRoot, env, shell: false, }); } async function runTauriDev( argv = process.argv.slice(2), { resolveDevEndpoint = resolveAgcDevEndpoint, preflight = preflightExistingVite, prepareFrontend = prepareFrontendDev, spawnCli = spawnTauriCli, waitForCli = waitForChildTermination, terminateTree = terminateChildTree, } = {}, ) { const endpoint = await resolveDevEndpoint(); await preflight({ endpoint }); let child = null; let frontendChild = null; const preparationAbort = new AbortController(); let resolveShutdown; let shutdownSignal = ''; let repeatedSignal = false; const shutdownRequested = new Promise((resolveRequest) => { resolveShutdown = resolveRequest; }); const signalHandlers = new Map(); for (const signal of ['SIGINT', 'SIGTERM']) { const handler = () => { if (!shutdownSignal) { shutdownSignal = signal; stopChild(child, 'SIGTERM'); stopChild(frontendChild, 'SIGTERM'); preparationAbort.abort(); resolveShutdown(signal); return; } repeatedSignal = true; stopChild(child, 'SIGKILL'); stopChild(frontendChild, 'SIGKILL'); }; signalHandlers.set(signal, handler); process.on(signal, handler); } try { const preparation = prepareFrontend(endpoint, { signal: preparationAbort.signal, onChild(frontend) { frontendChild = frontend; }, }); const prepared = await Promise.race([ preparation.then(() => true), shutdownRequested.then(() => false), ]); if (!prepared || shutdownSignal) return 1; const tauriArguments = buildTauriArguments(argv, endpoint.url); child = spawnCli(tauriArguments, { env: withAgcDevEndpointEnv(endpoint), }); const childResult = waitForCli(child); const outcome = await Promise.race([ childResult.then((failure) => ({ type: 'exit', failure })), shutdownRequested.then((signal) => ({ type: 'signal', signal })), ...(frontendChild ? [ waitForChildTermination(frontendChild).then((failure) => ({ type: 'frontend-exit', failure, })), ] : []), ]); const cleanup = await terminateTree(child, { gracefulTimeoutMs: repeatedSignal ? 0 : 2500, }); if (!cleanup.stopped) { console.error( '[ai-game-creator-shell] Tauri dev exited, but its process tree could not be fully stopped.', ); return 1; } if (outcome.type === 'signal') { return 1; } if (outcome.type === 'frontend-exit') return 1; const { failure } = outcome; return failure.type === 'error' || failure.signal ? 1 : (failure.code ?? 0); } finally { for (const [signal, handler] of signalHandlers) { process.off(signal, handler); } preparationAbort.abort(); if (frontendChild) { const cleanup = await terminateTree(frontendChild); if (!cleanup.stopped) { console.error('[ai-game-creator-shell] 配套开发服务未能完全停止。'); } } } } async function prepareFrontendDev(endpoint, { onChild, signal }) { const frontend = spawnChild( process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'agc:serve'], { cwd: repoRoot, env: withAgcDevEndpointEnv(endpoint) }, ); onChild(frontend); console.log( '[ai-game-creator-shell] 正在准备前端与配套后端,完成后启动 Tauri', ); const deadline = Date.now() + 660_000; while (Date.now() < deadline) { signal.throwIfAborted(); const failure = readChildFailure(frontend); if (failure) { throw new Error( `配套开发服务退出,前端未就绪:${failure.error?.message ?? failure.signal ?? failure.code}`, ); } if (isAiGameCreatorServer(await readExistingViteServer(endpoint))) return; await Promise.race([ new Promise((resolveWait) => setTimeout(resolveWait, 1000)), waitForChildTermination(frontend), ]); } throw new Error(`等待前端与配套后端就绪超时:${endpoint.url}`); } function isDirectModuleExecution() { return Boolean( process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url), ); } export { buildTauriArguments, isDirectModuleExecution, runTauriDev, spawnTauriCli, }; if (isDirectModuleExecution()) { try { process.exitCode = await runTauriDev(); } catch (error) { console.error( `[ai-game-creator-shell] ${error instanceof Error ? error.message : String(error)}`, ); process.exitCode = 1; } }