@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 CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync11 } from "node:fs";
5
- import { chown, cp, mkdir as mkdir6, readdir as readdir10, readFile as readFile12, rm as rm4, rmdir, stat as stat6, writeFile as writeFile7 } from "node:fs/promises";
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 dirname9, join as join22 } from "node:path";
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, saves included (needs --yes)").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");
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 existsSync5 } from "node:fs";
800
- import { mkdir as mkdir2, readFile as readFile7, readdir as readdir5, writeFile as writeFile3 } from "node:fs/promises";
801
- import { dirname as dirname6, join as join12, relative as relative2, resolve as resolve4 } from "node:path";
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 { delimiter, join as join10 } from "node:path";
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 dirname5, join as join11 } from "node:path";
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 body(text, start, depth) {
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
- for (;; ) {
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 depth === 0 ? null : { node, next: i + 1 };
3356
+ return { node, next: i + 1 };
3338
3357
  if (text[i] !== '"')
3339
3358
  return null;
3340
- const key = quoted(text, i);
3341
- if (key === null)
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
- const value = quoted(text, i);
3357
- if (value === null)
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] = value.value;
3360
- i = value.next;
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 body(text, 0, 0)?.node ?? {};
3389
+ return root2(text) ?? {};
3365
3390
  }
3366
- function section(root, name) {
3367
- const direct = root[name];
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(root)) {
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(dirname5(dirname5(root)), `appworkshop_${basename5(root)}.acf`);
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 (!existsSync5(target.file)) {
3557
- await mkdir2(dirname6(target.file), { recursive: true });
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 readdir5(dir, { withFileTypes: true }).catch(() => []);
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 existsSync6, readFileSync as readFileSync3 } from "node:fs";
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 (!existsSync6(CDI_SPEC)) {
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 (!existsSync6(files.host)) {
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 (!existsSync6(executable)) {
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 (existsSync6(mount.source))
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 readdir6, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
4335
- import { dirname as dirname7, join as join14 } from "node:path";
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 readdir6(dataDir, { withFileTypes: true });
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(dirname7(target), { recursive: true });
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(dirname7(target), { recursive: true });
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 existsSync7 } from "node:fs";
4450
- import { readFile as readFile9, readlink, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
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 rm2(join15(dir, ".gamecrate", "lock"), { force: true }).catch(() => {});
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 && existsSync7(currentLog(instanceDir)))
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 existsSync8, realpathSync as realpathSync2 } from "node:fs";
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 (!existsSync8(abs)) {
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 existsSync9, readFileSync as readFileSync5, statSync as statSync3 } from "node:fs";
4706
- import { mkdir as mkdir4, readdir as readdir7, readFile as readFile10, writeFile as writeFile6 } from "node:fs/promises";
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 dirname8, join as join17, relative as relative3, sep as sep2, resolve as resolvePath } from "node:path";
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 (existsSync9(git)) {
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 = dirname8(current);
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 (existsSync9(manifest)) {
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 readdir7(dir, { withFileTypes: true });
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 (!existsSync9(base))
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 readdir7(base, { withFileTypes: true });
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 (!existsSync9(join17(dir, manifestFile)))
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 readdir7(data, { withFileTypes: true });
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 (!existsSync9(join17(dir, game.manifest.file)))
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(dirname8(dirname8(resolvePath(expandHome(root)))), `appworkshop_${steamAppId}.acf`);
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("sha1").update(pairs.sort().join(`
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 (!existsSync9(file)) {
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 (!existsSync9(file))
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 === true)
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 readdir8 } from "node:fs/promises";
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 readdir8(dir, { withFileTypes: true }).catch(() => []);
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 readdir9, realpath as realpath2, rm as rm3, stat as stat5 } from "node:fs/promises";
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 rm3(plan.stageDirHost, { recursive: true, force: true });
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 readdir9(current))
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 existsSync10 } from "node:fs";
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
- try {
5676
- const drift = await checkDrift(frontier, roots);
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) => existsSync10(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 (!existsSync10(file))
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
- if (typeof entry !== "string" && "match" in entry)
5741
- continue;
5742
- const object = typeof entry === "string" ? { id: entry } : entry;
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.3.0";
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 (!existsSync11(source))
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}${existsSync11(root) ? "" : " (not created yet)"}`);
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 readdir10(runs, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort().at(-1);
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 readdir10(runDir, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
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 rm4(target, { recursive: true, force: true });
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 rm4(dir, { recursive: true, force: true });
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 readdir10(current, { withFileTypes: true });
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 (!existsSync11(from))
6437
+ if (!existsSync12(from))
6403
6438
  throw new GamecrateError(`${from} does not exist`, Exit.Usage);
6404
- if (existsSync11(to) && !args.yes) {
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 (!existsSync11(file)) {
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(dirname9(path), { recursive: true });
6505
- if (!existsSync11(path)) {
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 (!existsSync11(dir))
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 readdir10(path).catch(() => null);
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
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamecrate/cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.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",