@gamecrate/cli 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/gamecrate.js +246 -246
  2. package/dist/lib.js +296 -0
  3. package/dist/types/cli/args.d.ts +40 -0
  4. package/dist/types/cli/help.d.ts +7 -0
  5. package/dist/types/cli/output.d.ts +56 -0
  6. package/dist/types/config/builtin.d.ts +3 -0
  7. package/dist/types/config/jsonc.d.ts +5 -0
  8. package/dist/types/config/load.d.ts +45 -0
  9. package/dist/types/config/validate.d.ts +12 -0
  10. package/dist/types/docker/identity.d.ts +6 -0
  11. package/dist/types/docker/preflight.d.ts +3 -0
  12. package/dist/types/docker/run.d.ts +37 -0
  13. package/dist/types/docker/spec.d.ts +24 -0
  14. package/dist/types/docker/window.d.ts +21 -0
  15. package/dist/types/index.d.ts +2 -0
  16. package/dist/types/launch/generate.d.ts +8 -0
  17. package/dist/types/launch/instance.d.ts +20 -0
  18. package/dist/types/launch/prepare.d.ts +44 -0
  19. package/dist/types/launch/resolve.d.ts +17 -0
  20. package/dist/types/launch/stage.d.ts +13 -0
  21. package/dist/types/lib.d.ts +3 -0
  22. package/dist/types/mods/modindex.d.ts +29 -0
  23. package/dist/types/mods/staleness.d.ts +28 -0
  24. package/dist/types/mods/worktree.d.ts +18 -0
  25. package/dist/types/plugin.d.ts +35 -0
  26. package/dist/types/types.d.ts +394 -0
  27. package/package.json +15 -10
  28. package/src/cli/args.ts +0 -592
  29. package/src/cli/help.ts +0 -193
  30. package/src/cli/output.ts +0 -246
  31. package/src/config/builtin.ts +0 -19
  32. package/src/config/jsonc.ts +0 -21
  33. package/src/config/load.ts +0 -387
  34. package/src/config/validate.ts +0 -0
  35. package/src/docker/identity.ts +0 -25
  36. package/src/docker/preflight.ts +0 -243
  37. package/src/docker/run.ts +0 -212
  38. package/src/docker/spec.ts +0 -357
  39. package/src/docker/window.ts +0 -152
  40. package/src/index.ts +0 -875
  41. package/src/launch/generate.ts +0 -151
  42. package/src/launch/instance.ts +0 -106
  43. package/src/launch/prepare.ts +0 -332
  44. package/src/launch/resolve.ts +0 -383
  45. package/src/launch/stage.ts +0 -97
  46. package/src/lib.ts +0 -22
  47. package/src/mods/modindex.ts +0 -539
  48. package/src/mods/staleness.ts +0 -125
  49. package/src/mods/worktree.ts +0 -107
  50. package/src/plugin.ts +0 -152
  51. package/src/types.ts +0 -423
