@gamecrate/cli 0.1.0 → 1.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/dist/gamecrate.js +246 -246
- package/dist/lib.js +296 -0
- package/dist/types/cli/args.d.ts +40 -0
- package/dist/types/cli/help.d.ts +7 -0
- package/dist/types/cli/output.d.ts +56 -0
- package/dist/types/config/builtin.d.ts +3 -0
- package/dist/types/config/jsonc.d.ts +5 -0
- package/dist/types/config/load.d.ts +45 -0
- package/dist/types/config/validate.d.ts +12 -0
- package/dist/types/docker/identity.d.ts +6 -0
- package/dist/types/docker/preflight.d.ts +3 -0
- package/dist/types/docker/run.d.ts +37 -0
- package/dist/types/docker/spec.d.ts +24 -0
- package/dist/types/docker/window.d.ts +21 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/launch/generate.d.ts +8 -0
- package/dist/types/launch/instance.d.ts +20 -0
- package/dist/types/launch/prepare.d.ts +44 -0
- package/dist/types/launch/resolve.d.ts +17 -0
- package/dist/types/launch/stage.d.ts +13 -0
- package/dist/types/lib.d.ts +3 -0
- package/dist/types/mods/modindex.d.ts +29 -0
- package/dist/types/mods/staleness.d.ts +28 -0
- package/dist/types/mods/worktree.d.ts +18 -0
- package/dist/types/plugin.d.ts +35 -0
- package/dist/types/types.d.ts +394 -0
- package/package.json +15 -10
- package/src/cli/args.ts +0 -592
- package/src/cli/help.ts +0 -193
- package/src/cli/output.ts +0 -246
- package/src/config/builtin.ts +0 -19
- package/src/config/jsonc.ts +0 -21
- package/src/config/load.ts +0 -387
- package/src/config/validate.ts +0 -0
- package/src/docker/identity.ts +0 -25
- package/src/docker/preflight.ts +0 -243
- package/src/docker/run.ts +0 -212
- package/src/docker/spec.ts +0 -357
- package/src/docker/window.ts +0 -152
- package/src/index.ts +0 -875
- package/src/launch/generate.ts +0 -151
- package/src/launch/instance.ts +0 -106
- package/src/launch/prepare.ts +0 -332
- package/src/launch/resolve.ts +0 -383
- package/src/launch/stage.ts +0 -97
- package/src/lib.ts +0 -22
- package/src/mods/modindex.ts +0 -539
- package/src/mods/staleness.ts +0 -125
- package/src/mods/worktree.ts +0 -107
- package/src/plugin.ts +0 -152
- package/src/types.ts +0 -423
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LaunchPlan, Mount } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Wipes and rebuilds the staging tree, returning one read-only bind per mod.
|
|
4
|
+
* Never creates a symlink: a host symlink into a mod checkout dangles inside the container.
|
|
5
|
+
*/
|
|
6
|
+
export declare function stageMods(plan: LaunchPlan): Promise<Mount[]>;
|
|
7
|
+
/** Pre-creates the profile skeleton as the caller, before docker can create it as root. */
|
|
8
|
+
export declare function ensureProfileTree(plan: LaunchPlan): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Walks with lstat semantics, so a dangling symlink inside a mounted tree is reported
|
|
11
|
+
* rather than thrown. Stops once `limit` foreign paths are found.
|
|
12
|
+
*/
|
|
13
|
+
export declare function detectForeignOwnership(dir: string, uid: number, limit?: number): Promise<string[]>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { GamePlugin, ModsConfigInput } from './plugin';
|
|
2
|
+
export { PLUGIN_API_VERSION } from './plugin';
|
|
3
|
+
export type { DataDirSpec, DisplayBackend, DynamicModEntry, GameConfig, GameFilesSpec, ImageSpec, InstanceConfig, LibraryEntry, LogFileSpec, ModEntry, ModEntryObject, ModManifest, ModeName, ModsDirSpec, NetworkPolicy, ProfileConfig, ScanRoot, Settings, } from './types';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GamePlugin } from '../plugin';
|
|
2
|
+
import type { GameConfig, ModIndex, ModRecord, Problem, WorktreeRequest } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* `**` spans separators, `*` and `?` do not; everything else is literal. Mod folders are
|
|
5
|
+
* routinely named `[KV] Mod Manager`, so brackets and braces must not be glob syntax.
|
|
6
|
+
*/
|
|
7
|
+
export declare function globMatch(pattern: string, path: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Stamps every record inside a requested worktree, then scans the worktree itself so a tree
|
|
10
|
+
* no scanRoot reaches still contributes. Only a caller who stood in, typed, or exported a
|
|
11
|
+
* directory can produce `selectedWorktree`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyWorktreeRequests(index: ModIndex, requests: WorktreeRequest[], config: GameConfig): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Forces one packageId to come from a named directory, whatever the profile pinned. This is
|
|
16
|
+
* the only override that reaches a mod already in preCore/core/dlc/base, because slot
|
|
17
|
+
* expansion is first-wins and a later `--mod` entry for a present id is dropped.
|
|
18
|
+
*/
|
|
19
|
+
export declare function applySourceOverrides(index: ModIndex, overrides: string[], config: GameConfig): Promise<Problem[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Scans the game install, then every scan root in declaration order, then the workshop root.
|
|
22
|
+
* Local roots rescan every launch; only the workshop scan is cached, against the acf stamp.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildIndex(game: string, config: GameConfig, plugin: GamePlugin): Promise<ModIndex>;
|
|
25
|
+
/**
|
|
26
|
+
* `path:` and `workshop:` are explicit; a bare string resolves as exact packageId, then the
|
|
27
|
+
* game's alias map, then a CLI-only short name. Ambiguity that the ladder cannot break is fatal.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveModRef(index: ModIndex, ref: string, game: GameConfig): ModRecord | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StaleReport } from '../types';
|
|
2
|
+
export interface Timestamped {
|
|
3
|
+
/** Relative to the mod directory. */
|
|
4
|
+
path: string;
|
|
5
|
+
mtimeMs: number;
|
|
6
|
+
}
|
|
7
|
+
export interface BuildTimes {
|
|
8
|
+
newestSource?: Timestamped;
|
|
9
|
+
newestAssembly?: Timestamped;
|
|
10
|
+
/** Every .cs mtime, so a report can count how many beat the assembly. */
|
|
11
|
+
sourceTimes: number[];
|
|
12
|
+
}
|
|
13
|
+
/** One walk answers both questions: is this stale, and which files say so. */
|
|
14
|
+
export declare function scanBuildTimes(dir: string): Promise<BuildTimes>;
|
|
15
|
+
/**
|
|
16
|
+
* Drives `--build auto`, so a mod that has never been compiled counts as stale. The warning
|
|
17
|
+
* is the stricter one: see staleReport.
|
|
18
|
+
*/
|
|
19
|
+
export declare function decideStale(times: BuildTimes): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Null when there is nothing to say: no C#, no assemblies to compare against, or the build is
|
|
22
|
+
* current. A mod may legitimately ship XML only, so this never reports on one.
|
|
23
|
+
*/
|
|
24
|
+
export declare function staleReport(times: BuildTimes): StaleReport | null;
|
|
25
|
+
export declare function staleWarning(packageId: string, report: StaleReport, now?: number): string;
|
|
26
|
+
/** Coarse on purpose: "4m" is the whole signal, a duration to the second is noise. */
|
|
27
|
+
export declare function duration(ms: number): string;
|
|
28
|
+
export declare function ago(mtimeMs: number, now?: number): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Problem, WorktreeRequest, WorktreeSource } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves one worktree request. Returns null when `dir` is not inside a LINKED worktree,
|
|
4
|
+
* which is what keeps a primary checkout, a non-repo directory and a pruned gitdir from
|
|
5
|
+
* counting as a selection.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveWorktree(dir: string, source: WorktreeSource, order: number): WorktreeRequest | Problem;
|
|
8
|
+
/** True when `dir` is the worktree root or lives underneath it. */
|
|
9
|
+
export declare function contains(request: WorktreeRequest, dir: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Assembles every request in precedence order: explicit flags first (left to right), then the
|
|
12
|
+
* env var, then cwd. Only a linked worktree survives; everything else becomes an ignorable
|
|
13
|
+
* Problem so the caller can decide how loudly to say so.
|
|
14
|
+
*/
|
|
15
|
+
export declare function collectRequests(flags: string[], env: string | undefined, cwd: string, disabled: boolean): {
|
|
16
|
+
requests: WorktreeRequest[];
|
|
17
|
+
problems: Problem[];
|
|
18
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { GameConfig, ModManifest } from './types';
|
|
2
|
+
/** Bumped when a change would make an older plugin misbehave rather than merely lag. */
|
|
3
|
+
export declare const PLUGIN_API_VERSION = 1;
|
|
4
|
+
export interface ModsConfigInput {
|
|
5
|
+
version: string;
|
|
6
|
+
buildNumber: number;
|
|
7
|
+
/** Lowercased packageIds, load order. */
|
|
8
|
+
activeMods: string[];
|
|
9
|
+
knownExpansions: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A game's file formats plus the parts of its config that describe the game, not this machine.
|
|
13
|
+
* Everything here takes plain data and throws plain Errors, so no plugin links the core runtime.
|
|
14
|
+
*/
|
|
15
|
+
export interface GamePlugin {
|
|
16
|
+
apiVersion: number;
|
|
17
|
+
/** The key this game answers to on the command line. */
|
|
18
|
+
game: string;
|
|
19
|
+
/** Merged under the user's `games.<game>` block, so a user only writes what is theirs. */
|
|
20
|
+
defaults: Partial<GameConfig>;
|
|
21
|
+
/** null means "this file is not a mod manifest". A malformed file throws. */
|
|
22
|
+
parseManifest(text: string): ModManifest | null;
|
|
23
|
+
renderModsConfig(input: ModsConfigInput): string;
|
|
24
|
+
mergePrefs(existing: string | null, owned: Record<string, string>): string;
|
|
25
|
+
/** Prefs keys that put the game in a window instead of fullscreen. */
|
|
26
|
+
windowedPrefs: Record<string, string>;
|
|
27
|
+
/** Version.txt as the engine writes it. null when it does not parse. */
|
|
28
|
+
parseVersion(text: string): {
|
|
29
|
+
version: string;
|
|
30
|
+
buildNumber: number;
|
|
31
|
+
} | null;
|
|
32
|
+
}
|
|
33
|
+
/** Keyed by game name. A second plugin claiming a name already taken is a config error. */
|
|
34
|
+
export declare function loadPlugins(specs: string[], configFile: string): Promise<Map<string, GamePlugin>>;
|
|
35
|
+
export declare function requirePlugin(plugins: Map<string, GamePlugin>, game: string): GamePlugin;
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import type { GamePlugin } from './plugin';
|
|
2
|
+
/**
|
|
3
|
+
* Shared contract for every module. Units code against this and nothing else;
|
|
4
|
+
* if a signature here is wrong, fix it here rather than working around it locally.
|
|
5
|
+
*/
|
|
6
|
+
export declare const Exit: {
|
|
7
|
+
readonly Ok: 0;
|
|
8
|
+
readonly GameFailed: 1;
|
|
9
|
+
readonly Usage: 2;
|
|
10
|
+
readonly Config: 3;
|
|
11
|
+
readonly Resolution: 4;
|
|
12
|
+
readonly Environment: 5;
|
|
13
|
+
readonly MarkerTimeout: 6;
|
|
14
|
+
/** Refused: this profile+instance is already running. `--replace` is the way past it. */
|
|
15
|
+
readonly Refused: 7;
|
|
16
|
+
/** `verify`: a bound mod's sources are newer than its assemblies. */
|
|
17
|
+
readonly Stale: 8;
|
|
18
|
+
readonly Interrupted: 130;
|
|
19
|
+
};
|
|
20
|
+
export type ExitCode = (typeof Exit)[keyof typeof Exit];
|
|
21
|
+
/** Every failure the tool raises deliberately. Anything else is a bug. */
|
|
22
|
+
export declare class GamecrateError extends Error {
|
|
23
|
+
readonly code: ExitCode;
|
|
24
|
+
readonly detail?: string | undefined;
|
|
25
|
+
constructor(message: string, code: ExitCode, detail?: string | undefined);
|
|
26
|
+
}
|
|
27
|
+
/** Collected and reported together, so one run surfaces every problem at once. */
|
|
28
|
+
export interface Problem {
|
|
29
|
+
/** JSON Pointer into profiles.json, or a file path, or a mod id. */
|
|
30
|
+
where: string;
|
|
31
|
+
message: string;
|
|
32
|
+
/** Populated by did-you-mean matching where it applies. */
|
|
33
|
+
suggestion?: string;
|
|
34
|
+
}
|
|
35
|
+
export type ModeName = 'headed' | 'headless' | 'screenshot';
|
|
36
|
+
export type PullPolicy = 'always' | 'missing' | 'never';
|
|
37
|
+
export type BuildPolicy = 'auto' | 'always' | 'never';
|
|
38
|
+
export type NetworkPolicy = 'none' | 'bridge' | 'host';
|
|
39
|
+
export type DisplayBackend = 'x11' | 'wayland';
|
|
40
|
+
export interface Settings {
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
devMode: boolean;
|
|
44
|
+
runInBackground: boolean;
|
|
45
|
+
/** Forced false when written; see landmine 5. */
|
|
46
|
+
resetModsConfigOnCrash: boolean;
|
|
47
|
+
gpu: boolean;
|
|
48
|
+
audio: boolean;
|
|
49
|
+
input: boolean;
|
|
50
|
+
network: NetworkPolicy;
|
|
51
|
+
/** Which display server a headed run talks to. x11 is the one an outside tool can retitle. */
|
|
52
|
+
display: DisplayBackend;
|
|
53
|
+
memory: string;
|
|
54
|
+
cpus: number;
|
|
55
|
+
pidsLimit: number;
|
|
56
|
+
/** Verbatim passthrough into the generated Prefs file. */
|
|
57
|
+
prefsExtra?: Record<string, string>;
|
|
58
|
+
gameArgs?: string[];
|
|
59
|
+
dockerArgs?: string[];
|
|
60
|
+
}
|
|
61
|
+
export interface GameFilesSpec {
|
|
62
|
+
source: 'mount' | 'image';
|
|
63
|
+
/** Required when source is "mount". */
|
|
64
|
+
host?: string;
|
|
65
|
+
container: string;
|
|
66
|
+
}
|
|
67
|
+
export interface ImageSpec {
|
|
68
|
+
ref: string;
|
|
69
|
+
acquire: 'pull' | 'build';
|
|
70
|
+
/** Required when acquire is "build". */
|
|
71
|
+
context?: string;
|
|
72
|
+
}
|
|
73
|
+
/** How the engine is told where its data directory is. Verified per game. */
|
|
74
|
+
export type DataDirSpec = {
|
|
75
|
+
container: string;
|
|
76
|
+
mode: 'arg';
|
|
77
|
+
arg: string;
|
|
78
|
+
} | {
|
|
79
|
+
container: string;
|
|
80
|
+
mode: 'env';
|
|
81
|
+
env: Record<string, string>;
|
|
82
|
+
};
|
|
83
|
+
export interface ModsDirSpec {
|
|
84
|
+
/** Where the staged mod tree is bind-mounted. NOT necessarily under dataDir. */
|
|
85
|
+
container: string;
|
|
86
|
+
/** Extra mod roots inside the image that must be masked with a tmpfs. */
|
|
87
|
+
mask?: string[];
|
|
88
|
+
}
|
|
89
|
+
export type LogFileSpec = {
|
|
90
|
+
mode: 'arg';
|
|
91
|
+
arg: string;
|
|
92
|
+
} | {
|
|
93
|
+
mode: 'copy-out';
|
|
94
|
+
from: string;
|
|
95
|
+
};
|
|
96
|
+
export interface ScanRoot {
|
|
97
|
+
path: string;
|
|
98
|
+
maxDepth: number;
|
|
99
|
+
exclude?: string[];
|
|
100
|
+
}
|
|
101
|
+
export interface LibraryEntry {
|
|
102
|
+
workshop?: number;
|
|
103
|
+
path?: string;
|
|
104
|
+
}
|
|
105
|
+
/** Matches a family of mods by pattern instead of naming each one. */
|
|
106
|
+
export interface DynamicModEntry {
|
|
107
|
+
match: string;
|
|
108
|
+
first?: string[];
|
|
109
|
+
sort?: 'alpha' | 'none';
|
|
110
|
+
minMatches?: number;
|
|
111
|
+
}
|
|
112
|
+
export interface ModEntryObject {
|
|
113
|
+
id: string;
|
|
114
|
+
workshop?: number;
|
|
115
|
+
path?: string;
|
|
116
|
+
optional?: boolean;
|
|
117
|
+
}
|
|
118
|
+
/** A bare string is a packageId; `workshop:` and `path:` prefixes disambiguate. */
|
|
119
|
+
export type ModEntry = string | ModEntryObject | DynamicModEntry;
|
|
120
|
+
/**
|
|
121
|
+
* A named sub-run of a profile with its own data directory, lock and container, so several
|
|
122
|
+
* can run at once. Selected with `--instance`, or derived from an explicit `--worktree`.
|
|
123
|
+
*/
|
|
124
|
+
export interface InstanceConfig {
|
|
125
|
+
/** Promoted ahead of every other worktree request when this instance is selected. */
|
|
126
|
+
worktree?: string;
|
|
127
|
+
settings?: Partial<Settings>;
|
|
128
|
+
}
|
|
129
|
+
export interface ProfileConfig {
|
|
130
|
+
mods?: ModEntry[];
|
|
131
|
+
extends?: string;
|
|
132
|
+
exclude?: string[];
|
|
133
|
+
includeBase?: boolean;
|
|
134
|
+
autoDependencies?: boolean;
|
|
135
|
+
settings?: Partial<Settings>;
|
|
136
|
+
instances?: Record<string, InstanceConfig>;
|
|
137
|
+
/** Marks this profile as another name for an existing one. */
|
|
138
|
+
alias?: string;
|
|
139
|
+
/** Extra names this profile answers to, so one entry covers several spellings. */
|
|
140
|
+
aliases?: string[];
|
|
141
|
+
}
|
|
142
|
+
export interface GameConfig {
|
|
143
|
+
gameFiles: GameFilesSpec;
|
|
144
|
+
dataDir: DataDirSpec;
|
|
145
|
+
modsDir: ModsDirSpec;
|
|
146
|
+
logFile: LogFileSpec;
|
|
147
|
+
image: ImageSpec;
|
|
148
|
+
executable: string;
|
|
149
|
+
steamAppId: number;
|
|
150
|
+
workshopRoot: string | null;
|
|
151
|
+
scanRoots: ScanRoot[];
|
|
152
|
+
manifest: {
|
|
153
|
+
file: string;
|
|
154
|
+
};
|
|
155
|
+
modsConfig: {
|
|
156
|
+
file: string;
|
|
157
|
+
};
|
|
158
|
+
prefs: {
|
|
159
|
+
file: string;
|
|
160
|
+
};
|
|
161
|
+
/** Filename suffixes that mean "a save". `clean --all` counts them before it deletes. */
|
|
162
|
+
saveExtensions: string[];
|
|
163
|
+
core: string;
|
|
164
|
+
dlc: string[];
|
|
165
|
+
preCore?: string[];
|
|
166
|
+
base?: string[];
|
|
167
|
+
library?: Record<string, LibraryEntry>;
|
|
168
|
+
modes: ModeName[];
|
|
169
|
+
aliases?: Record<string, string>;
|
|
170
|
+
settings?: Partial<Settings>;
|
|
171
|
+
/** The engine claims WM_DELETE_WINDOW and drops it, so the titlebar X does nothing. */
|
|
172
|
+
ignoresWmDelete?: boolean;
|
|
173
|
+
profiles: Record<string, ProfileConfig>;
|
|
174
|
+
}
|
|
175
|
+
export interface RootConfig {
|
|
176
|
+
/** Package names or paths, resolved from the config file's directory. One per game. */
|
|
177
|
+
plugins?: string[];
|
|
178
|
+
dataRoot: string;
|
|
179
|
+
defaults?: {
|
|
180
|
+
settings?: Partial<Settings>;
|
|
181
|
+
};
|
|
182
|
+
games: Record<string, GameConfig>;
|
|
183
|
+
}
|
|
184
|
+
export type ModSourceKind = 'local' | 'workshop' | 'official' | 'core';
|
|
185
|
+
export interface ModManifest {
|
|
186
|
+
packageId: string;
|
|
187
|
+
name?: string;
|
|
188
|
+
modDependencies: {
|
|
189
|
+
packageId: string;
|
|
190
|
+
steamWorkshopUrl?: string;
|
|
191
|
+
}[];
|
|
192
|
+
loadAfter: string[];
|
|
193
|
+
loadBefore: string[];
|
|
194
|
+
forceLoadAfter: string[];
|
|
195
|
+
forceLoadBefore: string[];
|
|
196
|
+
incompatibleWith: string[];
|
|
197
|
+
}
|
|
198
|
+
export interface ModRecord {
|
|
199
|
+
/** Manifest casing, preserved. Match on the lowercased form. */
|
|
200
|
+
packageId: string;
|
|
201
|
+
dir: string;
|
|
202
|
+
kind: ModSourceKind;
|
|
203
|
+
manifest: ModManifest;
|
|
204
|
+
/** Workshop item id when kind is "workshop". */
|
|
205
|
+
workshopId?: number;
|
|
206
|
+
/** True when dir sits inside a linked git worktree; ranked below primaries. */
|
|
207
|
+
linkedWorktree: boolean;
|
|
208
|
+
/**
|
|
209
|
+
* Request order when a caller deliberately selected the worktree this record lives in.
|
|
210
|
+
* The scanner can never set it, which is what stops an unselected worktree from winning.
|
|
211
|
+
*/
|
|
212
|
+
selectedWorktree?: number;
|
|
213
|
+
/** Request order when `--use <packageId>=<dir>` named this record. Outranks a worktree. */
|
|
214
|
+
overridden?: number;
|
|
215
|
+
/** Set only for a selected worktree; the branch costs a git spawn, so it is not scanned for. */
|
|
216
|
+
worktree?: {
|
|
217
|
+
root: string;
|
|
218
|
+
branch: string;
|
|
219
|
+
source: WorktreeSource;
|
|
220
|
+
};
|
|
221
|
+
/** Which scanRoot produced it, for the precedence ladder. */
|
|
222
|
+
rootIndex: number;
|
|
223
|
+
}
|
|
224
|
+
export type WorktreeSource = 'flag' | 'env' | 'cwd' | 'ref';
|
|
225
|
+
export interface WorktreeRequest {
|
|
226
|
+
/** realpath'd worktree toplevel. */
|
|
227
|
+
root: string;
|
|
228
|
+
branch: string;
|
|
229
|
+
source: WorktreeSource;
|
|
230
|
+
/** Lower wins. */
|
|
231
|
+
order: number;
|
|
232
|
+
}
|
|
233
|
+
export interface ModIndex {
|
|
234
|
+
game: string;
|
|
235
|
+
plugin: GamePlugin;
|
|
236
|
+
/** Keyed by lowercased packageId. Several records means a collision to resolve. */
|
|
237
|
+
byPackageId: Map<string, ModRecord[]>;
|
|
238
|
+
byWorkshopId: Map<number, ModRecord>;
|
|
239
|
+
/** Lowercased last dot-segment -> packageIds. CLI matching only. */
|
|
240
|
+
byShortName: Map<string, string[]>;
|
|
241
|
+
problems: Problem[];
|
|
242
|
+
}
|
|
243
|
+
/** Why a mod looks stale, in enough detail to name the file that says so. */
|
|
244
|
+
export interface StaleReport {
|
|
245
|
+
/** Mod-relative path of the newest .cs. */
|
|
246
|
+
newestSource: string;
|
|
247
|
+
newestSourceMs: number;
|
|
248
|
+
/** Mod-relative path of the newest assembly it should have been compiled into. */
|
|
249
|
+
assembly: string;
|
|
250
|
+
assemblyMs: number;
|
|
251
|
+
newerCount: number;
|
|
252
|
+
}
|
|
253
|
+
export interface ResolvedMod {
|
|
254
|
+
packageId: string;
|
|
255
|
+
hostDir: string;
|
|
256
|
+
containerDir: string;
|
|
257
|
+
kind: ModSourceKind;
|
|
258
|
+
workshopId?: number;
|
|
259
|
+
/** False when the entry came from autoDependencies rather than the profile. */
|
|
260
|
+
explicit: boolean;
|
|
261
|
+
/** Set when a .cs file is newer than the staged assembly. */
|
|
262
|
+
stale?: boolean;
|
|
263
|
+
/** Set only when there is an assembly to be stale against; drives the launch warning. */
|
|
264
|
+
staleReport?: StaleReport;
|
|
265
|
+
/** Present when this mod came out of a linked worktree. */
|
|
266
|
+
worktree?: {
|
|
267
|
+
root: string;
|
|
268
|
+
branch: string;
|
|
269
|
+
source: WorktreeSource;
|
|
270
|
+
selected: boolean;
|
|
271
|
+
};
|
|
272
|
+
/** Other directories that declared this packageId and lost. Always emitted. */
|
|
273
|
+
shadowed?: string[];
|
|
274
|
+
}
|
|
275
|
+
export interface LaunchPlan {
|
|
276
|
+
game: string;
|
|
277
|
+
gameConfig: GameConfig;
|
|
278
|
+
plugin: GamePlugin;
|
|
279
|
+
profile: string;
|
|
280
|
+
settings: Settings;
|
|
281
|
+
mods: ResolvedMod[];
|
|
282
|
+
/** Absolute host path: <dataRoot>/<game>/<profile>. */
|
|
283
|
+
profileDir: string;
|
|
284
|
+
/** Undefined for the base profile; a name when several runs share one profile. */
|
|
285
|
+
instance?: string;
|
|
286
|
+
/** profileDir, or <profileDir>/instances/<instance>. Everything a run writes hangs off it. */
|
|
287
|
+
instanceDir: string;
|
|
288
|
+
dataDirHost: string;
|
|
289
|
+
/** <profileDir>/config. Shared by every instance, and holds the XDG dirs. */
|
|
290
|
+
configDirHost: string;
|
|
291
|
+
stageDirHost: string;
|
|
292
|
+
logsDirHost: string;
|
|
293
|
+
/** <logsDirHost>/runs/<ts>, bound into the container so Player.log lands with stdout.log. */
|
|
294
|
+
runDirHost: string;
|
|
295
|
+
mode: ModeName;
|
|
296
|
+
marker?: string;
|
|
297
|
+
timeoutSeconds: number;
|
|
298
|
+
renderWaitSeconds: number;
|
|
299
|
+
/** False under --no-stale-check. The check still runs, so staleReport stays truthful. */
|
|
300
|
+
warnOnStale: boolean;
|
|
301
|
+
warnings: string[];
|
|
302
|
+
}
|
|
303
|
+
export interface Identity {
|
|
304
|
+
uid: number;
|
|
305
|
+
gid: number;
|
|
306
|
+
home: string;
|
|
307
|
+
user: string;
|
|
308
|
+
}
|
|
309
|
+
export interface Mount {
|
|
310
|
+
type: 'bind' | 'tmpfs';
|
|
311
|
+
source?: string;
|
|
312
|
+
target: string;
|
|
313
|
+
readonly?: boolean;
|
|
314
|
+
/** tmpfs only. */
|
|
315
|
+
size?: string;
|
|
316
|
+
uid?: number;
|
|
317
|
+
gid?: number;
|
|
318
|
+
mode?: string;
|
|
319
|
+
}
|
|
320
|
+
export interface DockerRunSpec {
|
|
321
|
+
image: string;
|
|
322
|
+
name: string;
|
|
323
|
+
labels: Record<string, string>;
|
|
324
|
+
identity: Identity;
|
|
325
|
+
env: Record<string, string>;
|
|
326
|
+
mounts: Mount[];
|
|
327
|
+
devices: string[];
|
|
328
|
+
deviceCgroupRules: string[];
|
|
329
|
+
network: NetworkPolicy;
|
|
330
|
+
memory: string;
|
|
331
|
+
memorySwap: string;
|
|
332
|
+
cpus: number;
|
|
333
|
+
pidsLimit: number;
|
|
334
|
+
ulimits: string[];
|
|
335
|
+
workdir: string;
|
|
336
|
+
/** The host's, when set: KWin appends `<@name>` to a caption from a foreign machine. */
|
|
337
|
+
hostname?: string;
|
|
338
|
+
/** argv after the image name. */
|
|
339
|
+
command: string[];
|
|
340
|
+
extraArgs: string[];
|
|
341
|
+
}
|
|
342
|
+
export interface ParsedArgs {
|
|
343
|
+
subcommand: string;
|
|
344
|
+
game?: string;
|
|
345
|
+
profile?: string;
|
|
346
|
+
mods: string[];
|
|
347
|
+
without: string[];
|
|
348
|
+
only: string[];
|
|
349
|
+
mode?: ModeName;
|
|
350
|
+
marker?: string;
|
|
351
|
+
timeout?: number;
|
|
352
|
+
renderWait?: number;
|
|
353
|
+
resolution?: {
|
|
354
|
+
width: number;
|
|
355
|
+
height: number;
|
|
356
|
+
};
|
|
357
|
+
network?: NetworkPolicy;
|
|
358
|
+
log?: string;
|
|
359
|
+
pull?: PullPolicy;
|
|
360
|
+
build?: BuildPolicy;
|
|
361
|
+
sort?: 'topo' | 'none';
|
|
362
|
+
/** `clean` only: --staging is the default, --all additionally requires --yes. */
|
|
363
|
+
cleanTier?: 'staging' | 'logs' | 'all';
|
|
364
|
+
dockerArgs: string[];
|
|
365
|
+
gameArgs: string[];
|
|
366
|
+
dryRun: boolean;
|
|
367
|
+
printPlan: boolean;
|
|
368
|
+
json: boolean;
|
|
369
|
+
root: boolean;
|
|
370
|
+
yes: boolean;
|
|
371
|
+
help: boolean;
|
|
372
|
+
/** Repeatable; earlier flags outrank later ones. */
|
|
373
|
+
worktree: string[];
|
|
374
|
+
/** Names the sub-run: its own data directory, lock and container. */
|
|
375
|
+
instance?: string;
|
|
376
|
+
/** Repeatable `packageId=path`; forces one mod's source, whatever the profile says. */
|
|
377
|
+
use: string[];
|
|
378
|
+
/** Suppresses ambient cwd selection and the env var. */
|
|
379
|
+
noWorktree: boolean;
|
|
380
|
+
/** Suppresses the sources-newer-than-assemblies warning. The check itself still runs. */
|
|
381
|
+
noStaleCheck: boolean;
|
|
382
|
+
/** Stops whatever holds this profile+instance, then launches. Never refuses. */
|
|
383
|
+
replace: boolean;
|
|
384
|
+
rest: string[];
|
|
385
|
+
}
|
|
386
|
+
export type ProjectDefaults = Partial<Omit<ParsedArgs, 'subcommand' | 'cleanTier' | 'yes' | 'help' | 'rest'>>;
|
|
387
|
+
/** Names that can never be a game or profile key. Enforced at config load. */
|
|
388
|
+
export declare const RESERVED_NAMES: readonly string[];
|
|
389
|
+
export declare const NAME_PATTERN: RegExp;
|
|
390
|
+
/**
|
|
391
|
+
* Lookup in a bag keyed by user input. A bare index hands back Object.prototype members, so
|
|
392
|
+
* `--mod constructor` or a profile named `toString` would resolve to an inherited function.
|
|
393
|
+
*/
|
|
394
|
+
export declare function own<T>(bag: Record<string, T> | undefined, key: string): T | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamecrate/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Run a modded game in a container, with mods resolved from your local checkouts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,21 +21,26 @@
|
|
|
21
21
|
"gamecrate": "./dist/gamecrate.js"
|
|
22
22
|
},
|
|
23
23
|
"exports": {
|
|
24
|
-
".":
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/types/lib.d.ts",
|
|
26
|
+
"default": "./dist/lib.js"
|
|
27
|
+
}
|
|
25
28
|
},
|
|
26
|
-
"types": "./
|
|
29
|
+
"types": "./dist/types/lib.d.ts",
|
|
27
30
|
"files": [
|
|
28
|
-
"
|
|
29
|
-
"dist/
|
|
31
|
+
"dist/gamecrate.js",
|
|
32
|
+
"dist/lib.js",
|
|
33
|
+
"dist/types"
|
|
30
34
|
],
|
|
31
35
|
"scripts": {
|
|
32
36
|
"typecheck": "tsc --noEmit",
|
|
33
37
|
"test": "vitest run",
|
|
34
|
-
"build": "npm run build:node && npm run build:binary",
|
|
35
|
-
"build:node": "bun build ./src/index.ts --target=node --packages=external --outfile dist/gamecrate.js",
|
|
36
|
-
"build:
|
|
37
|
-
"
|
|
38
|
-
"
|
|
38
|
+
"build": "npm run build:node && npm run build:types && npm run build:binary",
|
|
39
|
+
"build:node": "bun build ./src/index.ts --target=node --packages=external --define __VERSION__='\"'${npm_package_version:-0.0.0-dev}'\"' --outfile dist/gamecrate.js && bun build ./src/lib.ts --target=node --packages=external --outfile dist/lib.js",
|
|
40
|
+
"build:types": "tsc -p tsconfig.build.json",
|
|
41
|
+
"build:binary": "bun build ./src/index.ts --compile --define __VERSION__='\"'${npm_package_version:-0.0.0-dev}'\"' --outfile dist/gamecrate",
|
|
42
|
+
"prepare": "npm run build:node && npm run build:types",
|
|
43
|
+
"prepack": "npm run build:node && npm run build:types"
|
|
39
44
|
},
|
|
40
45
|
"devDependencies": {
|
|
41
46
|
"@types/node": "^22.10.0",
|