@gamecrate/cli 0.1.0 → 1.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.
Files changed (58) hide show
  1. package/README.md +204 -33
  2. package/dist/gamecrate.js +1486 -715
  3. package/dist/lib.js +320 -0
  4. package/dist/types/cli/args.d.ts +56 -0
  5. package/dist/types/cli/game.d.ts +2 -0
  6. package/dist/types/cli/help.d.ts +7 -0
  7. package/dist/types/cli/list.d.ts +2 -0
  8. package/dist/types/cli/output.d.ts +78 -0
  9. package/dist/types/cli/profile.d.ts +6 -0
  10. package/dist/types/config/builtin.d.ts +3 -0
  11. package/dist/types/config/jsonc.d.ts +5 -0
  12. package/dist/types/config/load.d.ts +47 -0
  13. package/dist/types/config/read.d.ts +11 -0
  14. package/dist/types/config/validate.d.ts +12 -0
  15. package/dist/types/docker/identity.d.ts +6 -0
  16. package/dist/types/docker/preflight.d.ts +3 -0
  17. package/dist/types/docker/run.d.ts +39 -0
  18. package/dist/types/docker/spec.d.ts +24 -0
  19. package/dist/types/docker/window.d.ts +50 -0
  20. package/dist/types/index.d.ts +2 -0
  21. package/dist/types/launch/generate.d.ts +8 -0
  22. package/dist/types/launch/instance.d.ts +20 -0
  23. package/dist/types/launch/prepare.d.ts +87 -0
  24. package/dist/types/launch/resolve.d.ts +17 -0
  25. package/dist/types/launch/stage.d.ts +13 -0
  26. package/dist/types/launch/supervisor.d.ts +48 -0
  27. package/dist/types/lib.d.ts +3 -0
  28. package/dist/types/mods/modindex.d.ts +29 -0
  29. package/dist/types/mods/staleness.d.ts +28 -0
  30. package/dist/types/mods/worktree.d.ts +18 -0
  31. package/dist/types/plugin.d.ts +35 -0
  32. package/dist/types/run/registry.d.ts +22 -0
  33. package/dist/types/types.d.ts +428 -0
  34. package/package.json +15 -10
  35. package/src/cli/args.ts +0 -592
  36. package/src/cli/help.ts +0 -193
  37. package/src/cli/output.ts +0 -246
  38. package/src/config/builtin.ts +0 -19
  39. package/src/config/jsonc.ts +0 -21
  40. package/src/config/load.ts +0 -387
  41. package/src/config/validate.ts +0 -0
  42. package/src/docker/identity.ts +0 -25
  43. package/src/docker/preflight.ts +0 -243
  44. package/src/docker/run.ts +0 -212
  45. package/src/docker/spec.ts +0 -357
  46. package/src/docker/window.ts +0 -152
  47. package/src/index.ts +0 -875
  48. package/src/launch/generate.ts +0 -151
  49. package/src/launch/instance.ts +0 -106
  50. package/src/launch/prepare.ts +0 -332
  51. package/src/launch/resolve.ts +0 -383
  52. package/src/launch/stage.ts +0 -97
  53. package/src/lib.ts +0 -22
  54. package/src/mods/modindex.ts +0 -539
  55. package/src/mods/staleness.ts +0 -125
  56. package/src/mods/worktree.ts +0 -107
  57. package/src/plugin.ts +0 -152
  58. package/src/types.ts +0 -423
package/README.md CHANGED
@@ -22,30 +22,42 @@ The build needs [bun](https://bun.sh) and Node 22 or newer.
22
22
 
23
23
  ## Configuration
24
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.
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.
27
37
 
28
38
  A minimal config for RimWorld:
29
39
 
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
- }
40
+ ```yaml
41
+ plugins: ['@gamecrate/rimworld']
42
+ games:
43
+ rimworld:
44
+ # The plugin already says source: mount and container: /game.
45
+ gameFiles:
46
+ host: ~/games/RimWorld
47
+ image:
48
+ ref: ghcr.io/your-org/rimworld:1.6
49
+ acquire: pull
50
+ workshopRoot: ~/.steam/steam/steamapps/workshop/content/294100
51
+ scanRoots:
52
+ - path: ~/projects/mods
53
+ maxDepth: 2
54
+ profiles:
55
+ dev:
56
+ mods: [brrainz.harmony, yourname.yourmod]
46
57
  ```
47
58
 
48
- Run `gamecrate config edit` to open the file in `$EDITOR` and validate it on save.
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`.
49
61
 
50
62
  ### How plugins resolve
51
63
 
@@ -81,7 +93,8 @@ full list and append to it.
81
93
 
82
94
  The settings ladder is the one exception. `settings` merges through five layers: the top-level
83
95
  `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.
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.
85
98
 
86
99
  When a config error points at a key you never wrote, the message says which plugin's defaults
87
100
  supplied it.
@@ -93,23 +106,76 @@ A profile names a mod set. `extends` inherits a parent profile's mods and append
93
106
  share the parent's data directory. `instances` splits one profile into named sub-runs, each
