@gamecrate/cli 1.3.0 → 1.4.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 +177 -142
- package/dist/types/mods/steamcmd.d.ts +5 -0
- package/dist/types/types.d.ts +2 -1
- package/package.json +1 -1
package/dist/gamecrate.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { chown, cp, mkdir as mkdir6, readdir as
|
|
4
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
5
|
+
import { chown, cp, mkdir as mkdir6, readdir as readdir11, readFile as readFile12, rm as rm5, rmdir, stat as stat6, writeFile as writeFile7 } from "node:fs/promises";
|
|
6
6
|
import { homedir as homedir5 } from "node:os";
|
|
7
|
-
import { basename as basename11, dirname as
|
|
7
|
+
import { basename as basename11, dirname as dirname10, join as join22 } from "node:path";
|
|
8
8
|
import { setTimeout as sleep7 } from "node:timers/promises";
|
|
9
9
|
|
|
10
10
|
// src/cli/args.ts
|
|
@@ -150,7 +150,7 @@ var SUBCOMMANDS = [
|
|
|
150
150
|
summary: "tiered wipe of a profile",
|
|
151
151
|
usage: "<game> [profile]",
|
|
152
152
|
positionals: ["game", "profile"],
|
|
153
|
-
flags: ["--staging", "--logs", "--all", "--yes", "--instance", "--worktree", "--no-worktree"]
|
|
153
|
+
flags: ["--staging", "--logs", "--all", "--downloads", "--yes", "--instance", "--worktree", "--no-worktree"]
|
|
154
154
|
},
|
|
155
155
|
{
|
|
156
156
|
name: "clone",
|
|
@@ -291,7 +291,7 @@ function enumOption(flags, summary, values) {
|
|
|
291
291
|
}
|
|
292
292
|
function buildProgram() {
|
|
293
293
|
const program = new Command;
|
|
294
|
-
program.name("gamecrate").exitOverride().helpOption(false).allowExcessArguments(true).showSuggestionAfterError(false).configureOutput({ writeOut: () => {}, writeErr: () => {} }).argument("[args...]").option("--mod <id>", "add a mod to the profile set", collect, []).option("--without <id>", "drop a mod from the resolved set", collect, []).option("--only <id>", "restrict the resolved set to these mods", collect, []).option("--worktree <path>", "promote mods from this git worktree, in its own instance ($GAMECRATE_WORKTREE)", collect, []).option("--use <packageId>=<path>", "force one mod to load from this directory, whatever the profile pins", collect, []).option("--no-worktree", "ignore the current worktree and $GAMECRATE_WORKTREE").option("--instance <name>", "run under a named sub-profile with its own saves, logs and container").addOption(enumOption(`--mode <${MODES.join("|")}>`, "how the game is displayed", MODES)).option("--marker <str>", "exit 0 as soon as this string appears in the log").option("--timeout <seconds>", "bound a marker run, or a headless run with no marker", (v) => seconds("--timeout", v)).option("--render-wait <seconds>", "settle time before a screenshot is taken", (v) => seconds("--render-wait", v)).option("--resolution <width>x<height>", "override the game resolution", parseResolution).addOption(enumOption(`--network <${NETWORK_POLICIES.join("|")}>`, "the container's network mode", NETWORK_POLICIES)).option("--log <path>", "route launch stdout and stderr to one file").addOption(enumOption(`--pull <${PULL_POLICIES.join("|")}>`, "when to pull the runtime image", PULL_POLICIES)).option("--build", "build local C# mods before launching").option("--no-build", "never build, even when an assembly is stale").option("--no-stale-check", "do not warn when a mod's sources are newer than its assemblies").option("--replace", "stop whatever is holding this profile and instance, then launch").option("--no-replace", "refuse when this profile and instance are already running").option("--detach", "start the run in the background and return the prompt").option("--no-detach", "stay in the foreground, whatever the profile or project config asks for").addOption(new Option("--supervised <instanceDir>").hideHelp()).addOption(enumOption(`--sort <${SORTS.join("|")}>`, "load order: the profile order, or a topological sort", SORTS)).option("--docker-arg <arg>", "one extra argv element for docker run", collect, []).option("--dry-run", "resolve and validate fully, write nothing").option("--print-plan", "print the resolved launch plan instead of launching").option("--json", "machine-readable output").option("--root", "run as root instead of mapping the host uid").option("--staging", "clean: wipe .stage only (the default)").option("--logs", "clean: wipe the captured run logs").option("--all", "clean: wipe the whole profile
|
|
294
|
+
program.name("gamecrate").exitOverride().helpOption(false).allowExcessArguments(true).showSuggestionAfterError(false).configureOutput({ writeOut: () => {}, writeErr: () => {} }).argument("[args...]").option("--mod <id>", "add a mod to the profile set", collect, []).option("--without <id>", "drop a mod from the resolved set", collect, []).option("--only <id>", "restrict the resolved set to these mods", collect, []).option("--worktree <path>", "promote mods from this git worktree, in its own instance ($GAMECRATE_WORKTREE)", collect, []).option("--use <packageId>=<path>", "force one mod to load from this directory, whatever the profile pins", collect, []).option("--no-worktree", "ignore the current worktree and $GAMECRATE_WORKTREE").option("--instance <name>", "run under a named sub-profile with its own saves, logs and container").addOption(enumOption(`--mode <${MODES.join("|")}>`, "how the game is displayed", MODES)).option("--marker <str>", "exit 0 as soon as this string appears in the log").option("--timeout <seconds>", "bound a marker run, or a headless run with no marker", (v) => seconds("--timeout", v)).option("--render-wait <seconds>", "settle time before a screenshot is taken", (v) => seconds("--render-wait", v)).option("--resolution <width>x<height>", "override the game resolution", parseResolution).addOption(enumOption(`--network <${NETWORK_POLICIES.join("|")}>`, "the container's network mode", NETWORK_POLICIES)).option("--log <path>", "route launch stdout and stderr to one file").addOption(enumOption(`--pull <${PULL_POLICIES.join("|")}>`, "when to pull the runtime image", PULL_POLICIES)).option("--build", "build local C# mods before launching").option("--no-build", "never build, even when an assembly is stale").option("--no-stale-check", "do not warn when a mod's sources are newer than its assemblies").option("--replace", "stop whatever is holding this profile and instance, then launch").option("--no-replace", "refuse when this profile and instance are already running").option("--detach", "start the run in the background and return the prompt").option("--no-detach", "stay in the foreground, whatever the profile or project config asks for").addOption(new Option("--supervised <instanceDir>").hideHelp()).addOption(enumOption(`--sort <${SORTS.join("|")}>`, "load order: the profile order, or a topological sort", SORTS)).option("--docker-arg <arg>", "one extra argv element for docker run", collect, []).option("--dry-run", "resolve and validate fully, write nothing").option("--print-plan", "print the resolved launch plan instead of launching").option("--json", "machine-readable output").option("--root", "run as root instead of mapping the host uid").option("--staging", "clean: wipe .stage only (the default)").option("--logs", "clean: wipe the captured run logs").option("--all", "clean: wipe the whole profile and the game downloads (needs --yes)").option("--downloads", "clean: wipe this game's workshop downloads, keeping steamcmd").option("-f, --follow", "keep printing as the run writes").option("-y, --yes", "skip destructive-action confirmation").option("--path <dir>", "mods add: take the mod from this directory").option("--workshop <id>", "mods add: take the mod from this steam workshop item", workshopId).option("--git <url>", "mods add: clone the mod from this repository").option("--branch <name>", "mods add: track this git branch").option("--tag <name>", "mods add: pin this git tag").option("--commit <sha>", "mods add: pin this git commit").option("--subdir <path>", "mods add: the mod folder inside the repository").option("--global", "write to the global config").option("--project", "write to the project config").option("--force", "overwrite an entry that is already there").option("-h, --help", "this help");
|
|
295
295
|
return program;
|
|
296
296
|
}
|
|
297
297
|
function checkValueTokens(program, head) {
|
|
@@ -336,7 +336,7 @@ function parseArgs(argv, opts = {}) {
|
|
|
336
336
|
counts.set(long, (counts.get(long) ?? 0) + 1);
|
|
337
337
|
if (long === "--worktree" && value !== undefined)
|
|
338
338
|
worktree.push(value);
|
|
339
|
-
if (long === "--staging" || long === "--logs" || long === "--all") {
|
|
339
|
+
if (long === "--staging" || long === "--logs" || long === "--all" || long === "--downloads") {
|
|
340
340
|
cleanTier = long.slice(2);
|
|
341
341
|
}
|
|
342
342
|
});
|
|
@@ -796,9 +796,9 @@ function list(args, config, defaults) {
|
|
|
796
796
|
}
|
|
797
797
|
|
|
798
798
|
// src/cli/mods.ts
|
|
799
|
-
import { existsSync as
|
|
800
|
-
import { mkdir as mkdir2, readFile as readFile7, readdir as
|
|
801
|
-
import { dirname as
|
|
799
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
800
|
+
import { mkdir as mkdir2, readFile as readFile7, readdir as readdir6, writeFile as writeFile3 } from "node:fs/promises";
|
|
801
|
+
import { dirname as dirname7, join as join12, relative as relative2, resolve as resolve4 } from "node:path";
|
|
802
802
|
|
|
803
803
|
// src/config/load.ts
|
|
804
804
|
import { z as z2 } from "zod";
|
|
@@ -3143,8 +3143,9 @@ async function prepareSources(game, profileName, args, dataRoot, allowFetch) {
|
|
|
3143
3143
|
|
|
3144
3144
|
// src/mods/steamcmd.ts
|
|
3145
3145
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
3146
|
-
import { accessSync, constants, mkdirSync as mkdirSync3, statSync as statSync2 } from "node:fs";
|
|
3147
|
-
import {
|
|
3146
|
+
import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, statSync as statSync2 } from "node:fs";
|
|
3147
|
+
import { readdir as readdir5, rm as rm2 } from "node:fs/promises";
|
|
3148
|
+
import { delimiter, dirname as dirname5, join as join10 } from "node:path";
|
|
3148
3149
|
import { setTimeout as sleep4 } from "node:timers/promises";
|
|
3149
3150
|
var STEAMCMD_IMAGE = "steamcmd/steamcmd";
|
|
3150
3151
|
function steamHome(dataRoot) {
|
|
@@ -3153,6 +3154,14 @@ function steamHome(dataRoot) {
|
|
|
3153
3154
|
function downloadRoot(dataRoot, game) {
|
|
3154
3155
|
return join10(steamHome(dataRoot), "steamapps", "workshop", "content", String(game.steamAppId));
|
|
3155
3156
|
}
|
|
3157
|
+
async function removeDownloads(root, steamAppId) {
|
|
3158
|
+
if (!existsSync5(root))
|
|
3159
|
+
return `no workshop downloads at ${root}`;
|
|
3160
|
+
const items = (await readdir5(root).catch(() => [])).length;
|
|
3161
|
+
await rm2(root, { recursive: true, force: true });
|
|
3162
|
+
await rm2(join10(dirname5(dirname5(root)), `appworkshop_${steamAppId}.acf`), { force: true });
|
|
3163
|
+
return `removed ${root} (${items} item(s))`;
|
|
3164
|
+
}
|
|
3156
3165
|
function resolveSteamcmd(config) {
|
|
3157
3166
|
const home = steamHome(config.dataRoot);
|
|
3158
3167
|
const configured = config.steamcmd?.path === undefined ? undefined : expandHome(config.steamcmd.path);
|
|
@@ -3283,7 +3292,7 @@ ${r.stderr ?? ""}`.replace(ANSI, "");
|
|
|
3283
3292
|
|
|
3284
3293
|
// src/mods/workshopapi.ts
|
|
3285
3294
|
import { readFile as readFile6, stat as stat4 } from "node:fs/promises";
|
|
3286
|
-
import { basename as basename5, dirname as
|
|
3295
|
+
import { basename as basename5, dirname as dirname6, join as join11 } from "node:path";
|
|
3287
3296
|
|
|
3288
3297
|
// src/mods/acf.ts
|
|
3289
3298
|
var MAX_DEPTH = 100;
|
|
@@ -3324,50 +3333,66 @@ function quoted(text, start) {
|
|
|
3324
3333
|
}
|
|
3325
3334
|
return null;
|
|
3326
3335
|
}
|
|
3327
|
-
function
|
|
3336
|
+
function root2(text) {
|
|
3337
|
+
const node = {};
|
|
3338
|
+
let i = skip(text, 0);
|
|
3339
|
+
while (i < text.length) {
|
|
3340
|
+
if (text[i] !== '"')
|
|
3341
|
+
return null;
|
|
3342
|
+
const next = readEntry(text, i, 0, node);
|
|
3343
|
+
if (next === null)
|
|
3344
|
+
return null;
|
|
3345
|
+
i = skip(text, next);
|
|
3346
|
+
}
|
|
3347
|
+
return node;
|
|
3348
|
+
}
|
|
3349
|
+
function block(text, start, depth) {
|
|
3328
3350
|
if (depth > MAX_DEPTH)
|
|
3329
3351
|
return null;
|
|
3330
3352
|
const node = {};
|
|
3331
|
-
let i = start;
|
|
3332
|
-
|
|
3333
|
-
i = skip(text, i);
|
|
3334
|
-
if (i >= text.length)
|
|
3335
|
-
return depth === 0 ? { node, next: i } : null;
|
|
3353
|
+
let i = skip(text, start);
|
|
3354
|
+
while (i < text.length) {
|
|
3336
3355
|
if (text[i] === "}")
|
|
3337
|
-
return
|
|
3356
|
+
return { node, next: i + 1 };
|
|
3338
3357
|
if (text[i] !== '"')
|
|
3339
3358
|
return null;
|
|
3340
|
-
const
|
|
3341
|
-
if (
|
|
3342
|
-
return null;
|
|
3343
|
-
i = skip(text, key.next);
|
|
3344
|
-
if (i >= text.length)
|
|
3345
|
-
return null;
|
|
3346
|
-
if (text[i] === "{") {
|
|
3347
|
-
const child = body(text, i + 1, depth + 1);
|
|
3348
|
-
if (child === null)
|
|
3349
|
-
return null;
|
|
3350
|
-
node[key.value] = child.node;
|
|
3351
|
-
i = child.next;
|
|
3352
|
-
continue;
|
|
3353
|
-
}
|
|
3354
|
-
if (text[i] !== '"')
|
|
3359
|
+
const next = readEntry(text, i, depth, node);
|
|
3360
|
+
if (next === null)
|
|
3355
3361
|
return null;
|
|
3356
|
-
|
|
3357
|
-
|
|
3362
|
+
i = skip(text, next);
|
|
3363
|
+
}
|
|
3364
|
+
return null;
|
|
3365
|
+
}
|
|
3366
|
+
function readEntry(text, start, depth, node) {
|
|
3367
|
+
const key = quoted(text, start);
|
|
3368
|
+
if (key === null)
|
|
3369
|
+
return null;
|
|
3370
|
+
const i = skip(text, key.next);
|
|
3371
|
+
if (i >= text.length)
|
|
3372
|
+
return null;
|
|
3373
|
+
if (text[i] === "{") {
|
|
3374
|
+
const child = block(text, i + 1, depth + 1);
|
|
3375
|
+
if (child === null)
|
|
3358
3376
|
return null;
|
|
3359
|
-
node[key.value] =
|
|
3360
|
-
|
|
3377
|
+
node[key.value] = child.node;
|
|
3378
|
+
return child.next;
|
|
3361
3379
|
}
|
|
3380
|
+
if (text[i] !== '"')
|
|
3381
|
+
return null;
|
|
3382
|
+
const value = quoted(text, i);
|
|
3383
|
+
if (value === null)
|
|
3384
|
+
return null;
|
|
3385
|
+
node[key.value] = value.value;
|
|
3386
|
+
return value.next;
|
|
3362
3387
|
}
|
|
3363
3388
|
function parseAcf(text) {
|
|
3364
|
-
return
|
|
3389
|
+
return root2(text) ?? {};
|
|
3365
3390
|
}
|
|
3366
|
-
function section(
|
|
3367
|
-
const direct =
|
|
3391
|
+
function section(node, name) {
|
|
3392
|
+
const direct = node[name];
|
|
3368
3393
|
if (typeof direct === "object")
|
|
3369
3394
|
return direct;
|
|
3370
|
-
for (const child of Object.values(
|
|
3395
|
+
for (const child of Object.values(node)) {
|
|
3371
3396
|
if (typeof child === "object" && typeof child[name] === "object")
|
|
3372
3397
|
return child[name];
|
|
3373
3398
|
}
|
|
@@ -3491,7 +3516,7 @@ function readDetails(payload) {
|
|
|
3491
3516
|
return out;
|
|
3492
3517
|
}
|
|
3493
3518
|
function acfPath(root) {
|
|
3494
|
-
return join11(
|
|
3519
|
+
return join11(dirname6(dirname6(root)), `appworkshop_${basename5(root)}.acf`);
|
|
3495
3520
|
}
|
|
3496
3521
|
async function exists(dir) {
|
|
3497
3522
|
try {
|
|
@@ -3553,8 +3578,8 @@ overwrite them with --force`);
|
|
|
3553
3578
|
edits.push({ path: [...target.prefix, old], value: undefined });
|
|
3554
3579
|
edits.push({ path: [...target.prefix, pin.id], value: pin.entry });
|
|
3555
3580
|
}
|
|
3556
|
-
if (!
|
|
3557
|
-
await mkdir2(
|
|
3581
|
+
if (!existsSync6(target.file)) {
|
|
3582
|
+
await mkdir2(dirname7(target.file), { recursive: true });
|
|
3558
3583
|
await writeFile3(target.file, "");
|
|
3559
3584
|
}
|
|
3560
3585
|
await writeConfig(target.file, edits);
|
|
@@ -3777,7 +3802,7 @@ async function walk(root, manifestFile, plugin) {
|
|
|
3777
3802
|
out.push({ id, dir });
|
|
3778
3803
|
return;
|
|
3779
3804
|
}
|
|
3780
|
-
const entries = await
|
|
3805
|
+
const entries = await readdir6(dir, { withFileTypes: true }).catch(() => []);
|
|
3781
3806
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
3782
3807
|
if (entry.isDirectory() && !entry.name.startsWith("."))
|
|
3783
3808
|
await descend(join12(dir, entry.name));
|
|
@@ -3872,8 +3897,8 @@ function gameHelp(name, game) {
|
|
|
3872
3897
|
notes.push(`alias for ${config.alias}`);
|
|
3873
3898
|
if (config.extends)
|
|
3874
3899
|
notes.push(`extends ${config.extends}`);
|
|
3875
|
-
if (config.autoDependencies)
|
|
3876
|
-
notes.push("auto dependencies");
|
|
3900
|
+
if (config.autoDependencies === false)
|
|
3901
|
+
notes.push("no auto dependencies");
|
|
3877
3902
|
const count = config.mods?.length ?? 0;
|
|
3878
3903
|
if (!config.alias)
|
|
3879
3904
|
notes.push(count === 1 ? "1 entry" : `${count} entries`);
|
|
@@ -3995,7 +4020,7 @@ function hostUserName(uid) {
|
|
|
3995
4020
|
}
|
|
3996
4021
|
|
|
3997
4022
|
// src/docker/preflight.ts
|
|
3998
|
-
import { existsSync as
|
|
4023
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3 } from "node:fs";
|
|
3999
4024
|
import { homedir as homedir3 } from "node:os";
|
|
4000
4025
|
import { basename as basename6, join as join13 } from "node:path";
|
|
4001
4026
|
var CDI_SPEC = "/etc/cdi/nvidia.yaml";
|
|
@@ -4097,7 +4122,7 @@ function checkDisplay(plan, problems) {
|
|
|
4097
4122
|
});
|
|
4098
4123
|
}
|
|
4099
4124
|
function checkCdi(problems) {
|
|
4100
|
-
if (!
|
|
4125
|
+
if (!existsSync7(CDI_SPEC)) {
|
|
4101
4126
|
problems.push({
|
|
4102
4127
|
where: CDI_SPEC,
|
|
4103
4128
|
message: "no CDI spec, so --device nvidia.com/gpu=all cannot resolve",
|
|
@@ -4129,12 +4154,12 @@ function checkGameDir(plan, problems) {
|
|
|
4129
4154
|
problems.push({ where, message: 'gameFiles.source is "mount" but no host path is set' });
|
|
4130
4155
|
return;
|
|
4131
4156
|
}
|
|
4132
|
-
if (!
|
|
4157
|
+
if (!existsSync7(files.host)) {
|
|
4133
4158
|
problems.push({ where, message: `game directory does not exist: ${files.host}` });
|
|
4134
4159
|
return;
|
|
4135
4160
|
}
|
|
4136
4161
|
const executable = join13(files.host, basename6(plan.gameConfig.executable));
|
|
4137
|
-
if (!
|
|
4162
|
+
if (!existsSync7(executable)) {
|
|
4138
4163
|
problems.push({
|
|
4139
4164
|
where,
|
|
4140
4165
|
message: `${files.host} does not contain ${basename6(plan.gameConfig.executable)}`,
|
|
@@ -4157,7 +4182,7 @@ function checkBindSources(plan, problems) {
|
|
|
4157
4182
|
for (const mount of mounts) {
|
|
4158
4183
|
if (mount.type !== "bind" || !mount.source)
|
|
4159
4184
|
continue;
|
|
4160
|
-
if (
|
|
4185
|
+
if (existsSync7(mount.source))
|
|
4161
4186
|
continue;
|
|
4162
4187
|
if (mount.source.startsWith(plan.profileDir))
|
|
4163
4188
|
continue;
|
|
@@ -4331,8 +4356,8 @@ function parseAtoms(stdout) {
|
|
|
4331
4356
|
}
|
|
4332
4357
|
|
|
4333
4358
|
// src/launch/generate.ts
|
|
4334
|
-
import { mkdir as mkdir3, readdir as
|
|
4335
|
-
import { dirname as
|
|
4359
|
+
import { mkdir as mkdir3, readdir as readdir7, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
|
|
4360
|
+
import { dirname as dirname8, join as join14 } from "node:path";
|
|
4336
4361
|
async function readInstallVersion(game, plugin) {
|
|
4337
4362
|
const host = game.gameFiles.host;
|
|
4338
4363
|
if (game.gameFiles.source !== "mount" || host === undefined)
|
|
@@ -4354,7 +4379,7 @@ async function readKnownExpansions(game, plugin, warnings) {
|
|
|
4354
4379
|
const dataDir = join14(expandHome(host), "Data");
|
|
4355
4380
|
let entries;
|
|
4356
4381
|
try {
|
|
4357
|
-
entries = await
|
|
4382
|
+
entries = await readdir7(dataDir, { withFileTypes: true });
|
|
4358
4383
|
} catch {
|
|
4359
4384
|
warnings.push(`could not read ${dataDir}; falling back to the configured dlc list`);
|
|
4360
4385
|
return [...game.dlc];
|
|
@@ -4382,7 +4407,7 @@ async function readKnownExpansions(game, plugin, warnings) {
|
|
|
4382
4407
|
async function generateModsConfig(plan) {
|
|
4383
4408
|
const game = plan.gameConfig;
|
|
4384
4409
|
const target = join14(plan.dataDirHost, game.modsConfig.file);
|
|
4385
|
-
await mkdir3(
|
|
4410
|
+
await mkdir3(dirname8(target), { recursive: true });
|
|
4386
4411
|
const installed = await readInstallVersion(game, plan.plugin);
|
|
4387
4412
|
if (installed === null) {
|
|
4388
4413
|
plan.warnings.push(`could not read Version.txt for ${plan.game}; ModsConfig version may be rejected`);
|
|
@@ -4433,7 +4458,7 @@ function ownedPrefs(plan) {
|
|
|
4433
4458
|
}
|
|
4434
4459
|
async function mergePrefs(plan) {
|
|
4435
4460
|
const target = join14(plan.dataDirHost, plan.gameConfig.prefs.file);
|
|
4436
|
-
await mkdir3(
|
|
4461
|
+
await mkdir3(dirname8(target), { recursive: true });
|
|
4437
4462
|
let existing = null;
|
|
4438
4463
|
try {
|
|
4439
4464
|
existing = await readFile8(target, "utf8");
|
|
@@ -4446,8 +4471,8 @@ async function mergePrefs(plan) {
|
|
|
4446
4471
|
}
|
|
4447
4472
|
|
|
4448
4473
|
// src/launch/supervisor.ts
|
|
4449
|
-
import { existsSync as
|
|
4450
|
-
import { readFile as readFile9, readlink, rm as
|
|
4474
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
4475
|
+
import { readFile as readFile9, readlink, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
|
|
4451
4476
|
import { basename as basename8, join as join15 } from "node:path";
|
|
4452
4477
|
import { setTimeout as sleep6 } from "node:timers/promises";
|
|
4453
4478
|
async function forkSupervisor(plan, argv) {
|
|
@@ -4506,7 +4531,7 @@ async function supervisorFailed(dir, code) {
|
|
|
4506
4531
|
const record = { at: new Date().toISOString(), code, reason: "failed" };
|
|
4507
4532
|
const wrote = await writeExit(dir, record).then(() => true, () => false);
|
|
4508
4533
|
if (wrote)
|
|
4509
|
-
await
|
|
4534
|
+
await rm3(join15(dir, ".gamecrate", "lock"), { force: true }).catch(() => {});
|
|
4510
4535
|
return code;
|
|
4511
4536
|
}
|
|
4512
4537
|
async function writeExit(instanceDir, record) {
|
|
@@ -4564,7 +4589,7 @@ async function awaitRunLog(instanceDir, lock, poll = RUN_LOG_POLL_MS, notice = W
|
|
|
4564
4589
|
});
|
|
4565
4590
|
const began = target === undefined ? undefined : runStartedAt(basename8(target));
|
|
4566
4591
|
const current = began !== undefined && (Number.isNaN(written) || began >= written);
|
|
4567
|
-
if (current &&
|
|
4592
|
+
if (current && existsSync8(currentLog(instanceDir)))
|
|
4568
4593
|
return true;
|
|
4569
4594
|
if (!isRunning(lock.pid, lock.startedAt))
|
|
4570
4595
|
return false;
|
|
@@ -4584,7 +4609,7 @@ import { basename as basename9, join as join16 } from "node:path";
|
|
|
4584
4609
|
|
|
4585
4610
|
// src/mods/worktree.ts
|
|
4586
4611
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
4587
|
-
import { existsSync as
|
|
4612
|
+
import { existsSync as existsSync9, realpathSync as realpathSync2 } from "node:fs";
|
|
4588
4613
|
import { isAbsolute as isAbsolute2, resolve as resolve5, sep } from "node:path";
|
|
4589
4614
|
function inspect(dir) {
|
|
4590
4615
|
const r = spawnSync3("git", ["-C", dir, "rev-parse", "--path-format=absolute", "--show-toplevel", "--git-dir", "--git-common-dir", "--abbrev-ref", "HEAD"], { encoding: "utf8" });
|
|
@@ -4607,7 +4632,7 @@ function canonical(p) {
|
|
|
4607
4632
|
function resolveWorktree(dir, source, order) {
|
|
4608
4633
|
const raw = expandHome(dir);
|
|
4609
4634
|
const abs = isAbsolute2(raw) ? raw : resolve5(process.cwd(), raw);
|
|
4610
|
-
if (!
|
|
4635
|
+
if (!existsSync9(abs)) {
|
|
4611
4636
|
return { where: abs, message: `--worktree path does not exist`, suggestion: "check the path, or drop the flag" };
|
|
4612
4637
|
}
|
|
4613
4638
|
const info = inspect(abs);
|
|
@@ -4702,10 +4727,10 @@ import { join as join18 } from "node:path";
|
|
|
4702
4727
|
|
|
4703
4728
|
// src/mods/modindex.ts
|
|
4704
4729
|
import { createHash as createHash3 } from "node:crypto";
|
|
4705
|
-
import { existsSync as
|
|
4706
|
-
import { mkdir as mkdir4, readdir as
|
|
4730
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5, statSync as statSync3 } from "node:fs";
|
|
4731
|
+
import { mkdir as mkdir4, readdir as readdir8, readFile as readFile10, writeFile as writeFile6 } from "node:fs/promises";
|
|
4707
4732
|
import { homedir as homedir4 } from "node:os";
|
|
4708
|
-
import { dirname as
|
|
4733
|
+
import { dirname as dirname9, join as join17, relative as relative3, sep as sep2, resolve as resolvePath } from "node:path";
|
|
4709
4734
|
import picomatch from "picomatch";
|
|
4710
4735
|
function cacheDir() {
|
|
4711
4736
|
return join17(process.env["XDG_CACHE_HOME"] ?? join17(homedir4(), ".cache"), "gamecrate");
|
|
@@ -4721,7 +4746,7 @@ function inLinkedWorktree(dir, stopAt) {
|
|
|
4721
4746
|
let current = dir;
|
|
4722
4747
|
for (;; ) {
|
|
4723
4748
|
const git = join17(current, ".git");
|
|
4724
|
-
if (
|
|
4749
|
+
if (existsSync10(git)) {
|
|
4725
4750
|
try {
|
|
4726
4751
|
if (statSync3(git).isFile())
|
|
4727
4752
|
return true;
|
|
@@ -4732,7 +4757,7 @@ function inLinkedWorktree(dir, stopAt) {
|
|
|
4732
4757
|
}
|
|
4733
4758
|
if (current === stopAt)
|
|
4734
4759
|
return false;
|
|
4735
|
-
const parent =
|
|
4760
|
+
const parent = dirname9(current);
|
|
4736
4761
|
if (parent === current)
|
|
4737
4762
|
return false;
|
|
4738
4763
|
current = parent;
|
|
@@ -4743,7 +4768,7 @@ async function scanLocalRoot(root, rootIndex, manifestFile, found) {
|
|
|
4743
4768
|
const exclude = [...root.exclude ?? [], ...rootIndex === -1 ? [] : ALWAYS_EXCLUDE];
|
|
4744
4769
|
const walk = async (dir, depth) => {
|
|
4745
4770
|
const manifest = join17(dir, manifestFile);
|
|
4746
|
-
if (
|
|
4771
|
+
if (existsSync10(manifest)) {
|
|
4747
4772
|
found.push({
|
|
4748
4773
|
dir,
|
|
4749
4774
|
kind: "local",
|
|
@@ -4756,7 +4781,7 @@ async function scanLocalRoot(root, rootIndex, manifestFile, found) {
|
|
|
4756
4781
|
return;
|
|
4757
4782
|
let entries;
|
|
4758
4783
|
try {
|
|
4759
|
-
entries = await
|
|
4784
|
+
entries = await readdir8(dir, { withFileTypes: true });
|
|
4760
4785
|
} catch {
|
|
4761
4786
|
return;
|
|
4762
4787
|
}
|
|
@@ -4769,7 +4794,7 @@ async function scanLocalRoot(root, rootIndex, manifestFile, found) {
|
|
|
4769
4794
|
await walk(child, depth + 1);
|
|
4770
4795
|
}
|
|
4771
4796
|
};
|
|
4772
|
-
if (!
|
|
4797
|
+
if (!existsSync10(base))
|
|
4773
4798
|
return;
|
|
4774
4799
|
await walk(base, 0);
|
|
4775
4800
|
}
|
|
@@ -4777,7 +4802,7 @@ async function scanWorkshopRoot(workshopRoot, rootIndex, manifestFile, found) {
|
|
|
4777
4802
|
const base = resolvePath(expandHome(workshopRoot));
|
|
4778
4803
|
let entries;
|
|
4779
4804
|
try {
|
|
4780
|
-
entries = await
|
|
4805
|
+
entries = await readdir8(base, { withFileTypes: true });
|
|
4781
4806
|
} catch {
|
|
4782
4807
|
return;
|
|
4783
4808
|
}
|
|
@@ -4785,7 +4810,7 @@ async function scanWorkshopRoot(workshopRoot, rootIndex, manifestFile, found) {
|
|
|
4785
4810
|
if (!entry.isDirectory() || !/^\d+$/.test(entry.name))
|
|
4786
4811
|
continue;
|
|
4787
4812
|
const dir = join17(base, entry.name);
|
|
4788
|
-
if (!
|
|
4813
|
+
if (!existsSync10(join17(dir, manifestFile)))
|
|
4789
4814
|
continue;
|
|
4790
4815
|
found.push({ dir, kind: "workshop", rootIndex, linkedWorktree: false, workshopId: Number(entry.name) });
|
|
4791
4816
|
}
|
|
@@ -4797,7 +4822,7 @@ async function scanGameData(game, rootIndex, found) {
|
|
|
4797
4822
|
const data = join17(resolvePath(expandHome(host)), "Data");
|
|
4798
4823
|
let entries;
|
|
4799
4824
|
try {
|
|
4800
|
-
entries = await
|
|
4825
|
+
entries = await readdir8(data, { withFileTypes: true });
|
|
4801
4826
|
} catch {
|
|
4802
4827
|
return;
|
|
4803
4828
|
}
|
|
@@ -4805,14 +4830,14 @@ async function scanGameData(game, rootIndex, found) {
|
|
|
4805
4830
|
if (!entry.isDirectory())
|
|
4806
4831
|
continue;
|
|
4807
4832
|
const dir = join17(data, entry.name);
|
|
4808
|
-
if (!
|
|
4833
|
+
if (!existsSync10(join17(dir, game.manifest.file)))
|
|
4809
4834
|
continue;
|
|
4810
4835
|
found.push({ dir, kind: "official", rootIndex, linkedWorktree: false });
|
|
4811
4836
|
}
|
|
4812
4837
|
}
|
|
4813
4838
|
var CACHE_VERSION = 4;
|
|
4814
4839
|
function acfPath2(root, steamAppId) {
|
|
4815
|
-
return join17(
|
|
4840
|
+
return join17(dirname9(dirname9(resolvePath(expandHome(root)))), `appworkshop_${steamAppId}.acf`);
|
|
4816
4841
|
}
|
|
4817
4842
|
function contentPairs(acf) {
|
|
4818
4843
|
let text;
|
|
@@ -4836,7 +4861,7 @@ function workshopStamp(game, dataRoot) {
|
|
|
4836
4861
|
return null;
|
|
4837
4862
|
pairs.push(...found);
|
|
4838
4863
|
}
|
|
4839
|
-
const download = createHash3("
|
|
4864
|
+
const download = createHash3("sha256").update(pairs.toSorted().join(`
|
|
4840
4865
|
`)).digest("hex");
|
|
4841
4866
|
if (game.workshopRoot === null)
|
|
4842
4867
|
return download;
|
|
@@ -4950,7 +4975,7 @@ async function applySourceOverrides(index, overrides, config) {
|
|
|
4950
4975
|
const wanted = spec.slice(0, eq);
|
|
4951
4976
|
const dir = resolvePath(expandHome(spec.slice(eq + 1)));
|
|
4952
4977
|
const file = join17(dir, config.manifest.file);
|
|
4953
|
-
if (!
|
|
4978
|
+
if (!existsSync10(file)) {
|
|
4954
4979
|
problems.push({
|
|
4955
4980
|
where: spec,
|
|
4956
4981
|
message: `no ${config.manifest.file} under ${dir}`,
|
|
@@ -5155,7 +5180,7 @@ function byPath(index, raw, game) {
|
|
|
5155
5180
|
return hit;
|
|
5156
5181
|
}
|
|
5157
5182
|
const file = join17(dir, game.manifest.file);
|
|
5158
|
-
if (!
|
|
5183
|
+
if (!existsSync10(file))
|
|
5159
5184
|
return null;
|
|
5160
5185
|
try {
|
|
5161
5186
|
const manifest = index.plugin.parseManifest(readFileSync5(file, "utf8"));
|
|
@@ -5415,7 +5440,7 @@ async function resolvePlan(options) {
|
|
|
5415
5440
|
staged.push({ record, explicit: true });
|
|
5416
5441
|
}
|
|
5417
5442
|
}
|
|
5418
|
-
if (profile.autoDependencies
|
|
5443
|
+
if (profile.autoDependencies !== false)
|
|
5419
5444
|
insertDependencies(staged, present, index, game, problems);
|
|
5420
5445
|
if (unfetched.size > 0) {
|
|
5421
5446
|
for (const problem of problems) {
|
|
@@ -5493,7 +5518,7 @@ async function resolvePlan(options) {
|
|
|
5493
5518
|
}
|
|
5494
5519
|
|
|
5495
5520
|
// src/run/registry.ts
|
|
5496
|
-
import { readdir as
|
|
5521
|
+
import { readdir as readdir9 } from "node:fs/promises";
|
|
5497
5522
|
import { join as join19 } from "node:path";
|
|
5498
5523
|
var FORMAT = '{{.Names}}\t{{.Label "gamecrate.game"}}\t{{.Label "gamecrate.profile"}}\t{{.Label "gamecrate.instance"}}\t{{.Status}}';
|
|
5499
5524
|
function parseDockerRuns(stdout) {
|
|
@@ -5532,7 +5557,7 @@ async function walkLocks(dataRoot) {
|
|
|
5532
5557
|
return out;
|
|
5533
5558
|
}
|
|
5534
5559
|
async function entries(dir) {
|
|
5535
|
-
const found = await
|
|
5560
|
+
const found = await readdir9(dir, { withFileTypes: true }).catch(() => []);
|
|
5536
5561
|
return found.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
5537
5562
|
}
|
|
5538
5563
|
async function push(out, path) {
|
|
@@ -5582,10 +5607,10 @@ async function listRuns(dataRoot, docker = dockerPs) {
|
|
|
5582
5607
|
}
|
|
5583
5608
|
|
|
5584
5609
|
// src/launch/stage.ts
|
|
5585
|
-
import { lstat, mkdir as mkdir5, readdir as
|
|
5610
|
+
import { lstat, mkdir as mkdir5, readdir as readdir10, realpath as realpath2, rm as rm4, stat as stat5 } from "node:fs/promises";
|
|
5586
5611
|
import { basename as basename10, join as join20 } from "node:path";
|
|
5587
5612
|
async function stageMods(plan) {
|
|
5588
|
-
await
|
|
5613
|
+
await rm4(plan.stageDirHost, { recursive: true, force: true });
|
|
5589
5614
|
await mkdir5(plan.stageDirHost, { recursive: true });
|
|
5590
5615
|
const mounts = [];
|
|
5591
5616
|
for (const mod of plan.mods) {
|
|
@@ -5646,7 +5671,7 @@ async function detectForeignOwnership(dir, uid, limit = 100) {
|
|
|
5646
5671
|
if (!info.isDirectory())
|
|
5647
5672
|
continue;
|
|
5648
5673
|
try {
|
|
5649
|
-
for (const entry of await
|
|
5674
|
+
for (const entry of await readdir10(current))
|
|
5650
5675
|
queue.push(join20(current, entry));
|
|
5651
5676
|
} catch {
|
|
5652
5677
|
continue;
|
|
@@ -5656,7 +5681,7 @@ async function detectForeignOwnership(dir, uid, limit = 100) {
|
|
|
5656
5681
|
}
|
|
5657
5682
|
|
|
5658
5683
|
// src/mods/workshop.ts
|
|
5659
|
-
import { existsSync as
|
|
5684
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
5660
5685
|
import { readFile as readFile11 } from "node:fs/promises";
|
|
5661
5686
|
import { join as join21, resolve as resolvePath2 } from "node:path";
|
|
5662
5687
|
var ROUNDS = 5;
|
|
@@ -5671,26 +5696,9 @@ async function prepareWorkshop(game, profileName, args, config, allowFetch, plug
|
|
|
5671
5696
|
for (let round = 0;round < ROUNDS && frontier.length > 0; round++) {
|
|
5672
5697
|
for (const id of frontier)
|
|
5673
5698
|
ids.add(id);
|
|
5674
|
-
if (allowFetch)
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
warnings.push(...drift.warnings);
|
|
5678
|
-
const downloaded = await downloadItems(config, game, config.dataRoot, drift.needed);
|
|
5679
|
-
warnings.push(...downloaded.warnings);
|
|
5680
|
-
} catch (error) {
|
|
5681
|
-
warnings.push(`could not download workshop items: ${error instanceof Error ? error.message : String(error)}`);
|
|
5682
|
-
}
|
|
5683
|
-
}
|
|
5684
|
-
const next = new Set;
|
|
5685
|
-
for (const id of frontier) {
|
|
5686
|
-
const manifest = await manifestOf(roots, id, game, plugin, problems);
|
|
5687
|
-
for (const dep of manifest?.modDependencies ?? []) {
|
|
5688
|
-
const depId = workshopUrlId(dep.steamWorkshopUrl);
|
|
5689
|
-
if (depId !== undefined && !ids.has(depId))
|
|
5690
|
-
next.add(depId);
|
|
5691
|
-
}
|
|
5692
|
-
}
|
|
5693
|
-
frontier = [...next];
|
|
5699
|
+
if (allowFetch)
|
|
5700
|
+
warnings.push(...await fetchRound(config, game, roots, frontier));
|
|
5701
|
+
frontier = await dependenciesOf(frontier, ids, roots, game, plugin, problems);
|
|
5694
5702
|
}
|
|
5695
5703
|
if (frontier.length > 0) {
|
|
5696
5704
|
problems.push({
|
|
@@ -5701,11 +5709,32 @@ async function prepareWorkshop(game, profileName, args, config, allowFetch, plug
|
|
|
5701
5709
|
}
|
|
5702
5710
|
return { ids, warnings, problems, unfetched: [...ids].filter((id) => itemDir(roots, id) === undefined) };
|
|
5703
5711
|
}
|
|
5712
|
+
async function fetchRound(config, game, roots, frontier) {
|
|
5713
|
+
try {
|
|
5714
|
+
const drift = await checkDrift(frontier, roots);
|
|
5715
|
+
const downloaded = await downloadItems(config, game, config.dataRoot, drift.needed);
|
|
5716
|
+
return [...drift.warnings, ...downloaded.warnings];
|
|
5717
|
+
} catch (error) {
|
|
5718
|
+
return [`could not download workshop items: ${error instanceof Error ? error.message : String(error)}`];
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
async function dependenciesOf(frontier, seen, roots, game, plugin, problems) {
|
|
5722
|
+
const next = new Set;
|
|
5723
|
+
for (const id of frontier) {
|
|
5724
|
+
const manifest = await manifestOf(roots, id, game, plugin, problems);
|
|
5725
|
+
for (const dep of manifest?.modDependencies ?? []) {
|
|
5726
|
+
const depId = workshopUrlId(dep.steamWorkshopUrl);
|
|
5727
|
+
if (depId !== undefined && !seen.has(depId))
|
|
5728
|
+
next.add(depId);
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
return [...next];
|
|
5732
|
+
}
|
|
5704
5733
|
function mountedRoots(dataRoot, game) {
|
|
5705
5734
|
return [downloadRoot(dataRoot, game), ...game.workshopRoot === null ? [] : [game.workshopRoot]];
|
|
5706
5735
|
}
|
|
5707
5736
|
function itemDir(roots, id) {
|
|
5708
|
-
return roots.map((root) => join21(root, id)).find((dir) =>
|
|
5737
|
+
return roots.map((root) => join21(root, id)).find((dir) => existsSync11(dir));
|
|
5709
5738
|
}
|
|
5710
5739
|
async function manifestOf(roots, id, game, plugin, problems) {
|
|
5711
5740
|
const dir = itemDir(roots, id);
|
|
@@ -5713,7 +5742,7 @@ async function manifestOf(roots, id, game, plugin, problems) {
|
|
|
5713
5742
|
}
|
|
5714
5743
|
async function manifestAt(dir, game, plugin, problems) {
|
|
5715
5744
|
const file = join21(dir, game.manifest.file);
|
|
5716
|
-
if (!
|
|
5745
|
+
if (!existsSync11(file))
|
|
5717
5746
|
return null;
|
|
5718
5747
|
try {
|
|
5719
5748
|
return plugin.parseManifest(await readFile11(file, "utf8"));
|
|
@@ -5737,26 +5766,26 @@ async function localSeeds(game, profile, args, sources, plugin, problems) {
|
|
|
5737
5766
|
function localDirs(game, profile, args, sources) {
|
|
5738
5767
|
const out = new Set;
|
|
5739
5768
|
for (const entry of reachedEntries(game, profile, args)) {
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
if (object.path !== undefined) {
|
|
5744
|
-
out.add(resolvePath2(expandHome(object.path)));
|
|
5745
|
-
continue;
|
|
5746
|
-
}
|
|
5747
|
-
if (object.id.includes(":"))
|
|
5748
|
-
continue;
|
|
5749
|
-
const pin = libraryPin(game, object.id);
|
|
5750
|
-
const clone = sources.get(object.id.toLowerCase());
|
|
5751
|
-
if (clone !== undefined) {
|
|
5752
|
-
out.add(pin?.subdir === undefined ? clone : join21(clone, pin.subdir));
|
|
5753
|
-
continue;
|
|
5754
|
-
}
|
|
5755
|
-
if (pin?.path !== undefined)
|
|
5756
|
-
out.add(resolvePath2(expandHome(pin.path)));
|
|
5769
|
+
const dir = localDirOf(entry, game, sources);
|
|
5770
|
+
if (dir !== undefined)
|
|
5771
|
+
out.add(dir);
|
|
5757
5772
|
}
|
|
5758
5773
|
return [...out];
|
|
5759
5774
|
}
|
|
5775
|
+
function localDirOf(entry, game, sources) {
|
|
5776
|
+
if (typeof entry !== "string" && "match" in entry)
|
|
5777
|
+
return;
|
|
5778
|
+
const object = typeof entry === "string" ? { id: entry } : entry;
|
|
5779
|
+
if (object.path !== undefined)
|
|
5780
|
+
return resolvePath2(expandHome(object.path));
|
|
5781
|
+
if (object.id.includes(":"))
|
|
5782
|
+
return;
|
|
5783
|
+
const pin = libraryPin(game, object.id);
|
|
5784
|
+
const clone = sources.get(object.id.toLowerCase());
|
|
5785
|
+
if (clone !== undefined)
|
|
5786
|
+
return pin?.subdir === undefined ? clone : join21(clone, pin.subdir);
|
|
5787
|
+
return pin?.path === undefined ? undefined : resolvePath2(expandHome(pin.path));
|
|
5788
|
+
}
|
|
5760
5789
|
function wantedIds(game, profile, args) {
|
|
5761
5790
|
const out = new Set;
|
|
5762
5791
|
for (const entry of reachedEntries(game, profile, args)) {
|
|
@@ -5788,7 +5817,7 @@ function published2(value) {
|
|
|
5788
5817
|
}
|
|
5789
5818
|
|
|
5790
5819
|
// src/index.ts
|
|
5791
|
-
var VERSION = "1.
|
|
5820
|
+
var VERSION = "1.4.0";
|
|
5792
5821
|
async function main(argv) {
|
|
5793
5822
|
const supervised = supervisedDir(argv);
|
|
5794
5823
|
try {
|
|
@@ -6122,7 +6151,7 @@ async function copyOutLogs(plan) {
|
|
|
6122
6151
|
if (spec.mode !== "copy-out")
|
|
6123
6152
|
return;
|
|
6124
6153
|
const source = join22(plan.dataDirHost, spec.from);
|
|
6125
|
-
if (!
|
|
6154
|
+
if (!existsSync12(source))
|
|
6126
6155
|
return;
|
|
6127
6156
|
const target = join22(plan.runDirHost, basename11(spec.from.replace(/\/+$/, "")));
|
|
6128
6157
|
try {
|
|
@@ -6190,7 +6219,7 @@ async function doctor(config, plugins) {
|
|
|
6190
6219
|
});
|
|
6191
6220
|
}
|
|
6192
6221
|
const root = downloadRoot(config.dataRoot, gameConfig);
|
|
6193
|
-
status(`${game}: workshop downloads ${root}${
|
|
6222
|
+
status(`${game}: workshop downloads ${root}${existsSync12(root) ? "" : " (not created yet)"}`);
|
|
6194
6223
|
}
|
|
6195
6224
|
const all = [...problems, ...await preflight(plan), ...steamcmd];
|
|
6196
6225
|
if (all.length === 0) {
|
|
@@ -6217,12 +6246,12 @@ async function logs(args, config, defaults) {
|
|
|
6217
6246
|
if (args.follow)
|
|
6218
6247
|
return await follow(dir, false, `${game} ${profile}`);
|
|
6219
6248
|
const runs = join22(dir, "logs", "runs");
|
|
6220
|
-
const latest = (await
|
|
6249
|
+
const latest = (await readdir11(runs, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort().at(-1);
|
|
6221
6250
|
if (latest === undefined) {
|
|
6222
6251
|
throw new GamecrateError(`no runs recorded for ${game} ${profile}`, Exit.Usage, runs);
|
|
6223
6252
|
}
|
|
6224
6253
|
const runDir = join22(runs, latest);
|
|
6225
|
-
const files = (await
|
|
6254
|
+
const files = (await readdir11(runDir, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
|
|
6226
6255
|
if (args.json) {
|
|
6227
6256
|
process.stdout.write(`${JSON.stringify({ run: latest, dir: runDir, files }, null, 2)}
|
|
6228
6257
|
`);
|
|
@@ -6357,18 +6386,24 @@ async function clean(args, config, defaults) {
|
|
|
6357
6386
|
const dir = profileDataDir(config, game, profile);
|
|
6358
6387
|
const tier = args.cleanTier ?? "staging";
|
|
6359
6388
|
const saveSuffixes = config.games[game].saveExtensions.map((ext) => `.${ext.replace(/^\./, "")}`.toLowerCase());
|
|
6389
|
+
const downloads = downloadRoot(config.dataRoot, config.games[game]);
|
|
6390
|
+
if (tier === "downloads") {
|
|
6391
|
+
status(await removeDownloads(downloads, config.games[game].steamAppId));
|
|
6392
|
+
return Exit.Ok;
|
|
6393
|
+
}
|
|
6360
6394
|
if (tier !== "all") {
|
|
6361
6395
|
const target = join22(instanceDir(args, config, game, profile), tier === "logs" ? "logs" : ".stage");
|
|
6362
|
-
await
|
|
6396
|
+
await rm5(target, { recursive: true, force: true });
|
|
6363
6397
|
status(`removed ${target}`);
|
|
6364
6398
|
return Exit.Ok;
|
|
6365
6399
|
}
|
|
6366
6400
|
const saves = await countSaves(dir, saveSuffixes);
|
|
6367
6401
|
if (!args.yes) {
|
|
6368
|
-
throw new GamecrateError(`clean --all would delete ${dir}, including ${saves} save file(s)`, Exit.Usage, "add --yes to confirm");
|
|
6402
|
+
throw new GamecrateError(`clean --all would delete ${dir}, including ${saves} save file(s), and ${game}'s workshop downloads`, Exit.Usage, "add --yes to confirm, or --downloads to drop only the downloads");
|
|
6369
6403
|
}
|
|
6370
|
-
await
|
|
6404
|
+
await rm5(dir, { recursive: true, force: true });
|
|
6371
6405
|
status(`removed ${dir} (${saves} save file(s))`);
|
|
6406
|
+
status(await removeDownloads(downloads, config.games[game].steamAppId));
|
|
6372
6407
|
return Exit.Ok;
|
|
6373
6408
|
}
|
|
6374
6409
|
async function countSaves(dir, suffixes) {
|
|
@@ -6378,7 +6413,7 @@ async function countSaves(dir, suffixes) {
|
|
|
6378
6413
|
const current = queue.shift();
|
|
6379
6414
|
let entries;
|
|
6380
6415
|
try {
|
|
6381
|
-
entries = await
|
|
6416
|
+
entries = await readdir11(current, { withFileTypes: true });
|
|
6382
6417
|
} catch {
|
|
6383
6418
|
continue;
|
|
6384
6419
|
}
|
|
@@ -6399,9 +6434,9 @@ async function clone(args, config) {
|
|
|
6399
6434
|
}
|
|
6400
6435
|
const from = join22(profileDataDir(config, game, src), "game");
|
|
6401
6436
|
const to = join22(profileDataDir(config, game, dst), "game");
|
|
6402
|
-
if (!
|
|
6437
|
+
if (!existsSync12(from))
|
|
6403
6438
|
throw new GamecrateError(`${from} does not exist`, Exit.Usage);
|
|
6404
|
-
if (
|
|
6439
|
+
if (existsSync12(to) && !args.yes) {
|
|
6405
6440
|
throw new GamecrateError(`${to} already exists`, Exit.Usage, "add --yes to overwrite");
|
|
6406
6441
|
}
|
|
6407
6442
|
await mkdir6(to, { recursive: true });
|
|
@@ -6467,7 +6502,7 @@ async function follow(dir, fromStart, what) {
|
|
|
6467
6502
|
const held = lock !== undefined && isRunning(lock.pid, lock.startedAt);
|
|
6468
6503
|
const live = held && await awaitRunLog(dir, lock);
|
|
6469
6504
|
const file = currentLog(dir);
|
|
6470
|
-
if (!
|
|
6505
|
+
if (!existsSync12(file)) {
|
|
6471
6506
|
throw new GamecrateError(`no captured output for ${what}`, Exit.Usage, file);
|
|
6472
6507
|
}
|
|
6473
6508
|
return await spawnStatus(tailArgv(file, fromStart, live ? lock.pid : undefined), true);
|
|
@@ -6501,8 +6536,8 @@ async function configEdit(args) {
|
|
|
6501
6536
|
throw new GamecrateError("config takes one word: edit", Exit.Usage);
|
|
6502
6537
|
const existing = await findGlobalConfig();
|
|
6503
6538
|
const path = existing ?? join22(globalConfigDir(), "profiles.yml");
|
|
6504
|
-
await mkdir6(
|
|
6505
|
-
if (!
|
|
6539
|
+
await mkdir6(dirname10(path), { recursive: true });
|
|
6540
|
+
if (!existsSync12(path)) {
|
|
6506
6541
|
await writeFile7(path, `# gamecrate config. see https://github.com/RimWorks/gamecrate
|
|
6507
6542
|
` + `plugins: []
|
|
6508
6543
|
` + `games: {}
|
|
@@ -6522,7 +6557,7 @@ async function fixPerms(args, config) {
|
|
|
6522
6557
|
const identity = resolveIdentity(false);
|
|
6523
6558
|
const found = [];
|
|
6524
6559
|
for (const dir of await profileDirs(config, game, args.profile)) {
|
|
6525
|
-
if (!
|
|
6560
|
+
if (!existsSync12(dir))
|
|
6526
6561
|
continue;
|
|
6527
6562
|
found.push(...await detectForeignOwnership(dir, identity.uid, 1e4));
|
|
6528
6563
|
}
|
|
@@ -6565,7 +6600,7 @@ async function removeIfEmptyDir(path) {
|
|
|
6565
6600
|
const info = await stat6(path).catch(() => null);
|
|
6566
6601
|
if (info === null || !info.isDirectory())
|
|
6567
6602
|
return false;
|
|
6568
|
-
const entries = await
|
|
6603
|
+
const entries = await readdir11(path).catch(() => null);
|
|
6569
6604
|
if (entries === null || entries.length > 0)
|
|
6570
6605
|
return false;
|
|
6571
6606
|
return rmdir(path).then(() => true, () => false);
|
|
@@ -22,6 +22,11 @@ export declare function steamHome(dataRoot: string): string;
|
|
|
22
22
|
* and a plain Valve tarball writes $HOME/Steam. Pinning makes all three land here.
|
|
23
23
|
*/
|
|
24
24
|
export declare function downloadRoot(dataRoot: string, game: GameConfig): string;
|
|
25
|
+
/**
|
|
26
|
+
* Drops a game's downloaded items and the .acf beside them. Never the steamcmd install above
|
|
27
|
+
* those: that is 200MB of bootstrap which would re-download for nothing. Returns what it did.
|
|
28
|
+
*/
|
|
29
|
+
export declare function removeDownloads(root: string, steamAppId: number): Promise<string>;
|
|
25
30
|
/**
|
|
26
31
|
* Configured path if set, else PATH, else the docker image. A configured path that is not an
|
|
27
32
|
* executable file is an error, not a fallback. Docker gets `--user`: without it the tree
|
package/dist/types/types.d.ts
CHANGED
|
@@ -145,6 +145,7 @@ export interface ProfileConfig {
|
|
|
145
145
|
extends?: string;
|
|
146
146
|
exclude?: string[];
|
|
147
147
|
includeBase?: boolean;
|
|
148
|
+
/** Default true. False keeps the mod list literal, dependencies and all. */
|
|
148
149
|
autoDependencies?: boolean;
|
|
149
150
|
settings?: Partial<Settings>;
|
|
150
151
|
instances?: Record<string, InstanceConfig>;
|
|
@@ -389,7 +390,7 @@ export interface ParsedArgs {
|
|
|
389
390
|
build?: BuildPolicy;
|
|
390
391
|
sort?: 'topo' | 'none';
|
|
391
392
|
/** `clean` only: --staging is the default, --all additionally requires --yes. */
|
|
392
|
-
cleanTier?: 'staging' | 'logs' | 'all';
|
|
393
|
+
cleanTier?: 'staging' | 'logs' | 'all' | 'downloads';
|
|
393
394
|
dockerArgs: string[];
|
|
394
395
|
gameArgs: string[];
|
|
395
396
|
dryRun: boolean;
|