package/dist/gamecrate.js CHANGED
@@ -1060,14 +1060,14 @@ function staleWarning(packageId, report, now = Date.now()) {
1060
1060
  `);
1061
1061
  }
1062
1062
  function duration(ms) {
1063
- const seconds2 = Math.max(0, Math.round(ms / 1000));
1064
- if (seconds2 < 60)
1065
- return `${seconds2}s`;
1066
- if (seconds2 < 3600)
1067
- return `${Math.floor(seconds2 / 60)}m`;
1068
- if (seconds2 < 86400)
1069
- return `${Math.floor(seconds2 / 3600)}h`;
1070
- return `${Math.floor(seconds2 / 86400)}d`;
1063
+ const seconds = Math.max(0, Math.round(ms / 1000));
1064
+ if (seconds < 60)
1065
+ return `${seconds}s`;
1066
+ if (seconds < 3600)
1067
+ return `${Math.floor(seconds / 60)}m`;
1068
+ if (seconds < 86400)
1069
+ return `${Math.floor(seconds / 3600)}h`;
1070
+ return `${Math.floor(seconds / 86400)}d`;
1071
1071
  }
1072
1072
  function ago(mtimeMs, now = Date.now()) {
1073
1073
  return `${duration(now - mtimeMs)} ago`;
@@ -1575,15 +1575,15 @@ function collect2(problems, prefix, schema, value) {
1575
1575
  for (const issue of result.error.issues) {
1576
1576
  if (issue.code === "unrecognized_keys") {
1577
1577
  const hints = hintsFor(issue.message);
1578
- issue.keys.forEach((key2, i) => {
1579
- const problem2 = {
1580
- where: `${prefix}${pointer(issue.path)}/${esc(key2)}`,
1581
- message: `unknown key "${key2}"`
1578
+ issue.keys.forEach((key, i) => {
1579
+ const problem = {
1580
+ where: `${prefix}${pointer(issue.path)}/${esc(key)}`,
1581
+ message: `unknown key "${key}"`
1582
1582
  };
1583
- const hint2 = hints[i];
1584
- if (hint2 != null)
1585
- problem2.suggestion = `did you mean "${hint2}"?`;
1586
- problems.push(problem2);
1583
+ const hint = hints[i];
1584
+ if (hint != null)
1585
+ problem.suggestion = `did you mean "${hint}"?`;
1586
+ problems.push(problem);
1587
1587
  });
1588
1588
  continue;
1589
1589
  }
@@ -1840,31 +1840,31 @@ function valueAt2(value, segments) {
1840
1840
  }
1841
1841
  return current;
1842
1842
  }
1843
- function resolveSettings(root2, game2, profile2, ...overrides) {
1843
+ function resolveSettings(root, game, profile, ...overrides) {
1844
1844
  let out = structuredClone(DEFAULT_SETTINGS);
1845
- for (const layer of [root2.defaults?.settings, game2.settings, profile2.settings, ...overrides]) {
1845
+ for (const layer of [root.defaults?.settings, game.settings, profile.settings, ...overrides]) {
1846
1846
  if (layer)
1847
1847
  out = deepMerge(out, layer, true);
1848
1848
  }
1849
1849
  return out;
1850
1850
  }
1851
- function resolveProfile(game2, name) {
1852
- return resolveNamed(game2, name, []);
1851
+ function resolveProfile(game, name) {
1852
+ return resolveNamed(game, name, []);
1853
1853
  }
1854
- function resolveNamed(game2, name, seen) {
1854
+ function resolveNamed(game, name, seen) {
1855
1855
  if (name.toLowerCase() === "modless")
1856
1856
  return { mods: [], exclude: [], includeBase: false };
1857
- const key = profileKey(game2, name);
1857
+ const key = profileKey(game, name);
1858
1858
  if (key === undefined) {
1859
- throw new GamecrateError(`unknown profile "${name}"`, Exit.Resolution, `known profiles: ${Object.keys(game2.profiles).join(", ") || "(none)"}, modless`);
1859
+ throw new GamecrateError(`unknown profile "${name}"`, Exit.Resolution, `known profiles: ${Object.keys(game.profiles).join(", ") || "(none)"}, modless`);
1860
1860
  }
1861
1861
  if (seen.includes(key)) {
1862
1862
  throw new GamecrateError(`profile "${key}" inherits from itself`, Exit.Config, [...seen, key].join(" -> "));
1863
1863
  }
1864
- const self = own(game2.profiles, key);
1864
+ const self = own(game.profiles, key);
1865
1865
  if (self.alias !== undefined)
1866
- return resolveNamed(game2, self.alias, [...seen, key]);
1867
- const parent = self.extends !== undefined ? resolveNamed(game2, self.extends, [...seen, key]) : {};
1866
+ return resolveNamed(game, self.alias, [...seen, key]);
1867
+ const parent = self.extends !== undefined ? resolveNamed(game, self.extends, [...seen, key]) : {};
1868
1868
  const exclude = [...parent.exclude ?? [], ...self.exclude ?? []];
1869
1869
  const out = {
1870
1870
  mods: subtract([...parent.mods ?? [], ...self.mods ?? []], exclude),
@@ -1882,39 +1882,39 @@ function resolveNamed(game2, name, seen) {
1882
1882
  out.autoDependencies = auto;
1883
1883
  return out;
1884
1884
  }
1885
- function canonicalProfile(game2, name) {
1885
+ function canonicalProfile(game, name) {
1886
1886
  if (name.toLowerCase() === "modless")
1887
1887
  return "modless";
1888
1888
  const seen = [];
1889
1889
  let current = name;
1890
1890
  for (;; ) {
1891
- const key = profileKey(game2, current);
1891
+ const key = profileKey(game, current);
1892
1892
  if (key === undefined || seen.includes(key))
1893
1893
  return key ?? current;
1894
- const next = own(game2.profiles, key)?.alias;
1894
+ const next = own(game.profiles, key)?.alias;
1895
1895
  if (next === undefined)
1896
1896
  return key;
1897
1897
  seen.push(key);
1898
1898
  current = next;
1899
1899
  }
1900
1900
  }
1901
- function profileDataDir(root2, game2, profile2) {
1902
- return resolve3(expandHome(root2.dataRoot), game2, canonicalProfile(own(root2.games, game2), profile2));
1901
+ function profileDataDir(root, game, profile) {
1902
+ return resolve3(expandHome(root.dataRoot), game, canonicalProfile(own(root.games, game), profile));
1903
1903
  }
1904
- async function profileDirs(root2, game2, profile2) {
1905
- if (profile2 !== undefined)
1906
- return [profileDataDir(root2, game2, profile2)];
1907
- const dir = join5(expandHome(root2.dataRoot), game2);
1904
+ async function profileDirs(root, game, profile) {
1905
+ if (profile !== undefined)
1906
+ return [profileDataDir(root, game, profile)];
1907
+ const dir = join5(expandHome(root.dataRoot), game);
1908
1908
  return (await readdir2(dir).catch(() => [])).map((name) => join5(dir, name));
1909
1909
  }
1910
- function profileKey(game2, name) {
1911
- if (Object.hasOwn(game2.profiles, name))
1910
+ function profileKey(game, name) {
1911
+ if (Object.hasOwn(game.profiles, name))
1912
1912
  return name;
1913
1913
  const lower = name.toLowerCase();
1914
- const direct = Object.keys(game2.profiles).find((k) => k.toLowerCase() === lower);
1914
+ const direct = Object.keys(game.profiles).find((k) => k.toLowerCase() === lower);
1915
1915
  if (direct !== undefined)
1916
1916
  return direct;
1917
- return Object.keys(game2.profiles).find((k) => (own(game2.profiles, k)?.aliases ?? []).some((a) => a.toLowerCase() === lower));
1917
+ return Object.keys(game.profiles).find((k) => (own(game.profiles, k)?.aliases ?? []).some((a) => a.toLowerCase() === lower));
1918
1918
  }
1919
1919
  function subtract(mods, exclude) {
1920
1920
  if (exclude.length === 0)
@@ -1955,16 +1955,16 @@ function deepMerge(base, over, concatArrays = false) {
1955
1955
  }
1956
1956
  function expandPaths(config) {
1957
1957
  config.dataRoot = expandHome(config.dataRoot);
1958
- for (const game2 of Object.values(config.games)) {
1959
- if (game2.gameFiles.host !== undefined)
1960
- game2.gameFiles.host = expandHome(game2.gameFiles.host);
1961
- if (game2.image.context !== undefined)
1962
- game2.image.context = expandHome(game2.image.context);
1963
- if (game2.workshopRoot !== null)
1964
- game2.workshopRoot = expandHome(game2.workshopRoot);
1965
- for (const root2 of game2.scanRoots)
1966
- root2.path = expandHome(root2.path);
1967
- for (const entry of Object.values(game2.library ?? {})) {
1958
+ for (const game of Object.values(config.games)) {
1959
+ if (game.gameFiles.host !== undefined)
1960
+ game.gameFiles.host = expandHome(game.gameFiles.host);
1961
+ if (game.image.context !== undefined)
1962
+ game.image.context = expandHome(game.image.context);
1963
+ if (game.workshopRoot !== null)
1964
+ game.workshopRoot = expandHome(game.workshopRoot);
1965
+ for (const root of game.scanRoots)
1966
+ root.path = expandHome(root.path);
1967
+ for (const entry of Object.values(game.library ?? {})) {
1968
1968
  if (entry.path !== undefined)
1969
1969
  entry.path = expandHome(entry.path);
1970
1970
  }
@@ -2011,9 +2011,9 @@ function spawnArgv(argv, stdio) {
2011
2011
  return spawn(argv[0], argv.slice(1), { stdio });
2012
2012
  }
2013
2013
  function exited(proc) {
2014
- return new Promise((resolve4, reject) => {
2014
+ return new Promise((resolve, reject) => {
2015
2015
  proc.once("error", reject);
2016
- proc.once("close", (code) => resolve4(code ?? 1));
2016
+ proc.once("close", (code) => resolve(code ?? 1));
2017
2017
  });
2018
2018
  }
2019
2019
  async function collect3(stream) {
@@ -2057,12 +2057,12 @@ async function runContainer(spec, opts) {
2057
2057
  tee(proc.stdout, sink, process.stdout),
2058
2058
  tee(proc.stderr, sink, process.stderr)
2059
2059
  ]);
2060
- const status2 = await code;
2061
- return interrupted ? Exit.Interrupted : status2;
2060
+ const status = await code;
2061
+ return interrupted ? Exit.Interrupted : status;
2062
2062
  } finally {
2063
2063
  process.off("SIGINT", onSignal);
2064
2064
  process.off("SIGTERM", onSignal);
2065
- await new Promise((resolve4) => sink.end(resolve4));
2065
+ await new Promise((resolve) => sink.end(resolve));
2066
2066
  }
2067
2067
  }
2068
2068
  async function stopContainer(name, timeoutSeconds) {
@@ -2150,7 +2150,7 @@ async function tee(stream, sink, mirror) {
2150
2150
  var CDI_SPEC = "/etc/cdi/nvidia.yaml";
2151
2151
  async function preflight(plan) {
2152
2152
  const problems = [];
2153
- const game2 = plan.gameConfig;
2153
+ const game = plan.gameConfig;
2154
2154
  const dockerOk = await checkDocker(problems);
2155
2155
  if (dockerOk) {
2156
2156
  await checkImage(plan, problems);
@@ -2161,7 +2161,7 @@ async function preflight(plan) {
2161
2161
  checkBindSources(plan, problems);
2162
2162
  if (plan.mode === "headed")
2163
2163
  checkDisplay(plan, problems);
2164
- if (game2.gameFiles.source === "image" && game2.image.acquire === "build" && !game2.image.context) {
2164
+ if (game.gameFiles.source === "image" && game.image.acquire === "build" && !game.image.context) {
2165
2165
  problems.push({
2166
2166
  where: `/games/${plan.game}/image/context`,
2167
2167
  message: 'image.acquire is "build" but no build context is configured'
@@ -2180,7 +2180,7 @@ async function checkDocker(problems) {
2180
2180
  });
2181
2181
  return false;
2182
2182
  }
2183
- async function checkImageRunnable(ref, where, game2, problems) {
2183
+ async function checkImageRunnable(ref, where, game, problems) {
2184
2184
  const run = await capture(["docker", "run", "--rm", "--entrypoint", "/bin/true", ref]);
2185
2185
  if (run.code === 0)
2186
2186
  return;
@@ -2189,7 +2189,7 @@ async function checkImageRunnable(ref, where, game2, problems) {
2189
2189
  problems.push({
2190
2190
  where,
2191
2191
  message: corrupt ? `image ${ref} is present but unrunnable; its layers are missing from the content store` : `image ${ref} is present but failed to start: ${err || `exit ${run.code}`}`,
2192
- suggestion: corrupt ? `docker image rm ${ref} && docker builder prune -f, then gamecrate build ${game2}` : undefined
2192
+ suggestion: corrupt ? `docker image rm ${ref} && docker builder prune -f, then gamecrate build ${game}` : undefined
2193
2193
  });
2194
2194
  }
2195
2195
  async function checkImage(plan, problems) {
@@ -2356,9 +2356,9 @@ async function toplevels() {
2356
2356
  if (code === 127)
2357
2357
  return null;
2358
2358
  const found = [];
2359
- for (const line2 of stdout.split(`
2359
+ for (const line of stdout.split(`
2360
2360
  `)) {
2361
- const [id, , wmClass] = line2.trim().split(/\s+/);
2361
+ const [id, , wmClass] = line.trim().split(/\s+/);
2362
2362
  if (id !== undefined && wmClass !== undefined)
2363
2363
  found.push({ id, wmClass });
2364
2364
  }
@@ -2451,9 +2451,9 @@ function parseAtoms(stdout) {
2451
2451
  // src/launch/generate.ts
2452
2452
  import { mkdir, readdir as readdir4, readFile as readFile2, writeFile } from "node:fs/promises";
2453
2453
  import { dirname as dirname3, join as join8 } from "node:path";
2454
- async function readInstallVersion(game2, plugin) {
2455
- const host = game2.gameFiles.host;
2456
- if (game2.gameFiles.source !== "mount" || host === undefined)
2454
+ async function readInstallVersion(game, plugin) {
2455
+ const host = game.gameFiles.host;
2456
+ if (game.gameFiles.source !== "mount" || host === undefined)
2457
2457
  return null;
2458
2458
  let raw;
2459
2459
  try {
@@ -2463,19 +2463,19 @@ async function readInstallVersion(game2, plugin) {
2463
2463
  }
2464
2464
  return plugin.parseVersion(raw.replace(/^/, "").trim());
2465
2465
  }
2466
- async function readKnownExpansions(game2, plugin, warnings) {
2467
- if (game2.dlc.length === 0)
2466
+ async function readKnownExpansions(game, plugin, warnings) {
2467
+ if (game.dlc.length === 0)
2468
2468
  return [];
2469
- const host = game2.gameFiles.host;
2470
- if (game2.gameFiles.source !== "mount" || host === undefined)
2471
- return [...game2.dlc];
2469
+ const host = game.gameFiles.host;
2470
+ if (game.gameFiles.source !== "mount" || host === undefined)
2471
+ return [...game.dlc];
2472
2472
  const dataDir = join8(expandHome(host), "Data");
2473
2473
  let entries;
2474
2474
  try {
2475
2475
  entries = await readdir4(dataDir, { withFileTypes: true });
2476
2476
  } catch {
2477
2477
  warnings.push(`could not read ${dataDir}; falling back to the configured dlc list`);
2478
- return [...game2.dlc];
2478
+ return [...game.dlc];
2479
2479
  }
2480
2480
  const found = [];
2481
2481
  for (const entry of entries) {
@@ -2483,14 +2483,14 @@ async function readKnownExpansions(game2, plugin, warnings) {
2483
2483
  continue;
2484
2484
  let id = null;
2485
2485
  try {
2486
- id = plugin.parseManifest(await readFile2(join8(dataDir, entry.name, game2.manifest.file), "utf8"))?.packageId ?? null;
2486
+ id = plugin.parseManifest(await readFile2(join8(dataDir, entry.name, game.manifest.file), "utf8"))?.packageId ?? null;
2487
2487
  } catch {
2488
2488
  continue;
2489
2489
  }
2490
- if (id !== null && id.toLowerCase() !== game2.core.toLowerCase())
2490
+ if (id !== null && id.toLowerCase() !== game.core.toLowerCase())
2491
2491
  found.push(id);
2492
2492
  }
2493
- const order = game2.dlc.map((id) => id.toLowerCase());
2493
+ const order = game.dlc.map((id) => id.toLowerCase());
2494
2494
  return found.sort((a, b) => {
2495
2495
  const ai = order.indexOf(a.toLowerCase());
2496
2496
  const bi = order.indexOf(b.toLowerCase());
@@ -2498,14 +2498,14 @@ async function readKnownExpansions(game2, plugin, warnings) {
2498
2498
  });
2499
2499
  }
2500
2500
  async function generateModsConfig(plan) {
2501
- const game2 = plan.gameConfig;
2502
- const target = join8(plan.dataDirHost, game2.modsConfig.file);
2501
+ const game = plan.gameConfig;
2502
+ const target = join8(plan.dataDirHost, game.modsConfig.file);
2503
2503
  await mkdir(dirname3(target), { recursive: true });
2504
- const installed = await readInstallVersion(game2, plan.plugin);
2504
+ const installed = await readInstallVersion(game, plan.plugin);
2505
2505
  if (installed === null) {
2506
2506
  plan.warnings.push(`could not read Version.txt for ${plan.game}; ModsConfig version may be rejected`);
2507
2507
  }
2508
- const declared = new Map([game2.core, ...game2.dlc].map((id) => [id.toLowerCase(), id]));
2508
+ const declared = new Map([game.core, ...game.dlc].map((id) => [id.toLowerCase(), id]));
2509
2509
  const seen = new Set;
2510
2510
  const activeMods = [];
2511
2511
  for (const mod of plan.mods) {
@@ -2515,7 +2515,7 @@ async function generateModsConfig(plan) {
2515
2515
  seen.add(key);
2516
2516
  activeMods.push(key);
2517
2517
  }
2518
- const knownExpansions = (await readKnownExpansions(game2, plan.plugin, plan.warnings)).map((id) => declared.get(id.toLowerCase()) ?? id);
2518
+ const knownExpansions = (await readKnownExpansions(game, plan.plugin, plan.warnings)).map((id) => declared.get(id.toLowerCase()) ?? id);
2519
2519
  await writeFile(target, fromPlugin(target, () => plan.plugin.renderModsConfig({
2520
2520
  version: installed?.version ?? "",
2521
2521
  buildNumber: installed?.buildNumber ?? -1,
@@ -2536,16 +2536,16 @@ function fromPlugin(target, render) {
2536
2536
  }
2537
2537
  }
2538
2538
  function ownedPrefs(plan) {
2539
- const { settings: settings2 } = plan;
2540
- const bool2 = (value) => value ? "True" : "False";
2539
+ const { settings } = plan;
2540
+ const bool = (value) => value ? "True" : "False";
2541
2541
  const owned = {
2542
- screenWidth: String(settings2.width),
2543
- screenHeight: String(settings2.height),
2544
- devMode: bool2(settings2.devMode),
2545
- runInBackground: bool2(settings2.runInBackground),
2542
+ screenWidth: String(settings.width),
2543
+ screenHeight: String(settings.height),
2544
+ devMode: bool(settings.devMode),
2545
+ runInBackground: bool(settings.runInBackground),
2546
2546
  ...plan.plugin.windowedPrefs
2547
2547
  };
2548
- Object.assign(owned, settings2.prefsExtra ?? {});
2548
+ Object.assign(owned, settings.prefsExtra ?? {});
2549
2549
  owned.resetModsConfigOnCrash = "False";
2550
2550
  return owned;
2551
2551
  }
@@ -2594,12 +2594,12 @@ async function imageLabel(ref, label) {
2594
2594
  return null;
2595
2595
  return value;
2596
2596
  }
2597
- async function acquireImage(game2, config, pull) {
2597
+ async function acquireImage(game, config, pull) {
2598
2598
  const { image } = config;
2599
2599
  const present = await imageDigest(image.ref) !== null;
2600
2600
  if (image.acquire === "build") {
2601
2601
  if (image.context === undefined) {
2602
- throw new GamecrateError(`${game2} has image.acquire "build" but no context`, Exit.Config);
2602
+ throw new GamecrateError(`${game} has image.acquire "build" but no context`, Exit.Config);
2603
2603
  }
2604
2604
  if (present && pull !== "always")
2605
2605
  return;
@@ -2672,10 +2672,10 @@ async function buildTarget(dir) {
2672
2672
  const csproj = entries.find((e) => e.endsWith(".csproj"));
2673
2673
  return csproj ? join9(dir, csproj) : null;
2674
2674
  }
2675
- async function buildLocalMods(plan, policy2) {
2676
- if (policy2 === "never")
2675
+ async function buildLocalMods(plan, policy) {
2676
+ if (policy === "never")
2677
2677
  return;
2678
- const wanted = plan.mods.filter((m) => m.kind === "local" && (policy2 === "always" || m.stale === true));
2678
+ const wanted = plan.mods.filter((m) => m.kind === "local" && (policy === "always" || m.stale === true));
2679
2679
  if (wanted.length === 0)
2680
2680
  return;
2681
2681
  for (const mod of wanted) {
@@ -2772,7 +2772,7 @@ async function captureScreenshot(container, plan) {
2772
2772
  }
2773
2773
  async function writeLaunchRecord(plan, image) {
2774
2774
  const digest = await imageDigest(image);
2775
- const line2 = JSON.stringify({
2775
+ const line = JSON.stringify({
2776
2776
  at: new Date().toISOString(),
2777
2777
  game: plan.game,
2778
2778
  profile: plan.profile,
@@ -2787,7 +2787,7 @@ async function writeLaunchRecord(plan, image) {
2787
2787
  }))
2788
2788
  });
2789
2789
  const path = join9(plan.instanceDir, ".gamecrate", "launches.jsonl");
2790
- await writeFile2(path, `${line2}
2790
+ await writeFile2(path, `${line}
2791
2791
  `, { flag: "a" });
2792
2792
  }
2793
2793
 
@@ -2840,7 +2840,7 @@ function contains(request, dir) {
2840
2840
  const target = canonical(dir);
2841
2841
  return target === request.root || target.startsWith(request.root + sep);
2842
2842
  }
2843
- function collectRequests(flags2, env, cwd, disabled) {
2843
+ function collectRequests(flags, env, cwd, disabled) {
2844
2844
  if (disabled)
2845
2845
  return { requests: [], problems: [] };
2846
2846
  const requests = [];
@@ -2857,7 +2857,7 @@ function collectRequests(flags2, env, cwd, disabled) {
2857
2857
  problems.push(got);
2858
2858
  }
2859
2859
  };
2860
- for (const f of flags2)
2860
+ for (const f of flags)
2861
2861
  add(f, "flag");
2862
2862
  if (env !== undefined && env !== "" && env !== "off")
2863
2863
  add(env, "env");
@@ -2868,10 +2868,10 @@ function collectRequests(flags2, env, cwd, disabled) {
2868
2868
  // src/launch/instance.ts
2869
2869
  var SLUG_LIMIT = 24;
2870
2870
  function resolveInstance(options) {
2871
- const { profileDir, profile: profile2, args } = options;
2872
- const configured = lookup(profile2, args.instance);
2873
- const flags2 = configured?.worktree === undefined ? args.worktree ?? [] : [configured.worktree, ...args.worktree ?? []];
2874
- const { requests, problems } = collectRequests(flags2, options.env ?? process.env["GAMECRATE_WORKTREE"], options.cwd ?? process.cwd(), args.noWorktree ?? false);
2871
+ const { profileDir, profile, args } = options;
2872
+ const configured = lookup(profile, args.instance);
2873
+ const flags = configured?.worktree === undefined ? args.worktree ?? [] : [configured.worktree, ...args.worktree ?? []];
2874
+ const { requests, problems } = collectRequests(flags, options.env ?? process.env["GAMECRATE_WORKTREE"], options.cwd ?? process.cwd(), args.noWorktree ?? false);
2875
2875
  const name = args.instance === undefined ? derive(requests) : named(args.instance);
2876
2876
  return {
2877
2877
  ...name === undefined ? {} : { name },
@@ -2881,8 +2881,8 @@ function resolveInstance(options) {
2881
2881
  ...configured?.settings === undefined ? {} : { settings: configured.settings }
2882
2882
  };
2883
2883
  }
2884
- function lookup(profile2, name) {
2885
- const instances = profile2?.instances;
2884
+ function lookup(profile, name) {
2885
+ const instances = profile?.instances;
2886
2886
  if (instances === undefined || name === undefined)
2887
2887
  return;
2888
2888
  const exact = own(instances, name);
@@ -2905,8 +2905,8 @@ function derive(requests) {
2905
2905
  const digest = createHash("sha256").update(requests.map((r) => r.root).join("\x00")).digest("hex");
2906
2906
  return `${slug(first.root)}-${digest.slice(0, 6)}`;
2907
2907
  }
2908
- function slug(root2) {
2909
- const body = basename5(root2).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+/, "").slice(0, SLUG_LIMIT).replace(/[-._]+$/, "");
2908
+ function slug(root) {
2909
+ const body = basename5(root).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+/, "").slice(0, SLUG_LIMIT).replace(/[-._]+$/, "");
2910
2910
  return body === "" ? "wt" : body;
2911
2911
  }
2912
2912
 
@@ -2950,9 +2950,9 @@ function inLinkedWorktree(dir, stopAt) {
2950
2950
  current = parent;
2951
2951
  }
2952
2952
  }
2953
- async function scanLocalRoot(root2, rootIndex, manifestFile, found) {
2954
- const base = resolvePath(expandHome(root2.path));
2955
- const exclude = [...root2.exclude ?? [], ...rootIndex === -1 ? [] : ALWAYS_EXCLUDE];
2953
+ async function scanLocalRoot(root, rootIndex, manifestFile, found) {
2954
+ const base = resolvePath(expandHome(root.path));
2955
+ const exclude = [...root.exclude ?? [], ...rootIndex === -1 ? [] : ALWAYS_EXCLUDE];
2956
2956
  const walk = async (dir, depth) => {
2957
2957
  const manifest = join11(dir, manifestFile);
2958
2958
  if (existsSync6(manifest)) {
@@ -2964,7 +2964,7 @@ async function scanLocalRoot(root2, rootIndex, manifestFile, found) {
2964
2964
  });
2965
2965
  return;
2966
2966
  }
2967
- if (depth >= root2.maxDepth)
2967
+ if (depth >= root.maxDepth)
2968
2968
  return;
2969
2969
  let entries;
2970
2970
  try {
@@ -3002,9 +3002,9 @@ async function scanWorkshopRoot(workshopRoot, rootIndex, manifestFile, found) {
3002
3002
  found.push({ dir, kind: "workshop", rootIndex, linkedWorktree: false, workshopId: Number(entry.name) });
3003
3003
  }
3004
3004
  }
3005
- async function scanGameData(game2, rootIndex, found) {
3006
- const host = game2.gameFiles.host;
3007
- if (game2.gameFiles.source !== "mount" || host === undefined)
3005
+ async function scanGameData(game, rootIndex, found) {
3006
+ const host = game.gameFiles.host;
3007
+ if (game.gameFiles.source !== "mount" || host === undefined)
3008
3008
  return;
3009
3009
  const data = join11(resolvePath(expandHome(host)), "Data");
3010
3010
  let entries;
@@ -3017,16 +3017,16 @@ async function scanGameData(game2, rootIndex, found) {
3017
3017
  if (!entry.isDirectory())
3018
3018
  continue;
3019
3019
  const dir = join11(data, entry.name);
3020
- if (!existsSync6(join11(dir, game2.manifest.file)))
3020
+ if (!existsSync6(join11(dir, game.manifest.file)))
3021
3021
  continue;
3022
3022
  found.push({ dir, kind: "official", rootIndex, linkedWorktree: false });
3023
3023
  }
3024
3024
  }
3025
3025
  var CACHE_VERSION = 2;
3026
- function workshopStamp(game2) {
3027
- if (game2.workshopRoot === null)
3026
+ function workshopStamp(game) {
3027
+ if (game.workshopRoot === null)
3028
3028
  return null;
3029
- const acf = join11(dirname4(dirname4(resolvePath(expandHome(game2.workshopRoot)))), `appworkshop_${game2.steamAppId}.acf`);
3029
+ const acf = join11(dirname4(dirname4(resolvePath(expandHome(game.workshopRoot)))), `appworkshop_${game.steamAppId}.acf`);
3030
3030
  try {
3031
3031
  const info = statSync2(acf);
3032
3032
  return `${info.mtimeMs}:${info.size}`;
@@ -3034,9 +3034,9 @@ function workshopStamp(game2) {
3034
3034
  return null;
3035
3035
  }
3036
3036
  }
3037
- async function readWorkshopCache(game2, stamp) {
3037
+ async function readWorkshopCache(game, stamp) {
3038
3038
  try {
3039
- const raw = JSON.parse(await readFile4(join11(cacheDir(), `${game2}.workshop.json`), "utf8"));
3039
+ const raw = JSON.parse(await readFile4(join11(cacheDir(), `${game}.workshop.json`), "utf8"));
3040
3040
  if (raw.version !== CACHE_VERSION || raw.stamp !== stamp)
3041
3041
  return null;
3042
3042
  return raw.records;
@@ -3044,15 +3044,15 @@ async function readWorkshopCache(game2, stamp) {
3044
3044
  return null;
3045
3045
  }
3046
3046
  }
3047
- async function writeWorkshopCache(game2, stamp, records) {
3047
+ async function writeWorkshopCache(game, stamp, records) {
3048
3048
  const payload = { version: CACHE_VERSION, stamp, records };
3049
3049
  try {
3050
3050
  await mkdir2(cacheDir(), { recursive: true });
3051
- await writeFile3(join11(cacheDir(), `${game2}.workshop.json`), JSON.stringify(payload));
3051
+ await writeFile3(join11(cacheDir(), `${game}.workshop.json`), JSON.stringify(payload));
3052
3052
  } catch {}
3053
3053
  }
3054
- function toRecord(candidate, manifest, game2) {
3055
- const kind = manifest.packageId.toLowerCase() === game2.core.toLowerCase() ? "core" : candidate.kind;
3054
+ function toRecord(candidate, manifest, game) {
3055
+ const kind = manifest.packageId.toLowerCase() === game.core.toLowerCase() ? "core" : candidate.kind;
3056
3056
  const record = {
3057
3057
  packageId: manifest.packageId,
3058
3058
  dir: candidate.dir,
@@ -3193,9 +3193,9 @@ function insert(index, record) {
3193
3193
  }
3194
3194
  }
3195
3195
  }
3196
- async function buildIndex(game2, config, plugin) {
3196
+ async function buildIndex(game, config, plugin) {
3197
3197
  const index = {
3198
- game: game2,
3198
+ game,
3199
3199
  plugin,
3200
3200
  byPackageId: new Map,
3201
3201
  byWorkshopId: new Map,
@@ -3204,14 +3204,14 @@ async function buildIndex(game2, config, plugin) {
3204
3204
  };
3205
3205
  const local = [];
3206
3206
  await scanGameData(config, -1, local);
3207
- for (const [i, root2] of config.scanRoots.entries()) {
3208
- await scanLocalRoot(root2, i, config.manifest.file, local);
3207
+ for (const [i, root] of config.scanRoots.entries()) {
3208
+ await scanLocalRoot(root, i, config.manifest.file, local);
3209
3209
  }
3210
3210
  for (const record of await parseAll(local, config, plugin, index.problems))
3211
3211
  insert(index, record);
3212
3212
  if (config.workshopRoot !== null) {
3213
3213
  const stamp = workshopStamp(config);
3214
- const cached = stamp === null ? null : await readWorkshopCache(game2, stamp);
3214
+ const cached = stamp === null ? null : await readWorkshopCache(game, stamp);
3215
3215
  if (cached) {
3216
3216
  for (const record of cached)
3217
3217
  insert(index, record);
@@ -3222,7 +3222,7 @@ async function buildIndex(game2, config, plugin) {
3222
3222
  for (const record of records)
3223
3223
  insert(index, record);
3224
3224
  if (stamp !== null)
3225
- await writeWorkshopCache(game2, stamp, records);
3225
+ await writeWorkshopCache(game, stamp, records);
3226
3226
  }
3227
3227
  }
3228
3228
  for (const bucket of index.byPackageId.values())
@@ -3245,8 +3245,8 @@ async function parseAll(candidates, config, plugin, problems) {
3245
3245
  }));
3246
3246
  return records.filter((r) => r !== null);
3247
3247
  }
3248
- function resolveModRef(index, ref, game2) {
3249
- return honorOverride(index, resolveRaw(index, ref, game2));
3248
+ function resolveModRef(index, ref, game) {
3249
+ return honorOverride(index, resolveRaw(index, ref, game));
3250
3250
  }
3251
3251
  function honorOverride(index, record) {
3252
3252
  if (record === null || record.overridden !== undefined)
@@ -3254,9 +3254,9 @@ function honorOverride(index, record) {
3254
3254
  const best = index.byPackageId.get(record.packageId.toLowerCase())?.[0];
3255
3255
  return best?.overridden !== undefined ? best : record;
3256
3256
  }
3257
- function resolveRaw(index, ref, game2) {
3257
+ function resolveRaw(index, ref, game) {
3258
3258
  if (ref.startsWith("path:"))
3259
- return byPath(index, ref.slice(5), game2);
3259
+ return byPath(index, ref.slice(5), game);
3260
3260
  if (ref.startsWith("workshop:")) {
3261
3261
  const id = Number(ref.slice(9));
3262
3262
  return Number.isInteger(id) ? index.byWorkshopId.get(id) ?? null : null;
@@ -3264,9 +3264,9 @@ function resolveRaw(index, ref, game2) {
3264
3264
  const direct = pick(index, ref.toLowerCase(), ref);
3265
3265
  if (direct)
3266
3266
  return direct;
3267
- const alias = own(game2.aliases, ref) ?? own(game2.aliases, ref.toLowerCase());
3267
+ const alias = own(game.aliases, ref) ?? own(game.aliases, ref.toLowerCase());
3268
3268
  if (alias !== undefined && alias.toLowerCase() !== ref.toLowerCase()) {
3269
- return resolveRaw(index, alias, game2);
3269
+ return resolveRaw(index, alias, game);
3270
3270
  }
3271
3271
  const short = index.byShortName.get(ref.toLowerCase());
3272
3272
  if (short && short.length === 1)
@@ -3295,21 +3295,21 @@ function pick(index, key, ref) {
3295
3295
  }
3296
3296
  return best;
3297
3297
  }
3298
- function byPath(index, raw, game2) {
3298
+ function byPath(index, raw, game) {
3299
3299
  const dir = resolvePath(expandHome(raw));
3300
3300
  for (const bucket of index.byPackageId.values()) {
3301
3301
  const hit = bucket.find((record) => record.dir === dir || record.dir === raw);
3302
3302
  if (hit)
3303
3303
  return hit;
3304
3304
  }
3305
- const file = join11(dir, game2.manifest.file);
3305
+ const file = join11(dir, game.manifest.file);
3306
3306
  if (!existsSync6(file))
3307
3307
  return null;
3308
3308
  try {
3309
3309
  const manifest = index.plugin.parseManifest(readFileSync3(file, "utf8"));
3310
3310
  if (manifest === null)
3311
3311
  return null;
3312
- return toRecord({ dir, kind: "local", rootIndex: -1, linkedWorktree: false }, manifest, game2);
3312
+ return toRecord({ dir, kind: "local", rootIndex: -1, linkedWorktree: false }, manifest, game);
3313
3313
  } catch {
3314
3314
  return null;
3315
3315
  }
@@ -3321,7 +3321,7 @@ var DEFAULT_RENDER_WAIT_SECONDS = 25;
3321
3321
  function isDynamic(entry) {
3322
3322
  return typeof entry !== "string" && "match" in entry;
3323
3323
  }
3324
- function refFor(entry, game2) {
3324
+ function refFor(entry, game) {
3325
3325
  const object = typeof entry === "string" ? { id: entry } : entry;
3326
3326
  if (object.path !== undefined)
3327
3327
  return `path:${object.path}`;
@@ -3329,7 +3329,7 @@ function refFor(entry, game2) {
3329
3329
  return `workshop:${object.workshop}`;
3330
3330
  if (object.id.includes(":"))
3331
3331
  return object.id;
3332
- const pin = own(game2.library, object.id) ?? own(game2.library, object.id.toLowerCase());
3332
+ const pin = own(game.library, object.id) ?? own(game.library, object.id.toLowerCase());
3333
3333
  if (pin?.path !== undefined)
3334
3334
  return `path:${pin.path}`;
3335
3335
  if (pin?.workshop !== undefined)
@@ -3359,22 +3359,22 @@ function expandDynamic(entry, index, where, problems) {
3359
3359
  }
3360
3360
  return [...head, ...rest];
3361
3361
  }
3362
- function collectSlots(game2, gameName, profileName, profile2, args) {
3362
+ function collectSlots(game, gameName, profileName, profile, args) {
3363
3363
  const modless = profileName === "modless";
3364
3364
  const slots = [];
3365
3365
  const at = `/games/${gameName}`;
3366
3366
  if (!modless)
3367
- for (const [i, id] of (game2.preCore ?? []).entries())
3367
+ for (const [i, id] of (game.preCore ?? []).entries())
3368
3368
  slots.push({ entry: id, where: `${at}/preCore/${i}` });
3369
- slots.push({ entry: game2.core, where: `${at}/core` });
3370
- for (const [i, id] of game2.dlc.entries())
3369
+ slots.push({ entry: game.core, where: `${at}/core` });
3370
+ for (const [i, id] of game.dlc.entries())
3371
3371
  slots.push({ entry: id, where: `${at}/dlc/${i}`, dlc: true });
3372
- if (!modless && profile2.includeBase !== false) {
3373
- for (const [i, id] of (game2.base ?? []).entries())
3372
+ if (!modless && profile.includeBase !== false) {
3373
+ for (const [i, id] of (game.base ?? []).entries())
3374
3374
  slots.push({ entry: id, where: `${at}/base/${i}` });
3375
3375
  }
3376
3376
  const only = args.only ?? [];
3377
- const declared = only.length > 0 ? only : profile2.mods ?? [];
3377
+ const declared = only.length > 0 ? only : profile.mods ?? [];
3378
3378
  const source = only.length > 0 ? "flag --only" : `${at}/profiles/${profileName}/mods`;
3379
3379
  for (const [i, entry] of declared.entries())
3380
3380
  slots.push({ entry, where: `${source}/${i}` });
@@ -3382,7 +3382,7 @@ function collectSlots(game2, gameName, profileName, profile2, args) {
3382
3382
  slots.push({ entry: id, where: `flag --mod/${i}` });
3383
3383
  return slots;
3384
3384
  }
3385
- function insertDependencies(list, present, index, game2, problems) {
3385
+ function insertDependencies(list, present, index, game, problems) {
3386
3386
  let i = 0;
3387
3387
  outer:
3388
3388
  while (i < list.length) {
@@ -3392,7 +3392,7 @@ function insertDependencies(list, present, index, game2, problems) {
3392
3392
  if (present.has(key))
3393
3393
  continue;
3394
3394
  present.add(key);
3395
- const record = resolveModRef(index, dep.packageId, game2);
3395
+ const record = resolveModRef(index, dep.packageId, game);
3396
3396
  if (!record) {
3397
3397
  problems.push({
3398
3398
  where: mod.record.packageId,
@@ -3494,47 +3494,47 @@ function incompatibilityWarnings(list) {
3494
3494
  return warnings;
3495
3495
  }
3496
3496
  async function resolvePlan(options) {
3497
- const { game: gameName, profile: requestedProfile, root: root2 } = options;
3497
+ const { game: gameName, profile: requestedProfile, root } = options;
3498
3498
  const args = options.args ?? {};
3499
3499
  const problems = [];
3500
3500
  const warnings = [];
3501
- const game2 = own(root2.games, gameName);
3502
- if (!game2) {
3503
- throw new GamecrateError(`unknown game "${gameName}"`, Exit.Config, `known: ${Object.keys(root2.games).join(", ")}`);
3501
+ const game = own(root.games, gameName);
3502
+ if (!game) {
3503
+ throw new GamecrateError(`unknown game "${gameName}"`, Exit.Config, `known: ${Object.keys(root.games).join(", ")}`);
3504
3504
  }
3505
3505
  if (!NAME_PATTERN.test(requestedProfile)) {
3506
3506
  throw new GamecrateError(`invalid profile name "${requestedProfile}"`, Exit.Usage);
3507
3507
  }
3508
- const profileName = canonicalProfile(game2, requestedProfile);
3509
- const profile2 = profileName === "modless" ? { mods: [], includeBase: false } : resolveProfile(game2, profileName);
3510
- const profileDir = profileDataDir(root2, gameName, profileName);
3508
+ const profileName = canonicalProfile(game, requestedProfile);
3509
+ const profile = profileName === "modless" ? { mods: [], includeBase: false } : resolveProfile(game, profileName);
3510
+ const profileDir = profileDataDir(root, gameName, profileName);
3511
3511
  const instance = resolveInstance({
3512
3512
  profileDir,
3513
- profile: profile2,
3513
+ profile,
3514
3514
  args,
3515
3515
  ...options.cwd === undefined ? {} : { cwd: options.cwd }
3516
3516
  });
3517
3517
  problems.push(...instance.problems);
3518
- const settings2 = resolveSettings(root2, game2, profile2, instance.settings, args.resolution);
3518
+ const settings = resolveSettings(root, game, profile, instance.settings, args.resolution);
3519
3519
  if (args.network !== undefined)
3520
- settings2.network = args.network;
3520
+ settings.network = args.network;
3521
3521
  if (args.gameArgs?.length)
3522
- settings2.gameArgs = [...settings2.gameArgs ?? [], ...args.gameArgs];
3522
+ settings.gameArgs = [...settings.gameArgs ?? [], ...args.gameArgs];
3523
3523
  if (args.dockerArgs?.length)
3524
- settings2.dockerArgs = [...settings2.dockerArgs ?? [], ...args.dockerArgs];
3524
+ settings.dockerArgs = [...settings.dockerArgs ?? [], ...args.dockerArgs];
3525
3525
  const plugin = requirePlugin(options.plugins, gameName);
3526
- const index = options.index ?? await buildIndex(gameName, game2, plugin);
3527
- await applyWorktreeRequests(index, instance.requests, game2);
3528
- problems.push(...await applySourceOverrides(index, args.use ?? [], game2));
3529
- const excluded2 = [...profile2.exclude ?? [], ...args.without ?? []].map(globToRegExp);
3530
- const isExcluded = (id) => excluded2.some((pattern) => pattern.test(id));
3526
+ const index = options.index ?? await buildIndex(gameName, game, plugin);
3527
+ await applyWorktreeRequests(index, instance.requests, game);
3528
+ problems.push(...await applySourceOverrides(index, args.use ?? [], game));
3529
+ const excluded = [...profile.exclude ?? [], ...args.without ?? []].map(globToRegExp);
3530
+ const isExcluded = (id) => excluded.some((pattern) => pattern.test(id));
3531
3531
  const staged = [];
3532
3532
  const present = new Set;
3533
- for (const slot of collectSlots(game2, gameName, profileName, profile2, args)) {
3533
+ for (const slot of collectSlots(game, gameName, profileName, profile, args)) {
3534
3534
  const entry = slot.entry;
3535
- const refs = isDynamic(entry) ? expandDynamic(entry, index, slot.where, problems).map((id) => ({ ref: id, optional: false })) : [{ ref: refFor(entry, game2), optional: typeof entry !== "string" && entry.optional === true }];
3535
+ const refs = isDynamic(entry) ? expandDynamic(entry, index, slot.where, problems).map((id) => ({ ref: id, optional: false })) : [{ ref: refFor(entry, game), optional: typeof entry !== "string" && entry.optional === true }];
3536
3536
  for (const { ref, optional } of refs) {
3537
- const record = resolveModRef(index, ref, game2);
3537
+ const record = resolveModRef(index, ref, game);
3538
3538
  if (!record) {
3539
3539
  if (slot.dlc === true)
3540
3540
  continue;
@@ -3551,9 +3551,9 @@ async function resolvePlan(options) {
3551
3551
  staged.push({ record, explicit: true });
3552
3552
  }
3553
3553
  }
3554
- if (profile2.autoDependencies === true)
3555
- insertDependencies(staged, present, index, game2, problems);
3556
- const ordered = args.sort === "none" ? staged : topoSort(staged, problems, game2.core);
3554
+ if (profile.autoDependencies === true)
3555
+ insertDependencies(staged, present, index, game, problems);
3556
+ const ordered = args.sort === "none" ? staged : topoSort(staged, problems, game.core);
3557
3557
  warnings.push(...incompatibilityWarnings(ordered));
3558
3558
  const mods = [];
3559
3559
  for (const { record, explicit } of ordered) {
@@ -3566,7 +3566,7 @@ async function resolvePlan(options) {
3566
3566
  mods.push({
3567
3567
  packageId: record.packageId,
3568
3568
  hostDir: record.dir,
3569
- containerDir: `${game2.modsDir.container}/${record.packageId}`,
3569
+ containerDir: `${game.modsDir.container}/${record.packageId}`,
3570
3570
  kind: record.kind,
3571
3571
  ...record.workshopId === undefined ? {} : { workshopId: record.workshopId },
3572
3572
  explicit,
@@ -3577,28 +3577,28 @@ async function resolvePlan(options) {
3577
3577
  });
3578
3578
  }
3579
3579
  problems.push(...index.problems);
3580
- if (game2.dataDir.mode === "arg" && game2.dataDir.arg.split("=").length !== 2) {
3580
+ if (game.dataDir.mode === "arg" && game.dataDir.arg.split("=").length !== 2) {
3581
3581
  problems.push({
3582
3582
  where: `/games/${gameName}/dataDir/arg`,
3583
- message: `"${game2.dataDir.arg}" must contain exactly one "="; RimWorld silently ignores anything else`
3583
+ message: `"${game.dataDir.arg}" must contain exactly one "="; RimWorld silently ignores anything else`
3584
3584
  });
3585
3585
  }
3586
- if (game2.dataDir.container.includes("=")) {
3586
+ if (game.dataDir.container.includes("=")) {
3587
3587
  problems.push({
3588
3588
  where: `/games/${gameName}/dataDir/container`,
3589
- message: `container data path "${game2.dataDir.container}" contains "=", which disables the override silently`
3589
+ message: `container data path "${game.dataDir.container}" contains "=", which disables the override silently`
3590
3590
  });
3591
3591
  }
3592
3592
  const mode = args.mode ?? "headed";
3593
- if (!game2.modes.includes(mode)) {
3593
+ if (!game.modes.includes(mode)) {
3594
3594
  problems.push({ where: "flag --mode", message: `${gameName} does not support mode "${mode}"` });
3595
3595
  }
3596
3596
  const plan = {
3597
3597
  game: gameName,
3598
- gameConfig: game2,
3598
+ gameConfig: game,
3599
3599
  plugin,
3600
3600
  profile: profileName,
3601
- settings: settings2,
3601
+ settings,
3602
3602
  mods,
3603
3603
  profileDir,
3604
3604
  ...instance.name === undefined ? {} : { instance: instance.name },
@@ -3693,7 +3693,7 @@ async function detectForeignOwnership(dir, uid, limit = 100) {
3693
3693
  }
3694
3694
 
3695
3695
  // src/index.ts
3696
- var VERSION = "0.1.0";
3696
+ var VERSION = "1.0.0";
3697
3697
  var STOP_TIMEOUT_SECONDS = 10;
3698
3698
  async function main(argv) {
3699
3699
  const probe = parseArgs(argv);
@@ -3770,20 +3770,20 @@ function help(args, config) {
3770
3770
  return Exit.Ok;
3771
3771
  }
3772
3772
  function requireGame(args, config) {
3773
- const game2 = args.game;
3774
- if (game2 === undefined) {
3773
+ const game = args.game;
3774
+ if (game === undefined) {
3775
3775
  throw new GamecrateError(`${args.subcommand} needs a game`, Exit.Usage, `known games: ${Object.keys(config.games).join(", ")}`);
3776
3776
  }
3777
- if (!Object.hasOwn(config.games, game2)) {
3778
- throw new GamecrateError(`unknown game "${game2}"`, Exit.Config, `known games: ${Object.keys(config.games).join(", ")}`);
3777
+ if (!Object.hasOwn(config.games, game)) {
3778
+ throw new GamecrateError(`unknown game "${game}"`, Exit.Config, `known games: ${Object.keys(config.games).join(", ")}`);
3779
3779
  }
3780
- return game2;
3780
+ return game;
3781
3781
  }
3782
- function instanceDir(args, config, game2, profile2) {
3783
- const dir = profileDataDir(config, game2, profile2);
3782
+ function instanceDir(args, config, game, profile) {
3783
+ const dir = profileDataDir(config, game, profile);
3784
3784
  let spec;
3785
3785
  try {
3786
- spec = resolveProfile(config.games[game2], profile2);
3786
+ spec = resolveProfile(config.games[game], profile);
3787
3787
  } catch {
3788
3788
  spec = undefined;
3789
3789
  }
@@ -3803,25 +3803,25 @@ function reportEnvironment(problems) {
3803
3803
  process.exit(Exit.Environment);
3804
3804
  }
3805
3805
  async function run(args, config, plugins, asShell) {
3806
- const game2 = requireGame(args, config);
3807
- const profile2 = args.profile ?? "modless";
3808
- const index = await buildIndex(game2, config.games[game2], requirePlugin(plugins, game2));
3809
- const { plan, problems } = await resolvePlan({ game: game2, profile: profile2, root: config, plugins, args, index });
3806
+ const game = requireGame(args, config);
3807
+ const profile = args.profile ?? "modless";
3808
+ const index = await buildIndex(game, config.games[game], requirePlugin(plugins, game));
3809
+ const { plan, problems } = await resolvePlan({ game, profile, root: config, plugins, args, index });
3810
3810
  if (problems.length > 0)
3811
3811
  reportProblems(problems);
3812
3812
  const identity = resolveIdentity(args.root);
3813
3813
  if (args.printPlan || args.dryRun) {
3814
- const environment2 = await preflight(plan);
3814
+ const environment = await preflight(plan);
3815
3815
  buildRunSpec(plan, [], identity);
3816
3816
  if (args.printPlan)
3817
3817
  printPlan(plan, args.json);
3818
3818
  for (const warning of planWarnings(plan))
3819
3819
  warn(warning);
3820
- if (environment2.length > 0)
3821
- return reportEnvironment(environment2);
3820
+ if (environment.length > 0)
3821
+ return reportEnvironment(environment);
3822
3822
  if (!args.printPlan) {
3823
- const what = plan.instance === undefined ? profile2 : `${profile2}/${plan.instance}`;
3824
- status(`${game2} ${what}: ${plan.mods.length} mods resolve cleanly`);
3823
+ const what = plan.instance === undefined ? profile : `${profile}/${plan.instance}`;
3824
+ status(`${game} ${what}: ${plan.mods.length} mods resolve cleanly`);
3825
3825
  }
3826
3826
  return Exit.Ok;
3827
3827
  }
@@ -3839,19 +3839,19 @@ async function run(args, config, plugins, asShell) {
3839
3839
  }
3840
3840
  }
3841
3841
  async function launch(plan, args, config, identity, asShell) {
3842
- const game2 = plan.game;
3843
- const profile2 = plan.profile;
3842
+ const game = plan.game;
3843
+ const profile = plan.profile;
3844
3844
  const foreign = await detectForeignOwnership(plan.dataDirHost, identity.uid, 5);
3845
3845
  if (foreign.length > 0) {
3846
3846
  throw new GamecrateError(`${foreign.length} path(s) under ${plan.dataDirHost} are not owned by uid ${identity.uid}`, Exit.Environment, `${foreign.join(`
3847
3847
  `)}
3848
- run: gamecrate fix-perms ${game2} ${profile2}`);
3848
+ run: gamecrate fix-perms ${game} ${profile}`);
3849
3849
  }
3850
3850
  const runDir = openRunLog(plan.logsDirHost);
3851
3851
  plan.runDirHost = runDir;
3852
3852
  await buildLocalMods(plan, args.build ?? "auto");
3853
- await acquireImage(game2, config.games[game2], args.pull ?? "missing");
3854
- const runtimeImage = plan.mode === "headed" ? config.games[game2].image.ref : await ensureRuntimeLayer(config.games[game2].image.ref);
3853
+ await acquireImage(game, config.games[game], args.pull ?? "missing");
3854
+ const runtimeImage = plan.mode === "headed" ? config.games[game].image.ref : await ensureRuntimeLayer(config.games[game].image.ref);
3855
3855
  const modMounts = await stageMods(plan);
3856
3856
  await generateModsConfig(plan);
3857
3857
  await mergePrefs(plan);
@@ -3982,14 +3982,14 @@ function list(args, config) {
3982
3982
  const games = args.game === undefined ? Object.keys(config.games) : [requireGame(args, config)];
3983
3983
  if (args.json) {
3984
3984
  const payload = games.map((name) => {
3985
- const game2 = config.games[name];
3985
+ const game = config.games[name];
3986
3986
  return {
3987
3987
  game: name,
3988
- core: game2.core,
3989
- dlc: game2.dlc,
3990
- modes: game2.modes,
3991
- profiles: Object.entries(game2.profiles).map(([profile2, spec]) => ({
3992
- profile: profile2,
3988
+ core: game.core,
3989
+ dlc: game.dlc,
3990
+ modes: game.modes,
3991
+ profiles: Object.entries(game.profiles).map(([profile, spec]) => ({
3992
+ profile,
3993
3993
  alias: spec.alias ?? null,
3994
3994
  extends: spec.extends ?? null,
3995
3995
  mods: spec.mods?.length ?? 0,
@@ -4003,11 +4003,11 @@ function list(args, config) {
4003
4003
  }
4004
4004
  const out = [];
4005
4005
  for (const name of games) {
4006
- const game2 = config.games[name];
4007
- const width = Math.max(7, ...Object.keys(game2.profiles).map((n) => n.length));
4008
- out.push(`${name} (${game2.modes.join(", ")})`);
4006
+ const game = config.games[name];
4007
+ const width = Math.max(7, ...Object.keys(game.profiles).map((n) => n.length));
4008
+ out.push(`${name} (${game.modes.join(", ")})`);
4009
4009
  out.push(` ${"modless".padEnd(width)} built-in: core + official DLC`);
4010
- for (const [profile2, spec] of Object.entries(game2.profiles)) {
4010
+ for (const [profile, spec] of Object.entries(game.profiles)) {
4011
4011
  const notes = [];
4012
4012
  if (spec.alias)
4013
4013
  notes.push(`alias for ${spec.alias}`);
@@ -4018,7 +4018,7 @@ function list(args, config) {
4018
4018
  notes.push(count === 1 ? "1 entry" : `${count} entries`);
4019
4019
  if (spec.aliases?.length)
4020
4020
  notes.push(`aka ${spec.aliases.join(", ")}`);
4021
- out.push(` ${profile2.padEnd(width)} ${notes.join(", ")}`);
4021
+ out.push(` ${profile.padEnd(width)} ${notes.join(", ")}`);
4022
4022
  const instances = Object.keys(spec.instances ?? {});
4023
4023
  if (instances.length > 0)
4024
4024
  out.push(` ${" ".repeat(width)} instances: ${instances.join(", ")}`);
@@ -4030,10 +4030,10 @@ function list(args, config) {
4030
4030
  return Exit.Ok;
4031
4031
  }
4032
4032
  async function mods(args, config, plugins) {
4033
- const game2 = requireGame(args, config);
4034
- const profile2 = args.profile ?? "modless";
4035
- const index = await buildIndex(game2, config.games[game2], requirePlugin(plugins, game2));
4036
- const { plan, problems } = await resolvePlan({ game: game2, profile: profile2, root: config, plugins, args, index });
4033
+ const game = requireGame(args, config);
4034
+ const profile = args.profile ?? "modless";
4035
+ const index = await buildIndex(game, config.games[game], requirePlugin(plugins, game));
4036
+ const { plan, problems } = await resolvePlan({ game, profile, root: config, plugins, args, index });
4037
4037
  if (problems.length > 0)
4038
4038
  reportProblems(problems);
4039
4039
  printPlan(plan, args.json);
@@ -4041,15 +4041,15 @@ async function mods(args, config, plugins) {
4041
4041
  }
4042
4042
  async function doctor(config, plugins) {
4043
4043
  let failed = false;
4044
- for (const game2 of Object.keys(config.games)) {
4045
- const { plan, problems } = await resolvePlan({ game: game2, profile: "modless", root: config, plugins });
4044
+ for (const game of Object.keys(config.games)) {
4045
+ const { plan, problems } = await resolvePlan({ game, profile: "modless", root: config, plugins });
4046
4046
  const all = [...problems, ...await preflight(plan)];
4047
4047
  if (all.length === 0) {
4048
- status(`${game2}: ok`);
4048
+ status(`${game}: ok`);
4049
4049
  continue;
4050
4050
  }
4051
4051
  failed = true;
4052
- status(`${game2}: ${all.length} problem(s)`);
4052
+ status(`${game}: ${all.length} problem(s)`);
4053
4053
  for (const problem of all) {
4054
4054
  process.stderr.write(` ${problem.where}
4055
4055
  ${problem.message}
@@ -4062,12 +4062,12 @@ async function doctor(config, plugins) {
4062
4062
  return failed ? Exit.Environment : Exit.Ok;
4063
4063
  }
4064
4064
  async function logs(args, config) {
4065
- const game2 = requireGame(args, config);
4066
- const profile2 = args.profile ?? "modless";
4067
- const runs = join14(instanceDir(args, config, game2, profile2), "logs", "runs");
4065
+ const game = requireGame(args, config);
4066
+ const profile = args.profile ?? "modless";
4067
+ const runs = join14(instanceDir(args, config, game, profile), "logs", "runs");
4068
4068
  const latest = (await readdir8(runs, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort().at(-1);
4069
4069
  if (latest === undefined) {
4070
- throw new GamecrateError(`no runs recorded for ${game2} ${profile2}`, Exit.Usage, runs);
4070
+ throw new GamecrateError(`no runs recorded for ${game} ${profile}`, Exit.Usage, runs);
4071
4071
  }
4072
4072
  const dir = join14(runs, latest);
4073
4073
  const files = (await readdir8(dir, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
@@ -4079,10 +4079,10 @@ async function logs(args, config) {
4079
4079
  status(dir);
4080
4080
  for (const name of files) {
4081
4081
  const text = await readFile5(join14(dir, name), "utf8").catch(() => "");
4082
- for (const line2 of text.split(`
4082
+ for (const line of text.split(`
4083
4083
  `)) {
4084
- if (line2.length > 0)
4085
- process.stdout.write(`${name}: ${line2}
4084
+ if (line.length > 0)
4085
+ process.stdout.write(`${name}: ${line}
4086
4086
  `);
4087
4087
  }
4088
4088
  }
@@ -4117,9 +4117,9 @@ function boundStatus(mod) {
4117
4117
  return mod.hasSources ? "STALE - never built" : "(xml only)";
4118
4118
  }
4119
4119
  async function verify(args, config, plugins) {
4120
- const game2 = requireGame(args, config);
4121
- const profile2 = args.profile ?? "modless";
4122
- const { plan, problems } = await resolvePlan({ game: game2, profile: profile2, root: config, plugins, args });
4120
+ const game = requireGame(args, config);
4121
+ const profile = args.profile ?? "modless";
4122
+ const { plan, problems } = await resolvePlan({ game, profile, root: config, plugins, args });
4123
4123
  if (problems.length > 0)
4124
4124
  reportProblems(problems);
4125
4125
  const name = containerName(plan);
@@ -4187,8 +4187,8 @@ function renderVerify(name, info, plan, boundMods) {
4187
4187
  const stamps = boundMods.map((mod) => mod.assembly === null ? "no assemblies" : `${mod.assembly.path} ${ago(mod.assembly.mtimeMs)}`);
4188
4188
  const stampWidth = Math.max(...stamps.map((s) => s.length));
4189
4189
  for (const [i, mod] of boundMods.entries()) {
4190
- const origin2 = mod.branch === null ? "" : `worktree ${mod.branch}`;
4191
- out.push(` ${mod.packageId.padEnd(idWidth)} ${paths[i].padEnd(pathWidth)} ${origin2}`.trimEnd());
4190
+ const origin = mod.branch === null ? "" : `worktree ${mod.branch}`;
4191
+ out.push(` ${mod.packageId.padEnd(idWidth)} ${paths[i].padEnd(pathWidth)} ${origin}`.trimEnd());
4192
4192
  out.push(` ${" ".repeat(idWidth)} ${stamps[i].padEnd(stampWidth)} ${boundStatus(mod)}`);
4193
4193
  }
4194
4194
  return out.join(`
@@ -4199,15 +4199,15 @@ function shortenHome(path) {
4199
4199
  return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
4200
4200
  }
4201
4201
  async function clean(args, config) {
4202
- const game2 = requireGame(args, config);
4203
- const profile2 = args.profile;
4204
- if (profile2 === undefined)
4202
+ const game = requireGame(args, config);
4203
+ const profile = args.profile;
4204
+ if (profile === undefined)
4205
4205
  throw new GamecrateError("clean needs a profile", Exit.Usage);
4206
- const dir = profileDataDir(config, game2, profile2);
4207
- const tier2 = args.cleanTier ?? "staging";
4208
- const saveSuffixes = config.games[game2].saveExtensions.map((ext) => `.${ext.replace(/^\./, "")}`.toLowerCase());
4209
- if (tier2 !== "all") {
4210
- const target = join14(instanceDir(args, config, game2, profile2), tier2 === "logs" ? "logs" : ".stage");
4206
+ const dir = profileDataDir(config, game, profile);
4207
+ const tier = args.cleanTier ?? "staging";
4208
+ const saveSuffixes = config.games[game].saveExtensions.map((ext) => `.${ext.replace(/^\./, "")}`.toLowerCase());
4209
+ if (tier !== "all") {
4210
+ const target = join14(instanceDir(args, config, game, profile), tier === "logs" ? "logs" : ".stage");
4211
4211
  await rm2(target, { recursive: true, force: true });
4212
4212
  status(`removed ${target}`);
4213
4213
  return Exit.Ok;
@@ -4241,13 +4241,13 @@ async function countSaves(dir, suffixes) {
4241
4241
  return count;
4242
4242
  }
4243
4243
  async function clone(args, config) {
4244
- const game2 = requireGame(args, config);
4244
+ const game = requireGame(args, config);
4245
4245
  const [src, dst] = args.rest;
4246
4246
  if (src === undefined || dst === undefined) {
4247
4247
  throw new GamecrateError("clone needs a source and a destination profile", Exit.Usage);
4248
4248
  }
4249
- const from = join14(profileDataDir(config, game2, src), "game");
4250
- const to = join14(profileDataDir(config, game2, dst), "game");
4249
+ const from = join14(profileDataDir(config, game, src), "game");
4250
+ const to = join14(profileDataDir(config, game, dst), "game");
4251
4251
  if (!existsSync7(from))
4252
4252
  throw new GamecrateError(`${from} does not exist`, Exit.Usage);
4253
4253
  if (existsSync7(to) && !args.yes) {
@@ -4261,9 +4261,9 @@ async function clone(args, config) {
4261
4261
  return Exit.Ok;
4262
4262
  }
4263
4263
  async function build(args, config) {
4264
- const game2 = requireGame(args, config);
4265
- await acquireImage(game2, config.games[game2], args.pull ?? "always");
4266
- status(`${config.games[game2].image.ref} is ready`);
4264
+ const game = requireGame(args, config);
4265
+ await acquireImage(game, config.games[game], args.pull ?? "always");
4266
+ status(`${config.games[game].image.ref} is ready`);
4267
4267
  return Exit.Ok;
4268
4268
  }
4269
4269
  async function configEdit(args) {
@@ -4286,16 +4286,16 @@ async function configEdit(args) {
4286
4286
  return Exit.Ok;
4287
4287
  }
4288
4288
  async function fixPerms(args, config) {
4289
- const game2 = requireGame(args, config);
4289
+ const game = requireGame(args, config);
4290
4290
  const identity = resolveIdentity(false);
4291
4291
  const found = [];
4292
- for (const dir of await profileDirs(config, game2, args.profile)) {
4292
+ for (const dir of await profileDirs(config, game, args.profile)) {
4293
4293
  if (!existsSync7(dir))
4294
4294
  continue;
4295
4295
  found.push(...await detectForeignOwnership(dir, identity.uid, 1e4));
4296
4296
  }
4297
4297
  if (found.length === 0) {
4298
- status(`${game2}: every path is owned by uid ${identity.uid}`);
4298
+ status(`${game}: every path is owned by uid ${identity.uid}`);
4299
4299
  return Exit.Ok;
4300
4300
  }
4301
4301
  if (args.dryRun || !args.yes) {