@gamecrate/cli 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gamecrate.js +246 -246
- package/dist/lib.js +296 -0
- package/dist/types/cli/args.d.ts +40 -0
- package/dist/types/cli/help.d.ts +7 -0
- package/dist/types/cli/output.d.ts +56 -0
- package/dist/types/config/builtin.d.ts +3 -0
- package/dist/types/config/jsonc.d.ts +5 -0
- package/dist/types/config/load.d.ts +45 -0
- package/dist/types/config/validate.d.ts +12 -0
- package/dist/types/docker/identity.d.ts +6 -0
- package/dist/types/docker/preflight.d.ts +3 -0
- package/dist/types/docker/run.d.ts +37 -0
- package/dist/types/docker/spec.d.ts +24 -0
- package/dist/types/docker/window.d.ts +21 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/launch/generate.d.ts +8 -0
- package/dist/types/launch/instance.d.ts +20 -0
- package/dist/types/launch/prepare.d.ts +44 -0
- package/dist/types/launch/resolve.d.ts +17 -0
- package/dist/types/launch/stage.d.ts +13 -0
- package/dist/types/lib.d.ts +3 -0
- package/dist/types/mods/modindex.d.ts +29 -0
- package/dist/types/mods/staleness.d.ts +28 -0
- package/dist/types/mods/worktree.d.ts +18 -0
- package/dist/types/plugin.d.ts +35 -0
- package/dist/types/types.d.ts +394 -0
- package/package.json +15 -10
- package/src/cli/args.ts +0 -592
- package/src/cli/help.ts +0 -193
- package/src/cli/output.ts +0 -246
- package/src/config/builtin.ts +0 -19
- package/src/config/jsonc.ts +0 -21
- package/src/config/load.ts +0 -387
- package/src/config/validate.ts +0 -0
- package/src/docker/identity.ts +0 -25
- package/src/docker/preflight.ts +0 -243
- package/src/docker/run.ts +0 -212
- package/src/docker/spec.ts +0 -357
- package/src/docker/window.ts +0 -152
- package/src/index.ts +0 -875
- package/src/launch/generate.ts +0 -151
- package/src/launch/instance.ts +0 -106
- package/src/launch/prepare.ts +0 -332
- package/src/launch/resolve.ts +0 -383
- package/src/launch/stage.ts +0 -97
- package/src/lib.ts +0 -22
- package/src/mods/modindex.ts +0 -539
- package/src/mods/staleness.ts +0 -125
- package/src/mods/worktree.ts +0 -107
- package/src/plugin.ts +0 -152
- package/src/types.ts +0 -423
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
|
|
1064
|
-
if (
|
|
1065
|
-
return `${
|
|
1066
|
-
if (
|
|
1067
|
-
return `${Math.floor(
|
|
1068
|
-
if (
|
|
1069
|
-
return `${Math.floor(
|
|
1070
|
-
return `${Math.floor(
|
|
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((
|
|
1579
|
-
const
|
|
1580
|
-
where: `${prefix}${pointer(issue.path)}/${esc(
|
|
1581
|
-
message: `unknown key "${
|
|
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
|
|
1584
|
-
if (
|
|
1585
|
-
|
|
1586
|
-
problems.push(
|
|
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(
|
|
1843
|
+
function resolveSettings(root, game, profile, ...overrides) {
|
|
1844
1844
|
let out = structuredClone(DEFAULT_SETTINGS);
|
|
1845
|
-
for (const layer of [
|
|
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(
|
|
1852
|
-
return resolveNamed(
|
|
1851
|
+
function resolveProfile(game, name) {
|
|
1852
|
+
return resolveNamed(game, name, []);
|
|
1853
1853
|
}
|
|
1854
|
-
function resolveNamed(
|
|
1854
|
+
function resolveNamed(game, name, seen) {
|
|
1855
1855
|
if (name.toLowerCase() === "modless")
|
|
1856
1856
|
return { mods: [], exclude: [], includeBase: false };
|
|
1857
|
-
const key = profileKey(
|
|
1857
|
+
const key = profileKey(game, name);
|
|
1858
1858
|
if (key === undefined) {
|
|
1859
|
-
throw new GamecrateError(`unknown profile "${name}"`, Exit.Resolution, `known profiles: ${Object.keys(
|
|
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(
|
|
1864
|
+
const self = own(game.profiles, key);
|
|
1865
1865
|
if (self.alias !== undefined)
|
|
1866
|
-
return resolveNamed(
|
|
1867
|
-
const parent = self.extends !== undefined ? resolveNamed(
|
|
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(
|
|
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(
|
|
1891
|
+
const key = profileKey(game, current);
|
|
1892
1892
|
if (key === undefined || seen.includes(key))
|
|
1893
1893
|
return key ?? current;
|
|
1894
|
-
const next = own(
|
|
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(
|
|
1902
|
-
return resolve3(expandHome(
|
|
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(
|
|
1905
|
-
if (
|
|
1906
|
-
return [profileDataDir(
|
|
1907
|
-
const dir = join5(expandHome(
|
|
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(
|
|
1911
|
-
if (Object.hasOwn(
|
|
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(
|
|
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(
|
|
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
|
|
1959
|
-
if (
|
|
1960
|
-
|
|
1961
|
-
if (
|
|
1962
|
-
|
|
1963
|
-
if (
|
|
1964
|
-
|
|
1965
|
-
for (const
|
|
1966
|
-
|
|
1967
|
-
for (const entry of Object.values(
|
|
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((
|
|
2014
|
+
return new Promise((resolve, reject) => {
|
|
2015
2015
|
proc.once("error", reject);
|
|
2016
|
-
proc.once("close", (code) =>
|
|
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
|
|
2061
|
-
return interrupted ? Exit.Interrupted :
|
|
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((
|
|
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
|
|
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 (
|
|
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,
|
|
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 ${
|
|
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
|
|
2359
|
+
for (const line of stdout.split(`
|
|
2360
2360
|
`)) {
|
|
2361
|
-
const [id, , wmClass] =
|
|
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(
|
|
2455
|
-
const host =
|
|
2456
|
-
if (
|
|
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(
|
|
2467
|
-
if (
|
|
2466
|
+
async function readKnownExpansions(game, plugin, warnings) {
|
|
2467
|
+
if (game.dlc.length === 0)
|
|
2468
2468
|
return [];
|
|
2469
|
-
const host =
|
|
2470
|
-
if (
|
|
2471
|
-
return [...
|
|
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 [...
|
|
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,
|
|
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() !==
|
|
2490
|
+
if (id !== null && id.toLowerCase() !== game.core.toLowerCase())
|
|
2491
2491
|
found.push(id);
|
|
2492
2492
|
}
|
|
2493
|
-
const order =
|
|
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
|
|
2502
|
-
const target = join8(plan.dataDirHost,
|
|
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(
|
|
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([
|
|
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(
|
|
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
|
|
2540
|
-
const
|
|
2539
|
+
const { settings } = plan;
|
|
2540
|
+
const bool = (value) => value ? "True" : "False";
|
|
2541
2541
|
const owned = {
|
|
2542
|
-
screenWidth: String(
|
|
2543
|
-
screenHeight: String(
|
|
2544
|
-
devMode:
|
|
2545
|
-
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,
|
|
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(
|
|
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(`${
|
|
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,
|
|
2676
|
-
if (
|
|
2675
|
+
async function buildLocalMods(plan, policy) {
|
|
2676
|
+
if (policy === "never")
|
|
2677
2677
|
return;
|
|
2678
|
-
const wanted = plan.mods.filter((m) => m.kind === "local" && (
|
|
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
|
|
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, `${
|
|
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(
|
|
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
|
|
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
|
|
2872
|
-
const configured = lookup(
|
|
2873
|
-
const
|
|
2874
|
-
const { requests, problems } = collectRequests(
|
|
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(
|
|
2885
|
-
const 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(
|
|
2909
|
-
const body = basename5(
|
|
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(
|
|
2954
|
-
const base = resolvePath(expandHome(
|
|
2955
|
-
const 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 >=
|
|
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(
|
|
3006
|
-
const host =
|
|
3007
|
-
if (
|
|
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,
|
|
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(
|
|
3027
|
-
if (
|
|
3026
|
+
function workshopStamp(game) {
|
|
3027
|
+
if (game.workshopRoot === null)
|
|
3028
3028
|
return null;
|
|
3029
|
-
const acf = join11(dirname4(dirname4(resolvePath(expandHome(
|
|
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(
|
|
3037
|
+
async function readWorkshopCache(game, stamp) {
|
|
3038
3038
|
try {
|
|
3039
|
-
const raw = JSON.parse(await readFile4(join11(cacheDir(), `${
|
|
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(
|
|
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(), `${
|
|
3051
|
+
await writeFile3(join11(cacheDir(), `${game}.workshop.json`), JSON.stringify(payload));
|
|
3052
3052
|
} catch {}
|
|
3053
3053
|
}
|
|
3054
|
-
function toRecord(candidate, manifest,
|
|
3055
|
-
const kind = manifest.packageId.toLowerCase() ===
|
|
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(
|
|
3196
|
+
async function buildIndex(game, config, plugin) {
|
|
3197
3197
|
const index = {
|
|
3198
|
-
game
|
|
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,
|
|
3208
|
-
await scanLocalRoot(
|
|
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(
|
|
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(
|
|
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,
|
|
3249
|
-
return honorOverride(index, resolveRaw(index, ref,
|
|
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,
|
|
3257
|
+
function resolveRaw(index, ref, game) {
|
|
3258
3258
|
if (ref.startsWith("path:"))
|
|
3259
|
-
return byPath(index, ref.slice(5),
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
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(
|
|
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 (
|
|
3367
|
+
for (const [i, id] of (game.preCore ?? []).entries())
|
|
3368
3368
|
slots.push({ entry: id, where: `${at}/preCore/${i}` });
|
|
3369
|
-
slots.push({ entry:
|
|
3370
|
-
for (const [i, id] of
|
|
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 &&
|
|
3373
|
-
for (const [i, id] of (
|
|
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 :
|
|
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,
|
|
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,
|
|
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
|
|
3497
|
+
const { game: gameName, profile: requestedProfile, root } = options;
|
|
3498
3498
|
const args = options.args ?? {};
|
|
3499
3499
|
const problems = [];
|
|
3500
3500
|
const warnings = [];
|
|
3501
|
-
const
|
|
3502
|
-
if (!
|
|
3503
|
-
throw new GamecrateError(`unknown game "${gameName}"`, Exit.Config, `known: ${Object.keys(
|
|
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(
|
|
3509
|
-
const
|
|
3510
|
-
const profileDir = profileDataDir(
|
|
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
|
|
3513
|
+
profile,
|
|
3514
3514
|
args,
|
|
3515
3515
|
...options.cwd === undefined ? {} : { cwd: options.cwd }
|
|
3516
3516
|
});
|
|
3517
3517
|
problems.push(...instance.problems);
|
|
3518
|
-
const
|
|
3518
|
+
const settings = resolveSettings(root, game, profile, instance.settings, args.resolution);
|
|
3519
3519
|
if (args.network !== undefined)
|
|
3520
|
-
|
|
3520
|
+
settings.network = args.network;
|
|
3521
3521
|
if (args.gameArgs?.length)
|
|
3522
|
-
|
|
3522
|
+
settings.gameArgs = [...settings.gameArgs ?? [], ...args.gameArgs];
|
|
3523
3523
|
if (args.dockerArgs?.length)
|
|
3524
|
-
|
|
3524
|
+
settings.dockerArgs = [...settings.dockerArgs ?? [], ...args.dockerArgs];
|
|
3525
3525
|
const plugin = requirePlugin(options.plugins, gameName);
|
|
3526
|
-
const index = options.index ?? await buildIndex(gameName,
|
|
3527
|
-
await applyWorktreeRequests(index, instance.requests,
|
|
3528
|
-
problems.push(...await applySourceOverrides(index, args.use ?? [],
|
|
3529
|
-
const
|
|
3530
|
-
const isExcluded = (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(
|
|
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,
|
|
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,
|
|
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 (
|
|
3555
|
-
insertDependencies(staged, present, index,
|
|
3556
|
-
const ordered = args.sort === "none" ? staged : topoSort(staged, problems,
|
|
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: `${
|
|
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 (
|
|
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: `"${
|
|
3583
|
+
message: `"${game.dataDir.arg}" must contain exactly one "="; RimWorld silently ignores anything else`
|
|
3584
3584
|
});
|
|
3585
3585
|
}
|
|
3586
|
-
if (
|
|
3586
|
+
if (game.dataDir.container.includes("=")) {
|
|
3587
3587
|
problems.push({
|
|
3588
3588
|
where: `/games/${gameName}/dataDir/container`,
|
|
3589
|
-
message: `container data path "${
|
|
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 (!
|
|
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:
|
|
3598
|
+
gameConfig: game,
|
|
3599
3599
|
plugin,
|
|
3600
3600
|
profile: profileName,
|
|
3601
|
-
settings
|
|
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 = "
|
|
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
|
|
3774
|
-
if (
|
|
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,
|
|
3778
|
-
throw new GamecrateError(`unknown game "${
|
|
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
|
|
3780
|
+
return game;
|
|
3781
3781
|
}
|
|
3782
|
-
function instanceDir(args, config,
|
|
3783
|
-
const dir = profileDataDir(config,
|
|
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[
|
|
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
|
|
3807
|
-
const
|
|
3808
|
-
const index = await buildIndex(
|
|
3809
|
-
const { plan, problems } = await resolvePlan({ game
|
|
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
|
|
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 (
|
|
3821
|
-
return reportEnvironment(
|
|
3820
|
+
if (environment.length > 0)
|
|
3821
|
+
return reportEnvironment(environment);
|
|
3822
3822
|
if (!args.printPlan) {
|
|
3823
|
-
const what = plan.instance === undefined ?
|
|
3824
|
-
status(`${
|
|
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
|
|
3843
|
-
const
|
|
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 ${
|
|
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(
|
|
3854
|
-
const runtimeImage = plan.mode === "headed" ? config.games[
|
|
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
|
|
3985
|
+
const game = config.games[name];
|
|
3986
3986
|
return {
|
|
3987
3987
|
game: name,
|
|
3988
|
-
core:
|
|
3989
|
-
dlc:
|
|
3990
|
-
modes:
|
|
3991
|
-
profiles: Object.entries(
|
|
3992
|
-
profile
|
|
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
|
|
4007
|
-
const width = Math.max(7, ...Object.keys(
|
|
4008
|
-
out.push(`${name} (${
|
|
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 [
|
|
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(` ${
|
|
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
|
|
4034
|
-
const
|
|
4035
|
-
const index = await buildIndex(
|
|
4036
|
-
const { plan, problems } = await resolvePlan({ game
|
|
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
|
|
4045
|
-
const { plan, problems } = await resolvePlan({ game
|
|
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(`${
|
|
4048
|
+
status(`${game}: ok`);
|
|
4049
4049
|
continue;
|
|
4050
4050
|
}
|
|
4051
4051
|
failed = true;
|
|
4052
|
-
status(`${
|
|
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
|
|
4066
|
-
const
|
|
4067
|
-
const runs = join14(instanceDir(args, config,
|
|
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 ${
|
|
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
|
|
4082
|
+
for (const line of text.split(`
|
|
4083
4083
|
`)) {
|
|
4084
|
-
if (
|
|
4085
|
-
process.stdout.write(`${name}: ${
|
|
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
|
|
4121
|
-
const
|
|
4122
|
-
const { plan, problems } = await resolvePlan({ game
|
|
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
|
|
4191
|
-
out.push(` ${mod.packageId.padEnd(idWidth)} ${paths[i].padEnd(pathWidth)} ${
|
|
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
|
|
4203
|
-
const
|
|
4204
|
-
if (
|
|
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,
|
|
4207
|
-
const
|
|
4208
|
-
const saveSuffixes = config.games[
|
|
4209
|
-
if (
|
|
4210
|
-
const target = join14(instanceDir(args, config,
|
|
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
|
|
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,
|
|
4250
|
-
const to = join14(profileDataDir(config,
|
|
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
|
|
4265
|
-
await acquireImage(
|
|
4266
|
-
status(`${config.games[
|
|
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
|
|
4289
|
+
const game = requireGame(args, config);
|
|
4290
4290
|
const identity = resolveIdentity(false);
|
|
4291
4291
|
const found = [];
|
|
4292
|
-
for (const dir of await profileDirs(config,
|
|
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(`${
|
|
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) {
|