@gamecrate/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +257 -0
- package/dist/gamecrate.js +4365 -0
- package/package.json +53 -0
- package/src/cli/args.ts +592 -0
- package/src/cli/help.ts +193 -0
- package/src/cli/output.ts +246 -0
- package/src/config/builtin.ts +19 -0
- package/src/config/jsonc.ts +21 -0
- package/src/config/load.ts +387 -0
- package/src/config/validate.ts +0 -0
- package/src/docker/identity.ts +25 -0
- package/src/docker/preflight.ts +243 -0
- package/src/docker/run.ts +212 -0
- package/src/docker/spec.ts +357 -0
- package/src/docker/window.ts +152 -0
- package/src/index.ts +875 -0
- package/src/launch/generate.ts +151 -0
- package/src/launch/instance.ts +106 -0
- package/src/launch/prepare.ts +332 -0
- package/src/launch/resolve.ts +383 -0
- package/src/launch/stage.ts +97 -0
- package/src/lib.ts +22 -0
- package/src/mods/modindex.ts +539 -0
- package/src/mods/staleness.ts +125 -0
- package/src/mods/worktree.ts +107 -0
- package/src/plugin.ts +152 -0
- package/src/types.ts +423 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aaron Scherer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# @gamecrate/cli
|
|
2
|
+
|
|
3
|
+
The `gamecrate` command. It resolves a profile to a mod set, stages that set, and launches the
|
|
4
|
+
game in a Docker container.
|
|
5
|
+
|
|
6
|
+
The core knows nothing about any one game. A plugin supplies the file formats and the engine
|
|
7
|
+
facts. Without at least one plugin, `gamecrate` has no games to run.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
The package is not on npm yet, so build it from a clone of the monorepo:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
git clone https://github.com/RimWorks/gamecrate.git
|
|
15
|
+
cd gamecrate
|
|
16
|
+
npm install
|
|
17
|
+
npm run build
|
|
18
|
+
npm install -g ./packages/cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The build needs [bun](https://bun.sh) and Node 22 or newer.
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
Your config lives at `~/.config/gamecrate/profiles.json`. If you set `XDG_CONFIG_HOME`, the
|
|
26
|
+
path moves with it. The file accepts JSONC, so comments and trailing commas are fine.
|
|
27
|
+
|
|
28
|
+
A minimal config for RimWorld:
|
|
29
|
+
|
|
30
|
+
```jsonc
|
|
31
|
+
{
|
|
32
|
+
"plugins": ["@gamecrate/rimworld"],
|
|
33
|
+
"games": {
|
|
34
|
+
"rimworld": {
|
|
35
|
+
// The plugin already says source: "mount" and container: "/game".
|
|
36
|
+
"gameFiles": { "host": "~/games/RimWorld" },
|
|
37
|
+
"image": { "ref": "ghcr.io/your-org/rimworld:1.6", "acquire": "pull" },
|
|
38
|
+
"workshopRoot": "~/.steam/steam/steamapps/workshop/content/294100",
|
|
39
|
+
"scanRoots": [{ "path": "~/projects/mods", "maxDepth": 2 }],
|
|
40
|
+
"profiles": {
|
|
41
|
+
"dev": { "mods": ["brrainz.harmony", "yourname.yourmod"] }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Run `gamecrate config edit` to open the file in `$EDITOR` and validate it on save.
|
|
49
|
+
|
|
50
|
+
### How plugins resolve
|
|
51
|
+
|
|
52
|
+
`plugins` is an array of strings. gamecrate loads each one before it reads the rest of the
|
|
53
|
+
file, because a plugin decides what a valid game block looks like.
|
|
54
|
+
|
|
55
|
+
A string that starts with `.` or `/` is a path. gamecrate resolves it against the directory
|
|
56
|
+
holding your config, so `./plugins/mygame` means `~/.config/gamecrate/plugins/mygame`. A `~`
|
|
57
|
+
at the front expands to your home directory.
|
|
58
|
+
|
|
59
|
+
Anything else is a package name. gamecrate walks `node_modules` upward from the config
|
|
60
|
+
directory, the same way Node does. To use a bare name such as `@gamecrate/rimworld`, install it
|
|
61
|
+
where that walk can find it:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
cd ~/.config/gamecrate
|
|
65
|
+
npm init -y
|
|
66
|
+
npm install @gamecrate/rimworld
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Each plugin claims one game name. Two plugins claiming the same name is a config error, and so
|
|
70
|
+
is a `games` block whose name no plugin claims.
|
|
71
|
+
|
|
72
|
+
### An array you write replaces the plugin's array
|
|
73
|
+
|
|
74
|
+
A plugin ships defaults for its game. Your `games.<name>` block merges on top of those
|
|
75
|
+
defaults, key by key. Objects merge. Scalars overwrite.
|
|
76
|
+
|
|
77
|
+
**Arrays do not concatenate.** The array you write replaces the plugin's array outright. Write
|
|
78
|
+
three entries under `dlc` and the game has three, not the plugin's five plus your three. The
|
|
79
|
+
same holds for `modes`, `scanRoots`, and `saveExtensions`. To add one DLC, copy the plugin's
|
|
80
|
+
full list and append to it.
|
|
81
|
+
|
|
82
|
+
The settings ladder is the one exception. `settings` merges through five layers: the top-level
|
|
83
|
+
`defaults`, then `games.<game>`, then the profile, then the instance, then the command line.
|
|
84
|
+
Arrays inside `settings`, which means `gameArgs` and `dockerArgs`, concatenate at every layer.
|
|
85
|
+
|
|
86
|
+
When a config error points at a key you never wrote, the message says which plugin's defaults
|
|
87
|
+
supplied it.
|
|
88
|
+
|
|
89
|
+
### Profiles
|
|
90
|
+
|
|
91
|
+
A profile names a mod set. `extends` inherits a parent profile's mods and appends its own.
|
|
92
|
+
`exclude` drops entries by glob. `alias` and `aliases` give one profile extra names, and they
|
|
93
|
+
share the parent's data directory. `instances` splits one profile into named sub-runs, each
|
|
94
|
+
with its own saves, logs, lock, and container.
|
|
95
|
+
|
|
96
|
+
gamecrate ships one profile of its own, `modless`. It resolves to the core game plus its
|
|
97
|
+
official DLC, and you cannot redefine it.
|
|
98
|
+
|
|
99
|
+
### Per-directory defaults
|
|
100
|
+
|
|
101
|
+
gamecrate looks for a `.gamecrate.yml` file in the current directory and every parent. Keys in
|
|
102
|
+
it stand in for flags you would otherwise type, so a mod repo can pin its own game, profile,
|
|
103
|
+
and extra Docker arguments:
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
game: rimworld
|
|
107
|
+
profile: dev
|
|
108
|
+
mode: headed
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A flag on the command line beats the file.
|
|
112
|
+
|
|
113
|
+
## Subcommands
|
|
114
|
+
|
|
115
|
+
The subcommand slot defaults to `run`, so `gamecrate rimworld dev` and
|
|
116
|
+
`gamecrate run rimworld dev` do the same thing.
|
|
117
|
+
|
|
118
|
+
| Subcommand | What it does |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `run <game> [profile]` | Resolve, stage, and launch |
|
|
121
|
+
| `list [game]` | Games, profiles, and where each profile came from |
|
|
122
|
+
| `mods <game> [profile]` | The resolved mod set: source kind plus absolute path |
|
|
123
|
+
| `doctor` | Preflight: Docker, CDI, registry auth, game dirs, scan roots, permissions |
|
|
124
|
+
| `clean <game> <profile>` | Tiered wipe of a profile |
|
|
125
|
+
| `clone <game> <src> <dst>` | Reflink-copy a profile's precious tier |
|
|
126
|
+
| `logs <game> <profile>` | Print the last run's captured logs |
|
|
127
|
+
| `build <game>` | Build or pull the runtime image, no launch |
|
|
128
|
+
| `shell <game> [profile]` | Same mounts, bash instead of the game |
|
|
129
|
+
| `verify <game> [profile]` | What the running container bound, and whether it looks current |
|
|
130
|
+
| `config edit` | Open `profiles.json` in `$EDITOR`, validate on save |
|
|
131
|
+
| `fix-perms <game> [profile]` | Chown foreign-owned files back to the caller |
|
|
132
|
+
| `help [topic]` | Help for a subcommand or a game |
|
|
133
|
+
| `version` | Print the version |
|
|
134
|
+
|
|
135
|
+
`gamecrate help <game>` lists that game's profiles and modes. `gamecrate help completion bash`
|
|
136
|
+
and `gamecrate help completion zsh` print a shell completion script.
|
|
137
|
+
|
|
138
|
+
Game arguments go after a bare `--` and nowhere else.
|
|
139
|
+
|
|
140
|
+
## Flags
|
|
141
|
+
|
|
142
|
+
Mod set:
|
|
143
|
+
|
|
144
|
+
- `--mod <id>` adds a mod to the profile set. Repeatable.
|
|
145
|
+
- `--without <id>` drops a mod from the resolved set. Repeatable.
|
|
146
|
+
- `--only <id>` restricts the resolved set to these mods. Repeatable.
|
|
147
|
+
- `--use <packageId>=<path>` forces one mod to load from a directory. Repeatable.
|
|
148
|
+
- `--sort <topo|none>` picks the profile order or a topological sort.
|
|
149
|
+
|
|
150
|
+
Worktrees and instances:
|
|
151
|
+
|
|
152
|
+
- `--worktree <path>` promotes mods from a linked git worktree, in its own instance. Repeatable,
|
|
153
|
+
and earlier flags outrank later ones.
|
|
154
|
+
- `--no-worktree` ignores the current directory and `$GAMECRATE_WORKTREE`.
|
|
155
|
+
- `--instance <name>` runs under a named sub-profile with its own saves, logs, and container.
|
|
156
|
+
|
|
157
|
+
Display and lifetime:
|
|
158
|
+
|
|
159
|
+
- `--mode <headed|headless|screenshot>` chooses how the game displays.
|
|
160
|
+
- `--resolution <width>x<height>` overrides the game resolution.
|
|
161
|
+
- `--marker <str>` exits 0 as soon as that string appears in the log.
|
|
162
|
+
- `--timeout <seconds>` kills the container after that long.
|
|
163
|
+
- `--render-wait <seconds>` sets the settle time before a screenshot.
|
|
164
|
+
|
|
165
|
+
Container and build:
|
|
166
|
+
|
|
167
|
+
- `--network <none|bridge|host>` sets the container network mode.
|
|
168
|
+
- `--pull <always|missing|never>` decides when to pull the runtime image.
|
|
169
|
+
- `--build` compiles local C# mods first. `--no-build` never compiles, even when an assembly
|
|
170
|
+
looks stale.
|
|
171
|
+
- `--no-stale-check` drops the warning about sources newer than assemblies. The check still runs.
|
|
172
|
+
- `--docker-arg <arg>` adds one argv element to `docker run`. Repeatable.
|
|
173
|
+
- `--root` runs as root instead of mapping your uid.
|
|
174
|
+
- `--replace` stops whatever holds this profile and instance, then launches. `--no-replace`
|
|
175
|
+
refuses instead.
|
|
176
|
+
|
|
177
|
+
Output and dry runs:
|
|
178
|
+
|
|
179
|
+
- `--dry-run` resolves and validates fully, then writes nothing.
|
|
180
|
+
- `--print-plan` prints the resolved launch plan instead of launching.
|
|
181
|
+
- `--log <path>` routes launch output to one file.
|
|
182
|
+
- `--json` switches to machine-readable output.
|
|
183
|
+
|
|
184
|
+
`clean` adds three tier flags: `--staging` (the default), `--logs`, and `--all`. `--all` deletes
|
|
185
|
+
saves, so it needs `--yes`.
|
|
186
|
+
|
|
187
|
+
## Environment variables
|
|
188
|
+
|
|
189
|
+
Each variable stands in for one flag, and only when you leave that flag off:
|
|
190
|
+
|
|
191
|
+
`GAMECRATE_INSTANCE`, `GAMECRATE_MODE`, `GAMECRATE_MARKER`, `GAMECRATE_TIMEOUT`,
|
|
192
|
+
`GAMECRATE_RENDER_WAIT`, `GAMECRATE_NETWORK`, `GAMECRATE_PULL`, `GAMECRATE_BUILD`,
|
|
193
|
+
`GAMECRATE_SORT`, `GAMECRATE_ROOT`.
|
|
194
|
+
|
|
195
|
+
`GAMECRATE_WORKTREE` names a worktree to promote, and `--no-worktree` cancels it.
|
|
196
|
+
|
|
197
|
+
## Exit codes
|
|
198
|
+
|
|
199
|
+
- `0` success
|
|
200
|
+
- `1` the game itself failed
|
|
201
|
+
- `2` usage error
|
|
202
|
+
- `3` config error
|
|
203
|
+
- `4` resolution error
|
|
204
|
+
- `5` environment problem, such as a missing Docker
|
|
205
|
+
- `6` the marker never appeared before the timeout
|
|
206
|
+
- `7` refused, because this profile and instance already run
|
|
207
|
+
- `8` `verify` found a stale mod
|
|
208
|
+
- `130` interrupted
|
|
209
|
+
|
|
210
|
+
## Write a plugin
|
|
211
|
+
|
|
212
|
+
A plugin is an ES module with a default export that satisfies `GamePlugin`. It takes plain data
|
|
213
|
+
and throws plain `Error` objects, so it never imports the gamecrate runtime at run time.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import type { GamePlugin } from '@gamecrate/cli'
|
|
217
|
+
|
|
218
|
+
const plugin: GamePlugin = {
|
|
219
|
+
apiVersion: 1,
|
|
220
|
+
game: 'mygame',
|
|
221
|
+
defaults: { /* Partial<GameConfig> */ },
|
|
222
|
+
parseManifest: (text) => null,
|
|
223
|
+
renderModsConfig: (input) => '',
|
|
224
|
+
mergePrefs: (existing, owned) => '',
|
|
225
|
+
windowedPrefs: { fullscreen: 'False' },
|
|
226
|
+
parseVersion: (text) => null,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export default plugin
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The members:
|
|
233
|
+
|
|
234
|
+
- `apiVersion` must equal `PLUGIN_API_VERSION`, which is `1`. A mismatch fails the load with a
|
|
235
|
+
message naming both numbers.
|
|
236
|
+
- `game` is the word the command line answers to, such as `rimworld`.
|
|
237
|
+
- `defaults` is a `Partial<GameConfig>`. It holds facts about the game itself, never about one
|
|
238
|
+
machine. Leave install paths, workshop roots, scan roots, and images to the user.
|
|
239
|
+
- `parseManifest(text)` reads one mod manifest and returns a `ModManifest`, or `null` when the
|
|
240
|
+
file is not a manifest at all. A malformed manifest throws.
|
|
241
|
+
- `renderModsConfig(input)` takes a version, a build number, the active package ids in load
|
|
242
|
+
order, and the known expansions, then returns the file the engine reads.
|
|
243
|
+
- `mergePrefs(existing, owned)` folds the keys gamecrate owns into the player's own prefs file.
|
|
244
|
+
`existing` is `null` on the first run.
|
|
245
|
+
- `windowedPrefs` lists the prefs keys that put the game in a window rather than fullscreen.
|
|
246
|
+
- `parseVersion(text)` reads the engine's own version file and returns a version string and a
|
|
247
|
+
build number, or `null` when the text does not parse.
|
|
248
|
+
|
|
249
|
+
Load your plugin by path while you develop it:
|
|
250
|
+
|
|
251
|
+
```jsonc
|
|
252
|
+
{
|
|
253
|
+
"plugins": ["~/projects/gamecrate-mygame/dist/index.js"]
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
[`@gamecrate/rimworld`](../rimworld) is a complete example in about fifty lines.
|