@gamecrate/cli 1.1.0 → 1.2.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/README.md CHANGED
@@ -20,22 +20,9 @@ npm install -g ./packages/cli
20
20
 
21
21
  The build needs [bun](https://bun.sh) and Node 22 or newer.
22
22
 
23
- ## Configuration
23
+ ## First launch
24
24
 
25
- Your config lives in `~/.config/gamecrate/`. If you set `XDG_CONFIG_HOME`, the directory moves
26
- with it. The file is named `profiles`, and gamecrate reads four suffixes:
27
-
28
- | Suffix | Format |
29
- | --- | --- |
30
- | `.yml` | YAML |
31
- | `.yaml` | YAML |
32
- | `.json` | JSON with comments and trailing commas |
33
- | `.jsonc` | JSON with comments and trailing commas |
34
-
35
- Keep one. Two config files in the same directory is an error, because silent precedence is how
36
- you edit the wrong file for twenty minutes.
37
-
38
- A minimal config for RimWorld:
25
+ Write `~/.config/gamecrate/profiles.yml`. This is the smallest RimWorld config that runs:
39
26
 
40
27
  ```yaml
41
28
  plugins: ['@gamecrate/rimworld']
@@ -56,373 +43,29 @@ games:
56
43
  mods: [brrainz.harmony, yourname.yourmod]
57
44
  ```
58
45
 
59
- Run `gamecrate config edit` to open the file in `$VISUAL`, or `$EDITOR` when `$VISUAL` is
60
- unset, and validate it on save. With nothing on disk yet, it creates `profiles.yml`.
61
-
62
- ### How plugins resolve
63
-
64
- `plugins` is an array of strings. gamecrate loads each one before it reads the rest of the
65
- file, because a plugin decides what a valid game block looks like.
66
-
67
- A string that starts with `.` or `/` is a path. gamecrate resolves it against the directory
68
- holding your config, so `./plugins/mygame` means `~/.config/gamecrate/plugins/mygame`. A `~`
69
- at the front expands to your home directory.
70
-
71
- Anything else is a package name. gamecrate walks `node_modules` upward from the config
72
- directory, the same way Node does. To use a bare name such as `@gamecrate/rimworld`, install it
73
- where that walk can find it:
46
+ The bare name `@gamecrate/rimworld` only resolves once the package is installed next to the
47
+ config. [Configuration](docs/configuration.md#how-plugins-resolve) shows both ways to point at
48
+ a plugin. Then:
74
49
 
75
50
  ```sh
76
- cd ~/.config/gamecrate
77
- npm init -y
78
- npm install @gamecrate/rimworld
79
- ```
80
-
81
- Each plugin claims one game name. Two plugins claiming the same name is a config error, and so
82
- is a `games` block whose name no plugin claims.
83
-
84
- ### An array you write replaces the plugin's array
85
-
86
- A plugin ships defaults for its game. Your `games.<name>` block merges on top of those
87
- defaults, key by key. Objects merge. Scalars overwrite.
88
-
89
- **Arrays do not concatenate.** The array you write replaces the plugin's array outright. Write
90
- three entries under `dlc` and the game has three, not the plugin's five plus your three. The
91
- same holds for `modes`, `scanRoots`, and `saveExtensions`. To add one DLC, copy the plugin's
92
- full list and append to it.
93
-
94
- The settings ladder is the one exception. `settings` merges through five layers: the top-level
95
- `defaults`, then `games.<game>`, then the profile, then the instance, then the command line.
96
- Arrays inside `settings`, which means `gameArgs` and `dockerArgs`, concatenate at every layer,
97
- unlike the `dlc`, `modes` and `scanRoots` lists in the preceding section, which replace.
98
-
99
- When a config error points at a key you never wrote, the message says which plugin's defaults
100
- supplied it.
101
-
102
- ### Profiles
103
-
104
- A profile names a mod set. `extends` inherits a parent profile's mods and appends its own.
105
- `exclude` drops entries by glob. `alias` and `aliases` give one profile extra names, and they
106
- share the parent's data directory. `instances` splits one profile into named sub-runs, each
107
- with its own saves, logs, lock, and container.
108
-
109
- `description` is one line saying what the profile is for. `gamecrate list` prints it on its own
110
- line under the profile, and `gamecrate list --json` carries it as a `description` field on that
111
- profile. It changes nothing about the launch.
112
-
113
- ```yaml
114
- profiles:
115
- dev:
116
- description: harmony plus the mod I am working on
117
- mods: [brrainz.harmony, yourname.yourmod]
118
- ```
119
-
120
- ```
121
- rimworld (headed, headless, screenshot)
122
- modless built-in: core + official DLC
123
- dev 2 entries
124
- harmony plus the mod I am working on
125
- ```
126
-
127
- A profile can also carry a default for three flags, so you stop typing them:
128
-
129
- | Key | Stands in for |
130
- | --- | --- |
131
- | `detach` | `--detach`. `--no-detach` overrides it. |
132
- | `replace` | `--replace`. `--no-replace` overrides it. |
133
- | `build` | `--build` and `--no-build`. Takes `auto`, `always`, or `never`. |
134
-
135
- gamecrate ships one profile of its own, `modless`. It resolves to the core game plus its
136
- official DLC, and you cannot redefine it.
137
-
138
- ### Per-directory defaults
139
-
140
- gamecrate looks for a `.gamecrate` file in the current directory and every parent. It takes the
141
- same four suffixes as the global config. `.gamecrate.yml`, `.gamecrate.yaml`,
142
- `.gamecrate.json`, and `.gamecrate.jsonc` all work, and two of them in one directory is the same
143
- error. Most keys stand in for a flag of the same name, camel-cased. A mod repo can pin its own
144
- game, profile, and extra Docker arguments:
145
-
146
- ```yaml
147
- game: rimworld
148
- defaultProfile: dev
149
- mode: headed
150
- detach: true
151
- dockerArgs: ['--cpus', '4']
51
+ gamecrate doctor
52
+ gamecrate rimworld dev
152
53
  ```
153
54
 
154
- A flag on the command line beats the file.
155
-
156
- Four keys are not flags:
157
-
158
- | Key | What it does |
159
- | --- | --- |
160
- | `game` | The game the rest of the file talks about. `profiles` and `settings` both need it. |
161
- | `defaultProfile` | The profile to use when you name none. Without it, the first key in `profiles` wins, and with neither, `modless` does. |
162
- | `profiles` | Profiles for `game`, written exactly like the ones in the global config. |
163
- | `settings` | A settings block assigned over `games.<game>.settings` at load time. |
164
-
165
- **A repo `settings:` block is assigned over `games.<game>.settings` when the config loads**,
166
- before anything resolves. It is not a sixth layer: by the time the ladder runs, there is one game
167
- block holding whatever the repo supplied.
168
-
169
- **A repo profile replaces a global profile of the same name outright.** It does not merge, so
170
- the global profile's `instances` and `aliases` are gone for that run. This is deliberate: a
171
- merge would leave the global profile's `mods` showing through the repo's shorter list. Give the
172
- repo profile a name of its own when you want both.
173
-
174
- The file is untrusted input. It ships inside any repo you clone, and `mods`, `use`, `worktree`,
175
- and now `profiles` can all name directories anywhere on your disk to bind-mount into the
176
- container.
177
- Read a stranger's `.gamecrate.yml` before you run gamecrate in their repo.
178
-
179
- ## Subcommands
55
+ `doctor` checks Docker, the image, the game directory, and the workshop root before anything
56
+ launches. The second line resolves the `dev` profile, stages its two mods, and starts the game.
57
+ `gamecrate rimworld dev --print-plan` shows what it would do without launching.
180
58
 
181
- The subcommand slot defaults to `run`, so `gamecrate rimworld dev` and
182
- `gamecrate run rimworld dev` do the same thing.
59
+ ## Documentation
183
60
 
184
- | Subcommand | What it does |
61
+ | Read this | When you want to |
185
62
  | --- | --- |
186
- | `run <game> [profile]` | Resolve, stage, and launch |
187
- | `list [game]` | Games, profiles, and where each profile came from |
188
- | `mods <game> [profile]` | The resolved mod set: source kind plus absolute path |
189
- | `doctor` | Preflight: Docker, CDI, registry auth, game dirs, scan roots, permissions |
190
- | `clean <game> [profile]` | Tiered wipe of a profile |
191
- | `clone <game> <src> <dst>` | Reflink-copy a profile's precious tier |
192
- | `logs <game> [profile]` | Print the last run's captured logs. `-f` follows the live run instead |
193
- | `attach <game> [profile]` | Stream a detached run's output from the start. Ctrl-C leaves the game running |
194
- | `wait <game> [profile]` | Block until a detached run ends, then exit with its code |
195
- | `ps` | Every live run: game, profile/instance, mode, pid, container, then uptime or status |
196
- | `stop <game> [profile]` | Stop a detached run and release its lock |
197
- | `build <game>` | Build or pull the runtime image, no launch |
198
- | `shell <game> [profile]` | Same mounts, bash instead of the game |
199
- | `verify <game> [profile]` | What the running container bound, and whether it looks current |
200
- | `config edit` | Open the global config in `$VISUAL` or `$EDITOR`, validate on save |
201
- | `fix-perms <game> [profile]` | Chown foreign-owned files back to the caller |
202
- | `help [topic]` | Help for a subcommand or a game |
203
- | `version` | Print the version |
204
-
205
- `gamecrate help <game>` lists that game's profiles and modes. `gamecrate help completion bash`
206
- and `gamecrate help completion zsh` print a shell completion script.
207
-
208
- Game arguments go after a bare `--` and nowhere else.
209
-
210
- ## Flags
211
-
212
- Mod set:
213
-
214
- - `--mod <id>` adds a mod to the profile set. Repeatable.
215
- - `--without <id>` drops a mod from the resolved set. Repeatable.
216
- - `--only <id>` restricts the resolved set to these mods. Repeatable.
217
- - `--use <packageId>=<path>` forces one mod to load from a directory. Repeatable.
218
- - `--sort <topo|none>` picks the profile order or a topological sort.
219
-
220
- Worktrees and instances:
221
-
222
- - `--worktree <path>` promotes mods from a linked git worktree, in its own instance. Repeatable,
223
- and earlier flags outrank later ones.
224
- - `--no-worktree` turns worktree promotion off completely. It ignores the current directory,
225
- `$GAMECRATE_WORKTREE`, any `--worktree` flag you also typed, and an instance's configured
226
- `worktree`.
227
- - `--instance <name>` runs under a named sub-profile with its own saves, logs, and container.
228
-
229
- Display and lifetime:
230
-
231
- - `--mode <headed|headless|screenshot>` chooses how the game displays.
232
- - `--resolution <width>x<height>` overrides the game resolution.
233
- - `--marker <str>` exits 0 as soon as that string appears in the log.
234
- - `--timeout <seconds>` bounds a run with a `--marker`, in any mode, and a `--mode headless`
235
- run without one. A `--mode screenshot` run with no marker is bounded by `--render-wait`
236
- instead, and a headed run by its window, so neither of those reads this.
237
- - `--render-wait <seconds>` sets the settle time before a screenshot.
238
-
239
- Container and build:
240
-
241
- - `--network <none|bridge|host>` sets the container network mode.
242
- - `--pull <always|missing|never>` decides when to pull the runtime image.
243
- - `--build` compiles local C# mods first. `--no-build` never compiles, even when an assembly
244
- looks stale.
245
- - `--no-stale-check` drops the warning about sources newer than assemblies. The check still runs.
246
- - `--docker-arg <arg>` adds one argv element to `docker run`. Repeatable.
247
- - `--root` runs as root instead of mapping your uid.
248
- - `--replace` stops whatever holds this profile and instance, then launches. `--no-replace`
249
- refuses instead.
250
- - `--detach` launches in the background. `--no-detach` stays in the foreground, whatever the
251
- profile or the repo config asks for.
252
-
253
- Output and dry runs:
254
-
255
- - `--dry-run` resolves and validates fully, then writes nothing.
256
- - `--print-plan` prints the resolved launch plan instead of launching.
257
- - `--log <path>` routes launch output to one file.
258
- - `--json` switches to machine-readable output.
259
- - `-f`, `--follow` keeps printing as the run writes. `logs` takes it.
260
-
261
- `clean` adds three tier flags: `--staging` (the default), `--logs`, and `--all`. `--all` deletes
262
- saves, so it needs `--yes`.
263
-
264
- ## Detached runs
265
-
266
- `--detach` re-executes gamecrate as a background supervisor. It writes the lock with that
267
- supervisor's pid, prints the container name, and gives you the prompt back. The
268
- supervisor owns the run from there. It pulls or builds the image, stages the mods, starts the
269
- container, and records the exit.
270
-
271
- Three things refuse a `--detach` you typed, and each says so:
272
-
273
- - `shell` needs the terminal that `--detach` gives up.
274
- - `--dry-run` has no run to supervise.
275
- - `--print-plan` has no run to supervise.
276
-
277
- The refusal is for the flag, not for detaching. A `detach: true` in a profile or a repo config
278
- is a default, so `shell`, `--dry-run` and `--print-plan` ignore it and run in the foreground
279
- without a word.
280
-
281
- A profile or a repo config can set `detach: true`. `--no-detach` is the way past that.
282
-
283
- ### Read the result with `wait`
284
-
285
- A detached launch returns `0` to your shell as soon as gamecrate writes the lock. That says
286
- the supervisor started, and nothing about how the game ended. **`wait` is how a script gets the
287
- real exit code.** It blocks until the run ends, then exits with that code:
288
-
289
- ```sh
290
- gamecrate rimworld dev --detach
291
- gamecrate wait rimworld dev
292
- echo $?
293
- ```
294
-
295
- gamecrate sends no desktop notification when a detached run fails. Nothing pops up and nothing
296
- writes to your terminal, so `wait`, `ps`, and the `last-exit.json` file are the only ways to
297
- learn about it. That file sits at `.gamecrate/last-exit.json` inside the instance directory,
298
- whose name depends on how gamecrate hashes the worktree path, so a script cannot compute it.
299
- Use `wait`.
300
-
301
- `wait` exits `2` when no run was ever recorded, and `7` when the lock holder died without
302
- recording an exit. `7` means the lock is stale: `gamecrate stop <game> <profile>` clears it.
303
-
304
- ### Watch a run that is already going
305
-
306
- ```sh
307
- gamecrate ps
308
- ```
309
-
310
- ```
311
- rimworld mpf/simulator-test headless 1474870 gamecrate-rimworld-mpf-simulator-test Up 7 minutes
312
- ```
313
-
314
- The columns are game, profile with its instance, mode, supervisor pid, container, and
315
- uptime. A run that holds a lock but has no container yet reads `starting`, because staging and
316
- the image come before `docker run`. A lock with no live supervisor reads `orphaned`, and
317
- `ps` tells you to run `stop`.
318
-
319
- `gamecrate attach <game> <profile>` streams that run's captured output from the beginning of
320
- the log. Ctrl-C stops the stream and leaves the game running. `gamecrate logs <game> <profile>
321
- -f` follows the same log from the end instead.
322
-
323
- `gamecrate stop <game> <profile>` signals the supervisor and waits for the lock to be released.
324
- It clears the lock itself only when the holder died first; a live supervisor releases its own.
325
- It exits `7` when the lock still names a live process after the wait, and it does not look at
326
- the container to decide that.
327
-
328
- A run whose supervisor is already dead takes a different path: `stop` stops the container
329
- itself, clears the stale lock, and exits `0`.
330
-
331
- ## Headed runs on X11
332
-
333
- A headed run retitles its own window and fixes what the window manager knows about it. That
334
- needs two programs on the host.
335
-
336
- **`xprop` is load-bearing.** gamecrate writes its own pid onto the window it adopts, then reads
337
- that pid back. That is how it tells its window apart from another run's. Without `xprop` it can
338
- do neither, so two concurrent headed runs adopt the same window. Destroying that one window
339
- tears down both containers, and the second run loses an unsaved game.
340
-
341
- The titlebar X is the one close route that stays harmless without `xprop`. RimWorld claims
342
- `WM_DELETE_WINDOW` and then ignores it, so that button does nothing until gamecrate strips the
343
- claim. Stripping it takes `xprop`. Every other route to destroying the window, such as a window
344
- manager shortcut or `xkill`, fires both teardowns.
345
-
346
- With `xprop` installed, the strip is what makes the titlebar X end the run. The window manager
347
- destroys the window, gamecrate sees it go, and stops that run's container.
348
-
349
- A missing `wmctrl` costs more than the title. gamecrate warns and then skips the whole window
350
- step, because the snapshot it needs comes from `wmctrl`. So the `WM_DELETE_WINDOW` strip never
351
- runs even with `xprop` installed, the titlebar X goes back to doing nothing, and closing the
352
- window no longer stops the container.
353
-
354
- ## Environment variables
355
-
356
- Each variable stands in for one flag, and only when you leave that flag off:
357
-
358
- `GAMECRATE_INSTANCE`, `GAMECRATE_MODE`, `GAMECRATE_MARKER`, `GAMECRATE_TIMEOUT`,
359
- `GAMECRATE_RENDER_WAIT`, `GAMECRATE_NETWORK`, `GAMECRATE_PULL`, `GAMECRATE_BUILD`,
360
- `GAMECRATE_SORT`, `GAMECRATE_ROOT`.
361
-
362
- `GAMECRATE_WORKTREE` names a worktree to promote, and `--no-worktree` cancels it.
363
-
364
- ## Exit codes
365
-
366
- - `0` success
367
- - `1` the game itself failed
368
- - `2` usage error
369
- - `3` config error
370
- - `4` resolution error
371
- - `5` environment problem, such as a missing Docker
372
- - `6` the marker never appeared before the timeout
373
- - `7` refused, because this profile and instance already run
374
- - `8` `verify` found a stale mod
375
- - `130` interrupted
376
-
377
- `wait` exits with whatever code the detached run recorded, which is any code in this list
378
- except `8`, since only `verify` returns that and `verify` is never supervised.
379
- A supervisor that fails before Docker records `3`, `4`, `5` or `7`.
380
- `wait` hands that code back unchanged.
381
- So a script must handle the whole list, not only the four codes a finished game uses.
382
-
383
- ## Write a plugin
384
-
385
- A plugin is an ES module with a default export that satisfies `GamePlugin`. It takes plain data
386
- and throws plain `Error` objects, so it never imports the gamecrate runtime at run time.
387
-
388
- ```ts
389
- import type { GamePlugin } from '@gamecrate/cli'
390
-
391
- const plugin: GamePlugin = {
392
- apiVersion: 1,
393
- game: 'mygame',
394
- defaults: { /* Partial<GameConfig> */ },
395
- parseManifest: (text) => null,
396
- renderModsConfig: (input) => '',
397
- mergePrefs: (existing, owned) => '',
398
- windowedPrefs: { fullscreen: 'False' },
399
- parseVersion: (text) => null,
400
- }
401
-
402
- export default plugin
403
- ```
404
-
405
- The members:
406
-
407
- - `apiVersion` must equal `PLUGIN_API_VERSION`, which is `1`. A mismatch fails the load with a
408
- message naming both numbers.
409
- - `game` is the word the command line answers to, such as `rimworld`.
410
- - `defaults` is a `Partial<GameConfig>`. It holds facts about the game itself, never about one
411
- machine. Leave install paths, workshop roots, scan roots, and images to the user.
412
- - `parseManifest(text)` reads one mod manifest and returns a `ModManifest`, or `null` when the
413
- file is not a manifest at all. A malformed manifest throws.
414
- - `renderModsConfig(input)` takes a version, a build number, the active package ids in load
415
- order, and the known expansions, then returns the file the engine reads.
416
- - `mergePrefs(existing, owned)` folds the keys gamecrate owns into the player's own prefs file.
417
- `existing` is `null` on the first run.
418
- - `windowedPrefs` lists the prefs keys that put the game in a window rather than fullscreen.
419
- - `parseVersion(text)` reads the engine's own version file and returns a version string and a
420
- build number, or `null` when the text does not parse.
421
-
422
- Load your plugin by path while you develop it:
423
-
424
- ```yaml
425
- plugins: [~/projects/gamecrate-mygame/dist/index.js]
426
- ```
427
-
428
- [`@gamecrate/rimworld`](../rimworld) is a complete example in about fifty lines.
63
+ | [Configuration](docs/configuration.md) | Write the config file, load a plugin, define profiles, or set per-repository defaults |
64
+ | [Mod sources](docs/mod-sources.md) | Pin a mod to a directory, a workshop item, or a git repository, and learn which copy of a mod wins |
65
+ | [The `mods` commands](docs/mods-commands.md) | Add, remove, and sync library pins from the command line |
66
+ | [Running](docs/running.md) | Launch, run in the background, read the result, close a window, and clean up |
67
+ | [Reference](docs/reference.md) | Every subcommand, flag, environment variable, and exit code |
68
+ | [Writing a plugin](docs/plugins.md) | Teach gamecrate a new game |
69
+
70
+ [`@gamecrate/rimworld`](../rimworld) is the RimWorld plugin, and the one complete example of
71
+ the plugin contract.