windows-development-sandbox 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.sandbox-config.example.json +2 -1
  2. package/config.example.json +1 -0
  3. package/dist/cli/boot-progress.d.ts +22 -0
  4. package/dist/cli/boot-progress.js +59 -0
  5. package/dist/cli/commands/launch.d.ts +5 -2
  6. package/dist/cli/commands/launch.js +77 -22
  7. package/dist/cli/commands/maintenance.js +36 -3
  8. package/dist/cli/commands/project.d.ts +10 -6
  9. package/dist/cli/commands/project.js +42 -3
  10. package/dist/cli/commands/restart.js +17 -7
  11. package/dist/cli/commands/sessions.js +2 -1
  12. package/dist/cli/manifest.js +1 -1
  13. package/dist/cli/progress.d.ts +15 -0
  14. package/dist/cli/progress.js +52 -0
  15. package/dist/core/config.js +1 -1
  16. package/dist/core/local-config.d.ts +16 -1
  17. package/dist/core/local-config.js +15 -9
  18. package/dist/core/project-policy.d.ts +50 -3
  19. package/dist/core/project-policy.js +90 -4
  20. package/dist/core/schema/config.d.ts +3 -2
  21. package/dist/core/schema/config.js +1 -1
  22. package/dist/core/schema/projects.d.ts +1 -0
  23. package/dist/core/schema/projects.js +1 -0
  24. package/dist/core/schema/sandbox.d.ts +1 -1
  25. package/dist/core/schema/sandbox.js +1 -1
  26. package/dist/core/types.d.ts +18 -1
  27. package/dist/core/workspace.d.ts +21 -4
  28. package/dist/core/workspace.js +87 -26
  29. package/dist/guest/env.d.ts +6 -2
  30. package/dist/guest/env.js +15 -3
  31. package/dist/guest/main.js +5 -1
  32. package/dist/guest/toolchains/index.js +29 -3
  33. package/dist/host/channel.d.ts +2 -2
  34. package/dist/host/channel.js +3 -2
  35. package/dist/host/generate.d.ts +1 -1
  36. package/dist/host/generate.js +13 -6
  37. package/dist/host/join.d.ts +7 -0
  38. package/dist/host/join.js +23 -9
  39. package/dist/host/launch/mappings.d.ts +7 -2
  40. package/dist/host/launch/mappings.js +64 -18
  41. package/dist/host/launch/projects.d.ts +2 -0
  42. package/dist/host/launch/projects.js +11 -5
  43. package/dist/host/projects.d.ts +25 -2
  44. package/dist/host/projects.js +54 -5
  45. package/dist/runtime/daemon.js +71 -9
  46. package/dist/runtime/db.js +33 -3
  47. package/dist/runtime/main.js +191 -30
  48. package/dist/runtime/netproxy.js +15 -3
  49. package/dist/runtime/view.js +262 -128
  50. package/dist/selftest/channel.test.js +38 -2
  51. package/dist/selftest/fixtures.d.ts +2 -1
  52. package/dist/selftest/index.js +2 -1
  53. package/dist/selftest/live-terminal.test.d.ts +2 -0
  54. package/dist/selftest/live-terminal.test.js +169 -0
  55. package/dist/selftest/local-config.test.js +28 -3
  56. package/dist/selftest/mirror.test.js +36 -7
  57. package/dist/selftest/projects.test.js +76 -7
  58. package/dist/shared/console-input.d.ts +11 -0
  59. package/dist/shared/console-input.js +61 -0
  60. package/dist/shared/download.d.ts +3 -0
  61. package/dist/shared/download.js +25 -2
  62. package/dist/shared/guest-paths.d.ts +27 -0
  63. package/dist/shared/guest-paths.js +68 -0
  64. package/dist/shared/projects.d.ts +27 -4
  65. package/dist/shared/projects.js +72 -13
  66. package/dist/shared/pty-client.d.ts +17 -0
  67. package/dist/shared/pty-client.js +65 -8
  68. package/dist/shared/schema.d.ts +24 -0
  69. package/dist/shared/schema.js +12 -0
  70. package/package.json +1 -1
  71. package/sandbox/ptyhost.cs +60 -3
  72. package/schemas/config.schema.json +15 -1
  73. package/schemas/sandbox-config.schema.json +4 -3
