@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/dist/lib.js ADDED
@@ -0,0 +1,320 @@
1
+ // src/plugin.ts
2
+ import { exports as exportsField, legacy } from "resolve.exports";
3
+
4
+ // src/config/load.ts
5
+ import { z as z2 } from "zod";
6
+
7
+ // src/cli/args.ts
8
+ import { Command, CommanderError, Option } from "commander";
9
+
10
+ // src/types.ts
11
+ var Exit = {
12
+ Ok: 0,
13
+ GameFailed: 1,
14
+ Usage: 2,
15
+ Config: 3,
16
+ Resolution: 4,
17
+ Environment: 5,
18
+ MarkerTimeout: 6,
19
+ Refused: 7,
20
+ Stale: 8,
21
+ Interrupted: 130
22
+ };
23
+ class GamecrateError extends Error {
24
+ code;
25
+ detail;
26
+ constructor(message, code, detail) {
27
+ super(message);
28
+ this.code = code;
29
+ this.detail = detail;
30
+ this.name = "GamecrateError";
31
+ }
32
+ }
33
+ var NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
34
+
35
+ // src/cli/args.ts
36
+ function parseResolution(value) {
37
+ const match = /^(\d+)x(\d+)$/i.exec(value);
38
+ const width = Number(match?.[1]);
39
+ const height = Number(match?.[2]);
40
+ if (!Number.isSafeInteger(width) || width <= 0 || !Number.isSafeInteger(height) || height <= 0) {
41
+ throw usage(`--resolution takes positive dimensions like 1920x1080, got ${value}`);
42
+ }
43
+ return { width, height };
44
+ }
45
+ function usage(message, suggestion) {
46
+ return new GamecrateError(message, Exit.Usage, suggestion ? `did you mean ${suggestion}?` : undefined);
47
+ }
48
+ function suggest(word, candidates) {
49
+ const target = word.toLowerCase();
50
+ const limit = Math.max(2, Math.floor(target.length / 3));
51
+ let best;
52
+ let bestDistance = Infinity;
53
+ for (const candidate of candidates) {
54
+ const d = distance(target, candidate.toLowerCase());
55
+ if (d < bestDistance && d <= limit) {
56
+ best = candidate;
57
+ bestDistance = d;
58
+ }
59
+ }
60
+ return best;
61
+ }
62
+ function distance(a, b) {
63
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
64
+ for (let i = 1;i <= a.length; i++) {
65
+ const row = [i];
66
+ for (let j = 1;j <= b.length; j++) {
67
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
68
+ row[j] = Math.min(row[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
69
+ }
70
+ prev = row;
71
+ }
72
+ return prev[b.length];
73
+ }
74
+
75
+ // src/config/read.ts
76
+ import { parseTree } from "jsonc-parser";
77
+ import { isMap, parse as parseYaml, parseDocument } from "yaml";
78
+
79
+ // src/config/jsonc.ts
80
+ import { parse, printParseErrorCode } from "jsonc-parser";
81
+
82
+ // src/config/validate.ts
83
+ import { z } from "zod";
84
+ var MODES = ["headed", "headless", "screenshot"];
85
+ var HINTS = "\x00gamecrate/hints:";
86
+ function obj(shape) {
87
+ const known = Object.keys(shape);
88
+ return z.strictObject(shape, {
89
+ error: (issue) => issue.code === "unrecognized_keys" ? HINTS + JSON.stringify(issue.keys.map((key) => suggest(key, known) ?? null)) : "expected an object"
90
+ });
91
+ }
92
+ function requiredWhen(key, when) {
93
+ return (ctx) => {
94
+ if (!when(ctx.value) || ctx.value[key] !== undefined)
95
+ return;
96
+ ctx.issues.push({ code: "custom", message: `missing required key "${key}"`, path: [key], input: ctx.value });
97
+ };
98
+ }
99
+ var str = z.string({ error: "expected a string" });
100
+ var num = z.number({ error: "expected a number" });
101
+ var bool = z.boolean({ error: "expected a boolean" });
102
+ var strArray = z.array(z.string({ error: "expected an array of strings" }), {
103
+ error: "expected an array of strings"
104
+ });
105
+ var strMap = z.record(z.string(), z.string({ error: "expected an object of string values" }), {
106
+ error: "expected an object of string values"
107
+ });
108
+ function oneOf(values) {
109
+ return z.enum(values, { error: `expected one of ${values.join(", ")}` });
110
+ }
111
+ var modeName = z.unknown().check((ctx) => {
112
+ const value = ctx.value;
113
+ if (typeof value === "string" && MODES.includes(value))
114
+ return;
115
+ const hint = typeof value === "string" ? suggest(value, MODES) : undefined;
116
+ ctx.issues.push({
117
+ code: "custom",
118
+ message: `expected one of ${MODES.join(", ")}`,
119
+ input: value,
120
+ ...hint === undefined ? {} : { params: { suggestion: `did you mean "${hint}"?` } }
121
+ });
122
+ });
123
+ var settings = obj({
124
+ width: num.optional(),
125
+ height: num.optional(),
126
+ devMode: bool.optional(),
127
+ runInBackground: bool.optional(),
128
+ resetModsConfigOnCrash: bool.optional(),
129
+ gpu: bool.optional(),
130
+ audio: bool.optional(),
131
+ input: bool.optional(),
132
+ network: oneOf(["none", "bridge", "host"]).optional(),
133
+ display: oneOf(["x11", "wayland"]).optional(),
134
+ memory: str.optional(),
135
+ cpus: num.optional(),
136
+ pidsLimit: num.optional(),
137
+ prefsExtra: strMap.optional(),
138
+ gameArgs: strArray.optional(),
139
+ dockerArgs: strArray.optional()
140
+ });
141
+ var dynamicModEntry = obj({
142
+ match: str,
143
+ first: strArray.optional(),
144
+ sort: oneOf(["alpha", "none"]).optional(),
145
+ minMatches: num.optional()
146
+ });
147
+ var objectModEntry = obj({
148
+ id: str,
149
+ workshop: num.optional(),
150
+ path: str.optional(),
151
+ optional: bool.optional()
152
+ });
153
+ var modEntry = z.unknown().check((ctx) => {
154
+ const value = ctx.value;
155
+ if (typeof value === "string") {
156
+ if (value.trim() === "")
157
+ ctx.issues.push({ code: "custom", message: "mod entry is empty", input: value });
158
+ return;
159
+ }
160
+ if (!isObj(value)) {
161
+ ctx.issues.push({ code: "custom", message: "expected a packageId string or an object", input: value });
162
+ return;
163
+ }
164
+ const schema = value["match"] !== undefined ? dynamicModEntry : objectModEntry;
165
+ const result = schema.safeParse(value);
166
+ if (result.success)
167
+ return;
168
+ for (const issue of result.error.issues)
169
+ ctx.issues.push({ ...issue, input: value });
170
+ });
171
+ var profile = obj({
172
+ mods: z.array(modEntry, { error: "expected an array" }).optional(),
173
+ extends: str.optional(),
174
+ exclude: strArray.optional(),
175
+ includeBase: bool.optional(),
176
+ autoDependencies: bool.optional(),
177
+ settings: settings.optional(),
178
+ instances: z.record(z.string(), obj({ worktree: str.optional(), settings: settings.optional() }), {
179
+ error: "expected an object"
180
+ }).optional(),
181
+ alias: str.optional(),
182
+ aliases: strArray.optional(),
183
+ description: str.optional(),
184
+ detach: bool.optional(),
185
+ replace: bool.optional(),
186
+ build: oneOf(["auto", "always", "never"]).optional()
187
+ }).check((ctx) => {
188
+ const v = ctx.value;
189
+ if (v.alias !== undefined && (v.extends !== undefined || v.mods !== undefined)) {
190
+ ctx.issues.push({
191
+ code: "custom",
192
+ message: 'an alias profile cannot also declare "mods" or "extends"',
193
+ input: v
194
+ });
195
+ }
196
+ });
197
+ var game = obj({
198
+ gameFiles: obj({ source: oneOf(["mount", "image"]), host: str.optional(), container: str }).check(requiredWhen("host", (v) => v["source"] === "mount")),
199
+ dataDir: obj({
200
+ container: str,
201
+ mode: oneOf(["arg", "env"]),
202
+ arg: str.optional(),
203
+ env: strMap.optional()
204
+ }).check(requiredWhen("arg", (v) => v["mode"] === "arg"), requiredWhen("env", (v) => v["mode"] === "env")),
205
+ modsDir: obj({ container: str, mask: strArray.optional() }),
206
+ logFile: obj({ mode: oneOf(["arg", "copy-out"]), arg: str.optional(), from: str.optional() }).check(requiredWhen("arg", (v) => v["mode"] === "arg"), requiredWhen("from", (v) => v["mode"] === "copy-out")),
207
+ image: obj({ ref: str, acquire: oneOf(["pull", "build"]), context: str.optional() }).check(requiredWhen("context", (v) => v["acquire"] === "build")),
208
+ executable: str,
209
+ steamAppId: num,
210
+ workshopRoot: z.union([z.string(), z.null()], { error: "expected a string or null" }),
211
+ scanRoots: z.array(obj({ path: str, maxDepth: num, exclude: strArray.optional() }), {
212
+ error: "expected an array"
213
+ }),
214
+ manifest: obj({ file: str }),
215
+ modsConfig: obj({ file: str }),
216
+ prefs: obj({ file: str }),
217
+ saveExtensions: strArray,
218
+ core: str,
219
+ dlc: strArray,
220
+ preCore: strArray.optional(),
221
+ base: strArray.optional(),
222
+ library: z.record(z.string(), obj({ workshop: num.optional(), path: str.optional() }).check((ctx) => {
223
+ if (ctx.value["workshop"] === undefined && ctx.value["path"] === undefined) {
224
+ ctx.issues.push({
225
+ code: "custom",
226
+ message: 'library entry needs a "workshop" id or a "path"',
227
+ input: ctx.value
228
+ });
229
+ }
230
+ }), { error: "expected an object" }).optional(),
231
+ modes: z.array(modeName, { error: "expected a non-empty array" }).min(1, {
232
+ error: "expected a non-empty array"
233
+ }),
234
+ aliases: strMap.optional(),
235
+ settings: settings.optional(),
236
+ ignoresWmDelete: bool.optional(),
237
+ profiles: z.record(z.string(), profile, { error: "expected an object" })
238
+ });
239
+ var root = obj({
240
+ plugins: strArray.optional(),
241
+ dataRoot: str,
242
+ defaults: obj({ settings: settings.optional() }).optional(),
243
+ games: z.unknown()
244
+ });
245
+ function isObj(v) {
246
+ return typeof v === "object" && v !== null && !Array.isArray(v);
247
+ }
248
+
249
+ // src/config/load.ts
250
+ var projectName = z2.custom((v) => typeof v === "string" && NAME_PATTERN.test(v), "expected a name");
251
+ var projectStr = z2.string({ error: "expected a string" });
252
+ var projectBool = z2.boolean({ error: "expected true or false" });
253
+ var projectList = z2.custom((v) => Array.isArray(v) && v.every((entry) => typeof entry === "string"), "expected an array of strings");
254
+ var projectSeconds = z2.custom((v) => Number.isSafeInteger(v) && v >= 0, "expected a whole number of seconds");
255
+ function oneOf2(values) {
256
+ return z2.enum(values, { error: `expected one of ${values.join(", ")}` });
257
+ }
258
+ var BUILD_POLICIES = ["auto", "always", "never"];
259
+ var projectResolution = z2.string({ error: "expected dimensions like 1920x1080" }).check((ctx) => {
260
+ try {
261
+ parseResolution(ctx.value);
262
+ } catch (error) {
263
+ ctx.issues.push({ code: "custom", message: error.message, input: ctx.value });
264
+ }
265
+ }).transform(parseResolution);
266
+ var PROJECT_OBJECT = z2.strictObject({
267
+ game: projectName.optional(),
268
+ defaultProfile: projectName.optional(),
269
+ profiles: z2.record(z2.string(), z2.unknown()).optional(),
270
+ settings: z2.record(z2.string(), z2.unknown()).optional(),
271
+ detach: projectBool.optional(),
272
+ mods: projectList.optional(),
273
+ without: projectList.optional(),
274
+ only: projectList.optional(),
275
+ dockerArgs: projectList.optional(),
276
+ gameArgs: projectList.optional(),
277
+ worktree: projectList.optional(),
278
+ use: projectList.optional(),
279
+ marker: projectStr.optional(),
280
+ instance: projectStr.optional(),
281
+ log: projectStr.optional(),
282
+ timeout: projectSeconds.optional(),
283
+ renderWait: projectSeconds.optional(),
284
+ dryRun: projectBool.optional(),
285
+ printPlan: projectBool.optional(),
286
+ json: projectBool.optional(),
287
+ root: projectBool.optional(),
288
+ noWorktree: projectBool.optional(),
289
+ noStaleCheck: projectBool.optional(),
290
+ replace: projectBool.optional(),
291
+ mode: oneOf2(["headed", "headless", "screenshot"]).optional(),
292
+ pull: oneOf2(["always", "missing", "never"]).optional(),
293
+ sort: oneOf2(["topo", "none"]).optional(),
294
+ network: oneOf2(["none", "bridge", "host"]).optional(),
295
+ build: z2.union([z2.boolean().transform((on) => on ? "always" : "never"), z2.enum(BUILD_POLICIES)], {
296
+ error: `expected one of ${BUILD_POLICIES.join(", ")}`
297
+ }).optional(),
298
+ resolution: projectResolution.optional()
299
+ }, { error: "expected an object" });
300
+ var PROJECT_SCHEMA = PROJECT_OBJECT.check((ctx) => {
301
+ const { game, profiles, settings } = ctx.value;
302
+ if (game !== undefined)
303
+ return;
304
+ for (const [key, value] of [["profiles", profiles], ["settings", settings]]) {
305
+ if (value === undefined)
306
+ continue;
307
+ ctx.issues.push({
308
+ code: "custom",
309
+ path: [key],
310
+ message: "needs a top-level game: to say which game it belongs to",
311
+ input: ctx.value
312
+ });
313
+ }
314
+ });
315
+
316
+ // src/plugin.ts
317
+ var PLUGIN_API_VERSION = 1;
318
+ export {
319
+ PLUGIN_API_VERSION
320
+ };
@@ -0,0 +1,56 @@
1
+ import { Command } from 'commander';
2
+ import type { BuildPolicy, ParsedArgs, ProfileConfig, ProjectDefaults } from '../types';
3
+ export type PositionalSlot = 'game' | 'profile' | 'rest';
4
+ export interface SubcommandSpec {
5
+ name: string;
6
+ summary: string;
7
+ /** Rendered after the subcommand word in usage lines. */
8
+ usage: string;
9
+ positionals: PositionalSlot[];
10
+ /** Flag names beyond the global set, in the order help should show them. */
11
+ flags: readonly string[];
12
+ }
13
+ /** The subcommand table. `modless` is reserved as a built-in profile, not a verb. */
14
+ export declare const SUBCOMMANDS: readonly SubcommandSpec[];
15
+ /** Shown for every subcommand. */
16
+ export declare const GLOBAL_FLAGS: readonly ["--json", "--help"];
17
+ /** The GAMECRATE_ vars, by the flag they stand in for. Fallbacks only; a flag always wins. */
18
+ export declare const FLAG_ENV: Record<string, string>;
19
+ /**
20
+ * Every flag, in the order help lists them. The only source of truth for what the CLI
21
+ * accepts: help and completion read it back off the program.
22
+ */
23
+ export declare function buildProgram(): Command;
24
+ export interface ParseOptions {
25
+ env?: Record<string, string | undefined>;
26
+ defaults?: ProjectDefaults;
27
+ /** Game names from the loaded config; enables did-you-mean on the first positional. */
28
+ games?: readonly string[];
29
+ }
30
+ /**
31
+ * `gamecrate <game> [profile] [flags] [-- game args]`, with the subcommand slot
32
+ * defaulting to `run`. Game args come after a bare `--` and nowhere else.
33
+ */
34
+ export declare function parseArgs(argv: string[], opts?: ParseOptions): ParsedArgs;
35
+ /**
36
+ * Any layer can ask for this; the --no-detach flag is the only refusal. The supervisor is
37
+ * already the fork, and a profile or project `detach: true` reaches it too: it never forks again.
38
+ */
39
+ export declare function wantsDetach(args: ParsedArgs, profile: ProfileConfig): boolean;
40
+ /** The parent already replaced the previous run, and the only lock left is the child's own. */
41
+ export declare function wantsReplace(args: ParsedArgs, profile: ProfileConfig): boolean;
42
+ /** Three-way, so first defined wins. --no-build already arrives as 'never'. */
43
+ export declare function buildPolicy(args: ParsedArgs, profile: ProfileConfig): BuildPolicy;
44
+ export declare function parseResolution(value: string): {
45
+ width: number;
46
+ height: number;
47
+ };
48
+ /** Closest candidate within an edit distance that scales with word length. */
49
+ export declare function suggest(word: string, candidates: readonly string[]): string | undefined;
50
+ /**
51
+ * The argv that re-execs this same gamecrate as a supervisor. The compiled binary reports
52
+ * a virtual /$bunfs path as argv[1], which the child would read as a game name.
53
+ */
54
+ export declare function supervisorArgv(userArgs: string[], instanceDir: string, self?: string[], execPath?: string): string[];
55
+ /** Read straight off argv: the recovery path runs before anything is parsed or loaded. */
56
+ export declare function supervisedDir(argv: string[]): string | undefined;
@@ -0,0 +1,2 @@
1
+ import type { ParsedArgs, RootConfig } from '../types';
2
+ export declare function requireGame(args: ParsedArgs, config: RootConfig): string;
@@ -0,0 +1,7 @@
1
+ import type { RootConfig } from '../types';
2
+ /**
3
+ * Three levels: no topic gives the top level, a subcommand name gives its usage,
4
+ * a game name gives that game's profiles and modes.
5
+ */
6
+ export declare function renderHelp(topic?: string, config?: RootConfig): string;
7
+ export declare function renderCompletion(shell: 'bash' | 'zsh'): string;
@@ -0,0 +1,2 @@
1
+ import type { ParsedArgs, ProjectDefaults, RootConfig } from '../types';
2
+ export declare function list(args: ParsedArgs, config: RootConfig, defaults: ProjectDefaults): number;
@@ -0,0 +1,78 @@
1
+ import type { Readable } from 'node:stream';
2
+ import type { LaunchPlan, Problem, ResolvedMod } from '../types';
3
+ /** Tool status. Never stdout: stdout belongs to the game. */
4
+ export declare function status(message: string): void;
5
+ export declare function warn(message: string): void;
6
+ export interface OutputRedirect {
7
+ close(): void;
8
+ }
9
+ export declare function redirectOutput(path: string, keep?: boolean): OutputRedirect;
10
+ export declare function forwardOutput(stream: Readable, target: NodeJS.WriteStream): Promise<void>;
11
+ /**
12
+ * Every collected failure at once, grouped by location. Resolution stops before any
13
+ * side effect, so reporting the first problem only would hide the rest.
14
+ */
15
+ export declare function reportProblems(problems: Problem[]): never;
16
+ export interface PlanModPayload {
17
+ packageId: string;
18
+ kind: ResolvedMod['kind'];
19
+ hostDir: string;
20
+ containerDir: string;
21
+ origin: 'explicit' | 'auto';
22
+ stale: boolean;
23
+ staleReport?: ResolvedMod['staleReport'];
24
+ workshopId?: number;
25
+ }
26
+ export interface PlanPayload {
27
+ game: string;
28
+ profile: string;
29
+ instance?: string;
30
+ mode: string;
31
+ marker?: string;
32
+ timeoutSeconds: number;
33
+ renderWaitSeconds: number;
34
+ profileDir: string;
35
+ instanceDir: string;
36
+ containerName: string;
37
+ dataDirHost: string;
38
+ stageDirHost: string;
39
+ logsDirHost: string;
40
+ mods: PlanModPayload[];
41
+ warnings: string[];
42
+ }
43
+ /**
44
+ * Regenerated from the mods rather than stored, because a successful `--build` clears a stale
45
+ * report and the warning has to disappear with it.
46
+ */
47
+ export declare function planWarnings(plan: LaunchPlan): string[];
48
+ /** The `--print-plan --json` payload. Bind mounts leave no host-readable link to assert on. */
49
+ export declare function planPayload(plan: LaunchPlan): PlanPayload;
50
+ export declare function printPlan(plan: LaunchPlan, asJson: boolean): void;
51
+ /** Sortable lexicographically and safe on every filesystem: 20260730T142335123Z. */
52
+ export declare function runTimestamp(now?: Date): string;
53
+ /** Makes <logsDir>/runs/<ts>, repoints `current` at it, rotates the old ones, returns the dir. */
54
+ export declare function openRunLog(logsDir: string, now?: Date): string;
55
+ /** Below the first a wait is not worth mentioning; the log normally appears in milliseconds. */
56
+ export interface NoticeSchedule {
57
+ firstMs: number;
58
+ everyMs: number;
59
+ }
60
+ export declare const WAIT_NOTICE: NoticeSchedule;
61
+ /**
62
+ * The elapsed label when a wait is due a line, else undefined. Driven by elapsed time and not
63
+ * by poll count: a quarter-second poll would scroll a line per pass and read as its own kind
64
+ * of broken, while a silent ten-minute image build looks exactly like a hang. Both branches
65
+ * floor, so the label never claims more time than has passed.
66
+ */
67
+ export declare function waitNotice(waitedMs: number, lastNoticeMs: number, schedule?: NoticeSchedule): string | undefined;
68
+ /** Inverse of runTimestamp. Anchored at the start, so the -2 collision suffix is tolerated. */
69
+ export declare function runStartedAt(name: string): number | undefined;
70
+ /** linkCurrent keeps `current` pointed at the newest run, so the run dir is never guessed. */
71
+ export declare function currentLog(instanceDir: string): string;
72
+ /**
73
+ * tail -f never ends on its own, so a live run gets --pid and tail leaves when the holder
74
+ * does. With no live holder nothing will write again, so -f is dropped or it hangs forever.
75
+ */
76
+ export declare function tailArgv(file: string, fromStart: boolean, livePid?: number): string[];
77
+ /** Landmine 7: a 180MB Player-prev.log was 68% of a profile tree. Retention is the cap. */
78
+ export declare function rotateRuns(logsDir: string, keep: number): string[];
@@ -0,0 +1,6 @@
1
+ import type { ParsedArgs, ProjectDefaults } from '../types';
2
+ /**
3
+ * Not in parseArgs: filling a default there would erase the difference between a typed
4
+ * profile and a defaulted one, which clean and fix-perms both need.
5
+ */
6
+ export declare function profileOf(args: ParsedArgs, defaults: ProjectDefaults): string;
@@ -0,0 +1,3 @@
1
+ import type { Settings } from '../types';
2
+ export declare const DEFAULT_DATA_ROOT = "~/.local/share/gamecrate";
3
+ export declare const DEFAULT_SETTINGS: Settings;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * JSON with line and block comments and trailing commas. jsonc-parser recovers from
3
+ * syntax errors and still returns a value, so the error list is the only success signal.
4
+ */
5
+ export declare function parseJsonc(text: string): unknown;
@@ -0,0 +1,47 @@
1
+ import type { GameConfig, ModEntry, ProfileConfig, ProjectDefaults, RootConfig, Settings } from '../types';
2
+ import type { GamePlugin } from '../plugin';
3
+ export declare function globalConfigDir(): string;
4
+ export declare function findGlobalConfig(): Promise<string | undefined>;
5
+ export declare function findProjectConfig(start?: string): Promise<string | undefined>;
6
+ export declare function loadProjectDefaults(start?: string): Promise<ProjectDefaults>;
7
+ export interface LoadedConfig {
8
+ config: RootConfig;
9
+ plugins: Map<string, GamePlugin>;
10
+ }
11
+ /**
12
+ * Reads the global config, loads the plugins it lists, then merges the user's blocks over each
13
+ * plugin's defaults. A missing file means no games, which every non-launch subcommand survives.
14
+ */
15
+ export declare function loadConfig(path?: string, project?: ProjectDefaults): Promise<LoadedConfig>;
16
+ /** defaults -> games.<game> -> profile -> instance -> CLI. Scalars replace, arrays concatenate. */
17
+ export declare function resolveSettings(root: RootConfig, game: GameConfig, profile: ProfileConfig, ...overrides: (Partial<Settings> | undefined)[]): Settings;
18
+ /**
19
+ * Flattens `alias` and the `extends` chain into one profile. `exclude` survives on
20
+ * the result so the caller can subtract it from preCore/core/dlc/base too.
21
+ */
22
+ export declare function resolveProfile(game: GameConfig, name: string): ProfileConfig;
23
+ /**
24
+ * The name a profile stores its data under. An alias must not get its own data directory,
25
+ * or your saves split depending on which spelling you typed.
26
+ */
27
+ export declare function canonicalProfile(game: GameConfig, name: string): string;
28
+ /**
29
+ * The one place a profile's data directory is named. Every subcommand goes through it, so
30
+ * `logs`, `clean` and `clone` land on the directory `run` actually used, alias or not.
31
+ */
32
+ export declare function profileDataDir(root: RootConfig, game: string, profile: string): string;
33
+ /**
34
+ * The directories a command should touch for one game. With no profile it lists what is on disk,
35
+ * verbatim: a directory name is already a path, and canonicalizing it skips odd-cased ones.
36
+ */
37
+ export declare function profileDirs(root: RootConfig, game: string, profile?: string): Promise<string[]>;
38
+ export declare function profileKey(game: GameConfig, name: string): string | undefined;
39
+ /** Removes entries whose id matches an exclusion. Dynamic entries are filtered after expansion. */
40
+ export declare function subtract(mods: ModEntry[], exclude: string[]): ModEntry[];
41
+ export declare function globToRegExp(pattern: string): RegExp;
42
+ /**
43
+ * A later list replaces an earlier one, so a user can shorten a plugin's `dlc` or `modes`.
44
+ * `concatArrays` is the settings ladder's rule, where gameArgs accumulate across layers.
45
+ */
46
+ export declare function deepMerge<T>(base: T, over: unknown, concatArrays?: boolean): T;
47
+ export declare function expandHome(p: string): string;
@@ -0,0 +1,11 @@
1
+ /** Probe order. yaml first: it is what `config edit` writes and what the docs show. */
2
+ export declare const CONFIG_SUFFIXES: readonly [".yml", ".yaml", ".json", ".jsonc"];
3
+ export declare function readConfigText(text: string, path: string): unknown;
4
+ /** Undefined when the file is not there; every other read error propagates. */
5
+ export declare function readConfigFile(path: string): Promise<unknown>;
6
+ /**
7
+ * Source order of the keys under one top-level object. Object.keys sorts all-integer keys
8
+ * to the front, and NAME_PATTERN lets a profile be called 2024, so the syntax tree is the
9
+ * only honest answer to "which profile was written first".
10
+ */
11
+ export declare function orderedKeys(text: string, path: string, key: string): string[];
@@ -0,0 +1,12 @@
1
+ import type { Problem, RootConfig } from '../types';
2
+ type Bag = Record<string, unknown>;
3
+ /**
4
+ * Phase one checks shape, phase two checks cross-references. Every problem is
5
+ * collected with a JSON Pointer; nothing throws on the first failure.
6
+ */
7
+ export declare function validateConfig(cfg: unknown): {
8
+ config: RootConfig;
9
+ problems: Problem[];
10
+ };
11
+ export declare function isObj(v: unknown): v is Bag;
12
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { Identity } from '../types';
2
+ /**
3
+ * The single source of truth for `--user`, HOME, and every tmpfs uid=/gid=.
4
+ * Out-of-sync values give a blank window with no error, so nothing else may guess.
5
+ */
6
+ export declare function resolveIdentity(useRoot: boolean): Identity;
@@ -0,0 +1,3 @@
1
+ import type { LaunchPlan, Problem } from '../types';
2
+ /** Every check the launch depends on, collected so one run reports all of them at once. */
3
+ export declare function preflight(plan: LaunchPlan): Promise<Problem[]>;
@@ -0,0 +1,39 @@
1
+ import type { ChildProcess, StdioOptions } from 'node:child_process';
2
+ import type { Readable } from 'node:stream';
3
+ import type { DockerRunSpec } from '../types';
4
+ /** argv as one array, the way every caller here has it. */
5
+ export declare function spawnArgv(argv: string[], stdio: StdioOptions, detached?: boolean): ChildProcess;
6
+ /** Rejects when the spawn itself fails, so a missing binary lands where a bad exit code would. */
7
+ export declare function exited(proc: ChildProcess): Promise<number>;
8
+ export declare function collect(stream: Readable): Promise<string>;
9
+ /**
10
+ * Runs a short command and collects both streams. A missing binary is exit 127 with the spawn
11
+ * error as stderr, so every caller can report it the same way rather than throwing.
12
+ */
13
+ export declare function capture(argv: string[]): Promise<{
14
+ code: number;
15
+ stdout: string;
16
+ stderr: string;
17
+ }>;
18
+ /** The tee'd combined stream, and what waitForMarker watches. */
19
+ export declare const STDOUT_LOG = "stdout.log";
20
+ export interface RunOptions {
21
+ /** Run log directory; created if missing. Receives stdout.log. */
22
+ logDir: string;
23
+ /** Passed to `docker stop --timeout` when a signal arrives. */
24
+ stopTimeoutSeconds?: number;
25
+ }
26
+ /**
27
+ * Spawns docker directly rather than through a pipeline, so the game's status is the status.
28
+ * `exec docker run | tee` returns tee's code, which is why the old scripts always reported 0.
29
+ */
30
+ export declare function runContainer(spec: DockerRunSpec, opts: RunOptions): Promise<number>;
31
+ /** Grace docker gives the game before SIGKILL, and the budget every teardown is measured against. */
32
+ export declare const STOP_TIMEOUT_SECONDS = 10;
33
+ export declare function stopContainer(name: string, timeoutSeconds: number): Promise<void>;
34
+ /**
35
+ * Host-side marker watch. Watches container stdout AND the game's own log file: RimWorld
36
+ * sends Verse.Log output to -logfile, never to stdout, so a stdout-only watch can never
37
+ * match a RimWorld mod's message.
38
+ */
39
+ export declare function waitForMarker(sources: string[], marker: string, timeoutSeconds: number): Promise<boolean>;
@@ -0,0 +1,24 @@
1
+ import type { DockerRunSpec, Identity, LaunchPlan, Mount } from '../types';
2
+ /** Container-side XDG_RUNTIME_DIR. A sized tmpfs; display and audio sockets land inside it. */
3
+ export declare const CONTAINER_RUNTIME_DIR = "/tmp/xdg";
4
+ /** Where the run directory is bound, so `-logfile /logs/Player.log` lands beside stdout.log. */
5
+ export declare const CONTAINER_LOG_DIR = "/logs";
6
+ export declare function buildRunSpec(plan: LaunchPlan, modMounts: Mount[], identity: Identity): DockerRunSpec;
7
+ /** Instances of one profile run side by side, so the name has to carry which one this is. */
8
+ export declare function containerName(plan: LaunchPlan): string;
9
+ /** What the window is renamed to, so a taskbar full of worktrees is readable. */
10
+ export declare function windowTitle(plan: LaunchPlan): string;
11
+ export declare function toDockerArgs(spec: DockerRunSpec): string[];
12
+ /**
13
+ * The host X session a headed run joins. The cookie is looked up separately from the socket
14
+ * because XWayland under a display manager keeps it in XDG_RUNTIME_DIR, not ~/.Xauthority.
15
+ * Whether the socket directory is really there is checkBindSources' job, as with every bind.
16
+ */
17
+ export declare function x11Session(): {
18
+ display: string;
19
+ xauthority: string | null;
20
+ } | null;
21
+ export declare function waylandSocket(): {
22
+ source: string;
23
+ name: string;
24
+ } | null;