windows-development-sandbox 1.27.0 → 1.27.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/app/context.d.ts +5 -0
  2. package/dist/app/context.js +13 -0
  3. package/dist/app/features/accounts/accounts.def.d.ts +16 -0
  4. package/dist/app/features/accounts/accounts.run.d.ts +4 -0
  5. package/dist/app/features/accounts/services.def.d.ts +15 -1
  6. package/dist/app/features/accounts/services.def.js +67 -31
  7. package/dist/app/features/accounts/services.run.d.ts +7 -2
  8. package/dist/app/features/accounts/services.run.js +25 -6
  9. package/dist/app/features/defs.d.ts +17 -0
  10. package/dist/app/features/unreal/unreal.run.js +13 -3
  11. package/dist/app/registry.d.ts +1 -1
  12. package/dist/app/registry.js +15 -2
  13. package/dist/bin/agent-sandbox.js +2 -2
  14. package/dist/bin/{chunk-JXL6ZOO3.js → chunk-4M5SZZ5P.js} +46 -41
  15. package/dist/bin/{chunk-ZXCAVVS4.js → chunk-FA5VWLP4.js} +1 -1
  16. package/dist/bin/{chunk-6WWMBCT2.js → chunk-RU7XRN62.js} +303 -112
  17. package/dist/bin/claude-host.js +2 -2
  18. package/dist/bin/claude-sbx.js +2 -2
  19. package/dist/bin/codex-sbx.js +2 -2
  20. package/dist/bin/gemini-sbx.js +2 -2
  21. package/dist/bin/sandbox.js +2 -2
  22. package/dist/bin/sbx-hide.js +2 -2
  23. package/dist/bin/sbx-show.js +2 -2
  24. package/dist/cli/commands/launch/launch.js +2 -1
  25. package/dist/cli/menu/menu.js +6 -1
  26. package/dist/cli/menu/pages/accounts-page.js +9 -5
  27. package/dist/guest/apps/app-installer.js +7 -5
  28. package/dist/guest/apps/comet-unpack.js +2 -2
  29. package/dist/guest/runtime/pools.d.ts +4 -4
  30. package/dist/guest/runtime/pools.js +4 -15
  31. package/dist/guest/runtime/process.d.ts +5 -2
  32. package/dist/guest/runtime/process.js +23 -3
  33. package/dist/guest/setup/unity-agent.js +4 -1
  34. package/dist/guest/toolchains/engines/epic-launcher.d.ts +7 -1
  35. package/dist/guest/toolchains/engines/epic-launcher.js +30 -8
  36. package/dist/guest/toolchains/engines/unreal-engines.js +2 -1
  37. package/dist/guest/toolchains/engines/unreal-status.js +1 -1
  38. package/dist/guest/toolchains/installers.d.ts +3 -3
  39. package/dist/guest/toolchains/installers.js +5 -5
  40. package/dist/guest/toolchains/native/mesa.js +35 -11
  41. package/dist/guest/toolchains/native/unreal.js +1 -0
  42. package/dist/guest/toolchains/native/vcruntime.js +2 -2
  43. package/dist/guest/toolchains/native/vs-persist.d.ts +2 -2
  44. package/dist/guest/toolchains/signature.d.ts +2 -0
  45. package/dist/guest/toolchains/signature.js +9 -2
  46. package/dist/guest/toolchains/step-scheduler.js +1 -1
  47. package/dist/host/accounts/deliver.d.ts +3 -0
  48. package/dist/host/accounts/deliver.js +27 -0
  49. package/dist/host/accounts/source.d.ts +2 -0
  50. package/dist/host/accounts/source.js +2 -0
  51. package/dist/host/pc-installs/discover.js +8 -4
  52. package/dist/host/pc-installs/epic.js +3 -0
  53. package/dist/host/pc-installs/unity.d.ts +2 -0
  54. package/dist/host/pc-installs/unity.js +12 -3
  55. package/dist/host/pc-installs/unreal.d.ts +5 -1
  56. package/dist/host/pc-installs/unreal.js +51 -9
  57. package/dist/runtime/agent.js +34 -22
  58. package/dist/runtime/daemon.js +134 -65
  59. package/dist/runtime/db.js +32 -16
  60. package/dist/runtime/main.js +136 -67
  61. package/dist/runtime/netproxy.js +26 -12
  62. package/dist/runtime/tool.js +32 -15
  63. package/dist/runtime/view.js +26 -12
  64. package/dist/selftest/app.test.js +20 -2
  65. package/dist/selftest/boot-runner.test.js +15 -1
  66. package/dist/selftest/engines.test.js +52 -2
  67. package/dist/selftest/menu-screens.js +52 -1
  68. package/dist/selftest/menu.test.js +29 -3
  69. package/dist/selftest/mixamo.test.js +1 -1
  70. package/dist/selftest/service-accounts.test.js +88 -2
  71. package/dist/selftest/ui-text.test.js +4 -0
  72. package/dist/shared/accounts/kinds.js +9 -9
  73. package/dist/shared/apps/catalogue-gamedev.js +1 -0
  74. package/dist/shared/apps/catalogue.d.ts +2 -0
  75. package/dist/shared/concurrency/pools.js +1 -1
  76. package/dist/shared/download/archive.js +4 -2
  77. package/dist/shared/download/index.d.ts +1 -1
  78. package/dist/shared/download/index.js +1 -1
  79. package/dist/shared/download/spawn.d.ts +7 -1
  80. package/dist/shared/download/spawn.js +16 -1
  81. package/dist/shared/paths/pc-installs.d.ts +8 -0
  82. package/dist/shared/unreal/launcher-files.d.ts +7 -0
  83. package/dist/shared/unreal/launcher-files.js +32 -1
  84. package/dist/shared/unreal/status.d.ts +1 -0
  85. package/dist/shared/unreal/status.js +2 -0
  86. package/dist/web/index.html +1 -1
  87. package/dist/web/{main-VPMYTSUL.js → main-NKM7ECSB.js} +59 -59
  88. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  89. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  90. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  91. package/package.json +1 -1
  92. package/schemas/config.schema.json +9 -9
  93. package/schemas/sandbox-config.schema.json +9 -9
