commit 38fd33394ad491fcfc78ba1439bc33d7384e6511 Author: MerlinH Date: Wed May 27 22:43:29 2026 +0000 feat: add Open GameStudio Codex port diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d326297 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +projects/ +*.tgz diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ec2f2de --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,15 @@ +# Global Agent Instructions + +Use `npm run validate` before any parity claim. + +This project uses `"type": "module"`, `module: "NodeNext"`, and `moduleResolution: "NodeNext"`. Every relative TypeScript import must use the emitted `.js` specifier: write `import { x } from "./config.js"`, never `import { x } from "./config"`. + +For local development before install/link, use npm scripts. Package scripts build first and exercise the built CLI through `node dist/cli.js`; use `npm run init -- ...`, `npm run manage -- ...`, `npm run templates -- list`, and `npm run validate -- ...`. Use `npm exec open-gamestudio -- ...` only after build/link/install or inside package-bin smoke fixtures. Bare `open-gamestudio ...` is only guaranteed after package install/link. + +Keep generated game projects under `projects//`. + +Do not load all agents or all templates for a single role task. + +`src/agents.ts` is the single owner for generated project `AGENTS.md`. + +Direct Codex execution, telemetry, planner/next, ownership enforcement, and parallel orchestration are future-only. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7ef66d3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing + +Use Node 20 or newer. + +Before opening changes: + +```bash +npm run typecheck +npm run build +npm test +npm run validate +``` + +Do not add Python compatibility files, duplicate script-wrapper logic, telemetry, direct Codex execution, planner behavior, parallel orchestration, or ownership enforcement without a new design. + +Do not edit `research/*` as part of implementation changes unless the task explicitly asks for research updates. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c15f3c8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 MerlinH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4088727 --- /dev/null +++ b/README.md @@ -0,0 +1,134 @@ +# Open GameStudio + +Open GameStudio is a Node/TypeScript CLI package for creating and managing local, agent-assisted game projects. It provides project scaffolding, engine-aware configuration, base agent prompts, reusable templates, bounded prompt packets, and validation gates that keep generated project artifacts predictable. + +The package is an agent workflow layer for game making. It does not host a service, execute assistants directly, or require a specific model provider. The current CLI prepares the project structure and prompt context that Codex or another agent can use from your own environment. + +## Why This Exists + +Open GameStudio started as a port motivated by a simple need: make the game-studio workflow open, portable, local-first, scriptable, and usable outside a single assistant environment. + +Claude Game Studio deserves real kudos for proving that role-based game-development workflows can be practical and useful. Open GameStudio is inspired by that idea, but it is an independent implementation with different priorities: + +- CLI and package first, with deterministic npm scripts for local development. +- Provider and model agnostic, with no direct Claude, Codex, or assistant lock-in. +- Generated game projects live under `projects//`. +- Engine configs, templates, and base agents are package assets. +- Validation is explicit and hard-failing instead of advisory. +- Prompt packets are prepared for external agents instead of spawning a provider process. +- Direct Codex execution, telemetry, planner/`next`, ownership enforcement, changed-file tracking, and parallel orchestration are future-only. + +The goal is not to clone another tool. The goal is to make the workflow contract inspectable, portable, and easy to run in normal developer tooling. + +## Requirements + +- Node.js 20 or newer. +- npm. + +## Install + +For package use after installation or linking: + +```sh +npm exec open-gamestudio -- --help +npm exec open-gamestudio -- templates list +``` + +For local development from this repository, use the npm scripts. They build first and then exercise the built CLI through `node dist/cli.js`: + +```sh +npm run init -- --name "My Game" --engine godot --mode prototype --non-interactive +npm run manage -- --project projects/my-game +npm run templates -- list +npm run validate -- --project projects/my-game +``` + +## Quick Start + +Create a project: + +```sh +npm run init -- --name "My Game" --engine godot --mode prototype --non-interactive --concept "A compact puzzle game about routing trains" +``` + +Inspect project status: + +```sh +npm run manage -- --project projects/my-game +``` + +List templates: + +```sh +npm run templates -- list +``` + +Show a template: + +```sh +npm run templates -- show gdd +``` + +Validate the repository or a generated project: + +```sh +npm run validate +npm run validate -- --project projects/my-game +``` + +Prepare a bounded prompt packet for an agent: + +```sh +npm run build --silent +node dist/cli.js run market_analyst --project projects/my-game --task "Create the initial market overview." --dry-run +``` + +The `run` command prepares context and output paths. In this build, you execute Codex or another agent separately and point it at the generated prompt packet. + +## CLI Commands + +- `init` / `new`: create a project under `projects//`. +- `status`: print project phase, status, engine, and next validation command. +- `resume`: print a read-only continuation summary. +- `freeze`: mark a project as frozen. +- `validate`: run repository or project validation and exit nonzero on failure. +- `templates list`: list packaged template IDs. +- `templates show `: print a packaged template. +- `run `: prepare one bounded prompt packet for a project agent. + +## Project Layout + +Repository assets: + +- `src/`: TypeScript CLI implementation. +- `agents/base/`: base role prompts packaged with the CLI. +- `templates/`: reusable document and setup templates. +- `engine_configs/`: engine overlays for Godot, Unity, and Unreal. +- `docs/`: setup, migration, validation, and example notes. +- `tests/`: Vitest coverage for project workflow, templates, agents, runner prompts, validation, and engine behavior. + +Generated project artifacts: + +- `projects//`: the project root created by `init`. +- `project.gamestudio.json`: project metadata and workflow state. +- `AGENTS.md`: generated project instructions owned by `src/agents.ts`. +- `documentation/`: generated game-design and workflow documents. +- `source/project-/`: engine project location contract. +- `.gamestudio/runs/`: prepared prompt packets and run metadata. + +## Development + +Use the repository scripts: + +```sh +npm run build +npm run typecheck +npm run test +npm run validate +``` + +This project uses ESM TypeScript with `module` and `moduleResolution` set to `NodeNext`. Relative TypeScript imports include the emitted `.js` specifier. + +## License + +Open GameStudio is released under the MIT License. See `LICENSE`. diff --git a/agents/base/data_scientist.md b/agents/base/data_scientist.md new file mode 100644 index 0000000..ac62476 --- /dev/null +++ b/agents/base/data_scientist.md @@ -0,0 +1,27 @@ +# Role + +Define analytics, metrics, events, and experiment-readiness for the project. + +# Inputs + +Project config, core loop, target audience, and analytics template. + +# Outputs + +Analytics plan, event taxonomy, metric definitions, and validation notes. + +# Output Paths + +Use `documentation/technical/analytics/analytics-plan.md`. + +# Validation + +Run `npm run validate -- --project `. + +# Engine Notes + +Map instrumentation ideas to the selected engine. + +# Rules + +Do not add telemetry for this toolkit; discuss only game analytics artifacts. diff --git a/agents/base/game_feel_developer.md b/agents/base/game_feel_developer.md new file mode 100644 index 0000000..a5adb08 --- /dev/null +++ b/agents/base/game_feel_developer.md @@ -0,0 +1,27 @@ +# Role + +Improve responsiveness, feedback, controls, camera, and tuning. + +# Inputs + +Project config, mechanics notes, engine overlay, and playtest observations. + +# Outputs + +Game-feel tuning notes, implementation tasks, and validation criteria. + +# Output Paths + +Use `documentation/design/feel/` and selected engine source paths. + +# Validation + +Run `npm run validate -- --project `. + +# Engine Notes + +Map feedback and tuning work to engine-specific systems. + +# Rules + +Keep changes measurable and playtest-oriented. diff --git a/agents/base/market_analyst.md b/agents/base/market_analyst.md new file mode 100644 index 0000000..08d2169 --- /dev/null +++ b/agents/base/market_analyst.md @@ -0,0 +1,27 @@ +# Role + +Analyze audience, positioning, competitors, and monetization fit. + +# Inputs + +Project config, competitor names, audience, genre, platform, and market template. + +# Outputs + +Market overview, competitor comparison, positioning risks, and recommended research questions. + +# Output Paths + +Use `resources/market-research/market-analysis.md`. + +# Validation + +Run `npm run validate -- --project `. + +# Engine Notes + +Relate market risks to engine/platform constraints. + +# Rules + +Do not generate eager per-competitor reports unless the task asks for them. diff --git a/agents/base/master_orchestrator.md b/agents/base/master_orchestrator.md new file mode 100644 index 0000000..4b50aad --- /dev/null +++ b/agents/base/master_orchestrator.md @@ -0,0 +1,27 @@ +# Role + +Coordinate the game-studio workflow, sequence work, and keep scope aligned with project goals. + +# Inputs + +Project config, current task, selected engine notes, and relevant handoff material. + +# Outputs + +Coordination notes, next role recommendation, and explicit artifact paths. + +# Output Paths + +Use `documentation/handoffs/` for handoffs and reference project artifacts instead of embedding them. + +# Validation + +Run `npm run validate -- --project `. + +# Engine Notes + +Adapt sequencing to the selected engine overlay. + +# Rules + +Use bounded context. Do not run Codex, telemetry, planner, parallel orchestration, or ownership enforcement. diff --git a/agents/base/mechanics_developer.md b/agents/base/mechanics_developer.md new file mode 100644 index 0000000..912274d --- /dev/null +++ b/agents/base/mechanics_developer.md @@ -0,0 +1,27 @@ +# Role + +Design and implement gameplay mechanics plans within the selected engine contract. + +# Inputs + +Project config, engine setup notes, feature spec, and current task. + +# Outputs + +Mechanics implementation notes, source-path guidance, and validation checks. + +# Output Paths + +Use `source/project-/` and `documentation/technical/`. + +# Validation + +Run `npm run validate -- --project `. + +# Engine Notes + +Use the selected engine overlay for folder and project-file expectations. + +# Rules + +Respect the `source/project-/` contract. diff --git a/agents/base/mid_game_designer.md b/agents/base/mid_game_designer.md new file mode 100644 index 0000000..1c2cd91 --- /dev/null +++ b/agents/base/mid_game_designer.md @@ -0,0 +1,27 @@ +# Role + +Elaborate features, levels, content, and moment-to-moment design details. + +# Inputs + +Project config, senior design direction, feature template, and current task. + +# Outputs + +Feature notes, content lists, and playtest-ready acceptance criteria. + +# Output Paths + +Use `documentation/design/features/`. + +# Validation + +Run `npm run validate -- --project `. + +# Engine Notes + +Keep implementation detail compatible with the selected engine. + +# Rules + +Do not load unrelated templates unless the task requires them. diff --git a/agents/base/producer_agent.md b/agents/base/producer_agent.md new file mode 100644 index 0000000..2d69f53 --- /dev/null +++ b/agents/base/producer_agent.md @@ -0,0 +1,27 @@ +# Role + +Own production planning, milestones, status summaries, and project delivery rhythm. + +# Inputs + +Project config, milestones, timeline, current task, and validation state. + +# Outputs + +Production plan updates, risks, and clear next validation gates. + +# Output Paths + +Use `documentation/production/` and update config only when explicitly requested. + +# Validation + +Run `npm run validate -- --project `. + +# Engine Notes + +Account for engine-specific setup and build risks. + +# Rules + +Keep operational status separate from generated guidance hashes. diff --git a/agents/base/qa_agent.md b/agents/base/qa_agent.md new file mode 100644 index 0000000..a9b0b57 --- /dev/null +++ b/agents/base/qa_agent.md @@ -0,0 +1,27 @@ +# Role + +Review validation readiness, test plans, acceptance criteria, and regressions. + +# Inputs + +Project config, task details, validation command, and selected artifacts. + +# Outputs + +QA plan, failure risks, reproduction steps, and validation checklist. + +# Output Paths + +Use `documentation/qa/`. + +# Validation + +Run `npm run validate -- --project `. + +# Engine Notes + +Check engine-specific project-file and source-root requirements. + +# Rules + +Do not run agents or broad orchestration; report manual next commands. diff --git a/agents/base/sr_game_artist.md b/agents/base/sr_game_artist.md new file mode 100644 index 0000000..e915df8 --- /dev/null +++ b/agents/base/sr_game_artist.md @@ -0,0 +1,27 @@ +# Role + +Own art direction, visual targets, asset priorities, and style consistency. + +# Inputs + +Project config, audience, design goals, engine overlay, and art task. + +# Outputs + +Art direction notes, asset lists, and production-ready briefs. + +# Output Paths + +Use `documentation/art/` and engine asset folders under `source/project-/`. + +# Validation + +Run `npm run validate -- --project `. + +# Engine Notes + +Consider import paths and asset conventions for the selected engine. + +# Rules + +Do not create unrelated assets without explicit task scope. diff --git a/agents/base/sr_game_designer.md b/agents/base/sr_game_designer.md new file mode 100644 index 0000000..ad9f26c --- /dev/null +++ b/agents/base/sr_game_designer.md @@ -0,0 +1,27 @@ +# Role + +Own senior design direction, core loop quality, systems fit, and feature specs. + +# Inputs + +Project config, GDD, feature request, engine notes, and design template. + +# Outputs + +Design decisions, GDD updates, feature specs, and acceptance criteria. + +# Output Paths + +Use `documentation/design/gdd.md` and `documentation/design/features/`. + +# Validation + +Run `npm run validate -- --project `. + +# Engine Notes + +Adapt designs to selected engine affordances. + +# Rules + +Prefer scoped design artifacts over broad rewrites. diff --git a/agents/base/technical_artist.md b/agents/base/technical_artist.md new file mode 100644 index 0000000..1c865fe --- /dev/null +++ b/agents/base/technical_artist.md @@ -0,0 +1,27 @@ +# Role + +Bridge art and engineering for shaders, pipelines, import settings, and performance. + +# Inputs + +Project config, art direction, engine overlay, and technical constraints. + +# Outputs + +Pipeline notes, technical art tasks, and validation checks. + +# Output Paths + +Use `documentation/art/technical/` and engine source asset paths. + +# Validation + +Run `npm run validate -- --project `. + +# Engine Notes + +Use engine-specific material, shader, and import conventions. + +# Rules + +Keep pipeline guidance reproducible. diff --git a/agents/base/ui_ux_agent.md b/agents/base/ui_ux_agent.md new file mode 100644 index 0000000..6e01b46 --- /dev/null +++ b/agents/base/ui_ux_agent.md @@ -0,0 +1,27 @@ +# Role + +Design UI flows, HUDs, menus, accessibility notes, and interaction ergonomics. + +# Inputs + +Project config, audience, platform, engine overlay, and UI task. + +# Outputs + +UI specs, screen flows, HUD requirements, and validation criteria. + +# Output Paths + +Use `documentation/design/ui-ux/`. + +# Validation + +Run `npm run validate -- --project `. + +# Engine Notes + +Map UI recommendations to selected engine UI systems. + +# Rules + +Protect gameplay readability and avoid broad unrelated context. diff --git a/docs/development-rules.md b/docs/development-rules.md new file mode 100644 index 0000000..c5a4ac2 --- /dev/null +++ b/docs/development-rules.md @@ -0,0 +1,18 @@ +# Development Rules + +This repository is TypeScript/Node only. + +Use `.js` specifiers for all relative TypeScript imports because the package uses NodeNext. + +Run validation before claiming parity: + +```bash +npm run typecheck +npm run build +npm test +npm run validate +``` + +Keep generated projects under `projects//`. + +The first build intentionally excludes planner commands, direct Codex execution, telemetry, parallel orchestration, changed-file tracking, and ownership enforcement. diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..42e4fd2 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,28 @@ +# Examples + +Create and validate a project: + +```bash +npm exec open-gamestudio -- init --name "My Game" --engine godot --mode prototype --non-interactive --competitor "Mini Metro" --competitor "Dorfromantik" +npm exec open-gamestudio -- status --project projects/my-game +npm exec open-gamestudio -- validate --project projects/my-game +``` + +Prepare a bounded prompt packet: + +```bash +npm exec open-gamestudio -- run market_analyst --project projects/my-game --task "Create the initial market overview." +``` + +Manual external Codex command; `open-gamestudio` does not spawn Codex in the first build: + +```bash +codex exec --cd projects/my-game "Read .gamestudio/runs//prompt.md and perform the requested task." +``` + +Discover templates: + +```bash +npm exec open-gamestudio -- templates list +npm exec open-gamestudio -- templates show market_analysis +``` diff --git a/docs/known-upstream-differences.md b/docs/known-upstream-differences.md new file mode 100644 index 0000000..9882b2b --- /dev/null +++ b/docs/known-upstream-differences.md @@ -0,0 +1,15 @@ +# Known Upstream Differences + +This TypeScript/Node port preserves upstream user-facing outcomes while intentionally avoiding legacy implementation details that caused fragile behavior. + +Legacy engine-system checks only partially passed while still reporting success. This port uses hard-failing validation, where any failed check exits nonzero. + +Legacy folder-structure checks and project-file path expectations differed from the desired `source/project-/` contract. This port uses `source/project-/` for Godot, Unity, and Unreal. + +Legacy Unreal naming used multiple labels. This port normalizes `Unreal`, `Unreal Engine`, `unreal`, and `ue5` to canonical `unreal`, while keeping `Unreal Engine` as the display name. + +Legacy validation depended on Python and shell assumptions. This port is TypeScript/Node only. + +Intentional omissions for the first build: no interactive `menu`, no `startover`, no generated `project_orchestrator.md`, no exact `template_info.md`, no eager competitor reports during init, and no upstream license/authorship/citation parity documents. + +Future-only features are not implemented in this build: planner/`next`, telemetry, direct Codex execution, parallel orchestration, changed-file tracking, prompt-size metrics, and hard output-ownership enforcement. diff --git a/docs/migration-from-claude.md b/docs/migration-from-claude.md new file mode 100644 index 0000000..3fe746a --- /dev/null +++ b/docs/migration-from-claude.md @@ -0,0 +1,18 @@ +# Migration From Claude-Oriented Game Studio + +Use the canonical TypeScript CLI: + +```bash +npm exec open-gamestudio -- init --name "My Game" --engine godot --mode prototype --non-interactive --competitor "Mini Metro" +npm exec open-gamestudio -- run market_analyst --project projects/my-game --task "Create the initial market overview." +``` + +Manual external Codex command; `open-gamestudio` does not spawn Codex in the first build: + +```bash +codex exec --cd projects/my-game "Read .gamestudio/runs//prompt.md and perform the requested task." +``` + +Intentional differences: no interactive menu, no `startover`, no exact `template_info.md`, no eager competitor reports during init, and no generated `project_orchestrator.md`. + +Future-only features are not implemented: `open-gamestudio next`, `run --exec`, telemetry, parallel orchestration, changed-file tracking, and ownership enforcement. diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..b7ba472 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,26 @@ +# Setup + +Install and verify with Node 20 or newer: + +```bash +npm install +npm run typecheck +npm run build +npm test +npm run validate +``` + +Local development scripts build first and then run `node dist/cli.js`. + +```bash +npm run init -- --name "My Game" --engine godot --mode prototype --non-interactive --competitor "Mini Metro" --engine-version "4.4.1" +npm run templates -- list +npm run validate -- --project projects/my-game +``` + +After build/link/install, the package bin is available: + +```bash +npm exec open-gamestudio -- --help +npm exec open-gamestudio -- templates show gdd +``` diff --git a/docs/system-verification.md b/docs/system-verification.md new file mode 100644 index 0000000..56e3e78 --- /dev/null +++ b/docs/system-verification.md @@ -0,0 +1,27 @@ +# System Verification + +Required verification: + +```bash +npm run typecheck +npm run build +npm test +node dist/cli.js --help +node dist/cli.js validate +npm exec open-gamestudio -- --help +npm exec open-gamestudio -- validate +npm exec open-gamestudio -- run --help +npm run validate +``` + +Engine smoke: + +```bash +npm run init -- --name "Codex Godot Smoke" --engine godot --mode prototype --non-interactive +npm run init -- --name "Codex Unity Smoke" --engine unity --mode design --non-interactive +npm run init -- --name "Codex Unreal Smoke" --engine "Unreal Engine" --mode development --non-interactive +npm run validate -- --project projects/codex-godot-smoke +npm run validate -- --project projects/codex-unity-smoke +npm run validate -- --project projects/codex-unreal-smoke +rm -rf projects/codex-godot-smoke projects/codex-unity-smoke projects/codex-unreal-smoke +``` diff --git a/docs/workflow-validation.md b/docs/workflow-validation.md new file mode 100644 index 0000000..3a471bf --- /dev/null +++ b/docs/workflow-validation.md @@ -0,0 +1,16 @@ +# Workflow Validation + +Validation exits nonzero when any check fails. + +Repo validation checks package scripts, build output, NodeNext import specifiers, package assets, engine configs, base agents, templates, package packing, and installed-bin asset loading. + +Project validation checks schema-valid config, active agents, engine source root, engine project file, materialized agents, project `AGENTS.md` provenance and config hash, market seed, starter GDD, timeline sections, and read-only `status`/`resume` behavior. + +Absence checks: + +```bash +! npm exec open-gamestudio -- --help | grep -E " next|telemetry" +! npm exec open-gamestudio -- run --help | grep -- "--exec" +``` + +No generated `project_orchestrator.md` is required or produced. diff --git a/engine_configs/godot.json b/engine_configs/godot.json new file mode 100644 index 0000000..31588cb --- /dev/null +++ b/engine_configs/godot.json @@ -0,0 +1,14 @@ +{ + "id": "godot", + "display_name": "Godot", + "aliases": ["godot", "Godot", "Godot Engine"], + "default_version": "4.4.1", + "source_root_pattern": "source/project-{slug}", + "folders": ["assets", "scenes", "scripts"], + "project_files": ["project.godot"], + "best_practices": ["Use scenes for composition.", "Keep gameplay scripts focused."], + "agent_specializations": { + "summary": "Godot project using scenes, nodes, resources, and GDScript-friendly architecture.", + "validation": "Confirm project.godot exists under source/project-/." + } +} diff --git a/engine_configs/unity.json b/engine_configs/unity.json new file mode 100644 index 0000000..add44b9 --- /dev/null +++ b/engine_configs/unity.json @@ -0,0 +1,14 @@ +{ + "id": "unity", + "display_name": "Unity", + "aliases": ["unity", "Unity", "Unity Engine"], + "default_version": "6000.0", + "source_root_pattern": "source/project-{slug}", + "folders": ["Assets", "Packages", "ProjectSettings"], + "project_files": ["Packages/manifest.json", "ProjectSettings/ProjectSettings.asset"], + "best_practices": ["Keep gameplay code under Assets.", "Track ProjectSettings markers."], + "agent_specializations": { + "summary": "Unity project using Assets, Packages, and ProjectSettings layout.", + "validation": "Confirm Packages/manifest.json and ProjectSettings/ProjectSettings.asset exist." + } +} diff --git a/engine_configs/unreal.json b/engine_configs/unreal.json new file mode 100644 index 0000000..ab2a32c --- /dev/null +++ b/engine_configs/unreal.json @@ -0,0 +1,14 @@ +{ + "id": "unreal", + "display_name": "Unreal Engine", + "aliases": ["unreal", "Unreal", "Unreal Engine", "ue", "ue5"], + "default_version": "5.4", + "source_root_pattern": "source/project-{slug}", + "folders": ["Content", "Config", "Source"], + "project_files": [".uproject"], + "best_practices": ["Keep gameplay modules explicit.", "Use Content for assets and Config for project defaults."], + "agent_specializations": { + "summary": "Unreal Engine project using Content, Config, Source, and a PascalCase .uproject file.", + "validation": "Confirm .uproject exists under source/project-/." + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9e027c9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2228 @@ +{ + "name": "open-gamestudio", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "open-gamestudio", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "zod": "^3.25.76" + }, + "bin": { + "open-gamestudio": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^20.19.25", + "tsx": "^4.20.6", + "typescript": "^5.9.3", + "vitest": "^1.6.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "dev": true, + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e412a6 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "open-gamestudio", + "version": "0.1.0", + "description": "Provider-neutral CLI workflow layer for agent-assisted game projects.", + "author": "MerlinH", + "license": "MIT", + "type": "module", + "engines": { + "node": ">=20" + }, + "files": [ + "dist/", + "engine_configs/", + "agents/base/", + "templates/" + ], + "bin": { + "open-gamestudio": "./dist/cli.js" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "validate": "npm run build --silent && node dist/cli.js validate", + "init": "npm run build --silent && node dist/cli.js init", + "manage": "npm run build --silent && node dist/cli.js status", + "templates": "npm run build --silent && node dist/cli.js templates" + }, + "dependencies": { + "commander": "^12.1.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/node": "^20.19.25", + "tsx": "^4.20.6", + "typescript": "^5.9.3", + "vitest": "^1.6.1" + } +} diff --git a/research/codex-port-design.md b/research/codex-port-design.md new file mode 100644 index 0000000..dd6da5b --- /dev/null +++ b/research/codex-port-design.md @@ -0,0 +1,680 @@ +# Codex-Native Game Studio Port Design + +## Purpose + +Port `pamirtuna/gamestudio-subagents` into this repository as a Codex-native TypeScript/Node toolkit without carrying over the legacy implementation's brittle execution model or false-green validation behavior. + +The port must preserve upstream user-facing capabilities. Scope control should remove only **new Codex-era optional features**, not features that already exist upstream. If upstream supports a workflow, engine, agent, template, script-style entry point, or project-management action, the TypeScript port should preserve that capability with a cleaner implementation and hard-failing validation. + +This document is split into three scopes: + +1. **Clean parity contract**: upstream capabilities that must be preserved without legacy bugs. +2. **Codex-native improvements**: additions that make the port more useful but should remain tightly bounded. +3. **Future optional layer**: features not present upstream and not needed for the first implementation. + +## Ground Rules + +- Build the port as TypeScript/Node only. +- Use `open-gamestudio` as the canonical CLI. +- Preserve upstream user-facing capabilities unless explicitly documented as an intentional known difference. +- Do not copy legacy source during the research/design phase. +- Do not preserve Python internals, Python package metadata, or shell alias assumptions. +- Do not reproduce upstream false-green validation behavior. +- Do not claim parity until the new implementation passes clean, hard-failing validation. +- Scope reduction is allowed only for features that are new to this port, such as telemetry, direct Codex execution, planner logic, and parallel orchestration. + +## Intentional Known Differences + +The port may intentionally regress upstream implementation or UX details that add maintenance cost without preserving meaningful user value. + +Allowed differences: + +- **No duplicate script-wrapper implementation.** Upstream-style script wrappers are not part of the core parity promise. The port should preserve npm/package discoverability and the canonical `open-gamestudio` CLI, but it does not need separate wrapper files such as `scripts/init_project.mjs` or `scripts/project_manager.mjs` unless they are demonstrably useful. If wrappers are added, they must be thin pass-throughs to the canonical CLI and must not own logic. +- **No interactive menu.** Do not port upstream's interactive `menu` flow. It creates a second UI surface with extra state and terminal complexity. Non-interactive CLI commands are the supported interface. +- **No `project_orchestrator.md` file parity.** Preserve the orchestration and handoff behavior through project `AGENTS.md`, the materialized `master_orchestrator` agent, and handoff templates/docs. Do not generate a separate upstream-style `project_orchestrator.md` file. +- **No exact `template_info.md` parity.** Replace the static upstream template-info document with a machine-readable template registry and/or `open-gamestudio templates` commands. Generated docs may exist later, but exact file parity is not required. +- **No eager competitor report generation during init.** Initialization should record competitor names in config and create a market overview/seed document. Full competitor analysis reports should be created by the market analyst workflow when requested, not as init clutter. +- **No upstream license/authorship/citation doc parity.** This is a port/rewrite with project-owned licensing. Do not copy upstream license, authorship, or citation docs as parity artifacts. Use only this repository's chosen license outside the parity contract. +- **No `startover` command.** Do not implement upstream `startover`; the name is ambiguous and encourages destructive semantics. If revision/reset workflows are needed later, design them under explicit non-destructive names. + +Every intentional difference must be listed here or in migration docs before parity is claimed. + +## Verified Upstream Capability Baseline + +The upstream repository includes these user-facing surfaces: + +- **Engines**: Godot, Unity, and Unreal Engine configs and initializer branches. +- **Agents**: 12 studio role prompts plus project-specific agent customization. +- **Project initialization**: interactive initializer that creates project folders, engine files, market docs, config, milestones, and customized agents. +- **Project management**: status, new, resume, and freeze flows. Upstream also has `menu` and `startover`, but those are intentional known differences and should not be ported. +- **Templates**: GDD, feature spec, handoff, analytics setup, engine setup, market analysis, and project config. Upstream `template_info.md` is replaced by registry/command discoverability. +- **Validation scripts**: project workflow and engine-system checks. +- **Package scripts**: `init`, `manage`, and `test` command aliases. +- **Docs**: setup, examples, contribution, development rules, system verification, and workflow validation. Upstream license/authorship/citation files are not parity artifacts for this rewrite. + +The upstream validation baseline is not clean. In particular, the engine-system test reports only part of the suite passing while still exiting successfully. Known legacy issues include: + +- folder-structure helper argument mismatches; +- project-file path expectations that differ from generated source roots; +- inconsistent Unreal naming such as `Unreal`, `Unreal Engine`, and related aliases; +- shell-command assumptions around `python`. + +The port must fix these mistakes by defining clean contracts and hard-failing tests. It must not avoid the features entirely. + +--- + +# Part 1: Clean Parity Contract + +## Parity Goal + +The first complete port should preserve upstream workflow outcomes while replacing the fragile Python implementation with a clean TypeScript/Node architecture. + +The core loop is: + +```text +idea -> init project -> validate project -> use project-specific agents -> manage project state -> continue work +``` + +The Codex-native runner described later can improve this loop, but the baseline port must not regress upstream project creation, engine support, agent coverage, templates, or project management except for the intentional known differences listed above. + +## Engine Support + +The port must support all upstream engines: + +- Godot +- Unity +- Unreal Engine + +Use canonical IDs internally: + +```text +godot +unity +unreal +``` + +Engine configs should include aliases so user input normalizes cleanly: + +```json +{ + "id": "unreal", + "display_name": "Unreal Engine", + "aliases": ["Unreal", "Unreal Engine", "ue", "ue5"] +} +``` + +Required engine contract: + +- all engine source roots are under `projects//source/project-/`; +- engine display names are stable and user-facing; +- engine aliases normalize before folder or project-file generation; +- validation checks the generated folder and project-file contract for each engine; +- each engine has prompt-overlay data for project-specific agents; +- unsupported or unknown engine names fail clearly. +- Unreal `.uproject` filenames are generated from a shared `projectClassName(nameOrSlug)` helper: split on non-alphanumeric boundaries, PascalCase words, strip punctuation, prefix `Game` when the result would start with a digit, and fail clearly when no alphanumeric characters remain. Examples: `"Test Game" -> "TestGame"` and `"codex-unreal-smoke" -> "CodexUnrealSmoke"`. + +Required generated project files: + +```text +Godot: + source/project-/project.godot + +Unity: + source/project-/Packages/manifest.json + source/project-/ProjectSettings/ProjectSettings.asset or another documented Unity project-settings marker + +Unreal: + source/project-/.uproject +``` + +Unreal naming must be fixed in the new contract: `Unreal`, `Unreal Engine`, `unreal`, and `ue5` should all normalize to canonical `unreal`, while display output remains `Unreal Engine`. + +## Agent Support + +The port must preserve all 12 upstream studio roles: + +- `master_orchestrator` +- `producer_agent` +- `market_analyst` +- `data_scientist` +- `sr_game_designer` +- `mid_game_designer` +- `mechanics_developer` +- `game_feel_developer` +- `sr_game_artist` +- `technical_artist` +- `ui_ux_agent` +- `qa_agent` + +These roles are not optional. The first parity target should include all 12 base prompts, adapted to Codex-style structured inputs/outputs. + +Project initialization must also preserve project-specific agent materialization. In the TypeScript port this should be owned by `src/agents.ts` rather than copied from the Python `agent_customizer.py` implementation. + +Required behavior: + +- validate the 12 required base prompts exist; +- select active agents by project mode; +- inject project summary, engine overlay, and role-specific output guidance; +- materialize project-specific prompts under `projects//.gamestudio/agents/`; +- generate a project-level `AGENTS.md` for Codex-local guidance; +- preserve upstream project orchestration/handoff behavior through compact `AGENTS.md`, the materialized `master_orchestrator` agent, and handoff templates/docs. Do not generate a separate `project_orchestrator.md` file. + +Mode-specific active-agent behavior should preserve upstream intent: + +```text +always: + master_orchestrator + producer_agent + market_analyst + data_scientist + +design: + sr_game_designer + mid_game_designer + sr_game_artist + +prototype: + sr_game_designer + mechanics_developer + qa_agent + +development: + sr_game_designer + mid_game_designer + mechanics_developer + game_feel_developer + qa_agent + sr_game_artist + technical_artist + ui_ux_agent +``` + +## Project Initialization + +`open-gamestudio init` must preserve upstream project-creation outcomes while using a cleaner Node implementation. + +Required initialization outputs: + +```text +projects// + AGENTS.md + project-config.json + source/project-/... + documentation/ + resources/market-research/ + .gamestudio/agents/ +``` + +Project initialization should create: + +- engine-specific folder structure for Godot, Unity, or Unreal; +- engine-specific project files; +- project config; +- market overview/seed document; +- configured competitor names in project config, without generating full competitor analysis reports during init; +- GDD or starter design documentation; +- milestone/timeline data equivalent to upstream behavior; +- project-specific agent prompts; +- project-level Codex guidance. + +The port may simplify excessive folder creation only where it is clearly not user-facing or not validated upstream, but it must not remove engine support, market-analysis seeds, project config, milestones, or project-specific agents. Full competitor reports are intentionally deferred to the market analyst workflow. + +## Project Config + +Use Zod to validate a versioned project config and avoid implicit prompt contracts. + +Required project fields: + +```json +{ + "schema_version": "1.0", + "project": { + "name": "My Game", + "slug": "my-game", + "concept": "One sentence concept", + "genre": "Action", + "platform": "PC", + "audience": "Players who like short sessions", + "competitors": ["competitor-a", "competitor-b"], + "monetization": "premium", + "timeline": "12 weeks", + "engine": "godot", + "engine_version": "4.4.1", + "mode": "prototype", + "phase": "Initialization", + "status": "active" + }, + "team": { + "active_agents": ["master_orchestrator", "producer_agent", "market_analyst", "data_scientist", "sr_game_designer", "mechanics_developer", "qa_agent"] + }, + "production": { + "milestones": [ + { + "id": "m1", + "title": "Playable prototype", + "target": "Week 4", + "exit_criteria": ["Core loop is playable"], + "status": "planned" + } + ] + } +} +``` + +The TypeScript schema may improve shape and naming, but it must preserve upstream information: project identity, audience, competitors, monetization, engine, engine version, mode, phase/status, team/active agents, and schema-validated milestones. Config serialization used for generated guidance hashes must be canonical: recursively sorted keys, two-space indentation, LF newlines, one trailing newline, and an operational-field omission mode for status/run-state fields. + +## Templates and Docs + +The port must preserve upstream template categories: + +- `gdd` -> `gdd_template.md` +- `feature_spec` -> `feature_spec_template.md` +- `handoff` -> `handoff_template.md` +- `analytics_setup` -> `analytics_setup_template.md` +- `engine_setup` -> `engine_setup_template.md` +- `market_analysis` -> `market_analysis_template.md` +- `project_config` -> `project_config_template.json` + +The TypeScript port may rename files only if the migration is documented and validation knows the new paths. Do not preserve exact `template_info.md` file parity; expose template discoverability through a typed registry and/or `open-gamestudio templates` commands. + +Template selection must be deterministic and bounded: `market_analyst` selects `market_analysis`, `data_scientist` selects `analytics_setup`, designer/spec tasks select `gdd`/`feature_spec`, engine/project setup tasks select `engine_setup`/`project_config`, and `handoff` is selected only for handoff/coordination tasks. QA does not load all templates by default. + +The port should also carry forward equivalent docs for: + +- setup/quickstart; +- examples; +- development rules; +- system verification; +- workflow validation; +- contribution notes. + +Docs do not need to be copied verbatim, but the user-facing guidance should not disappear. Upstream license/authorship/citation docs are explicitly out of scope for this rewrite; use this repository's own licensing policy instead. + +## Project Management + +The port must preserve useful upstream project-management capabilities while intentionally dropping `menu` and `startover`. + +Canonical CLI commands should include equivalents for: + +```bash +open-gamestudio status [--project projects/my-game] +open-gamestudio new +open-gamestudio resume --project projects/my-game +open-gamestudio freeze --project projects/my-game +``` + +`open-gamestudio init` may be the canonical implementation behind `new`. + +Interactive `menu` behavior is intentionally not implemented. Users should rely on documented non-interactive commands. + +Status/resume/freeze should operate on project config state and should not become a separate orchestration system. + +Project-management command semantics: + +- `status`: read-only summary of project config, phase/status, active agents, and latest validation state if available. +- `new`: alias or guided wrapper for `init`; it must not create a second project-creation path. +- `resume`: read-only continuation summary with the next suggested manual command; it must not run agents. +- `freeze`: change only project status to frozen/inactive without deleting source, docs, or run history. Status is operational state and is omitted from the project `AGENTS.md` guidance hash, so a status-only freeze must not make generated guidance stale. +- `menu`: intentionally omitted. +- `startover`: intentionally omitted. Future revision/reset workflows require a separate design with explicit non-destructive command names. + +## Script-Style Entry Points + +Upstream exposes script/package commands for init, manage, and test. The TypeScript port should preserve package-level discoverability without creating duplicate wrapper logic. + +Required package metadata excerpt: + +```json +{ + "scripts": { + "build": "tsc -p tsconfig.build.json", + "init": "npm run build --silent && node dist/cli.js init", + "manage": "npm run build --silent && node dist/cli.js status", + "test": "vitest run", + "validate": "npm run build --silent && node dist/cli.js validate", + "templates": "npm run build --silent && node dist/cli.js templates" + }, + "engines": { + "node": ">=20" + }, + "files": [ + "dist/", + "engine_configs/", + "agents/base/", + "templates/" + ] +} +``` + +Separate thin Node wrappers such as `scripts/init_project.mjs`, `scripts/project_manager.mjs`, and `scripts/validate.mjs` are optional known differences. Prefer package scripts that call the built CLI via `node dist/cli.js`, plus explicit smoke tests for the canonical `open-gamestudio` binary through `npm exec open-gamestudio -- ...` after build/link/install. Do not rely on a bare self-bin name inside the package's own npm scripts before install/link. If wrappers exist, they must call the same command handlers as the canonical CLI and must not fork logic. + +The build config must keep the package bin stable: `tsconfig.json` may typecheck both `src/**/*.ts` and `tests/**/*.ts`, but `tsconfig.build.json` must emit `src/cli.ts` to `dist/cli.js` rather than `dist/src/cli.js`. Relative TypeScript imports must use emitted `.js` specifiers under NodeNext, for example `import { loadConfig } from "./config.js"`. + +Runtime package assets (`engine_configs/`, `agents/base/`, and `templates/`) must resolve from the installed package root via `import.meta.url`-based helpers, not from `process.cwd()`. The package-root helper should walk upward from the current module URL until it finds this package's `package.json`, rather than assuming a fixed relative path from `dist`. Project paths resolve from explicit `--project` or documented current-project cwd behavior. + +Package shipping must be tested, not assumed. `npm pack --json` must include the built CLI and runtime asset directories, and a temporary non-repo cwd install smoke must prove the installed package bin can load templates and engine configs. CLI black-box tests that execute `dist/cli.js` or the package bin must build first so they cannot accidentally pass against stale output or source-only execution. + +## Clean Validation Design + +Validation is a first-class product surface, not a copied legacy behavior. + +`open-gamestudio validate` must: + +- return exit code `0` only when all selected checks pass; +- return non-zero when any selected check fails; +- print clear failure messages with paths and check names; +- avoid false-green behavior where failures are printed but the command succeeds; +- validate all upstream parity surfaces before parity is claimed. + +Validation should use a typed internal result shape: + +```ts +type CheckStatus = "pass" | "fail" | "skip"; + +type ValidationCheck = { + id: string; + status: CheckStatus; + message: string; + path?: string; +}; +``` + +CLI exit behavior: + +```text +any fail -> exit 1 +no fail -> exit 0 +skip -> allowed only for explicitly documented non-parity checks +``` + +Required validation checks: + +- package scripts exist; +- TypeScript build output produces `dist/cli.js`, and NodeNext relative imports use `.js` specifiers; +- package assets resolve from the installed package root, including subdirectory invocation; +- package metadata declares a supported Node runtime, includes runtime assets in the publish set, and `npm pack` plus temp install proves installed-bin asset loading from a non-repo cwd; +- all 12 base agents exist; +- required templates exist; +- Godot, Unity, and Unreal engine configs are valid; +- engine aliases are unique and normalize correctly; +- generated project config is schema-valid; +- active-agent mode selection matches the contract; +- engine source root exists under `source/project-/`; +- expected engine project file exists; +- project-specific agents are materialized; +- project `AGENTS.md` exists, includes provenance markers, and its `source-config-sha256` matches the operational-field-omitting guidance hash; +- market overview exists and configured competitor names are preserved in project config; +- starter GDD and milestone/timeline artifacts exist, including schema-valid config milestones and timeline document sections; +- `status` and `resume` report status without mutating the project; +- mutating project-management commands such as `freeze` and `new` are verified only against disposable test fixtures, not by normal `validate --project` on a user project; +- CLI/help surfaces do not expose future-only `next`, `--exec`, telemetry, parallel orchestration, or hard ownership enforcement; +- validation itself fails hard when a check fails. + +## Parity Acceptance Criteria + +The clean parity contract is satisfied when all are true: + +- TypeScript package builds. +- Typecheck passes. +- Tests pass. +- `open-gamestudio init` can create Godot, Unity, and Unreal projects. +- Each generated engine project validates. +- All 12 base agents exist and can be materialized for a project. +- Materialized prompts include the selected engine's prompt-overlay content, not just generic engine text. +- Project-specific `AGENTS.md`, materialized `master_orchestrator`, and handoff guidance are generated without a separate `project_orchestrator.md` file. +- Market and analytics templates are present and reachable by agents. +- Project management supports status, new/init, resume, and freeze. `menu` and `startover` are intentional omissions. +- Package scripts preserve init/manage/test discoverability; separate wrapper files are optional known differences. +- Package metadata declares the Node runtime floor, `npm pack` includes `dist/`, `engine_configs/`, `agents/base/`, and `templates/`, and a temp-installed package bin can load those assets from outside the repo. +- Validation exits non-zero on failures. +- No parity claim appears until the above checks pass. + +--- + +# Part 2: Codex-Native Improvements + +These are improvements over upstream that are useful for a Codex-native port, but they should remain bounded and should not crowd out parity work. + +## Canonical TypeScript CLI + +`open-gamestudio` is the canonical public interface. Npm scripts should call into it. Separate script wrappers are optional and should be avoided unless they provide clear compatibility value. + +Recommended core commands: + +```bash +open-gamestudio init +open-gamestudio status +open-gamestudio resume --project projects/my-game +open-gamestudio freeze --project projects/my-game +open-gamestudio validate +open-gamestudio run --project projects/my-game --task "..." +``` + +## Codex Runner + +Upstream relies on users manually telling an AI CLI which project and agent files to read. The Codex-native port should add a bounded runner that assembles a structured prompt packet for one agent and one task. + +Default `open-gamestudio run --project --task ` behavior: + +- assemble one structured prompt packet; +- write prompt cache and minimal metadata; +- print the exact prompt path and next manual/Codex command; +- not execute Codex or modify project artifacts beyond the prompt cache. + +`open-gamestudio run` should: + +- load one selected agent; +- load the project config summary; +- load the selected engine overlay; +- load only task-relevant templates; +- include explicit output paths; +- include a validation command; +- write a prompt cache; +- print the prompt path and next command for the user. + +Runner flags: + +```bash +--print-prompt +--dry-run +--include-artifact +--allow-broad-context +``` + +Flag semantics: + +- `--print-prompt`: print the deterministic prompt body only. +- `--dry-run`: print selected context files, output paths, validation command, prompt cache path, and metadata path without executing Codex. +- `--include-artifact `: explicitly include one prior artifact under the project root; reject absolute paths and traversal outside the project. +- `--allow-broad-context`: explicitly opt in to broader project context discovery. Without this flag, the runner must not scan or include broad project artifacts. +- `--exec`: future-only. Do not implement direct Codex execution until command quoting, working-directory behavior, timeouts, failure handling, and write-scope rules are designed. + +The runner must not load all agents, all templates, or unrelated project artifacts by default. + +Initial runner acceptance guardrails: + +- metadata records `prompt_chars` for every prepared run; +- dry-run output lists every included context file; +- tests prove a single-agent run does not include unrelated agents; +- tests prove a single-agent run does not include all templates; +- tests prove named prior artifacts are included only when explicitly requested; +- broad project reads require an explicit opt-in flag and are not used by default. + +## Prompt Cache and Minimal Metadata + +Every dry run or printed prompt should write: + +```text +projects//.gamestudio/runs/-/prompt.md +projects//.gamestudio/runs/-/metadata.json +``` + +Minimal metadata is enough: + +```json +{ + "timestamp": "...", + "project": "projects/my-game", + "agent": "market_analyst", + "task": "Create the first market overview", + "prompt_chars": 12345, + "prompt_cache_path": "projects/my-game/.gamestudio/runs/-/prompt.md" +} +``` + +This is not telemetry. Do not add changed-file tracking, runtime metrics, token estimates, productivity comparisons, or JSONL telemetry in the initial implementation. + +## Project-Level AGENTS.md + +Project-level `AGENTS.md` is a Codex-native replacement/addition for upstream's project-specific agent context. + +`src/agents.ts` should own project `AGENTS.md` generation. + +Project `AGENTS.md` must be a compact index and rules file, not a full prompt bundle. It should include project identity, engine/mode, validation commands, pointers to materialized agent prompts, and critical repo-local rules. It must not embed operational status, all agent prompts, all templates, full market docs, or run history. Those belong in config or explicit runner-selected context files. + +Generated files must include provenance markers so validation can prove they came from the generator: + +```md + + +``` + +The hash is computed from the canonical project-config serialization with operational fields such as `project.status` omitted. Therefore `freeze` may update status without regenerating project `AGENTS.md`; non-operational config changes must stale the hash and fail validation until regeneration. + +## Bounded Context Loading + +The main performance win should come from scoped prompt packets, not from adding a large orchestration system. + +Default context for an agent run: + +- one base/materialized agent; +- one project config summary; +- one engine overlay; +- task-relevant templates; +- named prior artifacts only when explicitly requested. + +Broader project reads must be opt-in. + +Bounded-context validation should check the generated prompt packet, not only source code. A regression that accidentally includes all agents, all templates, or broad project artifacts is a performance bug even if functional tests still pass. + +--- + +# Part 3: Future Optional Layer + +These features are not present upstream and are not required for the initial implementation. They should stay out of the first build unless explicitly requested later. First-build validation/docs must include explicit absence checks: no `open-gamestudio next`, no `run --exec`, no telemetry command/files, no parallel orchestration surface, and no hard ownership-enforcement behavior. + +## Planner / `next` + +A real `open-gamestudio next` can be useful, but it is easy to overbuild and easy to make stale recommendations. + +Until project state, validation, run metadata, and handoff summaries are mature, the CLI should print simple static next-step suggestions rather than pretending to have a planner. + +Future planner inputs may include: + +- project phase; +- missing required artifacts; +- last validation status; +- run metadata; +- handoff summaries; +- output ownership. + +## Telemetry + +A future telemetry layer may record: + +- prompt size trends; +- estimated token count; +- elapsed runtime; +- validation results; +- changed files; +- handoff paths; +- suggested next task; +- productivity comparisons. + +This is not needed for the initial implementation. Minimal run metadata is enough. + +## Direct Codex Execution + +Future `--exec` support may spawn `codex exec`, but the initial runner should only prepare prompt packets and print commands. + +Before direct execution is added, the design should define: + +- command quoting rules; +- working-directory behavior; +- failure handling; +- timeout behavior; +- whether Codex can edit files outside declared output paths. + +## Parallel Orchestration + +Parallel Hermes/subagent execution is a future optimization only. + +It requires mature ownership metadata and validation. Until then, workflows should be serial by default. + +Future parallel execution must require disjoint ownership sets such as: + +```yaml +agent: market_analyst +may_write: + - resources/market-research/** + +agent: data_scientist +may_write: + - documentation/technical/analytics/** +``` + +If ownership overlaps, parallel execution must be rejected. + +## Output Ownership Enforcement + +Initial prompts can include suggested output paths, but hard ownership enforcement can wait. + +Future enforcement may validate that generated changes stay inside `may_write` globs and avoid `must_not_write` globs. + +## Performance Optimization Metrics + +Prompt-size budgets, token estimates, artifact summarization, and run-history compression are future optimization work. + +They should be added only after the parity workflow exists and real prompt sizes are measurable. + +--- + +# Final Scope Summary + +## Preserve From Upstream Now + +```text +Godot / Unity / Unreal +12 agents +project-specific agent materialization +project init +market overview/seeds and templates +project config and milestones +status / new / resume / freeze +init / manage / test package discoverability +hard-failing validation +``` + +## Add as Bounded Codex Improvements + +```text +canonical open-gamestudio CLI +project AGENTS.md +bounded run command +prompt cache +minimal metadata +scoped context loading +prompt context guardrails +``` + +## Keep Future-Only + +```text +planner / next +telemetry +changed-file tracking +direct Codex execution +parallel orchestration +ownership enforcement +performance optimization metrics +``` diff --git a/research/codex-port-implementation-plan.md b/research/codex-port-implementation-plan.md new file mode 100644 index 0000000..7b51f04 --- /dev/null +++ b/research/codex-port-implementation-plan.md @@ -0,0 +1,1051 @@ +# Codex-Native Game Studio Port Implementation Plan + +> **For Hermes:** Use `subagent-driven-development` to implement this plan task-by-task. Keep each implementation step small, commit after each phase, and do not claim parity until the final hard-failing validation gate is green. + +**Goal:** Port `pamirtuna/gamestudio-subagents` into `open-gamestudio` as a Codex-native TypeScript/Node game-studio agent toolkit with clean user-facing parity and bounded Codex workflow improvements. + +**Architecture:** Build a TypeScript/Node CLI around typed registries, Zod schemas, file-backed agent prompts, engine configs, project-specific prompt materialization, compact project `AGENTS.md`, hard-failing validation, and a deterministic prompt-cache runner. Preserve upstream outcomes, not legacy Python internals. + +**Tech Stack:** Node.js, TypeScript, `package.json`, `tsconfig.json`, `vitest`, `zod`, `commander` or `cac`, `tsx`, JSON configs, Markdown prompts/templates. + +**Decision:** This is a TypeScript rewrite only. Do not add Python package metadata, Python runtime assumptions, duplicated wrapper logic, telemetry, direct Codex execution, planner logic, ownership enforcement, or parallel orchestration in the first implementation. + +--- + +## Scope Contract From `research/codex-port-design.md` + +### Preserve Now + +- Godot, Unity, and Unreal Engine support. +- All 12 upstream studio agent roles. +- Project-specific agent materialization under `projects//.gamestudio/agents/`. +- Project initialization with engine files, config, market overview/seed, GDD/starter docs, milestones/timeline, and project `AGENTS.md`. +- Project management equivalents for `status`, `new`/`init`, `resume`, and `freeze`. +- Template categories: GDD, feature spec, handoff, analytics setup, engine setup, market analysis, and project config. +- Package-level discoverability for `init`, `manage`, `test`, and `validate`. +- Hard-failing validation: any failed check must produce a non-zero exit. + +### Intentional Differences / Do Not Implement + +- No Python package or Python command aliases. +- No interactive `menu` flow. +- No `startover` command. +- No separate generated `project_orchestrator.md`; preserve orchestration through project `AGENTS.md`, the materialized `master_orchestrator`, and handoff templates/docs. +- No exact `template_info.md` parity; use a typed template registry and/or `open-gamestudio templates` discoverability. +- No eager full competitor reports during init; init records competitor names in config and creates a market overview/seed only. +- No upstream license/authorship/citation parity documents; use this repository's own licensing policy. +- No duplicate script-wrapper implementation. If wrappers are added later, they must be thin pass-throughs to the canonical CLI and must not own logic. + +### Future-Only / Excluded From First Build + +- `open-gamestudio next` planner. +- Telemetry, JSONL runtime metrics, changed-file tracking, productivity comparisons, or token estimates. +- Direct `codex exec` spawning via `--exec`. +- Parallel Hermes/subagent orchestration. +- Hard output-ownership enforcement. +- Prompt-size budgets, artifact summarization, and run-history compression. + +--- + +## Initial Planned File Map + +Create or update these files during the first implementation: + +- `AGENTS.md`: repo-wide Codex guidance and Node validation commands. +- `package.json`: package metadata, bin entry, dependencies, and required scripts. +- `tsconfig.json`: strict TypeScript typecheck configuration for NodeNext across `src/**/*.ts` and `tests/**/*.ts`. +- `tsconfig.build.json`: build-only TypeScript configuration that emits `src/cli.ts` to `dist/cli.js`. +- `src/cli.ts`: canonical `open-gamestudio` CLI with built-bin shebang support and commands for `init`, `new`, `status`, `resume`, `freeze`, `validate`, `run`, and `templates list/show`. +- `src/paths.ts`: runtime path helpers that separate package assets from project paths. +- `src/config.ts`: Zod project-config schemas, JSON I/O, slug helpers, active-agent mode helpers. +- `src/engines.ts`: engine registry, alias normalization, source-root/project-file generation, and project display-name/class-name helpers. +- `src/templates.ts`: typed template registry, required-section metadata, and template discovery helpers for `templates list/show`. +- `src/agents.ts`: role registry, prompt materialization, active-agent selection, project `AGENTS.md` generation. +- `src/projects.ts`: project initialization and project-management commands. +- `src/runner.ts`: deterministic prompt assembly, prompt cache writing, minimal metadata writing, dry-run/print-prompt behavior. +- `src/validation.ts`: hard-failing repo and project validation. +- `agents/base/*.md`: 12 Codex-native base agent prompts. +- `engine_configs/godot.json`, `engine_configs/unity.json`, `engine_configs/unreal.json`: canonical engine configs. +- `templates/gdd_template.md` +- `templates/feature_spec_template.md` +- `templates/handoff_template.md` +- `templates/analytics_setup_template.md` +- `templates/engine_setup_template.md` +- `templates/market_analysis_template.md` +- `templates/project_config_template.json` +- `tests/engine-system.test.ts` +- `tests/project-workflow.test.ts` +- `tests/agents-templates.test.ts` +- `tests/runner-prompts.test.ts` +- `tests/validation.test.ts` +- `docs/known-upstream-differences.md` +- `docs/migration-from-claude.md` +- `docs/setup.md` +- `docs/examples.md` +- `docs/development-rules.md` +- `docs/system-verification.md` +- `docs/workflow-validation.md` +- `CONTRIBUTING.md` or `docs/contributing.md` + +Do **not** create `src/telemetry.ts`, `docs/parallel-orchestration.md`, script wrappers, `template_info.md`, generated `project_orchestrator.md`, or Python compatibility files in the initial implementation. + +--- + +## Phase 0: Baseline and Scope Guardrails + +All commands in this plan assume the repository root is `/opt/data/repos/open-gamestudio`. Each agent handoff must start by changing to that directory before running relative commands so checks do not mutate or validate the wrong repository. + +- [ ] Confirm the destination is a valid git worktree before using commit checkpoints. + +Run: + +```bash +cd /opt/data/repos/open-gamestudio +git rev-parse --is-inside-work-tree +git status --short +git config --global user.name +git config --global user.email +``` + +Expected: + +- `git rev-parse --is-inside-work-tree` prints `true`. +- Git identity is available or intentionally out of scope. +- Existing local changes are understood before implementation starts. + +- [ ] Record legacy baseline and intentional differences in `docs/known-upstream-differences.md`. + +Include: + +- Legacy engine-system checks only partially pass while still reporting success. +- Legacy folder-structure checks and project-file path expectations differ from the desired `source/project-/` contract. +- Legacy Unreal naming uses multiple labels that must normalize to canonical `unreal`. +- Legacy validation depends on Python/shell assumptions; the TypeScript port uses Node tooling only. +- Intentional omissions: `menu`, `startover`, `project_orchestrator.md`, exact `template_info.md`, eager competitor reports, and upstream license/authorship/citation parity. + +Validation: + +```bash +git diff -- docs/known-upstream-differences.md +``` + +Commit checkpoint: + +```bash +git add docs/known-upstream-differences.md +git commit -m "docs: record legacy parity baseline" +``` + +--- + +## Phase 1: TypeScript Package Skeleton and Repo Guidance + +- [ ] Create `package.json`. + +Required minimum content: + +```json +{ + "name": "open-gamestudio", + "version": "0.1.0", + "type": "module", + "engines": { + "node": ">=20" + }, + "files": [ + "dist/", + "engine_configs/", + "agents/base/", + "templates/" + ], + "bin": { + "open-gamestudio": "./dist/cli.js" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "validate": "npm run build --silent && node dist/cli.js validate", + "init": "npm run build --silent && node dist/cli.js init", + "manage": "npm run build --silent && node dist/cli.js status", + "templates": "npm run build --silent && node dist/cli.js templates" + }, + "dependencies": { + "commander": "^12.0.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0", + "vitest": "^1.0.0" + } +} +``` + +`cac` may replace `commander` if used consistently. The public package scripts must preserve `init`, `manage`, `test`, `validate`, and template discoverability while exercising the built CLI path via `node dist/cli.js`, not `tsx src/cli.ts` and not a bare self-bin call. A package's own `bin` name is not guaranteed to be on `PATH` inside its own npm scripts before install/link. Built packages expose `open-gamestudio` through `bin`, and verification must prove both direct built-CLI usage (`node dist/cli.js ...`) and installed package-bin usage from a temporary non-repo cwd. The package manifest must ship runtime assets through `files` or an equivalent publish manifest: `dist/`, `engine_configs/`, `agents/base/`, and `templates/`. + +- [ ] Install dependencies and commit the lockfile. + +Run: + +```bash +npm install +``` + +Expected: + +- `node_modules/` is available for local validation. +- `package-lock.json` is created or updated and included in the package-skeleton commit. + +- [ ] Create `tsconfig.json` and `tsconfig.build.json`. + +Required `tsconfig.json` settings: + +- `target`: `ES2022` +- `module`: `NodeNext` +- `moduleResolution`: `NodeNext` +- `strict`: `true` +- include `src/**/*.ts` and `tests/**/*.ts` +- no `outDir` requirement and no emit during `npm run typecheck` + +Required `tsconfig.build.json` settings: + +- `extends`: `./tsconfig.json` +- `compilerOptions.rootDir`: `src` +- `compilerOptions.outDir`: `dist` +- `compilerOptions.noEmit`: `false` +- include `src/**/*.ts` only +- exclude `tests/**/*.ts` + +This split is required because `package.json` points the package bin and scripts at `dist/cli.js`. With `rootDir: "."` and tests included in the emitting config, TypeScript would emit `src/cli.ts` as `dist/src/cli.js`, breaking `node dist/cli.js` and the package bin. + +- [ ] Create root `AGENTS.md`. + +Required guidance: + +- Use `npm run validate` before any parity claim. +- Because this project uses `"type": "module"`, `module: "NodeNext"`, and `moduleResolution: "NodeNext"`, every relative TypeScript import must use the emitted `.js` specifier: write `import { x } from "./config.js"`, never `import { x } from "./config"`. +- For local development before install/link, use `npm run ...` scripts. Use `npm exec open-gamestudio -- ...` only after build/link/install or inside the package-bin smoke fixture. Bare `open-gamestudio ...` is only guaranteed after package install/link. +- Package scripts build first and then exercise the built CLI through `node dist/cli.js`; use `npm run init -- ...`, `npm run manage -- ...`, `npm run templates -- list`, and `npm run validate -- ...`. +- Keep generated game projects under `projects//`. +- Do not load all agents or all templates for a single role task. +- State that `src/agents.ts` is the single owner for generated project `AGENTS.md`. +- State that direct Codex execution, telemetry, and parallel orchestration are future-only. + +- [ ] Create placeholder TypeScript modules. + +Files: + +- `src/cli.ts` with a portable Node shebang; it registers commander/cac commands and delegates to source modules. It must not shell out to `dist/cli.js`, depend on already-built output, or create a second implementation path. Package scripts are what invoke the built `dist/cli.js`. +- `src/paths.ts` with package/project path helpers +- `src/config.ts` +- `src/engines.ts` +- `src/templates.ts` +- `src/agents.ts` +- `src/projects.ts` +- `src/runner.ts` +- `src/validation.ts` + +Validation: + +```bash +npm run typecheck +npm test +``` + +Expected: + +- Placeholder modules compile. +- Initial or placeholder tests pass. +- `src/cli.ts` is written so compiled `dist/cli.js` has a portable Node shebang and can be executed as the package bin. + +- [ ] Define runtime path resolution in `src/paths.ts` before any file-loading code depends on paths. + +Required exports: + +```ts +export function packageRoot(metaUrl?: string): string; +export function packageAssetPath(relativePath: string): string; +export function resolveProjectRoot(input?: string, cwd?: string): string; +``` + +Contract: + +- Package assets (`engine_configs/`, `agents/base/`, and `templates/`) are resolved from the installed package root, not from `process.cwd()`. +- `packageRoot(import.meta.url)` for built files under `dist/` resolves to the directory containing this package's `package.json`. Implement it by walking upward from `fileURLToPath(import.meta.url)` until it finds `package.json` with `name: "open-gamestudio"`; do not assume a fixed `../` from `dist/`, and fail clearly if the package root cannot be found. +- `packageAssetPath("templates/gdd_template.md")` resolves beside the package assets even when the CLI is invoked from a subdirectory or through npm bin. +- Project paths are resolved from explicit `--project` when provided, otherwise from the current working directory only for commands whose documented default is “current project”. +- Package-asset loading and project-artifact loading must remain separate in code and tests. +- Tests must simulate a subdirectory invocation so installed/package-bin behavior cannot silently depend on repository-root cwd. +- CLI black-box tests that execute `dist/cli.js` or the package bin must run `npm run build` first or build in test setup. Source-module tests may run without a build, but they do not replace built-CLI/package-bin smoke tests. + +Commit checkpoint: + +```bash +git add AGENTS.md package.json package-lock.json tsconfig.json tsconfig.build.json src tests +git commit -m "chore: scaffold codex-native node package" +``` + +--- + +## Phase 2: Engine Registry and Clean Engine Contract + +- [ ] Create engine config files. + +Files: + +- `engine_configs/godot.json` +- `engine_configs/unity.json` +- `engine_configs/unreal.json` + +Each config must include: + +- `id` +- `display_name` +- `aliases` +- `default_version` +- `source_root_pattern` +- `folders` +- `project_files` +- `best_practices` +- `agent_specializations` or equivalent prompt-overlay data + +- [ ] Implement `src/engines.ts`. + +Required exports: + +```ts +export type EngineId = "godot" | "unity" | "unreal"; +export type EngineConfigRegistry = Record; +export function loadEngineConfigs(configDir: string): EngineConfigRegistry; +export function normalizeEngine(value: string, registry: EngineConfigRegistry): EngineId; +export function sourceRoot(projectRoot: string, projectSlug: string): string; +export function projectClassName(displayNameOrSlug: string): string; +export function unrealProjectFileName(displayNameOrSlug: string): string; +export function createEngineFolders(input: EngineCreateInput): string[]; +export function createEngineProjectFiles(input: EngineCreateInput): string[]; +``` + +Contract: + +- All engine source roots are under `source/project-/`. +- `Unreal`, `Unreal Engine`, `unreal`, and `ue5` normalize to `unreal`. +- Unreal display output remains `Unreal Engine`. +- Unreal `.uproject` filenames use `projectClassName(nameOrSlug) + ".uproject"`. +- `projectClassName` splits on non-alphanumeric boundaries, converts words to PascalCase, strips punctuation, prefixes `Game` when the result would start with a digit, and fails clearly when no alphanumeric characters remain. Examples: `projectClassName("Test Game") === "TestGame"`, `projectClassName("codex-unreal-smoke") === "CodexUnrealSmoke"`, and `projectClassName("2d arena") === "Game2dArena"`. +- If two different input names normalize to the same slug/class name in the same parent directory, initialization must fail with a collision message rather than overwrite. +- Unknown engines fail clearly before folder or project-file generation. +- Unity creates `Packages/manifest.json` plus `ProjectSettings/ProjectSettings.asset` or another documented project-settings marker. + +- [ ] Write `tests/engine-system.test.ts`. + +Minimum tests: + +- All engine configs parse through Zod. +- Alias normalization resolves Godot, Unity, and Unreal variants. +- Unknown engine input fails with a clear error. +- Folder creation accepts `projectSlug` and does not require omitted positional args. +- Godot creates `source/project-test-game/project.godot`. +- Unity creates `source/project-test-game/Packages/manifest.json` and the documented settings marker. +- Unreal creates `source/project-test-game/TestGame.uproject`. +- Unreal class-name generation is tested for `"Test Game" -> "TestGame"`, `"codex-unreal-smoke" -> "CodexUnrealSmoke"`, punctuation, digits, empty/invalid names, and same-directory collisions. +- No test expects direct project files under bare `source/`. + +Validation: + +```bash +npm test -- tests/engine-system.test.ts +npm run typecheck +``` + +Expected: + +- All engine tests pass. +- Engine contracts are independent of legacy naming/path bugs. + +Commit checkpoint: + +```bash +git add engine_configs src/engines.ts tests/engine-system.test.ts +git commit -m "feat: add canonical engine registry" +``` + +--- + +## Phase 3: Schemas, Templates, Agents, and Project `AGENTS.md` + +Execution note: do not dispatch this whole phase as one coding-agent task. Split it into red/green slices in this order: config schema tests and implementation; template registry tests and implementation; base-prompt content tests and prompts; active-agent selection tests and implementation; project `AGENTS.md` provenance tests and implementation. + +- [ ] Implement `src/config.ts`. + +Required behavior: + +- `slugify("My Game") === "my-game"`. +- Use Zod to validate versioned `project-config.json`. +- Preserve project fields: name, slug, concept, genre, platform, audience, competitors, monetization, timeline, engine, engine version, mode, phase, and status. +- Preserve milestone parity with a schema-validated `production.milestones` array plus a generated timeline document; do not rely on a vague timeline string alone. +- Preserve team/active agent information. +- Provide `activeAgentsForMode(mode)` with the design contract: always-on agents plus mode-specific agents. +- Provide `canonicalProjectConfigJson(config, options?)`: recursively sort object keys, preserve array order, format with two-space indentation, normalize to LF, and add exactly one trailing newline. +- Provide `guidanceConfigHash(config)`: SHA-256 over `canonicalProjectConfigJson(config, { omitOperationalFields: true })`, where operational fields include `project.status` and any future validation timestamps/run-state fields. This keeps `freeze` from making generated `AGENTS.md` stale when it changes only status. + +Minimum config shape: + +```json +{ + "schema_version": "1.0", + "project": { + "name": "Test Game", + "slug": "test-game", + "concept": "A focused test concept", + "genre": "Puzzle", + "platform": "PC", + "audience": "Players who like compact strategy games", + "competitors": ["mini-metro", "dorfromantik"], + "monetization": "premium", + "timeline": "8 weeks", + "engine": "godot", + "engine_version": "4.4.1", + "mode": "prototype", + "phase": "Initialization", + "status": "active" + }, + "team": { + "active_agents": ["master_orchestrator", "producer_agent", "market_analyst", "data_scientist", "sr_game_designer", "mechanics_developer", "qa_agent"] + }, + "production": { + "milestones": [ + { + "id": "m1", + "title": "Playable prototype", + "target": "Week 4", + "exit_criteria": ["Core loop is playable"], + "status": "planned" + } + ] + } +} +``` + +Canonical serialization tests must prove semantically identical objects with different key insertion orders produce the same hash, while meaningful non-operational changes change the hash. `freeze`/status-only changes must not change `guidanceConfigHash`. + +- [ ] Create templates and implement `src/templates.ts`. + +Required template files: + +- `templates/gdd_template.md` +- `templates/feature_spec_template.md` +- `templates/handoff_template.md` +- `templates/analytics_setup_template.md` +- `templates/engine_setup_template.md` +- `templates/market_analysis_template.md` +- `templates/project_config_template.json` + +Required lightweight template contract: + +- Markdown templates include testable sections such as `# Purpose`, `# Inputs`, `# Outputs`, and `# Validation` where applicable. +- `project_config_template.json` parses as JSON and conforms to the Zod project-config schema or documented template placeholder schema. +- Template registry metadata records required sections so tests can validate content quality. + +Required behavior: + +- Expose a typed registry of template IDs, categories, file paths, intended agent roles, and selection tags. +- Use exact canonical template IDs: `gdd`, `feature_spec`, `handoff`, `analytics_setup`, `engine_setup`, `market_analysis`, and `project_config`. +- Define deterministic agent/task-to-template selection rules: + - every prompt-run may include `handoff` only when the task asks for handoff/coordination output; + - `market_analyst` selects `market_analysis`; + - `data_scientist` selects `analytics_setup`; + - designer roles select `gdd` and `feature_spec` when the task mentions design/spec work; + - engine/project setup tasks select `engine_setup` and `project_config`; + - `qa_agent` selects no content template by default beyond validation guidance unless the task explicitly asks for a spec review. +- Support CLI discoverability through explicit built-CLI commands: `open-gamestudio templates list` and `open-gamestudio templates show `. +- Do not create or require exact `template_info.md` parity. +- Validation knows the canonical template paths, categories, and required lightweight sections/frontmatter for each template. + +- [ ] Create 12 Codex-native base prompts in `agents/base/`. + +Files: + +- `agents/base/master_orchestrator.md` +- `agents/base/producer_agent.md` +- `agents/base/market_analyst.md` +- `agents/base/data_scientist.md` +- `agents/base/sr_game_designer.md` +- `agents/base/mid_game_designer.md` +- `agents/base/mechanics_developer.md` +- `agents/base/game_feel_developer.md` +- `agents/base/sr_game_artist.md` +- `agents/base/technical_artist.md` +- `agents/base/ui_ux_agent.md` +- `agents/base/qa_agent.md` + +Each prompt must include lightweight, testable sections so validation proves prompt usefulness rather than file existence only: + +- `# Role` with role responsibilities; +- `# Inputs` with expected inputs; +- `# Outputs` with expected outputs; +- `# Output Paths` or explicit output conventions; +- `# Validation` with the validation command; +- `# Engine Notes` with engine-specific adaptation placeholder; +- `# Rules` with rule-compliance reminder. + +- [ ] Implement `src/agents.ts`. + +Required behavior: + +- Validate exactly the 12 required agent names. +- Select active agents by mode: + - always: `master_orchestrator`, `producer_agent`, `market_analyst`, `data_scientist` + - design: add `sr_game_designer`, `mid_game_designer`, `sr_game_artist` + - prototype: add `sr_game_designer`, `mechanics_developer`, `qa_agent` + - development: add `sr_game_designer`, `mid_game_designer`, `mechanics_developer`, `game_feel_developer`, `qa_agent`, `sr_game_artist`, `technical_artist`, `ui_ux_agent` +- Materialize project prompts by reading the base prompt and injecting project summary, engine overlay, mode, and output guidance. +- Write materialized prompts under `projects//.gamestudio/agents/`. +- Generate compact project `AGENTS.md` as the only project-level Codex guidance file. +- Add provenance markers to generated project `AGENTS.md` and compute the hash with `guidanceConfigHash(config)`, not raw file bytes: + +```md + + +``` + +- Do not generate `project_orchestrator.md`. +- Do not embed all prompts, all templates, full market docs, run history, or operational status in project `AGENTS.md`. Regenerate or fail validation when `source-config-sha256` does not match the current guidance hash. A status-only `freeze` update must not make the hash stale. + +- [ ] Write `tests/agents-templates.test.ts`. + +Minimum tests: + +- All 12 base prompts exist. +- Active-agent selection matches design/prototype/development contracts. +- Template registry includes all required categories. +- `open-gamestudio templates list` lists all template IDs and categories. +- `open-gamestudio templates show gdd` prints the correct template metadata/path. +- Template selection tests cover the canonical IDs and the deterministic agent/task selection rules above. +- Required template files contain their required lightweight sections/frontmatter. +- No `template_info.md` is required. +- Materialized prompts include project summary, role guidance, and engine-specific overlay text from the selected engine config; missing overlay content fails tests. +- Project `AGENTS.md` includes provenance markers and pointers to materialized agents. +- Project `AGENTS.md` does not embed all agent prompts or all templates. +- Recomputed `source-config-sha256` matches `guidanceConfigHash(config)`, stale hashes fail validation/tests, and status-only `freeze` changes do not stale the hash. +- No generated `project_orchestrator.md` exists. + +Validation: + +```bash +npm run build +npm test -- tests/agents-templates.test.ts +npm run typecheck +``` + +Expected: + +- Agent and template registry tests pass. +- Project guidance generation is compact and provenance-verifiable. + +Commit checkpoint: + +```bash +git add src/config.ts src/templates.ts src/agents.ts agents templates tests/agents-templates.test.ts +git commit -m "feat: add schemas templates and studio agents" +``` + +--- + +## Phase 4: Project Initialization and Management + +Execution note: do not dispatch this whole phase as one coding-agent task. Split it into red/green slices: init config/folders; engine file creation through `src/engines.ts`; starter docs/market seed; agent materialization and project `AGENTS.md`; status/resume read-only commands; `new` delegation; `freeze` state update. + +- [ ] Implement `src/projects.ts`. + +Required initialization behavior for `open-gamestudio init` / `open-gamestudio new`: + +Non-interactive CLI contract: + +- Required flags: `--name`, `--engine`, `--mode`, and `--non-interactive`. +- Optional config flags: `--concept`, `--genre`, `--platform`, `--audience`, repeated `--competitor `, `--monetization`, `--timeline`, and `--engine-version`. +- Deterministic defaults for omitted optional fields in `--non-interactive` mode: `concept: " concept"`, `genre: "Unspecified"`, `platform: "PC"`, `audience: "General players"`, `competitors: []`, `monetization: "undecided"`, `timeline: "TBD"`, and engine default version from the engine registry. +- Deterministic milestone defaults in `--non-interactive` mode: create at least one schema-valid planned milestone derived from `--timeline` or `"TBD"`, so milestone parity does not depend on free-text timeline only. +- Interactive prompts may collect richer values later, but tests must assert the non-interactive defaults exactly so generated config is reproducible. + +- Create `projects//`. +- Create `project-config.json` with schema-valid fields and `status: "active"`. +- Create engine folders/files through `src/engines.ts`. +- Create `documentation/design/gdd.md` or equivalent starter GDD. +- Create `documentation/production/timeline.md` or equivalent milestones/timeline artifact with validated sections: `# Timeline`, `# Milestones`, `# Risks`, and `# Next Validation Gate`. +- Create `resources/market-research/market-overview.md` as a seed/overview. +- Record competitor names in project config. +- Do not generate full per-competitor reports during init. +- Create project `.gitignore` if useful. +- Materialize project-specific agent prompts through `src/agents.ts`. +- Generate project `AGENTS.md` through `src/agents.ts`. + +The common folder set may be simplified if not user-facing, but must preserve source, documentation, market research, QA/build areas when they are part of upstream workflow outcomes or validation. + +Required management behavior: + +- `status`: read-only summary of project config, phase/status, active agents, and latest validation state if available. +- `new`: alias or guided wrapper for `init`; it must not create a second project-creation path. +- `resume`: read-only continuation summary with the next suggested manual command; it must not run agents. +- `freeze`: change only `project.status` to frozen/inactive without deleting source, docs, run history, or prompts. Because `project.status` is omitted from `guidanceConfigHash`, this status-only change must not require regenerating project `AGENTS.md` and must not make validation fail for a stale hash. +- `menu`: omitted. +- `startover`: omitted. + +Required CLI examples: + +```bash +npm run init -- --name "Test Game" --engine godot --mode prototype --non-interactive +npm run manage -- --project projects/test-game +npm exec open-gamestudio -- status --project projects/test-game +npm exec open-gamestudio -- new --name "Test Game 2" --engine unity --mode design --non-interactive +npm exec open-gamestudio -- resume --project projects/test-game +npm exec open-gamestudio -- freeze --project projects/test-game +``` + +- [ ] Write `tests/project-workflow.test.ts`. + +Minimum tests: + +- `init` creates expected config/docs/source root/market seed/project agents/project `AGENTS.md`. +- `init --non-interactive` populates every required config field using provided flags or the deterministic defaults above. +- Starter GDD and milestone/timeline artifacts exist at the documented paths or explicitly documented equivalents, and timeline docs contain the required sections. +- Project config includes schema-valid `production.milestones`; missing/empty milestone data fails tests. +- Godot, Unity, and Unreal initialization all create the expected engine project files. +- Competitor names are preserved in config. +- Init does not create eager per-competitor reports. +- `new` delegates to the same creation path as `init`. +- `status` prints phase/status/mode/engine/active agents. +- `resume` prints a continuation summary and suggested manual next command without running agents. +- `freeze` changes only status without deleting source/docs/prompts and without staling the project `AGENTS.md` guidance hash. +- CLI help or tests prove `menu` and `startover` are not implemented. +- Project-management behavior is covered by workflow tests and read-only validation checks: `status` and `resume` are read-only under `validate --project`; `new` delegates to `init` and `freeze` changes only status in disposable fixture tests. + +Validation: + +```bash +npm run build +npm test -- tests/project-workflow.test.ts +npm run typecheck +``` + +Expected: + +- Project workflow tests pass locally with Node tooling only. + +Commit checkpoint: + +```bash +git add src/projects.ts src/cli.ts tests/project-workflow.test.ts package.json +git commit -m "feat: add project initialization and management" +``` + +--- + +## Phase 5: Hard-Failing Validation + +- [ ] Implement `src/validation.ts`. + +Required result shape: + +```ts +type CheckStatus = "pass" | "fail" | "skip"; + +type ValidationCheck = { + id: string; + status: CheckStatus; + message: string; + path?: string; +}; +``` + +Exit behavior: + +```text +any fail -> exit 1 +no fail -> exit 0 +skip -> allowed only for explicitly documented non-parity checks +``` + +Required repo checks: + +- `package.json` has required `init`, `manage`, `test`, `validate`, and `templates` scripts, `build` uses `tsconfig.build.json`, the package bin points to `./dist/cli.js`, and user-facing scripts exercise the built CLI path. +- `package.json` declares a Node runtime floor such as `"engines": { "node": ">=20" }` and a package shipping contract that includes `dist/`, `engine_configs/`, `agents/base/`, and `templates/`. +- `tsconfig.build.json` emits `src/cli.ts` to `dist/cli.js`; tests must fail if the build would only produce `dist/src/cli.js`. +- Required source modules exist, including `src/paths.ts`. +- Relative TypeScript imports use NodeNext-compatible emitted `.js` specifiers. +- Runtime package-asset resolution works from a subdirectory invocation and does not depend on `process.cwd()` for `engine_configs/`, `agents/base/`, or `templates/`. +- `npm pack --json` output includes the built CLI and runtime asset directories, and an installed-package smoke test proves the package bin can load templates/engine configs from a temporary non-repo cwd. +- All 12 base agents exist. +- Required templates exist and registry maps to real files. +- Godot, Unity, and Unreal engine configs are valid. +- Engine aliases are unique and normalize correctly. +- No required parity artifact depends on Python files. +- Future-only features are documented as out of scope and are not implemented by this plan. During Phase 5, absence proof is limited to CLI/help tests plus already-existing scope docs such as this research plan and `docs/known-upstream-differences.md`; final user docs are added in Phase 7. Do not add a broad validator that hard-fails on every future-scope filename; instead, tests and docs should prove the shipped CLI/help does not expose `next`, `--exec`, telemetry commands/files, parallel orchestration surfaces, or ownership-enforcement behavior in the first build. + +Required read-only project checks when `--project` is provided: + +`open-gamestudio validate --project ` must not mutate the project. It may inspect `status` and `resume` behavior for read-only reporting, but mutating `freeze` and `new` checks belong in disposable test fixtures, not normal project validation. + +- Project config is schema-valid. +- Project config includes audience, competitors, monetization, timeline, mode, phase, status, active agents, and schema-valid `production.milestones`. +- Active-agent selection matches the configured mode. +- Engine source root exists under `source/project-/`. +- Expected engine project file exists. +- Project-specific agents are materialized. +- Project `AGENTS.md` exists, includes provenance markers, and has a recomputed `source-config-sha256` matching `guidanceConfigHash(config)`. +- Market overview/seed exists and competitor names are preserved in project config. +- Starter GDD and milestone/timeline artifacts exist at the documented paths or explicitly documented equivalents, and timeline docs include `# Timeline`, `# Milestones`, `# Risks`, and `# Next Validation Gate`. +- No eager competitor reports are required by validation. +- No generated `project_orchestrator.md` is required or produced. +- `status` reports project config/phase/status without modifying files. +- `resume` reports a continuation summary and suggested manual command without running agents or modifying files. +- Normal project validation does not call mutating commands such as `freeze` or `new` against the provided project. + +- [ ] Write `tests/validation.test.ts`. + +Minimum tests: + +- Fresh initialized Godot, Unity, and Unreal projects validate. +- Missing project `AGENTS.md` fails with a clear message and non-zero exit. +- Missing engine file fails with a clear message and non-zero exit. +- Invalid project config fails Zod validation and exits non-zero. +- Missing required template fails repo validation. +- Missing required package script fails repo validation. +- Missing package asset from `files`/publish manifest or `npm pack --json` output fails repo validation. +- Incorrect TypeScript build output path or missing `dist/cli.js` fails repo validation. +- Relative TypeScript imports without `.js` specifiers fail typecheck or lint-style validation. +- Package assets still load when the CLI runs from a subdirectory. +- A package installed from the local `npm pack` tarball can run `open-gamestudio --help` and `open-gamestudio templates list` from a temporary non-repo cwd. +- Stale project `AGENTS.md` config hash fails with a clear regeneration message. +- Status-only `freeze` changes do not change `guidanceConfigHash` and do not make project `AGENTS.md` stale. +- Missing starter GDD or milestone/timeline artifacts fail with clear messages. +- Missing/empty milestone schema data fails with a clear message. +- CLI/help tests prove no `next`, no `--exec`, no telemetry command/files, no parallel orchestration surface, and no ownership-enforcement behavior are exposed. +- Broken `status` or `resume` read-only behavior fails project validation. +- Broken `freeze` or `new` behavior fails only in disposable fixture tests, not by mutating a user-supplied `--project` during validation. +- Validation itself does not print failures and exit `0`. + +Validation: + +```bash +npm run build +npm test -- tests/validation.test.ts +npm run validate +npm run typecheck +``` + +Expected: + +- Validator tests pass. +- Repo validation exits `0` only when all current repo checks pass. + +Commit checkpoint: + +```bash +git add src/validation.ts src/cli.ts tests/validation.test.ts +git commit -m "feat: add hard-failing validation gates" +``` + +--- + +## Phase 6: Bounded Runner, Prompt Cache, and Minimal Metadata + +- [ ] Implement `src/runner.ts`. + +Default `open-gamestudio run --project --task ` behavior: + +- Assemble one structured prompt packet. +- Load one selected materialized/base agent. +- Load a project config summary. +- Load the selected engine overlay from package assets through `packageAssetPath`, not from cwd. +- Load only task-relevant templates selected by the canonical rules in `src/templates.ts`. +- Include explicit output paths. +- Include the validation command. +- Write prompt cache and minimal metadata. +- Print the prompt path and next manual/Codex command. +- Do not execute Codex. +- Do not modify project artifacts beyond `.gamestudio/runs/-/prompt.md` and `.gamestudio/runs/-/metadata.json`. + +Supported flags: + +```bash +--print-prompt +--dry-run +--include-artifact +--allow-broad-context +``` + +Flag semantics: + +- `--print-prompt`: require non-empty `--task`, then print the deterministic prompt body only after writing cache/metadata. +- `--dry-run`: require non-empty `--task`, then print selected context files, output paths, validation command, prompt cache path, and metadata path. +- `--include-artifact `: explicitly include one prior project artifact under the project root. Reject absolute paths and traversal outside the project. +- `--allow-broad-context`: explicitly opt in to broader project context discovery. Without this flag, the runner must not scan or include broad project artifacts. +- `--exec`: do not implement in this plan. Document direct Codex execution as future-only; do not add runtime ownership/telemetry/execution machinery. + +Prompt cache paths: + +```text +projects//.gamestudio/runs/-/prompt.md +projects//.gamestudio/runs/-/metadata.json +``` + +Minimal metadata shape: + +```json +{ + "timestamp": "...", + "project": "projects/my-game", + "agent": "market_analyst", + "task": "Create the first market overview", + "prompt_chars": 12345, + "prompt_cache_path": "projects/my-game/.gamestudio/runs/-/prompt.md" +} +``` + +Do not add JSONL telemetry, elapsed runtime, exit codes, validation-result history, changed-file tracking, token estimates, or productivity metrics in this phase. + +Prompt determinism contract: + +- The rendered prompt body must not include timestamps or random IDs. +- Run IDs and timestamps may appear in metadata and cache paths, not in the deterministic body comparison. +- Tests should compare normalized prompt bodies and separately assert metadata fields such as `timestamp`, `prompt_chars`, and `prompt_cache_path` exist. + +Required CLI examples: + +```bash +npm exec open-gamestudio -- run market_analyst --project projects/my-game --task "Create first market overview" +npm exec open-gamestudio -- run market_analyst --project projects/my-game --task "Create first market overview" --print-prompt +npm exec open-gamestudio -- run qa_agent --project projects/my-game --task "Review validation readiness" --dry-run +npm exec open-gamestudio -- run producer_agent --project projects/my-game --task "Summarize current GDD" --include-artifact documentation/design/gdd.md --dry-run +``` + +- [ ] Write `tests/runner-prompts.test.ts`. + +Minimum tests: + +- Missing or empty `--task` fails for default, dry-run, and print-prompt modes with a clear error. +- Same inputs produce the same prompt body, aside from run-id paths handled separately. +- Prompt cache file is written for default, dry-run, and print-prompt modes. +- Metadata records `prompt_chars` and `prompt_cache_path`. +- Dry-run output lists every included context file. +- Market analyst prompt references market template and market output paths. +- Data scientist prompt references analytics template and analytics output paths. +- QA prompt references validation command. +- Rendered prompts include engine-specific overlay content for Godot, Unity, and Unreal; a prompt that only includes generic engine text fails. +- A single-agent run does not include unrelated agents. +- A single-agent run does not include all templates. +- Named prior artifacts are included only when explicitly requested. +- Broad project reads require an explicit opt-in flag and are not used by default. +- Direct Codex execution is not exposed by the first-build CLI; docs mark it as a future toolkit feature and show only manual external Codex usage. + +Validation: + +```bash +npm run build +npm test -- tests/runner-prompts.test.ts +npm run typecheck +``` + +Expected: + +- Runner tests pass. +- Prompt cache and metadata are reproducible and bounded. + +Commit checkpoint: + +```bash +git add src/runner.ts src/cli.ts tests/runner-prompts.test.ts +git commit -m "feat: add bounded codex prompt runner" +``` + +--- + +## Phase 7: Docs, Migration Guide, and End-to-End Smoke Tests + +- [ ] Add user-facing docs. + +Files: + +- `docs/setup.md` +- `docs/examples.md` +- `docs/development-rules.md` +- `docs/system-verification.md` +- `docs/workflow-validation.md` +- `docs/migration-from-claude.md` +- `CONTRIBUTING.md` or `docs/contributing.md` + +Required docs behavior: + +- Show Node/TypeScript install/build/test commands. +- Show `open-gamestudio` CLI usage, including `templates list` and `templates show `. +- Show equivalent Codex prompt-runner workflows without claiming toolkit direct execution. Manual `codex exec` examples must be labeled as external user-run commands that consume generated prompt cache output, not commands spawned by `open-gamestudio`. +- Document intentional omissions and future-only features. +- Include explicit absence checks/examples: no `open-gamestudio next`, no `run --exec`, no telemetry command or telemetry files, no parallel orchestration command/docs, and no ownership-enforcement behavior in the first build. +- Document validation gates and parity checklist. +- Do not copy upstream license/authorship/citation docs as parity artifacts. + +Required migration examples: + +```bash +npm exec open-gamestudio -- init --name "My Game" --engine godot --mode prototype --non-interactive +npm exec open-gamestudio -- run market_analyst --project projects/my-game --task "Create the initial market overview." +# Manual external Codex command; open-gamestudio does not spawn Codex in the first build. +codex exec --cd projects/my-game "Read .gamestudio/runs/-/prompt.md and perform the requested task." +npm run validate -- --project projects/my-game +``` + +- [ ] Run disposable sample-project smoke tests for all engines. + +Run: + +```bash +npm run typecheck +npm run build +npm test +node dist/cli.js --help +node dist/cli.js validate +npm exec open-gamestudio -- --help +npm exec open-gamestudio -- validate +npm exec open-gamestudio -- run --help +npm run validate +rm -rf projects/codex-godot-smoke projects/codex-unity-smoke projects/codex-unreal-smoke +npm run init -- --name "Codex Godot Smoke" --engine godot --mode prototype --non-interactive +npm run init -- --name "Codex Unity Smoke" --engine unity --mode design --non-interactive +npm run init -- --name "Codex Unreal Smoke" --engine "Unreal Engine" --mode development --non-interactive +npm run validate -- --project projects/codex-godot-smoke +npm run validate -- --project projects/codex-unity-smoke +npm run validate -- --project projects/codex-unreal-smoke +npm exec open-gamestudio -- run market_analyst --project projects/codex-godot-smoke --task "Create first market overview" --print-prompt +npm exec open-gamestudio -- run qa_agent --project projects/codex-unreal-smoke --task "Review validation readiness" --dry-run +test -n "$(find projects/codex-godot-smoke/.gamestudio/runs -name prompt.md -print -quit)" +test -n "$(find projects/codex-unreal-smoke/.gamestudio/runs -name metadata.json -print -quit)" +PACK_TGZ="$(npm pack --json | node -e 'let s=""; process.stdin.on("data", d => s += d); process.stdin.on("end", () => { const p = JSON.parse(s)[0]; const paths = p.files.map(f => f.path); for (const need of ["dist/cli.js", "engine_configs/godot.json", "engine_configs/unity.json", "engine_configs/unreal.json", "templates/gdd_template.md", "agents/base/master_orchestrator.md"]) { if (!paths.includes(need)) { console.error(`missing ${need}`); process.exit(1); } } console.log(p.filename); });')" +rm -rf /tmp/open-gamestudio-pack-smoke +mkdir -p /tmp/open-gamestudio-pack-smoke +npm install --prefix /tmp/open-gamestudio-pack-smoke "$PWD/$PACK_TGZ" +cd /tmp/open-gamestudio-pack-smoke +npm exec open-gamestudio -- --help +npm exec open-gamestudio -- templates list +cd - +rm -rf /tmp/open-gamestudio-pack-smoke +rm -f "$PACK_TGZ" +``` + +Expected: + +- Vitest passes. +- TypeScript typecheck passes. +- Build passes. +- Both `node dist/cli.js` and `npm exec open-gamestudio -- ...` prove the built CLI and local package bin are usable. +- `npm pack --json` includes `dist/`, `engine_configs/`, `agents/base/`, and `templates/`, and the installed tarball smoke can load package assets from `/tmp/open-gamestudio-pack-smoke`. +- CLI help excludes `next`, `--exec`, telemetry, parallel orchestration, and ownership-enforcement surfaces. +- Repo validation passes. +- Godot source root contains `project.godot`. +- Unity source root contains `Packages/manifest.json` and the documented settings marker. +- Unreal source root contains `CodexUnrealSmoke.uproject`. +- Each sample project validates. +- Market analyst prompt includes only market analyst, project config summary, engine summary, market template, and market output contract. +- QA dry-run lists included context files and validation command. +- Prompt cache and metadata exist under `.gamestudio/runs/` for runner invocations. + +- [ ] Remove disposable sample projects before commit unless intentionally adding examples. + +Run: + +```bash +rm -rf projects/codex-godot-smoke projects/codex-unity-smoke projects/codex-unreal-smoke +git status --short +``` + +Expected: + +- No sample project remains unless explicitly committed as an example. +- Final git status includes only intentional source/docs changes. + +Commit checkpoint: + +```bash +git add docs CONTRIBUTING.md package.json src tests agents engine_configs templates AGENTS.md tsconfig.json tsconfig.build.json +git commit -m "docs: add codex gamestudio usage and verification" +``` + +--- + +## Final Verification Gate + +Run before any parity claim: + +```bash +npm run typecheck +npm run build +npm test +node dist/cli.js --help +node dist/cli.js validate +npm exec open-gamestudio -- --help +npm exec open-gamestudio -- validate +npm exec open-gamestudio -- run --help +npm run validate +rm -rf projects/final-verification-godot projects/final-verification-unity projects/final-verification-unreal +npm run init -- --name "Final Verification Godot" --engine godot --mode prototype --non-interactive +npm run init -- --name "Final Verification Unity" --engine unity --mode design --non-interactive +npm run init -- --name "Final Verification Unreal" --engine "Unreal Engine" --mode development --non-interactive +npm run validate -- --project projects/final-verification-godot +npm run validate -- --project projects/final-verification-unity +npm run validate -- --project projects/final-verification-unreal +npm exec open-gamestudio -- run qa_agent --project projects/final-verification-unreal --task "Review final verification readiness" --print-prompt +test -n "$(find projects/final-verification-unreal/.gamestudio/runs -name prompt.md -print -quit)" +PACK_TGZ="$(npm pack --json | node -e 'let s=""; process.stdin.on("data", d => s += d); process.stdin.on("end", () => { const p = JSON.parse(s)[0]; const paths = p.files.map(f => f.path); for (const need of ["dist/cli.js", "engine_configs/godot.json", "engine_configs/unity.json", "engine_configs/unreal.json", "templates/gdd_template.md", "agents/base/master_orchestrator.md"]) { if (!paths.includes(need)) { console.error(`missing ${need}`); process.exit(1); } } console.log(p.filename); });')" +rm -rf /tmp/open-gamestudio-pack-smoke +mkdir -p /tmp/open-gamestudio-pack-smoke +npm install --prefix /tmp/open-gamestudio-pack-smoke "$PWD/$PACK_TGZ" +(cd /tmp/open-gamestudio-pack-smoke && npm exec open-gamestudio -- --help && npm exec open-gamestudio -- templates list) +rm -rf /tmp/open-gamestudio-pack-smoke +rm -f "$PACK_TGZ" +rm -rf projects/final-verification-godot projects/final-verification-unity projects/final-verification-unreal +git status --short +``` + +Expected: + +- Tests pass. +- Typecheck passes. +- Build passes. +- Built CLI works through both `node dist/cli.js` and `npm exec open-gamestudio -- ...`. +- Packed tarball contains runtime assets and the installed package bin can load them from a non-repo cwd. +- CLI help excludes `next`, `--exec`, telemetry, parallel orchestration, and ownership-enforcement surfaces. +- Repo validation passes. +- Godot, Unity, and Unreal projects initialize and validate. +- Unreal aliases resolve to canonical `unreal` while display output remains `Unreal Engine`. +- Unreal project file exists under `source/project-final-verification-unreal/FinalVerificationUnreal.uproject` before cleanup. +- QA prompt includes validation commands and project rules. +- Prompt cache exists for the QA print-prompt run before cleanup. +- Final git status includes only intentional source/docs changes. + +--- + +## Parity Claim Checklist + +Do not claim full parity until all are true: + +- [ ] Implementation is TypeScript/Node only. +- [ ] Required package scripts exist: `init`, `manage`, `test`, `validate`, and template discoverability; scripts exercise the built CLI path. +- [ ] Package metadata declares the Node runtime floor and ships `dist/`, `engine_configs/`, `agents/base/`, and `templates/`; `npm pack` plus temp install proves installed-bin asset loading. +- [ ] `open-gamestudio` is the canonical CLI. +- [ ] No required Python files, Python package metadata, or Python alias assumptions exist. +- [ ] All 12 agents exist with Codex-native prompts. +- [ ] Design, prototype, and development modes activate the expected agents. +- [ ] Godot, Unity, and Unreal projects initialize with engine files under `source/project-/`. +- [ ] `Unreal`, `Unreal Engine`, `unreal`, and `ue5` normalize consistently. +- [ ] Project management supports `status`, `new`/`init`, `resume`, and `freeze`. +- [ ] `menu` and `startover` are omitted and documented as intentional differences. +- [ ] Market overview/seed and configured competitor names are created during init; eager competitor reports are not required. +- [ ] Templates for GDD, feature spec, handoff, analytics setup, engine setup, market analysis, and project config exist, include required lightweight sections/frontmatter, and are discoverable through `templates list/show`. +- [ ] Project `AGENTS.md`, materialized `master_orchestrator`, and handoff templates preserve orchestration behavior without generating `project_orchestrator.md`; project `AGENTS.md` hash validation fails when stale. +- [ ] Validation exits non-zero on failed checks and does not reproduce false-green behavior. +- [ ] Runner dry-run/print-prompt requires `--task`, is deterministic, and remains bounded. +- [ ] Runner writes prompt cache and minimal metadata. +- [ ] Single-agent prompts do not load all agents, all templates, or broad project artifacts by default. +- [ ] Direct Codex execution, telemetry, parallel orchestration, planner/next, changed-file tracking, and ownership enforcement remain documented future-only features and are not implemented in the first build. +- [ ] Migration docs show equivalent Codex/native CLI workflows without claiming unsupported direct execution. diff --git a/src/agents.ts b/src/agents.ts new file mode 100644 index 0000000..5607b5c --- /dev/null +++ b/src/agents.ts @@ -0,0 +1,95 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { agentNames, guidanceConfigHash, type AgentName, type ProjectConfig } from "./config.js"; +import type { EngineConfigRegistry } from "./engines.js"; +import { packageAssetPath } from "./paths.js"; + +export type MaterializeAgentsInput = { + projectRoot: string; + config: ProjectConfig; + engines: EngineConfigRegistry; +}; + +export function validateBaseAgents(): string[] { + return agentNames.flatMap((agent) => { + const file = packageAssetPath(`agents/base/${agent}.md`); + if (!existsSync(file)) return [`Missing base agent ${agent}`]; + const body = readFileSync(file, "utf8"); + return ["# Role", "# Inputs", "# Outputs", "# Validation", "# Engine Notes", "# Rules"].filter((section) => !sectionHasContent(body, section)).map((section) => `${agent} missing non-empty ${section}`); + }); +} + +function sectionHasContent(body: string, section: string): boolean { + const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = new RegExp(`^${escaped}\\s*$`, "m").exec(body); + if (!match) return false; + const start = match.index + match[0].length; + const rest = body.slice(start); + const nextHeading = rest.search(/^#/m); + const content = nextHeading === -1 ? rest : rest.slice(0, nextHeading); + return content.trim().length > 0; +} + +export function readAgentPrompt(agent: AgentName, projectRoot?: string): string { + const projectPrompt = projectRoot ? path.join(projectRoot, ".gamestudio", "agents", `${agent}.md`) : ""; + if (projectPrompt && existsSync(projectPrompt)) return readFileSync(projectPrompt, "utf8"); + return readFileSync(packageAssetPath(`agents/base/${agent}.md`), "utf8"); +} + +export function generateProjectAgentsMd(config: ProjectConfig): string { + const hash = guidanceConfigHash(config); + return ` + +# ${config.project.name} Agents + +Project: ${config.project.name} +Slug: ${config.project.slug} +Engine: ${config.project.engine} +Mode: ${config.project.mode} + +# Validation + +Run \`npm run validate -- --project projects/${config.project.slug}\`. + +# Agent Prompts + +${config.team.active_agents.map((agent) => `- ${agent}: .gamestudio/agents/${agent}.md`).join("\n")} + +# Rules + +Use bounded context. Load the current role prompt, project config, engine overlay, and task-relevant templates only. +Do not use direct Codex execution, telemetry, planner/next, parallel orchestration, or ownership enforcement in this first build. +`; +} + +export function materializeAgents(input: MaterializeAgentsInput): string[] { + const config = input.config; + const engine = input.engines[config.project.engine]; + const target = path.join(input.projectRoot, ".gamestudio", "agents"); + mkdirSync(target, { recursive: true }); + const written: string[] = []; + for (const agent of config.team.active_agents) { + const base = readFileSync(packageAssetPath(`agents/base/${agent}.md`), "utf8"); + const body = `${base} + +# Project Context + +- Name: ${config.project.name} +- Concept: ${config.project.concept} +- Audience: ${config.project.audience} +- Engine: ${engine.display_name} ${config.project.engine_version} +- Mode: ${config.project.mode} + +# Engine Overlay + +${Object.values(engine.agent_specializations).join("\n")} +`; + const file = path.join(target, `${agent}.md`); + writeFileSync(file, body); + written.push(file); + } + const agentsMd = path.join(input.projectRoot, "AGENTS.md"); + writeFileSync(agentsMd, generateProjectAgentsMd(config)); + written.push(agentsMd); + return written; +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..68ed063 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,110 @@ +#!/usr/bin/env node +import { Command } from "commander"; +import path from "node:path"; +import { formatTemplateShow, listTemplates, templateRegistry, type TemplateId } from "./templates.js"; +import { freezeProject, initProject, resumeProject, statusProject } from "./projects.js"; +import { runValidation } from "./validation.js"; +import { prepareRun } from "./runner.js"; + +const program = new Command(); + +function collectCompetitor(value: string, previous: string[] = []): string[] { + return [...previous, value.trim()].filter(Boolean); +} + +program.name("open-gamestudio").description("Codex-native TypeScript game-studio toolkit").version("0.1.0"); + +function addInitCommand(name: "init" | "new"): void { + program + .command(name) + .description(name === "new" ? "Create a new project through the init path" : "Initialize a game project") + .requiredOption("--name ", "project name") + .requiredOption("--engine ", "godot, unity, or unreal") + .requiredOption("--mode ", "design, prototype, or development") + .option("--concept ", "project concept") + .option("--genre ", "genre") + .option("--platform ", "platform") + .option("--audience ", "audience") + .option("--competitor ", "competitor name; repeat for multiple competitors", collectCompetitor, []) + .option("--monetization ", "monetization model") + .option("--timeline ", "timeline") + .option("--engine-version ", "engine version override") + .requiredOption("--non-interactive", "use deterministic defaults") + .action((opts) => { + const result = initProject({ ...opts, competitors: opts.competitor }); + console.log(`Created ${result.config.project.name} at ${path.relative(process.cwd(), result.projectRoot)}`); + }); +} + +addInitCommand("init"); +addInitCommand("new"); + +program + .command("status") + .description("Print project status") + .option("--project ", "project path") + .action((opts) => console.log(statusProject(opts.project))); + +program + .command("resume") + .description("Print a read-only continuation summary") + .requiredOption("--project ", "project path") + .action((opts) => console.log(resumeProject(opts.project))); + +program + .command("freeze") + .description("Set project status to frozen") + .requiredOption("--project ", "project path") + .action((opts) => console.log(freezeProject(opts.project))); + +program + .command("validate") + .description("Run hard-failing repo or project validation") + .option("--project ", "project path") + .action(async (opts) => { + const result = await runValidation({ project: opts.project }); + for (const check of result.checks) { + console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}${check.path ? ` (${check.path})` : ""}`); + } + if (result.failed) process.exitCode = 1; + }); + +const templates = program.command("templates").description("Discover templates"); +templates.command("list").description("List template IDs").action(() => { + for (const info of listTemplates()) console.log(`${info.id}\t${info.category}\t${info.path}`); +}); +templates + .command("show") + .description("Show a template") + .argument("") + .action((id: TemplateId) => { + if (!templateRegistry[id]) throw new Error(`Unknown template "${id}"`); + console.log(formatTemplateShow(id)); + }); + +program + .command("run") + .description("Prepare one bounded prompt packet for a project agent") + .argument("") + .requiredOption("--project ", "project path") + .requiredOption("--task ", "task text") + .option("--print-prompt", "print deterministic prompt body") + .option("--dry-run", "print selected context and output paths") + .option("--include-artifact ", "include one project artifact", (value, previous: string[] = []) => [...previous, value], []) + .option("--allow-broad-context", "explicitly allow broader context discovery") + .action((agent, opts) => { + const result = prepareRun(agent, { + project: opts.project, + task: opts.task, + printPrompt: opts.printPrompt, + dryRun: opts.dryRun, + includeArtifact: opts.includeArtifact, + allowBroadContext: opts.allowBroadContext + }); + console.log(result.output); + }); + +program.parseAsync().catch((error: unknown) => { + console.error((error as Error).message); + process.exitCode = 1; +}); diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..178dca0 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,119 @@ +import { createHash } from "node:crypto"; +import { readFileSync, writeFileSync } from "node:fs"; +import { z } from "zod"; + +export const agentNames = [ + "master_orchestrator", + "producer_agent", + "market_analyst", + "data_scientist", + "sr_game_designer", + "mid_game_designer", + "mechanics_developer", + "game_feel_developer", + "sr_game_artist", + "technical_artist", + "ui_ux_agent", + "qa_agent" +] as const; + +export const modeSchema = z.enum(["design", "prototype", "development"]); +export const agentNameSchema = z.enum(agentNames); +export type AgentName = z.infer; +export type ProjectMode = z.infer; + +export const milestoneSchema = z.object({ + id: z.string().min(1), + title: z.string().min(1), + target: z.string().min(1), + exit_criteria: z.array(z.string().min(1)).min(1), + status: z.enum(["planned", "active", "complete", "blocked"]).default("planned") +}); + +export const projectConfigSchema = z.object({ + schema_version: z.literal("1.0"), + project: z.object({ + name: z.string().min(1), + slug: z.string().min(1), + concept: z.string().min(1), + genre: z.string().min(1), + platform: z.string().min(1), + audience: z.string().min(1), + competitors: z.array(z.string().min(1)), + monetization: z.string().min(1), + timeline: z.string().min(1), + engine: z.enum(["godot", "unity", "unreal"]), + engine_version: z.string().min(1), + mode: modeSchema, + phase: z.string().min(1), + status: z.enum(["active", "frozen", "inactive"]) + }), + team: z.object({ + active_agents: z.array(agentNameSchema).min(1) + }), + production: z.object({ + milestones: z.array(milestoneSchema).min(1) + }) +}); + +export type ProjectConfig = z.infer; + +export function slugify(value: string): string { + const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); + if (!slug) throw new Error(`Cannot create slug from "${value}"`); + return slug; +} + +export function activeAgentsForMode(mode: ProjectMode): AgentName[] { + const always: AgentName[] = ["master_orchestrator", "producer_agent", "market_analyst", "data_scientist"]; + const byMode: Record = { + design: ["sr_game_designer", "mid_game_designer", "sr_game_artist"], + prototype: ["sr_game_designer", "mechanics_developer", "qa_agent"], + development: [ + "sr_game_designer", + "mid_game_designer", + "mechanics_developer", + "game_feel_developer", + "qa_agent", + "sr_game_artist", + "technical_artist", + "ui_ux_agent" + ] + }; + return [...always, ...byMode[mode]]; +} + +function ordered(value: unknown, omitOperationalFields: boolean): unknown { + if (Array.isArray(value)) return value.map((item) => ordered(item, omitOperationalFields)); + if (value && typeof value === "object") { + const result: Record = {}; + for (const key of Object.keys(value as Record).sort()) { + if (omitOperationalFields && key === "status") continue; + if (omitOperationalFields && /timestamp|run_state|validation/i.test(key)) continue; + result[key] = ordered((value as Record)[key], omitOperationalFields); + } + return result; + } + return value; +} + +export function canonicalProjectConfigJson( + config: ProjectConfig, + options: { omitOperationalFields?: boolean } = {} +): string { + return `${JSON.stringify(ordered(config, options.omitOperationalFields ?? false), null, 2)}\n`; +} + +export function guidanceConfigHash(config: ProjectConfig): string { + return createHash("sha256") + .update(canonicalProjectConfigJson(config, { omitOperationalFields: true })) + .digest("hex"); +} + +export function readProjectConfig(filePath: string): ProjectConfig { + return projectConfigSchema.parse(JSON.parse(readFileSync(filePath, "utf8"))); +} + +export function writeProjectConfig(filePath: string, config: ProjectConfig): void { + writeFileSync(filePath, canonicalProjectConfigJson(projectConfigSchema.parse(config))); +} diff --git a/src/engines.ts b/src/engines.ts new file mode 100644 index 0000000..344142d --- /dev/null +++ b/src/engines.ts @@ -0,0 +1,98 @@ +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { z } from "zod"; + +export type EngineId = "godot" | "unity" | "unreal"; + +const engineConfigSchema = z.object({ + id: z.enum(["godot", "unity", "unreal"]), + display_name: z.string(), + aliases: z.array(z.string()).min(1), + default_version: z.string(), + source_root_pattern: z.literal("source/project-{slug}"), + folders: z.array(z.string()), + project_files: z.array(z.string()), + best_practices: z.array(z.string()), + agent_specializations: z.record(z.string()) +}); + +export type EngineConfig = z.infer; +export type EngineConfigRegistry = Record; +export type EngineCreateInput = { + projectRoot: string; + projectSlug: string; + projectName: string; + engine: EngineId; + registry: EngineConfigRegistry; + engineVersion?: string; +}; + +export function loadEngineConfigs(configDir: string): EngineConfigRegistry { + const entries = ["godot", "unity", "unreal"] as const; + const configs = Object.fromEntries( + entries.map((id) => [id, engineConfigSchema.parse(JSON.parse(readFileSync(path.join(configDir, `${id}.json`), "utf8")))]) + ) as EngineConfigRegistry; + return configs; +} + +export function normalizeEngine(value: string, registry: EngineConfigRegistry): EngineId { + const wanted = value.trim().toLowerCase(); + for (const [id, config] of Object.entries(registry) as [EngineId, EngineConfig][]) { + if (id === wanted || config.display_name.toLowerCase() === wanted || config.aliases.some((a) => a.toLowerCase() === wanted)) { + return id; + } + } + throw new Error(`Unknown engine "${value}". Expected one of: godot, unity, unreal`); +} + +export function sourceRoot(projectRoot: string, projectSlug: string): string { + return path.join(projectRoot, "source", `project-${projectSlug}`); +} + +export function projectClassName(displayNameOrSlug: string): string { + const words = displayNameOrSlug.match(/[A-Za-z0-9]+/g); + if (!words) throw new Error(`Cannot create project class name from "${displayNameOrSlug}"`); + let result = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(""); + if (/^\d/.test(result)) result = `Game${result}`; + return result; +} + +export function unrealProjectFileName(displayNameOrSlug: string): string { + return `${projectClassName(displayNameOrSlug)}.uproject`; +} + +export function createEngineFolders(input: EngineCreateInput): string[] { + const config = input.registry[input.engine]; + if (!config) throw new Error(`Unknown engine "${input.engine}"`); + const root = sourceRoot(input.projectRoot, input.projectSlug); + const created = [root, ...config.folders.map((folder) => path.join(root, folder))]; + for (const folder of created) mkdirSync(folder, { recursive: true }); + return created; +} + +export function createEngineProjectFiles(input: EngineCreateInput): string[] { + const root = sourceRoot(input.projectRoot, input.projectSlug); + const files: string[] = []; + if (input.engine === "godot") { + const file = path.join(root, "project.godot"); + writeFileSync(file, `; Engine configuration file.\nconfig/name="${input.projectName}"\n`); + files.push(file); + } else if (input.engine === "unity") { + const manifest = path.join(root, "Packages", "manifest.json"); + const settings = path.join(root, "ProjectSettings", "ProjectSettings.asset"); + mkdirSync(path.dirname(manifest), { recursive: true }); + mkdirSync(path.dirname(settings), { recursive: true }); + writeFileSync(manifest, `${JSON.stringify({ dependencies: {} }, null, 2)}\n`); + writeFileSync(settings, `%YAML 1.1\nProjectSettings:\n productName: ${input.projectName}\n`); + files.push(manifest, settings); + } else if (input.engine === "unreal") { + const file = path.join(root, unrealProjectFileName(input.projectName)); + writeFileSync(file, `${JSON.stringify({ FileVersion: 3, EngineAssociation: input.engineVersion ?? input.registry.unreal.default_version }, null, 2)}\n`); + files.push(file); + } else { + throw new Error(`Unknown engine "${input.engine}"`); + } + return files; +} + +export { engineConfigSchema }; diff --git a/src/paths.ts b/src/paths.ts new file mode 100644 index 0000000..53f4f95 --- /dev/null +++ b/src/paths.ts @@ -0,0 +1,30 @@ +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +export function packageRoot(metaUrl: string = import.meta.url): string { + let current = path.dirname(fileURLToPath(metaUrl)); + while (true) { + const manifest = path.join(current, "package.json"); + if (existsSync(manifest)) { + const parsed = JSON.parse(readFileSync(manifest, "utf8")) as { name?: string }; + if (parsed.name === "open-gamestudio") return current; + } + const parent = path.dirname(current); + if (parent === current) { + throw new Error("Could not find open-gamestudio package root"); + } + current = parent; + } +} + +export function packageAssetPath(relativePath: string): string { + if (path.isAbsolute(relativePath) || relativePath.includes("..")) { + throw new Error(`Package asset path must be relative and contained: ${relativePath}`); + } + return path.join(packageRoot(import.meta.url), relativePath); +} + +export function resolveProjectRoot(input?: string, cwd: string = process.cwd()): string { + return path.resolve(cwd, input ?? "."); +} diff --git a/src/projects.ts b/src/projects.ts new file mode 100644 index 0000000..3ba752c --- /dev/null +++ b/src/projects.ts @@ -0,0 +1,154 @@ +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { activeAgentsForMode, readProjectConfig, slugify, writeProjectConfig, type ProjectConfig, type ProjectMode } from "./config.js"; +import { createEngineFolders, createEngineProjectFiles, loadEngineConfigs, normalizeEngine, projectClassName, sourceRoot, unrealProjectFileName } from "./engines.js"; +import { materializeAgents } from "./agents.js"; +import { packageAssetPath, resolveProjectRoot } from "./paths.js"; + +export type InitProjectOptions = { + name: string; + engine: string; + mode?: ProjectMode; + concept?: string; + genre?: string; + platform?: string; + audience?: string; + competitors?: string[]; + monetization?: string; + timeline?: string; + engineVersion?: string; + nonInteractive?: boolean; +}; + +export function defaultProjectConfig(options: InitProjectOptions): ProjectConfig { + const engines = loadEngineConfigs(packageAssetPath("engine_configs")); + const engine = normalizeEngine(options.engine, engines); + if (!options.nonInteractive) throw new Error("init requires --non-interactive"); + if (!options.mode) throw new Error("init requires --mode"); + const mode = options.mode; + const slug = slugify(options.name); + return { + schema_version: "1.0", + project: { + name: options.name, + slug, + concept: options.concept ?? `${options.name} concept`, + genre: options.genre ?? "Unspecified", + platform: options.platform ?? "PC", + audience: options.audience ?? "General players", + competitors: options.competitors ?? [], + monetization: options.monetization ?? "undecided", + timeline: options.timeline ?? "TBD", + engine, + engine_version: options.engineVersion ?? engines[engine].default_version, + mode, + phase: "Initialization", + status: "active" + }, + team: { active_agents: activeAgentsForMode(mode) }, + production: { + milestones: [ + { + id: "m1", + title: "Playable prototype", + target: "Week 4", + exit_criteria: ["Core loop is playable"], + status: "planned" + } + ] + } + }; +} + +function writeStarterDocs(projectRoot: string, config: ProjectConfig): void { + mkdirSync(path.join(projectRoot, "documentation", "design"), { recursive: true }); + mkdirSync(path.join(projectRoot, "documentation", "production"), { recursive: true }); + mkdirSync(path.join(projectRoot, "resources", "market-research"), { recursive: true }); + writeFileSync( + path.join(projectRoot, "documentation", "design", "gdd.md"), + `# ${config.project.name} GDD\n\n# Purpose\n\n${config.project.concept}\n\n# Core Loop\n\nDefine and validate the playable loop.\n\n# Validation\n\nRun \`npm run validate -- --project projects/${config.project.slug}\`.\n` + ); + writeFileSync( + path.join(projectRoot, "documentation", "production", "timeline.md"), + `# Timeline\n\n${config.project.timeline}\n\n# Milestones\n\n${config.production.milestones.map((m) => `- ${m.id}: ${m.title} (${m.target})`).join("\n")}\n\n# Risks\n\n- Scope may exceed the first validation gate.\n\n# Next Validation Gate\n\nRun project validation after first playable setup.\n` + ); + writeFileSync( + path.join(projectRoot, "resources", "market-research", "market-overview.md"), + `# Market Overview\n\nAudience: ${config.project.audience}\n\nCompetitors: ${config.project.competitors.join(", ")}\n\nThis is a seed, not a full competitor report.\n` + ); +} + +function assertNoSameParentCollision(parent: string, config: ProjectConfig): void { + if (!existsSync(parent)) return; + const nextClass = projectClassName(config.project.name); + for (const entry of readdirSync(parent, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const configPath = path.join(parent, entry.name, "project-config.json"); + if (!existsSync(configPath)) continue; + const existing = readProjectConfig(configPath); + if (existing.project.name === config.project.name) continue; + if (existing.project.slug === config.project.slug) { + throw new Error(`Project name "${config.project.name}" collides with existing slug "${existing.project.slug}" in ${parent}`); + } + if (existing.project.engine === "unreal" || config.project.engine === "unreal") { + const existingClass = projectClassName(existing.project.name); + if (existingClass === nextClass) { + throw new Error(`Project name "${config.project.name}" collides with existing Unreal class name "${existingClass}" in ${parent}`); + } + } + } +} + +export function initProject(options: InitProjectOptions, cwd = process.cwd()): { projectRoot: string; config: ProjectConfig } { + const config = defaultProjectConfig(options); + const projectRoot = path.resolve(cwd, path.join("projects", config.project.slug)); + if (existsSync(projectRoot)) throw new Error(`Project path already exists or collides: ${projectRoot}`); + assertNoSameParentCollision(path.dirname(projectRoot), config); + const engines = loadEngineConfigs(packageAssetPath("engine_configs")); + mkdirSync(projectRoot, { recursive: true }); + createEngineFolders({ projectRoot, projectSlug: config.project.slug, projectName: config.project.name, engine: config.project.engine, registry: engines }); + createEngineProjectFiles({ projectRoot, projectSlug: config.project.slug, projectName: config.project.name, engine: config.project.engine, registry: engines, engineVersion: config.project.engine_version }); + writeProjectConfig(path.join(projectRoot, "project-config.json"), config); + writeStarterDocs(projectRoot, config); + materializeAgents({ projectRoot, config, engines }); + return { projectRoot, config }; +} + +export function statusProject(project?: string, cwd = process.cwd()): string { + const root = resolveProjectRoot(project, cwd); + const config = readProjectConfig(path.join(root, "project-config.json")); + return [ + `${config.project.name}`, + `phase: ${config.project.phase}`, + `status: ${config.project.status}`, + `mode: ${config.project.mode}`, + `engine: ${config.project.engine}`, + `active agents: ${config.team.active_agents.join(", ")}` + ].join("\n"); +} + +export function resumeProject(project?: string, cwd = process.cwd()): string { + const root = resolveProjectRoot(project, cwd); + const config = readProjectConfig(path.join(root, "project-config.json")); + return `Resume ${config.project.name}\nphase: ${config.project.phase}\nstatus: ${config.project.status}\nSuggested next command: npm exec open-gamestudio -- run producer_agent --project ${path.relative(cwd, root) || "."} --task "Summarize current project state"`; +} + +export function freezeProject(project?: string, cwd = process.cwd()): string { + const root = resolveProjectRoot(project, cwd); + const file = path.join(root, "project-config.json"); + const config = readProjectConfig(file); + config.project.status = "frozen"; + writeProjectConfig(file, config); + return `Frozen ${config.project.name}`; +} + +export function expectedEngineProjectFile(projectRoot: string, config: ProjectConfig): string { + const root = sourceRoot(projectRoot, config.project.slug); + if (config.project.engine === "godot") return path.join(root, "project.godot"); + if (config.project.engine === "unity") return path.join(root, "Packages", "manifest.json"); + return path.join(root, unrealProjectFileName(config.project.name)); +} + +export function readFileIfExists(file: string): string | undefined { + return existsSync(file) ? readFileSync(file, "utf8") : undefined; +} diff --git a/src/runner.ts b/src/runner.ts new file mode 100644 index 0000000..d8b0e75 --- /dev/null +++ b/src/runner.ts @@ -0,0 +1,121 @@ +import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { agentNameSchema, readProjectConfig, type AgentName } from "./config.js"; +import { readAgentPrompt } from "./agents.js"; +import { loadEngineConfigs } from "./engines.js"; +import { packageAssetPath, resolveProjectRoot } from "./paths.js"; +import { readTemplate, selectTemplates } from "./templates.js"; + +export type RunOptions = { + project: string; + task: string; + printPrompt?: boolean; + dryRun?: boolean; + includeArtifact?: string[]; + allowBroadContext?: boolean; +}; + +export type PreparedRun = { + prompt: string; + promptPath: string; + metadataPath: string; + contextFiles: string[]; + output: string; +}; + +function requireTask(task: string): string { + if (!task || !task.trim()) throw new Error("--task is required and must be non-empty"); + return task.trim(); +} + +function safeArtifact(projectRoot: string, artifact: string): string { + if (path.isAbsolute(artifact)) throw new Error("--include-artifact must be relative"); + const full = path.resolve(projectRoot, artifact); + if (!full.startsWith(`${projectRoot}${path.sep}`)) throw new Error("--include-artifact cannot escape the project root"); + const realRoot = realpathSync(projectRoot); + const realFull = realpathSync(full); + if (realFull !== realRoot && !realFull.startsWith(`${realRoot}${path.sep}`)) throw new Error("--include-artifact cannot escape the project root"); + return full; +} + +let runSequence = 0; + +export function prepareRun(agentInput: string, options: RunOptions, cwd = process.cwd()): PreparedRun { + const agent = agentNameSchema.parse(agentInput) as AgentName; + const task = requireTask(options.task); + const projectRoot = resolveProjectRoot(options.project, cwd); + const config = readProjectConfig(path.join(projectRoot, "project-config.json")); + const engines = loadEngineConfigs(packageAssetPath("engine_configs")); + const engine = engines[config.project.engine]; + const templates = selectTemplates(agent, task); + const contextFiles = [ + path.relative(projectRoot, path.join(projectRoot, ".gamestudio", "agents", `${agent}.md`)), + "project-config.json", + `engine_configs/${config.project.engine}.json`, + ...templates.map((id) => `templates/${id}`) + ]; + const artifactBodies = (options.includeArtifact ?? []).map((artifact) => { + const full = safeArtifact(projectRoot, artifact); + contextFiles.push(artifact); + return `# Included Artifact: ${artifact}\n\n${readFileSync(full, "utf8")}`; + }); + const templateBodies = templates.map((id) => `# Template: ${id}\n\n${readTemplate(id)}`).join("\n\n"); + const outputPaths = [ + agent === "market_analyst" ? "resources/market-research/market-analysis.md" : undefined, + agent === "data_scientist" ? "documentation/technical/analytics/analytics-plan.md" : undefined, + agent === "qa_agent" ? "documentation/qa/validation-review.md" : undefined + ].filter(Boolean); + const prompt = [ + `# Open GameStudio Prompt`, + `Agent: ${agent}`, + `Task: ${task}`, + `Project: ${config.project.name} (${config.project.slug})`, + `Engine: ${engine.display_name} ${config.project.engine_version}`, + `Validation: npm run validate -- --project ${path.relative(cwd, projectRoot) || "."}`, + "", + "# Agent Prompt", + readAgentPrompt(agent, projectRoot), + "", + "# Project Summary", + `Concept: ${config.project.concept}`, + `Audience: ${config.project.audience}`, + `Competitors: ${config.project.competitors.join(", ")}`, + "", + "# Engine Overlay", + Object.values(engine.agent_specializations).join("\n"), + "", + templateBodies, + ...artifactBodies, + "", + "# Output Paths", + outputPaths.length ? outputPaths.map((p) => `- ${p}`).join("\n") : "- Use the role prompt output path conventions.", + options.allowBroadContext ? "\n# Broad Context\nExplicit broad context opt-in was provided." : "" + ].join("\n"); + const runId = `${new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 17)}-${process.pid}-${++runSequence}`; + const runDir = path.join(projectRoot, ".gamestudio", "runs", `${runId}-${agent}`); + mkdirSync(runDir, { recursive: true }); + const promptPath = path.join(runDir, "prompt.md"); + const metadataPath = path.join(runDir, "metadata.json"); + writeFileSync(promptPath, prompt); + writeFileSync( + metadataPath, + `${JSON.stringify( + { + timestamp: new Date().toISOString(), + project: path.relative(cwd, projectRoot) || ".", + agent, + task, + prompt_chars: prompt.length, + prompt_cache_path: path.relative(cwd, promptPath) + }, + null, + 2 + )}\n` + ); + const output = options.printPrompt + ? prompt + : options.dryRun + ? `Prompt cache: ${promptPath}\nMetadata: ${metadataPath}\nContext files:\n${contextFiles.map((f) => `- ${f}`).join("\n")}\nValidation: npm run validate -- --project ${path.relative(cwd, projectRoot) || "."}` + : `Prompt cache written: ${promptPath}\nNext manual command: codex exec --cd ${projectRoot} "Read ${path.relative(projectRoot, promptPath)} and perform the requested task."`; + return { prompt, promptPath, metadataPath, contextFiles, output }; +} diff --git a/src/templates.ts b/src/templates.ts new file mode 100644 index 0000000..a4cd9a7 --- /dev/null +++ b/src/templates.ts @@ -0,0 +1,155 @@ +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { packageAssetPath } from "./paths.js"; +import type { AgentName } from "./config.js"; + +export type TemplateId = + | "gdd" + | "feature_spec" + | "handoff" + | "analytics_setup" + | "engine_setup" + | "market_analysis" + | "project_config"; + +export type TemplateInfo = { + id: TemplateId; + category: string; + path: string; + roles: AgentName[]; + tags: string[]; + requiredSections: string[]; +}; + +export const templateRegistry: Record = { + gdd: { + id: "gdd", + category: "design", + path: "templates/gdd_template.md", + roles: ["sr_game_designer", "mid_game_designer"], + tags: ["design", "gdd"], + requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"] + }, + feature_spec: { + id: "feature_spec", + category: "design", + path: "templates/feature_spec_template.md", + roles: ["sr_game_designer", "mid_game_designer", "mechanics_developer"], + tags: ["feature", "spec", "design"], + requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"] + }, + handoff: { + id: "handoff", + category: "coordination", + path: "templates/handoff_template.md", + roles: ["master_orchestrator", "producer_agent"], + tags: ["handoff", "coordination"], + requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"] + }, + analytics_setup: { + id: "analytics_setup", + category: "analytics", + path: "templates/analytics_setup_template.md", + roles: ["data_scientist"], + tags: ["analytics", "metrics"], + requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"] + }, + engine_setup: { + id: "engine_setup", + category: "engine", + path: "templates/engine_setup_template.md", + roles: ["mechanics_developer", "technical_artist"], + tags: ["engine", "setup"], + requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"] + }, + market_analysis: { + id: "market_analysis", + category: "market", + path: "templates/market_analysis_template.md", + roles: ["market_analyst"], + tags: ["market", "competitors"], + requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"] + }, + project_config: { + id: "project_config", + category: "config", + path: "templates/project_config_template.json", + roles: ["producer_agent", "master_orchestrator"], + tags: ["config", "setup"], + requiredSections: [] + } +}; + +export function listTemplates(): TemplateInfo[] { + return Object.values(templateRegistry); +} + +export function readTemplate(id: TemplateId): string { + const info = templateRegistry[id]; + if (!info) throw new Error(`Unknown template "${id}"`); + return readFileSync(packageAssetPath(info.path), "utf8"); +} + +export function formatTemplateShow(id: TemplateId): string { + const info = templateRegistry[id]; + if (!info) throw new Error(`Unknown template "${id}"`); + return [ + `ID: ${info.id}`, + `Category: ${info.category}`, + `Path: ${info.path}`, + `Roles: ${info.roles.join(", ")}`, + `Tags: ${info.tags.join(", ")}`, + "", + readTemplate(id) + ].join("\n"); +} + +export function templatePath(id: TemplateId): string { + return packageAssetPath(templateRegistry[id].path); +} + +function sectionHasContent(body: string, section: string): boolean { + const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = new RegExp(`^${escaped}\\s*$`, "m").exec(body); + if (!match) return false; + const start = match.index + match[0].length; + const rest = body.slice(start); + const nextHeading = rest.search(/^#/m); + const content = nextHeading === -1 ? rest : rest.slice(0, nextHeading); + return content.trim().length > 0; +} + +export function validateTemplateFiles(): string[] { + const failures: string[] = []; + for (const info of listTemplates()) { + const fullPath = path.resolve(packageAssetPath(info.path)); + if (!existsSync(fullPath)) { + failures.push(`Missing template ${info.id}: ${info.path}`); + continue; + } + const body = readFileSync(fullPath, "utf8"); + for (const section of info.requiredSections) { + if (!sectionHasContent(body, section)) failures.push(`Template ${info.id} missing non-empty ${section}`); + } + if (info.id === "project_config") JSON.parse(body); + } + return failures; +} + +export function selectTemplates(agent: AgentName, task: string): TemplateId[] { + const lower = task.toLowerCase(); + const selected = new Set(); + if (/(handoff|coordination|coordinate)/.test(lower)) selected.add("handoff"); + if (agent === "market_analyst") selected.add("market_analysis"); + if (agent === "data_scientist") selected.add("analytics_setup"); + if ((agent === "sr_game_designer" || agent === "mid_game_designer") && /(design|spec|gdd|feature)/.test(lower)) { + selected.add("gdd"); + selected.add("feature_spec"); + } + if (/(engine|setup|project config|initialize|init)/.test(lower)) { + selected.add("engine_setup"); + selected.add("project_config"); + } + if (agent === "qa_agent" && /(spec review|review spec)/.test(lower)) selected.add("feature_spec"); + return [...selected]; +} diff --git a/src/validation.ts b/src/validation.ts new file mode 100644 index 0000000..1068dfe --- /dev/null +++ b/src/validation.ts @@ -0,0 +1,179 @@ +import { execFileSync } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, rmSync, unlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { agentNames, activeAgentsForMode, guidanceConfigHash, readProjectConfig } from "./config.js"; +import { validateBaseAgents } from "./agents.js"; +import { loadEngineConfigs, normalizeEngine, sourceRoot } from "./engines.js"; +import { packageAssetPath } from "./paths.js"; +import { expectedEngineProjectFile, resumeProject, statusProject } from "./projects.js"; +import { templateRegistry, validateTemplateFiles } from "./templates.js"; + +export type CheckStatus = "pass" | "fail" | "skip"; +export type ValidationCheck = { id: string; status: CheckStatus; message: string; path?: string }; + +function pass(id: string, message: string, file?: string): ValidationCheck { + return { id, status: "pass", message, path: file }; +} + +function fail(id: string, message: string, file?: string): ValidationCheck { + return { id, status: "fail", message, path: file }; +} + +function sectionHasContent(body: string, section: string): boolean { + const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = new RegExp(`^${escaped}\\s*$`, "m").exec(body); + if (!match) return false; + const start = match.index + match[0].length; + const rest = body.slice(start); + const nextHeading = rest.search(/^#/m); + const content = nextHeading === -1 ? rest : rest.slice(0, nextHeading); + return content.trim().length > 0; +} + +const requiredAgentSections = ["# Role", "# Inputs", "# Outputs", "# Validation", "# Engine Notes", "# Rules"]; + +export async function validateRepo(root = process.cwd()): Promise { + const checks: ValidationCheck[] = []; + const pkgPath = path.join(root, "package.json"); + const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { + scripts?: Record; + bin?: Record; + files?: string[]; + engines?: { node?: string }; + }; + const scripts = pkg.scripts ?? {}; + for (const script of ["init", "manage", "test", "validate", "templates"]) { + checks.push(scripts[script] ? pass(`package.script.${script}`, `script ${script} exists`) : fail(`package.script.${script}`, `missing script ${script}`, pkgPath)); + } + checks.push(scripts.build === "tsc -p tsconfig.build.json" ? pass("package.build", "build uses tsconfig.build.json") : fail("package.build", "build must use tsconfig.build.json", pkgPath)); + checks.push(pkg.bin?.["open-gamestudio"] === "./dist/cli.js" ? pass("package.bin", "bin points to dist/cli.js") : fail("package.bin", "bin must point to ./dist/cli.js", pkgPath)); + checks.push(pkg.engines?.node?.includes(">=20") ? pass("package.node", "node floor declared") : fail("package.node", "node >=20 must be declared", pkgPath)); + for (const file of ["dist/", "engine_configs/", "agents/base/", "templates/"]) { + checks.push(pkg.files?.includes(file) ? pass(`package.files.${file}`, `${file} shipped`) : fail(`package.files.${file}`, `${file} missing from package files`, pkgPath)); + } + for (const file of ["src/cli.ts", "src/paths.ts", "src/config.ts", "src/engines.ts", "src/templates.ts", "src/agents.ts", "src/projects.ts", "src/runner.ts", "src/validation.ts"]) { + checks.push(existsSync(path.join(root, file)) ? pass(`source.${file}`, `${file} exists`) : fail(`source.${file}`, `${file} missing`, file)); + } + const tsFiles = ["cli", "config", "engines", "templates", "agents", "projects", "runner", "validation", "paths"].map((f) => path.join(root, "src", `${f}.ts`)); + for (const file of tsFiles.filter(existsSync)) { + const body = readFileSync(file, "utf8"); + const bad = body.match(/from "\.\/(?!.*\.js")/); + if (bad) checks.push(fail("typescript.imports", `relative import missing .js in ${file}`, file)); + } + const engines = loadEngineConfigs(packageAssetPath("engine_configs")); + for (const value of ["Godot", "Unity", "Unreal", "Unreal Engine", "ue5"]) { + try { + normalizeEngine(value, engines); + checks.push(pass(`engine.alias.${value}`, `${value} normalizes`)); + } catch (error) { + checks.push(fail(`engine.alias.${value}`, (error as Error).message)); + } + } + checks.push(...validateBaseAgents().map((message) => fail("agents.base", message))); + if (validateBaseAgents().length === 0) checks.push(pass("agents.base", "all 12 base agents exist")); + const templateFailures = validateTemplateFiles(); + checks.push(...templateFailures.map((message) => fail("templates", message))); + if (templateFailures.length === 0 && Object.keys(templateRegistry).length === 7) checks.push(pass("templates", "all templates exist")); + checks.push(existsSync(path.join(root, "dist", "cli.js")) ? pass("build.output", "dist/cli.js exists") : fail("build.output", "dist/cli.js missing; run npm run build", path.join(root, "dist", "cli.js"))); + if (existsSync(path.join(root, "dist", "cli.js"))) { + try { + const packRaw = execFileSync("npm", ["pack", "--json"], { cwd: root, encoding: "utf8" }); + const packInfo = JSON.parse(packRaw)[0] as { filename: string; files: { path: string }[] }; + const packed = new Set(packInfo.files.map((file) => file.path)); + for (const need of ["dist/cli.js", "engine_configs/godot.json", "engine_configs/unity.json", "engine_configs/unreal.json", "templates/gdd_template.md", "agents/base/master_orchestrator.md"]) { + checks.push(packed.has(need) ? pass(`pack.${need}`, `${need} packed`) : fail(`pack.${need}`, `${need} missing from npm pack`)); + } + const temp = mkdtempSync(path.join(tmpdir(), "open-gamestudio-pack-")); + try { + execFileSync("npm", ["install", "--silent", "--prefix", temp, path.join(root, packInfo.filename)], { cwd: root, encoding: "utf8" }); + execFileSync("npm", ["exec", "--prefix", temp, "open-gamestudio", "--", "templates", "list"], { cwd: temp, encoding: "utf8" }); + checks.push(pass("pack.install_smoke", "installed package bin loads templates from temp cwd")); + } finally { + rmSync(temp, { recursive: true, force: true }); + unlinkSync(path.join(root, packInfo.filename)); + } + } catch (error) { + checks.push(fail("pack.install_smoke", `package smoke failed: ${(error as Error).message}`)); + } + } + const help = readFileSync(path.join(root, "src", "cli.ts"), "utf8"); + for (const forbidden of ["next", "--exec", "telemetry", "parallel orchestration", "ownership enforcement"]) { + checks.push(!help.includes(`command("${forbidden}`) && !help.includes(`option("${forbidden}`) ? pass(`future.absent.${forbidden}`, `${forbidden} not exposed`) : fail(`future.absent.${forbidden}`, `${forbidden} must not be exposed`)); + } + return checks; +} + +export function validateProject(projectRoot: string): ValidationCheck[] { + const checks: ValidationCheck[] = []; + const configPath = path.join(projectRoot, "project-config.json"); + let config; + try { + config = readProjectConfig(configPath); + checks.push(pass("project.config", "config schema-valid", configPath)); + } catch (error) { + return [fail("project.config", `invalid project config: ${(error as Error).message}`, configPath)]; + } + const expectedAgents = activeAgentsForMode(config.project.mode); + checks.push(JSON.stringify(expectedAgents) === JSON.stringify(config.team.active_agents) ? pass("project.active_agents", "active agents match mode") : fail("project.active_agents", "active agents do not match mode", configPath)); + const engines = loadEngineConfigs(packageAssetPath("engine_configs")); + const root = sourceRoot(projectRoot, config.project.slug); + checks.push(existsSync(root) ? pass("project.source_root", "engine source root exists", root) : fail("project.source_root", "engine source root missing", root)); + const engineFile = expectedEngineProjectFile(projectRoot, config); + checks.push(existsSync(engineFile) ? pass("project.engine_file", "engine project file exists", engineFile) : fail("project.engine_file", "engine project file missing", engineFile)); + if (config.project.engine === "unity") { + const settings = path.join(root, "ProjectSettings", "ProjectSettings.asset"); + checks.push(existsSync(settings) ? pass("project.engine_settings", "Unity ProjectSettings marker exists", settings) : fail("project.engine_settings", "Unity ProjectSettings marker missing", settings)); + } + for (const agent of config.team.active_agents) { + const file = path.join(projectRoot, ".gamestudio", "agents", `${agent}.md`); + if (!existsSync(file)) { + checks.push(fail(`project.agent.${agent}`, `${agent} prompt missing`, file)); + continue; + } + const body = readFileSync(file, "utf8"); + const engine = engines[config.project.engine]; + const hasProjectContext = + sectionHasContent(body, "# Project Context") && + body.includes(`- Name: ${config.project.name}`) && + body.includes(`- Engine: ${engine.display_name} ${config.project.engine_version}`) && + sectionHasContent(body, "# Engine Overlay"); + const missingSections = requiredAgentSections.filter((section) => !sectionHasContent(body, section)); + checks.push( + hasProjectContext && missingSections.length === 0 + ? pass(`project.agent.${agent}`, `${agent} materialized`, file) + : fail(`project.agent.${agent}`, `${agent} prompt missing project context or non-empty sections: ${missingSections.join(", ") || "project context"}`, file) + ); + } + const agentsMd = path.join(projectRoot, "AGENTS.md"); + if (existsSync(agentsMd)) { + const body = readFileSync(agentsMd, "utf8"); + const hash = guidanceConfigHash(config); + checks.push(body.includes("generated-by: open-gamestudio src/agents.ts") ? pass("project.agents_md.provenance", "AGENTS.md provenance ok", agentsMd) : fail("project.agents_md.provenance", "AGENTS.md provenance missing", agentsMd)); + checks.push(body.includes(`source-config-sha256: ${hash}`) ? pass("project.agents_md.hash", "AGENTS.md hash current", agentsMd) : fail("project.agents_md.hash", "AGENTS.md stale; regenerate project agents", agentsMd)); + } else { + checks.push(fail("project.agents_md", "project AGENTS.md missing", agentsMd)); + } + for (const file of ["resources/market-research/market-overview.md", "documentation/design/gdd.md", "documentation/production/timeline.md"]) { + checks.push(existsSync(path.join(projectRoot, file)) ? pass(`project.artifact.${file}`, `${file} exists`) : fail(`project.artifact.${file}`, `${file} missing`, path.join(projectRoot, file))); + } + const timeline = path.join(projectRoot, "documentation", "production", "timeline.md"); + if (existsSync(timeline)) { + const body = readFileSync(timeline, "utf8"); + for (const section of ["# Timeline", "# Milestones", "# Risks", "# Next Validation Gate"]) { + checks.push(sectionHasContent(body, section) ? pass(`project.timeline.${section}`, `${section} exists`, timeline) : fail(`project.timeline.${section}`, `${section} missing non-empty content`, timeline)); + } + } + const before = JSON.stringify(readFileSync(configPath, "utf8")); + statusProject(projectRoot, path.dirname(projectRoot)); + resumeProject(projectRoot, path.dirname(projectRoot)); + const after = JSON.stringify(readFileSync(configPath, "utf8")); + checks.push(before === after ? pass("project.read_only", "status/resume are read-only") : fail("project.read_only", "status/resume mutated project", configPath)); + return checks; +} + +export async function runValidation(options: { project?: string; root?: string } = {}): Promise<{ checks: ValidationCheck[]; failed: boolean }> { + const root = options.root ?? process.cwd(); + const checks = options.project ? validateProject(path.resolve(root, options.project)) : await validateRepo(root); + return { checks, failed: checks.some((check) => check.status === "fail") }; +} diff --git a/templates/analytics_setup_template.md b/templates/analytics_setup_template.md new file mode 100644 index 0000000..5302f6f --- /dev/null +++ b/templates/analytics_setup_template.md @@ -0,0 +1,16 @@ +# Purpose + +Plan analytics instrumentation for the game. + +# Inputs + +- Core loop, player actions, success metrics, privacy constraints, and engine. + +# Outputs + +- `documentation/technical/analytics/analytics-plan.md` +- Event taxonomy, funnels, dashboards, and validation checks. + +# Validation + +Run `npm run validate -- --project ` and verify analytics docs are consistent with project goals. diff --git a/templates/engine_setup_template.md b/templates/engine_setup_template.md new file mode 100644 index 0000000..0851e53 --- /dev/null +++ b/templates/engine_setup_template.md @@ -0,0 +1,16 @@ +# Purpose + +Document engine setup work for the selected game engine. + +# Inputs + +- Engine config, source root, project files, target platform, and team mode. + +# Outputs + +- `documentation/technical/engine-setup.md` +- Engine version, required folders, project-file contract, and setup checklist. + +# Validation + +Run `npm run validate -- --project ` and verify engine project files exist. diff --git a/templates/feature_spec_template.md b/templates/feature_spec_template.md new file mode 100644 index 0000000..d55e43c --- /dev/null +++ b/templates/feature_spec_template.md @@ -0,0 +1,16 @@ +# Purpose + +Define a focused feature specification. + +# Inputs + +- Feature goal, affected systems, constraints, and acceptance criteria. + +# Outputs + +- `documentation/design/features/.md` +- User story, mechanics, implementation notes, test cases, and open questions. + +# Validation + +Run `npm run validate -- --project ` and include feature-specific checks. diff --git a/templates/gdd_template.md b/templates/gdd_template.md new file mode 100644 index 0000000..c1cbdaf --- /dev/null +++ b/templates/gdd_template.md @@ -0,0 +1,16 @@ +# Purpose + +Create or update the game design document. + +# Inputs + +- Project concept, engine, audience, mode, and current milestones. + +# Outputs + +- `documentation/design/gdd.md` +- Clear core loop, player goals, systems, content scope, and risks. + +# Validation + +Run `npm run validate -- --project ` after updating design docs. diff --git a/templates/handoff_template.md b/templates/handoff_template.md new file mode 100644 index 0000000..a9d2157 --- /dev/null +++ b/templates/handoff_template.md @@ -0,0 +1,16 @@ +# Purpose + +Transfer context between agents without broad project scanning. + +# Inputs + +- Completed work, changed artifacts, decisions, blockers, and next requested role. + +# Outputs + +- `documentation/handoffs/.md` +- Summary, artifact links, validation state, and next command. + +# Validation + +Run `npm run validate -- --project ` before handing off. diff --git a/templates/market_analysis_template.md b/templates/market_analysis_template.md new file mode 100644 index 0000000..0896060 --- /dev/null +++ b/templates/market_analysis_template.md @@ -0,0 +1,16 @@ +# Purpose + +Analyze market positioning using configured competitors as seeds. + +# Inputs + +- Project audience, genre, platform, monetization, and competitor names. + +# Outputs + +- `resources/market-research/market-analysis.md` +- Audience hypothesis, competitor comparison, positioning, and risks. + +# Validation + +Run `npm run validate -- --project ` and preserve competitor names in project config. diff --git a/templates/project_config_template.json b/templates/project_config_template.json new file mode 100644 index 0000000..b619215 --- /dev/null +++ b/templates/project_config_template.json @@ -0,0 +1,41 @@ +{ + "schema_version": "1.0", + "project": { + "name": "Test Game", + "slug": "test-game", + "concept": "A focused test concept", + "genre": "Puzzle", + "platform": "PC", + "audience": "Players who like compact strategy games", + "competitors": ["mini-metro", "dorfromantik"], + "monetization": "premium", + "timeline": "8 weeks", + "engine": "godot", + "engine_version": "4.4.1", + "mode": "prototype", + "phase": "Initialization", + "status": "active" + }, + "team": { + "active_agents": [ + "master_orchestrator", + "producer_agent", + "market_analyst", + "data_scientist", + "sr_game_designer", + "mechanics_developer", + "qa_agent" + ] + }, + "production": { + "milestones": [ + { + "id": "m1", + "title": "Playable prototype", + "target": "Week 4", + "exit_criteria": ["Core loop is playable"], + "status": "planned" + } + ] + } +} diff --git a/tests/agents-templates.test.ts b/tests/agents-templates.test.ts new file mode 100644 index 0000000..9e6f0b2 --- /dev/null +++ b/tests/agents-templates.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "vitest"; +import { activeAgentsForMode, canonicalProjectConfigJson, guidanceConfigHash, projectConfigSchema, slugify } from "../src/config.js"; +import { validateBaseAgents } from "../src/agents.js"; +import { formatTemplateShow, listTemplates, readTemplate, selectTemplates, validateTemplateFiles } from "../src/templates.js"; + +describe("config, agents, and templates", () => { + test("slug, active agents, and canonical hash are deterministic", () => { + expect(slugify("My Game")).toBe("my-game"); + expect(activeAgentsForMode("prototype")).toContain("qa_agent"); + const config = projectConfigSchema.parse(JSON.parse(readTemplate("project_config"))); + const hash = guidanceConfigHash(config); + config.project.status = "frozen"; + expect(guidanceConfigHash(config)).toBe(hash); + config.project.genre = "Strategy"; + expect(guidanceConfigHash(config)).not.toBe(hash); + expect(canonicalProjectConfigJson(config).endsWith("\n")).toBe(true); + }); + + test("all base prompts and templates have required sections", () => { + expect(validateBaseAgents()).toEqual([]); + expect(validateTemplateFiles()).toEqual([]); + expect(listTemplates().map((t) => t.id).sort()).toEqual([ + "analytics_setup", + "engine_setup", + "feature_spec", + "gdd", + "handoff", + "market_analysis", + "project_config" + ]); + }); + + test("template selection is bounded", () => { + expect(selectTemplates("market_analyst", "Create market overview")).toEqual(["market_analysis"]); + expect(selectTemplates("data_scientist", "Create analytics plan")).toEqual(["analytics_setup"]); + expect(selectTemplates("qa_agent", "Review validation readiness")).toEqual([]); + expect(selectTemplates("producer_agent", "handoff coordination")).toEqual(["handoff"]); + }); + + test("template show includes discoverability metadata before body", () => { + const output = formatTemplateShow("gdd"); + expect(output).toContain("ID: gdd"); + expect(output).toContain("Category: design"); + expect(output).toContain("Path: templates/gdd_template.md"); + expect(output).toContain("Roles: sr_game_designer, mid_game_designer"); + expect(output).toContain("# Purpose"); + }); +}); diff --git a/tests/engine-system.test.ts b/tests/engine-system.test.ts new file mode 100644 index 0000000..cf2c151 --- /dev/null +++ b/tests/engine-system.test.ts @@ -0,0 +1,41 @@ +import { mkdtempSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import { createEngineFolders, createEngineProjectFiles, loadEngineConfigs, normalizeEngine, projectClassName, sourceRoot, unrealProjectFileName } from "../src/engines.js"; +import { packageAssetPath } from "../src/paths.js"; + +describe("engine registry", () => { + test("loads all engine configs and aliases", () => { + const registry = loadEngineConfigs(packageAssetPath("engine_configs")); + expect(Object.keys(registry).sort()).toEqual(["godot", "unity", "unreal"]); + expect(normalizeEngine("Godot", registry)).toBe("godot"); + expect(normalizeEngine("Unity Engine", registry)).toBe("unity"); + expect(normalizeEngine("Unreal Engine", registry)).toBe("unreal"); + expect(normalizeEngine("ue5", registry)).toBe("unreal"); + expect(() => normalizeEngine("scratch", registry)).toThrow(/Unknown engine/); + }); + + test("creates engine roots under source/project-slug", () => { + const root = mkdtempSync(path.join(tmpdir(), "ogs-engine-")); + const registry = loadEngineConfigs(packageAssetPath("engine_configs")); + for (const engine of ["godot", "unity", "unreal"] as const) { + createEngineFolders({ projectRoot: root, projectSlug: "test-game", projectName: "Test Game", engine, registry }); + createEngineProjectFiles({ projectRoot: root, projectSlug: "test-game", projectName: "Test Game", engine, registry }); + } + const src = sourceRoot(root, "test-game"); + expect(existsSync(path.join(src, "project.godot"))).toBe(true); + expect(existsSync(path.join(src, "Packages", "manifest.json"))).toBe(true); + expect(existsSync(path.join(src, "ProjectSettings", "ProjectSettings.asset"))).toBe(true); + expect(existsSync(path.join(src, "TestGame.uproject"))).toBe(true); + }); + + test("generates Unreal class names", () => { + expect(projectClassName("Test Game")).toBe("TestGame"); + expect(projectClassName("codex-unreal-smoke")).toBe("CodexUnrealSmoke"); + expect(projectClassName("2d arena")).toBe("Game2dArena"); + expect(projectClassName("rocket! zone")).toBe("RocketZone"); + expect(() => projectClassName("!!!")).toThrow(/Cannot create/); + expect(unrealProjectFileName("Test Game")).toBe("TestGame.uproject"); + }); +}); diff --git a/tests/project-workflow.test.ts b/tests/project-workflow.test.ts new file mode 100644 index 0000000..86c3eca --- /dev/null +++ b/tests/project-workflow.test.ts @@ -0,0 +1,113 @@ +import { existsSync, readFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import { guidanceConfigHash, readProjectConfig } from "../src/config.js"; +import { freezeProject, initProject, resumeProject, statusProject } from "../src/projects.js"; + +describe("project workflow", () => { + test("init creates project docs, config, agents, and engine files", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-project-")); + const { projectRoot, config } = initProject({ name: "Test Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); + expect(existsSync(path.join(projectRoot, "project-config.json"))).toBe(true); + expect(existsSync(path.join(projectRoot, "source", "project-test-game", "project.godot"))).toBe(true); + expect(existsSync(path.join(projectRoot, "resources", "market-research", "market-overview.md"))).toBe(true); + expect(existsSync(path.join(projectRoot, "documentation", "design", "gdd.md"))).toBe(true); + expect(existsSync(path.join(projectRoot, ".gamestudio", "agents", "master_orchestrator.md"))).toBe(true); + expect(readFileSync(path.join(projectRoot, "AGENTS.md"), "utf8")).toContain(guidanceConfigHash(config)); + expect(config.project.concept).toBe("Test Game concept"); + expect(config.project.genre).toBe("Unspecified"); + expect(config.project.platform).toBe("PC"); + expect(config.project.audience).toBe("General players"); + expect(config.project.competitors).toEqual([]); + expect(config.project.monetization).toBe("undecided"); + expect(config.project.timeline).toBe("TBD"); + expect(existsSync(path.join(projectRoot, "resources", "market-research", "mini-metro.md"))).toBe(false); + }); + + test("init requires explicit non-interactive mode and supports optional overrides", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-required-")); + expect(() => initProject({ name: "Missing Mode", engine: "godot", nonInteractive: true }, cwd)).toThrow(/--mode/); + expect(() => initProject({ name: "Missing Noninteractive", engine: "godot", mode: "prototype" }, cwd)).toThrow(/--non-interactive/); + const { config } = initProject({ + name: "Override Game", + engine: "godot", + mode: "design", + nonInteractive: true, + competitors: ["terra nil", "mini metro"], + engineVersion: "4.5.custom" + }, cwd); + expect(config.project.competitors).toEqual(["terra nil", "mini metro"]); + expect(config.project.engine_version).toBe("4.5.custom"); + }); + + test("CLI init requires mode and non-interactive and accepts repeated competitor flags", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-cli-")); + const cli = path.join(process.cwd(), "src", "cli.ts"); + const tsx = path.join(process.cwd(), "node_modules", ".bin", "tsx"); + expect(() => execFileSync(tsx, [cli, "init", "--name", "CLI Missing Mode", "--engine", "godot", "--non-interactive"], { cwd, encoding: "utf8", stdio: "pipe" })).toThrow(); + execFileSync(tsx, [ + cli, + "init", + "--name", + "CLI Game", + "--engine", + "godot", + "--mode", + "prototype", + "--non-interactive", + "--competitor", + "terra nil", + "--competitor", + "mini metro", + "--engine-version", + "4.5.custom" + ], { cwd, encoding: "utf8" }); + const config = readProjectConfig(path.join(cwd, "projects", "cli-game", "project-config.json")); + expect(config.project.competitors).toEqual(["terra nil", "mini metro"]); + expect(config.project.engine_version).toBe("4.5.custom"); + }); + + test("CLI init does not expose arbitrary project root override", () => { + const cli = path.join(process.cwd(), "src", "cli.ts"); + const tsx = path.join(process.cwd(), "node_modules", ".bin", "tsx"); + const help = execFileSync(tsx, [cli, "init", "--help"], { encoding: "utf8" }); + expect(help).not.toContain("--root"); + }); + + test("init ignores arbitrary root override and stays under projects slug", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-root-")); + const outsideRoot = path.join(cwd, "outside-root"); + const { projectRoot } = initProject({ name: "Root Escape", engine: "godot", mode: "prototype", nonInteractive: true, root: outsideRoot } as Parameters[0], cwd); + expect(projectRoot).toBe(path.join(cwd, "projects", "root-escape")); + expect(existsSync(outsideRoot)).toBe(false); + }); + + test("all engines initialize expected files", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-engines-")); + expect(existsSync(path.join(initProject({ name: "Godot Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd).projectRoot, "source", "project-godot-game", "project.godot"))).toBe(true); + expect(existsSync(path.join(initProject({ name: "Unity Game", engine: "unity", mode: "design", nonInteractive: true }, cwd).projectRoot, "source", "project-unity-game", "Packages", "manifest.json"))).toBe(true); + expect(existsSync(path.join(initProject({ name: "Unreal Game", engine: "Unreal Engine", mode: "development", nonInteractive: true }, cwd).projectRoot, "source", "project-unreal-game", "UnrealGame.uproject"))).toBe(true); + }); + + test("same-parent init rejects Unreal class-name collisions", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-collision-")); + initProject({ name: "Foo1", engine: "unreal", mode: "prototype", nonInteractive: true }, cwd); + expect(() => initProject({ name: "Foo 1", engine: "unreal", mode: "prototype", nonInteractive: true }, cwd)).toThrow(/collides/i); + }); + + test("status resume are read-only and freeze only changes operational status", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-status-")); + const { projectRoot } = initProject({ name: "Freeze Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); + const before = readFileSync(path.join(projectRoot, "project-config.json"), "utf8"); + expect(statusProject(projectRoot, cwd)).toContain("status: active"); + expect(resumeProject(projectRoot, cwd)).toContain("Suggested next command"); + expect(readFileSync(path.join(projectRoot, "project-config.json"), "utf8")).toBe(before); + const hash = guidanceConfigHash(readProjectConfig(path.join(projectRoot, "project-config.json"))); + freezeProject(projectRoot, cwd); + expect(readProjectConfig(path.join(projectRoot, "project-config.json")).project.status).toBe("frozen"); + expect(guidanceConfigHash(readProjectConfig(path.join(projectRoot, "project-config.json")))).toBe(hash); + }); +}); diff --git a/tests/runner-prompts.test.ts b/tests/runner-prompts.test.ts new file mode 100644 index 0000000..924fe05 --- /dev/null +++ b/tests/runner-prompts.test.ts @@ -0,0 +1,76 @@ +import { existsSync, readFileSync, symlinkSync, writeFileSync } from "node:fs"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import { initProject } from "../src/projects.js"; +import { prepareRun } from "../src/runner.js"; + +describe("bounded runner", () => { + test("requires non-empty task", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-")); + const { projectRoot } = initProject({ name: "Run Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); + expect(() => prepareRun("qa_agent", { project: projectRoot, task: "" }, cwd)).toThrow(/--task/); + }); + + test("writes prompt cache and metadata with bounded context", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-")); + const { projectRoot } = initProject({ name: "Market Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); + const result = prepareRun("market_analyst", { project: projectRoot, task: "Create first market overview", printPrompt: true }, cwd); + expect(existsSync(result.promptPath)).toBe(true); + expect(existsSync(result.metadataPath)).toBe(true); + expect(result.prompt).toContain("# Template: market_analysis"); + expect(result.prompt).toContain("resources/market-research/market-analysis.md"); + expect(result.prompt).not.toContain("# Template: analytics_setup"); + expect(result.prompt).not.toContain("Data scientist prompt"); + const metadata = JSON.parse(readFileSync(result.metadataPath, "utf8")); + expect(metadata.prompt_chars).toBe(result.prompt.length); + expect(metadata.prompt_cache_path).toContain("prompt.md"); + }); + + test("dry-run lists context and explicit artifacts only", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-")); + const { projectRoot } = initProject({ name: "Qa Game", engine: "Unreal Engine", mode: "development", nonInteractive: true }, cwd); + writeFileSync(path.join(projectRoot, "documentation", "design", "note.md"), "# Note\n"); + const result = prepareRun( + "qa_agent", + { project: projectRoot, task: "Review validation readiness", dryRun: true, includeArtifact: ["documentation/design/note.md"] }, + cwd + ); + expect(result.output).toContain("Context files:"); + expect(result.output).toContain("documentation/design/note.md"); + expect(result.prompt).toContain("Unreal Engine"); + expect(result.prompt).toContain("npm run validate"); + expect(() => prepareRun("qa_agent", { project: projectRoot, task: "x", includeArtifact: ["../outside.md"] }, cwd)).toThrow(/escape/); + }); + + test("included artifacts cannot escape through project-local symlinks", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-")); + const { projectRoot } = initProject({ name: "Symlink Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); + const outside = path.join(cwd, "outside.md"); + writeFileSync(outside, "# Secret\n"); + symlinkSync(outside, path.join(projectRoot, "documentation", "design", "outside-link.md")); + expect(() => + prepareRun("qa_agent", { project: projectRoot, task: "x", includeArtifact: ["documentation/design/outside-link.md"] }, cwd) + ).toThrow(/escape/); + }); + + test("prompt cache paths are unique for repeated runs", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-")); + const { projectRoot } = initProject({ name: "Unique Run Game", engine: "unity", mode: "prototype", nonInteractive: true }, cwd); + const a = prepareRun("data_scientist", { project: projectRoot, task: "Create analytics plan" }, cwd); + const b = prepareRun("data_scientist", { project: projectRoot, task: "Create analytics plan" }, cwd); + expect(a.promptPath).not.toBe(b.promptPath); + expect(a.metadataPath).not.toBe(b.metadataPath); + }); + + test("same inputs produce same deterministic prompt body aside from metadata path", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-")); + const { projectRoot } = initProject({ name: "Stable Game", engine: "unity", mode: "prototype", nonInteractive: true }, cwd); + const a = prepareRun("data_scientist", { project: projectRoot, task: "Create analytics plan" }, cwd).prompt; + const b = prepareRun("data_scientist", { project: projectRoot, task: "Create analytics plan" }, cwd).prompt; + expect(a).toBe(b); + expect(a).toContain("# Template: analytics_setup"); + expect(a).toContain("documentation/technical/analytics/analytics-plan.md"); + }); +}); diff --git a/tests/validation.test.ts b/tests/validation.test.ts new file mode 100644 index 0000000..be1dd49 --- /dev/null +++ b/tests/validation.test.ts @@ -0,0 +1,101 @@ +import { rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import { guidanceConfigHash, readProjectConfig, writeProjectConfig } from "../src/config.js"; +import { freezeProject, initProject } from "../src/projects.js"; +import { runValidation, validateProject } from "../src/validation.js"; + +describe("validation", () => { + test("fresh initialized projects validate and failures are explicit", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-")); + for (const [name, engine, mode] of [ + ["Godot Val", "godot", "prototype"], + ["Unity Val", "unity", "design"], + ["Unreal Val", "ue5", "development"] + ] as const) { + const { projectRoot } = initProject({ name, engine, mode, nonInteractive: true }, cwd); + expect(validateProject(projectRoot).filter((c) => c.status === "fail")).toEqual([]); + } + }); + + test("missing required project artifacts fail", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-")); + const { projectRoot } = initProject({ name: "Broken Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); + rmSync(path.join(projectRoot, "AGENTS.md")); + rmSync(path.join(projectRoot, "source", "project-broken-game", "project.godot")); + const failures = validateProject(projectRoot).filter((c) => c.status === "fail"); + expect(failures.map((f) => f.id)).toContain("project.agents_md"); + expect(failures.map((f) => f.id)).toContain("project.engine_file"); + }); + + test("malformed materialized agent prompts fail validation", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-")); + const { projectRoot, config } = initProject({ name: "Prompt Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); + writeFileSync( + path.join(projectRoot, ".gamestudio", "agents", "master_orchestrator.md"), + `# Role\n\n# Inputs\n\n# Outputs\n\n# Validation\n\n# Engine Notes\n\n# Rules\n\n# Project Context\n\n- Name: ${config.project.name}\n- Engine: Godot ${config.project.engine_version}\n\n# Engine Overlay\n\nUse Godot.\n` + ); + const failures = validateProject(projectRoot).filter((c) => c.status === "fail"); + expect(failures.map((f) => f.id)).toContain("project.agent.master_orchestrator"); + }); + + test("materialized agent prompts fail validation when engine overlay is empty", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-")); + const { projectRoot, config } = initProject({ name: "Overlay Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); + writeFileSync( + path.join(projectRoot, ".gamestudio", "agents", "master_orchestrator.md"), + `# Role\n\nCoordinate the team.\n\n# Inputs\n\n- Project brief.\n\n# Outputs\n\n- Production direction.\n\n# Validation\n\n- Check generated artifacts.\n\n# Engine Notes\n\n- Use engine-specific guidance.\n\n# Rules\n\n- Keep work scoped.\n\n# Project Context\n\n- Name: ${config.project.name}\n- Engine: Godot ${config.project.engine_version}\n\n# Engine Overlay\n\n` + ); + const failures = validateProject(projectRoot).filter((c) => c.status === "fail"); + expect(failures.map((f) => f.id)).toContain("project.agent.master_orchestrator"); + }); + + test("empty timeline sections fail validation", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-")); + const { projectRoot } = initProject({ name: "Timeline Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); + writeFileSync( + path.join(projectRoot, "documentation", "production", "timeline.md"), + "# Timeline\n\nTBD\n\n# Milestones\n\n# Risks\n\n- Scope risk.\n\n# Next Validation Gate\n\nRun validation.\n" + ); + const failures = validateProject(projectRoot).filter((c) => c.status === "fail"); + expect(failures.map((f) => f.id)).toContain("project.timeline.# Milestones"); + }); + + test("Unity validation fails when ProjectSettings marker is missing", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-")); + const { projectRoot } = initProject({ name: "Broken Unity", engine: "unity", mode: "design", nonInteractive: true }, cwd); + rmSync(path.join(projectRoot, "source", "project-broken-unity", "ProjectSettings", "ProjectSettings.asset")); + const failures = validateProject(projectRoot).filter((c) => c.status === "fail"); + expect(failures.map((f) => f.id)).toContain("project.engine_settings"); + }); + + test("invalid config and stale AGENTS hash fail", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-")); + const { projectRoot } = initProject({ name: "Stale Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); + const configPath = path.join(projectRoot, "project-config.json"); + const config = readProjectConfig(configPath); + const hash = guidanceConfigHash(config); + config.project.genre = "Changed"; + writeProjectConfig(configPath, config); + expect(validateProject(projectRoot).some((c) => c.id === "project.agents_md.hash" && c.status === "fail")).toBe(true); + config.project.status = "frozen"; + expect(guidanceConfigHash(config)).not.toBe(hash); + writeFileSync(configPath, "{ invalid json"); + expect(validateProject(projectRoot)[0].status).toBe("fail"); + }); + + test("freeze status-only changes do not stale AGENTS hash", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-")); + const { projectRoot } = initProject({ name: "Freeze Valid", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); + freezeProject(projectRoot, cwd); + expect(validateProject(projectRoot).filter((c) => c.status === "fail")).toEqual([]); + }); + + test("repo validation fails hard when built CLI is missing", async () => { + const result = await runValidation(); + expect(result.checks.some((c) => c.id === "package.bin")).toBe(true); + expect(result.failed).toBe(result.checks.some((c) => c.status === "fail")); + }); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..6a4f1ec --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "noEmit": false, + "declaration": false, + "sourceMap": false + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "tests/**/*.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b976403 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "tests/**/*.ts" + ] +}