@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
@@ -0,0 +1,428 @@
1
+ import type { GamePlugin } from './plugin';
2
+ /**
3
+ * Shared contract for every module. Units code against this and nothing else;
4
+ * if a signature here is wrong, fix it here rather than working around it locally.
5
+ */
6
+ export declare const Exit: {
7
+ readonly Ok: 0;
8
+ readonly GameFailed: 1;
9
+ readonly Usage: 2;
10
+ readonly Config: 3;
11
+ readonly Resolution: 4;
12
+ readonly Environment: 5;
13
+ readonly MarkerTimeout: 6;
14
+ /** Refused: this profile+instance is already running. `--replace` is the way past it. */
15
+ readonly Refused: 7;
16
+ /** `verify`: a bound mod's sources are newer than its assemblies. */
17
+ readonly Stale: 8;
18
+ readonly Interrupted: 130;
19
+ };
20
+ export type ExitCode = (typeof Exit)[keyof typeof Exit];
21
+ export type ExitReason = 'exited' | 'marker' | 'marker-timeout' | 'stopped' | 'window-closed' | 'timeout'
22
+ /** The run never reached docker: staging, a pull or a build failed. */
23
+ | 'failed';
24
+ export interface LaunchResult {
25
+ code: number;
26
+ reason: ExitReason;
27
+ }
28
+ /** 130 means runContainer caught a signal and stopped the container, not that the game died. */
29
+ export declare function reasonFor(code: number): ExitReason;
30
+ /** Every failure the tool raises deliberately. Anything else is a bug. */
31
+ export declare class GamecrateError extends Error {
32
+ readonly code: ExitCode;
33
+ readonly detail?: string | undefined;
34
+ constructor(message: string, code: ExitCode, detail?: string | undefined);
35
+ }
36
+ /** Collected and reported together, so one run surfaces every problem at once. */
37
+ export interface Problem {
38
+ /** JSON Pointer into the global config, or a file path, or a mod id. */
39
+ where: string;
40
+ message: string;
41
+ /** Populated by did-you-mean matching where it applies. */
42
+ suggestion?: string;
43
+ }
44
+ export type ModeName = 'headed' | 'headless' | 'screenshot';
45
+ export type PullPolicy = 'always' | 'missing' | 'never';
46
+ export type BuildPolicy = 'auto' | 'always' | 'never';
47
+ export type NetworkPolicy = 'none' | 'bridge' | 'host';
48
+ export type DisplayBackend = 'x11' | 'wayland';
49
+ export interface Settings {
50
+ width: number;
51
+ height: number;
52
+ devMode: boolean;
53
+ runInBackground: boolean;
54
+ /** Forced false when written; see landmine 5. */
55
+ resetModsConfigOnCrash: boolean;
56
+ gpu: boolean;
57
+ audio: boolean;
58
+ input: boolean;
59
+ network: NetworkPolicy;
60
+ /** Which display server a headed run talks to. x11 is the one an outside tool can retitle. */
61
+ display: DisplayBackend;
62
+ memory: string;
63
+ cpus: number;
64
+ pidsLimit: number;
65
+ /** Verbatim passthrough into the generated Prefs file. */
66
+ prefsExtra?: Record<string, string>;
67
+ gameArgs?: string[];
68
+ dockerArgs?: string[];
69
+ }
70
+ export interface GameFilesSpec {
71
+ source: 'mount' | 'image';
72
+ /** Required when source is "mount". */
73
+ host?: string;
74
+ container: string;
75
+ }
76
+ export interface ImageSpec {
77
+ ref: string;
78
+ acquire: 'pull' | 'build';
79
+ /** Required when acquire is "build". */
80
+ context?: string;
81
+ }
82
+ /** How the engine is told where its data directory is. Verified per game. */
83
+ export type DataDirSpec = {
84
+ container: string;
85
+ mode: 'arg';
86
+ arg: string;
87
+ } | {
88
+ container: string;
89
+ mode: 'env';
90
+ env: Record<string, string>;
91
+ };
92
+ export interface ModsDirSpec {
93
+ /** Where the staged mod tree is bind-mounted. NOT necessarily under dataDir. */
94
+ container: string;
95
+ /** Extra mod roots inside the image that must be masked with a tmpfs. */
96
+ mask?: string[];
97
+ }
98
+ export type LogFileSpec = {
99
+ mode: 'arg';
100
+ arg: string;
101
+ } | {
102
+ mode: 'copy-out';
103
+ from: string;
104
+ };
105
+ export interface ScanRoot {
106
+ path: string;
107
+ maxDepth: number;
108
+ exclude?: string[];
109
+ }
110
+ export interface LibraryEntry {
111
+ workshop?: number;
112
+ path?: string;
113
+ }
114
+ /** Matches a family of mods by pattern instead of naming each one. */
115
+ export interface DynamicModEntry {
116
+ match: string;
117
+ first?: string[];
118
+ sort?: 'alpha' | 'none';
119
+ minMatches?: number;
120
+ }
121
+ export interface ModEntryObject {
122
+ id: string;
123
+ workshop?: number;
124
+ path?: string;
125
+ optional?: boolean;
126
+ }
127
+ /** A bare string is a packageId; `workshop:` and `path:` prefixes disambiguate. */
128
+ export type ModEntry = string | ModEntryObject | DynamicModEntry;
129
+ /**
130
+ * A named sub-run of a profile with its own data directory, lock and container, so several
131
+ * can run at once. Selected with `--instance`, or derived from an explicit `--worktree`.
132
+ */
133
+ export interface InstanceConfig {
134
+ /** Promoted ahead of every other worktree request when this instance is selected. */
135
+ worktree?: string;
136
+ settings?: Partial<Settings>;
137
+ }
138
+ export interface ProfileConfig {
139
+ mods?: ModEntry[];
140
+ extends?: string;
141
+ exclude?: string[];
142
+ includeBase?: boolean;
143
+ autoDependencies?: boolean;
144
+ settings?: Partial<Settings>;
145
+ instances?: Record<string, InstanceConfig>;
146
+ /** Marks this profile as another name for an existing one. */
147
+ alias?: string;
148
+ /** Extra names this profile answers to, so one entry covers several spellings. */
149
+ aliases?: string[];
150
+ /** A one-line note for `gamecrate list`. Never read by the launcher. */
151
+ description?: string;
152
+ /** Launch defaults for this profile. The matching --no-* flag overrides each one. */
153
+ detach?: boolean;
154
+ replace?: boolean;
155
+ build?: BuildPolicy;
156
+ }
157
+ export interface GameConfig {
158
+ gameFiles: GameFilesSpec;
159
+ dataDir: DataDirSpec;
160
+ modsDir: ModsDirSpec;
161
+ logFile: LogFileSpec;
162
+ image: ImageSpec;
163
+ executable: string;
164
+ steamAppId: number;
165
+ workshopRoot: string | null;
166
+ scanRoots: ScanRoot[];
167
+ manifest: {
168
+ file: string;
169
+ };
170
+ modsConfig: {
171
+ file: string;
172
+ };
173
+ prefs: {
174
+ file: string;
175
+ };
176
+ /** Filename suffixes that mean "a save". `clean --all` counts them before it deletes. */
177
+ saveExtensions: string[];
178
+ core: string;
179
+ dlc: string[];
180
+ preCore?: string[];
181
+ base?: string[];
182
+ library?: Record<string, LibraryEntry>;
183
+ modes: ModeName[];
184
+ aliases?: Record<string, string>;
185
+ settings?: Partial<Settings>;
186
+ /** The engine claims WM_DELETE_WINDOW and drops it, so the titlebar X does nothing. */
187
+ ignoresWmDelete?: boolean;
188
+ profiles: Record<string, ProfileConfig>;
189
+ }
190
+ export interface RootConfig {
191
+ /** Package names or paths, resolved from the config file's directory. One per game. */
192
+ plugins?: string[];
193
+ dataRoot: string;
194
+ defaults?: {
195
+ settings?: Partial<Settings>;
196
+ };
197
+ games: Record<string, GameConfig>;
198
+ }
199
+ export type ModSourceKind = 'local' | 'workshop' | 'official' | 'core';
200
+ export interface ModManifest {
201
+ packageId: string;
202
+ name?: string;
203
+ modDependencies: {
204
+ packageId: string;
205
+ steamWorkshopUrl?: string;
206
+ }[];
207
+ loadAfter: string[];
208
+ loadBefore: string[];
209
+ forceLoadAfter: string[];
210
+ forceLoadBefore: string[];
211
+ incompatibleWith: string[];
212
+ }
213
+ export interface ModRecord {
214
+ /** Manifest casing, preserved. Match on the lowercased form. */
215
+ packageId: string;
216
+ dir: string;
217
+ kind: ModSourceKind;
218
+ manifest: ModManifest;
219
+ /** Workshop item id when kind is "workshop". */
220
+ workshopId?: number;
221
+ /** True when dir sits inside a linked git worktree; ranked below primaries. */
222
+ linkedWorktree: boolean;
223
+ /**
224
+ * Request order when a caller deliberately selected the worktree this record lives in.
225
+ * The scanner can never set it, which is what stops an unselected worktree from winning.
226
+ */
227
+ selectedWorktree?: number;
228
+ /** Request order when `--use <packageId>=<dir>` named this record. Outranks a worktree. */
229
+ overridden?: number;
230
+ /** Set only for a selected worktree; the branch costs a git spawn, so it is not scanned for. */
231
+ worktree?: {
232
+ root: string;
233
+ branch: string;
234
+ source: WorktreeSource;
235
+ };
236
+ /** Which scanRoot produced it, for the precedence ladder. */
237
+ rootIndex: number;
238
+ }
239
+ export type WorktreeSource = 'flag' | 'env' | 'cwd' | 'ref';
240
+ export interface WorktreeRequest {
241
+ /** realpath'd worktree toplevel. */
242
+ root: string;
243
+ branch: string;
244
+ source: WorktreeSource;
245
+ /** Lower wins. */
246
+ order: number;
247
+ }
248
+ export interface ModIndex {
249
+ game: string;
250
+ plugin: GamePlugin;
251
+ /** Keyed by lowercased packageId. Several records means a collision to resolve. */
252
+ byPackageId: Map<string, ModRecord[]>;
253
+ byWorkshopId: Map<number, ModRecord>;
254
+ /** Lowercased last dot-segment -> packageIds. CLI matching only. */
255
+ byShortName: Map<string, string[]>;
256
+ problems: Problem[];
257
+ }
258
+ /** Why a mod looks stale, in enough detail to name the file that says so. */
259
+ export interface StaleReport {
260
+ /** Mod-relative path of the newest .cs. */
261
+ newestSource: string;
262
+ newestSourceMs: number;
263
+ /** Mod-relative path of the newest assembly it should have been compiled into. */
264
+ assembly: string;
265
+ assemblyMs: number;
266
+ newerCount: number;
267
+ }
268
+ export interface ResolvedMod {
269
+ packageId: string;
270
+ hostDir: string;
271
+ containerDir: string;
272
+ kind: ModSourceKind;
273
+ workshopId?: number;
274
+ /** False when the entry came from autoDependencies rather than the profile. */
275
+ explicit: boolean;
276
+ /** Set when a .cs file is newer than the staged assembly. */
277
+ stale?: boolean;
278
+ /** Set only when there is an assembly to be stale against; drives the launch warning. */
279
+ staleReport?: StaleReport;
280
+ /** Present when this mod came out of a linked worktree. */
281
+ worktree?: {
282
+ root: string;
283
+ branch: string;
284
+ source: WorktreeSource;
285
+ selected: boolean;
286
+ };
287
+ /** Other directories that declared this packageId and lost. Always emitted. */
288
+ shadowed?: string[];
289
+ }
290
+ export interface LaunchPlan {
291
+ game: string;
292
+ gameConfig: GameConfig;
293
+ plugin: GamePlugin;
294
+ profile: string;
295
+ settings: Settings;
296
+ mods: ResolvedMod[];
297
+ /** Absolute host path: <dataRoot>/<game>/<profile>. */
298
+ profileDir: string;
299
+ /** Undefined for the base profile; a name when several runs share one profile. */
300
+ instance?: string;
301
+ /** profileDir, or <profileDir>/instances/<instance>. Everything a run writes hangs off it. */
302
+ instanceDir: string;
303
+ dataDirHost: string;
304
+ /** <profileDir>/config. Shared by every instance, and holds the XDG dirs. */
305
+ configDirHost: string;
306
+ stageDirHost: string;
307
+ logsDirHost: string;
308
+ /** <logsDirHost>/runs/<ts>, bound into the container so Player.log lands with stdout.log. */
309
+ runDirHost: string;
310
+ mode: ModeName;
311
+ marker?: string;
312
+ timeoutSeconds: number;
313
+ renderWaitSeconds: number;
314
+ /** False under --no-stale-check. The check still runs, so staleReport stays truthful. */
315
+ warnOnStale: boolean;
316
+ warnings: string[];
317
+ }
318
+ export interface Identity {
319
+ uid: number;
320
+ gid: number;
321
+ home: string;
322
+ user: string;
323
+ }
324
+ export interface Mount {
325
+ type: 'bind' | 'tmpfs';
326
+ source?: string;
327
+ target: string;
328
+ readonly?: boolean;
329
+ /** tmpfs only. */
330
+ size?: string;
331
+ uid?: number;
332
+ gid?: number;
333
+ mode?: string;
334
+ }
335
+ export interface DockerRunSpec {
336
+ image: string;
337
+ name: string;
338
+ labels: Record<string, string>;
339
+ identity: Identity;
340
+ env: Record<string, string>;
341
+ mounts: Mount[];
342
+ devices: string[];
343
+ deviceCgroupRules: string[];
344
+ network: NetworkPolicy;
345
+ memory: string;
346
+ memorySwap: string;
347
+ cpus: number;
348
+ pidsLimit: number;
349
+ ulimits: string[];
350
+ workdir: string;
351
+ /** The host's, when set: KWin appends `<@name>` to a caption from a foreign machine. */
352
+ hostname?: string;
353
+ /** argv after the image name. */
354
+ command: string[];
355
+ extraArgs: string[];
356
+ }
357
+ export interface ParsedArgs {
358
+ subcommand: string;
359
+ game?: string;
360
+ profile?: string;
361
+ mods: string[];
362
+ without: string[];
363
+ only: string[];
364
+ mode?: ModeName;
365
+ marker?: string;
366
+ timeout?: number;
367
+ renderWait?: number;
368
+ resolution?: {
369
+ width: number;
370
+ height: number;
371
+ };
372
+ network?: NetworkPolicy;
373
+ log?: string;
374
+ pull?: PullPolicy;
375
+ build?: BuildPolicy;
376
+ sort?: 'topo' | 'none';
377
+ /** `clean` only: --staging is the default, --all additionally requires --yes. */
378
+ cleanTier?: 'staging' | 'logs' | 'all';
379
+ dockerArgs: string[];
380
+ gameArgs: string[];
381
+ dryRun: boolean;
382
+ printPlan: boolean;
383
+ json: boolean;
384
+ root: boolean;
385
+ yes: boolean;
386
+ help: boolean;
387
+ /** Repeatable; earlier flags outrank later ones. */
388
+ worktree: string[];
389
+ /** Names the sub-run: its own data directory, lock and container. */
390
+ instance?: string;
391
+ /** Repeatable `packageId=path`; forces one mod's source, whatever the profile says. */
392
+ use: string[];
393
+ /** Suppresses ambient cwd selection and the env var. */
394
+ noWorktree: boolean;
395
+ /** Suppresses the sources-newer-than-assemblies warning. The check itself still runs. */
396
+ noStaleCheck: boolean;
397
+ /** Stops whatever holds this profile+instance, then launches. Never refuses. */
398
+ replace: boolean;
399
+ /** Runs in the background: this process forks a supervisor and returns the prompt. */
400
+ detach: boolean;
401
+ /** One-run overrides. Only these turn the matching boolean back off. */
402
+ noDetach: boolean;
403
+ noReplace: boolean;
404
+ /** Set on the forked supervisor only. Never a config key, never in help. */
405
+ supervised: boolean;
406
+ /** `-f`: keep printing as the run writes, instead of dumping what is there. */
407
+ follow: boolean;
408
+ rest: string[];
409
+ }
410
+ export type ProjectDefaults = Partial<Omit<ParsedArgs, 'subcommand' | 'cleanTier' | 'yes' | 'help' | 'rest' | 'profile' | 'supervised' | 'noDetach' | 'noReplace' | 'follow'>> & {
411
+ /** Replaces the old `profile:` key. Falls back to the first entry in `profiles`. */
412
+ defaultProfile?: string;
413
+ /** Validated by validateConfig after the splice, not here. */
414
+ profiles?: Record<string, unknown>;
415
+ settings?: Record<string, unknown>;
416
+ /** Profile keys in source order. Object.keys sorts integer-like names to the front. */
417
+ profileOrder?: string[];
418
+ /** The file these came from. Four suffixes are legal, so output must not guess the name. */
419
+ configPath?: string;
420
+ };
421
+ /** Names that can never be a game or profile key. Enforced at config load. */
422
+ export declare const RESERVED_NAMES: readonly string[];
423
+ export declare const NAME_PATTERN: RegExp;
424
+ /**
425
+ * Lookup in a bag keyed by user input. A bare index hands back Object.prototype members, so
426
+ * `--mod constructor` or a profile named `toString` would resolve to an inherited function.
427
+ */
428
+ export declare function own<T>(bag: Record<string, T> | undefined, key: string): T | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamecrate/cli",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "Run a modded game in a container, with mods resolved from your local checkouts.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,21 +21,26 @@
21
21
  "gamecrate": "./dist/gamecrate.js"