@@ -5534,7 +5534,7 @@ var require_lodash = __commonJS({
5534
5534
  // src/guest/main.ts
5535
5535
  import fs36 from "node:fs";
5536
5536
  import path50 from "node:path";
5537
- import { spawn as spawn7 } from "node:child_process";
5537
+ import { spawn as spawn8 } from "node:child_process";
5538
5538
 
5539
5539
  // src/guest/guide.ts
5540
5540
  import fs6 from "node:fs";
@@ -5694,6 +5694,10 @@ var checksumRequired = false;
5694
5694
  var setChecksumRequired = (on) => {
5695
5695
  checksumRequired = on;
5696
5696
  };
5697
+ var downloadWatcher = null;
5698
+ var watchDownloads = (watcher) => {
5699
+ downloadWatcher = watcher;
5700
+ };
5697
5701
  var download = async (url2, destination, expected = null) => {
5698
5702
  const sums = typeof expected === "string" || expected === null ? { sha256: expected } : expected;
5699
5703
  if (checksumRequired && !sums.sha256?.trim() && !sums.sha512?.trim()) {
@@ -5706,8 +5710,25 @@ var download = async (url2, destination, expected = null) => {
5706
5710
  const want = sums[kind]?.trim().toLowerCase();
5707
5711
  return want ? [{ want, hash: crypto.createHash(kind) }] : [];
5708
5712
  });
5713
+ const size = Number(response.headers.get("content-length")) || 0;
5714
+ const report = size > 0 ? downloadWatcher?.() ?? null : null;
5715
+ let received = 0;
5716
+ let tenth = 0;
5717
+ let reportedAt = Date.now();
5709
5718
  const tap = new Transform({ transform(chunk, _encoding, done) {
5710
5719
  for (const c of checks) c.hash.update(chunk);
5720
+ if (report) {
5721
+ received += chunk.length;
5722
+ const now2 = Math.floor(received * 10 / size);
5723
+ if (now2 !== tenth || Date.now() - reportedAt > 5e3) {
5724
+ tenth = now2;
5725
+ reportedAt = Date.now();
5726
+ try {
5727
+ report(Math.min(1, received / size));
5728
+ } catch {
5729
+ }
5730
+ }
5731
+ }
5711
5732
  done(null, chunk);
5712
5733
  } });
5713
5734
  const partial2 = destination + ".part";
@@ -25603,7 +25624,7 @@ var ClaudeProfile = external_exports.strictObject({
25603
25624
  var GuestWorkspace = external_exports.union([external_exports.literal("auto"), external_exports.literal("mirror"), GuestPath]).describe('Where the workspace appears inside the sandbox. "auto" = C:\\Workspace\\<folder name>. "mirror" = the same path as on this PC, so absolute paths in configs and build caches match (another drive letter such as F: is a subst drive onto C:\\Mirror\\F); paths the sandbox reserves (C:\\Users\\..., C:\\Program Files\\...) fall back to "auto".');
25604
25625
  var Mapping = external_exports.strictObject({
25605
25626
  host: external_exports.string().min(1).describe("Folder on this PC (relative paths are relative to the config file)."),
25606
- guest: GuestPath.describe("Where it appears inside the sandbox, e.g. C:\\Assets, or on another drive letter (D: to Z:) such as F:\\SteamLibrary\\steamapps\\common\\Game: mapped to C:\\Mirror\\F\\... and shown as F: (subst) at boot."),
25627
+ guest: external_exports.string().min(1).max(1024).optional().describe("Where it appears inside the sandbox, e.g. C:\\Assets, or on another drive letter (D: to Z:) such as F:\\SteamLibrary\\steamapps\\common\\Game (shown as F: at boot). Leave it out to use the same path as on this PC (a folder inside the project stays where it is in the project). A path the sandbox cannot use (C:\\Windows\\..., C:\\Users\\..., one already in use) goes to C:\\Mapped\\<drive>\\<path> instead, and the launch says so."),
25607
25628
  readOnly: external_exports.boolean().describe("true = the agent can only read it."),
25608
25629
  optional: external_exports.boolean().optional().describe("true = skip it quietly when the host folder does not exist.")
25609
25630
  });
@@ -25891,6 +25912,7 @@ var Projects = external_exports.strictObject({
25891
25912
  roots: external_exports.array(external_exports.string().min(1)).describe('Folders whose subfolders are your projects (e.g. "F:\\\\Code"), mapped read/write at every start. Any project inside one, including ones you create later, opens in the running sandbox at once, without a restart. Every agent in the sandbox can see all of them.'),
25892
25913
  askToAddRoot: external_exports.boolean().describe("When you start a project whose parent folder is not a root yet, ask once whether to add it."),
25893
25914
  declinedRoots: external_exports.array(external_exports.string().min(1)).describe("Parent folders you said no to (not asked again)."),
25915
+ allowOutsideMappings: external_exports.union([external_exports.literal("ask"), external_exports.boolean()]).describe(`A project's .sandbox-config.json may map folders from outside the project (e.g. a game folder on another drive). "ask" = an interactive launch asks once per project and set of folders, and remembers your answer; true = always allowed (still listed when you approve the file); false = never (the launch stops and says which folders). Only your own settings decide this, never a project file.`),
25894
25916
  wsbShare: external_exports.boolean().describe("On Windows 11 24H2+ with the Windows Sandbox command line (wsb), add other folders to the running sandbox with `wsb share` (untested). Otherwise starting a project outside every root offers a restart.")
25895
25917
  });
25896
25918
 
@@ -25933,7 +25955,7 @@ var LocalOverrides = external_exports.strictObject({
25933
25955
  readOnly: external_exports.boolean().describe("Map this project read-only."),
25934
25956
  // A project may turn the filter on and keep more out, never let ignored files in.
25935
25957
  workspaceFilter: WorkspaceFilter.pick({ enabled: true, exclude: true }).partial(),
25936
- mappings: external_exports.array(Mapping).describe("Extra folders inside this project (relative to this file), e.g. a read-only data folder at another place in the sandbox."),
25958
+ mappings: external_exports.array(Mapping).describe("Extra folders (relative paths are relative to this file), e.g. a read-only data folder at another place in the sandbox. Folders outside the project are used only as your projects.allowOutsideMappings setting allows (by default you are asked once)."),
25937
25959
  scripts: Strings.describe("Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent."),
25938
25960
  // This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
25939
25961
  sandbox: Sandbox.omit({ cpuLimit: true, pageFileMb: true }).partial(),
@@ -25964,6 +25986,15 @@ var AttachEndpoint = external_exports.strictObject({
25964
25986
  port: external_exports.number().int().min(1024).max(65535),
25965
25987
  token: Text(256)
25966
25988
  });
25989
+ var BootProgress = external_exports.strictObject({
25990
+ step: external_exports.number().int().min(1).max(32),
25991
+ steps: external_exports.number().int().min(1).max(32),
25992
+ title: Text(120),
25993
+ /** Being set up now; percent while it downloads (when the size is known). */
25994
+ active: external_exports.array(external_exports.strictObject({ name: Text(64), percent: external_exports.number().int().min(0).max(100).optional() })).max(8),
25995
+ done: external_exports.number().int().min(0).max(1e3),
25996
+ total: external_exports.number().int().min(0).max(1e3)
25997
+ });
25967
25998
  var SandboxStatus = external_exports.object({
25968
25999
  launchId: Text(64),
25969
26000
  phase: external_exports.enum(["booting", "provisioning", "ready", "failed"]),
@@ -25973,7 +26004,9 @@ var SandboxStatus = external_exports.object({
25973
26004
  guestWorkspace: Text(),
25974
26005
  headless: external_exports.boolean(),
25975
26006
  /** The sandbox's IPv4 addresses (for network.forwardPorts; the host only uses ones on its Hyper-V network). */
25976
- addresses: external_exports.array(external_exports.string().max(45)).max(8).optional()
26007
+ addresses: external_exports.array(external_exports.string().max(45)).max(8).optional(),
26008
+ /** A malformed progress is dropped, never the status (the phase still counts). */
26009
+ progress: BootProgress.optional().catch(void 0)
25977
26010
  });
25978
26011
  var Version = external_exports.strictObject({ spec: Text(128), source: Text(256) });
25979
26012
  var DatabasePlan = external_exports.strictObject({
@@ -26136,16 +26169,25 @@ var mkdirp = (p) => {
26136
26169
  return p;
26137
26170
  };
26138
26171
  var logPath = logFile;
26139
- var setSandboxStatus = (phase, message) => {
26172
+ var progress;
26173
+ var setSandboxStatus = (phase, message, update) => {
26140
26174
  try {
26175
+ if (update) progress = fitProgress({ step: 1, steps: 1, title: "", active: [], done: 0, total: 0, ...progress, ...update });
26141
26176
  writeJsonAtomicChecked(phase, message);
26142
26177
  } catch (e) {
26143
26178
  warn("Could not update sandbox status: " + errMsg(e));
26144
26179
  }
26145
26180
  };
26181
+ var fitProgress = (p) => ({
26182
+ ...p,
26183
+ title: p.title.slice(0, 120),
26184
+ done: Math.min(p.done, 1e3),
26185
+ total: Math.min(p.total, 1e3),
26186
+ active: p.active.slice(0, 8).map((a) => ({ name: a.name.slice(0, 64), ...a.percent === void 0 ? {} : { percent: Math.max(0, Math.min(100, Math.round(a.percent))) } }))
26187
+ });
26146
26188
  var ownAddresses = () => Object.values(os3.networkInterfaces()).flat().filter((i) => Boolean(i) && i.family === "IPv4" && !i.internal).map((i) => i.address).slice(0, 8);
26147
26189
  var writeJsonAtomicChecked = (phase, message) => {
26148
- writeJsonAtomic(run.sandbox, { launchId: session.launchId, phase, message, updatedAt: now(), workspaceHost: session.workspaceHost, guestWorkspace: workspace, headless: session.headless, addresses: ownAddresses() });
26190
+ writeJsonAtomic(run.sandbox, { launchId: session.launchId, phase, message, updatedAt: now(), workspaceHost: session.workspaceHost, guestWorkspace: workspace, headless: session.headless, addresses: ownAddresses(), ...progress ? { progress } : {} });
26149
26191
  };
26150
26192
  var exec = (file3, args, options = {}) => {
26151
26193
  const result = spawnSync(file3, args, { stdio: "inherit", windowsHide: true, ...options });
@@ -26160,7 +26202,7 @@ var execOutput = (file3, args, options = {}) => {
26160
26202
  const result = spawnSync(file3, args, { encoding: "utf8", windowsHide: true, ...options });
26161
26203
  return result.status === 0 ? String(result.stdout ?? "").trim() : "";
26162
26204
  };
26163
- var powershell = (script) => exec(SYSTEM.powershell(), ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script]);
26205
+ var powershell = (script2) => exec(SYSTEM.powershell(), ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script2]);
26164
26206
  var robocopy = (from, to, files = []) => {
26165
26207
  if (!isWin) {
26166
26208
  if (!files.length) {
@@ -26239,6 +26281,9 @@ var removeGuide = (file3) => {
26239
26281
  if (start >= 0 && end > start) fs6.writeFileSync(file3, (current.slice(0, start) + current.slice(end + END.length)).trim() + "\n", "utf8");
26240
26282
  };
26241
26283
 
26284
+ // src/guest/toolchains/index.ts
26285
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
26286
+
26242
26287
  // src/guest/toolchains/common.ts
26243
26288
  import fs7 from "node:fs";
26244
26289
  import path8 from "node:path";
@@ -26846,8 +26891,8 @@ var setupDynamo = async (env, plan2) => {
26846
26891
  env.summary.push("dynamodb (DynamoDB Local): " + endpoint + " (AWS SDKs use it automatically via AWS_ENDPOINT_URL_DYNAMODB; dummy credentials)");
26847
26892
  if (!plan2.adminPort) return;
26848
26893
  const prefix = tool("dynamodb-admin");
26849
- const script = path13.join(prefix, "node_modules", "dynamodb-admin", "dist", "dynamodb-admin.js");
26850
- if (!exists(script)) {
26894
+ const script2 = path13.join(prefix, "node_modules", "dynamodb-admin", "dist", "dynamodb-admin.js");
26895
+ if (!exists(script2)) {
26851
26896
  log("Installing dynamodb-admin, the DynamoDB web UI (~20 MB once) ...");
26852
26897
  const npmCli = path13.join(path13.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
26853
26898
  await execAsync(process.execPath, [npmCli, "install", "--global", "--prefix", prefix, "dynamodb-admin", "--no-fund", "--no-audit"], { stdio: "ignore" });
@@ -26857,7 +26902,7 @@ var setupDynamo = async (env, plan2) => {
26857
26902
  return;
26858
26903
  }
26859
26904
  const out = fs12.openSync(path13.join(logsDir, "dynamodb-admin.log"), "a");
26860
- spawn3(process.execPath, [script, "--port", String(plan2.adminPort), "--dynamo-endpoint", endpoint, "--skip-default-credentials"], { detached: true, stdio: ["ignore", out, out], windowsHide: true, env: { ...process.env, ...aws } }).unref();
26905
+ spawn3(process.execPath, [script2, "--port", String(plan2.adminPort), "--dynamo-endpoint", endpoint, "--skip-default-credentials"], { detached: true, stdio: ["ignore", out, out], windowsHide: true, env: { ...process.env, ...aws } }).unref();
26861
26906
  fs12.closeSync(out);
26862
26907
  if (!await waitForPort(plan2.adminPort, 30)) throw new Error("dynamodb-admin did not start; see " + path13.join(logsDir, "dynamodb-admin.log"));
26863
26908
  env.summary.push("dynamodb-admin: http://localhost:" + plan2.adminPort + " (browse and edit tables in the sandbox's browser)");
@@ -27315,7 +27360,7 @@ var DotnetMarker = external_exports.record(external_exports.string(), external_e
27315
27360
  var setupDotnet = async (env, config2, refresh) => {
27316
27361
  const root = tool("dotnet");
27317
27362
  const marker = path24.join(root, ".agent-sandbox-installed.json");
27318
- const script = tool("dotnet-install.ps1");
27363
+ const script2 = tool("dotnet-install.ps1");
27319
27364
  if (dryRun) {
27320
27365
  log("(dry run) would provision .NET SDK channels " + config2.channels.join(", "));
27321
27366
  return;
@@ -27328,9 +27373,9 @@ var setupDotnet = async (env, config2, refresh) => {
27328
27373
  ...exists(globalJson) ? [["global.json:" + fs17.readFileSync(globalJson, "utf8").replace(/\s+/g, ""), ["-JSonFile", globalJson]]] : []
27329
27374
  ];
27330
27375
  for (const [key, args] of installs.filter(([k]) => !done[k])) {
27331
- await downloadFile("https://dot.net/v1/dotnet-install.ps1", script);
27376
+ await downloadFile("https://dot.net/v1/dotnet-install.ps1", script2);
27332
27377
  log("Installing the .NET SDK " + (key.startsWith("channel:") ? key.slice(8) : "pinned in global.json") + " (~250 MB once) ...");
27333
- await execAsync(SYSTEM.powershell(), ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script, ...args, "-InstallDir", root, "-NoPath"]);
27378
+ await execAsync(SYSTEM.powershell(), ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script2, ...args, "-InstallDir", root, "-NoPath"]);
27334
27379
  done[key] = true;
27335
27380
  writeJsonAtomic(marker, done);
27336
27381
  }
@@ -27881,8 +27926,8 @@ var shimPair = (folder, version2, dir) => {
27881
27926
  shim(folder, "pip" + version2, python + " -m pip");
27882
27927
  };
27883
27928
  var pyLauncher = (folder, py3, py2) => {
27884
- const script = path33.join(mkdirp(folder), "py-launcher.py");
27885
- fs24.writeFileSync(script, [
27929
+ const script2 = path33.join(mkdirp(folder), "py-launcher.py");
27930
+ fs24.writeFileSync(script2, [
27886
27931
  "import subprocess, sys",
27887
27932
  "PY3, PY2 = " + JSON.stringify(path33.join(py3, "python.exe")) + ", " + JSON.stringify(py2 ? path33.join(py2, "python.exe") : null),
27888
27933
  "args = sys.argv[1:]",
@@ -27895,7 +27940,7 @@ var pyLauncher = (folder, py3, py2) => {
27895
27940
  "sys.exit(subprocess.call([exe] + args))",
27896
27941
  ""
27897
27942
  ].join("\n"));
27898
- shim(folder, "py", '"' + path33.join(py3, "python.exe") + '" "' + script + '"');
27943
+ shim(folder, "py", '"' + path33.join(py3, "python.exe") + '" "' + script2 + '"');
27899
27944
  };
27900
27945
  var setupPython = async (env, config2, versions) => {
27901
27946
  if (dryRun) {
@@ -29706,6 +29751,7 @@ var plan = (config2, versions, o) => {
29706
29751
 
29707
29752
  // src/guest/toolchains/index.ts
29708
29753
  var PARALLEL2 = 4;
29754
+ var currentStep = new AsyncLocalStorage2();
29709
29755
  var locks = /* @__PURE__ */ new Map();
29710
29756
  var lockKey = (name) => name.split(" ")[0].toLowerCase();
29711
29757
  var exclusive = async (name, work) => {
@@ -29731,10 +29777,19 @@ var runSteps = async (steps, o) => {
29731
29777
  const running = /* @__PURE__ */ new Set();
29732
29778
  const waiting = [];
29733
29779
  let done = 0;
29734
- const say = o.report ?? ((message) => setSandboxStatus("provisioning", message));
29780
+ const percent = /* @__PURE__ */ new Map();
29781
+ const active = () => [...running].map((name) => ({ name, ...percent.has(name) ? { percent: percent.get(name) } : {} }));
29782
+ const say = o.report ?? ((message) => setSandboxStatus("provisioning", message, { active: active(), done, total: steps.length }));
29735
29783
  const report = () => {
29736
29784
  if (running.size) say("Installing " + [...running].join(", ") + " (" + done + " of " + steps.length + " done)");
29737
29785
  };
29786
+ if (!o.report) watchDownloads(() => {
29787
+ const name = currentStep.getStore();
29788
+ return name ? (fraction) => {
29789
+ percent.set(name, Math.round(fraction * 100));
29790
+ report();
29791
+ } : null;
29792
+ });
29738
29793
  for (const s of steps) {
29739
29794
  finished.set(s.name, (async () => {
29740
29795
  await Promise.all((s.after ?? []).map((name) => finished.get(name)));
@@ -29748,16 +29803,22 @@ var runSteps = async (steps, o) => {
29748
29803
  running.add(s.name);
29749
29804
  report();
29750
29805
  try {
29751
- await exclusive(s.name, () => step(s.name, envs.get(s.name), async () => s.run(envs.get(s.name))));
29806
+ await currentStep.run(s.name, () => exclusive(s.name, () => step(s.name, envs.get(s.name), async () => s.run(envs.get(s.name)))));
29752
29807
  } finally {
29753
29808
  running.delete(s.name);
29809
+ percent.delete(s.name);
29754
29810
  done++;
29755
29811
  report();
29756
29812
  waiting.shift()?.();
29757
29813
  }
29758
29814
  })());
29759
29815
  }
29760
- await Promise.all(finished.values());
29816
+ try {
29817
+ await Promise.all(finished.values());
29818
+ } finally {
29819
+ if (!o.report) watchDownloads(null);
29820
+ }
29821
+ if (!o.report) setSandboxStatus("provisioning", "Set up " + steps.length + " toolchains and databases", { active: [], done, total: steps.length });
29761
29822
  return envs;
29762
29823
  };
29763
29824
  var setupToolchains = async (config2, versions, options, run3 = {}) => {
@@ -29976,13 +30037,13 @@ var agentEnv = (tools, target = { workspace, project: session.project, base: pro
29976
30037
  var persistEnvironment = (env, toolVars2) => {
29977
30038
  if (!isWin || dryRun || testMode) return;
29978
30039
  const vars = Object.fromEntries([...toolVars2, ...AGENT_VARS].filter((n) => env[n] !== void 0).map((n) => [n, env[n]]));
29979
- const script = [
30040
+ const script2 = [
29980
30041
  "$vars = '" + JSON.stringify(vars).replace(/'/g, "''") + "' | ConvertFrom-Json",
29981
30042
  "foreach ($p in $vars.PSObject.Properties) { [Environment]::SetEnvironmentVariable($p.Name, [string]$p.Value, 'Machine') }",
29982
30043
  "[Environment]::SetEnvironmentVariable('Path', '" + (env.PATH ?? "").replace(/'/g, "''") + "', 'Machine')"
29983
30044
  ].join("\n");
29984
30045
  try {
29985
- powershell(script);
30046
+ powershell(script2);
29986
30047
  } catch (e) {
29987
30048
  warn("Could not persist the environment machine-wide: " + errMsg(e));
29988
30049
  }
@@ -30441,8 +30502,65 @@ var enableProcessAudit = () => {
30441
30502
 
30442
30503
  // src/shared/pty-client.ts
30443
30504
  import net2 from "node:net";
30505
+
30506
+ // src/shared/console-input.ts
30507
+ import { spawn as spawn7 } from "node:child_process";
30508
+ var script = (pid) => [
30509
+ 'Add-Type -TypeDefinition @"',
30510
+ "using System; using System.Runtime.InteropServices;",
30511
+ "public static class SbxVtInput {",
30512
+ ' [DllImport("kernel32.dll")] static extern bool FreeConsole();',
30513
+ ' [DllImport("kernel32.dll")] static extern bool AttachConsole(uint pid);',
30514
+ ' [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] static extern IntPtr CreateFileW(string name, uint access, uint share, IntPtr security, uint creation, uint flags, IntPtr template);',
30515
+ ' [DllImport("kernel32.dll")] static extern bool GetConsoleMode(IntPtr handle, out uint mode);',
30516
+ ' [DllImport("kernel32.dll")] static extern bool SetConsoleMode(IntPtr handle, uint mode);',
30517
+ ' [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);',
30518
+ " public static int On(uint pid) {",
30519
+ " FreeConsole();",
30520
+ " if (!AttachConsole(pid)) return 2;",
30521
+ ' IntPtr input = CreateFileW("CONIN$", 0xC0000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);',
30522
+ " if (input == new IntPtr(-1)) return 3;",
30523
+ " uint mode; bool ok = GetConsoleMode(input, out mode) && SetConsoleMode(input, mode | 0x200);",
30524
+ " CloseHandle(input);",
30525
+ " FreeConsole();",
30526
+ " return ok ? 0 : 4;",
30527
+ " }",
30528
+ "}",
30529
+ '"@',
30530
+ "exit [SbxVtInput]::On(" + pid + ")"
30531
+ ].join("\n");
30532
+ var enableVtInput = () => {
30533
+ if (process.platform !== "win32" || !process.stdin.isTTY) return { ready: Promise.resolve(false), stop: () => Promise.resolve() };
30534
+ let child = null;
30535
+ let exited = Promise.resolve();
30536
+ const ready = new Promise((resolve2) => {
30537
+ try {
30538
+ child = spawn7(SYSTEM.powershell(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(script(process.pid), "utf16le").toString("base64")], { stdio: "ignore", windowsHide: true });
30539
+ } catch {
30540
+ resolve2(false);
30541
+ return;
30542
+ }
30543
+ exited = new Promise((done) => {
30544
+ child?.on("exit", (code) => {
30545
+ resolve2(code === 0);
30546
+ done();
30547
+ });
30548
+ child?.on("error", () => {
30549
+ resolve2(false);
30550
+ done();
30551
+ });
30552
+ });
30553
+ });
30554
+ return { ready, stop: () => {
30555
+ if (child && child.exitCode === null && child.signalCode === null) child.kill();
30556
+ return exited;
30557
+ } };
30558
+ };
30559
+
30560
+ // src/shared/pty-client.ts
30444
30561
  var FrameType = { Hello: 1, Input: 2, Resize: 3, Restart: 4, Ping: 5, Output: 16, Exit: 17, Error: 18, Restarting: 19, Status: 20, Console: 21 };
30445
30562
  var PING_INTERVAL_MS = 5e3;
30563
+ var MOUSE_OFF = "\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l";
30446
30564
  var encodeFrame = (type, payload = Buffer.alloc(0)) => {
30447
30565
  const header = Buffer.alloc(5);
30448
30566
  header.writeUInt8(type, 0);
@@ -30618,6 +30736,34 @@ var fixWindowsKeys = (chunk) => {
30618
30736
  if (!chunk.includes(8) && !chunk.includes(127)) return chunk;
30619
30737
  return Buffer.from(chunk.map((b) => b === 8 ? 127 : b === 127 ? 8 : b));
30620
30738
  };
30739
+ var JOIN_MS = 30;
30740
+ var MAX_HELD = 64;
30741
+ var InputJoiner = class {
30742
+ held = Buffer.alloc(0);
30743
+ /** The bytes to send now; an unfinished sequence at the end is kept for the next chunk. */
30744
+ push(chunk) {
30745
+ const data = this.held.length ? Buffer.concat([this.held, chunk]) : chunk;
30746
+ const cut = unfinishedSequence(data);
30747
+ this.held = Buffer.from(data.subarray(cut));
30748
+ return data.subarray(0, cut);
30749
+ }
30750
+ holding() {
30751
+ return this.held.length > 0;
30752
+ }
30753
+ flush() {
30754
+ const out = this.held;
30755
+ this.held = Buffer.alloc(0);
30756
+ return out;
30757
+ }
30758
+ };
30759
+ var unfinishedSequence = (data) => {
30760
+ const start = data.lastIndexOf(27, -1);
30761
+ if (start === -1 || data.length - start > MAX_HELD) return data.length;
30762
+ const rest = data.subarray(start + 1);
30763
+ if (rest.length === 0) return start;
30764
+ if (rest[0] === 91 || rest[0] === 79) return rest.subarray(1).every((b) => b >= 32 && b <= 63) && (rest[0] === 91 || rest.length === 1) ? start : data.length;
30765
+ return data.length;
30766
+ };
30621
30767
  var runTerminalClient = (socket, token, options = {}) => {
30622
30768
  const stdin = process.stdin;
30623
30769
  const stdout = process.stdout;
@@ -30625,13 +30771,14 @@ var runTerminalClient = (socket, token, options = {}) => {
30625
30771
  const [c, r] = stdout.isTTY ? stdout.getWindowSize() : [0, 0];
30626
30772
  return [c || 120, r || 30];
30627
30773
  };
30628
- const swapKeys = process.platform === "win32" && stdin.isTTY;
30774
+ let swapKeys = process.platform === "win32" && stdin.isTTY;
30629
30775
  return new Promise((resolve2) => {
30630
30776
  let done = false;
30631
30777
  const reader = new FrameReader();
30632
30778
  const filter = new OutputFilter();
30633
30779
  let sent = size();
30634
30780
  let resizeTimer;
30781
+ let vtInput;
30635
30782
  const finish = (result) => {
30636
30783
  if (done) return;
30637
30784
  done = true;
@@ -30640,18 +30787,28 @@ var runTerminalClient = (socket, token, options = {}) => {
30640
30787
  clearTimeout(resizeTimer);
30641
30788
  clearInterval(heartbeat);
30642
30789
  clearInterval(sizeWatch);
30643
- if (stdin.isTTY) stdin.setRawMode(false);
30644
- stdin.pause();
30790
+ clearTimeout(joinTimer);
30645
30791
  socket.destroy();
30646
- stdout.write("\x1B[0m\x1B[?25h\r\n");
30647
- resolve2(result);
30792
+ stdout.write("\x1B[0m\x1B[?25h" + MOUSE_OFF + "\r\n");
30793
+ void (vtInput?.stop() ?? Promise.resolve()).then(() => {
30794
+ if (stdin.isTTY) stdin.setRawMode(false);
30795
+ stdin.pause();
30796
+ resolve2(result);
30797
+ });
30798
+ };
30799
+ const joiner = new InputJoiner();
30800
+ let joinTimer;
30801
+ const sendInput = (data) => {
30802
+ if (data.length && !done) socket.write(encodeFrame(FrameType.Input, swapKeys ? fixWindowsKeys(data) : data));
30648
30803
  };
30649
30804
  const onInput = (chunk) => {
30650
30805
  if (options.detachKey !== void 0 && chunk.includes(options.detachKey)) {
30651
30806
  finish({ reason: "detach" });
30652
30807
  return;
30653
30808
  }
30654
- socket.write(encodeFrame(FrameType.Input, swapKeys ? fixWindowsKeys(chunk) : chunk));
30809
+ clearTimeout(joinTimer);
30810
+ sendInput(joiner.push(chunk));
30811
+ if (joiner.holding()) joinTimer = setTimeout(() => sendInput(joiner.flush()), JOIN_MS);
30655
30812
  };
30656
30813
  let passthrough = false;
30657
30814
  const sendSize = () => {
@@ -30683,7 +30840,7 @@ var runTerminalClient = (socket, token, options = {}) => {
30683
30840
  for (const frame of frames) {
30684
30841
  if (frame.type === FrameType.Output) stdout.write(filter.push(frame.payload));
30685
30842
  else if (frame.type === FrameType.Console) passthrough = frame.payload.toString("latin1") === "conpty.dll";
30686
- else if (frame.type === FrameType.Restarting) stdout.write("\x1B[2J\x1B[3J\x1B[H");
30843
+ else if (frame.type === FrameType.Restarting) stdout.write(MOUSE_OFF + "\x1B[2J\x1B[3J\x1B[H");
30687
30844
  else if (frame.type === FrameType.Exit) finish({ reason: "exit", exitCode: frame.payload.length >= 4 ? frame.payload.readInt32LE(0) : 1 });
30688
30845
  else if (frame.type === FrameType.Error) finish({ reason: "error", message: frame.payload.toString("utf8").slice(0, 500).replace(/[\x00-\x1f\x7f]/g, " ") });
30689
30846
  }
@@ -30694,6 +30851,10 @@ var runTerminalClient = (socket, token, options = {}) => {
30694
30851
  stdout.write("\x1B[2J\x1B[3J\x1B[H");
30695
30852
  socket.write(helloFrame(token, c, r));
30696
30853
  if (stdin.isTTY) stdin.setRawMode(true);
30854
+ vtInput = enableVtInput();
30855
+ void vtInput.ready.then((on) => {
30856
+ if (on) swapKeys = false;
30857
+ });
30697
30858
  stdin.on("data", onInput);
30698
30859
  stdin.resume();
30699
30860
  stdout.on("resize", onResize);
@@ -30794,7 +30955,7 @@ var saveProjectStatus = (name, workspace2, phase, message, databases, summary) =
30794
30955
  var STEPS = 8;
30795
30956
  var step2 = (n, title) => {
30796
30957
  log("\n\x1B[36m[" + n + "/" + STEPS + "] " + title + "\x1B[0m");
30797
- setSandboxStatus(n === 1 ? "booting" : "provisioning", "[" + n + "/" + STEPS + "] " + title);
30958
+ setSandboxStatus(n === 1 ? "booting" : "provisioning", "[" + n + "/" + STEPS + "] " + title, { step: n, steps: STEPS, title: title.replace(/ \(.*\)$/, ""), active: [], done: 0, total: 0 });
30798
30959
  };
30799
30960
  var soft = async (what, work) => {
30800
30961
  try {
@@ -30804,7 +30965,7 @@ var soft = async (what, work) => {
30804
30965
  }
30805
30966
  };
30806
30967
  var startDaemon = (env) => {
30807
- spawn7(process.execPath, [path50.join(path50.dirname(process.argv[1]), "daemon.js")], { detached: true, stdio: "ignore", windowsHide: true, env: { ...env, AGENT_LOG_PREFIX: "daemon" } }).unref();
30968
+ spawn8(process.execPath, [path50.join(path50.dirname(process.argv[1]), "daemon.js")], { detached: true, stdio: "ignore", windowsHide: true, env: { ...env, AGENT_LOG_PREFIX: "daemon" } }).unref();
30808
30969
  };
30809
30970
  var writeDbCommand = () => {
30810
30971
  const dir = mkdirp(path50.join(localDir, "bin"));
@@ -25404,7 +25404,7 @@ var ClaudeProfile = external_exports.strictObject({
25404
25404
  var GuestWorkspace = external_exports.union([external_exports.literal("auto"), external_exports.literal("mirror"), GuestPath]).describe('Where the workspace appears inside the sandbox. "auto" = C:\\Workspace\\<folder name>. "mirror" = the same path as on this PC, so absolute paths in configs and build caches match (another drive letter such as F: is a subst drive onto C:\\Mirror\\F); paths the sandbox reserves (C:\\Users\\..., C:\\Program Files\\...) fall back to "auto".');
25405
25405
  var Mapping = external_exports.strictObject({
25406
25406
  host: external_exports.string().min(1).describe("Folder on this PC (relative paths are relative to the config file)."),
25407
- guest: GuestPath.describe("Where it appears inside the sandbox, e.g. C:\\Assets, or on another drive letter (D: to Z:) such as F:\\SteamLibrary\\steamapps\\common\\Game: mapped to C:\\Mirror\\F\\... and shown as F: (subst) at boot."),
25407
+ guest: external_exports.string().min(1).max(1024).optional().describe("Where it appears inside the sandbox, e.g. C:\\Assets, or on another drive letter (D: to Z:) such as F:\\SteamLibrary\\steamapps\\common\\Game (shown as F: at boot). Leave it out to use the same path as on this PC (a folder inside the project stays where it is in the project). A path the sandbox cannot use (C:\\Windows\\..., C:\\Users\\..., one already in use) goes to C:\\Mapped\\<drive>\\<path> instead, and the launch says so."),
25408
25408
  readOnly: external_exports.boolean().describe("true = the agent can only read it."),
25409
25409
  optional: external_exports.boolean().optional().describe("true = skip it quietly when the host folder does not exist.")
25410
25410
  });
@@ -25661,6 +25661,7 @@ var Projects = external_exports.strictObject({
25661
25661
  roots: external_exports.array(external_exports.string().min(1)).describe('Folders whose subfolders are your projects (e.g. "F:\\\\Code"), mapped read/write at every start. Any project inside one, including ones you create later, opens in the running sandbox at once, without a restart. Every agent in the sandbox can see all of them.'),
25662
25662
  askToAddRoot: external_exports.boolean().describe("When you start a project whose parent folder is not a root yet, ask once whether to add it."),
25663
25663
  declinedRoots: external_exports.array(external_exports.string().min(1)).describe("Parent folders you said no to (not asked again)."),
25664
+ allowOutsideMappings: external_exports.union([external_exports.literal("ask"), external_exports.boolean()]).describe(`A project's .sandbox-config.json may map folders from outside the project (e.g. a game folder on another drive). "ask" = an interactive launch asks once per project and set of folders, and remembers your answer; true = always allowed (still listed when you approve the file); false = never (the launch stops and says which folders). Only your own settings decide this, never a project file.`),
25664
25665
  wsbShare: external_exports.boolean().describe("On Windows 11 24H2+ with the Windows Sandbox command line (wsb), add other folders to the running sandbox with `wsb share` (untested). Otherwise starting a project outside every root offers a restart.")
25665
25666
  });
25666
25667
 
@@ -25703,7 +25704,7 @@ var LocalOverrides = external_exports.strictObject({
25703
25704
  readOnly: external_exports.boolean().describe("Map this project read-only."),
25704
25705
  // A project may turn the filter on and keep more out, never let ignored files in.
25705
25706
  workspaceFilter: WorkspaceFilter.pick({ enabled: true, exclude: true }).partial(),
25706
- mappings: external_exports.array(Mapping).describe("Extra folders inside this project (relative to this file), e.g. a read-only data folder at another place in the sandbox."),
25707
+ mappings: external_exports.array(Mapping).describe("Extra folders (relative paths are relative to this file), e.g. a read-only data folder at another place in the sandbox. Folders outside the project are used only as your projects.allowOutsideMappings setting allows (by default you are asked once)."),
25707
25708
  scripts: Strings.describe("Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent."),
25708
25709
  // This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
25709
25710
  sandbox: Sandbox.omit({ cpuLimit: true, pageFileMb: true }).partial(),
@@ -25734,6 +25735,15 @@ var AttachEndpoint = external_exports.strictObject({
25734
25735
  port: external_exports.number().int().min(1024).max(65535),
25735
25736
  token: Text(256)
25736
25737
  });
25738
+ var BootProgress = external_exports.strictObject({
25739
+ step: external_exports.number().int().min(1).max(32),
25740
+ steps: external_exports.number().int().min(1).max(32),
25741
+ title: Text(120),
25742
+ /** Being set up now; percent while it downloads (when the size is known). */
25743
+ active: external_exports.array(external_exports.strictObject({ name: Text(64), percent: external_exports.number().int().min(0).max(100).optional() })).max(8),
25744
+ done: external_exports.number().int().min(0).max(1e3),
25745
+ total: external_exports.number().int().min(0).max(1e3)
25746
+ });
25737
25747
  var SandboxStatus = external_exports.object({
25738
25748
  launchId: Text(64),
25739
25749
  phase: external_exports.enum(["booting", "provisioning", "ready", "failed"]),
@@ -25743,7 +25753,9 @@ var SandboxStatus = external_exports.object({
25743
25753
  guestWorkspace: Text(),
25744
25754
  headless: external_exports.boolean(),
25745
25755
  /** The sandbox's IPv4 addresses (for network.forwardPorts; the host only uses ones on its Hyper-V network). */
25746
- addresses: external_exports.array(external_exports.string().max(45)).max(8).optional()
25756
+ addresses: external_exports.array(external_exports.string().max(45)).max(8).optional(),
25757
+ /** A malformed progress is dropped, never the status (the phase still counts). */
25758
+ progress: BootProgress.optional().catch(void 0)
25747
25759
  });
25748
25760
  var Version = external_exports.strictObject({ spec: Text(128), source: Text(256) });
25749
25761
  var DatabasePlan = external_exports.strictObject({