@gamecrate/cli 1.0.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.
- package/README.md +204 -33
- package/dist/gamecrate.js +1270 -499
- package/dist/lib.js +29 -5
- package/dist/types/cli/args.d.ts +17 -1
- package/dist/types/cli/game.d.ts +2 -0
- package/dist/types/cli/list.d.ts +2 -0
- package/dist/types/cli/output.d.ts +23 -1
- package/dist/types/cli/profile.d.ts +6 -0
- package/dist/types/config/load.d.ts +5 -3
- package/dist/types/config/read.d.ts +11 -0
- package/dist/types/docker/run.d.ts +3 -1
- package/dist/types/docker/window.d.ts +30 -1
- package/dist/types/launch/prepare.d.ts +46 -3
- package/dist/types/launch/supervisor.d.ts +48 -0
- package/dist/types/run/registry.d.ts +22 -0
- package/dist/types/types.d.ts +36 -2
- package/package.json +1 -1
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
|
|
26
|
-
|
|
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
|
-
```
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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`
|
|
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
|
|
102
|
-
|
|
103
|
-
and
|
|
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
|
-
|
|
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>
|
|
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>
|
|
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
|
|
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
|
|
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>`
|
|
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
|
-
```
|
|
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.
|