@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/dist/lib.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { exports as exportsField, legacy } from "resolve.exports";
|
|
3
3
|
|
|
4
4
|
// src/config/load.ts
|
|
5
|
-
import { parse as parseYaml } from "yaml";
|
|
6
5
|
import { z as z2 } from "zod";
|
|
7
6
|
|
|
8
7
|
// src/cli/args.ts
|
|
@@ -21,7 +20,6 @@ var Exit = {
|
|
|
21
20
|
Stale: 8,
|
|
22
21
|
Interrupted: 130
|
|
23
22
|
};
|
|
24
|
-
|
|
25
23
|
class GamecrateError extends Error {
|
|
26
24
|
code;
|
|
27
25
|
detail;
|
|
@@ -74,6 +72,10 @@ function distance(a, b) {
|
|
|
74
72
|
return prev[b.length];
|
|
75
73
|
}
|
|
76
74
|
|
|
75
|
+
// src/config/read.ts
|
|
76
|
+
import { parseTree } from "jsonc-parser";
|
|
77
|
+
import { isMap, parse as parseYaml, parseDocument } from "yaml";
|
|
78
|
+
|
|
77
79
|
// src/config/jsonc.ts
|
|
78
80
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
79
81
|
|
|
@@ -177,7 +179,11 @@ var profile = obj({
|
|
|
177
179
|
error: "expected an object"
|
|
178
180
|
}).optional(),
|
|
179
181
|
alias: str.optional(),
|
|
180
|
-
aliases: strArray.optional()
|
|
182
|
+
aliases: strArray.optional(),
|
|
183
|
+
description: str.optional(),
|
|
184
|
+
detach: bool.optional(),
|
|
185
|
+
replace: bool.optional(),
|
|
186
|
+
build: oneOf(["auto", "always", "never"]).optional()
|
|
181
187
|
}).check((ctx) => {
|
|
182
188
|
const v = ctx.value;
|
|
183
189
|
if (v.alias !== undefined && (v.extends !== undefined || v.mods !== undefined)) {
|
|
@@ -257,9 +263,12 @@ var projectResolution = z2.string({ error: "expected dimensions like 1920x1080"
|
|
|
257
263
|
ctx.issues.push({ code: "custom", message: error.message, input: ctx.value });
|
|
258
264
|
}
|
|
259
265
|
}).transform(parseResolution);
|
|
260
|
-
var
|
|
266
|
+
var PROJECT_OBJECT = z2.strictObject({
|
|
261
267
|
game: projectName.optional(),
|
|
262
|
-
|
|
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(),
|
|
263
272
|
mods: projectList.optional(),
|
|
264
273
|
without: projectList.optional(),
|
|
265
274
|
only: projectList.optional(),
|
|
@@ -288,6 +297,21 @@ var PROJECT_SCHEMA = z2.strictObject({
|
|
|
288
297
|
}).optional(),
|
|
289
298
|
resolution: projectResolution.optional()
|
|
290
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
|
+
});
|
|
291
315
|
|
|
292
316
|
// src/plugin.ts
|
|
293
317
|
var PLUGIN_API_VERSION = 1;
|
package/dist/types/cli/args.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import type { ParsedArgs, ProjectDefaults } from '../types';
|
|
2
|
+
import type { BuildPolicy, ParsedArgs, ProfileConfig, ProjectDefaults } from '../types';
|
|
3
3
|
export type PositionalSlot = 'game' | 'profile' | 'rest';
|
|
4
4
|
export interface SubcommandSpec {
|
|
5
5
|
name: string;
|
|
@@ -32,9 +32,25 @@ export interface ParseOptions {
|
|
|
32
32
|
* defaulting to `run`. Game args come after a bare `--` and nowhere else.
|
|
33
33
|
*/
|
|
34
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;
|
|
35
44
|
export declare function parseResolution(value: string): {
|
|
36
45
|
width: number;
|
|
37
46
|
height: number;
|
|
38
47
|
};
|
|
39
48
|
/** Closest candidate within an edit distance that scales with word length. */
|
|
40
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;
|
|
@@ -6,7 +6,7 @@ export declare function warn(message: string): void;
|
|
|
6
6
|
export interface OutputRedirect {
|
|
7
7
|
close(): void;
|
|
8
8
|
}
|
|
9
|
-
export declare function redirectOutput(path: string): OutputRedirect;
|
|
9
|
+
export declare function redirectOutput(path: string, keep?: boolean): OutputRedirect;
|
|
10
10
|
export declare function forwardOutput(stream: Readable, target: NodeJS.WriteStream): Promise<void>;
|
|
11
11
|
/**
|
|
12
12
|
* Every collected failure at once, grouped by location. Resolution stops before any
|
|
@@ -52,5 +52,27 @@ export declare function printPlan(plan: LaunchPlan, asJson: boolean): void;
|
|
|
52
52
|
export declare function runTimestamp(now?: Date): string;
|
|
53
53
|
/** Makes <logsDir>/runs/<ts>, repoints `current` at it, rotates the old ones, returns the dir. */
|
|
54
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[];
|
|
55
77
|
/** Landmine 7: a 180MB Player-prev.log was 68% of a profile tree. Retention is the cap. */
|
|
56
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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GameConfig, ModEntry, ProfileConfig, ProjectDefaults, RootConfig, Settings } from '../types';
|
|
2
2
|
import type { GamePlugin } from '../plugin';
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function globalConfigDir(): string;
|
|
4
|
+
export declare function findGlobalConfig(): Promise<string | undefined>;
|
|
4
5
|
export declare function findProjectConfig(start?: string): Promise<string | undefined>;
|
|
5
6
|
export declare function loadProjectDefaults(start?: string): Promise<ProjectDefaults>;
|
|
6
7
|
export interface LoadedConfig {
|
|
@@ -8,10 +9,10 @@ export interface LoadedConfig {
|
|
|
8
9
|
plugins: Map<string, GamePlugin>;
|
|
9
10
|
}
|
|
10
11
|
/**
|
|
11
|
-
* Reads
|
|
12
|
+
* Reads the global config, loads the plugins it lists, then merges the user's blocks over each
|
|
12
13
|
* plugin's defaults. A missing file means no games, which every non-launch subcommand survives.
|
|
13
14
|
*/
|
|
14
|
-
export declare function loadConfig(path?: string): Promise<LoadedConfig>;
|
|
15
|
+
export declare function loadConfig(path?: string, project?: ProjectDefaults): Promise<LoadedConfig>;
|
|
15
16
|
/** defaults -> games.<game> -> profile -> instance -> CLI. Scalars replace, arrays concatenate. */
|
|
16
17
|
export declare function resolveSettings(root: RootConfig, game: GameConfig, profile: ProfileConfig, ...overrides: (Partial<Settings> | undefined)[]): Settings;
|
|
17
18
|
/**
|
|
@@ -34,6 +35,7 @@ export declare function profileDataDir(root: RootConfig, game: string, profile:
|
|
|
34
35
|
* verbatim: a directory name is already a path, and canonicalizing it skips odd-cased ones.
|
|
35
36
|
*/
|
|
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;
|
|
37
39
|
/** Removes entries whose id matches an exclusion. Dynamic entries are filtered after expansion. */
|
|
38
40
|
export declare function subtract(mods: ModEntry[], exclude: string[]): ModEntry[];
|
|
39
41
|
export declare function globToRegExp(pattern: string): RegExp;
|
|
@@ -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[];
|
|
@@ -2,7 +2,7 @@ import type { ChildProcess, StdioOptions } from 'node:child_process';
|
|
|
2
2
|
import type { Readable } from 'node:stream';
|
|
3
3
|
import type { DockerRunSpec } from '../types';
|
|
4
4
|
/** argv as one array, the way every caller here has it. */
|
|
5
|
-
export declare function spawnArgv(argv: string[], stdio: StdioOptions): ChildProcess;
|
|
5
|
+
export declare function spawnArgv(argv: string[], stdio: StdioOptions, detached?: boolean): ChildProcess;
|
|
6
6
|
/** Rejects when the spawn itself fails, so a missing binary lands where a bad exit code would. */
|
|
7
7
|
export declare function exited(proc: ChildProcess): Promise<number>;
|
|
8
8
|
export declare function collect(stream: Readable): Promise<string>;
|
|
@@ -28,6 +28,8 @@ export interface RunOptions {
|
|
|
28
28
|
* `exec docker run | tee` returns tee's code, which is why the old scripts always reported 0.
|
|
29
29
|
*/
|
|
30
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;
|
|
31
33
|
export declare function stopContainer(name: string, timeoutSeconds: number): Promise<void>;
|
|
32
34
|
/**
|
|
33
35
|
* Host-side marker watch. Watches container stdout AND the game's own log file: RimWorld
|
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
export interface WindowWatch {
|
|
2
2
|
stop: () => void;
|
|
3
3
|
}
|
|
4
|
+
export interface Toplevel {
|
|
5
|
+
id: string;
|
|
6
|
+
wmClass: string;
|
|
7
|
+
}
|
|
8
|
+
/** Every window that appeared since the snapshot and belongs to this game, in wmctrl's order. */
|
|
9
|
+
export declare function newMatches(now: Toplevel[], seen: Set<string>, executable: string): Toplevel[];
|
|
10
|
+
export declare function parseWindowPid(stdout: string): number | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Another supervisor's window. Our own claim, or a dead or non-gamecrate pid, is not.
|
|
13
|
+
* Deliberately not `isRunning`: the cmdline read already throws for a dead pid, and it stays
|
|
14
|
+
* readable for a supervisor owned by another user, where a signal check is denied and would
|
|
15
|
+
* have us steal that run's window.
|
|
16
|
+
*
|
|
17
|
+
* Matched on each argument's own basename rather than anywhere in the raw cmdline, so a
|
|
18
|
+
* gamecrate log path or data directory in an unrelated process's arguments is not a peer. An
|
|
19
|
+
* editor opened on a directory literally named gamecrate still is; nothing in /proc separates
|
|
20
|
+
* those two.
|
|
21
|
+
*
|
|
22
|
+
* It also stops matching `bun run src/index.ts`, where the old substring match caught the repo
|
|
23
|
+
* directory in the script path. Installed users are unaffected, the bin and gamecrate.js both
|
|
24
|
+
* match; it costs peer detection between two concurrent from-source dev runs.
|
|
25
|
+
*
|
|
26
|
+
* Residual: before adoption `_NET_WM_PID` is the container's pid namespace, so this looks a
|
|
27
|
+
* container-local number up in the host's `/proc`. A false hit makes the run skip its own
|
|
28
|
+
* window for the whole wait. Container game pids are small and low host pids are kernel
|
|
29
|
+
* threads with an empty cmdline, so in practice the read returns nothing and no match happens.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isPeerClaim(pid: number, self: number): boolean;
|
|
4
32
|
export interface AdoptOptions {
|
|
5
33
|
executable: string;
|
|
6
34
|
title: string;
|
|
@@ -14,7 +42,8 @@ export interface AdoptOptions {
|
|
|
14
42
|
* the screen first and takes the first window that was not there: an X client inside a
|
|
15
43
|
* container reports a container-local _NET_WM_PID and hostname, so neither of those identifies
|
|
16
44
|
* the run from out here. The WM_CLASS comes from the executable, which narrows it to this game
|
|
17
|
-
* rather than any window that opened.
|
|
45
|
+
* rather than any window that opened. Once adopted, _NET_WM_PID holds our pid, so a second run
|
|
46
|
+
* starting at the same time can see the window is already spoken for and keep waiting for its own.
|
|
18
47
|
*/
|
|
19
48
|
export declare function adoptNewWindow(opts: AdoptOptions): Promise<WindowWatch>;
|
|
20
49
|
/** `WM_PROTOCOLS(ATOM): protocols WM_DELETE_WINDOW, WM_TAKE_FOCUS`, or `: not found.` */
|
|
@@ -29,13 +29,56 @@ export declare function buildLocalMods(plan: LaunchPlan, policy: BuildPolicy): P
|
|
|
29
29
|
export interface ProfileLock {
|
|
30
30
|
release: () => Promise<void>;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Refuses when this profile and instance are already up, and clears a lock whose holder is
|
|
34
|
+
* gone. Detach runs this before it forks, so a stale lock cannot strand the child.
|
|
35
|
+
*/
|
|
36
|
+
export declare function clearLock(plan: LaunchPlan): Promise<void>;
|
|
37
|
+
/** The parent wrote this lock with the child's pid, so the child only has to drop it. */
|
|
38
|
+
export declare function heldLock(plan: LaunchPlan): ProfileLock;
|
|
32
39
|
export declare function takeLock(plan: LaunchPlan): Promise<ProfileLock>;
|
|
40
|
+
/** Everything ps, stop, attach and wait need about a run, without reopening the container. */
|
|
41
|
+
export interface LockRecord {
|
|
42
|
+
pid: number;
|
|
43
|
+
container: string;
|
|
44
|
+
game: string;
|
|
45
|
+
profile: string;
|
|
46
|
+
instance?: string;
|
|
47
|
+
detached: boolean;
|
|
48
|
+
mode?: string;
|
|
49
|
+
startedAt: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function lockPath(plan: LaunchPlan): string;
|
|
52
|
+
export declare function readLock(path: string): Promise<LockRecord | undefined>;
|
|
53
|
+
/** wx fails rather than truncating, which is what makes this a lock and not a note. */
|
|
54
|
+
export declare function writeLock(plan: LaunchPlan, record: Omit<LockRecord, 'startedAt'>): Promise<void>;
|
|
33
55
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
56
|
+
* A signalled supervisor cannot finish before its own `docker stop --timeout` does, so the
|
|
57
|
+
* budget is derived from that rather than guessed alongside it.
|
|
58
|
+
*/
|
|
59
|
+
export declare const STOP_RELEASE_WAIT_MS: number;
|
|
60
|
+
/**
|
|
61
|
+
* What stopRun did, because "the supervisor took the signal" and "the lock was stale anyway"
|
|
62
|
+
* are not the same answer and the caller has to say which one it is.
|
|
63
|
+
*/
|
|
64
|
+
export type StopOutcome = 'signalled' | 'orphaned' | 'held';
|
|
65
|
+
/**
|
|
66
|
+
* Signals the supervisor, not the container. runContainer turns SIGTERM into a docker stop and
|
|
67
|
+
* returns 130, so the run records itself as stopped rather than crashed. Only a run whose
|
|
68
|
+
* supervisor is already gone gets the container stopped out from under it.
|
|
69
|
+
*/
|
|
70
|
+
export declare function stopRun(record: LockRecord, lockFile: string): Promise<StopOutcome>;
|
|
71
|
+
/**
|
|
72
|
+
* `--replace`: ends the run for this profile and instance only, so a parallel worktree run is
|
|
73
|
+
* untouched. Goes through stopRun so the replaced run reports `stopped`; a raw docker stop
|
|
74
|
+
* would land as 137 instead.
|
|
37
75
|
*/
|
|
38
76
|
export declare function replacePrevious(plan: LaunchPlan): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* A pid alone is not proof. Detach leaves a long-lived process per run, so a recycled number
|
|
79
|
+
* would refuse every future launch and give ps a ghost with an invented uptime.
|
|
80
|
+
*/
|
|
81
|
+
export declare function isRunning(pid: number, startedAt?: string): boolean;
|
|
39
82
|
/**
|
|
40
83
|
* Grabs one frame from inside the running container. The run dir is already bind-mounted at
|
|
41
84
|
* CONTAINER_LOG_DIR, so the png lands next to that run's logs with no extra mount.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { NoticeSchedule } from '../cli/output';
|
|
2
|
+
import type { LockRecord } from './prepare';
|
|
3
|
+
import type { ExitReason, LaunchPlan, LaunchResult } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* Written once, after the spawn, with the child's pid: a take-then-rewrite would leave a dead
|
|
6
|
+
* pid over a live child, and the next launcher would unlink it and race the same stage tree.
|
|
7
|
+
* Writing first is not open either, since a placeholder pid reads as "no lock" everywhere.
|
|
8
|
+
*
|
|
9
|
+
* So the child is killed when the write loses the `wx` race, because a supervisor with no lock
|
|
10
|
+
* runs unguarded and its release would drop the winner's lock.
|
|
11
|
+
*
|
|
12
|
+
* Residual: a parent killed between the spawn and the write leaves that child unguarded, with
|
|
13
|
+
* nothing left to kill it. That window spans a fork and an exec, so milliseconds.
|
|
14
|
+
*/
|
|
15
|
+
export declare function forkSupervisor(plan: LaunchPlan, argv: string[]): Promise<number>;
|
|
16
|
+
export declare function recordExit(plan: LaunchPlan, result: LaunchResult): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* The supervisor has no terminal and its stdio is discarded, so the exit record is the only way
|
|
19
|
+
* a caller ever learns it died. The lock goes only once that record is on disk, so a failed
|
|
20
|
+
* write leaves it: clearLock takes a dead holder on the next launch, while a cleared lock with
|
|
21
|
+
* no record makes wait answer "no run recorded" for a run that really failed.
|
|
22
|
+
*/
|
|
23
|
+
export declare function supervisorFailed(dir: string, code: number): Promise<number>;
|
|
24
|
+
export interface ExitRecord {
|
|
25
|
+
at: string;
|
|
26
|
+
code: number;
|
|
27
|
+
reason: ExitReason;
|
|
28
|
+
container?: string;
|
|
29
|
+
runDir?: string;
|
|
30
|
+
}
|
|
31
|
+
/** The other half of writeExit. Missing or corrupt reads as "no exit recorded", never a throw. */
|
|
32
|
+
export declare function lastExit(instanceDir: string): Promise<ExitRecord | undefined>;
|
|
33
|
+
/**
|
|
34
|
+
* Blocks while a live holder has the lock. `orphaned` is a holder that died without recording
|
|
35
|
+
* anything, which is reachable with SIGKILL; `absent` is a run that never happened. Neither
|
|
36
|
+
* can ever produce an exit code, so neither may keep waiting for one.
|
|
37
|
+
*
|
|
38
|
+
* Once a lock has been seen, the pid outranks the file: the lock can vanish because someone
|
|
39
|
+
* else cleared it, but a live pid still owes us a record.
|
|
40
|
+
*/
|
|
41
|
+
export declare function awaitExit(instanceDir: string, poll?: number): Promise<ExitRecord | 'orphaned' | 'absent'>;
|
|
42
|
+
/**
|
|
43
|
+
* The lock is written right after the spawn, but the supervisor only opens its run log after
|
|
44
|
+
* preflight, staging and the image, so `current` can point at the previous run for as long as
|
|
45
|
+
* a pull takes. tail follows a descriptor, so starting there would watch a finished run and
|
|
46
|
+
* never catch up. Waiting is bounded by the holder: if it dies first, there is nothing coming.
|
|
47
|
+
*/
|
|
48
|
+
export declare function awaitRunLog(instanceDir: string, lock: LockRecord, poll?: number, notice?: NoticeSchedule): Promise<boolean>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LockRecord } from '../launch/prepare';
|
|
2
|
+
export interface RunRecord {
|
|
3
|
+
game: string;
|
|
4
|
+
profile: string;
|
|
5
|
+
instance?: string;
|
|
6
|
+
container: string;
|
|
7
|
+
pid?: number;
|
|
8
|
+
mode?: string;
|
|
9
|
+
startedAt?: string;
|
|
10
|
+
uptime?: string;
|
|
11
|
+
status: 'running' | 'starting' | 'orphaned';
|
|
12
|
+
}
|
|
13
|
+
export declare function parseDockerRuns(stdout: string): RunRecord[];
|
|
14
|
+
/** <dataRoot>/<game>/<profile>/.gamecrate/lock, plus one level of instances under each. */
|
|
15
|
+
export declare function walkLocks(dataRoot: string): Promise<LockRecord[]>;
|
|
16
|
+
type Docker = () => Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Docker answers for anything with a container. The lock walk covers the phase before one
|
|
19
|
+
* exists (pull, build, stage) and a lock whose container is already gone.
|
|
20
|
+
*/
|
|
21
|
+
export declare function listRuns(dataRoot: string, docker?: Docker): Promise<RunRecord[]>;
|
|
22
|
+
export {};
|
package/dist/types/types.d.ts
CHANGED
|
@@ -18,6 +18,15 @@ export declare const Exit: {
|
|
|
18
18
|
readonly Interrupted: 130;
|
|
19
19
|
};
|
|
20
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;
|
|
21
30
|
/** Every failure the tool raises deliberately. Anything else is a bug. */
|
|
22
31
|
export declare class GamecrateError extends Error {
|
|
23
32
|
readonly code: ExitCode;
|
|
@@ -26,7 +35,7 @@ export declare class GamecrateError extends Error {
|
|
|
26
35
|
}
|
|
27
36
|
/** Collected and reported together, so one run surfaces every problem at once. */
|
|
28
37
|
export interface Problem {
|
|
29
|
-
/** JSON Pointer into
|
|
38
|
+
/** JSON Pointer into the global config, or a file path, or a mod id. */
|
|
30
39
|
where: string;
|
|
31
40
|
message: string;
|
|
32
41
|
/** Populated by did-you-mean matching where it applies. */
|
|
@@ -138,6 +147,12 @@ export interface ProfileConfig {
|
|
|
138
147
|
alias?: string;
|
|
139
148
|
/** Extra names this profile answers to, so one entry covers several spellings. */
|
|
140
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;
|
|
141
156
|
}
|
|
142
157
|
export interface GameConfig {
|
|
143
158
|
gameFiles: GameFilesSpec;
|
|
@@ -381,9 +396,28 @@ export interface ParsedArgs {
|
|
|
381
396
|
noStaleCheck: boolean;
|
|
382
397
|
/** Stops whatever holds this profile+instance, then launches. Never refuses. */
|
|
383
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;
|
|
384
408
|
rest: string[];
|
|
385
409
|
}
|
|
386
|
-
export type ProjectDefaults = Partial<Omit<ParsedArgs, 'subcommand' | 'cleanTier' | 'yes' | 'help' | 'rest'
|
|
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
|
+
};
|
|
387
421
|
/** Names that can never be a game or profile key. Enforced at config load. */
|
|
388
422
|
export declare const RESERVED_NAMES: readonly string[];
|
|
389
423
|
export declare const NAME_PATTERN: RegExp;
|