22
22
  },
23
23
  "exports": {
24
- ".": "./src/lib.ts"
24
+ ".": {
25
+ "types": "./dist/types/lib.d.ts",
26
+ "default": "./dist/lib.js"
27
+ }
25
28
  },
26
- "types": "./src/lib.ts",
29
+ "types": "./dist/types/lib.d.ts",
27
30
  "files": [
28
- "src",
29
- "dist/gamecrate.js"
31
+ "dist/gamecrate.js",
32
+ "dist/lib.js",
33
+ "dist/types"
30
34
  ],
31
35
  "scripts": {
32
36
  "typecheck": "tsc --noEmit",
33
37
  "test": "vitest run",
34
- "build": "npm run build:node && npm run build:binary",
35
- "build:node": "bun build ./src/index.ts --target=node --packages=external --outfile dist/gamecrate.js",
36
- "build:binary": "bun build ./src/index.ts --compile --outfile dist/gamecrate",
37
- "prepare": "npm run build:node",
38
- "prepack": "npm run build:node"
38
+ "build": "npm run build:node && npm run build:types && npm run build:binary",
39
+ "build:node": "bun build ./src/index.ts --target=node --packages=external --define __VERSION__='\"'${npm_package_version:-0.0.0-dev}'\"' --outfile dist/gamecrate.js && bun build ./src/lib.ts --target=node --packages=external --outfile dist/lib.js",
40
+ "build:types": "tsc -p tsconfig.build.json",
41
+ "build:binary": "bun build ./src/index.ts --compile --define __VERSION__='\"'${npm_package_version:-0.0.0-dev}'\"' --outfile dist/gamecrate",
42
+ "prepare": "npm run build:node && npm run build:types",
43
+ "prepack": "npm run build:node && npm run build:types"
39
44
  },
40
45
  "devDependencies": {
41
46
  "@types/node": "^22.10.0",