@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/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,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(path14, retried = false) {
4035
- const res = await fetch(`${this.options.api ?? NexusApi}${path14}`, {
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(path14, true);
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((resolve3, reject) => {
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}`)) : resolve3();
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, method, gamePath) {
4734
+ function stillOurs(entry, _method, gamePath) {
4697
4735
  const id = identity(gameFile(gamePath, entry.path));
4698
4736
  if (!id) return "missing";
4699
- if (method === "link" && entry.ino) {
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 = manifest?.method ?? (canHardlink(lib.root, gamePath) ? "link" : "copy");
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 deploy(lib, gamePath, decisions = /* @__PURE__ */ new Map(), progress, gameVersion2) {
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
- const result = { linked: 0, removed: 0, kept: 0, backedUp: 0, method: manifest.method, skipped: [], changes: [], modsInGame: 0 };
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 = p.stale.length + p.add.length;
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
- for (const entry of p.stale) {
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
- progress?.(++done, total);
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
- continue;
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
- result.kept = p.desired.length - result.linked - result.skipped.length;
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/loadouts.ts
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 path8.join(root, "loadouts");
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(fs9.readFileSync(file, "utf8"));
5371
+ return JSON.parse(fs10.readFileSync(file, "utf8"));
5120
5372
  }
5121
5373
  function listLoadouts(root) {
5122
5374
  const dir = loadoutsFolder(root);
5123
- if (!fs9.existsSync(dir)) return [];
5375
+ if (!fs10.existsSync(dir)) return [];
5124
5376
  const out = [];
5125
- for (const f of fs9.readdirSync(dir)) {
5377
+ for (const f of fs10.readdirSync(dir)) {
5126
5378
  if (!f.toLowerCase().endsWith(".json")) continue;
5127
5379
  try {
5128
- const l = readLoadout(path8.join(dir, f));
5129
- 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 });
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
- fs9.mkdirSync(loadoutsFolder(root), { recursive: true });
5410
+ fs10.mkdirSync(loadoutsFolder(root), { recursive: true });
5159
5411
  let target = file;
5160
5412
  if (!target) {
5161
- target = path8.join(loadoutsFolder(root), `${slug(loadout.name)}.json`);
5162
- for (let n = 2; fs9.existsSync(target); n++) target = path8.join(loadoutsFolder(root), `${slug(loadout.name)}-${n}.json`);
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
- fs9.writeFileSync(target, JSON.stringify(loadout, null, 2) + "\n");
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
- fs9.rmSync(info.file);
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) fs9.rmSync(info.file);
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
- fs9.copyFileSync(info.file, dest);
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(`${path8.basename(file)} is not a EDGEdeck loadout.`);
5289
- let name = (loadout.name || path8.basename(file, ".json")).trim();
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((resolve3, reject) => {
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) resolve3(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((resolve3, reject) => {
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) resolve3(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 fs10 = __toESM(require("node:fs"));
5585
- var path9 = __toESM(require("node:path"));
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 = path9.join(path9.dirname(projectFile), "source", "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 = path9.join("source", "resources", ...f.split("/"));
5596
- const dest = path9.join(resources, ...f.split("/"));
5597
- if (fs10.existsSync(dest)) {
5598
- if (fs10.readFileSync(dest).equals(fs10.readFileSync(src))) {
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
- fs10.mkdirSync(path9.dirname(dest), { recursive: true });
5608
- fs10.copyFileSync(src, dest);
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 (fs10.existsSync(target) && fs10.statSync(target).isFile()) return target.toLowerCase().endsWith(".cpmodproj") ? target : void 0;
5615
- const files = fs10.existsSync(target) ? fs10.readdirSync(target).filter((f) => f.toLowerCase().endsWith(".cpmodproj")) : [];
5616
- return files.length ? path9.join(target, files[0]) : void 0;
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 = fs10.readFileSync(projectFile, "utf8");
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 path9.basename(projectFile, path9.extname(projectFile));
5877
+ return path10.basename(projectFile, path10.extname(projectFile));
5626
5878
  }
5627
5879
  function packedFolder(projectFile) {
5628
- return path9.join(path9.dirname(projectFile), "packed");
5880
+ return path10.join(path10.dirname(projectFile), "packed");
5629
5881
  }
5630
5882
  function classifyProject(projectFile) {
5631
5883
  const packed = packedFolder(projectFile);
5632
- if (!fs10.existsSync(packed)) return { placements: [], kinds: [], unplaced: [], ignored: [] };
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 fs10.readdirSync(dir, { withFileTypes: true })) {
5638
- const p = path9.join(dir, e.name);
5639
- t = Math.max(t, e.isDirectory() ? newest(p) : fs10.statSync(p).mtimeMs);
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 (!fs10.existsSync(packed)) continue;
5900
+ if (!fs11.existsSync(packed)) continue;
5649
5901
  const libNewest = mod.files.reduce((t, f) => {
5650
5902
  try {
5651
- return Math.max(t, fs10.statSync(sourcePath(lib, mod, f)).mtimeMs);
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
- fs10.mkdirSync(path9.dirname(dest), { recursive: true });
5663
- fs10.copyFileSync(p.from, dest);
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)) fs10.rmSync(sourcePath(lib, mod, f), { force: true });
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 installProject(lib, projectFile) {
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
- const existing = lib.mods.find((m) => m.source.type === "project" && m.source.path?.toLowerCase() === projectFile.toLowerCase());
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
- fs10.mkdirSync(path9.dirname(dest), { recursive: true });
5686
- fs10.copyFileSync(p.from, dest);
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)) fs10.rmSync(sourcePath(lib, existing, f), { force: true });
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 fs11 = __toESM(require("node:fs"));
5700
- var path10 = __toESM(require("node:path"));
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 path10.join(gamePath, "tools", "redmod", "bin", "redMod.exe");
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
- async function deployRedmod(lib, gamePath, log) {
5718
- const packages = enabledRedmodPackages(lib);
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 (!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` };
5722
- const listFile = path10.join(lib.root, "redmod-modlist.txt");
5723
- fs11.writeFileSync(listFile, packages.join("\r\n") + "\r\n");
5724
- const args = ["deploy", `-root=${gamePath}`, `-modlist=${listFile}`, `-rttiSchemaPath=${path10.join(gamePath, "tools", "redmod", "metadata.json")}`];
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: path10.dirname(exe), windowsHide: true, maxBuffer: 1 << 26 });
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 fs12 = __toESM(require("node:fs"));
5846
- var path11 = __toESM(require("node:path"));
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 = fs12.statSync(path11.join(folder, ...f.split("/")));
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 fs12.readdirSync(dir, { withFileTypes: true })) {
5879
- const p = path11.join(dir, e.name);
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 += fs12.statSync(p).size;
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 fs14 = __toESM(require("node:fs"));
6202
+ var fs15 = __toESM(require("node:fs"));
5903
6203
  var os3 = __toESM(require("node:os"));
5904
- var path13 = __toESM(require("node:path"));
6204
+ var path14 = __toESM(require("node:path"));
5905
6205
 
5906
6206
  // src/vortex.ts
5907
- var fs13 = __toESM(require("node:fs"));
5908
- var path12 = __toESM(require("node:path"));
6207
+ var fs14 = __toESM(require("node:fs"));
6208
+ var path13 = __toESM(require("node:path"));
5909
6209
  function vortexStagingPath() {
5910
- return path12.join(process.env.APPDATA ?? path12.join(process.env.USERPROFILE ?? "", "AppData", "Roaming"), "Vortex", "cyberpunk2077", "mods");
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(fs13.readFileSync(path12.join(gamePath, "vortex.deployment.json"), "utf8"));
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 ? path12.parse(path12.resolve(gamePath)).root.toUpperCase() : "";
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 && fs13.existsSync(r)) roots.push(r);
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(path12.join(root, "Vortex Mods", "cyberpunk2077"), path12.join(root, "VortexMods", "cyberpunk2077"), path12.join(root, "Vortex", "cyberpunk2077", "mods"), path12.join(root, "Vortex", "mods", "cyberpunk2077"));
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 (fs13.readdirSync(dir, { withFileTypes: true }).some((e) => e.isDirectory() && !e.name.startsWith("__"))) return dir;
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 fs13.readdirSync(dir, { withFileTypes: true })) {
5951
- if (e.isDirectory()) n += countFiles(path12.join(dir, e.name));
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 = path12.join(folder, ...parts);
5959
- if (!fs13.existsSync(d)) return void 0;
5960
- const names = fs13.readdirSync(d, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
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 = path12.join(folder, "archive", "pc", "mod");
5966
- if (fs13.existsSync(archives)) {
5967
- const a = fs13.readdirSync(archives).find((f) => f.toLowerCase().endsWith(".archive"));
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 = path12.join(gamePath, "vortex.deployment.json");
5978
- if (fs13.existsSync(manifest)) {
6277
+ const manifest = path13.join(gamePath, "vortex.deployment.json");
6278
+ if (fs14.existsSync(manifest)) {
5979
6279
  try {
5980
- const m = JSON.parse(fs13.readFileSync(manifest, "utf8"));
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 fs13.readdirSync(staging, { withFileTypes: true })) {
6286
+ for (const entry of fs14.readdirSync(staging, { withFileTypes: true })) {
5987
6287
  if (!entry.isDirectory() || entry.name.startsWith("__")) continue;
5988
- const folder = path12.join(staging, entry.name);
6288
+ const folder = path13.join(staging, entry.name);
5989
6289
  const files = countFiles(folder);
5990
6290
  if (files === 0) continue;
5991
- if (fs13.existsSync(path12.join(folder, "collection.json")) && files === 1) continue;
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 = fs14.existsSync(game) && isGameFolder(game) ? plan(lib, game) : void 0;
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 (!fs14.existsSync(source)) throw new Error(`${source} does not exist`);
6115
- const isFile = fs14.statSync(source).isFile();
6116
- const modName = name ?? path13.basename(source, isFile ? path13.extname(source) : "");
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 = fs14.mkdtempSync(path13.join(os3.tmpdir(), "edgedeck-verb-"));
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) fs14.rmSync(temp, { recursive: true, force: true });
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
- installProject(s, projectFile) {
6143
- if (!fs14.existsSync(projectFile)) throw new Error(`${projectFile} does not exist`);
6144
- const { mod, added } = installProject(s.lib, projectFile);
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
- /** Deploys. A file changed outside EDGEdeck is skipped unless named in keep (import it) or restore (overwrite it). */
6167
- async deploy(s, keep = [], restore = []) {
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 redmod = enabledRedmodPackages(s.lib).length ? await deployRedmod(s.lib, s.game) : void 0;
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 = fs14.existsSync(name) && name.toLowerCase().endsWith(".json") ? importLoadout(s.lib, name) : name;
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,