@gamecrate/cli 1.1.0 → 1.3.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 +37 -382
- package/dist/gamecrate.js +3935 -2474
- package/dist/lib.js +40 -11
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/mods.d.ts +18 -0
- package/dist/types/config/write.d.ts +14 -0
- package/dist/types/launch/prepare.d.ts +1 -1
- package/dist/types/launch/resolve.d.ts +5 -0
- package/dist/types/mods/acf.d.ts +19 -0
- package/dist/types/mods/modindex.d.ts +9 -3
- package/dist/types/mods/source.d.ts +61 -0
- package/dist/types/mods/steamcmd.d.ts +56 -0
- package/dist/types/mods/workshop.d.ts +15 -0
- package/dist/types/mods/workshopapi.d.ts +13 -0
- package/dist/types/types.d.ts +39 -1
- package/package.json +1 -1
package/dist/lib.js
CHANGED
|
@@ -194,6 +194,37 @@ var profile = obj({
|
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
196
|
});
|
|
197
|
+
var libraryEntry = obj({
|
|
198
|
+
workshop: num.optional(),
|
|
199
|
+
path: str.optional(),
|
|
200
|
+
git: str.optional(),
|
|
201
|
+
branch: str.optional(),
|
|
202
|
+
tag: str.optional(),
|
|
203
|
+
commit: str.optional(),
|
|
204
|
+
subdir: str.optional()
|
|
205
|
+
}).check((ctx) => {
|
|
206
|
+
const v = ctx.value;
|
|
207
|
+
const push = (message, path) => {
|
|
208
|
+
ctx.issues.push({ code: "custom", message, input: v, ...path === undefined ? {} : { path } });
|
|
209
|
+
};
|
|
210
|
+
const sources = ["workshop", "path", "git"].filter((k) => v[k] !== undefined);
|
|
211
|
+
if (sources.length === 0)
|
|
212
|
+
push('library entry needs a "workshop" id, a "path", or a "git" url');
|
|
213
|
+
if (sources.length > 1)
|
|
214
|
+
push(`library entry takes only one of ${sources.join(", ")}`);
|
|
215
|
+
const refs = ["branch", "tag", "commit"].filter((k) => v[k] !== undefined);
|
|
216
|
+
if (refs.length > 1)
|
|
217
|
+
push(`library entry takes only one of branch, tag or commit, got ${refs.join(", ")}`);
|
|
218
|
+
if (v["git"] === undefined) {
|
|
219
|
+
for (const key of [...refs, ...v["subdir"] === undefined ? [] : ["subdir"]]) {
|
|
220
|
+
push(`"${key}" needs a "git" url`, [key]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const subdir = v["subdir"];
|
|
224
|
+
if (typeof subdir === "string" && (subdir.startsWith("/") || subdir.split("/").includes(".."))) {
|
|
225
|
+
push('"subdir" must be a relative path inside the repo, with no ".." segment', ["subdir"]);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
197
228
|
var game = obj({
|
|
198
229
|
gameFiles: obj({ source: oneOf(["mount", "image"]), host: str.optional(), container: str }).check(requiredWhen("host", (v) => v["source"] === "mount")),
|
|
199
230
|
dataDir: obj({
|
|
@@ -219,15 +250,7 @@ var game = obj({
|
|
|
219
250
|
dlc: strArray,
|
|
220
251
|
preCore: strArray.optional(),
|
|
221
252
|
base: strArray.optional(),
|
|
222
|
-
library: z.record(z.string(),
|
|
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(),
|
|
253
|
+
library: z.record(z.string(), libraryEntry, { error: "expected an object" }).optional(),
|
|
231
254
|
modes: z.array(modeName, { error: "expected a non-empty array" }).min(1, {
|
|
232
255
|
error: "expected a non-empty array"
|
|
233
256
|
}),
|
|
@@ -240,6 +263,7 @@ var root = obj({
|
|
|
240
263
|
plugins: strArray.optional(),
|
|
241
264
|
dataRoot: str,
|
|
242
265
|
defaults: obj({ settings: settings.optional() }).optional(),
|
|
266
|
+
steamcmd: obj({ path: str.optional() }).optional(),
|
|
243
267
|
games: z.unknown()
|
|
244
268
|
});
|
|
245
269
|
function isObj(v) {
|
|
@@ -268,6 +292,7 @@ var PROJECT_OBJECT = z2.strictObject({
|
|
|
268
292
|
defaultProfile: projectName.optional(),
|
|
269
293
|
profiles: z2.record(z2.string(), z2.unknown()).optional(),
|
|
270
294
|
settings: z2.record(z2.string(), z2.unknown()).optional(),
|
|
295
|
+
library: z2.record(z2.string(), z2.unknown()).optional(),
|
|
271
296
|
detach: projectBool.optional(),
|
|
272
297
|
mods: projectList.optional(),
|
|
273
298
|
without: projectList.optional(),
|
|
@@ -298,10 +323,14 @@ var PROJECT_OBJECT = z2.strictObject({
|
|
|
298
323
|
resolution: projectResolution.optional()
|
|
299
324
|
}, { error: "expected an object" });
|
|
300
325
|
var PROJECT_SCHEMA = PROJECT_OBJECT.check((ctx) => {
|
|
301
|
-
const { game, profiles, settings } = ctx.value;
|
|
326
|
+
const { game, profiles, settings, library } = ctx.value;
|
|
302
327
|
if (game !== undefined)
|
|
303
328
|
return;
|
|
304
|
-
for (const [key, value] of [
|
|
329
|
+
for (const [key, value] of [
|
|
330
|
+
["profiles", profiles],
|
|
331
|
+
["settings", settings],
|
|
332
|
+
["library", library]
|
|
333
|
+
]) {
|
|
305
334
|
if (value === undefined)
|
|
306
335
|
continue;
|
|
307
336
|
ctx.issues.push({
|
package/dist/types/cli/args.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface SubcommandSpec {
|
|
|
9
9
|
positionals: PositionalSlot[];
|
|
10
10
|
/** Flag names beyond the global set, in the order help should show them. */
|
|
11
11
|
flags: readonly string[];
|
|
12
|
+
/** Slots to use instead of `positionals` when the next word is one of these. */
|
|
13
|
+
subverbs?: Readonly<Record<string, PositionalSlot[]>>;
|
|
12
14
|
}
|
|
13
15
|
/** The subcommand table. `modless` is reserved as a built-in profile, not a verb. */
|
|
14
16
|
export declare const SUBCOMMANDS: readonly SubcommandSpec[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GamePlugin } from '../plugin';
|
|
2
|
+
import type { ParsedArgs, ProjectDefaults, RootConfig } from '../types';
|
|
3
|
+
export interface ModsContext {
|
|
4
|
+
config: RootConfig;
|
|
5
|
+
plugins: Map<string, GamePlugin>;
|
|
6
|
+
defaults: ProjectDefaults;
|
|
7
|
+
/** Where findProjectConfig starts walking. Injectable so tests need no chdir. */
|
|
8
|
+
cwd: string;
|
|
9
|
+
/** The resolved global config path, or where to create one. Injectable for the same reason. */
|
|
10
|
+
globalPath: string;
|
|
11
|
+
/** What `checkDrift` asks steam with. Injectable so tests never reach the network. */
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
}
|
|
14
|
+
/** The same fallback loadConfig uses, so an error names the file `config edit` would open. */
|
|
15
|
+
export declare function globalConfigPath(): Promise<string>;
|
|
16
|
+
export declare function modsAdd(args: ParsedArgs, ctx: ModsContext): Promise<number>;
|
|
17
|
+
export declare function modsRm(args: ParsedArgs, ctx: ModsContext): Promise<number>;
|
|
18
|
+
export declare function modsSync(args: ParsedArgs, ctx: ModsContext): Promise<number>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A value of `undefined` deletes the key rather than writing an undefined. */
|
|
2
|
+
export interface ConfigEdit {
|
|
3
|
+
path: (string | number)[];
|
|
4
|
+
value: unknown;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* jsonc-parser never sniffs the file, so an inserted block lands with whatever width it is
|
|
8
|
+
* handed. A file indented with tabs and edited with spaces reads as two files.
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectIndent(text: string): {
|
|
11
|
+
tabSize: number;
|
|
12
|
+
insertSpaces: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function writeConfig(file: string, edits: ConfigEdit[]): Promise<void>;
|
|
@@ -18,7 +18,7 @@ export declare function runtimeLayerRef(ref: string): string;
|
|
|
18
18
|
export declare function ensureRuntimeLayer(ref: string): Promise<string>;
|
|
19
19
|
/**
|
|
20
20
|
* Builds local mods before launch. `always` builds every local mod; `auto` builds only the
|
|
21
|
-
* ones resolution flagged stale; `never` skips. A build failure stops the launch
|
|
21
|
+
* ones resolution flagged stale; `never` skips. A build failure stops the launch, shipping
|
|
22
22
|
* the previous DLL after a failed compile is how you debug code that is not running.
|
|
23
23
|
*/
|
|
24
24
|
export declare function buildLocalMods(plan: LaunchPlan, policy: BuildPolicy): Promise<void>;
|
|
@@ -10,7 +10,12 @@ export interface ResolveOptions {
|
|
|
10
10
|
index?: ModIndex;
|
|
11
11
|
/** Overrides process.cwd() for ambient worktree detection; tests set it. */
|
|
12
12
|
cwd?: string;
|
|
13
|
+
/** Lowercased packageId to the clone directory prepared for its git pin. */
|
|
14
|
+
sources?: ReadonlyMap<string, string>;
|
|
15
|
+
/** Workshop ids a real launch would fetch, which this command did not. */
|
|
16
|
+
unfetched?: string[];
|
|
13
17
|
}
|
|
18
|
+
export declare function notFetched(id: string): string;
|
|
14
19
|
export declare function resolvePlan(options: ResolveOptions): Promise<{
|
|
15
20
|
plan: LaunchPlan;
|
|
16
21
|
problems: Problem[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Valve's KeyValues text format, as Steam writes it to `appworkshop_<appid>.acf`. Steam owns
|
|
3
|
+
* that file, so every failure here degrades to an empty result instead of throwing.
|
|
4
|
+
*/
|
|
5
|
+
export type AcfNode = {
|
|
6
|
+
[key: string]: string | AcfNode;
|
|
7
|
+
};
|
|
8
|
+
type Installed = {
|
|
9
|
+
manifest: string;
|
|
10
|
+
timeupdated: number;
|
|
11
|
+
};
|
|
12
|
+
/** Parses KeyValues text. A malformed file is an empty object, never a throw. */
|
|
13
|
+
export declare function parseAcf(text: string): AcfNode;
|
|
14
|
+
/**
|
|
15
|
+
* The installed workshop items, keyed by item id. Manifest ids stay strings: they run past
|
|
16
|
+
* Number.MAX_SAFE_INTEGER and parsing one as a number corrupts it silently.
|
|
17
|
+
*/
|
|
18
|
+
export declare function installedItems(text: string): Map<string, Installed>;
|
|
19
|
+
export {};
|
|
@@ -5,6 +5,11 @@ import type { GameConfig, ModIndex, ModRecord, Problem, WorktreeRequest } from '
|
|
|
5
5
|
* routinely named `[KV] Mod Manager`, so brackets and braces must not be glob syntax.
|
|
6
6
|
*/
|
|
7
7
|
export declare function globMatch(pattern: string, path: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Content hash across every download root, mtime for `game.workshopRoot`. The steam client owns
|
|
10
|
+
* that one and leaves it alone when idle, and an unreadable acf there still means no caching.
|
|
11
|
+
*/
|
|
12
|
+
export declare function workshopStamp(game: GameConfig, dataRoot: string | undefined): string | null;
|
|
8
13
|
/**
|
|
9
14
|
* Stamps every record inside a requested worktree, then scans the worktree itself so a tree
|
|
10
15
|
* no scanRoot reaches still contributes. Only a caller who stood in, typed, or exported a
|
|
@@ -18,10 +23,11 @@ export declare function applyWorktreeRequests(index: ModIndex, requests: Worktre
|
|
|
18
23
|
*/
|
|
19
24
|
export declare function applySourceOverrides(index: ModIndex, overrides: string[], config: GameConfig): Promise<Problem[]>;
|
|
20
25
|
/**
|
|
21
|
-
* Scans the game install, then every scan root in declaration order, then the
|
|
22
|
-
* Local roots rescan every launch; only the workshop scan is cached,
|
|
26
|
+
* Scans the game install, then every scan root in declaration order, then the source cache,
|
|
27
|
+
* then the workshop roots. Local roots rescan every launch; only the workshop scan is cached,
|
|
28
|
+
* against the acf stamps. Without `dataRoot` there is no download root to scan.
|
|
23
29
|
*/
|
|
24
|
-
export declare function buildIndex(game: string, config: GameConfig, plugin: GamePlugin): Promise<ModIndex>;
|
|
30
|
+
export declare function buildIndex(game: string, config: GameConfig, plugin: GamePlugin, sourcesDir?: string, dataRoot?: string): Promise<ModIndex>;
|
|
25
31
|
/**
|
|
26
32
|
* `path:` and `workshop:` are explicit; a bare string resolves as exact packageId, then the
|
|
27
33
|
* game's alias map, then a CLI-only short name. Ambiguity that the ladder cannot break is fatal.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { GameConfig, LibraryEntry, ModEntry, ParsedArgs, ProfileConfig } from '../types';
|
|
2
|
+
export type GitRef = {
|
|
3
|
+
kind: 'branch' | 'tag' | 'commit';
|
|
4
|
+
value: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* A library pin by id, case-blind. Exact then lowercase covers every normal config without a
|
|
8
|
+
* scan; the scan is the only way to reach a mixed-case key from a ref spelled differently.
|
|
9
|
+
*/
|
|
10
|
+
export declare function libraryPin(game: GameConfig, id: string): LibraryEntry | undefined;
|
|
11
|
+
export declare function sourcesRoot(dataRoot: string): string;
|
|
12
|
+
export declare function normalizeUrl(url: string): string;
|
|
13
|
+
export declare function cloneDir(dataRoot: string, url: string, ref: GitRef): string;
|
|
14
|
+
export interface GitPin {
|
|
15
|
+
url: string;
|
|
16
|
+
ref?: GitRef;
|
|
17
|
+
subdir?: string;
|
|
18
|
+
}
|
|
19
|
+
export type SyncMode = 'use' | 'fetch' | 'force';
|
|
20
|
+
export interface SyncResult {
|
|
21
|
+
dir: string;
|
|
22
|
+
warning?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function isMoving(ref: GitRef): boolean;
|
|
25
|
+
export declare function gitRefOf(pin: {
|
|
26
|
+
branch?: string;
|
|
27
|
+
tag?: string;
|
|
28
|
+
commit?: string;
|
|
29
|
+
}): GitRef | undefined;
|
|
30
|
+
export declare function defaultBranch(url: string): GitRef;
|
|
31
|
+
export declare function ensureClone(dataRoot: string, pin: GitPin, ref: GitRef, mode: SyncMode): Promise<SyncResult>;
|
|
32
|
+
export declare function lockDir(dir: string): Promise<() => Promise<void>>;
|
|
33
|
+
export declare function unlinkOrphan(path: string, seen: string): Promise<boolean>;
|
|
34
|
+
export interface PreparedSources {
|
|
35
|
+
/** lowercased packageId -> clone directory. Handed to resolvePlan as `sources`. */
|
|
36
|
+
dirs: Map<string, string>;
|
|
37
|
+
warnings: string[];
|
|
38
|
+
/** One entry per distinct clone this call handled, fetched or reused. Makes dedupe testable. */
|
|
39
|
+
fetched: string[];
|
|
40
|
+
/** Released by execute(), and by run()'s finally. Never before buildLocalMods. */
|
|
41
|
+
release: () => Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Every entry a launch of this profile reaches, in collectSlots order, minus what `exclude` and
|
|
45
|
+
* `--without` drop by id. A `match:` glob has no id to drop by, so it survives to the index.
|
|
46
|
+
*/
|
|
47
|
+
export declare function reachedEntries(game: GameConfig, profile: ProfileConfig, args: Partial<ParsedArgs>): ModEntry[];
|
|
48
|
+
/**
|
|
49
|
+
* The map `prepareSources` builds, read off the cache alone: no lock, no clone, no network.
|
|
50
|
+
* `mods` and `verify` must not fetch, but with no map at all a git pin's `subdir` is dropped and
|
|
51
|
+
* the scan answers by packageId instead, which is a coin flip between two directories of one
|
|
52
|
+
* repo. An unpinned entry has no ref to derive a directory from, so it takes the one `branch-*`
|
|
53
|
+
* clone already on disk.
|
|
54
|
+
*/
|
|
55
|
+
export declare function cachedSources(game: GameConfig, profileName: string, args: Partial<ParsedArgs>, dataRoot: string): Map<string, string>;
|
|
56
|
+
/**
|
|
57
|
+
* Clones or fetches every git-pinned mod the run will ask for, before the index is built, and
|
|
58
|
+
* keeps one lock per clone until the caller releases it. `git()` is spawnSync, so these
|
|
59
|
+
* serialize whatever this function looks like.
|
|
60
|
+
*/
|
|
61
|
+
export declare function prepareSources(game: GameConfig, profileName: string, args: ParsedArgs, dataRoot: string, allowFetch: boolean): Promise<PreparedSources>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { GameConfig, RootConfig } from '../types';
|
|
2
|
+
export declare const STEAMCMD_IMAGE = "steamcmd/steamcmd";
|
|
3
|
+
/**
|
|
4
|
+
* `host` spawns argv directly with env; `docker` already carries the bind and the user mapping.
|
|
5
|
+
* Either way a caller appends steamcmd's own flags to argv.
|
|
6
|
+
*/
|
|
7
|
+
export type SteamcmdRunner = {
|
|
8
|
+
kind: 'host';
|
|
9
|
+
argv: string[];
|
|
10
|
+
env: Record<string, string>;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'docker';
|
|
13
|
+
argv: string[];
|
|
14
|
+
env: Record<string, string>;
|
|
15
|
+
};
|
|
16
|
+
/** HOME for steamcmd. Everything it writes hangs off here, so it is per-dataRoot, not the user's. */
|
|
17
|
+
export declare function steamHome(dataRoot: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Where downloads land. `run` pins it with `+force_install_dir`, so this is our layout rather
|
|
20
|
+
* than whichever one the steamcmd on this machine would have picked: measured 2026-09-21, the
|
|
21
|
+
* arch wrapper writes .steam/SteamApps, the docker image writes .local/share/Steam/steamapps,
|
|
22
|
+
* and a plain Valve tarball writes $HOME/Steam. Pinning makes all three land here.
|
|
23
|
+
*/
|
|
24
|
+
export declare function downloadRoot(dataRoot: string, game: GameConfig): string;
|
|
25
|
+
/**
|
|
26
|
+
* Configured path if set, else PATH, else the docker image. A configured path that is not an
|
|
27
|
+
* executable file is an error, not a fallback. Docker gets `--user`: without it the tree
|
|
28
|
+
* comes back root-owned and the next run with a host binary cannot write it. The bind is an
|
|
29
|
+
* identity bind so the paths steamcmd prints are valid on the host too.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveSteamcmd(config: RootConfig): SteamcmdRunner;
|
|
32
|
+
/** The published file id out of a workshop url, or undefined if it is not one. */
|
|
33
|
+
export declare function workshopUrlId(url: string | undefined): string | undefined;
|
|
34
|
+
export type DownloadOutcome = {
|
|
35
|
+
ok: true;
|
|
36
|
+
dir: string;
|
|
37
|
+
bytes: number;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
reason: string;
|
|
41
|
+
};
|
|
42
|
+
export interface DownloadReport {
|
|
43
|
+
items: Map<string, DownloadOutcome>;
|
|
44
|
+
warnings: string[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Downloads every id in one steamcmd invocation. Connect is 3.4s of a 3.5s single-item run, so a
|
|
48
|
+
* hundred ids cost about what one does plus the transfer.
|
|
49
|
+
*
|
|
50
|
+
* A failed item is a warning, not a throw: whatever is already on disk stays usable.
|
|
51
|
+
*
|
|
52
|
+
* An anonymous download can fail transiently and succeed untouched on a second pass, so the
|
|
53
|
+
* misses get one re-run before they count as failures. Two passes, not more: a genuinely
|
|
54
|
+
* unavailable item pays the 3.4s connect for every pass that will never succeed.
|
|
55
|
+
*/
|
|
56
|
+
export declare function downloadItems(config: RootConfig, game: GameConfig, dataRoot: string, ids: string[]): Promise<DownloadReport>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GamePlugin } from '../plugin';
|
|
2
|
+
import type { GameConfig, ParsedArgs, Problem, RootConfig } from '../types';
|
|
3
|
+
export interface PreparedWorkshop {
|
|
4
|
+
/** Every id this walk inspected: the profile's own, plus the dependencies it reached. */
|
|
5
|
+
ids: Set<string>;
|
|
6
|
+
warnings: string[];
|
|
7
|
+
problems: Problem[];
|
|
8
|
+
/** Inspected ids with no directory under any mounted root when the walk finished. */
|
|
9
|
+
unfetched: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Downloads every workshop item the profile names and every workshop dependency of those,
|
|
13
|
+
* recursively, before the mod index is built. Never throws for a download that failed.
|
|
14
|
+
*/
|
|
15
|
+
export declare function prepareWorkshop(game: GameConfig, profileName: string, args: ParsedArgs, config: RootConfig, allowFetch: boolean, plugin: GamePlugin, sources: ReadonlyMap<string, string>): Promise<PreparedWorkshop>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface DriftReport {
|
|
2
|
+
/** Ids that need steamcmd, because they drifted, are absent from the .acf, or are not on disk. */
|
|
3
|
+
needed: string[];
|
|
4
|
+
/** Ids steam will not serve: removed, private or hidden. Not the same as up to date. */
|
|
5
|
+
unavailable: string[];
|
|
6
|
+
warnings: string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Which of these items changed, without spawning anything. `roots` is every workshop content
|
|
10
|
+
* root, what `downloadRoots` returns, and none of them has to exist. The whole check is best
|
|
11
|
+
* effort: any failure downgrades to the items that are not on disk, never to fetching everything.
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkDrift(ids: string[], roots: string[], fetchImpl?: typeof fetch): Promise<DriftReport>;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -110,6 +110,11 @@ export interface ScanRoot {
|
|
|
110
110
|
export interface LibraryEntry {
|
|
111
111
|
workshop?: number;
|
|
112
112
|
path?: string;
|
|
113
|
+
git?: string;
|
|
114
|
+
branch?: string;
|
|
115
|
+
tag?: string;
|
|
116
|
+
commit?: string;
|
|
117
|
+
subdir?: string;
|
|
113
118
|
}
|
|
114
119
|
/** Matches a family of mods by pattern instead of naming each one. */
|
|
115
120
|
export interface DynamicModEntry {
|
|
@@ -194,6 +199,10 @@ export interface RootConfig {
|
|
|
194
199
|
defaults?: {
|
|
195
200
|
settings?: Partial<Settings>;
|
|
196
201
|
};
|
|
202
|
+
/** Where the steamcmd binary is. Root level, not per game, because it names a host tool. */
|
|
203
|
+
steamcmd?: {
|
|
204
|
+
path?: string;
|
|
205
|
+
};
|
|
197
206
|
games: Record<string, GameConfig>;
|
|
198
207
|
}
|
|
199
208
|
export type ModSourceKind = 'local' | 'workshop' | 'official' | 'core';
|
|
@@ -235,6 +244,11 @@ export interface ModRecord {
|
|
|
235
244
|
};
|
|
236
245
|
/** Which scanRoot produced it, for the precedence ladder. */
|
|
237
246
|
rootIndex: number;
|
|
247
|
+
/**
|
|
248
|
+
* The clone directory's mtime. Source-cache records only, which is what keeps it from
|
|
249
|
+
* reordering a user's checkout against anything.
|
|
250
|
+
*/
|
|
251
|
+
clonedAt?: number;
|
|
238
252
|
}
|
|
239
253
|
export type WorktreeSource = 'flag' | 'env' | 'cwd' | 'ref';
|
|
240
254
|
export interface WorktreeRequest {
|
|
@@ -405,14 +419,38 @@ export interface ParsedArgs {
|
|
|
405
419
|
supervised: boolean;
|
|
406
420
|
/** `-f`: keep printing as the run writes, instead of dumping what is there. */
|
|
407
421
|
follow: boolean;
|
|
422
|
+
/** The write verb under `mods`. Unset means the read verb. */
|
|
423
|
+
subverb?: 'add' | 'rm' | 'sync';
|
|
424
|
+
/** Where `mods add` pulls the mod from. */
|
|
425
|
+
source?: {
|
|
426
|
+
kind: 'path';
|
|
427
|
+
value: string;
|
|
428
|
+
} | {
|
|
429
|
+
kind: 'workshop';
|
|
430
|
+
value: number;
|
|
431
|
+
} | {
|
|
432
|
+
kind: 'git';
|
|
433
|
+
url: string;
|
|
434
|
+
ref?: {
|
|
435
|
+
kind: 'branch' | 'tag' | 'commit';
|
|
436
|
+
value: string;
|
|
437
|
+
};
|
|
438
|
+
subdir?: string;
|
|
439
|
+
};
|
|
440
|
+
/** Which config file a write lands in. */
|
|
441
|
+
target?: 'global' | 'project';
|
|
442
|
+
/** Overwrite an existing entry instead of refusing. */
|
|
443
|
+
force?: boolean;
|
|
408
444
|
rest: string[];
|
|
409
445
|
}
|
|
410
|
-
export type ProjectDefaults = Partial<Omit<ParsedArgs, 'subcommand' | 'cleanTier' | 'yes' | 'help' | 'rest' | 'profile' | 'supervised' | 'noDetach' | 'noReplace' | 'follow'>> & {
|
|
446
|
+
export type ProjectDefaults = Partial<Omit<ParsedArgs, 'subcommand' | 'cleanTier' | 'yes' | 'help' | 'rest' | 'profile' | 'supervised' | 'noDetach' | 'noReplace' | 'follow' | 'subverb' | 'source' | 'target' | 'force'>> & {
|
|
411
447
|
/** Replaces the old `profile:` key. Falls back to the first entry in `profiles`. */
|
|
412
448
|
defaultProfile?: string;
|
|
413
449
|
/** Validated by validateConfig after the splice, not here. */
|
|
414
450
|
profiles?: Record<string, unknown>;
|
|
415
451
|
settings?: Record<string, unknown>;
|
|
452
|
+
/** Spliced per id over games.<game>.library, so a repo pin replaces a global one whole. */
|
|
453
|
+
library?: Record<string, unknown>;
|
|
416
454
|
/** Profile keys in source order. Object.keys sorts integer-like names to the front. */
|
|
417
455
|
profileOrder?: string[];
|
|
418
456
|
/** The file these came from. Four suffixes are legal, so output must not guess the name. */
|