@edgemodders/edgedeck-mcp 0.27.3 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/bin/edgedeck.mjs +10 -5
- package/dist/core.js +491 -143
- package/dist/mcp.js +256 -52
- package/package.json +1 -1
package/dist/core.js
CHANGED
|
@@ -1250,8 +1250,8 @@ var require_semaphore = __commonJS({
|
|
|
1250
1250
|
this._waiting = [];
|
|
1251
1251
|
}
|
|
1252
1252
|
lock(thunk) {
|
|
1253
|
-
return new Promise((
|
|
1254
|
-
this._waiting.push({ thunk, resolve:
|
|
1253
|
+
return new Promise((resolve5, reject) => {
|
|
1254
|
+
this._waiting.push({ thunk, resolve: resolve5, reject });
|
|
1255
1255
|
this.runNext();
|
|
1256
1256
|
});
|
|
1257
1257
|
}
|
|
@@ -2877,9 +2877,9 @@ ${JSON.stringify(message, null, 4)}`);
|
|
|
2877
2877
|
if (typeof cancellationStrategy.sender.enableCancellation === "function") {
|
|
2878
2878
|
cancellationStrategy.sender.enableCancellation(requestMessage);
|
|
2879
2879
|
}
|
|
2880
|
-
return new Promise(async (
|
|
2880
|
+
return new Promise(async (resolve5, reject) => {
|
|
2881
2881
|
const resolveWithCleanup = (r) => {
|
|
2882
|
-
|
|
2882
|
+
resolve5(r);
|
|
2883
2883
|
cancellationStrategy.sender.cleanup(id);
|
|
2884
2884
|
disposable?.dispose();
|
|
2885
2885
|
};
|
|
@@ -3308,10 +3308,10 @@ var require_ril = __commonJS({
|
|
|
3308
3308
|
return api_1.Disposable.create(() => this.stream.off("end", listener));
|
|
3309
3309
|
}
|
|
3310
3310
|
write(data, encoding) {
|
|
3311
|
-
return new Promise((
|
|
3311
|
+
return new Promise((resolve5, reject) => {
|
|
3312
3312
|
const callback = (error) => {
|
|
3313
3313
|
if (error === void 0 || error === null) {
|
|
3314
|
-
|
|
3314
|
+
resolve5();
|
|
3315
3315
|
} else {
|
|
3316
3316
|
reject(error);
|
|
3317
3317
|
}
|
|
@@ -3447,9 +3447,9 @@ var require_main = __commonJS({
|
|
|
3447
3447
|
exports2.createMessageConnection = createMessageConnection2;
|
|
3448
3448
|
var ril_1 = __importDefault(require_ril());
|
|
3449
3449
|
ril_1.default.install();
|
|
3450
|
-
var
|
|
3450
|
+
var path15 = __importStar(require("path"));
|
|
3451
3451
|
var os4 = __importStar(require("os"));
|
|
3452
|
-
var
|
|
3452
|
+
var fs16 = __importStar(require("fs"));
|
|
3453
3453
|
var crypto_1 = require("crypto");
|
|
3454
3454
|
var net_1 = require("net");
|
|
3455
3455
|
var api_1 = require_api();
|
|
@@ -3590,7 +3590,7 @@ var require_main = __commonJS({
|
|
|
3590
3590
|
}
|
|
3591
3591
|
let randomLength = 32;
|
|
3592
3592
|
const fixedLength = "/lsp-.sock".length;
|
|
3593
|
-
const tmpDir =
|
|
3593
|
+
const tmpDir = fs16.realpathSync(XDG_RUNTIME_DIR ?? os4.tmpdir());
|
|
3594
3594
|
const limit = safeIpcPathLengths.get(process.platform);
|
|
3595
3595
|
if (limit !== void 0) {
|
|
3596
3596
|
randomLength = Math.min(limit - tmpDir.length - fixedLength, randomLength);
|
|
@@ -3599,14 +3599,14 @@ var require_main = __commonJS({
|
|
|
3599
3599
|
throw new Error(`Unable to generate a random pipe name with ${randomLength} characters.`);
|
|
3600
3600
|
}
|
|
3601
3601
|
const randomSuffix = (0, crypto_1.randomBytes)(Math.floor(randomLength / 2)).toString("hex");
|
|
3602
|
-
return
|
|
3602
|
+
return path15.join(tmpDir, `lsp-${randomSuffix}.sock`);
|
|
3603
3603
|
}
|
|
3604
3604
|
function createClientPipeTransport(pipeName, encoding = "utf-8") {
|
|
3605
3605
|
let connectResolve;
|
|
3606
|
-
const connected = new Promise((
|
|
3607
|
-
connectResolve =
|
|
3606
|
+
const connected = new Promise((resolve5, _reject) => {
|
|
3607
|
+
connectResolve = resolve5;
|
|
3608
3608
|
});
|
|
3609
|
-
return new Promise((
|
|
3609
|
+
return new Promise((resolve5, reject) => {
|
|
3610
3610
|
const server = (0, net_1.createServer)((socket) => {
|
|
3611
3611
|
server.close();
|
|
3612
3612
|
connectResolve([
|
|
@@ -3617,7 +3617,7 @@ var require_main = __commonJS({
|
|
|
3617
3617
|
server.on("error", reject);
|
|
3618
3618
|
server.listen(pipeName, () => {
|
|
3619
3619
|
server.removeListener("error", reject);
|
|
3620
|
-
|
|
3620
|
+
resolve5({
|
|
3621
3621
|
onConnected: () => {
|
|
3622
3622
|
return connected;
|
|
3623
3623
|
}
|
|
@@ -3634,10 +3634,10 @@ var require_main = __commonJS({
|
|
|
3634
3634
|
}
|
|
3635
3635
|
function createClientSocketTransport(port, encoding = "utf-8") {
|
|
3636
3636
|
let connectResolve;
|
|
3637
|
-
const connected = new Promise((
|
|
3638
|
-
connectResolve =
|
|
3637
|
+
const connected = new Promise((resolve5, _reject) => {
|
|
3638
|
+
connectResolve = resolve5;
|
|
3639
3639
|
});
|
|
3640
|
-
return new Promise((
|
|
3640
|
+
return new Promise((resolve5, reject) => {
|
|
3641
3641
|
const server = (0, net_1.createServer)((socket) => {
|
|
3642
3642
|
server.close();
|
|
3643
3643
|
connectResolve([
|
|
@@ -3654,7 +3654,7 @@ var require_main = __commonJS({
|
|
|
3654
3654
|
return;
|
|
3655
3655
|
}
|
|
3656
3656
|
const boundPort = address.port;
|
|
3657
|
-
|
|
3657
|
+
resolve5({
|
|
3658
3658
|
port: () => boundPort,
|
|
3659
3659
|
onConnected: () => {
|
|
3660
3660
|
return connected;
|
|
@@ -3700,6 +3700,7 @@ __export(core_exports, {
|
|
|
3700
3700
|
BridgeClient: () => BridgeClient,
|
|
3701
3701
|
BridgeRepo: () => BridgeRepo,
|
|
3702
3702
|
DefaultLoadout: () => DefaultLoadout,
|
|
3703
|
+
FrameworkSources: () => FrameworkSources,
|
|
3703
3704
|
FrameworksSection: () => FrameworksSection,
|
|
3704
3705
|
KnownModNames: () => KnownModNames,
|
|
3705
3706
|
ManifestName: () => ManifestName,
|
|
@@ -3737,8 +3738,10 @@ __export(core_exports, {
|
|
|
3737
3738
|
deleteLoadout: () => deleteLoadout,
|
|
3738
3739
|
deleteSection: () => deleteSection,
|
|
3739
3740
|
deploy: () => deploy,
|
|
3741
|
+
deployMethod: () => deployMethod,
|
|
3740
3742
|
deployRedmod: () => deployRedmod,
|
|
3741
3743
|
deployReportsFolder: () => deployReportsFolder,
|
|
3744
|
+
deployedRedmodPackages: () => deployedRedmodPackages,
|
|
3742
3745
|
describeHandler: () => describeHandler,
|
|
3743
3746
|
detectChanges: () => detectChanges,
|
|
3744
3747
|
dotnet10Root: () => dotnet10Root,
|
|
@@ -3767,12 +3770,15 @@ __export(core_exports, {
|
|
|
3767
3770
|
formatBytes: () => formatBytes,
|
|
3768
3771
|
forwarderCommand: () => forwarderCommand,
|
|
3769
3772
|
frameworkDetail: () => frameworkDetail,
|
|
3773
|
+
frameworkFamily: () => frameworkFamily,
|
|
3774
|
+
frameworkStates: () => frameworkStates,
|
|
3770
3775
|
frameworkSummary: () => frameworkSummary,
|
|
3771
3776
|
gameExe: () => gameExe,
|
|
3772
3777
|
gameRunning: () => gameRunning,
|
|
3773
3778
|
gameVersion: () => gameVersion,
|
|
3774
3779
|
guessName: () => guessName,
|
|
3775
3780
|
hasWebSocket: () => hasWebSocket,
|
|
3781
|
+
holdsLibrary: () => holdsLibrary,
|
|
3776
3782
|
idFor: () => idFor,
|
|
3777
3783
|
importFromVortex: () => importFromVortex,
|
|
3778
3784
|
importLoadout: () => importLoadout,
|
|
@@ -3784,6 +3790,8 @@ __export(core_exports, {
|
|
|
3784
3790
|
isGameFolder: () => isGameFolder,
|
|
3785
3791
|
keepDownload: () => keepDownload,
|
|
3786
3792
|
latestBridgeRelease: () => latestBridgeRelease,
|
|
3793
|
+
latestFrameworkRelease: () => latestFrameworkRelease,
|
|
3794
|
+
launchTarget: () => launchTarget,
|
|
3787
3795
|
libraryFolder: () => libraryFolder,
|
|
3788
3796
|
librarySize: () => librarySize,
|
|
3789
3797
|
listLoadouts: () => listLoadouts,
|
|
@@ -3792,8 +3800,11 @@ __export(core_exports, {
|
|
|
3792
3800
|
loadoutNameProblem: () => loadoutNameProblem,
|
|
3793
3801
|
loadoutState: () => loadoutState,
|
|
3794
3802
|
loadoutsFolder: () => loadoutsFolder,
|
|
3803
|
+
lostFiles: () => lostFiles,
|
|
3795
3804
|
makeActive: () => makeActive,
|
|
3796
3805
|
md5Of: () => md5Of,
|
|
3806
|
+
missingFrameworks: () => missingFrameworks,
|
|
3807
|
+
modDeployPaths: () => modDeployPaths,
|
|
3797
3808
|
modFolder: () => modFolder,
|
|
3798
3809
|
modPageUrl: () => modPageUrl,
|
|
3799
3810
|
modSize: () => modSize,
|
|
@@ -3801,8 +3812,10 @@ __export(core_exports, {
|
|
|
3801
3812
|
modsRemoved: () => modsRemoved,
|
|
3802
3813
|
modsTouched: () => modsTouched,
|
|
3803
3814
|
moveInSection: () => moveInSection,
|
|
3815
|
+
moveLibrary: () => moveLibrary,
|
|
3804
3816
|
moveMod: () => moveMod,
|
|
3805
3817
|
moveSection: () => moveSection,
|
|
3818
|
+
moveTarget: () => moveTarget,
|
|
3806
3819
|
needsRefresh: () => needsRefresh,
|
|
3807
3820
|
newLoadout: () => newLoadout,
|
|
3808
3821
|
newestFile: () => newestFile,
|
|
@@ -3817,6 +3830,7 @@ __export(core_exports, {
|
|
|
3817
3830
|
placeAbove: () => placeAbove,
|
|
3818
3831
|
placeNextTo: () => placeNextTo,
|
|
3819
3832
|
plan: () => plan,
|
|
3833
|
+
projectMod: () => projectMod,
|
|
3820
3834
|
projectName: () => projectName,
|
|
3821
3835
|
putInSection: () => putInSection,
|
|
3822
3836
|
readLoadout: () => readLoadout,
|
|
@@ -3828,8 +3842,11 @@ __export(core_exports, {
|
|
|
3828
3842
|
registerNxmHandler: () => registerNxmHandler,
|
|
3829
3843
|
removeMod: () => removeMod,
|
|
3830
3844
|
removeOlderBridges: () => removeOlderBridges,
|
|
3845
|
+
renameFamily: () => renameFamily,
|
|
3831
3846
|
renameLoadout: () => renameLoadout,
|
|
3832
3847
|
renameSection: () => renameSection,
|
|
3848
|
+
sameDrive: () => sameDrive,
|
|
3849
|
+
sameNameMod: () => sameNameMod,
|
|
3833
3850
|
saveLibrary: () => saveLibrary,
|
|
3834
3851
|
saveLoadout: () => saveLoadout,
|
|
3835
3852
|
saveMod: () => saveMod,
|
|
@@ -3847,6 +3864,7 @@ __export(core_exports, {
|
|
|
3847
3864
|
steamLibraries: () => steamLibraries,
|
|
3848
3865
|
syncActiveLoadout: () => syncActiveLoadout,
|
|
3849
3866
|
syncProjects: () => syncProjects,
|
|
3867
|
+
takeFamilyName: () => takeFamilyName,
|
|
3850
3868
|
tokenUser: () => tokenUser,
|
|
3851
3869
|
undo: () => undo,
|
|
3852
3870
|
unpackOrCopy: () => unpackOrCopy,
|
|
@@ -4031,8 +4049,8 @@ var NexusClient = class {
|
|
|
4031
4049
|
get usesToken() {
|
|
4032
4050
|
return "bearer" in this.auth;
|
|
4033
4051
|
}
|
|
4034
|
-
async get(
|
|
4035
|
-
const res = await fetch(`${this.options.api ?? NexusApi}${
|
|
4052
|
+
async get(path15, retried = false) {
|
|
4053
|
+
const res = await fetch(`${this.options.api ?? NexusApi}${path15}`, {
|
|
4036
4054
|
headers: {
|
|
4037
4055
|
..."bearer" in this.auth ? { authorization: `Bearer ${this.auth.bearer}` } : { apikey: this.auth.apiKey },
|
|
4038
4056
|
accept: "application/json",
|
|
@@ -4049,7 +4067,7 @@ var NexusClient = class {
|
|
|
4049
4067
|
const bearer = await this.options.refresh();
|
|
4050
4068
|
if (bearer) {
|
|
4051
4069
|
this.auth = { bearer };
|
|
4052
|
-
return this.get(
|
|
4070
|
+
return this.get(path15, true);
|
|
4053
4071
|
}
|
|
4054
4072
|
}
|
|
4055
4073
|
if (!res.ok) {
|
|
@@ -4155,11 +4173,11 @@ async function installDotnetRuntime(log) {
|
|
|
4155
4173
|
try {
|
|
4156
4174
|
await download(DotnetInstallScript, script);
|
|
4157
4175
|
log(`Installing the .NET 10 runtime into ${root}`);
|
|
4158
|
-
await new Promise((
|
|
4176
|
+
await new Promise((resolve5, reject) => {
|
|
4159
4177
|
(0, import_node_child_process3.execFile)("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script, "-Channel", "10.0", "-Runtime", "dotnet", "-InstallDir", root, "-NoPath"], { windowsHide: true, maxBuffer: 1 << 24 }, (err, stdout, stderr) => {
|
|
4160
4178
|
for (const line of `${stdout}
|
|
4161
4179
|
${stderr}`.split(/\r?\n/)) if (line.trim()) log(` ${line.trim()}`);
|
|
4162
|
-
err ? reject(new Error(`dotnet-install failed: ${err.message}`)) :
|
|
4180
|
+
err ? reject(new Error(`dotnet-install failed: ${err.message}`)) : resolve5();
|
|
4163
4181
|
});
|
|
4164
4182
|
});
|
|
4165
4183
|
} finally {
|
|
@@ -4467,6 +4485,11 @@ function gameRunning() {
|
|
|
4467
4485
|
return false;
|
|
4468
4486
|
}
|
|
4469
4487
|
}
|
|
4488
|
+
function launchTarget(gamePath, how) {
|
|
4489
|
+
const steam = how === "steam" || how === "auto" && /[\\/]steamapps[\\/]common[\\/]/i.test(gamePath);
|
|
4490
|
+
if (steam) return { kind: "steam", url: "steam://run/1091500//-modded/" };
|
|
4491
|
+
return { kind: "exe", exe: gameExe(gamePath), args: ["-modded"] };
|
|
4492
|
+
}
|
|
4470
4493
|
function isGameFolder(gamePath) {
|
|
4471
4494
|
return !!gamePath && fs6.existsSync(gameExe(gamePath));
|
|
4472
4495
|
}
|
|
@@ -4686,6 +4709,21 @@ function canHardlink(libraryRoot, gamePath) {
|
|
|
4686
4709
|
return false;
|
|
4687
4710
|
}
|
|
4688
4711
|
}
|
|
4712
|
+
var hardlinkable = /* @__PURE__ */ new Map();
|
|
4713
|
+
function deployMethod(libraryRoot, gamePath, current) {
|
|
4714
|
+
try {
|
|
4715
|
+
if (fs8.statSync(libraryRoot).dev !== fs8.statSync(gamePath).dev) return "copy";
|
|
4716
|
+
} catch {
|
|
4717
|
+
return current ?? "copy";
|
|
4718
|
+
}
|
|
4719
|
+
if (current === "link") return "link";
|
|
4720
|
+
const k = `${path7.resolve(libraryRoot).toLowerCase()}|${path7.resolve(gamePath).toLowerCase()}`;
|
|
4721
|
+
if (!hardlinkable.has(k)) hardlinkable.set(k, canHardlink(libraryRoot, gamePath));
|
|
4722
|
+
return hardlinkable.get(k) ? "link" : "copy";
|
|
4723
|
+
}
|
|
4724
|
+
function otherMethod(entry, method) {
|
|
4725
|
+
return method === "link" !== !!entry.ino;
|
|
4726
|
+
}
|
|
4689
4727
|
function sourceReplaced(entry, method, d) {
|
|
4690
4728
|
if (d.content !== void 0) return false;
|
|
4691
4729
|
const src = identity(d.src);
|
|
@@ -4693,10 +4731,10 @@ function sourceReplaced(entry, method, d) {
|
|
|
4693
4731
|
if (method === "link" && entry.ino) return src.ino !== entry.ino;
|
|
4694
4732
|
return src.size !== entry.size || Math.abs(src.mtimeMs - entry.mtimeMs) >= 2e3;
|
|
4695
4733
|
}
|
|
4696
|
-
function stillOurs(entry,
|
|
4734
|
+
function stillOurs(entry, _method, gamePath) {
|
|
4697
4735
|
const id = identity(gameFile(gamePath, entry.path));
|
|
4698
4736
|
if (!id) return "missing";
|
|
4699
|
-
if (
|
|
4737
|
+
if (entry.ino) {
|
|
4700
4738
|
if (id.ino !== entry.ino) return "changed";
|
|
4701
4739
|
if (id.nlink < 2 && entry.mod !== Generated) return "sourceMissing";
|
|
4702
4740
|
return "ok";
|
|
@@ -4712,7 +4750,7 @@ function generatedDiffers(file, content) {
|
|
|
4712
4750
|
}
|
|
4713
4751
|
function plan(lib, gamePath) {
|
|
4714
4752
|
const manifest = readManifest(gamePath);
|
|
4715
|
-
const method =
|
|
4753
|
+
const method = deployMethod(lib.root, gamePath, manifest?.method);
|
|
4716
4754
|
const desired = /* @__PURE__ */ new Map();
|
|
4717
4755
|
const conflicts = /* @__PURE__ */ new Map();
|
|
4718
4756
|
for (const mod of lib.mods) {
|
|
@@ -4743,6 +4781,7 @@ function plan(lib, gamePath) {
|
|
|
4743
4781
|
if (!e || (d.mod?.id ?? Generated) !== e.mod) return true;
|
|
4744
4782
|
if (d.content !== void 0 && generatedDiffers(gameFile(gamePath, d.rel), d.content)) return true;
|
|
4745
4783
|
if (stillOurs(e, method, gamePath) !== "ok") return true;
|
|
4784
|
+
if (d.content === void 0 && otherMethod(e, method)) return true;
|
|
4746
4785
|
return sourceReplaced(e, method, d);
|
|
4747
4786
|
});
|
|
4748
4787
|
return { method, desired: [...desired.values()], conflicts: [...conflicts.values()], stale, add, vortex: vortexManages(gamePath) };
|
|
@@ -4757,6 +4796,18 @@ function detectChanges(gamePath) {
|
|
|
4757
4796
|
}
|
|
4758
4797
|
return changes;
|
|
4759
4798
|
}
|
|
4799
|
+
function lostFiles(p) {
|
|
4800
|
+
const out = /* @__PURE__ */ new Map();
|
|
4801
|
+
for (const c of p.conflicts) {
|
|
4802
|
+
for (const loser of c.losers) {
|
|
4803
|
+
const k = `${loser}\0${c.winner}`;
|
|
4804
|
+
const l = out.get(k) ?? { mod: loser, files: 0, winner: c.winner };
|
|
4805
|
+
l.files++;
|
|
4806
|
+
out.set(k, l);
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
return [...out.values()];
|
|
4810
|
+
}
|
|
4760
4811
|
function modsTouched(result) {
|
|
4761
4812
|
return [...new Set(result.changes.filter((c) => c.kind !== "removed" && c.mod !== Generated).map((c) => c.mod))];
|
|
4762
4813
|
}
|
|
@@ -4849,28 +4900,47 @@ function cleanDirs(gamePath, manifest) {
|
|
|
4849
4900
|
}
|
|
4850
4901
|
manifest.dirs = manifest.dirs.filter((d) => fs8.existsSync(path7.join(gamePath, ...d.split("/"))));
|
|
4851
4902
|
}
|
|
4852
|
-
function
|
|
4903
|
+
function modDeployPaths(gamePath, p, id) {
|
|
4904
|
+
const paths = new Set((readManifest(gamePath)?.files ?? []).filter((e) => e.mod === id).map((e) => key(e.path)));
|
|
4905
|
+
for (const d of p.desired) if (d.mod?.id === id) paths.add(key(d.rel));
|
|
4906
|
+
return paths;
|
|
4907
|
+
}
|
|
4908
|
+
function modlistFor(lib, files) {
|
|
4909
|
+
const rank = new Map(lib.mods.map((m, i) => [m.id, i]));
|
|
4910
|
+
const archives = files.filter((e) => e.mod !== Generated && e.path.toLowerCase().startsWith("archive/pc/mod/") && e.path.toLowerCase().endsWith(".archive")).sort((a, b) => (rank.get(a.mod) ?? 1e9) - (rank.get(b.mod) ?? 1e9) || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
4911
|
+
return archives.length ? archives.map((e) => path7.posix.basename(e.path)).join("\r\n") + "\r\n" : "";
|
|
4912
|
+
}
|
|
4913
|
+
function deploy(lib, gamePath, decisions = /* @__PURE__ */ new Map(), progress, gameVersion2, only) {
|
|
4853
4914
|
if (vortexManages(gamePath)) throw new Error("Vortex manages this game folder. Purge in Vortex first (Mods > Purge), then deploy with EDGEdeck.");
|
|
4854
4915
|
if (!isGameFolder(gamePath)) throw new Error(`${gamePath} does not look like the Cyberpunk 2077 folder (no bin\\x64\\Cyberpunk2077.exe).`);
|
|
4855
4916
|
const p = plan(lib, gamePath);
|
|
4856
4917
|
const manifest = readManifest(gamePath) ?? { version: 1, library: lib.root, gamePath, method: p.method, time: "", files: [], dirs: [] };
|
|
4857
4918
|
manifest.library = lib.root;
|
|
4858
|
-
|
|
4919
|
+
manifest.method = p.method;
|
|
4920
|
+
const result = { linked: 0, removed: 0, kept: 0, backedUp: 0, method: manifest.method, skipped: [], changes: [], modsInGame: 0, lost: lostFiles(p) };
|
|
4859
4921
|
const entries = new Map(manifest.files.map((e) => [key(e.path), e]));
|
|
4922
|
+
const scope = only ? modDeployPaths(gamePath, p, only) : void 0;
|
|
4923
|
+
const inScope = (rel) => !scope || scope.has(key(rel));
|
|
4924
|
+
const stale = p.stale.filter((e) => inScope(e.path) && !(scope && e.mod === Generated));
|
|
4925
|
+
const add = p.add.filter((d) => inScope(d.rel) && !(scope && d.content !== void 0));
|
|
4926
|
+
if (scope) {
|
|
4927
|
+
const waiting = new Set([...p.stale.map((e) => e.path), ...p.add.map((d) => d.rel)].filter((r) => !scope.has(key(r)) && key(r) !== key(ModListPath)).map(key));
|
|
4928
|
+
result.waiting = waiting.size;
|
|
4929
|
+
}
|
|
4860
4930
|
for (const [rel, decision] of decisions) {
|
|
4861
4931
|
const d = p.desired.find((x) => key(x.rel) === key(rel));
|
|
4862
4932
|
if (decision === "keep" && d?.mod) {
|
|
4863
4933
|
fs8.copyFileSync(gameFile(gamePath, rel), d.src);
|
|
4864
4934
|
}
|
|
4865
4935
|
}
|
|
4866
|
-
const total =
|
|
4936
|
+
const total = stale.length + add.length;
|
|
4867
4937
|
let done = 0;
|
|
4868
4938
|
const save = () => {
|
|
4869
4939
|
manifest.time = (/* @__PURE__ */ new Date()).toISOString();
|
|
4870
4940
|
if (gameVersion2) manifest.gameVersion = gameVersion2;
|
|
4871
4941
|
writeManifest(gamePath, manifest);
|
|
4872
4942
|
};
|
|
4873
|
-
|
|
4943
|
+
const takeOut = (entry) => {
|
|
4874
4944
|
const decided = decisions.get(entry.path);
|
|
4875
4945
|
if (removeDeployed(lib, gamePath, manifest, entry, decided !== void 0, result.skipped)) {
|
|
4876
4946
|
entries.delete(key(entry.path));
|
|
@@ -4878,10 +4948,8 @@ function deploy(lib, gamePath, decisions = /* @__PURE__ */ new Map(), progress,
|
|
|
4878
4948
|
result.changes.push({ path: entry.path, mod: entry.mod, kind: "removed", ...entry.backup ? { backup: true } : {} });
|
|
4879
4949
|
}
|
|
4880
4950
|
manifest.files = [...entries.values()];
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
if (p.stale.length) cleanDirs(gamePath, manifest);
|
|
4884
|
-
for (const d of p.add) {
|
|
4951
|
+
};
|
|
4952
|
+
const putIn = (d) => {
|
|
4885
4953
|
const target = gameFile(gamePath, d.rel);
|
|
4886
4954
|
const existing = entries.get(key(d.rel));
|
|
4887
4955
|
let backup = existing?.backup ?? false;
|
|
@@ -4889,7 +4957,7 @@ function deploy(lib, gamePath, decisions = /* @__PURE__ */ new Map(), progress,
|
|
|
4889
4957
|
const state = stillOurs(existing, manifest.method, gamePath);
|
|
4890
4958
|
if (state === "changed" && !decisions.has(d.rel)) {
|
|
4891
4959
|
result.skipped.push(d.rel);
|
|
4892
|
-
|
|
4960
|
+
return;
|
|
4893
4961
|
}
|
|
4894
4962
|
fs8.rmSync(target, { force: true });
|
|
4895
4963
|
} else if (fs8.existsSync(target)) {
|
|
@@ -4915,10 +4983,29 @@ function deploy(lib, gamePath, decisions = /* @__PURE__ */ new Map(), progress,
|
|
|
4915
4983
|
manifest.files = [...entries.values()];
|
|
4916
4984
|
result.linked++;
|
|
4917
4985
|
result.changes.push({ path: d.rel, mod: entry.mod, kind: existing ? "replaced" : "added", ...backup && !existing ? { backup: true } : {} });
|
|
4986
|
+
};
|
|
4987
|
+
for (const entry of stale) {
|
|
4988
|
+
takeOut(entry);
|
|
4989
|
+
progress?.(++done, total);
|
|
4990
|
+
}
|
|
4991
|
+
if (stale.length) cleanDirs(gamePath, manifest);
|
|
4992
|
+
for (const d of add) {
|
|
4993
|
+
putIn(d);
|
|
4918
4994
|
progress?.(++done, total);
|
|
4919
4995
|
if (done % 200 === 0) save();
|
|
4920
4996
|
}
|
|
4921
|
-
|
|
4997
|
+
if (scope) {
|
|
4998
|
+
const content = modlistFor(lib, manifest.files);
|
|
4999
|
+
const e = entries.get(key(ModListPath));
|
|
5000
|
+
if (!content && e) {
|
|
5001
|
+
takeOut(e);
|
|
5002
|
+
cleanDirs(gamePath, manifest);
|
|
5003
|
+
} else if (content && (!e || generatedDiffers(gameFile(gamePath, ModListPath), content))) {
|
|
5004
|
+
putIn({ rel: ModListPath, mod: void 0, src: "", content });
|
|
5005
|
+
}
|
|
5006
|
+
}
|
|
5007
|
+
result.kept = (scope ? p.desired.filter((d) => scope.has(key(d.rel))).length : p.desired.length) - result.linked - result.skipped.length;
|
|
5008
|
+
if (scope) result.kept = Math.max(0, result.kept);
|
|
4922
5009
|
result.modsInGame = new Set(manifest.files.map((e) => e.mod).filter((m) => m !== Generated)).size;
|
|
4923
5010
|
save();
|
|
4924
5011
|
return result;
|
|
@@ -4956,6 +5043,7 @@ function pendingChanges(lib, gamePath, p) {
|
|
|
4956
5043
|
const st = stillOurs(e, p.method, gamePath);
|
|
4957
5044
|
if (st === "missing") out.push({ path: d.rel, mod: id, kind: "restore", detail: "missing from the game folder" });
|
|
4958
5045
|
else if (st === "changed") out.push({ path: d.rel, mod: id, kind: "restore", detail: "changed in the game folder; deploy asks whether to keep it" });
|
|
5046
|
+
else if (d.content === void 0 && otherMethod(e, p.method)) out.push({ path: d.rel, mod: id, kind: "relink", detail: p.method === "link" ? "the library is on the game's drive now: a hardlink instead of a copy" : "the library is on another drive now: a copy instead of a hardlink" });
|
|
4959
5047
|
else out.push({ path: d.rel, mod: id, kind: "relink", detail: d.content !== void 0 ? "the generated content changed (load order)" : "the library file was replaced (new version or rebuild)" });
|
|
4960
5048
|
}
|
|
4961
5049
|
}
|
|
@@ -4992,6 +5080,7 @@ function rednodePackageId(mod) {
|
|
|
4992
5080
|
return void 0;
|
|
4993
5081
|
}
|
|
4994
5082
|
function familyKey(mod) {
|
|
5083
|
+
if (mod.family) return mod.family;
|
|
4995
5084
|
if (mod.source.modId) return `nexus:${mod.source.modId}`;
|
|
4996
5085
|
const pkg = rednodePackageId(mod);
|
|
4997
5086
|
if (pkg) return `rednode:${pkg}`;
|
|
@@ -5044,6 +5133,7 @@ function settleNewVersion(lib, mod) {
|
|
|
5044
5133
|
const fam = familyOf(lib, mod);
|
|
5045
5134
|
const others = fam.mods.filter((m) => m.id !== mod.id);
|
|
5046
5135
|
if (!others.length) return {};
|
|
5136
|
+
takeFamilyName(lib, mod, others);
|
|
5047
5137
|
const previous = others.find((m) => m.enabled) ?? [...others].sort(newestFirst)[0];
|
|
5048
5138
|
if (mod.version && previous.version && versionNewer(previous.version, mod.version)) {
|
|
5049
5139
|
if (mod.enabled) {
|
|
@@ -5066,6 +5156,31 @@ function settleNewVersion(lib, mod) {
|
|
|
5066
5156
|
}
|
|
5067
5157
|
return { replaced: previous };
|
|
5068
5158
|
}
|
|
5159
|
+
function renameFamily(lib, mod, name) {
|
|
5160
|
+
const fam = familyOf(lib, mod);
|
|
5161
|
+
const to = name.trim();
|
|
5162
|
+
for (const m of fam.mods) {
|
|
5163
|
+
m.family = fam.key;
|
|
5164
|
+
if (!to) {
|
|
5165
|
+
if (m.renamedFrom !== void 0) m.name = m.renamedFrom;
|
|
5166
|
+
delete m.renamedFrom;
|
|
5167
|
+
} else if (m.name !== to) {
|
|
5168
|
+
m.renamedFrom ??= m.name;
|
|
5169
|
+
m.name = to;
|
|
5170
|
+
if (m.renamedFrom === to) delete m.renamedFrom;
|
|
5171
|
+
}
|
|
5172
|
+
saveMod(lib, m);
|
|
5173
|
+
}
|
|
5174
|
+
return familyOf(lib, mod);
|
|
5175
|
+
}
|
|
5176
|
+
function takeFamilyName(lib, mod, others) {
|
|
5177
|
+
const renamed = others.find((m) => m.renamedFrom !== void 0);
|
|
5178
|
+
if (!renamed || mod.name === renamed.name) return;
|
|
5179
|
+
mod.family = familyKey(renamed);
|
|
5180
|
+
mod.renamedFrom = mod.name;
|
|
5181
|
+
mod.name = renamed.name;
|
|
5182
|
+
saveMod(lib, mod);
|
|
5183
|
+
}
|
|
5069
5184
|
function familiesWithTwoOn(lib) {
|
|
5070
5185
|
return families(lib).filter((f) => f.mods.filter((m) => m.enabled).length > 1);
|
|
5071
5186
|
}
|
|
@@ -5105,28 +5220,165 @@ function frameworkDetail(c) {
|
|
|
5105
5220
|
return lines;
|
|
5106
5221
|
}
|
|
5107
5222
|
|
|
5108
|
-
// src/
|
|
5223
|
+
// src/frameworkGet.ts
|
|
5224
|
+
var FrameworkSources = [
|
|
5225
|
+
{ name: "RED4ext", nexusId: 2380, repo: "WopsS/RED4ext", asset: /^red4ext-[\d.]+\.zip$/i, signature: "red4ext/RED4ext.dll" },
|
|
5226
|
+
{ name: "Cyber Engine Tweaks", nexusId: 107, repo: "maximegmd/CyberEngineTweaks", asset: /^cet_[\d_]+\.zip$/i, signature: "bin/x64/plugins/cyber_engine_tweaks.asi" },
|
|
5227
|
+
{ name: "redscript", nexusId: 1511, repo: "jac3km4/redscript", asset: /^redscript-v?[\d.]+-windows\.zip$/i, signature: "engine/tools/scc.exe" },
|
|
5228
|
+
{ name: "ArchiveXL", nexusId: 4198, repo: "psiberx/cp2077-archive-xl", asset: /^ArchiveXL-[\d.]+\.zip$/i, signature: "red4ext/plugins/ArchiveXL/ArchiveXL.dll" },
|
|
5229
|
+
{ name: "TweakXL", nexusId: 4197, repo: "psiberx/cp2077-tweak-xl", asset: /^TweakXL-[\d.]+\.zip$/i, signature: "red4ext/plugins/TweakXL/TweakXL.dll" },
|
|
5230
|
+
{ name: "Codeware", nexusId: 7780, repo: "psiberx/cp2077-codeware", asset: /^Codeware-[\d.]+\.zip$/i, signature: "red4ext/plugins/Codeware/Codeware.dll" }
|
|
5231
|
+
];
|
|
5232
|
+
function frameworkFamily(lib, f) {
|
|
5233
|
+
const sig = f.signature.toLowerCase();
|
|
5234
|
+
return families(lib).find((fam) => fam.nexusId === f.nexusId || fam.mods.some((m) => m.files.some((x) => x.toLowerCase() === sig)));
|
|
5235
|
+
}
|
|
5236
|
+
function missingFrameworks(lib) {
|
|
5237
|
+
return FrameworkSources.filter((f) => !frameworkFamily(lib, f));
|
|
5238
|
+
}
|
|
5239
|
+
async function latestFrameworkRelease(f, fetchImpl = fetch, api = "https://api.github.com") {
|
|
5240
|
+
const headers = { Accept: "application/vnd.github+json", "User-Agent": "EDGEdeck" };
|
|
5241
|
+
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
5242
|
+
const res = await fetchImpl(`${api}/repos/${f.repo}/releases/latest`, { headers });
|
|
5243
|
+
if (!res.ok) throw new Error(res.status === 403 ? "GitHub's limit for calls without an account was reached; try again within the hour." : `GitHub answered ${res.status}.`);
|
|
5244
|
+
const rel = await res.json();
|
|
5245
|
+
const asset = rel.assets.find((a) => f.asset.test(a.name));
|
|
5246
|
+
if (!asset) throw new Error(`release ${rel.tag_name} has no file named like ${f.asset.source}.`);
|
|
5247
|
+
return { version: rel.tag_name.replace(/^v/i, ""), tag: rel.tag_name, assetName: asset.name, url: asset.browser_download_url, size: asset.size, page: rel.html_url };
|
|
5248
|
+
}
|
|
5249
|
+
async function frameworkStates(lib, fetchImpl, api) {
|
|
5250
|
+
return Promise.all(
|
|
5251
|
+
FrameworkSources.map(async (source) => {
|
|
5252
|
+
const family = frameworkFamily(lib, source);
|
|
5253
|
+
try {
|
|
5254
|
+
const release = await latestFrameworkRelease(source, fetchImpl, api);
|
|
5255
|
+
const newest2 = family?.mods[0]?.version ?? "";
|
|
5256
|
+
return { source, family, release, wanted: !family || !!newest2 && versionNewer(release.version, newest2) };
|
|
5257
|
+
} catch (err) {
|
|
5258
|
+
return { source, family, error: err instanceof Error ? err.message : String(err), wanted: !family };
|
|
5259
|
+
}
|
|
5260
|
+
})
|
|
5261
|
+
);
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5264
|
+
// src/libraryMove.ts
|
|
5109
5265
|
var fs9 = __toESM(require("node:fs"));
|
|
5110
5266
|
var path8 = __toESM(require("node:path"));
|
|
5267
|
+
function holdsLibrary(dir) {
|
|
5268
|
+
return fs9.existsSync(path8.join(dir, "library.json")) || fs9.existsSync(path8.join(dir, "library"));
|
|
5269
|
+
}
|
|
5270
|
+
function moveTarget(picked) {
|
|
5271
|
+
if (!fs9.existsSync(picked)) return picked;
|
|
5272
|
+
return fs9.readdirSync(picked).length ? path8.join(picked, "EDGEdeck") : picked;
|
|
5273
|
+
}
|
|
5274
|
+
function volume(p) {
|
|
5275
|
+
let dir = path8.resolve(p);
|
|
5276
|
+
for (; ; ) {
|
|
5277
|
+
try {
|
|
5278
|
+
return fs9.statSync(dir).dev;
|
|
5279
|
+
} catch {
|
|
5280
|
+
const up = path8.dirname(dir);
|
|
5281
|
+
if (up === dir) return void 0;
|
|
5282
|
+
dir = up;
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
function sameDrive(a, b) {
|
|
5287
|
+
const va = volume(a);
|
|
5288
|
+
const vb = volume(b);
|
|
5289
|
+
return va !== void 0 && va === vb;
|
|
5290
|
+
}
|
|
5291
|
+
function inside(child, parent) {
|
|
5292
|
+
const rel = path8.relative(parent, child);
|
|
5293
|
+
return !!rel && !rel.startsWith("..") && !path8.isAbsolute(rel);
|
|
5294
|
+
}
|
|
5295
|
+
function listFiles(root) {
|
|
5296
|
+
const out = [];
|
|
5297
|
+
const walk2 = (dir) => {
|
|
5298
|
+
for (const e of fs9.readdirSync(dir, { withFileTypes: true })) {
|
|
5299
|
+
const abs = path8.join(dir, e.name);
|
|
5300
|
+
if (e.isDirectory()) walk2(abs);
|
|
5301
|
+
else if (e.isFile()) out.push({ rel: path8.relative(root, abs), size: fs9.statSync(abs).size });
|
|
5302
|
+
}
|
|
5303
|
+
};
|
|
5304
|
+
walk2(root);
|
|
5305
|
+
return out;
|
|
5306
|
+
}
|
|
5307
|
+
async function moveLibrary(from, to, progress, cancelled) {
|
|
5308
|
+
const src = path8.resolve(from);
|
|
5309
|
+
const dest = path8.resolve(to);
|
|
5310
|
+
if (src.toLowerCase() === dest.toLowerCase()) throw new Error(`The library is already in ${dest}.`);
|
|
5311
|
+
if (inside(dest, src)) throw new Error(`${dest} is inside the library itself; pick a folder outside it.`);
|
|
5312
|
+
if (!holdsLibrary(src)) throw new Error(`${src} does not hold a library.`);
|
|
5313
|
+
const existed = fs9.existsSync(dest);
|
|
5314
|
+
if (existed && fs9.readdirSync(dest).length) throw new Error(`${dest} is not empty.`);
|
|
5315
|
+
if (sameDrive(src, dest)) {
|
|
5316
|
+
const files2 = listFiles(src);
|
|
5317
|
+
fs9.mkdirSync(path8.dirname(dest), { recursive: true });
|
|
5318
|
+
if (existed) fs9.rmdirSync(dest);
|
|
5319
|
+
fs9.renameSync(src, dest);
|
|
5320
|
+
return { how: "rename", files: files2.length, bytes: files2.reduce((n, f) => n + f.size, 0), leftBehind: [] };
|
|
5321
|
+
}
|
|
5322
|
+
const files = listFiles(src);
|
|
5323
|
+
const totalBytes = files.reduce((n, f) => n + f.size, 0);
|
|
5324
|
+
const done = { files: 0, totalFiles: files.length, bytes: 0, totalBytes };
|
|
5325
|
+
try {
|
|
5326
|
+
fs9.mkdirSync(dest, { recursive: true });
|
|
5327
|
+
for (const f of files) {
|
|
5328
|
+
if (cancelled?.()) throw new Error("Cancelled: the library stays where it was.");
|
|
5329
|
+
const a = path8.join(src, f.rel);
|
|
5330
|
+
const b = path8.join(dest, f.rel);
|
|
5331
|
+
fs9.mkdirSync(path8.dirname(b), { recursive: true });
|
|
5332
|
+
await fs9.promises.copyFile(a, b);
|
|
5333
|
+
const st = fs9.statSync(a);
|
|
5334
|
+
fs9.utimesSync(b, st.atime, st.mtime);
|
|
5335
|
+
if (fs9.statSync(b).size !== f.size) throw new Error(`${f.rel} did not copy whole.`);
|
|
5336
|
+
done.files++;
|
|
5337
|
+
done.bytes += f.size;
|
|
5338
|
+
progress?.({ ...done });
|
|
5339
|
+
}
|
|
5340
|
+
} catch (err) {
|
|
5341
|
+
fs9.rmSync(dest, { recursive: true, force: true });
|
|
5342
|
+
if (existed) fs9.mkdirSync(dest, { recursive: true });
|
|
5343
|
+
throw err;
|
|
5344
|
+
}
|
|
5345
|
+
const leftBehind = [];
|
|
5346
|
+
for (const f of files) {
|
|
5347
|
+
try {
|
|
5348
|
+
fs9.rmSync(path8.join(src, f.rel), { force: true });
|
|
5349
|
+
} catch {
|
|
5350
|
+
leftBehind.push(path8.join(src, f.rel));
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
try {
|
|
5354
|
+
fs9.rmSync(src, { recursive: true, force: true });
|
|
5355
|
+
} catch {
|
|
5356
|
+
}
|
|
5357
|
+
return { how: "copy", files: files.length, bytes: totalBytes, leftBehind };
|
|
5358
|
+
}
|
|
5359
|
+
|
|
5360
|
+
// src/loadouts.ts
|
|
5361
|
+
var fs10 = __toESM(require("node:fs"));
|
|
5362
|
+
var path9 = __toESM(require("node:path"));
|
|
5111
5363
|
var DefaultLoadout = "Default";
|
|
5112
5364
|
function loadoutsFolder(root) {
|
|
5113
|
-
return
|
|
5365
|
+
return path9.join(root, "loadouts");
|
|
5114
5366
|
}
|
|
5115
5367
|
function slug(name) {
|
|
5116
5368
|
return name.replace(/[<>:"/\\|?*]+/g, "-").trim() || "loadout";
|
|
5117
5369
|
}
|
|
5118
5370
|
function readLoadout(file) {
|
|
5119
|
-
return JSON.parse(
|
|
5371
|
+
return JSON.parse(fs10.readFileSync(file, "utf8"));
|
|
5120
5372
|
}
|
|
5121
5373
|
function listLoadouts(root) {
|
|
5122
5374
|
const dir = loadoutsFolder(root);
|
|
5123
|
-
if (!
|
|
5375
|
+
if (!fs10.existsSync(dir)) return [];
|
|
5124
5376
|
const out = [];
|
|
5125
|
-
for (const f of
|
|
5377
|
+
for (const f of fs10.readdirSync(dir)) {
|
|
5126
5378
|
if (!f.toLowerCase().endsWith(".json")) continue;
|
|
5127
5379
|
try {
|
|
5128
|
-
const l = readLoadout(
|
|
5129
|
-
out.push({ name: l.name, file:
|
|
5380
|
+
const l = readLoadout(path9.join(dir, f));
|
|
5381
|
+
out.push({ name: l.name, file: path9.join(dir, f), saved: l.saved, mods: l.mods.length, on: l.mods.filter((m) => m.enabled).length });
|
|
5130
5382
|
} catch {
|
|
5131
5383
|
}
|
|
5132
5384
|
}
|
|
@@ -5155,13 +5407,13 @@ function entryFor(f, mod, enabled) {
|
|
|
5155
5407
|
return { family: f.key, id: mod.id, name: f.name, version: mod.version, enabled, nexusModId: f.nexusId };
|
|
5156
5408
|
}
|
|
5157
5409
|
function writeLoadout(root, loadout, file) {
|
|
5158
|
-
|
|
5410
|
+
fs10.mkdirSync(loadoutsFolder(root), { recursive: true });
|
|
5159
5411
|
let target = file;
|
|
5160
5412
|
if (!target) {
|
|
5161
|
-
target =
|
|
5162
|
-
for (let n = 2;
|
|
5413
|
+
target = path9.join(loadoutsFolder(root), `${slug(loadout.name)}.json`);
|
|
5414
|
+
for (let n = 2; fs10.existsSync(target); n++) target = path9.join(loadoutsFolder(root), `${slug(loadout.name)}-${n}.json`);
|
|
5163
5415
|
}
|
|
5164
|
-
|
|
5416
|
+
fs10.writeFileSync(target, JSON.stringify(loadout, null, 2) + "\n");
|
|
5165
5417
|
return target;
|
|
5166
5418
|
}
|
|
5167
5419
|
function saveLoadout(lib, name, gameVersion2) {
|
|
@@ -5265,7 +5517,7 @@ function renameLoadout(lib, from, to) {
|
|
|
5265
5517
|
if (!info) throw new Error(`There is no loadout called ${from}.`);
|
|
5266
5518
|
const loadout = readLoadout(info.file);
|
|
5267
5519
|
loadout.name = to.trim();
|
|
5268
|
-
|
|
5520
|
+
fs10.rmSync(info.file);
|
|
5269
5521
|
writeLoadout(lib.root, loadout);
|
|
5270
5522
|
if (lib.info.activeLoadout === from) {
|
|
5271
5523
|
lib.info.activeLoadout = to.trim();
|
|
@@ -5275,18 +5527,18 @@ function renameLoadout(lib, from, to) {
|
|
|
5275
5527
|
function deleteLoadout(lib, name) {
|
|
5276
5528
|
if (lib.info.activeLoadout === name) throw new Error(`${name} is the active loadout; make another one active first.`);
|
|
5277
5529
|
const info = findLoadout(lib.root, name);
|
|
5278
|
-
if (info)
|
|
5530
|
+
if (info) fs10.rmSync(info.file);
|
|
5279
5531
|
}
|
|
5280
5532
|
function exportLoadout(lib, name, dest, gameVersion2) {
|
|
5281
5533
|
if (lib.info.activeLoadout === name) syncActiveLoadout(lib, gameVersion2);
|
|
5282
5534
|
const info = findLoadout(lib.root, name);
|
|
5283
5535
|
if (!info) throw new Error(`There is no loadout called ${name}.`);
|
|
5284
|
-
|
|
5536
|
+
fs10.copyFileSync(info.file, dest);
|
|
5285
5537
|
}
|
|
5286
5538
|
function importLoadout(lib, file) {
|
|
5287
5539
|
const loadout = readLoadout(file);
|
|
5288
|
-
if (!Array.isArray(loadout.mods)) throw new Error(`${
|
|
5289
|
-
let name = (loadout.name ||
|
|
5540
|
+
if (!Array.isArray(loadout.mods)) throw new Error(`${path9.basename(file)} is not a EDGEdeck loadout.`);
|
|
5541
|
+
let name = (loadout.name || path9.basename(file, ".json")).trim();
|
|
5290
5542
|
for (let n = 2; findLoadout(lib.root, name); n++) name = `${(loadout.name || "Loadout").trim()} (${n})`;
|
|
5291
5543
|
writeLoadout(lib.root, { ...loadout, name, saved: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5292
5544
|
return name;
|
|
@@ -5311,7 +5563,7 @@ function signIn(options) {
|
|
|
5311
5563
|
let done = false;
|
|
5312
5564
|
let attempts = 0;
|
|
5313
5565
|
let socket;
|
|
5314
|
-
return new Promise((
|
|
5566
|
+
return new Promise((resolve5, reject) => {
|
|
5315
5567
|
const finish = (err, key2) => {
|
|
5316
5568
|
if (done) return;
|
|
5317
5569
|
done = true;
|
|
@@ -5321,7 +5573,7 @@ function signIn(options) {
|
|
|
5321
5573
|
socket?.close();
|
|
5322
5574
|
} catch {
|
|
5323
5575
|
}
|
|
5324
|
-
if (key2)
|
|
5576
|
+
if (key2) resolve5(key2);
|
|
5325
5577
|
else reject(err ?? new Error("Nexus sign-in failed"));
|
|
5326
5578
|
};
|
|
5327
5579
|
const onAbort = () => finish(new Error("Sign-in cancelled"));
|
|
@@ -5452,7 +5704,7 @@ function signInWithOAuth(options) {
|
|
|
5452
5704
|
const { verifier, challenge } = pkce();
|
|
5453
5705
|
const state = crypto3.randomUUID();
|
|
5454
5706
|
let done = false;
|
|
5455
|
-
return new Promise((
|
|
5707
|
+
return new Promise((resolve5, reject) => {
|
|
5456
5708
|
const server = http.createServer();
|
|
5457
5709
|
const finish = (err, tokens) => {
|
|
5458
5710
|
if (done) return;
|
|
@@ -5460,7 +5712,7 @@ function signInWithOAuth(options) {
|
|
|
5460
5712
|
clearTimeout(timer);
|
|
5461
5713
|
options.signal?.removeEventListener("abort", onAbort);
|
|
5462
5714
|
server.close();
|
|
5463
|
-
if (tokens)
|
|
5715
|
+
if (tokens) resolve5(tokens);
|
|
5464
5716
|
else reject(err ?? new Error("Nexus sign-in failed"));
|
|
5465
5717
|
};
|
|
5466
5718
|
const onAbort = () => finish(new Error("Sign-in cancelled"));
|
|
@@ -5581,10 +5833,10 @@ function describeHandler(command) {
|
|
|
5581
5833
|
}
|
|
5582
5834
|
|
|
5583
5835
|
// src/project.ts
|
|
5584
|
-
var
|
|
5585
|
-
var
|
|
5836
|
+
var fs11 = __toESM(require("node:fs"));
|
|
5837
|
+
var path10 = __toESM(require("node:path"));
|
|
5586
5838
|
function addToProject(lib, mod, files, projectFile, replace = /* @__PURE__ */ new Set()) {
|
|
5587
|
-
const resources =
|
|
5839
|
+
const resources = path10.join(path10.dirname(projectFile), "source", "resources");
|
|
5588
5840
|
const out = { copied: [], same: [], kept: [], archives: [] };
|
|
5589
5841
|
for (const f of files) {
|
|
5590
5842
|
if (f.toLowerCase().endsWith(".archive")) {
|
|
@@ -5592,10 +5844,10 @@ function addToProject(lib, mod, files, projectFile, replace = /* @__PURE__ */ ne
|
|
|
5592
5844
|
continue;
|
|
5593
5845
|
}
|
|
5594
5846
|
const src = sourcePath(lib, mod, f);
|
|
5595
|
-
const rel =
|
|
5596
|
-
const dest =
|
|
5597
|
-
if (
|
|
5598
|
-
if (
|
|
5847
|
+
const rel = path10.join("source", "resources", ...f.split("/"));
|
|
5848
|
+
const dest = path10.join(resources, ...f.split("/"));
|
|
5849
|
+
if (fs11.existsSync(dest)) {
|
|
5850
|
+
if (fs11.readFileSync(dest).equals(fs11.readFileSync(src))) {
|
|
5599
5851
|
out.same.push(rel);
|
|
5600
5852
|
continue;
|
|
5601
5853
|
}
|
|
@@ -5604,39 +5856,39 @@ function addToProject(lib, mod, files, projectFile, replace = /* @__PURE__ */ ne
|
|
|
5604
5856
|
continue;
|
|
5605
5857
|
}
|
|
5606
5858
|
}
|
|
5607
|
-
|
|
5608
|
-
|
|
5859
|
+
fs11.mkdirSync(path10.dirname(dest), { recursive: true });
|
|
5860
|
+
fs11.copyFileSync(src, dest);
|
|
5609
5861
|
out.copied.push(rel);
|
|
5610
5862
|
}
|
|
5611
5863
|
return out;
|
|
5612
5864
|
}
|
|
5613
5865
|
function findProjectFile(target) {
|
|
5614
|
-
if (
|
|
5615
|
-
const files =
|
|
5616
|
-
return files.length ?
|
|
5866
|
+
if (fs11.existsSync(target) && fs11.statSync(target).isFile()) return target.toLowerCase().endsWith(".cpmodproj") ? target : void 0;
|
|
5867
|
+
const files = fs11.existsSync(target) ? fs11.readdirSync(target).filter((f) => f.toLowerCase().endsWith(".cpmodproj")) : [];
|
|
5868
|
+
return files.length ? path10.join(target, files[0]) : void 0;
|
|
5617
5869
|
}
|
|
5618
5870
|
function projectName(projectFile) {
|
|
5619
5871
|
try {
|
|
5620
|
-
const xml =
|
|
5872
|
+
const xml = fs11.readFileSync(projectFile, "utf8");
|
|
5621
5873
|
const m = /<ModName>\s*([^<]+?)\s*<\//.exec(xml) ?? /<Name>\s*([^<]+?)\s*<\//.exec(xml);
|
|
5622
5874
|
if (m) return m[1];
|
|
5623
5875
|
} catch {
|
|
5624
5876
|
}
|
|
5625
|
-
return
|
|
5877
|
+
return path10.basename(projectFile, path10.extname(projectFile));
|
|
5626
5878
|
}
|
|
5627
5879
|
function packedFolder(projectFile) {
|
|
5628
|
-
return
|
|
5880
|
+
return path10.join(path10.dirname(projectFile), "packed");
|
|
5629
5881
|
}
|
|
5630
5882
|
function classifyProject(projectFile) {
|
|
5631
5883
|
const packed = packedFolder(projectFile);
|
|
5632
|
-
if (!
|
|
5884
|
+
if (!fs11.existsSync(packed)) return { placements: [], kinds: [], unplaced: [], ignored: [] };
|
|
5633
5885
|
return classify(packed, projectName(projectFile));
|
|
5634
5886
|
}
|
|
5635
5887
|
function newest(dir) {
|
|
5636
5888
|
let t = 0;
|
|
5637
|
-
for (const e of
|
|
5638
|
-
const p =
|
|
5639
|
-
t = Math.max(t, e.isDirectory() ? newest(p) :
|
|
5889
|
+
for (const e of fs11.readdirSync(dir, { withFileTypes: true })) {
|
|
5890
|
+
const p = path10.join(dir, e.name);
|
|
5891
|
+
t = Math.max(t, e.isDirectory() ? newest(p) : fs11.statSync(p).mtimeMs);
|
|
5640
5892
|
}
|
|
5641
5893
|
return t;
|
|
5642
5894
|
}
|
|
@@ -5645,10 +5897,10 @@ function syncProjects(lib) {
|
|
|
5645
5897
|
for (const mod of lib.mods) {
|
|
5646
5898
|
if (mod.source.type !== "project" || !mod.source.path) continue;
|
|
5647
5899
|
const packed = packedFolder(mod.source.path);
|
|
5648
|
-
if (!
|
|
5900
|
+
if (!fs11.existsSync(packed)) continue;
|
|
5649
5901
|
const libNewest = mod.files.reduce((t, f) => {
|
|
5650
5902
|
try {
|
|
5651
|
-
return Math.max(t,
|
|
5903
|
+
return Math.max(t, fs11.statSync(sourcePath(lib, mod, f)).mtimeMs);
|
|
5652
5904
|
} catch {
|
|
5653
5905
|
return t;
|
|
5654
5906
|
}
|
|
@@ -5659,11 +5911,11 @@ function syncProjects(lib) {
|
|
|
5659
5911
|
const keep = /* @__PURE__ */ new Set();
|
|
5660
5912
|
for (const p of c.placements) {
|
|
5661
5913
|
const dest = sourcePath(lib, mod, p.to);
|
|
5662
|
-
|
|
5663
|
-
|
|
5914
|
+
fs11.mkdirSync(path10.dirname(dest), { recursive: true });
|
|
5915
|
+
fs11.copyFileSync(p.from, dest);
|
|
5664
5916
|
keep.add(p.to);
|
|
5665
5917
|
}
|
|
5666
|
-
for (const f of mod.files) if (!keep.has(f))
|
|
5918
|
+
for (const f of mod.files) if (!keep.has(f)) fs11.rmSync(sourcePath(lib, mod, f), { force: true });
|
|
5667
5919
|
mod.files = [...keep].sort();
|
|
5668
5920
|
mod.kinds = c.kinds;
|
|
5669
5921
|
saveMod(lib, mod);
|
|
@@ -5671,37 +5923,75 @@ function syncProjects(lib) {
|
|
|
5671
5923
|
}
|
|
5672
5924
|
return refreshed;
|
|
5673
5925
|
}
|
|
5674
|
-
function
|
|
5926
|
+
function projectMod(lib, projectFile) {
|
|
5927
|
+
return lib.mods.find((m) => m.source.type === "project" && m.source.path?.toLowerCase() === projectFile.toLowerCase());
|
|
5928
|
+
}
|
|
5929
|
+
function sameNameMod(lib, projectFile) {
|
|
5930
|
+
if (projectMod(lib, projectFile)) return void 0;
|
|
5931
|
+
const name = baseName(projectName(projectFile));
|
|
5932
|
+
const match = name ? lib.mods.find((m) => baseName(m.name) === name || m.renamedFrom !== void 0 && baseName(m.renamedFrom) === name) : void 0;
|
|
5933
|
+
return match && familyOf(lib, match).active;
|
|
5934
|
+
}
|
|
5935
|
+
function devVersion(lib, family) {
|
|
5936
|
+
const taken = new Set(lib.mods.filter((m) => familyKey(m) === family).map((m) => m.version.toLowerCase()));
|
|
5937
|
+
if (!taken.has("dev")) return "dev";
|
|
5938
|
+
let n = 2;
|
|
5939
|
+
while (taken.has(`dev ${n}`)) n++;
|
|
5940
|
+
return `dev ${n}`;
|
|
5941
|
+
}
|
|
5942
|
+
function installProject(lib, projectFile, beside) {
|
|
5675
5943
|
const c = classifyProject(projectFile);
|
|
5676
5944
|
if (!c.placements.length) throw new Error(`${projectName(projectFile)} has no packed output yet. Pack the project first (packed/ next to the project file).`);
|
|
5677
|
-
|
|
5945
|
+
let existing = projectMod(lib, projectFile);
|
|
5946
|
+
if (!existing && beside?.how === "follow") {
|
|
5947
|
+
existing = beside.mod;
|
|
5948
|
+
existing.family = familyKey(existing);
|
|
5949
|
+
existing.source = { type: "project", path: projectFile };
|
|
5950
|
+
}
|
|
5678
5951
|
if (!existing) {
|
|
5952
|
+
if (beside?.how === "version") {
|
|
5953
|
+
const family = familyKey(beside.mod);
|
|
5954
|
+
const previous = familyOf(lib, beside.mod);
|
|
5955
|
+
const mod2 = addMod(lib, c.placements, { name: projectName(projectFile), version: devVersion(lib, family), source: { type: "project", path: projectFile } });
|
|
5956
|
+
mod2.family = family;
|
|
5957
|
+
takeFamilyName(lib, mod2, previous.mods);
|
|
5958
|
+
placeAbove(lib, mod2, previous.active);
|
|
5959
|
+
for (const m of previous.mods) {
|
|
5960
|
+
if (m.enabled) {
|
|
5961
|
+
m.enabled = false;
|
|
5962
|
+
saveMod(lib, m);
|
|
5963
|
+
}
|
|
5964
|
+
}
|
|
5965
|
+
saveMod(lib, mod2);
|
|
5966
|
+
return { mod: mod2, added: true, beside };
|
|
5967
|
+
}
|
|
5679
5968
|
const mod = addMod(lib, c.placements, { name: projectName(projectFile), version: "dev", source: { type: "project", path: projectFile } });
|
|
5680
5969
|
return { mod, added: true };
|
|
5681
5970
|
}
|
|
5682
5971
|
const keep = /* @__PURE__ */ new Set();
|
|
5683
5972
|
for (const p of c.placements) {
|
|
5684
5973
|
const dest = sourcePath(lib, existing, p.to);
|
|
5685
|
-
|
|
5686
|
-
|
|
5974
|
+
fs11.mkdirSync(path10.dirname(dest), { recursive: true });
|
|
5975
|
+
fs11.copyFileSync(p.from, dest);
|
|
5687
5976
|
keep.add(p.to);
|
|
5688
5977
|
}
|
|
5689
|
-
for (const f of existing.files) if (!keep.has(f))
|
|
5978
|
+
for (const f of existing.files) if (!keep.has(f)) fs11.rmSync(sourcePath(lib, existing, f), { force: true });
|
|
5690
5979
|
existing.files = [...keep].sort();
|
|
5691
5980
|
existing.kinds = c.kinds;
|
|
5981
|
+
activateVersion(lib, existing);
|
|
5692
5982
|
existing.enabled = true;
|
|
5693
5983
|
saveMod(lib, existing);
|
|
5694
|
-
return { mod: existing, added: false };
|
|
5984
|
+
return { mod: existing, added: false, ...beside ? { beside } : {} };
|
|
5695
5985
|
}
|
|
5696
5986
|
|
|
5697
5987
|
// src/redmod.ts
|
|
5698
5988
|
var import_node_child_process6 = require("node:child_process");
|
|
5699
|
-
var
|
|
5700
|
-
var
|
|
5989
|
+
var fs12 = __toESM(require("node:fs"));
|
|
5990
|
+
var path11 = __toESM(require("node:path"));
|
|
5701
5991
|
var import_node_util4 = require("node:util");
|
|
5702
5992
|
var run4 = (0, import_node_util4.promisify)(import_node_child_process6.execFile);
|
|
5703
5993
|
function redModExe(gamePath) {
|
|
5704
|
-
return
|
|
5994
|
+
return path11.join(gamePath, "tools", "redmod", "bin", "redMod.exe");
|
|
5705
5995
|
}
|
|
5706
5996
|
function enabledRedmodPackages(lib) {
|
|
5707
5997
|
const names = [];
|
|
@@ -5714,17 +6004,27 @@ function enabledRedmodPackages(lib) {
|
|
|
5714
6004
|
}
|
|
5715
6005
|
return names;
|
|
5716
6006
|
}
|
|
5717
|
-
|
|
5718
|
-
const
|
|
6007
|
+
function deployedRedmodPackages(lib, gamePath) {
|
|
6008
|
+
const inGame = new Set((readManifest(gamePath)?.files ?? []).map((e) => /^mods\/([^/]+)\//i.exec(e.path)?.[1]).filter((n) => !!n));
|
|
6009
|
+
const names = [];
|
|
6010
|
+
for (const mod of lib.mods) {
|
|
6011
|
+
for (const f of mod.files) {
|
|
6012
|
+
const m = /^mods\/([^/]+)\//i.exec(f);
|
|
6013
|
+
if (m && inGame.has(m[1]) && !names.includes(m[1])) names.push(m[1]);
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
return names;
|
|
6017
|
+
}
|
|
6018
|
+
async function deployRedmod(lib, gamePath, log, packages = enabledRedmodPackages(lib)) {
|
|
5719
6019
|
const exe = redModExe(gamePath);
|
|
5720
6020
|
if (!packages.length) return { ran: false, packages, output: "no REDmod packages enabled" };
|
|
5721
|
-
if (!
|
|
5722
|
-
const listFile =
|
|
5723
|
-
|
|
5724
|
-
const args = ["deploy", `-root=${gamePath}`, `-modlist=${listFile}`, `-rttiSchemaPath=${
|
|
6021
|
+
if (!fs12.existsSync(exe)) return { ran: false, packages, output: `redMod.exe not found at ${exe}; the REDmod tools are a free DLC on the game's store page` };
|
|
6022
|
+
const listFile = path11.join(lib.root, "redmod-modlist.txt");
|
|
6023
|
+
fs12.writeFileSync(listFile, packages.join("\r\n") + "\r\n");
|
|
6024
|
+
const args = ["deploy", `-root=${gamePath}`, `-modlist=${listFile}`, `-rttiSchemaPath=${path11.join(gamePath, "tools", "redmod", "metadata.json")}`];
|
|
5725
6025
|
log?.(`redMod.exe ${args.join(" ")}`);
|
|
5726
6026
|
try {
|
|
5727
|
-
const { stdout, stderr } = await run4(exe, args, { cwd:
|
|
6027
|
+
const { stdout, stderr } = await run4(exe, args, { cwd: path11.dirname(exe), windowsHide: true, maxBuffer: 1 << 26 });
|
|
5728
6028
|
const output = (stdout + stderr).trim();
|
|
5729
6029
|
for (const line of output.split(/\r?\n/)) if (line.trim()) log?.(` ${line}`);
|
|
5730
6030
|
return { ran: true, packages, output };
|
|
@@ -5842,8 +6142,8 @@ function setSectionOn(lib, name, on) {
|
|
|
5842
6142
|
}
|
|
5843
6143
|
|
|
5844
6144
|
// src/sizes.ts
|
|
5845
|
-
var
|
|
5846
|
-
var
|
|
6145
|
+
var fs13 = __toESM(require("node:fs"));
|
|
6146
|
+
var path12 = __toESM(require("node:path"));
|
|
5847
6147
|
var cache = /* @__PURE__ */ new Map();
|
|
5848
6148
|
function modSize(lib, mod) {
|
|
5849
6149
|
const stamp = `${mod.files.length}:${mod.added}:${lib.root}`;
|
|
@@ -5856,7 +6156,7 @@ function modSize(lib, mod) {
|
|
|
5856
6156
|
for (const f of mod.files) {
|
|
5857
6157
|
let n = 0;
|
|
5858
6158
|
try {
|
|
5859
|
-
const st =
|
|
6159
|
+
const st = fs13.statSync(path12.join(folder, ...f.split("/")));
|
|
5860
6160
|
n = st.size;
|
|
5861
6161
|
mtimes.set(f, st.mtimeMs);
|
|
5862
6162
|
} catch {
|
|
@@ -5875,10 +6175,10 @@ function forgetSizes(modId) {
|
|
|
5875
6175
|
function folderSize(dir) {
|
|
5876
6176
|
let total = 0;
|
|
5877
6177
|
try {
|
|
5878
|
-
for (const e of
|
|
5879
|
-
const p =
|
|
6178
|
+
for (const e of fs13.readdirSync(dir, { withFileTypes: true })) {
|
|
6179
|
+
const p = path12.join(dir, e.name);
|
|
5880
6180
|
if (e.isDirectory()) total += folderSize(p);
|
|
5881
|
-
else if (e.isFile()) total +=
|
|
6181
|
+
else if (e.isFile()) total += fs13.statSync(p).size;
|
|
5882
6182
|
}
|
|
5883
6183
|
} catch {
|
|
5884
6184
|
}
|
|
@@ -5899,20 +6199,20 @@ function formatBytes(n) {
|
|
|
5899
6199
|
}
|
|
5900
6200
|
|
|
5901
6201
|
// src/verbs.ts
|
|
5902
|
-
var
|
|
6202
|
+
var fs15 = __toESM(require("node:fs"));
|
|
5903
6203
|
var os3 = __toESM(require("node:os"));
|
|
5904
|
-
var
|
|
6204
|
+
var path14 = __toESM(require("node:path"));
|
|
5905
6205
|
|
|
5906
6206
|
// src/vortex.ts
|
|
5907
|
-
var
|
|
5908
|
-
var
|
|
6207
|
+
var fs14 = __toESM(require("node:fs"));
|
|
6208
|
+
var path13 = __toESM(require("node:path"));
|
|
5909
6209
|
function vortexStagingPath() {
|
|
5910
|
-
return
|
|
6210
|
+
return path13.join(process.env.APPDATA ?? path13.join(process.env.USERPROFILE ?? "", "AppData", "Roaming"), "Vortex", "cyberpunk2077", "mods");
|
|
5911
6211
|
}
|
|
5912
6212
|
function manifestStaging(gamePath) {
|
|
5913
6213
|
if (!gamePath) return void 0;
|
|
5914
6214
|
try {
|
|
5915
|
-
const m = JSON.parse(
|
|
6215
|
+
const m = JSON.parse(fs14.readFileSync(path13.join(gamePath, "vortex.deployment.json"), "utf8"));
|
|
5916
6216
|
return m.stagingPath || void 0;
|
|
5917
6217
|
} catch {
|
|
5918
6218
|
return void 0;
|
|
@@ -5920,18 +6220,18 @@ function manifestStaging(gamePath) {
|
|
|
5920
6220
|
}
|
|
5921
6221
|
function driveRoots(gamePath) {
|
|
5922
6222
|
if (process.platform !== "win32") return [];
|
|
5923
|
-
const first = gamePath ?
|
|
6223
|
+
const first = gamePath ? path13.parse(path13.resolve(gamePath)).root.toUpperCase() : "";
|
|
5924
6224
|
const roots = first ? [first] : [];
|
|
5925
6225
|
for (let c = 67; c <= 90; c++) {
|
|
5926
6226
|
const r = `${String.fromCharCode(c)}:\\`;
|
|
5927
|
-
if (r !== first &&
|
|
6227
|
+
if (r !== first && fs14.existsSync(r)) roots.push(r);
|
|
5928
6228
|
}
|
|
5929
6229
|
return roots;
|
|
5930
6230
|
}
|
|
5931
6231
|
function vortexStagingCandidates(gamePath, setting) {
|
|
5932
6232
|
const list = [setting, manifestStaging(gamePath), vortexStagingPath()];
|
|
5933
6233
|
for (const root of driveRoots(gamePath)) {
|
|
5934
|
-
list.push(
|
|
6234
|
+
list.push(path13.join(root, "Vortex Mods", "cyberpunk2077"), path13.join(root, "VortexMods", "cyberpunk2077"), path13.join(root, "Vortex", "cyberpunk2077", "mods"), path13.join(root, "Vortex", "mods", "cyberpunk2077"));
|
|
5935
6235
|
}
|
|
5936
6236
|
const seen = /* @__PURE__ */ new Set();
|
|
5937
6237
|
return list.filter((p) => !!p && !seen.has(p.toLowerCase()) && !!seen.add(p.toLowerCase()));
|
|
@@ -5939,7 +6239,7 @@ function vortexStagingCandidates(gamePath, setting) {
|
|
|
5939
6239
|
function findVortexStaging(gamePath, setting) {
|
|
5940
6240
|
for (const dir of vortexStagingCandidates(gamePath, setting)) {
|
|
5941
6241
|
try {
|
|
5942
|
-
if (
|
|
6242
|
+
if (fs14.readdirSync(dir, { withFileTypes: true }).some((e) => e.isDirectory() && !e.name.startsWith("__"))) return dir;
|
|
5943
6243
|
} catch {
|
|
5944
6244
|
}
|
|
5945
6245
|
}
|
|
@@ -5947,24 +6247,24 @@ function findVortexStaging(gamePath, setting) {
|
|
|
5947
6247
|
}
|
|
5948
6248
|
function countFiles(dir) {
|
|
5949
6249
|
let n = 0;
|
|
5950
|
-
for (const e of
|
|
5951
|
-
if (e.isDirectory()) n += countFiles(
|
|
6250
|
+
for (const e of fs14.readdirSync(dir, { withFileTypes: true })) {
|
|
6251
|
+
if (e.isDirectory()) n += countFiles(path13.join(dir, e.name));
|
|
5952
6252
|
else n++;
|
|
5953
6253
|
}
|
|
5954
6254
|
return n;
|
|
5955
6255
|
}
|
|
5956
6256
|
function guessName(folder) {
|
|
5957
6257
|
const sub = (...parts) => {
|
|
5958
|
-
const d =
|
|
5959
|
-
if (!
|
|
5960
|
-
const names =
|
|
6258
|
+
const d = path13.join(folder, ...parts);
|
|
6259
|
+
if (!fs14.existsSync(d)) return void 0;
|
|
6260
|
+
const names = fs14.readdirSync(d, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
5961
6261
|
return names.length === 1 ? names[0] : void 0;
|
|
5962
6262
|
};
|
|
5963
6263
|
const named = sub("r6", "scripts") ?? sub("bin", "x64", "plugins", "cyber_engine_tweaks", "mods") ?? sub("red4ext", "plugins") ?? sub("r6", "tweaks") ?? sub("mods");
|
|
5964
6264
|
if (named) return named;
|
|
5965
|
-
const archives =
|
|
5966
|
-
if (
|
|
5967
|
-
const a =
|
|
6265
|
+
const archives = path13.join(folder, "archive", "pc", "mod");
|
|
6266
|
+
if (fs14.existsSync(archives)) {
|
|
6267
|
+
const a = fs14.readdirSync(archives).find((f) => f.toLowerCase().endsWith(".archive"));
|
|
5968
6268
|
if (a) return a.replace(/\.archive$/i, "").replace(/^[#!_0-9]+/, "") || a;
|
|
5969
6269
|
}
|
|
5970
6270
|
return void 0;
|
|
@@ -5974,21 +6274,21 @@ function listVortexMods(gamePath, staging) {
|
|
|
5974
6274
|
if (!dir) return [];
|
|
5975
6275
|
staging = dir;
|
|
5976
6276
|
const deployed = /* @__PURE__ */ new Set();
|
|
5977
|
-
const manifest =
|
|
5978
|
-
if (
|
|
6277
|
+
const manifest = path13.join(gamePath, "vortex.deployment.json");
|
|
6278
|
+
if (fs14.existsSync(manifest)) {
|
|
5979
6279
|
try {
|
|
5980
|
-
const m = JSON.parse(
|
|
6280
|
+
const m = JSON.parse(fs14.readFileSync(manifest, "utf8"));
|
|
5981
6281
|
for (const f of m.files ?? []) deployed.add(f.source);
|
|
5982
6282
|
} catch {
|
|
5983
6283
|
}
|
|
5984
6284
|
}
|
|
5985
6285
|
const mods = [];
|
|
5986
|
-
for (const entry of
|
|
6286
|
+
for (const entry of fs14.readdirSync(staging, { withFileTypes: true })) {
|
|
5987
6287
|
if (!entry.isDirectory() || entry.name.startsWith("__")) continue;
|
|
5988
|
-
const folder =
|
|
6288
|
+
const folder = path13.join(staging, entry.name);
|
|
5989
6289
|
const files = countFiles(folder);
|
|
5990
6290
|
if (files === 0) continue;
|
|
5991
|
-
if (
|
|
6291
|
+
if (fs14.existsSync(path13.join(folder, "collection.json")) && files === 1) continue;
|
|
5992
6292
|
const m = /^(.*?)-(\d+)-([\d-]+)-(\d{10})$/.exec(entry.name);
|
|
5993
6293
|
const guid = /^[0-9a-f]{2}_[0-9a-f]{2}_[0-9a-f]{2}_[0-9a-f-]{36}$/i.test(entry.name);
|
|
5994
6294
|
mods.push({
|
|
@@ -6045,6 +6345,13 @@ function importFromVortex(lib, mods) {
|
|
|
6045
6345
|
}
|
|
6046
6346
|
|
|
6047
6347
|
// src/verbs.ts
|
|
6348
|
+
function lostProjectsText(lib, lost) {
|
|
6349
|
+
const lines = lost.flatMap((l) => {
|
|
6350
|
+
const mod = lib.mods.find((m) => m.id === l.mod);
|
|
6351
|
+
return mod?.source.type === "project" ? [`the game still has ${l.winner} for ${l.files} file(s), not the project ${l.mod} (move ${l.mod} up to let it win)`] : [];
|
|
6352
|
+
});
|
|
6353
|
+
return lines.length ? `; ${lines.join("; ")}` : "";
|
|
6354
|
+
}
|
|
6048
6355
|
var DefaultGame = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Cyberpunk 2077";
|
|
6049
6356
|
function openSession(opts) {
|
|
6050
6357
|
const libraryRoot = opts.library ?? defaultLibraryPath();
|
|
@@ -6069,7 +6376,7 @@ var verbs = {
|
|
|
6069
6376
|
/** Every mod with its status and size, the library totals and the frameworks line. */
|
|
6070
6377
|
async list(s, largestFirst = false) {
|
|
6071
6378
|
const { lib, game } = s;
|
|
6072
|
-
const p =
|
|
6379
|
+
const p = fs15.existsSync(game) && isGameFolder(game) ? plan(lib, game) : void 0;
|
|
6073
6380
|
const manifest = p ? readManifest(game) : void 0;
|
|
6074
6381
|
const changes = p ? detectChanges(game) : [];
|
|
6075
6382
|
const rows = lib.mods.map((m) => ({ id: m.id, name: m.name, version: m.version, enabled: m.enabled, kinds: m.kinds, files: m.files.length, bytes: modSize(lib, m).total, status: p ? modStatus(lib, game, m, p, manifest, changes) : "" }));
|
|
@@ -6111,14 +6418,14 @@ ${mod.files.join("\n")}`;
|
|
|
6111
6418
|
},
|
|
6112
6419
|
/** Adds a zip, 7z, rar, a single mod file or a folder. An archive with options needs the option's label. */
|
|
6113
6420
|
async add(s, source, name, option) {
|
|
6114
|
-
if (!
|
|
6115
|
-
const isFile =
|
|
6116
|
-
const modName = name ??
|
|
6421
|
+
if (!fs15.existsSync(source)) throw new Error(`${source} does not exist`);
|
|
6422
|
+
const isFile = fs15.statSync(source).isFile();
|
|
6423
|
+
const modName = name ?? path14.basename(source, isFile ? path14.extname(source) : "");
|
|
6117
6424
|
let folder = source;
|
|
6118
6425
|
let temp;
|
|
6119
6426
|
try {
|
|
6120
6427
|
if (isFile) {
|
|
6121
|
-
temp =
|
|
6428
|
+
temp = fs15.mkdtempSync(path14.join(os3.tmpdir(), "edgedeck-verb-"));
|
|
6122
6429
|
await unpackOrCopy(source, temp, s.sevenZa);
|
|
6123
6430
|
folder = temp;
|
|
6124
6431
|
}
|
|
@@ -6135,16 +6442,24 @@ ${mod.files.join("\n")}`;
|
|
|
6135
6442
|
const note = settled.replaced ? `; a new version of ${settled.replaced.name} ${settled.replaced.version}, which is off now` : settled.older ? `; older than ${settled.older.version}, so it is off` : "";
|
|
6136
6443
|
return { data: { added: { id: mod.id, name: mod.name, version: mod.version, kinds: mod.kinds, files: mod.files.length, enabled: mod.enabled }, replaced: settled.replaced?.id, unplaced: c.unplaced }, text: `added ${mod.id} (${mod.files.length} files, ${mod.kinds.join(", ")})${note}; ${mod.enabled ? "it is on, " : ""}deploy to put it in the game${c.unplaced.length ? `; not placed: ${c.unplaced.join(", ")}` : ""}` };
|
|
6137
6444
|
} finally {
|
|
6138
|
-
if (temp)
|
|
6445
|
+
if (temp) fs15.rmSync(temp, { recursive: true, force: true });
|
|
6139
6446
|
}
|
|
6140
6447
|
},
|
|
6141
6448
|
/** Adds a REDcode or WolvenKit project's packed output, or refreshes the mod that follows it, and turns it on. */
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6449
|
+
/**
|
|
6450
|
+
* `sameName` answers for a mod of the same name that does not follow this project: follow (it
|
|
6451
|
+
* follows this project from now on) or version (the project is another version of it, on in its
|
|
6452
|
+
* place). Without it, the project is a mod of its own and the text says what it sits beside.
|
|
6453
|
+
*/
|
|
6454
|
+
installProject(s, projectFile, sameName) {
|
|
6455
|
+
if (!fs15.existsSync(projectFile)) throw new Error(`${projectFile} does not exist`);
|
|
6456
|
+
const other = sameNameMod(s.lib, projectFile);
|
|
6457
|
+
const before = other && `${other.id}${other.source.type === "project" && other.source.path ? ` (${path14.dirname(other.source.path)})` : ""}`;
|
|
6458
|
+
const { mod, added, beside } = installProject(s.lib, projectFile, other && sameName ? { mod: other, how: sameName } : void 0);
|
|
6459
|
+
const note = beside?.how === "follow" ? `; ${before} follows this project now` : beside ? `; another version of ${before}, on in its place` : other ? `; note: the library also has ${before} under the same name, a separate mod; the one higher in the list wins the files they share (call again with sameName follow or version to settle it)` : "";
|
|
6145
6460
|
return {
|
|
6146
|
-
data: { id: mod.id, name: mod.name, version: mod.version, kinds: mod.kinds, files: mod.files.length, added },
|
|
6147
|
-
text: `${added ? "added" : "refreshed"} ${mod.id} from its project (${mod.files.length} files, ${mod.kinds.join(", ")}); it is on, deploy to put it in the game`
|
|
6461
|
+
data: { id: mod.id, name: mod.name, version: mod.version, kinds: mod.kinds, files: mod.files.length, added, sameName: other?.id, beside: beside?.how },
|
|
6462
|
+
text: `${added ? "added" : "refreshed"} ${mod.id} from its project (${mod.files.length} files, ${mod.kinds.join(", ")}); it is on, deploy to put it in the game${note}`
|
|
6148
6463
|
};
|
|
6149
6464
|
},
|
|
6150
6465
|
turn(s, id, on) {
|
|
@@ -6158,22 +6473,37 @@ ${mod.files.join("\n")}`;
|
|
|
6158
6473
|
moveMod(s.lib, mod, direction === "up" ? -1 : 1);
|
|
6159
6474
|
return { data: { order: s.lib.info.order }, text: s.lib.info.order.join("\n") };
|
|
6160
6475
|
},
|
|
6476
|
+
/** A new name in EDGEdeck for a mod and all its versions; an empty name puts the first name back. */
|
|
6477
|
+
rename(s, id, name) {
|
|
6478
|
+
const mod = find(s, id);
|
|
6479
|
+
const before = familyOf(s.lib, mod).name;
|
|
6480
|
+
const fam = renameFamily(s.lib, mod, name);
|
|
6481
|
+
return {
|
|
6482
|
+
data: { id: mod.id, name: fam.name, versions: fam.mods.map((m) => m.id) },
|
|
6483
|
+
text: `${before} is now ${fam.name}${fam.mods.length > 1 ? ` (${fam.mods.length} versions)` : ""}; its files and the game do not change`
|
|
6484
|
+
};
|
|
6485
|
+
},
|
|
6161
6486
|
remove(s, id) {
|
|
6162
6487
|
const mod = find(s, id);
|
|
6163
6488
|
removeMod(s.lib, mod);
|
|
6164
6489
|
return { data: { removed: mod.id }, text: `removed ${mod.id} from the library; its deployed files leave on the next deploy` };
|
|
6165
6490
|
},
|
|
6166
|
-
/**
|
|
6167
|
-
|
|
6491
|
+
/**
|
|
6492
|
+
* Deploys. A file changed outside EDGEdeck is skipped unless named in keep (import it) or restore
|
|
6493
|
+
* (overwrite it). With `only` (a mod id), deploys that mod's changes and leaves the rest waiting.
|
|
6494
|
+
*/
|
|
6495
|
+
async deploy(s, keep = [], restore = [], only) {
|
|
6168
6496
|
gameReady(s);
|
|
6169
6497
|
if (gameRunning()) throw new Error("Cyberpunk 2077 is running; it keeps mod files open. Quit the game, then deploy");
|
|
6498
|
+
if (only) find(s, only);
|
|
6170
6499
|
const decisions = /* @__PURE__ */ new Map();
|
|
6171
6500
|
for (const p of keep) decisions.set(p, "keep");
|
|
6172
6501
|
for (const p of restore) decisions.set(p, "restore");
|
|
6173
|
-
const result = deploy(s.lib, s.game, decisions, void 0, await gameVersion(s.game));
|
|
6174
|
-
const
|
|
6502
|
+
const result = deploy(s.lib, s.game, decisions, void 0, await gameVersion(s.game), only);
|
|
6503
|
+
const packages = only ? result.changes.some((c) => c.path.toLowerCase().startsWith("mods/")) ? deployedRedmodPackages(s.lib, s.game) : [] : enabledRedmodPackages(s.lib);
|
|
6504
|
+
const redmod = packages.length ? await deployRedmod(s.lib, s.game, void 0, packages) : void 0;
|
|
6175
6505
|
const report = writeDeployReport(s.lib, s.game, result);
|
|
6176
|
-
const text = `deployed (${result.method}): ${result.linked} added for ${modsTouched(result).length} mod(s), ${result.removed} removed, ${result.kept} unchanged; ${result.modsInGame} mod(s) in the game; report ${report}${result.skipped.length ? `; skipped (changed in game folder, answer with keep or restore): ${result.skipped.join(", ")}` : ""}${redmod ? `; REDmod: ${redmod.ran ? `${redmod.packages.length} prepared` : redmod.output}` : ""}`;
|
|
6506
|
+
const text = `deployed${only ? ` ${only} only` : ""} (${result.method}): ${result.linked} added for ${modsTouched(result).length} mod(s), ${result.removed} removed, ${result.kept} unchanged; ${result.modsInGame} mod(s) in the game; report ${report}${result.skipped.length ? `; skipped (changed in game folder, answer with keep or restore): ${result.skipped.join(", ")}` : ""}${redmod ? `; REDmod: ${redmod.ran ? `${redmod.packages.length} prepared` : redmod.output}` : ""}${result.waiting ? `; ${result.waiting} file(s) of other mods' changes still waiting` : ""}${lostProjectsText(s.lib, result.lost)}`;
|
|
6177
6507
|
return { data: { ...result, redmod, report }, text };
|
|
6178
6508
|
},
|
|
6179
6509
|
undo(s) {
|
|
@@ -6208,7 +6538,7 @@ ${mod.files.join("\n")}`;
|
|
|
6208
6538
|
}
|
|
6209
6539
|
if (action === "load") {
|
|
6210
6540
|
if (!name) throw new Error("loadout load needs a name or file");
|
|
6211
|
-
const target =
|
|
6541
|
+
const target = fs15.existsSync(name) && name.toLowerCase().endsWith(".json") ? importLoadout(s.lib, name) : name;
|
|
6212
6542
|
const r = makeActive(s.lib, target);
|
|
6213
6543
|
return { data: { ...r, active: target }, text: `loadout ${target} active: ${r.on} on, ${r.off} off${r.missing.length ? `; not in the library: ${r.missing.join(", ")}` : ""}; deploy to apply` };
|
|
6214
6544
|
}
|
|
@@ -6232,6 +6562,7 @@ ${mod.files.join("\n")}`;
|
|
|
6232
6562
|
BridgeClient,
|
|
6233
6563
|
BridgeRepo,
|
|
6234
6564
|
DefaultLoadout,
|
|
6565
|
+
FrameworkSources,
|
|
6235
6566
|
FrameworksSection,
|
|
6236
6567
|
KnownModNames,
|
|
6237
6568
|
ManifestName,
|
|
@@ -6269,8 +6600,10 @@ ${mod.files.join("\n")}`;
|
|
|
6269
6600
|
deleteLoadout,
|
|
6270
6601
|
deleteSection,
|
|
6271
6602
|
deploy,
|
|
6603
|
+
deployMethod,
|
|
6272
6604
|
deployRedmod,
|
|
6273
6605
|
deployReportsFolder,
|
|
6606
|
+
deployedRedmodPackages,
|
|
6274
6607
|
describeHandler,
|
|
6275
6608
|
detectChanges,
|
|
6276
6609
|
dotnet10Root,
|
|
@@ -6299,12 +6632,15 @@ ${mod.files.join("\n")}`;
|
|
|
6299
6632
|
formatBytes,
|
|
6300
6633
|
forwarderCommand,
|
|
6301
6634
|
frameworkDetail,
|
|
6635
|
+
frameworkFamily,
|
|
6636
|
+
frameworkStates,
|
|
6302
6637
|
frameworkSummary,
|
|
6303
6638
|
gameExe,
|
|
6304
6639
|
gameRunning,
|
|
6305
6640
|
gameVersion,
|
|
6306
6641
|
guessName,
|
|
6307
6642
|
hasWebSocket,
|
|
6643
|
+
holdsLibrary,
|
|
6308
6644
|
idFor,
|
|
6309
6645
|
importFromVortex,
|
|
6310
6646
|
importLoadout,
|
|
@@ -6316,6 +6652,8 @@ ${mod.files.join("\n")}`;
|
|
|
6316
6652
|
isGameFolder,
|
|
6317
6653
|
keepDownload,
|
|
6318
6654
|
latestBridgeRelease,
|
|
6655
|
+
latestFrameworkRelease,
|
|
6656
|
+
launchTarget,
|
|
6319
6657
|
libraryFolder,
|
|
6320
6658
|
librarySize,
|
|
6321
6659
|
listLoadouts,
|
|
@@ -6324,8 +6662,11 @@ ${mod.files.join("\n")}`;
|
|
|
6324
6662
|
loadoutNameProblem,
|
|
6325
6663
|
loadoutState,
|
|
6326
6664
|
loadoutsFolder,
|
|
6665
|
+
lostFiles,
|
|
6327
6666
|
makeActive,
|
|
6328
6667
|
md5Of,
|
|
6668
|
+
missingFrameworks,
|
|
6669
|
+
modDeployPaths,
|
|
6329
6670
|
modFolder,
|
|
6330
6671
|
modPageUrl,
|
|
6331
6672
|
modSize,
|
|
@@ -6333,8 +6674,10 @@ ${mod.files.join("\n")}`;
|
|
|
6333
6674
|
modsRemoved,
|
|
6334
6675
|
modsTouched,
|
|
6335
6676
|
moveInSection,
|
|
6677
|
+
moveLibrary,
|
|
6336
6678
|
moveMod,
|
|
6337
6679
|
moveSection,
|
|
6680
|
+
moveTarget,
|
|
6338
6681
|
needsRefresh,
|
|
6339
6682
|
newLoadout,
|
|
6340
6683
|
newestFile,
|
|
@@ -6349,6 +6692,7 @@ ${mod.files.join("\n")}`;
|
|
|
6349
6692
|
placeAbove,
|
|
6350
6693
|
placeNextTo,
|
|
6351
6694
|
plan,
|
|
6695
|
+
projectMod,
|
|
6352
6696
|
projectName,
|
|
6353
6697
|
putInSection,
|
|
6354
6698
|
readLoadout,
|
|
@@ -6360,8 +6704,11 @@ ${mod.files.join("\n")}`;
|
|
|
6360
6704
|
registerNxmHandler,
|
|
6361
6705
|
removeMod,
|
|
6362
6706
|
removeOlderBridges,
|
|
6707
|
+
renameFamily,
|
|
6363
6708
|
renameLoadout,
|
|
6364
6709
|
renameSection,
|
|
6710
|
+
sameDrive,
|
|
6711
|
+
sameNameMod,
|
|
6365
6712
|
saveLibrary,
|
|
6366
6713
|
saveLoadout,
|
|
6367
6714
|
saveMod,
|
|
@@ -6379,6 +6726,7 @@ ${mod.files.join("\n")}`;
|
|
|
6379
6726
|
steamLibraries,
|
|
6380
6727
|
syncActiveLoadout,
|
|
6381
6728
|
syncProjects,
|
|
6729
|
+
takeFamilyName,
|
|
6382
6730
|
tokenUser,
|
|
6383
6731
|
undo,
|
|
6384
6732
|
unpackOrCopy,
|