@edgemodders/edgedeck-mcp 0.28.0 → 0.29.1

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/bin/edgedeck.mjs CHANGED
@@ -23,7 +23,7 @@
23
23
  // edgedeck mcp serve the same verbs as MCP tools over stdio
24
24
  //
25
25
  // Options: --library <folder> (default %USERPROFILE%\EDGEdeck), --game <folder> (default: the
26
- // library's remembered game, else the Steam folder), --json.
26
+ // library's remembered game, else the game found through Steam, GOG or Epic), --json.
27
27
 
28
28
  import { createRequire } from "node:module";
29
29
  import * as fs from "node:fs";
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((resolve3, reject) => {
1254
- this._waiting.push({ thunk, resolve: resolve3, reject });
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 (resolve3, reject) => {
2880
+ return new Promise(async (resolve5, reject) => {
2881
2881
  const resolveWithCleanup = (r) => {
2882
- resolve3(r);
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((resolve3, reject) => {
3311
+ return new Promise((resolve5, reject) => {
3312
3312
  const callback = (error) => {
3313
3313
  if (error === void 0 || error === null) {
3314
- resolve3();
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 path14 = __importStar(require("path"));
3450
+ var path15 = __importStar(require("path"));
3451
3451
  var os4 = __importStar(require("os"));
3452
- var fs15 = __importStar(require("fs"));
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 = fs15.realpathSync(XDG_RUNTIME_DIR ?? os4.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 path14.join(tmpDir, `lsp-${randomSuffix}.sock`);
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((resolve3, _reject) => {
3607
- connectResolve = resolve3;
3606
+ const connected = new Promise((resolve5, _reject) => {
3607
+ connectResolve = resolve5;
3608
3608
  });
3609
- return new Promise((resolve3, reject) => {
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
- resolve3({
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((resolve3, _reject) => {
3638
- connectResolve = resolve3;
3637
+ const connected = new Promise((resolve5, _reject) => {
3638
+ connectResolve = resolve5;
3639
3639
  });
3640
- return new Promise((resolve3, reject) => {
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
- resolve3({
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,6 +3738,7 @@ __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,
3742
3744
  deployedRedmodPackages: () => deployedRedmodPackages,
@@ -3768,12 +3770,15 @@ __export(core_exports, {
3768
3770
  formatBytes: () => formatBytes,
3769
3771
  forwarderCommand: () => forwarderCommand,
3770
3772
  frameworkDetail: () => frameworkDetail,
3773
+ frameworkFamily: () => frameworkFamily,
3774
+ frameworkStates: () => frameworkStates,
3771
3775
  frameworkSummary: () => frameworkSummary,
3772
3776
  gameExe: () => gameExe,
3773
3777
  gameRunning: () => gameRunning,
3774
3778
  gameVersion: () => gameVersion,
3775
3779
  guessName: () => guessName,
3776
3780
  hasWebSocket: () => hasWebSocket,
3781
+ holdsLibrary: () => holdsLibrary,
3777
3782
  idFor: () => idFor,
3778
3783
  importFromVortex: () => importFromVortex,
3779
3784
  importLoadout: () => importLoadout,
@@ -3785,6 +3790,7 @@ __export(core_exports, {
3785
3790
  isGameFolder: () => isGameFolder,
3786
3791
  keepDownload: () => keepDownload,
3787
3792
  latestBridgeRelease: () => latestBridgeRelease,
3793
+ latestFrameworkRelease: () => latestFrameworkRelease,
3788
3794
  launchTarget: () => launchTarget,
3789
3795
  libraryFolder: () => libraryFolder,
3790
3796
  librarySize: () => librarySize,
@@ -3797,6 +3803,7 @@ __export(core_exports, {
3797
3803
  lostFiles: () => lostFiles,
3798
3804
  makeActive: () => makeActive,
3799
3805
  md5Of: () => md5Of,
3806
+ missingFrameworks: () => missingFrameworks,
3800
3807
  modDeployPaths: () => modDeployPaths,
3801
3808
  modFolder: () => modFolder,
3802
3809
  modPageUrl: () => modPageUrl,
@@ -3805,8 +3812,10 @@ __export(core_exports, {
3805
3812
  modsRemoved: () => modsRemoved,
3806
3813
  modsTouched: () => modsTouched,
3807
3814
  moveInSection: () => moveInSection,
3815
+ moveLibrary: () => moveLibrary,
3808
3816
  moveMod: () => moveMod,
3809
3817
  moveSection: () => moveSection,
3818
+ moveTarget: () => moveTarget,
3810
3819
  needsRefresh: () => needsRefresh,
3811
3820
  newLoadout: () => newLoadout,
3812
3821
  newestFile: () => newestFile,
@@ -3833,9 +3842,11 @@ __export(core_exports, {
3833
3842
  registerNxmHandler: () => registerNxmHandler,
3834
3843
  removeMod: () => removeMod,
3835
3844
  removeOlderBridges: () => removeOlderBridges,
3845
+ removeTemp: () => removeTemp,
3836
3846
  renameFamily: () => renameFamily,
3837
3847
  renameLoadout: () => renameLoadout,
3838
3848
  renameSection: () => renameSection,
3849
+ sameDrive: () => sameDrive,
3839
3850
  sameNameMod: () => sameNameMod,
3840
3851
  saveLibrary: () => saveLibrary,
3841
3852
  saveLoadout: () => saveLoadout,
@@ -3848,6 +3859,7 @@ __export(core_exports, {
3848
3859
  settleNewVersion: () => settleNewVersion,
3849
3860
  signIn: () => signIn,
3850
3861
  signInWithOAuth: () => signInWithOAuth,
3862
+ singleRednodeApp: () => singleRednodeApp,
3851
3863
  snapshot: () => snapshot,
3852
3864
  sourcePath: () => sourcePath,
3853
3865
  ssoPageUrl: () => ssoPageUrl,
@@ -3993,6 +4005,14 @@ function findRarTool() {
3993
4005
  if (unrar) return { exe: unrar, args: (file, dest) => ["x", "-y", "-o+", file, dest + path2.sep] };
3994
4006
  return void 0;
3995
4007
  }
4008
+ function removeTemp(dir) {
4009
+ try {
4010
+ fs2.rmSync(dir, { recursive: true, force: true, maxRetries: 6, retryDelay: 250 });
4011
+ return void 0;
4012
+ } catch (err) {
4013
+ return `could not remove the temporary folder ${dir} (${err instanceof Error ? err.message : String(err)}); Windows clears its temp folder in time`;
4014
+ }
4015
+ }
3996
4016
  async function unpackOrCopy(file, dest, sevenZa) {
3997
4017
  if (isArchiveFile(file)) return extractArchive(file, dest, sevenZa);
3998
4018
  fs2.mkdirSync(dest, { recursive: true });
@@ -4039,8 +4059,8 @@ var NexusClient = class {
4039
4059
  get usesToken() {
4040
4060
  return "bearer" in this.auth;
4041
4061
  }
4042
- async get(path14, retried = false) {
4043
- const res = await fetch(`${this.options.api ?? NexusApi}${path14}`, {
4062
+ async get(path15, retried = false) {
4063
+ const res = await fetch(`${this.options.api ?? NexusApi}${path15}`, {
4044
4064
  headers: {
4045
4065
  ..."bearer" in this.auth ? { authorization: `Bearer ${this.auth.bearer}` } : { apikey: this.auth.apiKey },
4046
4066
  accept: "application/json",
@@ -4057,7 +4077,7 @@ var NexusClient = class {
4057
4077
  const bearer = await this.options.refresh();
4058
4078
  if (bearer) {
4059
4079
  this.auth = { bearer };
4060
- return this.get(path14, true);
4080
+ return this.get(path15, true);
4061
4081
  }
4062
4082
  }
4063
4083
  if (!res.ok) {
@@ -4163,15 +4183,15 @@ async function installDotnetRuntime(log) {
4163
4183
  try {
4164
4184
  await download(DotnetInstallScript, script);
4165
4185
  log(`Installing the .NET 10 runtime into ${root}`);
4166
- await new Promise((resolve3, reject) => {
4186
+ await new Promise((resolve5, reject) => {
4167
4187
  (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) => {
4168
4188
  for (const line of `${stdout}
4169
4189
  ${stderr}`.split(/\r?\n/)) if (line.trim()) log(` ${line.trim()}`);
4170
- err ? reject(new Error(`dotnet-install failed: ${err.message}`)) : resolve3();
4190
+ err ? reject(new Error(`dotnet-install failed: ${err.message}`)) : resolve5();
4171
4191
  });
4172
4192
  });
4173
4193
  } finally {
4174
- fs4.rmSync(temp, { recursive: true, force: true });
4194
+ removeTemp(temp);
4175
4195
  }
4176
4196
  if (!hasNet10(root)) throw new Error("The .NET 10 runtime did not install. See the log.");
4177
4197
  return root;
@@ -4221,7 +4241,7 @@ async function installBridge(release, storageDir, sevenZa, progress, log) {
4221
4241
  fs4.mkdirSync(target, { recursive: true });
4222
4242
  await extractArchive(zip, target, sevenZa);
4223
4243
  } finally {
4224
- fs4.rmSync(temp, { recursive: true, force: true });
4244
+ removeTemp(temp);
4225
4245
  }
4226
4246
  if (!fs4.existsSync(exe)) throw new Error("The zip did not contain REDbridge.exe.");
4227
4247
  log(`REDbridge ${release.version} installed at ${exe}`);
@@ -4296,7 +4316,8 @@ function folderName(name) {
4296
4316
  return name.replace(/[<>:"/\\|?*]+/g, "").trim() || "Mod";
4297
4317
  }
4298
4318
  function rednodeManifest(placements) {
4299
- const p = placements.find((x) => /^red4ext\/plugins\/REDnode\/apps\/[^/]+\/rednode-app\.json$/i.test(x.to));
4319
+ const to = singleRednodeApp(placements.map((x) => x.to));
4320
+ const p = to === void 0 ? void 0 : placements.find((x) => x.to === to);
4300
4321
  if (!p) return void 0;
4301
4322
  try {
4302
4323
  const m = JSON.parse(fs5.readFileSync(p.from, "utf8"));
@@ -4306,6 +4327,12 @@ function rednodeManifest(placements) {
4306
4327
  return void 0;
4307
4328
  }
4308
4329
  }
4330
+ function singleRednodeApp(paths) {
4331
+ const manifests = paths.filter((f) => /^red4ext\/plugins\/REDnode\/apps\/[^/]+\/rednode-app\.json$/i.test(f));
4332
+ if (manifests.length !== 1) return void 0;
4333
+ if (paths.some((f) => /^red4ext\/plugins\/REDnode\//i.test(f) && !/^red4ext\/plugins\/REDnode\/apps\//i.test(f))) return void 0;
4334
+ return manifests[0];
4335
+ }
4309
4336
  function rednodeId(manifest) {
4310
4337
  try {
4311
4338
  const m = JSON.parse(fs5.readFileSync(manifest, "utf8"));
@@ -4699,6 +4726,21 @@ function canHardlink(libraryRoot, gamePath) {
4699
4726
  return false;
4700
4727
  }
4701
4728
  }
4729
+ var hardlinkable = /* @__PURE__ */ new Map();
4730
+ function deployMethod(libraryRoot, gamePath, current) {
4731
+ try {
4732
+ if (fs8.statSync(libraryRoot).dev !== fs8.statSync(gamePath).dev) return "copy";
4733
+ } catch {
4734
+ return current ?? "copy";
4735
+ }
4736
+ if (current === "link") return "link";
4737
+ const k = `${path7.resolve(libraryRoot).toLowerCase()}|${path7.resolve(gamePath).toLowerCase()}`;
4738
+ if (!hardlinkable.has(k)) hardlinkable.set(k, canHardlink(libraryRoot, gamePath));
4739
+ return hardlinkable.get(k) ? "link" : "copy";
4740
+ }
4741
+ function otherMethod(entry, method) {
4742
+ return method === "link" !== !!entry.ino;
4743
+ }
4702
4744
  function sourceReplaced(entry, method, d) {
4703
4745
  if (d.content !== void 0) return false;
4704
4746
  const src = identity(d.src);
@@ -4706,10 +4748,10 @@ function sourceReplaced(entry, method, d) {
4706
4748
  if (method === "link" && entry.ino) return src.ino !== entry.ino;
4707
4749
  return src.size !== entry.size || Math.abs(src.mtimeMs - entry.mtimeMs) >= 2e3;
4708
4750
  }
4709
- function stillOurs(entry, method, gamePath) {
4751
+ function stillOurs(entry, _method, gamePath) {
4710
4752
  const id = identity(gameFile(gamePath, entry.path));
4711
4753
  if (!id) return "missing";
4712
- if (method === "link" && entry.ino) {
4754
+ if (entry.ino) {
4713
4755
  if (id.ino !== entry.ino) return "changed";
4714
4756
  if (id.nlink < 2 && entry.mod !== Generated) return "sourceMissing";
4715
4757
  return "ok";
@@ -4725,7 +4767,7 @@ function generatedDiffers(file, content) {
4725
4767
  }
4726
4768
  function plan(lib, gamePath) {
4727
4769
  const manifest = readManifest(gamePath);
4728
- const method = manifest?.method ?? (canHardlink(lib.root, gamePath) ? "link" : "copy");
4770
+ const method = deployMethod(lib.root, gamePath, manifest?.method);
4729
4771
  const desired = /* @__PURE__ */ new Map();
4730
4772
  const conflicts = /* @__PURE__ */ new Map();
4731
4773
  for (const mod of lib.mods) {
@@ -4756,6 +4798,7 @@ function plan(lib, gamePath) {
4756
4798
  if (!e || (d.mod?.id ?? Generated) !== e.mod) return true;
4757
4799
  if (d.content !== void 0 && generatedDiffers(gameFile(gamePath, d.rel), d.content)) return true;
4758
4800
  if (stillOurs(e, method, gamePath) !== "ok") return true;
4801
+ if (d.content === void 0 && otherMethod(e, method)) return true;
4759
4802
  return sourceReplaced(e, method, d);
4760
4803
  });
4761
4804
  return { method, desired: [...desired.values()], conflicts: [...conflicts.values()], stale, add, vortex: vortexManages(gamePath) };
@@ -4890,6 +4933,7 @@ function deploy(lib, gamePath, decisions = /* @__PURE__ */ new Map(), progress,
4890
4933
  const p = plan(lib, gamePath);
4891
4934
  const manifest = readManifest(gamePath) ?? { version: 1, library: lib.root, gamePath, method: p.method, time: "", files: [], dirs: [] };
4892
4935
  manifest.library = lib.root;
4936
+ manifest.method = p.method;
4893
4937
  const result = { linked: 0, removed: 0, kept: 0, backedUp: 0, method: manifest.method, skipped: [], changes: [], modsInGame: 0, lost: lostFiles(p) };
4894
4938
  const entries = new Map(manifest.files.map((e) => [key(e.path), e]));
4895
4939
  const scope = only ? modDeployPaths(gamePath, p, only) : void 0;
@@ -5016,6 +5060,7 @@ function pendingChanges(lib, gamePath, p) {
5016
5060
  const st = stillOurs(e, p.method, gamePath);
5017
5061
  if (st === "missing") out.push({ path: d.rel, mod: id, kind: "restore", detail: "missing from the game folder" });
5018
5062
  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" });
5063
+ 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" });
5019
5064
  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)" });
5020
5065
  }
5021
5066
  }
@@ -5045,11 +5090,8 @@ function baseName(name) {
5045
5090
  return name.toLowerCase().replace(/[_\-]+/g, " ").replace(/\bv?\d+(?:\.\d+)+[a-z0-9]*\b/g, " ").replace(/[^a-z0-9]+/g, " ").trim();
5046
5091
  }
5047
5092
  function rednodePackageId(mod) {
5048
- for (const f of mod.files) {
5049
- const m = /^red4ext\/plugins\/REDnode\/apps\/([^/]+)\/rednode-app\.json$/i.exec(f);
5050
- if (m) return m[1].toLowerCase();
5051
- }
5052
- return void 0;
5093
+ const manifest = singleRednodeApp(mod.files);
5094
+ return manifest ? /^red4ext\/plugins\/REDnode\/apps\/([^/]+)\//i.exec(manifest)[1].toLowerCase() : void 0;
5053
5095
  }
5054
5096
  function familyKey(mod) {
5055
5097
  if (mod.family) return mod.family;
@@ -5192,28 +5234,165 @@ function frameworkDetail(c) {
5192
5234
  return lines;
5193
5235
  }
5194
5236
 
5195
- // src/loadouts.ts
5237
+ // src/frameworkGet.ts
5238
+ var FrameworkSources = [
5239
+ { name: "RED4ext", nexusId: 2380, repo: "WopsS/RED4ext", asset: /^red4ext-[\d.]+\.zip$/i, signature: "red4ext/RED4ext.dll" },
5240
+ { name: "Cyber Engine Tweaks", nexusId: 107, repo: "maximegmd/CyberEngineTweaks", asset: /^cet_[\d_]+\.zip$/i, signature: "bin/x64/plugins/cyber_engine_tweaks.asi" },
5241
+ { name: "redscript", nexusId: 1511, repo: "jac3km4/redscript", asset: /^redscript-v?[\d.]+-windows\.zip$/i, signature: "engine/tools/scc.exe" },
5242
+ { name: "ArchiveXL", nexusId: 4198, repo: "psiberx/cp2077-archive-xl", asset: /^ArchiveXL-[\d.]+\.zip$/i, signature: "red4ext/plugins/ArchiveXL/ArchiveXL.dll" },
5243
+ { name: "TweakXL", nexusId: 4197, repo: "psiberx/cp2077-tweak-xl", asset: /^TweakXL-[\d.]+\.zip$/i, signature: "red4ext/plugins/TweakXL/TweakXL.dll" },
5244
+ { name: "Codeware", nexusId: 7780, repo: "psiberx/cp2077-codeware", asset: /^Codeware-[\d.]+\.zip$/i, signature: "red4ext/plugins/Codeware/Codeware.dll" }
5245
+ ];
5246
+ function frameworkFamily(lib, f) {
5247
+ const sig = f.signature.toLowerCase();
5248
+ return families(lib).find((fam) => fam.nexusId === f.nexusId || fam.mods.some((m) => m.files.some((x) => x.toLowerCase() === sig)));
5249
+ }
5250
+ function missingFrameworks(lib) {
5251
+ return FrameworkSources.filter((f) => !frameworkFamily(lib, f));
5252
+ }
5253
+ async function latestFrameworkRelease(f, fetchImpl = fetch, api = "https://api.github.com") {
5254
+ const headers = { Accept: "application/vnd.github+json", "User-Agent": "EDGEdeck" };
5255
+ if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
5256
+ const res = await fetchImpl(`${api}/repos/${f.repo}/releases/latest`, { headers });
5257
+ 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}.`);
5258
+ const rel = await res.json();
5259
+ const asset = rel.assets.find((a) => f.asset.test(a.name));
5260
+ if (!asset) throw new Error(`release ${rel.tag_name} has no file named like ${f.asset.source}.`);
5261
+ 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 };
5262
+ }
5263
+ async function frameworkStates(lib, fetchImpl, api) {
5264
+ return Promise.all(
5265
+ FrameworkSources.map(async (source) => {
5266
+ const family = frameworkFamily(lib, source);
5267
+ try {
5268
+ const release = await latestFrameworkRelease(source, fetchImpl, api);
5269
+ const newest2 = family?.mods[0]?.version ?? "";
5270
+ return { source, family, release, wanted: !family || !!newest2 && versionNewer(release.version, newest2) };
5271
+ } catch (err) {
5272
+ return { source, family, error: err instanceof Error ? err.message : String(err), wanted: !family };
5273
+ }
5274
+ })
5275
+ );
5276
+ }
5277
+
5278
+ // src/libraryMove.ts
5196
5279
  var fs9 = __toESM(require("node:fs"));
5197
5280
  var path8 = __toESM(require("node:path"));
5281
+ function holdsLibrary(dir) {
5282
+ return fs9.existsSync(path8.join(dir, "library.json")) || fs9.existsSync(path8.join(dir, "library"));
5283
+ }
5284
+ function moveTarget(picked) {
5285
+ if (!fs9.existsSync(picked)) return picked;
5286
+ return fs9.readdirSync(picked).length ? path8.join(picked, "EDGEdeck") : picked;
5287
+ }
5288
+ function volume(p) {
5289
+ let dir = path8.resolve(p);
5290
+ for (; ; ) {
5291
+ try {
5292
+ return fs9.statSync(dir).dev;
5293
+ } catch {
5294
+ const up = path8.dirname(dir);
5295
+ if (up === dir) return void 0;
5296
+ dir = up;
5297
+ }
5298
+ }
5299
+ }
5300
+ function sameDrive(a, b) {
5301
+ const va = volume(a);
5302
+ const vb = volume(b);
5303
+ return va !== void 0 && va === vb;
5304
+ }
5305
+ function inside(child, parent) {
5306
+ const rel = path8.relative(parent, child);
5307
+ return !!rel && !rel.startsWith("..") && !path8.isAbsolute(rel);
5308
+ }
5309
+ function listFiles(root) {
5310
+ const out = [];
5311
+ const walk2 = (dir) => {
5312
+ for (const e of fs9.readdirSync(dir, { withFileTypes: true })) {
5313
+ const abs = path8.join(dir, e.name);
5314
+ if (e.isDirectory()) walk2(abs);
5315
+ else if (e.isFile()) out.push({ rel: path8.relative(root, abs), size: fs9.statSync(abs).size });
5316
+ }
5317
+ };
5318
+ walk2(root);
5319
+ return out;
5320
+ }
5321
+ async function moveLibrary(from, to, progress, cancelled) {
5322
+ const src = path8.resolve(from);
5323
+ const dest = path8.resolve(to);
5324
+ if (src.toLowerCase() === dest.toLowerCase()) throw new Error(`The library is already in ${dest}.`);
5325
+ if (inside(dest, src)) throw new Error(`${dest} is inside the library itself; pick a folder outside it.`);
5326
+ if (!holdsLibrary(src)) throw new Error(`${src} does not hold a library.`);
5327
+ const existed = fs9.existsSync(dest);
5328
+ if (existed && fs9.readdirSync(dest).length) throw new Error(`${dest} is not empty.`);
5329
+ if (sameDrive(src, dest)) {
5330
+ const files2 = listFiles(src);
5331
+ fs9.mkdirSync(path8.dirname(dest), { recursive: true });
5332
+ if (existed) fs9.rmdirSync(dest);
5333
+ fs9.renameSync(src, dest);
5334
+ return { how: "rename", files: files2.length, bytes: files2.reduce((n, f) => n + f.size, 0), leftBehind: [] };
5335
+ }
5336
+ const files = listFiles(src);
5337
+ const totalBytes = files.reduce((n, f) => n + f.size, 0);
5338
+ const done = { files: 0, totalFiles: files.length, bytes: 0, totalBytes };
5339
+ try {
5340
+ fs9.mkdirSync(dest, { recursive: true });
5341
+ for (const f of files) {
5342
+ if (cancelled?.()) throw new Error("Cancelled: the library stays where it was.");
5343
+ const a = path8.join(src, f.rel);
5344
+ const b = path8.join(dest, f.rel);
5345
+ fs9.mkdirSync(path8.dirname(b), { recursive: true });
5346
+ await fs9.promises.copyFile(a, b);
5347
+ const st = fs9.statSync(a);
5348
+ fs9.utimesSync(b, st.atime, st.mtime);
5349
+ if (fs9.statSync(b).size !== f.size) throw new Error(`${f.rel} did not copy whole.`);
5350
+ done.files++;
5351
+ done.bytes += f.size;
5352
+ progress?.({ ...done });
5353
+ }
5354
+ } catch (err) {
5355
+ fs9.rmSync(dest, { recursive: true, force: true });
5356
+ if (existed) fs9.mkdirSync(dest, { recursive: true });
5357
+ throw err;
5358
+ }
5359
+ const leftBehind = [];
5360
+ for (const f of files) {
5361
+ try {
5362
+ fs9.rmSync(path8.join(src, f.rel), { force: true });
5363
+ } catch {
5364
+ leftBehind.push(path8.join(src, f.rel));
5365
+ }
5366
+ }
5367
+ try {
5368
+ fs9.rmSync(src, { recursive: true, force: true });
5369
+ } catch {
5370
+ }
5371
+ return { how: "copy", files: files.length, bytes: totalBytes, leftBehind };
5372
+ }
5373
+
5374
+ // src/loadouts.ts
5375
+ var fs10 = __toESM(require("node:fs"));
5376
+ var path9 = __toESM(require("node:path"));
5198
5377
  var DefaultLoadout = "Default";
5199
5378
  function loadoutsFolder(root) {
5200
- return path8.join(root, "loadouts");
5379
+ return path9.join(root, "loadouts");
5201
5380
  }
5202
5381
  function slug(name) {
5203
5382
  return name.replace(/[<>:"/\\|?*]+/g, "-").trim() || "loadout";
5204
5383
  }
5205
5384
  function readLoadout(file) {
5206
- return JSON.parse(fs9.readFileSync(file, "utf8"));
5385
+ return JSON.parse(fs10.readFileSync(file, "utf8"));
5207
5386
  }
5208
5387
  function listLoadouts(root) {
5209
5388
  const dir = loadoutsFolder(root);
5210
- if (!fs9.existsSync(dir)) return [];
5389
+ if (!fs10.existsSync(dir)) return [];
5211
5390
  const out = [];
5212
- for (const f of fs9.readdirSync(dir)) {
5391
+ for (const f of fs10.readdirSync(dir)) {
5213
5392
  if (!f.toLowerCase().endsWith(".json")) continue;
5214
5393
  try {
5215
- const l = readLoadout(path8.join(dir, f));
5216
- out.push({ name: l.name, file: path8.join(dir, f), saved: l.saved, mods: l.mods.length, on: l.mods.filter((m) => m.enabled).length });
5394
+ const l = readLoadout(path9.join(dir, f));
5395
+ 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 });
5217
5396
  } catch {
5218
5397
  }
5219
5398
  }
@@ -5242,13 +5421,13 @@ function entryFor(f, mod, enabled) {
5242
5421
  return { family: f.key, id: mod.id, name: f.name, version: mod.version, enabled, nexusModId: f.nexusId };
5243
5422
  }
5244
5423
  function writeLoadout(root, loadout, file) {
5245
- fs9.mkdirSync(loadoutsFolder(root), { recursive: true });
5424
+ fs10.mkdirSync(loadoutsFolder(root), { recursive: true });
5246
5425
  let target = file;
5247
5426
  if (!target) {
5248
- target = path8.join(loadoutsFolder(root), `${slug(loadout.name)}.json`);
5249
- for (let n = 2; fs9.existsSync(target); n++) target = path8.join(loadoutsFolder(root), `${slug(loadout.name)}-${n}.json`);
5427
+ target = path9.join(loadoutsFolder(root), `${slug(loadout.name)}.json`);
5428
+ for (let n = 2; fs10.existsSync(target); n++) target = path9.join(loadoutsFolder(root), `${slug(loadout.name)}-${n}.json`);
5250
5429
  }
5251
- fs9.writeFileSync(target, JSON.stringify(loadout, null, 2) + "\n");
5430
+ fs10.writeFileSync(target, JSON.stringify(loadout, null, 2) + "\n");
5252
5431
  return target;
5253
5432
  }
5254
5433
  function saveLoadout(lib, name, gameVersion2) {
@@ -5352,7 +5531,7 @@ function renameLoadout(lib, from, to) {
5352
5531
  if (!info) throw new Error(`There is no loadout called ${from}.`);
5353
5532
  const loadout = readLoadout(info.file);
5354
5533
  loadout.name = to.trim();
5355
- fs9.rmSync(info.file);
5534
+ fs10.rmSync(info.file);
5356
5535
  writeLoadout(lib.root, loadout);
5357
5536
  if (lib.info.activeLoadout === from) {
5358
5537
  lib.info.activeLoadout = to.trim();
@@ -5362,18 +5541,18 @@ function renameLoadout(lib, from, to) {
5362
5541
  function deleteLoadout(lib, name) {
5363
5542
  if (lib.info.activeLoadout === name) throw new Error(`${name} is the active loadout; make another one active first.`);
5364
5543
  const info = findLoadout(lib.root, name);
5365
- if (info) fs9.rmSync(info.file);
5544
+ if (info) fs10.rmSync(info.file);
5366
5545
  }
5367
5546
  function exportLoadout(lib, name, dest, gameVersion2) {
5368
5547
  if (lib.info.activeLoadout === name) syncActiveLoadout(lib, gameVersion2);
5369
5548
  const info = findLoadout(lib.root, name);
5370
5549
  if (!info) throw new Error(`There is no loadout called ${name}.`);
5371
- fs9.copyFileSync(info.file, dest);
5550
+ fs10.copyFileSync(info.file, dest);
5372
5551
  }
5373
5552
  function importLoadout(lib, file) {
5374
5553
  const loadout = readLoadout(file);
5375
- if (!Array.isArray(loadout.mods)) throw new Error(`${path8.basename(file)} is not a EDGEdeck loadout.`);
5376
- let name = (loadout.name || path8.basename(file, ".json")).trim();
5554
+ if (!Array.isArray(loadout.mods)) throw new Error(`${path9.basename(file)} is not a EDGEdeck loadout.`);
5555
+ let name = (loadout.name || path9.basename(file, ".json")).trim();
5377
5556
  for (let n = 2; findLoadout(lib.root, name); n++) name = `${(loadout.name || "Loadout").trim()} (${n})`;
5378
5557
  writeLoadout(lib.root, { ...loadout, name, saved: (/* @__PURE__ */ new Date()).toISOString() });
5379
5558
  return name;
@@ -5398,7 +5577,7 @@ function signIn(options) {
5398
5577
  let done = false;
5399
5578
  let attempts = 0;
5400
5579
  let socket;
5401
- return new Promise((resolve3, reject) => {
5580
+ return new Promise((resolve5, reject) => {
5402
5581
  const finish = (err, key2) => {
5403
5582
  if (done) return;
5404
5583
  done = true;
@@ -5408,7 +5587,7 @@ function signIn(options) {
5408
5587
  socket?.close();
5409
5588
  } catch {
5410
5589
  }
5411
- if (key2) resolve3(key2);
5590
+ if (key2) resolve5(key2);
5412
5591
  else reject(err ?? new Error("Nexus sign-in failed"));
5413
5592
  };
5414
5593
  const onAbort = () => finish(new Error("Sign-in cancelled"));
@@ -5539,7 +5718,7 @@ function signInWithOAuth(options) {
5539
5718
  const { verifier, challenge } = pkce();
5540
5719
  const state = crypto3.randomUUID();
5541
5720
  let done = false;
5542
- return new Promise((resolve3, reject) => {
5721
+ return new Promise((resolve5, reject) => {
5543
5722
  const server = http.createServer();
5544
5723
  const finish = (err, tokens) => {
5545
5724
  if (done) return;
@@ -5547,7 +5726,7 @@ function signInWithOAuth(options) {
5547
5726
  clearTimeout(timer);
5548
5727
  options.signal?.removeEventListener("abort", onAbort);
5549
5728
  server.close();
5550
- if (tokens) resolve3(tokens);
5729
+ if (tokens) resolve5(tokens);
5551
5730
  else reject(err ?? new Error("Nexus sign-in failed"));
5552
5731
  };
5553
5732
  const onAbort = () => finish(new Error("Sign-in cancelled"));
@@ -5668,10 +5847,10 @@ function describeHandler(command) {
5668
5847
  }
5669
5848
 
5670
5849
  // src/project.ts
5671
- var fs10 = __toESM(require("node:fs"));
5672
- var path9 = __toESM(require("node:path"));
5850
+ var fs11 = __toESM(require("node:fs"));
5851
+ var path10 = __toESM(require("node:path"));
5673
5852
  function addToProject(lib, mod, files, projectFile, replace = /* @__PURE__ */ new Set()) {
5674
- const resources = path9.join(path9.dirname(projectFile), "source", "resources");
5853
+ const resources = path10.join(path10.dirname(projectFile), "source", "resources");
5675
5854
  const out = { copied: [], same: [], kept: [], archives: [] };
5676
5855
  for (const f of files) {
5677
5856
  if (f.toLowerCase().endsWith(".archive")) {
@@ -5679,10 +5858,10 @@ function addToProject(lib, mod, files, projectFile, replace = /* @__PURE__ */ ne
5679
5858
  continue;
5680
5859
  }
5681
5860
  const src = sourcePath(lib, mod, f);
5682
- const rel = path9.join("source", "resources", ...f.split("/"));
5683
- const dest = path9.join(resources, ...f.split("/"));
5684
- if (fs10.existsSync(dest)) {
5685
- if (fs10.readFileSync(dest).equals(fs10.readFileSync(src))) {
5861
+ const rel = path10.join("source", "resources", ...f.split("/"));
5862
+ const dest = path10.join(resources, ...f.split("/"));
5863
+ if (fs11.existsSync(dest)) {
5864
+ if (fs11.readFileSync(dest).equals(fs11.readFileSync(src))) {
5686
5865
  out.same.push(rel);
5687
5866
  continue;
5688
5867
  }
@@ -5691,39 +5870,39 @@ function addToProject(lib, mod, files, projectFile, replace = /* @__PURE__ */ ne
5691
5870
  continue;
5692
5871
  }
5693
5872
  }
5694
- fs10.mkdirSync(path9.dirname(dest), { recursive: true });
5695
- fs10.copyFileSync(src, dest);
5873
+ fs11.mkdirSync(path10.dirname(dest), { recursive: true });
5874
+ fs11.copyFileSync(src, dest);
5696
5875
  out.copied.push(rel);
5697
5876
  }
5698
5877
  return out;
5699
5878
  }
5700
5879
  function findProjectFile(target) {
5701
- if (fs10.existsSync(target) && fs10.statSync(target).isFile()) return target.toLowerCase().endsWith(".cpmodproj") ? target : void 0;
5702
- const files = fs10.existsSync(target) ? fs10.readdirSync(target).filter((f) => f.toLowerCase().endsWith(".cpmodproj")) : [];
5703
- return files.length ? path9.join(target, files[0]) : void 0;
5880
+ if (fs11.existsSync(target) && fs11.statSync(target).isFile()) return target.toLowerCase().endsWith(".cpmodproj") ? target : void 0;
5881
+ const files = fs11.existsSync(target) ? fs11.readdirSync(target).filter((f) => f.toLowerCase().endsWith(".cpmodproj")) : [];
5882
+ return files.length ? path10.join(target, files[0]) : void 0;
5704
5883
  }
5705
5884
  function projectName(projectFile) {
5706
5885
  try {
5707
- const xml = fs10.readFileSync(projectFile, "utf8");
5886
+ const xml = fs11.readFileSync(projectFile, "utf8");
5708
5887
  const m = /<ModName>\s*([^<]+?)\s*<\//.exec(xml) ?? /<Name>\s*([^<]+?)\s*<\//.exec(xml);
5709
5888
  if (m) return m[1];
5710
5889
  } catch {
5711
5890
  }
5712
- return path9.basename(projectFile, path9.extname(projectFile));
5891
+ return path10.basename(projectFile, path10.extname(projectFile));
5713
5892
  }
5714
5893
  function packedFolder(projectFile) {
5715
- return path9.join(path9.dirname(projectFile), "packed");
5894
+ return path10.join(path10.dirname(projectFile), "packed");
5716
5895
  }
5717
5896
  function classifyProject(projectFile) {
5718
5897
  const packed = packedFolder(projectFile);
5719
- if (!fs10.existsSync(packed)) return { placements: [], kinds: [], unplaced: [], ignored: [] };
5898
+ if (!fs11.existsSync(packed)) return { placements: [], kinds: [], unplaced: [], ignored: [] };
5720
5899
  return classify(packed, projectName(projectFile));
5721
5900
  }
5722
5901
  function newest(dir) {
5723
5902
  let t = 0;
5724
- for (const e of fs10.readdirSync(dir, { withFileTypes: true })) {
5725
- const p = path9.join(dir, e.name);
5726
- t = Math.max(t, e.isDirectory() ? newest(p) : fs10.statSync(p).mtimeMs);
5903
+ for (const e of fs11.readdirSync(dir, { withFileTypes: true })) {
5904
+ const p = path10.join(dir, e.name);
5905
+ t = Math.max(t, e.isDirectory() ? newest(p) : fs11.statSync(p).mtimeMs);
5727
5906
  }
5728
5907
  return t;
5729
5908
  }
@@ -5732,10 +5911,10 @@ function syncProjects(lib) {
5732
5911
  for (const mod of lib.mods) {
5733
5912
  if (mod.source.type !== "project" || !mod.source.path) continue;
5734
5913
  const packed = packedFolder(mod.source.path);
5735
- if (!fs10.existsSync(packed)) continue;
5914
+ if (!fs11.existsSync(packed)) continue;
5736
5915
  const libNewest = mod.files.reduce((t, f) => {
5737
5916
  try {
5738
- return Math.max(t, fs10.statSync(sourcePath(lib, mod, f)).mtimeMs);
5917
+ return Math.max(t, fs11.statSync(sourcePath(lib, mod, f)).mtimeMs);
5739
5918
  } catch {
5740
5919
  return t;
5741
5920
  }
@@ -5746,11 +5925,11 @@ function syncProjects(lib) {
5746
5925
  const keep = /* @__PURE__ */ new Set();
5747
5926
  for (const p of c.placements) {
5748
5927
  const dest = sourcePath(lib, mod, p.to);
5749
- fs10.mkdirSync(path9.dirname(dest), { recursive: true });
5750
- fs10.copyFileSync(p.from, dest);
5928
+ fs11.mkdirSync(path10.dirname(dest), { recursive: true });
5929
+ fs11.copyFileSync(p.from, dest);
5751
5930
  keep.add(p.to);
5752
5931
  }
5753
- for (const f of mod.files) if (!keep.has(f)) fs10.rmSync(sourcePath(lib, mod, f), { force: true });
5932
+ for (const f of mod.files) if (!keep.has(f)) fs11.rmSync(sourcePath(lib, mod, f), { force: true });
5754
5933
  mod.files = [...keep].sort();
5755
5934
  mod.kinds = c.kinds;
5756
5935
  saveMod(lib, mod);
@@ -5806,11 +5985,11 @@ function installProject(lib, projectFile, beside) {
5806
5985
  const keep = /* @__PURE__ */ new Set();
5807
5986
  for (const p of c.placements) {
5808
5987
  const dest = sourcePath(lib, existing, p.to);
5809
- fs10.mkdirSync(path9.dirname(dest), { recursive: true });
5810
- fs10.copyFileSync(p.from, dest);
5988
+ fs11.mkdirSync(path10.dirname(dest), { recursive: true });
5989
+ fs11.copyFileSync(p.from, dest);
5811
5990
  keep.add(p.to);
5812
5991
  }
5813
- for (const f of existing.files) if (!keep.has(f)) fs10.rmSync(sourcePath(lib, existing, f), { force: true });
5992
+ for (const f of existing.files) if (!keep.has(f)) fs11.rmSync(sourcePath(lib, existing, f), { force: true });
5814
5993
  existing.files = [...keep].sort();
5815
5994
  existing.kinds = c.kinds;
5816
5995
  activateVersion(lib, existing);
@@ -5821,12 +6000,12 @@ function installProject(lib, projectFile, beside) {
5821
6000
 
5822
6001
  // src/redmod.ts
5823
6002
  var import_node_child_process6 = require("node:child_process");
5824
- var fs11 = __toESM(require("node:fs"));
5825
- var path10 = __toESM(require("node:path"));
6003
+ var fs12 = __toESM(require("node:fs"));
6004
+ var path11 = __toESM(require("node:path"));
5826
6005
  var import_node_util4 = require("node:util");
5827
6006
  var run4 = (0, import_node_util4.promisify)(import_node_child_process6.execFile);
5828
6007
  function redModExe(gamePath) {
5829
- return path10.join(gamePath, "tools", "redmod", "bin", "redMod.exe");
6008
+ return path11.join(gamePath, "tools", "redmod", "bin", "redMod.exe");
5830
6009
  }
5831
6010
  function enabledRedmodPackages(lib) {
5832
6011
  const names = [];
@@ -5853,13 +6032,13 @@ function deployedRedmodPackages(lib, gamePath) {
5853
6032
  async function deployRedmod(lib, gamePath, log, packages = enabledRedmodPackages(lib)) {
5854
6033
  const exe = redModExe(gamePath);
5855
6034
  if (!packages.length) return { ran: false, packages, output: "no REDmod packages enabled" };
5856
- if (!fs11.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` };
5857
- const listFile = path10.join(lib.root, "redmod-modlist.txt");
5858
- fs11.writeFileSync(listFile, packages.join("\r\n") + "\r\n");
5859
- const args = ["deploy", `-root=${gamePath}`, `-modlist=${listFile}`, `-rttiSchemaPath=${path10.join(gamePath, "tools", "redmod", "metadata.json")}`];
6035
+ 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` };
6036
+ const listFile = path11.join(lib.root, "redmod-modlist.txt");
6037
+ fs12.writeFileSync(listFile, packages.join("\r\n") + "\r\n");
6038
+ const args = ["deploy", `-root=${gamePath}`, `-modlist=${listFile}`, `-rttiSchemaPath=${path11.join(gamePath, "tools", "redmod", "metadata.json")}`];
5860
6039
  log?.(`redMod.exe ${args.join(" ")}`);
5861
6040
  try {
5862
- const { stdout, stderr } = await run4(exe, args, { cwd: path10.dirname(exe), windowsHide: true, maxBuffer: 1 << 26 });
6041
+ const { stdout, stderr } = await run4(exe, args, { cwd: path11.dirname(exe), windowsHide: true, maxBuffer: 1 << 26 });
5863
6042
  const output = (stdout + stderr).trim();
5864
6043
  for (const line of output.split(/\r?\n/)) if (line.trim()) log?.(` ${line}`);
5865
6044
  return { ran: true, packages, output };
@@ -5977,8 +6156,8 @@ function setSectionOn(lib, name, on) {
5977
6156
  }
5978
6157
 
5979
6158
  // src/sizes.ts
5980
- var fs12 = __toESM(require("node:fs"));
5981
- var path11 = __toESM(require("node:path"));
6159
+ var fs13 = __toESM(require("node:fs"));
6160
+ var path12 = __toESM(require("node:path"));
5982
6161
  var cache = /* @__PURE__ */ new Map();
5983
6162
  function modSize(lib, mod) {
5984
6163
  const stamp = `${mod.files.length}:${mod.added}:${lib.root}`;
@@ -5991,7 +6170,7 @@ function modSize(lib, mod) {
5991
6170
  for (const f of mod.files) {
5992
6171
  let n = 0;
5993
6172
  try {
5994
- const st = fs12.statSync(path11.join(folder, ...f.split("/")));
6173
+ const st = fs13.statSync(path12.join(folder, ...f.split("/")));
5995
6174
  n = st.size;
5996
6175
  mtimes.set(f, st.mtimeMs);
5997
6176
  } catch {
@@ -6010,10 +6189,10 @@ function forgetSizes(modId) {
6010
6189
  function folderSize(dir) {
6011
6190
  let total = 0;
6012
6191
  try {
6013
- for (const e of fs12.readdirSync(dir, { withFileTypes: true })) {
6014
- const p = path11.join(dir, e.name);
6192
+ for (const e of fs13.readdirSync(dir, { withFileTypes: true })) {
6193
+ const p = path12.join(dir, e.name);
6015
6194
  if (e.isDirectory()) total += folderSize(p);
6016
- else if (e.isFile()) total += fs12.statSync(p).size;
6195
+ else if (e.isFile()) total += fs13.statSync(p).size;
6017
6196
  }
6018
6197
  } catch {
6019
6198
  }
@@ -6034,20 +6213,20 @@ function formatBytes(n) {
6034
6213
  }
6035
6214
 
6036
6215
  // src/verbs.ts
6037
- var fs14 = __toESM(require("node:fs"));
6216
+ var fs15 = __toESM(require("node:fs"));
6038
6217
  var os3 = __toESM(require("node:os"));
6039
- var path13 = __toESM(require("node:path"));
6218
+ var path14 = __toESM(require("node:path"));
6040
6219
 
6041
6220
  // src/vortex.ts
6042
- var fs13 = __toESM(require("node:fs"));
6043
- var path12 = __toESM(require("node:path"));
6221
+ var fs14 = __toESM(require("node:fs"));
6222
+ var path13 = __toESM(require("node:path"));
6044
6223
  function vortexStagingPath() {
6045
- return path12.join(process.env.APPDATA ?? path12.join(process.env.USERPROFILE ?? "", "AppData", "Roaming"), "Vortex", "cyberpunk2077", "mods");
6224
+ return path13.join(process.env.APPDATA ?? path13.join(process.env.USERPROFILE ?? "", "AppData", "Roaming"), "Vortex", "cyberpunk2077", "mods");
6046
6225
  }
6047
6226
  function manifestStaging(gamePath) {
6048
6227
  if (!gamePath) return void 0;
6049
6228
  try {
6050
- const m = JSON.parse(fs13.readFileSync(path12.join(gamePath, "vortex.deployment.json"), "utf8"));
6229
+ const m = JSON.parse(fs14.readFileSync(path13.join(gamePath, "vortex.deployment.json"), "utf8"));
6051
6230
  return m.stagingPath || void 0;
6052
6231
  } catch {
6053
6232
  return void 0;
@@ -6055,18 +6234,18 @@ function manifestStaging(gamePath) {
6055
6234
  }
6056
6235
  function driveRoots(gamePath) {
6057
6236
  if (process.platform !== "win32") return [];
6058
- const first = gamePath ? path12.parse(path12.resolve(gamePath)).root.toUpperCase() : "";
6237
+ const first = gamePath ? path13.parse(path13.resolve(gamePath)).root.toUpperCase() : "";
6059
6238
  const roots = first ? [first] : [];
6060
6239
  for (let c = 67; c <= 90; c++) {
6061
6240
  const r = `${String.fromCharCode(c)}:\\`;
6062
- if (r !== first && fs13.existsSync(r)) roots.push(r);
6241
+ if (r !== first && fs14.existsSync(r)) roots.push(r);
6063
6242
  }
6064
6243
  return roots;
6065
6244
  }
6066
6245
  function vortexStagingCandidates(gamePath, setting) {
6067
6246
  const list = [setting, manifestStaging(gamePath), vortexStagingPath()];
6068
6247
  for (const root of driveRoots(gamePath)) {
6069
- list.push(path12.join(root, "Vortex Mods", "cyberpunk2077"), path12.join(root, "VortexMods", "cyberpunk2077"), path12.join(root, "Vortex", "cyberpunk2077", "mods"), path12.join(root, "Vortex", "mods", "cyberpunk2077"));
6248
+ 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"));
6070
6249
  }
6071
6250
  const seen = /* @__PURE__ */ new Set();
6072
6251
  return list.filter((p) => !!p && !seen.has(p.toLowerCase()) && !!seen.add(p.toLowerCase()));
@@ -6074,7 +6253,7 @@ function vortexStagingCandidates(gamePath, setting) {
6074
6253
  function findVortexStaging(gamePath, setting) {
6075
6254
  for (const dir of vortexStagingCandidates(gamePath, setting)) {
6076
6255
  try {
6077
- if (fs13.readdirSync(dir, { withFileTypes: true }).some((e) => e.isDirectory() && !e.name.startsWith("__"))) return dir;
6256
+ if (fs14.readdirSync(dir, { withFileTypes: true }).some((e) => e.isDirectory() && !e.name.startsWith("__"))) return dir;
6078
6257
  } catch {
6079
6258
  }
6080
6259
  }
@@ -6082,24 +6261,24 @@ function findVortexStaging(gamePath, setting) {
6082
6261
  }
6083
6262
  function countFiles(dir) {
6084
6263
  let n = 0;
6085
- for (const e of fs13.readdirSync(dir, { withFileTypes: true })) {
6086
- if (e.isDirectory()) n += countFiles(path12.join(dir, e.name));
6264
+ for (const e of fs14.readdirSync(dir, { withFileTypes: true })) {
6265
+ if (e.isDirectory()) n += countFiles(path13.join(dir, e.name));
6087
6266
  else n++;
6088
6267
  }
6089
6268
  return n;
6090
6269
  }
6091
6270
  function guessName(folder) {
6092
6271
  const sub = (...parts) => {
6093
- const d = path12.join(folder, ...parts);
6094
- if (!fs13.existsSync(d)) return void 0;
6095
- const names = fs13.readdirSync(d, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6272
+ const d = path13.join(folder, ...parts);
6273
+ if (!fs14.existsSync(d)) return void 0;
6274
+ const names = fs14.readdirSync(d, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6096
6275
  return names.length === 1 ? names[0] : void 0;
6097
6276
  };
6098
6277
  const named = sub("r6", "scripts") ?? sub("bin", "x64", "plugins", "cyber_engine_tweaks", "mods") ?? sub("red4ext", "plugins") ?? sub("r6", "tweaks") ?? sub("mods");
6099
6278
  if (named) return named;
6100
- const archives = path12.join(folder, "archive", "pc", "mod");
6101
- if (fs13.existsSync(archives)) {
6102
- const a = fs13.readdirSync(archives).find((f) => f.toLowerCase().endsWith(".archive"));
6279
+ const archives = path13.join(folder, "archive", "pc", "mod");
6280
+ if (fs14.existsSync(archives)) {
6281
+ const a = fs14.readdirSync(archives).find((f) => f.toLowerCase().endsWith(".archive"));
6103
6282
  if (a) return a.replace(/\.archive$/i, "").replace(/^[#!_0-9]+/, "") || a;
6104
6283
  }
6105
6284
  return void 0;
@@ -6109,21 +6288,21 @@ function listVortexMods(gamePath, staging) {
6109
6288
  if (!dir) return [];
6110
6289
  staging = dir;
6111
6290
  const deployed = /* @__PURE__ */ new Set();
6112
- const manifest = path12.join(gamePath, "vortex.deployment.json");
6113
- if (fs13.existsSync(manifest)) {
6291
+ const manifest = path13.join(gamePath, "vortex.deployment.json");
6292
+ if (fs14.existsSync(manifest)) {
6114
6293
  try {
6115
- const m = JSON.parse(fs13.readFileSync(manifest, "utf8"));
6294
+ const m = JSON.parse(fs14.readFileSync(manifest, "utf8"));
6116
6295
  for (const f of m.files ?? []) deployed.add(f.source);
6117
6296
  } catch {
6118
6297
  }
6119
6298
  }
6120
6299
  const mods = [];
6121
- for (const entry of fs13.readdirSync(staging, { withFileTypes: true })) {
6300
+ for (const entry of fs14.readdirSync(staging, { withFileTypes: true })) {
6122
6301
  if (!entry.isDirectory() || entry.name.startsWith("__")) continue;
6123
- const folder = path12.join(staging, entry.name);
6302
+ const folder = path13.join(staging, entry.name);
6124
6303
  const files = countFiles(folder);
6125
6304
  if (files === 0) continue;
6126
- if (fs13.existsSync(path12.join(folder, "collection.json")) && files === 1) continue;
6305
+ if (fs14.existsSync(path13.join(folder, "collection.json")) && files === 1) continue;
6127
6306
  const m = /^(.*?)-(\d+)-([\d-]+)-(\d{10})$/.exec(entry.name);
6128
6307
  const guid = /^[0-9a-f]{2}_[0-9a-f]{2}_[0-9a-f]{2}_[0-9a-f-]{36}$/i.test(entry.name);
6129
6308
  mods.push({
@@ -6211,7 +6390,7 @@ var verbs = {
6211
6390
  /** Every mod with its status and size, the library totals and the frameworks line. */
6212
6391
  async list(s, largestFirst = false) {
6213
6392
  const { lib, game } = s;
6214
- const p = fs14.existsSync(game) && isGameFolder(game) ? plan(lib, game) : void 0;
6393
+ const p = fs15.existsSync(game) && isGameFolder(game) ? plan(lib, game) : void 0;
6215
6394
  const manifest = p ? readManifest(game) : void 0;
6216
6395
  const changes = p ? detectChanges(game) : [];
6217
6396
  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) : "" }));
@@ -6253,14 +6432,14 @@ ${mod.files.join("\n")}`;
6253
6432
  },
6254
6433
  /** Adds a zip, 7z, rar, a single mod file or a folder. An archive with options needs the option's label. */
6255
6434
  async add(s, source, name, option) {
6256
- if (!fs14.existsSync(source)) throw new Error(`${source} does not exist`);
6257
- const isFile = fs14.statSync(source).isFile();
6258
- const modName = name ?? path13.basename(source, isFile ? path13.extname(source) : "");
6435
+ if (!fs15.existsSync(source)) throw new Error(`${source} does not exist`);
6436
+ const isFile = fs15.statSync(source).isFile();
6437
+ const modName = name ?? path14.basename(source, isFile ? path14.extname(source) : "");
6259
6438
  let folder = source;
6260
6439
  let temp;
6261
6440
  try {
6262
6441
  if (isFile) {
6263
- temp = fs14.mkdtempSync(path13.join(os3.tmpdir(), "edgedeck-verb-"));
6442
+ temp = fs15.mkdtempSync(path14.join(os3.tmpdir(), "edgedeck-verb-"));
6264
6443
  await unpackOrCopy(source, temp, s.sevenZa);
6265
6444
  folder = temp;
6266
6445
  }
@@ -6277,7 +6456,7 @@ ${mod.files.join("\n")}`;
6277
6456
  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` : "";
6278
6457
  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(", ")}` : ""}` };
6279
6458
  } finally {
6280
- if (temp) fs14.rmSync(temp, { recursive: true, force: true });
6459
+ if (temp) removeTemp(temp);
6281
6460
  }
6282
6461
  },
6283
6462
  /** Adds a REDcode or WolvenKit project's packed output, or refreshes the mod that follows it, and turns it on. */
@@ -6287,9 +6466,9 @@ ${mod.files.join("\n")}`;
6287
6466
  * place). Without it, the project is a mod of its own and the text says what it sits beside.
6288
6467
  */
6289
6468
  installProject(s, projectFile, sameName) {
6290
- if (!fs14.existsSync(projectFile)) throw new Error(`${projectFile} does not exist`);
6469
+ if (!fs15.existsSync(projectFile)) throw new Error(`${projectFile} does not exist`);
6291
6470
  const other = sameNameMod(s.lib, projectFile);
6292
- const before = other && `${other.id}${other.source.type === "project" && other.source.path ? ` (${path13.dirname(other.source.path)})` : ""}`;
6471
+ const before = other && `${other.id}${other.source.type === "project" && other.source.path ? ` (${path14.dirname(other.source.path)})` : ""}`;
6293
6472
  const { mod, added, beside } = installProject(s.lib, projectFile, other && sameName ? { mod: other, how: sameName } : void 0);
6294
6473
  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)` : "";
6295
6474
  return {
@@ -6373,7 +6552,7 @@ ${mod.files.join("\n")}`;
6373
6552
  }
6374
6553
  if (action === "load") {
6375
6554
  if (!name) throw new Error("loadout load needs a name or file");
6376
- const target = fs14.existsSync(name) && name.toLowerCase().endsWith(".json") ? importLoadout(s.lib, name) : name;
6555
+ const target = fs15.existsSync(name) && name.toLowerCase().endsWith(".json") ? importLoadout(s.lib, name) : name;
6377
6556
  const r = makeActive(s.lib, target);
6378
6557
  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` };
6379
6558
  }
@@ -6397,6 +6576,7 @@ ${mod.files.join("\n")}`;
6397
6576
  BridgeClient,
6398
6577
  BridgeRepo,
6399
6578
  DefaultLoadout,
6579
+ FrameworkSources,
6400
6580
  FrameworksSection,
6401
6581
  KnownModNames,
6402
6582
  ManifestName,
@@ -6434,6 +6614,7 @@ ${mod.files.join("\n")}`;
6434
6614
  deleteLoadout,
6435
6615
  deleteSection,
6436
6616
  deploy,
6617
+ deployMethod,
6437
6618
  deployRedmod,
6438
6619
  deployReportsFolder,
6439
6620
  deployedRedmodPackages,
@@ -6465,12 +6646,15 @@ ${mod.files.join("\n")}`;
6465
6646
  formatBytes,
6466
6647
  forwarderCommand,
6467
6648
  frameworkDetail,
6649
+ frameworkFamily,
6650
+ frameworkStates,
6468
6651
  frameworkSummary,
6469
6652
  gameExe,
6470
6653
  gameRunning,
6471
6654
  gameVersion,
6472
6655
  guessName,
6473
6656
  hasWebSocket,
6657
+ holdsLibrary,
6474
6658
  idFor,
6475
6659
  importFromVortex,
6476
6660
  importLoadout,
@@ -6482,6 +6666,7 @@ ${mod.files.join("\n")}`;
6482
6666
  isGameFolder,
6483
6667
  keepDownload,
6484
6668
  latestBridgeRelease,
6669
+ latestFrameworkRelease,
6485
6670
  launchTarget,
6486
6671
  libraryFolder,
6487
6672
  librarySize,
@@ -6494,6 +6679,7 @@ ${mod.files.join("\n")}`;
6494
6679
  lostFiles,
6495
6680
  makeActive,
6496
6681
  md5Of,
6682
+ missingFrameworks,
6497
6683
  modDeployPaths,
6498
6684
  modFolder,
6499
6685
  modPageUrl,
@@ -6502,8 +6688,10 @@ ${mod.files.join("\n")}`;
6502
6688
  modsRemoved,
6503
6689
  modsTouched,
6504
6690
  moveInSection,
6691
+ moveLibrary,
6505
6692
  moveMod,
6506
6693
  moveSection,
6694
+ moveTarget,
6507
6695
  needsRefresh,
6508
6696
  newLoadout,
6509
6697
  newestFile,
@@ -6530,9 +6718,11 @@ ${mod.files.join("\n")}`;
6530
6718
  registerNxmHandler,
6531
6719
  removeMod,
6532
6720
  removeOlderBridges,
6721
+ removeTemp,
6533
6722
  renameFamily,
6534
6723
  renameLoadout,
6535
6724
  renameSection,
6725
+ sameDrive,
6536
6726
  sameNameMod,
6537
6727
  saveLibrary,
6538
6728
  saveLoadout,
@@ -6545,6 +6735,7 @@ ${mod.files.join("\n")}`;
6545
6735
  settleNewVersion,
6546
6736
  signIn,
6547
6737
  signInWithOAuth,
6738
+ singleRednodeApp,
6548
6739
  snapshot,
6549
6740
  sourcePath,
6550
6741
  ssoPageUrl,
package/dist/mcp.js CHANGED
@@ -2984,7 +2984,7 @@ var require_compile = __commonJS({
2984
2984
  const schOrFunc = root.refs[ref];
2985
2985
  if (schOrFunc)
2986
2986
  return schOrFunc;
2987
- let _sch = resolve4.call(this, root, ref);
2987
+ let _sch = resolve5.call(this, root, ref);
2988
2988
  if (_sch === void 0) {
2989
2989
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
2990
2990
  const { schemaId } = this.opts;
@@ -3011,7 +3011,7 @@ var require_compile = __commonJS({
3011
3011
  function sameSchemaEnv(s1, s2) {
3012
3012
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3013
3013
  }
3014
- function resolve4(root, ref) {
3014
+ function resolve5(root, ref) {
3015
3015
  let sch;
3016
3016
  while (typeof (sch = this.refs[ref]) == "string")
3017
3017
  ref = sch;
@@ -3841,7 +3841,7 @@ var require_fast_uri = __commonJS({
3841
3841
  }
3842
3842
  return uri;
3843
3843
  }
3844
- function resolve4(baseURI, relativeURI, options2) {
3844
+ function resolve5(baseURI, relativeURI, options2) {
3845
3845
  const schemelessOptions = options2 ? Object.assign({ scheme: "null" }, options2) : { scheme: "null" };
3846
3846
  const {
3847
3847
  parsed: baseParsed,
@@ -4210,7 +4210,7 @@ var require_fast_uri = __commonJS({
4210
4210
  var fastUri = {
4211
4211
  SCHEMES,
4212
4212
  normalize,
4213
- resolve: resolve4,
4213
+ resolve: resolve5,
4214
4214
  resolveComponent,
4215
4215
  equal,
4216
4216
  serialize,
@@ -15901,7 +15901,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
15901
15901
  var NONE = 0;
15902
15902
  var ASSUMED = 1;
15903
15903
  var PROVEN = 2;
15904
- function isRecursive(inst, stack, resolve4) {
15904
+ function isRecursive(inst, stack, resolve5) {
15905
15905
  const cached2 = recursive.get(inst);
15906
15906
  if (cached2 !== void 0)
15907
15907
  return cached2 ? PROVEN : NONE;
@@ -15911,7 +15911,7 @@ function isRecursive(inst, stack, resolve4) {
15911
15911
  let result = NONE;
15912
15912
  const check2 = (child) => {
15913
15913
  if (result !== PROVEN && child?._zod) {
15914
- const answer = isRecursive(child, stack, resolve4);
15914
+ const answer = isRecursive(child, stack, resolve5);
15915
15915
  if (answer > result)
15916
15916
  result = answer;
15917
15917
  }
@@ -15922,7 +15922,7 @@ function isRecursive(inst, stack, resolve4) {
15922
15922
  const desc = Object.getOwnPropertyDescriptor(sh, key2);
15923
15923
  if (spread && !desc.enumerable)
15924
15924
  continue;
15925
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve4) : NONE;
15925
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
15926
15926
  if (child > answer)
15927
15927
  answer = child;
15928
15928
  }
@@ -15986,7 +15986,7 @@ function isRecursive(inst, stack, resolve4) {
15986
15986
  break;
15987
15987
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
15988
15988
  case "lazy": {
15989
- const inner = def._cachedInner ?? (resolve4 ? inst._zod.innerType : void 0);
15989
+ const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
15990
15990
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
15991
15991
  break;
15992
15992
  }
@@ -34424,7 +34424,7 @@ var Protocol = class {
34424
34424
  return;
34425
34425
  }
34426
34426
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
34427
- await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
34427
+ await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
34428
34428
  options2?.signal?.throwIfAborted();
34429
34429
  }
34430
34430
  } catch (error62) {
@@ -34441,7 +34441,7 @@ var Protocol = class {
34441
34441
  */
34442
34442
  request(request, resultSchema, options2) {
34443
34443
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options2 ?? {};
34444
- return new Promise((resolve4, reject) => {
34444
+ return new Promise((resolve5, reject) => {
34445
34445
  const earlyReject = (error62) => {
34446
34446
  reject(error62);
34447
34447
  };
@@ -34519,7 +34519,7 @@ var Protocol = class {
34519
34519
  if (!parseResult.success) {
34520
34520
  reject(parseResult.error);
34521
34521
  } else {
34522
- resolve4(parseResult.data);
34522
+ resolve5(parseResult.data);
34523
34523
  }
34524
34524
  } catch (error62) {
34525
34525
  reject(error62);
@@ -34781,12 +34781,12 @@ var Protocol = class {
34781
34781
  }
34782
34782
  } catch {
34783
34783
  }
34784
- return new Promise((resolve4, reject) => {
34784
+ return new Promise((resolve5, reject) => {
34785
34785
  if (signal.aborted) {
34786
34786
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
34787
34787
  return;
34788
34788
  }
34789
- const timeoutId = setTimeout(resolve4, interval);
34789
+ const timeoutId = setTimeout(resolve5, interval);
34790
34790
  signal.addEventListener("abort", () => {
34791
34791
  clearTimeout(timeoutId);
34792
34792
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -35917,7 +35917,7 @@ var McpServer = class {
35917
35917
  let task = createTaskResult.task;
35918
35918
  const pollInterval = task.pollInterval ?? 5e3;
35919
35919
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
35920
- await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
35920
+ await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
35921
35921
  const updatedTask = await extra.taskStore.getTask(taskId);
35922
35922
  if (!updatedTask) {
35923
35923
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -36581,12 +36581,12 @@ var StdioServerTransport = class {
36581
36581
  this.onclose?.();
36582
36582
  }
36583
36583
  send(message) {
36584
- return new Promise((resolve4) => {
36584
+ return new Promise((resolve5) => {
36585
36585
  const json2 = serializeMessage(message);
36586
36586
  if (this._stdout.write(json2)) {
36587
- resolve4();
36587
+ resolve5();
36588
36588
  } else {
36589
- this._stdout.once("drain", resolve4);
36589
+ this._stdout.once("drain", resolve5);
36590
36590
  }
36591
36591
  });
36592
36592
  }
@@ -36665,7 +36665,8 @@ function folderName(name) {
36665
36665
  return name.replace(/[<>:"/\\|?*]+/g, "").trim() || "Mod";
36666
36666
  }
36667
36667
  function rednodeManifest(placements) {
36668
- const p = placements.find((x) => /^red4ext\/plugins\/REDnode\/apps\/[^/]+\/rednode-app\.json$/i.test(x.to));
36668
+ const to = singleRednodeApp(placements.map((x) => x.to));
36669
+ const p = to === void 0 ? void 0 : placements.find((x) => x.to === to);
36669
36670
  if (!p) return void 0;
36670
36671
  try {
36671
36672
  const m = JSON.parse(fs.readFileSync(p.from, "utf8"));
@@ -36675,6 +36676,12 @@ function rednodeManifest(placements) {
36675
36676
  return void 0;
36676
36677
  }
36677
36678
  }
36679
+ function singleRednodeApp(paths) {
36680
+ const manifests = paths.filter((f) => /^red4ext\/plugins\/REDnode\/apps\/[^/]+\/rednode-app\.json$/i.test(f));
36681
+ if (manifests.length !== 1) return void 0;
36682
+ if (paths.some((f) => /^red4ext\/plugins\/REDnode\//i.test(f) && !/^red4ext\/plugins\/REDnode\/apps\//i.test(f))) return void 0;
36683
+ return manifests[0];
36684
+ }
36678
36685
  function rednodeId(manifest) {
36679
36686
  try {
36680
36687
  const m = JSON.parse(fs.readFileSync(manifest, "utf8"));
@@ -36925,11 +36932,8 @@ function baseName(name) {
36925
36932
  return name.toLowerCase().replace(/[_\-]+/g, " ").replace(/\bv?\d+(?:\.\d+)+[a-z0-9]*\b/g, " ").replace(/[^a-z0-9]+/g, " ").trim();
36926
36933
  }
36927
36934
  function rednodePackageId(mod) {
36928
- for (const f of mod.files) {
36929
- const m = /^red4ext\/plugins\/REDnode\/apps\/([^/]+)\/rednode-app\.json$/i.exec(f);
36930
- if (m) return m[1].toLowerCase();
36931
- }
36932
- return void 0;
36935
+ const manifest = singleRednodeApp(mod.files);
36936
+ return manifest ? /^red4ext\/plugins\/REDnode\/apps\/([^/]+)\//i.exec(manifest)[1].toLowerCase() : void 0;
36933
36937
  }
36934
36938
  function familyKey(mod) {
36935
36939
  if (mod.family) return mod.family;
@@ -37148,6 +37152,21 @@ function canHardlink(libraryRoot, gamePath) {
37148
37152
  return false;
37149
37153
  }
37150
37154
  }
37155
+ var hardlinkable = /* @__PURE__ */ new Map();
37156
+ function deployMethod(libraryRoot, gamePath, current) {
37157
+ try {
37158
+ if (fs4.statSync(libraryRoot).dev !== fs4.statSync(gamePath).dev) return "copy";
37159
+ } catch {
37160
+ return current ?? "copy";
37161
+ }
37162
+ if (current === "link") return "link";
37163
+ const k = `${path4.resolve(libraryRoot).toLowerCase()}|${path4.resolve(gamePath).toLowerCase()}`;
37164
+ if (!hardlinkable.has(k)) hardlinkable.set(k, canHardlink(libraryRoot, gamePath));
37165
+ return hardlinkable.get(k) ? "link" : "copy";
37166
+ }
37167
+ function otherMethod(entry, method) {
37168
+ return method === "link" !== !!entry.ino;
37169
+ }
37151
37170
  function sourceReplaced(entry, method, d) {
37152
37171
  if (d.content !== void 0) return false;
37153
37172
  const src = identity(d.src);
@@ -37155,10 +37174,10 @@ function sourceReplaced(entry, method, d) {
37155
37174
  if (method === "link" && entry.ino) return src.ino !== entry.ino;
37156
37175
  return src.size !== entry.size || Math.abs(src.mtimeMs - entry.mtimeMs) >= 2e3;
37157
37176
  }
37158
- function stillOurs(entry, method, gamePath) {
37177
+ function stillOurs(entry, _method, gamePath) {
37159
37178
  const id = identity(gameFile(gamePath, entry.path));
37160
37179
  if (!id) return "missing";
37161
- if (method === "link" && entry.ino) {
37180
+ if (entry.ino) {
37162
37181
  if (id.ino !== entry.ino) return "changed";
37163
37182
  if (id.nlink < 2 && entry.mod !== Generated) return "sourceMissing";
37164
37183
  return "ok";
@@ -37174,7 +37193,7 @@ function generatedDiffers(file2, content) {
37174
37193
  }
37175
37194
  function plan(lib, gamePath) {
37176
37195
  const manifest = readManifest(gamePath);
37177
- const method = manifest?.method ?? (canHardlink(lib.root, gamePath) ? "link" : "copy");
37196
+ const method = deployMethod(lib.root, gamePath, manifest?.method);
37178
37197
  const desired = /* @__PURE__ */ new Map();
37179
37198
  const conflicts = /* @__PURE__ */ new Map();
37180
37199
  for (const mod of lib.mods) {
@@ -37205,6 +37224,7 @@ function plan(lib, gamePath) {
37205
37224
  if (!e || (d.mod?.id ?? Generated) !== e.mod) return true;
37206
37225
  if (d.content !== void 0 && generatedDiffers(gameFile(gamePath, d.rel), d.content)) return true;
37207
37226
  if (stillOurs(e, method, gamePath) !== "ok") return true;
37227
+ if (d.content === void 0 && otherMethod(e, method)) return true;
37208
37228
  return sourceReplaced(e, method, d);
37209
37229
  });
37210
37230
  return { method, desired: [...desired.values()], conflicts: [...conflicts.values()], stale, add, vortex: vortexManages(gamePath) };
@@ -37336,6 +37356,7 @@ function deploy(lib, gamePath, decisions = /* @__PURE__ */ new Map(), progress,
37336
37356
  const p = plan(lib, gamePath);
37337
37357
  const manifest = readManifest(gamePath) ?? { version: 1, library: lib.root, gamePath, method: p.method, time: "", files: [], dirs: [] };
37338
37358
  manifest.library = lib.root;
37359
+ manifest.method = p.method;
37339
37360
  const result = { linked: 0, removed: 0, kept: 0, backedUp: 0, method: manifest.method, skipped: [], changes: [], modsInGame: 0, lost: lostFiles(p) };
37340
37361
  const entries = new Map(manifest.files.map((e) => [key(e.path), e]));
37341
37362
  const scope = only ? modDeployPaths(gamePath, p, only) : void 0;
@@ -37462,6 +37483,7 @@ function pendingChanges(lib, gamePath, p) {
37462
37483
  const st = stillOurs(e, p.method, gamePath);
37463
37484
  if (st === "missing") out.push({ path: d.rel, mod: id, kind: "restore", detail: "missing from the game folder" });
37464
37485
  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" });
37486
+ 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" });
37465
37487
  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)" });
37466
37488
  }
37467
37489
  }
@@ -37512,6 +37534,14 @@ function findRarTool() {
37512
37534
  if (unrar) return { exe: unrar, args: (file2, dest) => ["x", "-y", "-o+", file2, dest + path5.sep] };
37513
37535
  return void 0;
37514
37536
  }
37537
+ function removeTemp(dir) {
37538
+ try {
37539
+ fs5.rmSync(dir, { recursive: true, force: true, maxRetries: 6, retryDelay: 250 });
37540
+ return void 0;
37541
+ } catch (err) {
37542
+ return `could not remove the temporary folder ${dir} (${err instanceof Error ? err.message : String(err)}); Windows clears its temp folder in time`;
37543
+ }
37544
+ }
37515
37545
  async function unpackOrCopy(file2, dest, sevenZa) {
37516
37546
  if (isArchiveFile(file2)) return extractArchive(file2, dest, sevenZa);
37517
37547
  fs5.mkdirSync(dest, { recursive: true });
@@ -38112,7 +38142,7 @@ ${mod.files.join("\n")}`;
38112
38142
  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` : "";
38113
38143
  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(", ")}` : ""}` };
38114
38144
  } finally {
38115
- if (temp) fs11.rmSync(temp, { recursive: true, force: true });
38145
+ if (temp) removeTemp(temp);
38116
38146
  }
38117
38147
  },
38118
38148
  /** Adds a REDcode or WolvenKit project's packed output, or refreshes the mod that follows it, and turns it on. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgemodders/edgedeck-mcp",
3
- "version": "0.28.0",
3
+ "version": "0.29.1",
4
4
  "description": "EDGEdeck's MCP server and command line: manage Cyberpunk 2077 mods (add, turn on or off, order, deploy, undo, conflicts, loadouts) from an AI assistant or a script.",
5
5
  "license": "MIT",
6
6
  "author": "Edgemodders",