Monorepo of Nexus Mods Vortex game extensions, built from a single shared GDL (Game Description Language) toolchain.
Each game is a declarative game.yaml plus a gameart.webp logo under games/<id>/ (and an
optional src/hooks.ts for game-specific logic such as version detection or deploy hooks). GDL
compiles that into a bundled Vortex extension. There is one copy of the GDL toolchain for the
whole repo (the gdl/ submodule), and one set of orchestration / CI / packaging config at the
root — no per-game package.json, vitest.config, or workflow files.
Task running is handled by Nx: each games/*/game.yaml is detected as an Nx
project (via an inference plugin — no per-game config), giving cached, parallel build/test/
package targets and nx affected.
gdl-games/
├── gdl/ # shared GDL toolchain (git submodule, built once)
├── games/ # one folder per game (007firstlight, gothic1remake,
│ ├── solarpunk/ # paralives, solarpunk, subnautica2)
│ │ ├── game.yaml # the game definition (incl. top-level `version:`)
│ │ └── gameart.webp # the logo
│ └── subnautica2/ # games needing custom logic also have:
│ └── src/hooks.ts # version detection / deploy hooks
├── tools/nx/gdl-plugin.js # Nx inference plugin: game.yaml → project + targets
├── nx.json # Nx caching inputs/outputs + targetDefaults
├── vitest.config.ts # one config that tests every game
├── tsconfig.base.json # minimal stub — required for Nx local-plugin resolution
├── .claude/skills/ # /implement-game-extension skill (scaffolds a new game)
├── .github/workflows/ci.yml # build/test (Nx affected) + release on version bump
└── package.json # root scripts (wrap nx)
git clone --recurse-submodules <repo-url>
cd gdl-games
pnpm init-gdl # install + build the shared GDL toolchain (run once, or after a gdl bump)
pnpm install # install root dev deps (nx, vitest)If you already cloned without submodules: git submodule update --init --recursive.
pnpm build # nx run-many -t build — build every game (cached)
pnpm test # nx run-many -t test — build + run every game's generated tests
pnpm package # nx run-many -t package — zip games/<id>/out/<id>-vortex-v<version>.zip
pnpm test:corpus # nx run-many -t test-corpus — installer rules vs cached Nexus manifestsNx caches each target by game.yaml + src/** + gameart.webp + the gdl toolchain commit, so
unchanged games are restored from cache. Use pnpm affected to act only on games touched by your
changes, e.g. pnpm nx affected -t build test.
Nx is a local dependency, so run it via pnpm (or npx nx / pnpm exec nx). Bare nx ... only
works if Nx is installed globally.
pnpm nx run solarpunk:build
pnpm nx run solarpunk:test
pnpm nx run solarpunk:package
pnpm nx run solarpunk:test-corpus # corpus vs cached manifests
pnpm nx run solarpunk:test-corpus -- --fetch # fetch fresh from Nexus (needs NEXUS_API_KEY)The Nx target is test-corpus (hyphen) because Nx target names can't contain : (it separates
project:target); the underlying gdl CLI subcommand is still test:corpus.
Or from inside the game folder, call the shared toolchain directly (bypassing Nx):
cd games/solarpunk
node ../../gdl/dist/cli.js build
node ../../gdl/dist/cli.js package
node ../../gdl/dist/cli.js test:corpus --fetch # needs NEXUS_API_KEYCorpus testing pulls every published mod's file-listing for a game's nexusDomain from the Nexus
API into games/<id>/tests/cache/ (git-ignored) and runs the installer rules + validators
against them. It needs a NEXUS_API_KEY env var and is a local check — it is not wired into CI.
pnpm nx run solarpunk:test-corpus -- --fetch # one game: fetch fresh, then check
pnpm test:corpus # all games: check against cached manifestsOr run it raw from inside the game folder: node ../../gdl/dist/cli.js test:corpus --fetch.
Releases are gated on a version bump — there are no hand-typed tags. To ship a game:
- Bump the top-level
version:ingames/<id>/game.yaml. - Make sure that
game.yamlhas anexus:block with real ids and real store ids (noPLACEHOLDER/0). - Merge to
main.
CI then walks every game, and for each one whose version has no matching <id>-v<version> git
tag yet, it packages the extension, creates a GitHub release (the tag doubles as the
"already-published" ledger), and uploads to Nexus Mods. Games with an unchanged version are
skipped. A placeholder guard refuses to publish any game whose game.yaml still contains stub ids.
The guided path is the /implement-game-extension skill: give it the Nexus site/extension id
and the game name, and it researches the game, writes game.yaml, fetches the art, supports every
mod currently on the game's Nexus page, and gets the build green.
To do it by hand: create games/<id>/game.yaml and drop in a games/<id>/gameart.webp. That's it
— no package.json, no vitest.config, no workflow (add a src/hooks.ts only if the game needs
version-detection or deploy hooks). The root scripts and CI pick up any games/*/game.yaml
automatically.
A minimal game.yaml:
gdl: 1
version: 0.0.1
game:
id: <id>
name: <Human Friendly Name>
executable: <id>.exe
requiredFiles: [<id>.exe]
logo: gameart.webp
nexusDomain: <id>
stores:
steam: "<steam app id>"
modTypes: []
installers: []
tests:
corpus: off
cases: []From inside games/<id>/, node ../../gdl/dist/cli.js init <id> -n "Human Friendly Name"
scaffolds the starting game.yaml (root-driven — it emits only game.yaml, with a top-level
version:). Add a nexus: block only once the extension's Nexus page exists (GDL rejects 0/
placeholder nexus ids at build time).