@gamecrate/cli 1.4.0 → 2.0.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 +7 -2
- package/dist/gamecrate.js +2752 -1002
- package/dist/lib.js +101 -2
- package/dist/types/cli/args.d.ts +6 -3
- package/dist/types/cli/output.d.ts +1 -1
- package/dist/types/cli/profile.d.ts +6 -1
- package/dist/types/cli/steam.d.ts +27 -0
- package/dist/types/config/load.d.ts +5 -0
- package/dist/types/config/validate.d.ts +23 -0
- package/dist/types/docker/preflight.d.ts +1 -1
- package/dist/types/docker/run.d.ts +8 -0
- package/dist/types/docker/spec.d.ts +12 -2
- package/dist/types/image/base.d.ts +11 -0
- package/dist/types/image/build.d.ts +26 -0
- package/dist/types/image/crane.d.ts +27 -0
- package/dist/types/image/gate.d.ts +18 -0
- package/dist/types/image/input.d.ts +40 -0
- package/dist/types/image/tags.d.ts +23 -0
- package/dist/types/launch/image.d.ts +45 -0
- package/dist/types/launch/prepare.d.ts +4 -10
- package/dist/types/launch/updates.d.ts +35 -0
- package/dist/types/mods/steamcmd.d.ts +34 -0
- package/dist/types/plugin.d.ts +1 -1
- package/dist/types/types.d.ts +54 -3
- package/package.json +1 -1
package/dist/lib.js
CHANGED
|
@@ -33,6 +33,7 @@ class GamecrateError extends Error {
|
|
|
33
33
|
var NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
34
34
|
|
|
35
35
|
// src/cli/args.ts
|
|
36
|
+
var ACTIVE = new WeakMap;
|
|
36
37
|
function parseResolution(value) {
|
|
37
38
|
const match = /^(\d+)x(\d+)$/i.exec(value);
|
|
38
39
|
const width = Number(match?.[1]);
|
|
@@ -181,6 +182,8 @@ var profile = obj({
|
|
|
181
182
|
alias: str.optional(),
|
|
182
183
|
aliases: strArray.optional(),
|
|
183
184
|
description: str.optional(),
|
|
185
|
+
gameVersion: str.optional(),
|
|
186
|
+
image: str.optional(),
|
|
184
187
|
detach: bool.optional(),
|
|
185
188
|
replace: bool.optional(),
|
|
186
189
|
build: oneOf(["auto", "always", "never"]).optional()
|
|
@@ -225,6 +228,95 @@ var libraryEntry = obj({
|
|
|
225
228
|
push('"subdir" must be a relative path inside the repo, with no ".." segment', ["subdir"]);
|
|
226
229
|
}
|
|
227
230
|
});
|
|
231
|
+
function repeats(entries, key) {
|
|
232
|
+
const seen = new Set;
|
|
233
|
+
const found = [];
|
|
234
|
+
entries.forEach((entry, index) => {
|
|
235
|
+
const name = entry?.[key];
|
|
236
|
+
if (typeof name !== "string")
|
|
237
|
+
return;
|
|
238
|
+
if (seen.has(name))
|
|
239
|
+
found.push({ index, name });
|
|
240
|
+
else
|
|
241
|
+
seen.add(name);
|
|
242
|
+
});
|
|
243
|
+
return found;
|
|
244
|
+
}
|
|
245
|
+
var TAG_COMPONENT = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
|
|
246
|
+
function steamBuildRules(ctx) {
|
|
247
|
+
const push = (message, path, suggestion) => {
|
|
248
|
+
ctx.issues.push({
|
|
249
|
+
code: "custom",
|
|
250
|
+
message,
|
|
251
|
+
path,
|
|
252
|
+
input: ctx.value,
|
|
253
|
+
...suggestion === undefined ? {} : { params: { suggestion } }
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
const branches = ctx.value["branches"];
|
|
257
|
+
const variants = ctx.value["variants"];
|
|
258
|
+
if (Array.isArray(variants) && variants.length === 0) {
|
|
259
|
+
push("steamBuild.variants cannot be empty", ["variants"]);
|
|
260
|
+
}
|
|
261
|
+
if (Array.isArray(branches)) {
|
|
262
|
+
if (branches.length === 0)
|
|
263
|
+
push("steamBuild.branches cannot be empty", ["branches"]);
|
|
264
|
+
for (const dup of repeats(branches, "name")) {
|
|
265
|
+
push(`duplicate branch name "${dup.name}"`, ["branches", dup.index, "name"]);
|
|
266
|
+
}
|
|
267
|
+
branches.forEach((branch, index) => {
|
|
268
|
+
const name = branch?.["name"];
|
|
269
|
+
if (typeof name === "string" && !TAG_COMPONENT.test(name)) {
|
|
270
|
+
push(`branch name "${name}" must match ${TAG_COMPONENT.source}`, ["branches", index, "name"]);
|
|
271
|
+
}
|
|
272
|
+
const tags = branch?.["tags"];
|
|
273
|
+
if (!Array.isArray(tags))
|
|
274
|
+
return;
|
|
275
|
+
tags.forEach((tag, at) => {
|
|
276
|
+
if (typeof tag !== "string" || TAG_COMPONENT.test(tag))
|
|
277
|
+
return;
|
|
278
|
+
push(`branch tag "${String(tag)}" must match ${TAG_COMPONENT.source}`, ["branches", index, "tags", at]);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (!Array.isArray(variants))
|
|
283
|
+
return;
|
|
284
|
+
for (const dup of repeats(variants, "name")) {
|
|
285
|
+
push(`duplicate variant name "${dup.name}"`, ["variants", dup.index, "name"]);
|
|
286
|
+
}
|
|
287
|
+
variants.forEach((variant, index) => {
|
|
288
|
+
const v = variant;
|
|
289
|
+
const base = v?.["base"];
|
|
290
|
+
if (base !== "xvfb" && base !== "proton")
|
|
291
|
+
return;
|
|
292
|
+
const depot = v?.["depot"] ?? "linux";
|
|
293
|
+
if (depot === "macos") {
|
|
294
|
+
push("a macos depot cannot be runnable", ["variants", index, "base"], 'set base to "none"; no macos container runtime exists');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const wants = depot === "windows" ? "proton" : "xvfb";
|
|
298
|
+
if (base === wants)
|
|
299
|
+
return;
|
|
300
|
+
push(`a ${String(depot)} depot cannot run on the "${base}" base`, ["variants", index, "base"], depot === "windows" ? 'set base to "proton"; it is the only base with wine' : 'set base to "xvfb", or set depot to "windows" if the image should run under wine');
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
var steamBuildSchema = obj({
|
|
304
|
+
branches: z.array(obj({
|
|
305
|
+
name: str,
|
|
306
|
+
password: bool.optional(),
|
|
307
|
+
tags: strArray.optional(),
|
|
308
|
+
executable: z.record(str, str).optional()
|
|
309
|
+
}), {
|
|
310
|
+
error: "expected an array"
|
|
311
|
+
}),
|
|
312
|
+
variants: z.array(obj({
|
|
313
|
+
name: str,
|
|
314
|
+
depot: oneOf(["linux", "windows", "macos"]).optional(),
|
|
315
|
+
base: oneOf(["xvfb", "proton", "none"]),
|
|
316
|
+
include: strArray,
|
|
317
|
+
executable: str.optional()
|
|
318
|
+
}), { error: "expected an array" })
|
|
319
|
+
}).check(steamBuildRules);
|
|
228
320
|
var game = obj({
|
|
229
321
|
gameFiles: obj({ source: oneOf(["mount", "image"]), host: str.optional(), container: str }).check(requiredWhen("host", (v) => v["source"] === "mount")),
|
|
230
322
|
dataDir: obj({
|
|
@@ -235,7 +327,12 @@ var game = obj({
|
|
|
235
327
|
}).check(requiredWhen("arg", (v) => v["mode"] === "arg"), requiredWhen("env", (v) => v["mode"] === "env")),
|
|
236
328
|
modsDir: obj({ container: str, mask: strArray.optional() }),
|
|
237
329
|
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")),
|
|
238
|
-
image: obj({
|
|
330
|
+
image: obj({
|
|
331
|
+
ref: str,
|
|
332
|
+
acquire: oneOf(["pull", "build"]),
|
|
333
|
+
context: str.optional(),
|
|
334
|
+
updates: obj({ check: bool.optional(), everyHours: num.optional() }).optional()
|
|
335
|
+
}).check(requiredWhen("context", (v) => v["acquire"] === "build")),
|
|
239
336
|
executable: str,
|
|
240
337
|
steamAppId: num,
|
|
241
338
|
workshopRoot: z.union([z.string(), z.null()], { error: "expected a string or null" }),
|
|
@@ -245,6 +342,8 @@ var game = obj({
|
|
|
245
342
|
manifest: obj({ file: str }),
|
|
246
343
|
modsConfig: obj({ file: str }),
|
|
247
344
|
prefs: obj({ file: str }),
|
|
345
|
+
version: obj({ file: str }),
|
|
346
|
+
steamBuild: steamBuildSchema,
|
|
248
347
|
saveExtensions: strArray,
|
|
249
348
|
core: str,
|
|
250
349
|
dlc: strArray,
|
|
@@ -343,7 +442,7 @@ var PROJECT_SCHEMA = PROJECT_OBJECT.check((ctx) => {
|
|
|
343
442
|
});
|
|
344
443
|
|
|
345
444
|
// src/plugin.ts
|
|
346
|
-
var PLUGIN_API_VERSION =
|
|
445
|
+
var PLUGIN_API_VERSION = 2;
|
|
347
446
|
export {
|
|
348
447
|
PLUGIN_API_VERSION
|
|
349
448
|
};
|
package/dist/types/cli/args.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
1
|
+
import { Command, Option } from 'commander';
|
|
2
2
|
import type { BuildPolicy, ParsedArgs, ProfileConfig, ProjectDefaults } from '../types';
|
|
3
3
|
export type PositionalSlot = 'game' | 'profile' | 'rest';
|
|
4
4
|
export interface SubcommandSpec {
|
|
@@ -19,10 +19,13 @@ export declare const GLOBAL_FLAGS: readonly ["--json", "--help"];
|
|
|
19
19
|
/** The GAMECRATE_ vars, by the flag they stand in for. Fallbacks only; a flag always wins. */
|
|
20
20
|
export declare const FLAG_ENV: Record<string, string>;
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* A root that carries the run flags, because a bare game name means run, plus one command per
|
|
23
|
+
* verb carrying only its own. enablePositionalOptions is what makes that a fence: without it
|
|
24
|
+
* commander hands a flag typed after a verb back to the root and accepts it.
|
|
24
25
|
*/
|
|
25
26
|
export declare function buildProgram(): Command;
|
|
27
|
+
/** Every command's options, for the checks that have to see the whole vocabulary. */
|
|
28
|
+
export declare function allOptions(program: Command): Option[];
|
|
26
29
|
export interface ParseOptions {
|
|
27
30
|
env?: Record<string, string | undefined>;
|
|
28
31
|
defaults?: ProjectDefaults;
|
|
@@ -20,7 +20,7 @@ export interface PlanModPayload {
|
|
|
20
20
|
containerDir: string;
|
|
21
21
|
origin: 'explicit' | 'auto';
|
|
22
22
|
stale: boolean;
|
|
23
|
-
staleReport?: ResolvedMod['staleReport']
|
|
23
|
+
staleReport?: NonNullable<ResolvedMod['staleReport']>;
|
|
24
24
|
workshopId?: number;
|
|
25
25
|
}
|
|
26
26
|
export interface PlanPayload {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type { ParsedArgs, ProjectDefaults } from '../types';
|
|
1
|
+
import type { GameConfig, ParsedArgs, ProjectDefaults } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Not in parseArgs: filling a default there would erase the difference between a typed
|
|
4
4
|
* profile and a defaulted one, which clean and fix-perms both need.
|
|
5
5
|
*/
|
|
6
6
|
export declare function profileOf(args: ParsedArgs, defaults: ProjectDefaults): string;
|
|
7
|
+
/**
|
|
8
|
+
* The launch path, where a silent fallback to modless drops every mod the game needs. With no
|
|
9
|
+
* profile configured there is nothing to be ambiguous about, so modless still answers.
|
|
10
|
+
*/
|
|
11
|
+
export declare function launchProfile(args: ParsedArgs, defaults: ProjectDefaults, game: GameConfig): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { GamePlugin } from '../plugin';
|
|
2
|
+
import type { ParsedArgs, RootConfig } from '../types';
|
|
3
|
+
export interface SteamContext {
|
|
4
|
+
config: RootConfig;
|
|
5
|
+
plugins: Map<string, GamePlugin>;
|
|
6
|
+
/** Where a bare plugin specifier resolves from. Injectable so tests need no chdir. */
|
|
7
|
+
cwd: string;
|
|
8
|
+
/** The file loadConfig read, so config.plugins resolves here the way it does there. */
|
|
9
|
+
configFile?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Every layout a steamcmd writes config.vdf into, measured 2026-09-23: the arch wrapper writes
|
|
13
|
+
* .steam/config, the docker image writes .local/share/Steam/config, a valve tarball writes Steam/config.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sessionPaths(home: string): string[];
|
|
16
|
+
export declare function findSession(home: string): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* STEAM_CONFIG_VDF, then the file `steam login` wrote, then a hand-primed session in the user's
|
|
19
|
+
* own home. An expired session reports as a missing one: from out here the two files look identical.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveSession(config: RootConfig, env?: Record<string, string | undefined>): string;
|
|
22
|
+
export declare function steamBuildCommand(args: ParsedArgs, ctx: SteamContext): Promise<number>;
|
|
23
|
+
/**
|
|
24
|
+
* The one interactive path in gamecrate: steamcmd gets the TTY so it can ask for the password and
|
|
25
|
+
* the 2FA code itself. Neither ever passes through an argv we build.
|
|
26
|
+
*/
|
|
27
|
+
export declare function steamLogin(args: ParsedArgs, ctx: SteamContext): Promise<number>;
|
|
@@ -39,6 +39,11 @@ export declare function profileKey(game: GameConfig, name: string): string | und
|
|
|
39
39
|
/** Removes entries whose id matches an exclusion. Dynamic entries are filtered after expansion. */
|
|
40
40
|
export declare function subtract(mods: ModEntry[], exclude: string[]): ModEntry[];
|
|
41
41
|
export declare function globToRegExp(pattern: string): RegExp;
|
|
42
|
+
/**
|
|
43
|
+
* deepMerge, then the one array that concatenates. `branches[0]` gets the bare tag, so a
|
|
44
|
+
* replacing override would move it silently and drop the plugin's other branches.
|
|
45
|
+
*/
|
|
46
|
+
export declare function mergeUserConfig(base: RootConfig, user: unknown): RootConfig;
|
|
42
47
|
/**
|
|
43
48
|
* A later list replaces an earlier one, so a user can shorten a plugin's `dlc` or `modes`.
|
|
44
49
|
* `concatArrays` is the settings ladder's rule, where gameArgs accumulate across layers.
|
|
@@ -1,5 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { Problem, RootConfig } from '../types';
|
|
2
3
|
type Bag = Record<string, unknown>;
|
|
4
|
+
/** Exported so a config-less `steam build` gets the same refusals a config file would. */
|
|
5
|
+
export declare const steamBuildSchema: z.ZodObject<{
|
|
6
|
+
branches: z.ZodArray<z.ZodObject<{
|
|
7
|
+
name: z.ZodString;
|
|
8
|
+
password: z.ZodOptional<z.ZodBoolean>;
|
|
9
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
10
|
+
executable: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
11
|
+
}, z.core.$strict>>;
|
|
12
|
+
variants: z.ZodArray<z.ZodObject<{
|
|
13
|
+
name: z.ZodString;
|
|
14
|
+
depot: z.ZodOptional<z.ZodEnum<{
|
|
15
|
+
linux: "linux";
|
|
16
|
+
windows: "windows";
|
|
17
|
+
macos: "macos";
|
|
18
|
+
}>>;
|
|
19
|
+
base: z.ZodEnum<{
|
|
20
|
+
[x: string]: string;
|
|
21
|
+
}>;
|
|
22
|
+
include: z.ZodArray<z.ZodString>;
|
|
23
|
+
executable: z.ZodOptional<z.ZodString>;
|
|
24
|
+
}, z.core.$strict>>;
|
|
25
|
+
}, z.core.$strict>;
|
|
3
26
|
/**
|
|
4
27
|
* Phase one checks shape, phase two checks cross-references. Every problem is
|
|
5
28
|
* collected with a JSON Pointer; nothing throws on the first failure.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { LaunchPlan, Problem } from '../types';
|
|
2
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[]>;
|
|
3
|
+
export declare function preflight(plan: LaunchPlan, asShell?: boolean): Promise<Problem[]>;
|
|
@@ -15,6 +15,14 @@ export declare function capture(argv: string[]): Promise<{
|
|
|
15
15
|
stdout: string;
|
|
16
16
|
stderr: string;
|
|
17
17
|
}>;
|
|
18
|
+
/**
|
|
19
|
+
* Streams a child's output while keeping it, for a caller that parses it too. Both streams as
|
|
20
|
+
* one string because the parse reads them as one, and to stderr because --json owns stdout.
|
|
21
|
+
*/
|
|
22
|
+
export declare function captureLive(argv: string[], env?: NodeJS.ProcessEnv): Promise<{
|
|
23
|
+
code: number;
|
|
24
|
+
text: string;
|
|
25
|
+
}>;
|
|
18
26
|
/** The tee'd combined stream, and what waitForMarker watches. */
|
|
19
27
|
export declare const STDOUT_LOG = "stdout.log";
|
|
20
28
|
export interface RunOptions {
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import type { DockerRunSpec, Identity, LaunchPlan, Mount } from '../types';
|
|
1
|
+
import type { DockerRunSpec, Identity, LaunchPlan, ModeName, Mount } from '../types';
|
|
2
2
|
/** Container-side XDG_RUNTIME_DIR. A sized tmpfs; display and audio sockets land inside it. */
|
|
3
3
|
export declare const CONTAINER_RUNTIME_DIR = "/tmp/xdg";
|
|
4
4
|
/** Where the run directory is bound, so `-logfile /logs/Player.log` lands beside stdout.log. */
|
|
5
5
|
export declare const CONTAINER_LOG_DIR = "/logs";
|
|
6
|
-
|
|
6
|
+
/** What the image says about starting itself. Absent means fall back to config. */
|
|
7
|
+
export interface ImageLaunch {
|
|
8
|
+
launcher?: 'direct' | 'proton';
|
|
9
|
+
executable?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Explorer detaches under wine, so a proton image has no headed path at all. run() calls this
|
|
13
|
+
* before the marker gate, so the mode is the first thing a person reads.
|
|
14
|
+
*/
|
|
15
|
+
export declare function refuseProtonHeaded(game: string, mode: ModeName, image?: ImageLaunch): void;
|
|
16
|
+
export declare function buildRunSpec(plan: LaunchPlan, modMounts: Mount[], identity: Identity, image?: ImageLaunch): DockerRunSpec;
|
|
7
17
|
/** Instances of one profile run side by side, so the name has to carry which one this is. */
|
|
8
18
|
export declare function containerName(plan: LaunchPlan): string;
|
|
9
19
|
/** What the window is renamed to, so a taskbar full of worktrees is readable. */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type BaseKind = 'xvfb' | 'proton' | 'none';
|
|
2
|
+
/**
|
|
3
|
+
* Pinned by digest, never by tag: a rebuilt base would otherwise change under released code.
|
|
4
|
+
* These are the linux/amd64 manifests, not the index a push reports. See landmines.md.
|
|
5
|
+
*/
|
|
6
|
+
export declare const RUNTIME_BASE: Readonly<Record<'xvfb' | 'proton', string>>;
|
|
7
|
+
/**
|
|
8
|
+
* The ref a variant's layer gets appended onto. null for 'none': a reference image
|
|
9
|
+
* appends onto scratch and never runs, so `--base` does not make one runnable.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveBase(kind: BaseKind, override?: string): string | null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RootConfig } from '../types';
|
|
2
|
+
import type { SteamBuildInput } from './input';
|
|
3
|
+
export interface CellResult {
|
|
4
|
+
branch: string;
|
|
5
|
+
variant: string;
|
|
6
|
+
status: 'built' | 'skipped' | 'failed';
|
|
7
|
+
reason: string;
|
|
8
|
+
tags: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface SteamBuildOptions {
|
|
11
|
+
config: RootConfig;
|
|
12
|
+
push: boolean;
|
|
13
|
+
load: boolean;
|
|
14
|
+
platform: string;
|
|
15
|
+
baseOverride?: string;
|
|
16
|
+
force: boolean;
|
|
17
|
+
/** --beta, repeatable. Undefined or empty means every declared branch. */
|
|
18
|
+
onlyBranches?: string[];
|
|
19
|
+
/** --variant, repeatable. Undefined or empty means every declared variant. */
|
|
20
|
+
onlyVariants?: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One row per cell attempted, in branch order then variant order. A cell failure is a row and
|
|
24
|
+
* never a throw, because a throw would lose the rows for the cells that worked.
|
|
25
|
+
*/
|
|
26
|
+
export declare function steamBuild(input: SteamBuildInput, opts: SteamBuildOptions): Promise<CellResult[]>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The debug tag, because the default one has no shell and the tar step needs one. */
|
|
2
|
+
export declare const CRANE_IMAGE = "gcr.io/go-containerregistry/crane:debug";
|
|
3
|
+
/**
|
|
4
|
+
* Runs before the first download. Only a helper covering the push target is a problem: its binary
|
|
5
|
+
* is not in the crane container, so the push would go out anonymous for no visible reason.
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkRegistryAuthEarly(ref: string): void;
|
|
8
|
+
export declare function craneAppend(opts: {
|
|
9
|
+
gameDir: string;
|
|
10
|
+
/** Subpaths to include. Empty means the whole game. */
|
|
11
|
+
include: string[];
|
|
12
|
+
/** Where the game goes inside the image, e.g. /game. */
|
|
13
|
+
gamePath: string;
|
|
14
|
+
/** null appends onto an empty base, which is what a reference image wants. */
|
|
15
|
+
base: string | null;
|
|
16
|
+
platform: string;
|
|
17
|
+
/** The name the tar carries. crane refuses an append without one, and docker load reads it. */
|
|
18
|
+
tag: string;
|
|
19
|
+
out: string;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
/** The one call that retries. A 502 from a registry should not cost a two gigabyte download. */
|
|
22
|
+
export declare function cranePush(tar: string, ref: string): Promise<void>;
|
|
23
|
+
/** Runs after the push, never before. A moving tag only moves once the real tag is there. */
|
|
24
|
+
export declare function craneTag(ref: string, tag: string): Promise<void>;
|
|
25
|
+
export declare function craneMutateLabels(ref: string, labels: Record<string, string>): Promise<void>;
|
|
26
|
+
/** null when the image or its config cannot be read. An empty record means no labels. */
|
|
27
|
+
export declare function craneLabels(ref: string): Promise<Record<string, string> | null>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface GateInput {
|
|
2
|
+
/** What steam says the branch is at. null when steamcmd reported none for this branch. */
|
|
3
|
+
published: string | null;
|
|
4
|
+
/** False when no image exists yet. */
|
|
5
|
+
imagePresent: boolean;
|
|
6
|
+
/** The steam.buildid label, null when absent. Ignored when imagePresent is false. */
|
|
7
|
+
labelled: string | null;
|
|
8
|
+
force: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface GateDecision {
|
|
11
|
+
build: boolean;
|
|
12
|
+
reason: 'forced' | 'no-image' | 'no-label' | 'buildid-changed' | 'up-to-date' | 'unknown-published';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A skip needs proof, so everything short of two matching buildids builds.
|
|
16
|
+
* Callers pass `imagePresent: labels !== null`, `labelled: labels?.['steam.buildid'] ?? null`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function decideGate(input: GateInput): GateDecision;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RootConfig, SteamBranch, SteamVariant } from '../types';
|
|
2
|
+
export interface SteamBuildInput {
|
|
3
|
+
game: string;
|
|
4
|
+
steamAppId: number;
|
|
5
|
+
versionFile: string;
|
|
6
|
+
gamePath: string;
|
|
7
|
+
executable: string;
|
|
8
|
+
branches: SteamBranch[];
|
|
9
|
+
variants: SteamVariant[];
|
|
10
|
+
/** Target repo, never a tag. p3-t1 supplies the tags. */
|
|
11
|
+
image: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SteamBuildOverrides {
|
|
14
|
+
/** --image, a repo with no tag. */
|
|
15
|
+
image?: string;
|
|
16
|
+
/** --plugin, repeatable. Skips the @gamecrate/<game> convention. */
|
|
17
|
+
plugins?: string[];
|
|
18
|
+
/** --push. Only a push makes --image mandatory. */
|
|
19
|
+
push?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** "1.5-test" -> "STEAM_BRANCH_PASSWORD_1_5_TEST" */
|
|
22
|
+
export declare function branchPasswordKey(branch: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* The keyed variable names its branch, so it answers whatever the config says. The bare one hits
|
|
25
|
+
* every branch in a build, so it stays behind `password: true`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function branchPassword(branch: SteamBranch): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* `configured` is --image if given, else the config's ref. --load gets a default because a local
|
|
30
|
+
* build has no registry to name; --push refuses, because a guessed path pushes to the wrong account.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveImage(flags: {
|
|
33
|
+
load: boolean;
|
|
34
|
+
push: boolean;
|
|
35
|
+
}, game: string, configured: string | undefined): string;
|
|
36
|
+
/**
|
|
37
|
+
* Plugin defaults, then games.<game> when a config exists, then the flags. The same
|
|
38
|
+
* one-direction merge the loader already does, over fewer fields.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveSteamBuildInput(game: string, config: RootConfig | null, overrides: SteamBuildOverrides, cwd: string, configFile?: string): Promise<SteamBuildInput>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface TagInput {
|
|
2
|
+
version: string;
|
|
3
|
+
branch: string;
|
|
4
|
+
variant: string;
|
|
5
|
+
/** True when this branch is the first entry of steamBuild.branches. */
|
|
6
|
+
defaultBranch: boolean;
|
|
7
|
+
/** True when this variant is the first entry of steamBuild.variants. */
|
|
8
|
+
defaultVariant: boolean;
|
|
9
|
+
/** Branch aliases, so a moving "2.0" can point at whatever beta is today. */
|
|
10
|
+
aliases?: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* An OCI tag takes no space, so the first whitespace-delimited field is the version and the
|
|
14
|
+
* rest of the line is dropped. "1.6.4871 rev598" becomes "1.6.4871".
|
|
15
|
+
*/
|
|
16
|
+
export declare function sanitizeVersion(raw: string, fallback: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* The moving prefixes of a dotted version: 1.6.4871 gives 1 and 1.6, never the whole thing.
|
|
19
|
+
* A prefix follows the newest build that carries it, the way `latest` does.
|
|
20
|
+
*/
|
|
21
|
+
export declare function versionPrefixes(version: string): string[];
|
|
22
|
+
/** Every tag this cell writes. Versioned forms first, latest forms after. */
|
|
23
|
+
export declare function tagsFor(input: TagInput): string[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ImageLaunch } from '../docker/spec';
|
|
2
|
+
import type { ModeName, Problem } from '../types';
|
|
3
|
+
import type { GameConfig } from '../types';
|
|
4
|
+
/** What an image says about itself. Every field is null for an image gamecrate did not build. */
|
|
5
|
+
export interface ImageFacts {
|
|
6
|
+
present: boolean;
|
|
7
|
+
/** gamecrate.runtime: the base digest a steam build appended onto. */
|
|
8
|
+
runtime: string | null;
|
|
9
|
+
launcher: string | null;
|
|
10
|
+
executable: string | null;
|
|
11
|
+
/** The cell that produced this image, and the steam build it carries. */
|
|
12
|
+
branch: string | null;
|
|
13
|
+
variant: string | null;
|
|
14
|
+
buildid: string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function readImageFacts(ref: string): Promise<ImageFacts>;
|
|
17
|
+
/** The labels buildRunSpec reads. An unknown launcher is dropped, not passed through. */
|
|
18
|
+
export declare function imageLaunch(facts: ImageFacts): ImageLaunch;
|
|
19
|
+
export declare function imageProblem(input: {
|
|
20
|
+
game: string;
|
|
21
|
+
ref: string;
|
|
22
|
+
mode: ModeName;
|
|
23
|
+
facts: ImageFacts;
|
|
24
|
+
}): Problem | null;
|
|
25
|
+
/**
|
|
26
|
+
* Explorer detaches under wine, so the game's exit code never reaches gamecrate. A marker is the
|
|
27
|
+
* only success signal a proton variant has, and without one a crash reads like a clean run.
|
|
28
|
+
*/
|
|
29
|
+
export declare function markerProblem(input: {
|
|
30
|
+
game: string;
|
|
31
|
+
facts: ImageFacts;
|
|
32
|
+
marker: string | undefined;
|
|
33
|
+
}): Problem | null;
|
|
34
|
+
/**
|
|
35
|
+
* The flag wins, then the profile's own ref, then its version tag on the game's repository.
|
|
36
|
+
* A profile that names neither leaves the configured ref alone.
|
|
37
|
+
*/
|
|
38
|
+
export declare function imageFor(game: GameConfig, profile: string, flag?: string): string | undefined;
|
|
39
|
+
/** The ref without its tag. A colon after the last slash is a tag; before it, a registry port. */
|
|
40
|
+
export declare function repoOf(ref: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* `--image <ref>` launches one gamecrate-built image. The game lives inside such an image, so
|
|
43
|
+
* the host mount goes with it: a bind over /game would shadow what the image carries.
|
|
44
|
+
*/
|
|
45
|
+
export declare function withImageOverride(game: GameConfig, ref?: string): GameConfig;
|
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import type { BuildPolicy, GameConfig, LaunchPlan, PullPolicy } from '../types';
|
|
2
2
|
/** Resolved image id, so `launches.jsonl` records what actually ran, not a floating tag. */
|
|
3
3
|
export declare function imageDigest(ref: string): Promise<string | null>;
|
|
4
|
+
/** One file out of an image, for a fact the host copy would answer wrongly. */
|
|
5
|
+
export declare function readFromImage(ref: string, path: string): Promise<string | null>;
|
|
6
|
+
/** One label off an image, or null when the image or the label is missing. */
|
|
7
|
+
export declare function imageLabel(ref: string, label: string): Promise<string | null>;
|
|
4
8
|
/**
|
|
5
9
|
* Pulls or builds per policy. Shared by the `build` subcommand and `run`, so a launch can no
|
|
6
10
|
* longer proceed against an image the user asked to refresh.
|
|
7
11
|
*/
|
|
8
12
|
export declare function acquireImage(game: string, config: GameConfig, pull: PullPolicy): Promise<void>;
|
|
9
|
-
/**
|
|
10
|
-
* Tag for the derived image, keeping the registry path readable. A tag only exists after the
|
|
11
|
-
* last `/`: before it a colon is a registry port, and an `@` means a digest no suffix can ride.
|
|
12
|
-
*/
|
|
13
|
-
export declare function runtimeLayerRef(ref: string): string;
|
|
14
|
-
/**
|
|
15
|
-
* Builds (once) a thin layer over the adapter's image carrying an X server and imagemagick.
|
|
16
|
-
* Detects the package manager so a debian-based game image works the same as an Arch one.
|
|
17
|
-
*/
|
|
18
|
-
export declare function ensureRuntimeLayer(ref: string): Promise<string>;
|
|
19
13
|
/**
|
|
20
14
|
* Builds local mods before launch. `always` builds every local mod; `auto` builds only the
|
|
21
15
|
* ones resolution flagged stale; `never` skips. A build failure stops the launch, shipping
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ImageFacts } from './image';
|
|
2
|
+
import type { ParsedArgs, RootConfig } from '../types';
|
|
3
|
+
import type { UpdateCheckSpec } from '../types';
|
|
4
|
+
export interface CheckInput {
|
|
5
|
+
facts: ImageFacts;
|
|
6
|
+
spec: UpdateCheckSpec | undefined;
|
|
7
|
+
/** null when this image has never been checked. */
|
|
8
|
+
lastCheckedAt: number | null;
|
|
9
|
+
now: number;
|
|
10
|
+
}
|
|
11
|
+
export type SkipReason = 'disabled' | 'not-ours' | 'throttled';
|
|
12
|
+
/**
|
|
13
|
+
* An update check spends four seconds and needs a steam session, so it runs only for an image
|
|
14
|
+
* gamecrate built, only when asked, and only once per window.
|
|
15
|
+
*/
|
|
16
|
+
export declare function shouldCheck(input: CheckInput): {
|
|
17
|
+
check: boolean;
|
|
18
|
+
reason?: SkipReason;
|
|
19
|
+
};
|
|
20
|
+
export declare function lastCheckedAt(imageId: string): number | null;
|
|
21
|
+
export declare function recordCheck(imageId: string, now: number): void;
|
|
22
|
+
/**
|
|
23
|
+
* The throttled staleness check a launch runs. Rebuilds the one cell this image came from,
|
|
24
|
+
* after asking, because a rebuild is a multi-gigabyte download nobody asked for by typing a
|
|
25
|
+
* game name.
|
|
26
|
+
*/
|
|
27
|
+
export declare function offerRebuild(input: {
|
|
28
|
+
game: string;
|
|
29
|
+
config: RootConfig;
|
|
30
|
+
args: ParsedArgs;
|
|
31
|
+
facts: ImageFacts;
|
|
32
|
+
cwd: string;
|
|
33
|
+
configFile?: string;
|
|
34
|
+
ask: (question: string) => Promise<boolean>;
|
|
35
|
+
}): Promise<void>;
|
|
@@ -15,6 +15,13 @@ export type SteamcmdRunner = {
|
|
|
15
15
|
};
|
|
16
16
|
/** HOME for steamcmd. Everything it writes hangs off here, so it is per-dataRoot, not the user's. */
|
|
17
17
|
export declare function steamHome(dataRoot: string): string;
|
|
18
|
+
/** Where `steam login` records the account it signed in with, so a build needs no variable. */
|
|
19
|
+
export declare function accountFile(dataRoot: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* One answer to "who is logging in": STEAM_USERNAME for CI, else the name `steam login` wrote.
|
|
22
|
+
* Neither is an error rather than a null, because a null reads downstream as "steam said nothing".
|
|
23
|
+
*/
|
|
24
|
+
export declare function steamAccount(dataRoot: string): string;
|
|
18
25
|
/**
|
|
19
26
|
* Where downloads land. `run` pins it with `+force_install_dir`, so this is our layout rather
|
|
20
27
|
* than whichever one the steamcmd on this machine would have picked: measured 2026-09-21, the
|
|
@@ -59,3 +66,30 @@ export interface DownloadReport {
|
|
|
59
66
|
* unavailable item pays the 3.4s connect for every pass that will never succeed.
|
|
60
67
|
*/
|
|
61
68
|
export declare function downloadItems(config: RootConfig, game: GameConfig, dataRoot: string, ids: string[]): Promise<DownloadReport>;
|
|
69
|
+
/**
|
|
70
|
+
* Per branch and per depot, not per variant: `linux` and `linux-ref` come out of one +app_update.
|
|
71
|
+
* A shared directory means the second +app_update rewrites the first one's files.
|
|
72
|
+
*/
|
|
73
|
+
export declare function appDownloadRoot(dataRoot: string, appId: number, branch: string, depot?: string): string;
|
|
74
|
+
export interface AppDownload {
|
|
75
|
+
dir: string;
|
|
76
|
+
warnings: string[];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A wrong -betapassword and a branch that does not exist look identical from outside: steam
|
|
80
|
+
* reports both as the branch being unavailable, so the error names both.
|
|
81
|
+
*/
|
|
82
|
+
export declare function downloadApp(config: RootConfig, opts: {
|
|
83
|
+
steamAppId: number;
|
|
84
|
+
branch: string;
|
|
85
|
+
depot?: 'linux' | 'windows' | 'macos';
|
|
86
|
+
password?: string;
|
|
87
|
+
dataRoot: string;
|
|
88
|
+
}): Promise<AppDownload>;
|
|
89
|
+
/** null when steam reports no buildid for that branch. A missing account throws, it is not a null. */
|
|
90
|
+
export declare function publishedBuildId(config: RootConfig, appId: number, branch: string): Promise<string | null>;
|
|
91
|
+
/**
|
|
92
|
+
* The buildid sits at depots -> branches -> <branch> -> buildid, and the branch name shows up
|
|
93
|
+
* elsewhere too, so anchor on "branches", then the branch key, then its first "buildid".
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildIdFor(output: string, branch: string): string | null;
|
package/dist/types/plugin.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GameConfig, ModManifest } from './types';
|
|
2
2
|
/** Bumped when a change would make an older plugin misbehave rather than merely lag. */
|
|
3
|
-
export declare const PLUGIN_API_VERSION =
|
|
3
|
+
export declare const PLUGIN_API_VERSION = 2;
|
|
4
4
|
export interface ModsConfigInput {
|
|
5
5
|
version: string;
|
|
6
6
|
buildNumber: number;
|