94
107
  with its own saves, logs, lock, and container.
95
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
+
96
135
  gamecrate ships one profile of its own, `modless`. It resolves to the core game plus its
97
136
  official DLC, and you cannot redefine it.
98
137
 
99
138
  ### Per-directory defaults
100
139
 
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:
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:
104
145
 
105
146
  ```yaml
106
147
  game: rimworld
107
- profile: dev
148
+ defaultProfile: dev
108
149
  mode: headed
150
+ detach: true
151
+ dockerArgs: ['--cpus', '4']
109
152
  ```
110
153
 
111
154
  A flag on the command line beats the file.
112
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
+
113
179
  ## Subcommands
114
180
 
115
181
  The subcommand slot defaults to `run`, so `gamecrate rimworld dev` and
@@ -121,13 +187,17 @@ The subcommand slot defaults to `run`, so `gamecrate rimworld dev` and
121
187
  | `list [game]` | Games, profiles, and where each profile came from |
122
188
  | `mods <game> [profile]` | The resolved mod set: source kind plus absolute path |
123
189
  | `doctor` | Preflight: Docker, CDI, registry auth, game dirs, scan roots, permissions |
124
- | `clean <game> <profile>` | Tiered wipe of a profile |
190
+ | `clean <game> [profile]` | Tiered wipe of a profile |
125
191
  | `clone <game> <src> <dst>` | Reflink-copy a profile's precious tier |
126
- | `logs <game> <profile>` | Print the last run's captured logs |
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 |
127
197
  | `build <game>` | Build or pull the runtime image, no launch |
128
198
  | `shell <game> [profile]` | Same mounts, bash instead of the game |
129
199
  | `verify <game> [profile]` | What the running container bound, and whether it looks current |
130
- | `config edit` | Open `profiles.json` in `$EDITOR`, validate on save |
200
+ | `config edit` | Open the global config in `$VISUAL` or `$EDITOR`, validate on save |
131
201
  | `fix-perms <game> [profile]` | Chown foreign-owned files back to the caller |
132
202
  | `help [topic]` | Help for a subcommand or a game |
133
203
  | `version` | Print the version |
@@ -151,7 +221,9 @@ Worktrees and instances:
151
221
 
152
222
  - `--worktree <path>` promotes mods from a linked git worktree, in its own instance. Repeatable,
153
223
  and earlier flags outrank later ones.
154
- - `--no-worktree` ignores the current directory and `$GAMECRATE_WORKTREE`.
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`.
155
227
  - `--instance <name>` runs under a named sub-profile with its own saves, logs, and container.
156
228
 
157
229
  Display and lifetime:
@@ -159,7 +231,9 @@ Display and lifetime:
159
231
  - `--mode <headed|headless|screenshot>` chooses how the game displays.
160
232
  - `--resolution <width>x<height>` overrides the game resolution.
161
233
  - `--marker <str>` exits 0 as soon as that string appears in the log.
162
- - `--timeout <seconds>` kills the container after that long.
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.
163
237
  - `--render-wait <seconds>` sets the settle time before a screenshot.
164
238
 
165
239
  Container and build:
@@ -173,6 +247,8 @@ Container and build:
173
247
  - `--root` runs as root instead of mapping your uid.
174
248
  - `--replace` stops whatever holds this profile and instance, then launches. `--no-replace`
175
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.
176
252
 
177
253
  Output and dry runs:
178
254
 
@@ -180,10 +256,101 @@ Output and dry runs:
180
256
  - `--print-plan` prints the resolved launch plan instead of launching.
181
257
  - `--log <path>` routes launch output to one file.
182
258
  - `--json` switches to machine-readable output.
259
+ - `-f`, `--follow` keeps printing as the run writes. `logs` takes it.
183
260
 
184
261
  `clean` adds three tier flags: `--staging` (the default), `--logs`, and `--all`. `--all` deletes
185
262
  saves, so it needs `--yes`.
186
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
+
187
354
  ## Environment variables
188
355
 
189
356
  Each variable stands in for one flag, and only when you leave that flag off:
@@ -207,6 +374,12 @@ Each variable stands in for one flag, and only when you leave that flag off:
207
374
  - `8` `verify` found a stale mod
208
375
  - `130` interrupted
209
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
+
210
383
  ## Write a plugin
211
384
 
212
385
  A plugin is an ES module with a default export that satisfies `GamePlugin`. It takes plain data
@@ -248,10 +421,8 @@ The members:
248
421
 
249
422
  Load your plugin by path while you develop it:
250
423
 
251
- ```jsonc
252
- {
253
- "plugins": ["~/projects/gamecrate-mygame/dist/index.js"]
254
- }
424
+ ```yaml
425
+ plugins: [~/projects/gamecrate-mygame/dist/index.js]
255
426
  ```
256
427
 
257
428
  [`@gamecrate/rimworld`](../rimworld) is a complete example in about fifty lines.