@@ -5551,7 +5551,7 @@ var androidTool = {
5551
5551
 
5552
5552
  // src/guest/devtools/fivem.ts
5553
5553
  import fs17 from "node:fs";
5554
- import os3 from "node:os";
5554
+ import os4 from "node:os";
5555
5555
  import path20 from "node:path";
5556
5556
  import { spawnSync as spawnSync2 } from "node:child_process";
5557
5557
 
@@ -5831,10 +5831,20 @@ function scriptArgs(name2, args2 = []) {
5831
5831
  }
5832
5832
 
5833
5833
  // src/shared/download/spawn.ts
5834
+ import os3 from "node:os";
5834
5835
  import { spawn } from "node:child_process";
5836
+ function lowerPriority(pid) {
5837
+ if (pid === void 0) return;
5838
+ try {
5839
+ os3.setPriority(pid, os3.constants.priority.PRIORITY_BELOW_NORMAL);
5840
+ } catch {
5841
+ }
5842
+ }
5835
5843
  function spawnAsync(file2, args2, options = {}) {
5844
+ const { lowPriority, ...spawnOptions } = options;
5836
5845
  return new Promise((resolve, reject) => {
5837
- const child = spawn(file2, args2, { stdio: "ignore", windowsHide: true, ...options });
5846
+ const child = spawn(file2, args2, { stdio: "ignore", windowsHide: true, ...spawnOptions });
5847
+ if (lowPriority) lowerPriority(child.pid);
5838
5848
  child.once("error", reject);
5839
5849
  child.once("close", (code) => resolve(code ?? -1));
5840
5850
  });
@@ -5855,13 +5865,16 @@ function extractArchive(archive, destination, members2 = []) {
5855
5865
  }
5856
5866
  async function extract(archive, destination, members2) {
5857
5867
  fs7.mkdirSync(destination, { recursive: true });
5858
- let status = await spawnAsync(SYSTEM.tar(), ["-xf", archive, "-C", destination, ...members2]);
5868
+ let status = await spawnAsync(SYSTEM.tar(), ["-xf", archive, "-C", destination, ...members2], {
5869
+ lowPriority: true
5870
+ });
5859
5871
  if (status !== 0 && /\.(zip|nupkg)$/i.test(archive)) {
5860
5872
  const zip = archive.endsWith(".zip") ? archive : archive + ".zip";
5861
5873
  if (zip !== archive) fs7.copyFileSync(archive, zip);
5862
5874
  status = await spawnAsync(
5863
5875
  SYSTEM.powershell(),
5864
- scriptArgs("expand-archive.ps1", ["-Archive", zip, "-Destination", destination])
5876
+ scriptArgs("expand-archive.ps1", ["-Archive", zip, "-Destination", destination]),
5877
+ { lowPriority: true }
5865
5878
  );
5866
5879
  }
5867
5880
  if (status !== 0) throw new Error("Could not extract " + archive);
@@ -26494,6 +26507,7 @@ var GAMEDEV_APPS = [
26494
26507
  size: "100 MB",
26495
26508
  source: { github: "f3d-app/f3d", asset: "^F3D-[\\d.]+-Windows-x86_64\\.zip$" },
26496
26509
  exe: "f3d.exe",
26510
+ onPath: true,
26497
26511
  needs: ["mesa"]
26498
26512
  },
26499
26513
  {
@@ -27593,7 +27607,7 @@ var SERVICE_SPECS = {
27593
27607
  },
27594
27608
  steam: {
27595
27609
  kind: "steam",
27596
- label: "Steam (SteamCMD)",
27610
+ label: "Steam",
27597
27611
  usedBy: ["steam"],
27598
27612
  sources: ["file", "sandbox"],
27599
27613
  singleHolder: true,
@@ -27629,7 +27643,7 @@ var SERVICE_SPECS = {
27629
27643
  },
27630
27644
  cfx: {
27631
27645
  kind: "cfx",
27632
- label: "Cfx.re (FiveM / RedM server key)",
27646
+ label: "FiveM key",
27633
27647
  usedBy: ["fivem"],
27634
27648
  sources: ["typed", "file"],
27635
27649
  singleHolder: false,
@@ -27658,7 +27672,7 @@ var SERVICE_SPECS = {
27658
27672
  },
27659
27673
  keystore: {
27660
27674
  kind: "keystore",
27661
- label: "Android upload keystore",
27675
+ label: "Android keystore",
27662
27676
  usedBy: ["android"],
27663
27677
  sources: ["file"],
27664
27678
  singleHolder: false,
@@ -27673,7 +27687,7 @@ var SERVICE_SPECS = {
27673
27687
  },
27674
27688
  play: {
27675
27689
  kind: "play",
27676
- label: "Google Play (service account)",
27690
+ label: "Google Play",
27677
27691
  usedBy: ["android"],
27678
27692
  sources: ["file"],
27679
27693
  singleHolder: false,
@@ -27684,7 +27698,7 @@ var SERVICE_SPECS = {
27684
27698
  },
27685
27699
  expo: {
27686
27700
  kind: "expo",
27687
- label: "Expo (EAS)",
27701
+ label: "Expo",
27688
27702
  usedBy: ["reactNative"],
27689
27703
  sources: ["typed"],
27690
27704
  singleHolder: false,
@@ -27695,7 +27709,7 @@ var SERVICE_SPECS = {
27695
27709
  },
27696
27710
  appstore: {
27697
27711
  kind: "appstore",
27698
- label: "App Store Connect API key",
27712
+ label: "App Store Connect",
27699
27713
  usedBy: ["reactNative", "flutter"],
27700
27714
  sources: ["file", "typed"],
27701
27715
  singleHolder: false,
@@ -27709,7 +27723,7 @@ var SERVICE_SPECS = {
27709
27723
  },
27710
27724
  perplexity: {
27711
27725
  kind: "perplexity",
27712
- label: "Perplexity API key",
27726
+ label: "Perplexity API",
27713
27727
  usedBy: ["perplexity"],
27714
27728
  sources: ["pc", "file", "typed"],
27715
27729
  singleHolder: false,
@@ -27733,7 +27747,7 @@ var SERVICE_SPECS = {
27733
27747
  },
27734
27748
  comet: {
27735
27749
  kind: "comet",
27736
- label: "Comet (Perplexity browser)",
27750
+ label: "Comet",
27737
27751
  usedBy: ["comet"],
27738
27752
  sources: ["sandbox"],
27739
27753
  singleHolder: false,
@@ -27749,7 +27763,7 @@ var SERVICE_SPECS = {
27749
27763
  },
27750
27764
  perplexityapp: {
27751
27765
  kind: "perplexityapp",
27752
- label: "Perplexity desktop app",
27766
+ label: "Perplexity app",
27753
27767
  usedBy: ["perplexityapp"],
27754
27768
  sources: ["sandbox"],
27755
27769
  singleHolder: false,
@@ -28484,7 +28498,10 @@ function warn(message) {
28484
28498
  // src/guest/toolchains/signature.ts
28485
28499
  var SignatureSchema = external_exports.object({ subject: external_exports.string().max(4096), issuer: external_exports.string().max(4096) });
28486
28500
  function signatureOf(file2) {
28487
- const text = scriptOutput("get-signature.ps1", ["-Path", file2]).trim();
28501
+ return parseSignature(scriptOutput("get-signature.ps1", ["-Path", file2]));
28502
+ }
28503
+ function parseSignature(output2) {
28504
+ const text = output2.trim();
28488
28505
  if (!text) return null;
28489
28506
  try {
28490
28507
  const parsed = SignatureSchema.safeParse(JSON.parse(text));
@@ -29014,7 +29031,7 @@ function parse3(args2) {
29014
29031
  return { positionals, flags, dir: dir === void 0 ? void 0 : path20.resolve(dir) };
29015
29032
  }
29016
29033
  function addresses() {
29017
- return Object.values(os3.networkInterfaces()).flat().filter((a) => Boolean(a && a.family === "IPv4" && !a.internal)).map((a) => a.address);
29034
+ return Object.values(os4.networkInterfaces()).flat().filter((a) => Boolean(a && a.family === "IPv4" && !a.internal)).map((a) => a.address);
29018
29035
  }
29019
29036
  function mariadbClient() {
29020
29037
  const root = path20.join(toolsDir, "mariadb");
@@ -26697,6 +26697,7 @@ var GAMEDEV_APPS = [
26697
26697
  size: "100 MB",
26698
26698
  source: { github: "f3d-app/f3d", asset: "^F3D-[\\d.]+-Windows-x86_64\\.zip$" },
26699
26699
  exe: "f3d.exe",
26700
+ onPath: true,
26700
26701
  needs: ["mesa"]
26701
26702
  },
26702
26703
  {
@@ -27796,7 +27797,7 @@ var SERVICE_SPECS = {
27796
27797
  },
27797
27798
  steam: {
27798
27799
  kind: "steam",
27799
- label: "Steam (SteamCMD)",
27800
+ label: "Steam",
27800
27801
  usedBy: ["steam"],
27801
27802
  sources: ["file", "sandbox"],
27802
27803
  singleHolder: true,
@@ -27832,7 +27833,7 @@ var SERVICE_SPECS = {
27832
27833
  },
27833
27834
  cfx: {
27834
27835
  kind: "cfx",
27835
- label: "Cfx.re (FiveM / RedM server key)",
27836
+ label: "FiveM key",
27836
27837
  usedBy: ["fivem"],
27837
27838
  sources: ["typed", "file"],
27838
27839
  singleHolder: false,
@@ -27861,7 +27862,7 @@ var SERVICE_SPECS = {
27861
27862
  },
27862
27863
  keystore: {
27863
27864
  kind: "keystore",
27864
- label: "Android upload keystore",
27865
+ label: "Android keystore",
27865
27866
  usedBy: ["android"],
27866
27867
  sources: ["file"],
27867
27868
  singleHolder: false,
@@ -27876,7 +27877,7 @@ var SERVICE_SPECS = {
27876
27877
  },
27877
27878
  play: {
27878
27879
  kind: "play",
27879
- label: "Google Play (service account)",
27880
+ label: "Google Play",
27880
27881
  usedBy: ["android"],
27881
27882
  sources: ["file"],
27882
27883
  singleHolder: false,
@@ -27887,7 +27888,7 @@ var SERVICE_SPECS = {
27887
27888
  },
27888
27889
  expo: {
27889
27890
  kind: "expo",
27890
- label: "Expo (EAS)",
27891
+ label: "Expo",
27891
27892
  usedBy: ["reactNative"],
27892
27893
  sources: ["typed"],
27893
27894
  singleHolder: false,
@@ -27898,7 +27899,7 @@ var SERVICE_SPECS = {
27898
27899
  },
27899
27900
  appstore: {
27900
27901
  kind: "appstore",
27901
- label: "App Store Connect API key",
27902
+ label: "App Store Connect",
27902
27903
  usedBy: ["reactNative", "flutter"],
27903
27904
  sources: ["file", "typed"],
27904
27905
  singleHolder: false,
@@ -27912,7 +27913,7 @@ var SERVICE_SPECS = {
27912
27913
  },
27913
27914
  perplexity: {
27914
27915
  kind: "perplexity",
27915
- label: "Perplexity API key",
27916
+ label: "Perplexity API",
27916
27917
  usedBy: ["perplexity"],
27917
27918
  sources: ["pc", "file", "typed"],
27918
27919
  singleHolder: false,
@@ -27936,7 +27937,7 @@ var SERVICE_SPECS = {
27936
27937
  },
27937
27938
  comet: {
27938
27939
  kind: "comet",
27939
- label: "Comet (Perplexity browser)",
27940
+ label: "Comet",
27940
27941
  usedBy: ["comet"],
27941
27942
  sources: ["sandbox"],
27942
27943
  singleHolder: false,
@@ -27952,7 +27953,7 @@ var SERVICE_SPECS = {
27952
27953
  },
27953
27954
  perplexityapp: {
27954
27955
  kind: "perplexityapp",
27955
- label: "Perplexity desktop app",
27956
+ label: "Perplexity app",
27956
27957
  usedBy: ["perplexityapp"],
27957
27958
  sources: ["sandbox"],
27958
27959
  singleHolder: false,
@@ -28650,10 +28651,20 @@ import path7 from "node:path";
28650
28651
  import crypto from "node:crypto";
28651
28652
 
28652
28653
  // src/shared/download/spawn.ts
28654
+ import os3 from "node:os";
28653
28655
  import { spawn as spawn2 } from "node:child_process";
28656
+ function lowerPriority(pid) {
28657
+ if (pid === void 0) return;
28658
+ try {
28659
+ os3.setPriority(pid, os3.constants.priority.PRIORITY_BELOW_NORMAL);
28660
+ } catch {
28661
+ }
28662
+ }
28654
28663
  function spawnAsync(file2, args, options = {}) {
28664
+ const { lowPriority, ...spawnOptions } = options;
28655
28665
  return new Promise((resolve, reject) => {
28656
- const child = spawn2(file2, args, { stdio: "ignore", windowsHide: true, ...options });
28666
+ const child = spawn2(file2, args, { stdio: "ignore", windowsHide: true, ...spawnOptions });
28667
+ if (lowPriority) lowerPriority(child.pid);
28657
28668
  child.once("error", reject);
28658
28669
  child.once("close", (code) => resolve(code ?? -1));
28659
28670
  });
@@ -28674,13 +28685,16 @@ function extractArchive(archive, destination, members2 = []) {
28674
28685
  }
28675
28686
  async function extract(archive, destination, members2) {
28676
28687
  fs6.mkdirSync(destination, { recursive: true });
28677
- let status = await spawnAsync(SYSTEM.tar(), ["-xf", archive, "-C", destination, ...members2]);
28688
+ let status = await spawnAsync(SYSTEM.tar(), ["-xf", archive, "-C", destination, ...members2], {
28689
+ lowPriority: true
28690
+ });
28678
28691
  if (status !== 0 && /\.(zip|nupkg)$/i.test(archive)) {
28679
28692
  const zip = archive.endsWith(".zip") ? archive : archive + ".zip";
28680
28693
  if (zip !== archive) fs6.copyFileSync(archive, zip);
28681
28694
  status = await spawnAsync(
28682
28695
  SYSTEM.powershell(),
28683
- scriptArgs("expand-archive.ps1", ["-Archive", zip, "-Destination", destination])
28696
+ scriptArgs("expand-archive.ps1", ["-Archive", zip, "-Destination", destination]),
28697
+ { lowPriority: true }
28684
28698
  );
28685
28699
  }
28686
28700
  if (status !== 0) throw new Error("Could not extract " + archive);
@@ -7,13 +7,13 @@ import path from 'node:path';
7
7
  import { z } from 'zod';
8
8
  import { agentNameProblem, agentsPage, agentsView } from '../app/features/agents/index.js';
9
9
  import { DEFINITIONS, NAV, navSections, navViews, QUERIES, VIEWS, viewById } from '../app/features/defs.js';
10
- import { accountsPage, agentPage, listAccounts } from '../app/features/accounts/index.js';
10
+ import { accountsPage, agentPage, listAccounts, listServices, } from '../app/features/accounts/index.js';
11
11
  import { addProject, projectRoots, projectsPage } from '../app/features/projects/index.js';
12
12
  import { cleanData, dataPage, showData } from '../app/features/data/index.js';
13
13
  import { showStorage, storagePage, storageView, } from '../app/features/storage/index.js';
14
14
  import { insideSandbox, isSandboxRunning } from '../host/sandbox/index.js';
15
15
  import { CRASH, listLogs, logsPage, tailLog } from '../app/features/logs/index.js';
16
- import { DEFAULTS } from '../core/config/index.js';
16
+ import { DEFAULTS, loadConfig } from '../core/config/index.js';
17
17
  import { sandboxPage } from '../app/features/sandbox/index.js';
18
18
  import { closeSession, sessionsPage, startShell, } from '../app/features/sessions/index.js';
19
19
  import { APP_GROUPS, appsPage, listApps } from '../app/features/apps/index.js';
@@ -460,6 +460,24 @@ export const appSuite = {
460
460
  storageRoot: t.at('app-apps', 'storage'),
461
461
  dataPath: t.dir('app-apps', 'data'),
462
462
  };
463
+ t.section('a page read after an action shows what the action saved');
464
+ const freshConfig = t.writeJson(t.at('app-fresh', 'config.json'), {
465
+ storage: { root: t.dir('app-fresh', 'storage') },
466
+ });
467
+ const freshCtx = {
468
+ loaded: loadConfig(freshConfig),
469
+ options: {},
470
+ storageRoot: t.at('app-fresh', 'storage'),
471
+ dataPath: t.dir('app-fresh', 'data'),
472
+ };
473
+ const shareOf = async () => (await readQuery(listServices, freshCtx, { name: 'comet' })).services[0]?.share;
474
+ t.check((await shareOf()) === false, 'a service starts not shared');
475
+ await runDefinition('settings.set', freshCtx, { scope: 'user', key: 'accounts.comet.share', value: true }, quiet);
476
+ t.check((await shareOf()) === true, 'after the share toggle runs, the service page reads it as shared');
477
+ await runDefinition('settings.set', freshCtx, { scope: 'user', key: 'accounts.comet.share', value: false }, quiet);
478
+ t.check((await shareOf()) === false, 'and as not shared again after the next toggle');
479
+ await runDefinition('apps.toggle', freshCtx, { id: 'perplexity-desktop', on: true }, quiet);
480
+ t.check(freshCtx.loaded.config.apps.install.includes('perplexity-desktop'), 'an app turned on in Apps is in the settings every page reads next');
463
481
  await runDefinition('apps.toggle', appsCtx, { id: 'lazygit', on: true }, quiet);
464
482
  const onText = fs.readFileSync(appsConfig, 'utf8');
465
483
  await runDefinition('apps.toggle', appsCtx, { id: 'lazygit', on: false }, quiet);
@@ -4,7 +4,9 @@ import { BootRunner, checkGraph, duration } from '../guest/boot/runner.js';
4
4
  import { BOOT_TASKS, STAGES, stageTitle } from '../guest/boot/graph.js';
5
5
  import { backgroundNames, mergeParts, runSplit } from '../guest/toolchains/split.js';
6
6
  import { StepScheduler } from '../guest/toolchains/step-scheduler.js';
7
- import { Limiter, asBackground, installerRetry, INSTALLER_RETRIES } from '../shared/concurrency/index.js';
7
+ import os from 'node:os';
8
+ import { Limiter, asBackground, installerRetry, INSTALLER_RETRIES, net, heavy, installer, } from '../shared/concurrency/index.js';
9
+ import { spawnAsync } from '../shared/download/index.js';
8
10
  const deferred = () => {
9
11
  let resolve = () => undefined;
10
12
  let reject = (_) => undefined;
@@ -288,6 +290,18 @@ export const bootRunnerSuite = {
288
290
  t.check(message.includes('nested pool') && again.includes('nested pool'), 'taking a second pool (or the same one) while holding one throws "nested pool"', message);
289
291
  t.check((await a.run(() => 7)) === 7 && a.running === 0, 'the slot is free again after a nested-pool error');
290
292
  }
293
+ {
294
+ // At most 3 downloads + 2 unpacks + 1 installer run at once, whatever the boot tasks, steps and apps ask for.
295
+ const busy = async (pool) => {
296
+ await Promise.all(Array.from({ length: 12 }, () => pool.run(() => sleep(5))));
297
+ return pool.peak;
298
+ };
299
+ t.check((await busy(net)) <= 3 && (await busy(heavy)) <= 2 && (await busy(installer)) <= 1, 'the shared pools cap at net 3, heavy 2 and installer 1', 'net ' + net.peak + ', heavy ' + heavy.peak + ', installer ' + installer.peak);
300
+ const script = 'setTimeout(() => process.exit(require("os").getPriority() > 0 ? 0 : 3), 400)';
301
+ const low = await spawnAsync(process.execPath, ['-e', script], { lowPriority: true });
302
+ const normal = await spawnAsync(process.execPath, ['-e', script]);
303
+ t.check(low === 0 && normal === 3 && os.getPriority() <= 0, 'lowPriority starts a program below normal priority; the default stays normal', 'low ' + low + ', normal ' + normal);
304
+ }
291
305
  t.check(installerRetry(1618, 1) &&
292
306
  installerRetry(1618, INSTALLER_RETRIES) &&
293
307
  !installerRetry(1618, INSTALLER_RETRIES + 1) &&
@@ -12,7 +12,7 @@ import { isWin } from '../core/util/index.js';
12
12
  import { ConfigSchema } from '../core/schema/index.js';
13
13
  import { decodeText, encodeText, readBoundedFile } from '../shared/io/index.js';
14
14
  import { SYSTEM } from '../shared/system.js';
15
- import { chooseEngine, ddcVars, enginePlaceProblem, ENGINE_NEED_BYTES, engineAt, engineExtraDirs, engineToolDirs, engineVars, entriesUnder, fileAssociationPlan, findEngines, findUproject, hasMcpPlugin, incompleteDirs, isBuildId, installLocationOf, itemSizes, launcherEntriesFor, launcherEntry, launcherInstallations, mergeLauncherInstalled, parseBuildVersion, parseEngineAssociation, preferredInstalled, registryPlan, restoreItems, saveItems, setLauncherInstallLocation, stableBuildId, UNREAL_STATUS_FILE, unrealGuideLines, unrealSummaryLine, uprojectAssociation, vaultDirsFromIni, vaultPacks, vsVerdict, VsSetupReportSchema, withInstallLocation, } from '../shared/unreal/index.js';
15
+ import { chooseEngine, ddcVars, enginePlaceProblem, ENGINE_NEED_BYTES, engineAt, engineExtraDirs, engineToolDirs, engineVars, entriesUnder, fileAssociationPlan, findEngines, findUproject, hasMcpPlugin, incompleteDirs, isBuildId, installLocationOf, itemSizes, launcherEntriesFor, launcherEntry, launcherInstallations, mergeLauncherInstalled, parseBuildVersion, parseEngineAssociation, preferredInstalled, registryPlan, relocateItem, restoreItems, saveItems, setLauncherInstallLocation, stableBuildId, UNREAL_STATUS_FILE, unrealGuideLines, unrealSummaryLine, uprojectAssociation, vaultDirsFromIni, vaultPacks, vsVerdict, VsSetupReportSchema, withInstallLocation, } from '../shared/unreal/index.js';
16
16
  import { discoverPcInstalls } from '../host/pc-installs/index.js';
17
17
  import { findEngineFolders, findUnrealInstalls, pcSourceBuilds, regValues } from '../host/pc-installs/unreal.js';
18
18
  import { findEpicGames } from '../host/pc-installs/epic.js';
@@ -255,6 +255,7 @@ export const enginesSuite = {
255
255
  projects: [{ workspace: 'C:\\Projects\\Work\\MyGame', engine: pcEngine, reason: 'the project asks for 5.4' }],
256
256
  launcher: {
257
257
  installed: true,
258
+ exe: null,
258
259
  installLocation: enginesRoot,
259
260
  problem: null,
260
261
  freeBytes: null,
@@ -380,6 +381,14 @@ export const enginesSuite = {
380
381
  AppVersion: 'v',
381
382
  AppName: 'UE_5.3',
382
383
  },
384
+ {
385
+ InstallLocation: pcLauncher,
386
+ NamespaceId: 'ue',
387
+ ItemId: 'fab',
388
+ ArtifactId: 'FabPlugin_5.4',
389
+ AppVersion: '5.4.4-1+++UE5+Release-5.4-0.0.21',
390
+ AppName: 'FabPlugin_5.4',
391
+ },
383
392
  ],
384
393
  });
385
394
  const reg = '\r\nHKEY_CURRENT_USER\\Software\\Epic Games\\Unreal Engine\\Builds\r\n {2D9FCA5D-4E7B-4C12-8B6E-1A2B3C4D5E6F} REG_SZ ' +
@@ -401,13 +410,52 @@ export const enginesSuite = {
401
410
  t.check(pcFound.every((i) => i.host !== path.join(pcRoot, 'gone')), 'an entry whose folder is gone is left out');
402
411
  t.equal(findUnrealInstalls(files, ['5.6']).map((i) => i.host), [pcDefault], 'a list maps only those versions');
403
412
  t.equal(findUnrealInstalls({ ...files, launcherInstalled: null, builds: '' }, true).map((i) => i.id), ['UE_5.4', 'UE_5.6'], 'no LauncherInstalled.dat: the default folders');
413
+ const otherDrive = makeEngine(path.join(pcRoot, 'F', 'Program Files', 'Epic Games', 'UE_5.8'), [5, 8, 0], true);
414
+ t.equal(findUnrealInstalls({
415
+ ...files,
416
+ launcherInstalled: null,
417
+ defaultRoot: path.join(pcRoot, 'none'),
418
+ otherRoots: [path.join(pcRoot, 'F', 'Program Files', 'Epic Games')],
419
+ }, true).map((i) => i.host), [otherDrive], 'an engine in the default place on another drive is found');
420
+ const nextTo = JSON.stringify({
421
+ InstallationList: [
422
+ {
423
+ InstallLocation: path.join(pcRoot, 'F', 'Program Files', 'Epic Games', 'SomeGame'),
424
+ NamespaceId: 'fn',
425
+ ItemId: 'g',
426
+ ArtifactId: 'SomeGame',
427
+ AppVersion: '1',
428
+ AppName: 'SomeGame',
429
+ },
430
+ ],
431
+ });
432
+ t.equal(findUnrealInstalls({ ...files, launcherInstalled: nextTo, defaultRoot: path.join(pcRoot, 'none') }, true).map((i) => i.id), ['UE_5.8'], 'and one next to what the launcher lists');
404
433
  const folders = findEngineFolders({ read: DISK.read, builds: reg }, [
405
434
  pcSource,
406
435
  path.join(pcRoot, 'nothing'),
407
436
  'relative',
408
437
  ]);
409
438
  t.equal(folders.map((f) => [f.id, f.writable]), [['dir-UnrealEngine', true]], 'a registered folder is mapped writable; one without an engine is left out');
410
- t.equal(findEpicGames(dat, true).map((g) => g.id), ['Fortnite'], "the launcher's games are the epic-game installs (engines are not)");
439
+ const pcManifests = t.dir('pc-pcManifests');
440
+ const itemText = JSON.stringify({
441
+ AppName: 'UE_5.4',
442
+ InstallLocation: pcLauncher.replace(/\\/g, '/'),
443
+ ManifestLocation: pcLauncher + '/.egstore',
444
+ StagingLocation: 'D:\\Elsewhere\\bps',
445
+ InstallSize: 123,
446
+ });
447
+ fs.writeFileSync(path.join(pcManifests, 'ABCDEF0123.item'), itemText);
448
+ fs.writeFileSync(path.join(pcManifests, 'notes.txt'), 'not a manifest');
449
+ const pcRegistered = findEngineFolders({ read: DISK.read, builds: '', launcherInstalled: dat, manifests: pcManifests, list: DISK.list }, [pcLauncher])[0];
450
+ t.check(pcRegistered?.launcher?.itemId === 'item54' && pcRegistered.launcherItem?.name === 'ABCDEF0123.item', 'a pcRegistered folder the launcher installed keeps its ids and its manifest');
451
+ const moved = relocateItem(itemText, pcLauncher, 'C:\\PcInstalls\\unreal\\dir-UE_5.4');
452
+ const movedItem = JSON.parse(moved ?? '{}');
453
+ t.check(movedItem.InstallLocation === 'C:\\PcInstalls\\unreal\\dir-UE_5.4' &&
454
+ movedItem.ManifestLocation === 'C:\\PcInstalls\\unreal\\dir-UE_5.4\\.egstore' &&
455
+ !('StagingLocation' in movedItem) &&
456
+ movedItem.InstallSize === 123, "the manifest names the sandbox's folder; a path elsewhere on this PC is dropped");
457
+ t.equal(relocateItem(itemText, path.join(pcRoot, 'other'), 'C:\\x'), null, 'a manifest of another folder: none');
458
+ t.equal(findEpicGames(dat, true).map((g) => g.id), ['Fortnite'], "the launcher's games are the epic-game installs (engines and the Fab plugin in one are not)");
411
459
  t.equal(findEpicGames(dat, ['nothing']), [], 'a list maps only those games');
412
460
  t.check(engineFolderProblem(pcSource, DISK.read) === null, 'Register an engine folder accepts an engine folder');
413
461
  t.includes(engineFolderProblem(pcRoot, DISK.read) ?? '', 'not an Unreal Engine folder', 'and refuses one without Build.version');
@@ -441,6 +489,8 @@ export const enginesSuite = {
441
489
  t.check(new Set(ids).size === ids.length, 'every app id is used once');
442
490
  t.equal(GAMEDEV_APPS.map((a) => a.id), ['krita', 'inkscape', 'audacity', 'renderdoc', 'f3d', 'imagemagick', 'nomacs', 'unity-mcp', 'openupm-cli'], 'Krita, Inkscape, Audacity, RenderDoc, F3D, ImageMagick, nomacs, then the Unity add-ons (Blender was there)');
443
491
  t.check(ids.includes('blender'), 'Blender is in the catalogue');
492
+ const f3d = GAMEDEV_APPS.find((a) => a.id === 'f3d');
493
+ t.check(f3d.onPath === true && !f3d.cli, 'F3D is on PATH (the preview guide runs `f3d`) and keeps its shortcut');
444
494
  t.check(GAMEDEV_APPS.every((a) => (a.category === 'gamedev' || a.id === 'unity-mcp') && a.exe && a.about.length > 20), 'each is a game-development app with its program');
445
495
  const audacity = new RegExp(GAMEDEV_APPS.find((a) => a.id === 'audacity').source.asset);
446
496
  t.check(audacity.test('audacity-win-4.0.2-x86_64.7z') &&
@@ -19,6 +19,7 @@ import { SECTION_LABELS } from '../cli/menu/shell.js';
19
19
  import { Frame, ListScreen, OverlayScreen, Text } from '../cli/tui/index.js';
20
20
  export const SIZES = [
21
21
  { width: 200, height: 60 },
22
+ { width: 120, height: 30 },
22
23
  { width: 100, height: 30 },
23
24
  { width: 80, height: 24 },
24
25
  { width: 60, height: 20 },
@@ -38,6 +39,48 @@ const DATA_FOLDER_ROW = [
38
39
  const ACCOUNTS = {
39
40
  agent: 'claude',
40
41
  services: [
42
+ {
43
+ kind: 'epic',
44
+ label: SERVICE_SPECS.epic.label,
45
+ share: true,
46
+ state: 'none',
47
+ source: null,
48
+ savedAt: null,
49
+ summary: null,
50
+ heldBy: null,
51
+ usedBy: ['unreal'],
52
+ usedFor: 'unreal',
53
+ warnings: [...SERVICE_SPECS.epic.warnings],
54
+ canImport: true,
55
+ canLogin: false,
56
+ needsFile: false,
57
+ fromPc: true,
58
+ hasFileSource: false,
59
+ onPc: true,
60
+ sandboxOnly: false,
61
+ ready: true,
62
+ },
63
+ {
64
+ kind: 'keystore',
65
+ label: SERVICE_SPECS.keystore.label,
66
+ share: false,
67
+ state: 'none',
68
+ source: null,
69
+ savedAt: null,
70
+ summary: null,
71
+ heldBy: null,
72
+ usedBy: ['flutter'],
73
+ usedFor: 'flutter',
74
+ warnings: [...SERVICE_SPECS.keystore.warnings],
75
+ canImport: true,
76
+ canLogin: false,
77
+ needsFile: true,
78
+ fromPc: false,
79
+ hasFileSource: true,
80
+ onPc: null,
81
+ sandboxOnly: false,
82
+ ready: false,
83
+ },
41
84
  {
42
85
  kind: 'perplexity',
43
86
  label: SERVICE_SPECS.perplexity.label,
@@ -53,6 +96,10 @@ const ACCOUNTS = {
53
96
  canImport: true,
54
97
  canLogin: true,
55
98
  needsFile: false,
99
+ fromPc: true,
100
+ hasFileSource: true,
101
+ onPc: true,
102
+ sandboxOnly: false,
56
103
  ready: true,
57
104
  },
58
105
  {
@@ -70,6 +117,10 @@ const ACCOUNTS = {
70
117
  canImport: false,
71
118
  canLogin: false,
72
119
  needsFile: false,
120
+ fromPc: false,
121
+ hasFileSource: false,
122
+ onPc: null,
123
+ sandboxOnly: true,
73
124
  ready: true,
74
125
  },
75
126
  ],
@@ -423,7 +474,7 @@ export function renderScreens(dir) {
423
474
  ],
424
475
  escape: 'cancel',
425
476
  });
426
- out.push(block('5 Settings: Save / Discard / Cancel', Frame.render({ ...dialog.render(bodySize), nav }, size.width, size.height).map((l) => Text.strip(l).replace(/\s+$/, '')), size), block('6 Accounts', draw(5, accountsOptions(ACCOUNTS), size, status), size), block('6 Accounts · codex', draw(5, { ...accountsOptions(ACCOUNTS), startCategory: 'codex' }, size, status), size), block('6 Accounts · opencode (login not kept)', draw(5, { ...accountsOptions(ACCOUNTS), startCategory: 'opencode' }, size, status), size));
477
+ out.push(block('5 Settings: Save / Discard / Cancel', Frame.render({ ...dialog.render(bodySize), nav }, size.width, size.height).map((l) => Text.strip(l).replace(/\s+$/, '')), size), block('6 Accounts', draw(5, accountsOptions(ACCOUNTS), size, status), size), ...['epic', 'perplexity', 'comet'].map((kind) => block('6 Accounts · ' + kind, draw(5, { ...accountsOptions(ACCOUNTS), startCategory: 'service:' + kind }, size, status), size)), block('6 Accounts · codex', draw(5, { ...accountsOptions(ACCOUNTS), startCategory: 'codex' }, size, status), size), block('6 Accounts · opencode (login not kept)', draw(5, { ...accountsOptions(ACCOUNTS), startCategory: 'opencode' }, size, status), size));
427
478
  for (const category of ['updates', 'data', 'health', 'logs', 'more'])
428
479
  out.push(block('7 Maintenance · ' + category, draw(6, { ...maintenanceOptions(maintenance), startCategory: category }, size, status), size));
429
480
  }
@@ -7,7 +7,7 @@ import { Draft } from '../cli/menu/settings/draft.js';
7
7
  import { z } from 'zod';
8
8
  import { addedAgents, agentNameProblem, commandFromPackage, newAgent } from '../app/features/agents/index.js';
9
9
  import { APP_GROUPS, appRows, REQUEST_APP, requestAppUrl } from '../app/features/apps/index.js';
10
- import { appsCategories, appsItems } from '../cli/menu/pages/apps-page.js';
10
+ import { appsCategories, appsItems, stageTicks } from '../cli/menu/pages/apps-page.js';
11
11
  import { readMenuView, saveMenuView } from '../cli/menu/memory.js';
12
12
  import path from 'node:path';
13
13
  import { argPathKind, optionLabel, optionPathKind, plainDescription } from '../cli/menu/command-fields.js';
@@ -633,6 +633,24 @@ export const menuSuite = {
633
633
  appItems.every((i) => groupIds.includes(i.category ?? '')) &&
634
634
  appItems.length === appRows(user2, t.dir('apps-groups')).length &&
635
635
  APP_CATEGORIES.every((c) => APP_GROUPS.some((g) => g.members.includes(c.id))), 'the apps are in at most 7 groups (with your own programs); every catalogue category belongs to one and every app is listed', groupIds.join());
636
+ t.section('a Store app ticked on the Apps page is saved and shown on');
637
+ const appsFile = ConfigFile.user(t.at('apps-store', 'config.json'));
638
+ const appsDraft = new Draft();
639
+ const perplexityRow = (d) => appsItems(appsFile, t.dir('apps-store-data'), d).find((i) => i.value === 'perplexity-desktop')?.checked;
640
+ t.check(perplexityRow(appsDraft) === false, 'the Perplexity desktop app starts off');
641
+ const alreadyOn = appRows(appsFile, t.dir('apps-store-data'))
642
+ .filter((r) => r.on)
643
+ .map((r) => r.id);
644
+ stageTicks(appsFile, appsDraft, [...alreadyOn, 'perplexity-desktop']);
645
+ t.check(perplexityRow(appsDraft) === true && appsDraft.count() === 1, 'ticking it shows it on and stages one change');
646
+ const applied = appsDraft.apply();
647
+ t.check(applied.saved === 1 &&
648
+ applied.problems.length === 0 &&
649
+ appsFile.valueInFile(['apps', 'install']).includes('perplexity-desktop') &&
650
+ perplexityRow(new Draft()) === true, 'Ctrl+S writes it into apps.install and the page still shows it on');
651
+ const toggleCtx = { loaded: { configPath: appsFile.file }, options: {}, dataPath: t.dir('apps-store-data') };
652
+ await runDefinition('apps.toggle', toggleCtx, { id: 'perplexity-desktop', on: false }, quiet());
653
+ t.check(!(ConfigFile.user(appsFile.file).valueInFile(['apps', 'install']) ?? []).includes('perplexity-desktop'), 'and the web toggle (apps.toggle) turns it off in the same list');
636
654
  t.section('dialog box');
637
655
  const box = dialogBox({ title: 'Unsaved changes', lines: ['You have 2 unsaved changes.'], buttons: 'Save' }, 61);
638
656
  const base = Array.from({ length: 12 }, () => 'x'.repeat(60));
@@ -742,11 +760,19 @@ export const menuSuite = {
742
760
  const size = /\((\d+)x(\d+)\)/.exec(head);
743
761
  return !size || rows.length !== Number(size[2]) || rows.some((l) => Text.width(l) > Number(size[1]) - 1);
744
762
  });
745
- t.equal(overflow.map((b) => b.slice(0, b.indexOf('\n'))), [], 'every screen is exactly as tall as the terminal and never wider, at 200x60, 100x30, 80x24 and 60x20');
746
- t.check(SIZES.length === 4 &&
763
+ t.equal(overflow.map((b) => b.slice(0, b.indexOf('\n'))), [], 'every screen is exactly as tall as the terminal and never wider, at 200x60, 120x30, 100x30, 80x24 and 60x20');
764
+ t.check(SIZES.length === 5 &&
747
765
  screens.includes('=== 7 Maintenance · health (80x24) ===') &&
748
766
  screens.includes('unsaved: 2') &&
749
767
  screens.includes('Save'), 'all screens render at both sizes; the settings screen shows its unsaved count and the leave dialog offers Save');
768
+ const serviceScreens = blocks.filter((b) => /^6 Accounts · (epic|perplexity|comet) \((80x24|120x30)\)/.test(b));
769
+ const serviceRows = serviceScreens.flatMap((b) => b.split('\n').slice(1));
770
+ t.check(serviceScreens.length === 6 &&
771
+ serviceRows.every((l) => !/\bsandbox (account|launch|config)\b/.test(l)) &&
772
+ serviceRows.every((l) => !l.includes('(encrypted)') && !/[A-Za-z]…/.test(l.replace(/(file|sign-in|Forget)…/g, ''))) &&
773
+ serviceRows.some((l) => l.includes('On this PC') && l.includes('found')) &&
774
+ serviceRows.some((l) => l.includes('Perplexity API')) &&
775
+ !serviceRows.some((l) => l.includes('Perplexity API key')), 'the service pages read short at 80 and 120 columns: short names, a few facts, no cut words, no command hints');
750
776
  const out = process.env.MENU_SCREENS_OUT;
751
777
  if (out)
752
778
  fs.writeFileSync(out, screens, 'utf8');
@@ -18,7 +18,7 @@ function statusWith(version) {
18
18
  },
19
19
  ],
20
20
  projects: [{ workspace: PROJECT.workspace, engine: 'C:\\E', reason: 'test' }],
21
- launcher: { installed: false, installLocation: null, problem: null, freeBytes: null, needBytes: 1 },
21
+ launcher: { installed: false, exe: null, installLocation: null, problem: null, freeBytes: null, needBytes: 1 },
22
22
  visualStudio: { version: '17.14', path: 'C:/VS', how: 'kept', check: 'ok', ok: true },
23
23
  debugger: null,
24
24
  ddc: { mode: 'persisted', dir: null },