windows-development-sandbox 1.1.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/config.example.json +38 -5
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.js +3 -1
- package/dist/cli/commands/accounts.js +2 -2
- package/dist/cli/commands/agent-commands.js +1 -1
- package/dist/cli/commands/context.js +2 -1
- package/dist/cli/commands/data/clean.js +6 -1
- package/dist/cli/commands/data/collect.js +3 -3
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/launch.js +36 -3
- package/dist/cli/commands/maintenance.d.ts +2 -0
- package/dist/cli/commands/maintenance.js +35 -2
- package/dist/cli/commands/sessions.js +3 -3
- package/dist/cli/commands/user-config.js +1 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/manifest.js +5 -0
- package/dist/cli/menu/apps.d.ts +21 -0
- package/dist/cli/menu/apps.js +122 -0
- package/dist/cli/menu/command-form.d.ts +28 -0
- package/dist/cli/menu/command-form.js +219 -0
- package/dist/cli/menu/index.d.ts +26 -0
- package/dist/cli/menu/index.js +180 -0
- package/dist/cli/menu/settings/categories.d.ts +9 -0
- package/dist/cli/menu/settings/categories.js +28 -0
- package/dist/cli/menu/settings/config-file.d.ts +40 -0
- package/dist/cli/menu/settings/config-file.js +71 -0
- package/dist/cli/menu/settings/editors.d.ts +30 -0
- package/dist/cli/menu/settings/editors.js +305 -0
- package/dist/cli/menu/settings/index.d.ts +32 -0
- package/dist/cli/menu/settings/index.js +136 -0
- package/dist/cli/menu/settings/schema-node.d.ts +62 -0
- package/dist/cli/menu/settings/schema-node.js +104 -0
- package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
- package/dist/cli/menu/several.js +102 -0
- package/dist/cli/tui/app.d.ts +39 -0
- package/dist/cli/tui/app.js +52 -0
- package/dist/cli/tui/dialogs.d.ts +57 -0
- package/dist/cli/tui/dialogs.js +162 -0
- package/dist/cli/tui/frame.d.ts +18 -0
- package/dist/cli/tui/frame.js +27 -0
- package/dist/cli/tui/index.d.ts +7 -0
- package/dist/cli/tui/index.js +8 -0
- package/dist/cli/tui/list-screen.d.ts +101 -0
- package/dist/cli/tui/list-screen.js +299 -0
- package/dist/cli/tui/terminal.d.ts +37 -0
- package/dist/cli/tui/terminal.js +89 -0
- package/dist/cli/tui/text.d.ts +20 -0
- package/dist/cli/tui/text.js +105 -0
- package/dist/cli/tui/theme.d.ts +45 -0
- package/dist/cli/tui/theme.js +30 -0
- package/dist/core/config.js +9 -6
- package/dist/core/local-config.d.ts +15 -0
- package/dist/core/local-config.js +63 -4
- package/dist/core/schema/apps.d.ts +40 -0
- package/dist/core/schema/apps.js +27 -0
- package/dist/core/schema/config.d.ts +66 -1
- package/dist/core/schema/config.js +18 -6
- package/dist/core/schema/profile.d.ts +5 -0
- package/dist/core/schema/profile.js +4 -2
- package/dist/core/schema/sandbox.d.ts +28 -0
- package/dist/core/schema/sandbox.js +28 -0
- package/dist/core/schema/toolchains.d.ts +3 -0
- package/dist/core/schema/toolchains.js +6 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/util.d.ts +2 -0
- package/dist/core/util.js +2 -0
- package/dist/core/workspace.js +22 -5
- package/dist/guest/apps/index.d.ts +10 -0
- package/dist/guest/apps/index.js +206 -0
- package/dist/guest/apps/resolvers.d.ts +8 -0
- package/dist/guest/apps/resolvers.js +90 -0
- package/dist/guest/audit-parse.d.ts +7 -0
- package/dist/guest/audit-parse.js +24 -0
- package/dist/guest/audit.d.ts +4 -0
- package/dist/guest/audit.js +47 -0
- package/dist/guest/daemon/relays.d.ts +1 -0
- package/dist/guest/daemon/relays.js +18 -0
- package/dist/guest/daemon.js +4 -0
- package/dist/guest/env.d.ts +1 -0
- package/dist/guest/env.js +3 -1
- package/dist/guest/helpers.d.ts +1 -1
- package/dist/guest/main.js +15 -2
- package/dist/guest/netproxy.d.ts +1 -0
- package/dist/guest/netproxy.js +100 -0
- package/dist/guest/ptyhost.js +14 -4
- package/dist/guest/setup/claude.d.ts +0 -4
- package/dist/guest/setup/claude.js +48 -2
- package/dist/guest/setup/network.d.ts +9 -0
- package/dist/guest/setup/network.js +97 -4
- package/dist/guest/setup/user.js +4 -0
- package/dist/guest/toolchains/native/mesa.js +13 -3
- package/dist/guest/toolchains/plan.d.ts +3 -1
- package/dist/guest/toolchains/plan.js +53 -9
- package/dist/guest/toolchains/python.js +26 -2
- package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
- package/dist/guest/toolchains/vscode-profile.js +69 -0
- package/dist/guest/toolchains/vscode.d.ts +17 -0
- package/dist/guest/toolchains/vscode.js +87 -1
- package/dist/host/attach.js +2 -1
- package/dist/host/audit/collect.d.ts +34 -0
- package/dist/host/audit/collect.js +126 -0
- package/dist/host/audit/log.d.ts +46 -0
- package/dist/host/audit/log.js +143 -0
- package/dist/host/generate.d.ts +6 -0
- package/dist/host/generate.js +10 -0
- package/dist/host/launch/mappings.js +5 -1
- package/dist/host/launch/stage.js +1 -1
- package/dist/host/motw.d.ts +17 -0
- package/dist/host/motw.js +74 -0
- package/dist/host/platform.js +3 -0
- package/dist/host/port-forward.d.ts +8 -0
- package/dist/host/port-forward.js +37 -0
- package/dist/host/repo-guard.d.ts +10 -3
- package/dist/host/repo-guard.js +142 -14
- package/dist/host/stops.d.ts +67 -0
- package/dist/host/stops.js +126 -0
- package/dist/host/terminal.d.ts +8 -0
- package/dist/host/terminal.js +45 -1
- package/dist/host/workspace-view/index.d.ts +20 -0
- package/dist/host/workspace-view/index.js +53 -0
- package/dist/host/workspace-view/rules.d.ts +26 -0
- package/dist/host/workspace-view/rules.js +93 -0
- package/dist/host/workspace-view/view.d.ts +50 -0
- package/dist/host/workspace-view/view.js +281 -0
- package/dist/runtime/daemon.js +1319 -552
- package/dist/runtime/db.js +168 -13
- package/dist/runtime/main.js +2898 -608
- package/dist/runtime/netproxy.js +25936 -0
- package/dist/runtime/view.js +272 -23
- package/dist/selftest/audit.test.d.ts +2 -0
- package/dist/selftest/audit.test.js +57 -0
- package/dist/selftest/channel.test.js +7 -0
- package/dist/selftest/cli.test.js +47 -17
- package/dist/selftest/fixtures.d.ts +45 -0
- package/dist/selftest/fixtures.js +3 -1
- package/dist/selftest/generate.test.js +7 -2
- package/dist/selftest/index.js +5 -2
- package/dist/selftest/live-network.test.d.ts +2 -0
- package/dist/selftest/live-network.test.js +63 -0
- package/dist/selftest/local-config.test.js +16 -3
- package/dist/selftest/network.test.js +12 -0
- package/dist/selftest/projects.test.js +1 -1
- package/dist/selftest/repo-guard.test.js +59 -1
- package/dist/selftest/sync.test.js +50 -10
- package/dist/selftest/toolchains.test.js +30 -0
- package/dist/selftest/workspace-view.test.d.ts +2 -0
- package/dist/selftest/workspace-view.test.js +89 -0
- package/dist/shared/apps.d.ts +49 -0
- package/dist/shared/apps.js +114 -0
- package/dist/shared/netpolicy.d.ts +27 -0
- package/dist/shared/netpolicy.js +51 -0
- package/dist/shared/pty-client.d.ts +9 -4
- package/dist/shared/pty-client.js +139 -17
- package/dist/shared/rundir.d.ts +2 -0
- package/dist/shared/rundir.js +2 -0
- package/dist/shared/schema.d.ts +10 -0
- package/dist/shared/schema.js +2 -0
- package/dist/shared/system.d.ts +1 -0
- package/dist/shared/system.js +1 -0
- package/dist/sync/claude-mem.js +3 -2
- package/dist/sync/history.d.ts +6 -0
- package/dist/sync/history.js +26 -11
- package/dist/sync/watcher.js +78 -6
- package/package.json +2 -1
- package/sandbox/cargoshim.cs +56 -0
- package/sandbox/cpucap.cs +33 -0
- package/sandbox/cpucap.ps1 +4 -35
- package/sandbox/logon.cmd +6 -0
- package/sandbox/memlog.ps1 +34 -0
- package/sandbox/pagefile.cs +31 -0
- package/sandbox/pagefile.ps1 +21 -0
- package/sandbox/ptyhost.cs +105 -7
- package/schemas/config.schema.json +303 -2
- package/schemas/sandbox-config.schema.json +126 -12
- package/dist/cli/menu-multi.js +0 -82
- package/dist/cli/menu.d.ts +0 -14
- package/dist/cli/menu.js +0 -290
- package/dist/cli/settings/edit.d.ts +0 -13
- package/dist/cli/settings/edit.js +0 -19
- package/dist/cli/settings/index.d.ts +0 -8
- package/dist/cli/settings/index.js +0 -131
- package/dist/cli/settings/prompts.d.ts +0 -7
- package/dist/cli/settings/prompts.js +0 -48
- package/dist/cli/settings/schema.d.ts +0 -33
- package/dist/cli/settings/schema.js +0 -29
package/dist/runtime/view.js
CHANGED
|
@@ -5533,7 +5533,7 @@ var require_lodash = __commonJS({
|
|
|
5533
5533
|
|
|
5534
5534
|
// src/shared/pty-client.ts
|
|
5535
5535
|
import net from "node:net";
|
|
5536
|
-
var FrameType = { Hello: 1, Input: 2, Resize: 3, Restart: 4, Ping: 5, Output: 16, Exit: 17, Error: 18, Restarting: 19, Status: 20 };
|
|
5536
|
+
var FrameType = { Hello: 1, Input: 2, Resize: 3, Restart: 4, Ping: 5, Output: 16, Exit: 17, Error: 18, Restarting: 19, Status: 20, Console: 21 };
|
|
5537
5537
|
var PING_INTERVAL_MS = 5e3;
|
|
5538
5538
|
var encodeFrame = (type, payload = Buffer.alloc(0)) => {
|
|
5539
5539
|
const header = Buffer.alloc(5);
|
|
@@ -5570,18 +5570,111 @@ var FrameReader = class {
|
|
|
5570
5570
|
return frames;
|
|
5571
5571
|
}
|
|
5572
5572
|
};
|
|
5573
|
+
var KEPT_OSC = (body) => {
|
|
5574
|
+
const number4 = /^(\d{1,4})(;|$)/.exec(body);
|
|
5575
|
+
if (!number4) return false;
|
|
5576
|
+
const n = Number(number4[1]);
|
|
5577
|
+
return n <= 2 || n === 8 || n === 133 || n === 9 && body.startsWith(number4[1] + ";4;");
|
|
5578
|
+
};
|
|
5579
|
+
var MAX_KEPT_STRING = 4096;
|
|
5573
5580
|
var OutputFilter = class {
|
|
5574
|
-
|
|
5581
|
+
/** text: normal output; esc: after ESC; c2: after a 0xC2 lead byte; string: inside an OSC/DCS/...; stringEsc / stringC2: a possible terminator inside one. */
|
|
5582
|
+
state = "text";
|
|
5583
|
+
/** The string being read: an OSC worth keeping is collected until its terminator; null = being dropped. */
|
|
5584
|
+
body = null;
|
|
5575
5585
|
push(chunk) {
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
const
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5586
|
+
const out = Buffer.allocUnsafe(chunk.length + MAX_KEPT_STRING + 8);
|
|
5587
|
+
let n = 0;
|
|
5588
|
+
const put = (...bytes) => {
|
|
5589
|
+
for (const b of bytes) out[n++] = b;
|
|
5590
|
+
};
|
|
5591
|
+
const opens = (b, c1) => {
|
|
5592
|
+
if (b === (c1 ? 157 : 93)) {
|
|
5593
|
+
this.body = c1 ? null : [];
|
|
5594
|
+
this.state = "string";
|
|
5595
|
+
return true;
|
|
5596
|
+
}
|
|
5597
|
+
if ((c1 ? [144, 152, 158, 159] : [80, 88, 94, 95]).includes(b)) {
|
|
5598
|
+
this.body = null;
|
|
5599
|
+
this.state = "string";
|
|
5600
|
+
return true;
|
|
5601
|
+
}
|
|
5602
|
+
return false;
|
|
5603
|
+
};
|
|
5604
|
+
const endString = (terminator) => {
|
|
5605
|
+
const body = this.body && Buffer.from(this.body).toString("latin1");
|
|
5606
|
+
if (body !== null && KEPT_OSC(body)) {
|
|
5607
|
+
put(27, 93);
|
|
5608
|
+
out.write(body, n, "latin1");
|
|
5609
|
+
n += body.length;
|
|
5610
|
+
put(...terminator);
|
|
5611
|
+
}
|
|
5612
|
+
this.body = null;
|
|
5613
|
+
this.state = "text";
|
|
5614
|
+
};
|
|
5615
|
+
const collect = (b) => {
|
|
5616
|
+
if (this.body && this.body.push(b) > MAX_KEPT_STRING) this.body = null;
|
|
5617
|
+
};
|
|
5618
|
+
for (const b of chunk) {
|
|
5619
|
+
switch (this.state) {
|
|
5620
|
+
case "text":
|
|
5621
|
+
if (b === 27) this.state = "esc";
|
|
5622
|
+
else if (b === 194) this.state = "c2";
|
|
5623
|
+
else put(b);
|
|
5624
|
+
break;
|
|
5625
|
+
case "esc":
|
|
5626
|
+
if (opens(b, false)) break;
|
|
5627
|
+
if (b !== 27) {
|
|
5628
|
+
put(27, b);
|
|
5629
|
+
this.state = "text";
|
|
5630
|
+
} else put(27);
|
|
5631
|
+
break;
|
|
5632
|
+
case "c2":
|
|
5633
|
+
if (opens(b, true)) break;
|
|
5634
|
+
if (b === 27) {
|
|
5635
|
+
put(194);
|
|
5636
|
+
this.state = "esc";
|
|
5637
|
+
} else if (b === 194) put(194);
|
|
5638
|
+
else {
|
|
5639
|
+
put(194, b);
|
|
5640
|
+
this.state = "text";
|
|
5641
|
+
}
|
|
5642
|
+
break;
|
|
5643
|
+
case "string":
|
|
5644
|
+
if (b === 7) endString([7]);
|
|
5645
|
+
else if (b === 27) this.state = "stringEsc";
|
|
5646
|
+
else if (b === 194) this.state = "stringC2";
|
|
5647
|
+
else if (b === 24 || b === 26) {
|
|
5648
|
+
this.body = null;
|
|
5649
|
+
this.state = "text";
|
|
5650
|
+
} else collect(b);
|
|
5651
|
+
break;
|
|
5652
|
+
case "stringC2":
|
|
5653
|
+
if (b === 156) endString([27, 92]);
|
|
5654
|
+
else {
|
|
5655
|
+
collect(194);
|
|
5656
|
+
this.state = "string";
|
|
5657
|
+
if (b === 7) endString([7]);
|
|
5658
|
+
else if (b === 27) this.state = "stringEsc";
|
|
5659
|
+
else if (b !== 194) collect(b);
|
|
5660
|
+
else this.state = "stringC2";
|
|
5661
|
+
}
|
|
5662
|
+
break;
|
|
5663
|
+
case "stringEsc":
|
|
5664
|
+
if (b === 92) {
|
|
5665
|
+
endString([27, 92]);
|
|
5666
|
+
break;
|
|
5667
|
+
}
|
|
5668
|
+
this.body = null;
|
|
5669
|
+
this.state = "esc";
|
|
5670
|
+
if (!opens(b, false) && b !== 27) {
|
|
5671
|
+
put(27, b);
|
|
5672
|
+
this.state = "text";
|
|
5673
|
+
}
|
|
5674
|
+
break;
|
|
5675
|
+
}
|
|
5583
5676
|
}
|
|
5584
|
-
return
|
|
5677
|
+
return out.subarray(0, n);
|
|
5585
5678
|
}
|
|
5586
5679
|
};
|
|
5587
5680
|
var connectOnce = (host, port, timeoutMs) => {
|
|
@@ -5652,11 +5745,12 @@ var runTerminalClient = (socket, token, options = {}) => {
|
|
|
5652
5745
|
}
|
|
5653
5746
|
socket.write(encodeFrame(FrameType.Input, swapKeys ? fixWindowsKeys(chunk) : chunk));
|
|
5654
5747
|
};
|
|
5748
|
+
let passthrough = false;
|
|
5655
5749
|
const sendSize = () => {
|
|
5656
5750
|
const [c2, r2] = size();
|
|
5657
5751
|
if (c2 === sent[0] && r2 === sent[1]) return;
|
|
5658
5752
|
sent = [c2, r2];
|
|
5659
|
-
stdout.write("\x1B[H\x1B[J");
|
|
5753
|
+
if (!passthrough) stdout.write("\x1B[H\x1B[J");
|
|
5660
5754
|
socket.write(resizeFrame(c2, r2));
|
|
5661
5755
|
};
|
|
5662
5756
|
const onResize = () => {
|
|
@@ -5680,6 +5774,7 @@ var runTerminalClient = (socket, token, options = {}) => {
|
|
|
5680
5774
|
}
|
|
5681
5775
|
for (const frame of frames) {
|
|
5682
5776
|
if (frame.type === FrameType.Output) stdout.write(filter.push(frame.payload));
|
|
5777
|
+
else if (frame.type === FrameType.Console) passthrough = frame.payload.toString("latin1") === "conpty.dll";
|
|
5683
5778
|
else if (frame.type === FrameType.Restarting) stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
5684
5779
|
else if (frame.type === FrameType.Exit) finish({ reason: "exit", exitCode: frame.payload.length >= 4 ? frame.payload.readInt32LE(0) : 1 });
|
|
5685
5780
|
else if (frame.type === FrameType.Error) finish({ reason: "error", message: frame.payload.toString("utf8").slice(0, 500).replace(/[\x00-\x1f\x7f]/g, " ") });
|
|
@@ -25473,8 +25568,10 @@ var ClaudeProfile = external_exports.strictObject({
|
|
|
25473
25568
|
globalStateFile: external_exports.string().describe('Your .claude.json, or "auto". Empty string = do not copy.'),
|
|
25474
25569
|
copyCredentials: external_exports.boolean().describe("Copy your Claude LOGIN into the sandbox. Prefer sandbox account instead."),
|
|
25475
25570
|
overwrite: external_exports.boolean().describe("Refresh these settings from this PC on every launch."),
|
|
25476
|
-
syncHistory: external_exports.boolean().describe("Bring
|
|
25477
|
-
|
|
25571
|
+
syncHistory: external_exports.boolean().describe("Bring conversation history in, and sync new history back (path-translated)."),
|
|
25572
|
+
historyScope: external_exports.enum(["project", "all"]).describe(`Which history comes in: "project" = only the projects in this sandbox (other projects' conversations, file backups and prompts stay on this PC), "all" = everything. Only this sandbox's projects ever sync back.`),
|
|
25573
|
+
syncClaudeMem: external_exports.boolean().describe("Bring claude-mem data into the sandbox (its settings and backups stay on this PC)."),
|
|
25574
|
+
syncClaudeMemBack: external_exports.boolean().describe("Also copy claude-mem's database back to this PC when the sandbox closes. Off by default: what is in it is loaded into every Claude session on this PC, so a compromised agent could plant instructions there.")
|
|
25478
25575
|
});
|
|
25479
25576
|
|
|
25480
25577
|
// src/core/schema/sandbox.ts
|
|
@@ -25490,6 +25587,7 @@ var Sandbox = external_exports.strictObject({
|
|
|
25490
25587
|
maxMemoryMb: external_exports.number().int().positive().describe('Upper limit for memoryMb "auto".'),
|
|
25491
25588
|
maxMemoryPercent: external_exports.number().int().min(10).max(100).describe(`memoryMb "auto" never takes more than this share of this PC's total RAM (percent).`),
|
|
25492
25589
|
cpuLimit: external_exports.union([external_exports.literal("none"), external_exports.number().int().min(5).max(100)]).describe(`Most of this PC's CPU the sandbox may use, in percent (5-100), e.g. 50 keeps half free for you; "none" = no cap (Windows Sandbox would take every processor). Covers everything this tool starts in the sandbox (agents, installs, builds, its terminal).`),
|
|
25590
|
+
pageFileMb: external_exports.number().int().min(0).max(262144).describe(`Size of the sandbox's paging file in MB, created at full size when it starts so it never grows while builds run (8 GB of its disk always stays free). Windows Sandbox starts with about 1 GB: parallel builds then fail with "The paging file is too small", and Windows grows the file under load. 0 = leave Windows' own.`),
|
|
25493
25591
|
reserveHostMb: external_exports.number().int().nonnegative().describe('RAM always left for this PC when memoryMb is "auto".'),
|
|
25494
25592
|
vGpu: external_exports.boolean().describe("Virtual GPU. Off by default: Microsoft notes it widens the sandbox attack surface."),
|
|
25495
25593
|
networking: external_exports.boolean().describe("Internet access. false is the only hard network isolation Windows Sandbox offers."),
|
|
@@ -25498,7 +25596,33 @@ var Sandbox = external_exports.strictObject({
|
|
|
25498
25596
|
videoInput: external_exports.boolean().describe("Camera access."),
|
|
25499
25597
|
printerRedirection: external_exports.boolean().describe("Printers from this PC."),
|
|
25500
25598
|
protectedClient: external_exports.boolean().describe("Run the sandbox window in Protected Client mode (recommended)."),
|
|
25501
|
-
headless: external_exports.boolean().describe("Hide the sandbox window (sbx-show brings it back). A headless sandbox shuts down when its last session ends.")
|
|
25599
|
+
headless: external_exports.boolean().describe("Hide the sandbox window (sbx-show brings it back). A headless sandbox shuts down when its last session ends."),
|
|
25600
|
+
keepAwake: external_exports.boolean().describe("Keep this PC from sleeping while a sandbox runs, so agents left working overnight keep going (the screen may still turn off; closing a laptop lid still sleeps it).")
|
|
25601
|
+
});
|
|
25602
|
+
var Port2 = external_exports.number().int().min(1).max(65535);
|
|
25603
|
+
var HostEntry = external_exports.string().max(253).regex(/^(@developer|(\*\.)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+)$/, "a host name like example.com, *.example.com, or @developer");
|
|
25604
|
+
var Network = external_exports.strictObject({
|
|
25605
|
+
blockPrivateRanges: external_exports.boolean().describe("Block your LAN (10/8, 172.16/12, 192.168/16, SMB) from inside the sandbox."),
|
|
25606
|
+
allowOnly: external_exports.array(HostEntry).describe("Only these hosts can be reached once the sandbox is set up (tools still download first). *.example.com covers its subdomains; @developer adds package registries, GitHub, toolchain downloads and the AI providers. Programs go through a proxy in the sandbox (HTTP_PROXY); anything else is blocked by its firewall. Empty = no limit."),
|
|
25607
|
+
blockHosts: external_exports.array(external_exports.string().max(253).regex(/^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/, "a host name like telemetry.example.com")).describe("Never reachable from the sandbox, e.g. production servers or telemetry (each exact name; the sandbox's hosts file sends them nowhere)."),
|
|
25608
|
+
forwardPorts: external_exports.array(external_exports.strictObject({
|
|
25609
|
+
pc: Port2.describe("The port on this PC (localhost only)."),
|
|
25610
|
+
sandbox: Port2.describe("The port inside the sandbox (a server there may listen on localhost only).")
|
|
25611
|
+
})).describe("Reach servers in the sandbox from this PC: http://localhost:<pc> here goes to <sandbox> inside, while it runs. Nothing else on your network can use them."),
|
|
25612
|
+
dns: external_exports.array(external_exports.string().regex(/^\d{1,3}(\.\d{1,3}){3}$/, "an IPv4 address like 1.1.1.1")).describe("DNS servers for the sandbox instead of your network's, e.g. 1.1.1.1 and 9.9.9.9."),
|
|
25613
|
+
proxy: external_exports.string().regex(/^(https?:\/\/[^\s]+)?$/, "http://host:port").describe("An HTTP proxy the sandbox uses to reach the internet (HTTP_PROXY / HTTPS_PROXY for everything in it), e.g. http://proxy.corp:8080. Empty = none.")
|
|
25614
|
+
});
|
|
25615
|
+
var Audit = external_exports.strictObject({
|
|
25616
|
+
enabled: external_exports.boolean().describe("Keep an audit log on this PC of what happened in your sandboxes: programs started (with their command lines), files changed in the project, sessions, and what the repo guard caught. The sandbox cannot change it. Read it with sandbox audit."),
|
|
25617
|
+
days: external_exports.number().int().min(1).max(365).describe("Days the audit log is kept."),
|
|
25618
|
+
maxMb: external_exports.number().int().min(5).max(5e3).describe("Most space the audit log may take, in MB (older days go first). Days before today are compressed."),
|
|
25619
|
+
processes: external_exports.boolean().describe("Record every program started in the sandbox with its command line (Windows process auditing; repeated runs are counted, not listed)."),
|
|
25620
|
+
files: external_exports.boolean().describe("Record the files changed in the project while the sandbox runs.")
|
|
25621
|
+
});
|
|
25622
|
+
var WorkspaceFilter = external_exports.strictObject({
|
|
25623
|
+
enabled: external_exports.boolean().describe("Keep what your .gitignore lists (.env files, node_modules, build output ...) out of the sandbox. The sandbox works on a copy of the project without those files, kept in step with it both ways within about a second; what it creates that is ignored (its own node_modules, builds) stays in the sandbox. Git works as usual. Off = the project folder itself is mapped."),
|
|
25624
|
+
exclude: external_exports.array(external_exports.string().max(500)).describe("Also keep these out, as .gitignore patterns, e.g. secrets/, *.pem, data/private/."),
|
|
25625
|
+
include: external_exports.array(external_exports.string().max(500)).describe("Let these in although .gitignore lists them, e.g. .env.example, data/fixtures/.")
|
|
25502
25626
|
});
|
|
25503
25627
|
var Git = external_exports.strictObject({ enabled: external_exports.boolean().describe("Provide Git for Windows (and Git Bash, which Claude Code needs)."), access: GitAccess });
|
|
25504
25628
|
var Provisioning = external_exports.strictObject({
|
|
@@ -25506,7 +25630,8 @@ var Provisioning = external_exports.strictObject({
|
|
|
25506
25630
|
fallbackToInstall: external_exports.boolean().describe("Download anything that cannot be copied from this PC."),
|
|
25507
25631
|
refresh: external_exports.boolean().describe("Re-copy / re-download everything on the next launch."),
|
|
25508
25632
|
git: Git,
|
|
25509
|
-
terminal: toggle("Windows Terminal as the sandbox window (downloaded once).")
|
|
25633
|
+
terminal: toggle("Windows Terminal as the sandbox window (downloaded once)."),
|
|
25634
|
+
conpty: toggle("Agent sessions use Microsoft's newer pseudo console (conpty.dll, downloaded once) instead of the one built into Windows: no garbled screen after resizing, real scrollback for the mouse wheel. Off = the one built into Windows.")
|
|
25510
25635
|
});
|
|
25511
25636
|
var Downloads = external_exports.strictObject({
|
|
25512
25637
|
requireChecksum: external_exports.boolean().describe("Refuse any toolchain, database or tool download that has no published checksum (SHA-256/SHA-512) to verify it against. false = allow those, as before. Downloads that do have a published checksum are always verified. A project's .sandbox-config.json can only turn this on.")
|
|
@@ -25590,10 +25715,119 @@ var Toolchains = external_exports.strictObject({
|
|
|
25590
25715
|
enabled: external_exports.boolean().describe("Mesa 3D (llvmpipe): OpenGL 4.5 in software for games and apps when there is no GPU (sandbox.vGpu off). Copy opengl32.dll and libgallium_wgl.dll from %MESA_DIR% next to the program's .exe."),
|
|
25591
25716
|
vulkan: external_exports.boolean().describe("Also software Vulkan 1.4 (Mesa lavapipe) with the Khronos loader, registered for every program; for wgpu set WGPU_BACKEND=vulkan.")
|
|
25592
25717
|
}),
|
|
25593
|
-
vscode:
|
|
25718
|
+
vscode: external_exports.strictObject({
|
|
25719
|
+
enabled: external_exports.boolean().describe("Portable VS Code inside the sandbox (code on PATH, desktop shortcut); its settings and extensions persist."),
|
|
25720
|
+
setup: external_exports.boolean().describe("Point VS Code at the sandbox's own tools (JDK, Python, Go, compilers, .NET, Git, PowerShell 7 and Git Bash terminals) and install the language extensions for the toolchains in use."),
|
|
25721
|
+
recommended: external_exports.boolean().describe("Also install the extensions a project recommends in its .vscode/extensions.json."),
|
|
25722
|
+
extensions: external_exports.array(external_exports.string().regex(/^[\w-]+\.[\w-]+$/, "publisher.name, e.g. eamodio.gitlens")).describe("More extensions to install in every sandbox, e.g. eamodio.gitlens, ms-azuretools.vscode-docker.")
|
|
25723
|
+
}),
|
|
25594
25724
|
environment: external_exports.record(external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "letters, digits and _ only"), external_exports.string()).describe('Extra environment variables for the agent and every terminal, e.g. registry mirrors: {"NPM_CONFIG_REGISTRY": "https://...", "PIP_INDEX_URL": "https://..."}.')
|
|
25595
25725
|
});
|
|
25596
25726
|
|
|
25727
|
+
// src/shared/apps.ts
|
|
25728
|
+
var gh = (github, asset) => ({ github, asset });
|
|
25729
|
+
var APPS = [
|
|
25730
|
+
// ---- editors
|
|
25731
|
+
{ id: "vscode", name: "Visual Studio Code", category: "editors", about: "Code editor with debugging, set up for the sandbox's languages.", size: "150 MB", source: { resolve: "setting" }, exe: "Code.exe", setting: "toolchains.vscode.enabled" },
|
|
25732
|
+
{ id: "windows-terminal", name: "Windows Terminal", category: "utilities", about: "The sandbox window: tabs, panes, PowerShell 7 profile.", size: "40 MB", source: { resolve: "setting" }, exe: "WindowsTerminal.exe", setting: "provisioning.terminal.enabled" },
|
|
25733
|
+
{ id: "notepadpp", name: "Notepad++", category: "editors", about: "Fast text and code editor.", size: "8 MB", source: gh("notepad-plus-plus/notepad-plus-plus", "^npp\\.[\\d.]+\\.portable\\.x64\\.zip$"), exe: "notepad++.exe" },
|
|
25734
|
+
{ id: "sublime", name: "Sublime Text", category: "editors", about: "Fast code editor (paid licence, free to evaluate).", size: "28 MB", source: { resolve: "sublime" }, exe: "sublime_text.exe" },
|
|
25735
|
+
{ id: "neovim", name: "Neovim", category: "editors", about: "Terminal text editor (nvim).", size: "12 MB", source: gh("neovim/neovim", "^nvim-win64\\.zip$"), exe: "nvim.exe", cli: true },
|
|
25736
|
+
// ---- database clients
|
|
25737
|
+
{ id: "dbeaver", name: "DBeaver Community", category: "databases", about: "Universal database client: PostgreSQL, MySQL, SQLite, SQL Server and more.", size: "120 MB", source: gh("dbeaver/dbeaver", "^dbeaver-ce-[\\d.]+-windows-x86_64\\.zip$"), exe: "dbeaver.exe" },
|
|
25738
|
+
{ id: "pgadmin", name: "pgAdmin 4", category: "databases", about: "The PostgreSQL admin GUI (from the PostgreSQL binaries zip).", size: "330 MB", source: { resolve: "pgadmin" }, exe: "pgAdmin4.exe" },
|
|
25739
|
+
{ id: "heidisql", name: "HeidiSQL", category: "databases", about: "Light client for MySQL, MariaDB, PostgreSQL and SQL Server.", size: "28 MB", source: gh("HeidiSQL/HeidiSQL", "^HeidiSQL_[\\d.]+_64_Portable\\.zip$"), exe: "heidisql.exe" },
|
|
25740
|
+
{ id: "sqlitebrowser", name: "DB Browser for SQLite", category: "databases", about: "Open, browse and edit SQLite files.", size: "21 MB", source: gh("sqlitebrowser/sqlitebrowser", "^DB\\.Browser\\.for\\.SQLite-v?[\\d.]+-win64\\.zip$"), exe: "DB Browser for SQLite.exe" },
|
|
25741
|
+
{ id: "compass", name: "MongoDB Compass", category: "databases", about: "The MongoDB GUI.", size: "220 MB", source: gh("mongodb-js/compass", "^mongodb-compass-[\\d.]+-win32-x64\\.zip$"), exe: "MongoDBCompass.exe" },
|
|
25742
|
+
{ id: "redisinsight", name: "Redis Insight", category: "databases", about: "The Redis GUI.", size: "110 MB", source: gh("redis/RedisInsight", "^Redis-Insight-win-installer\\.exe$"), unpack: "nsis", exe: "Redis Insight.exe", needs: ["7zip"] },
|
|
25743
|
+
{ id: "dbgate", name: "DbGate", category: "databases", about: "Database manager for SQL and NoSQL databases.", size: "180 MB", source: gh("dbgate/dbgate", "^dbgate-[\\d.]+-win_x64\\.zip$"), exe: "DbGate.exe" },
|
|
25744
|
+
{ id: "beekeeper", name: "Beekeeper Studio", category: "databases", about: "SQL editor and database manager.", size: "245 MB", source: gh("beekeeper-studio/beekeeper-studio", "^Beekeeper-Studio-[\\d.]+-portable\\.exe$"), unpack: "nsis", exe: "Beekeeper Studio.exe", needs: ["7zip"] },
|
|
25745
|
+
{ id: "dynamodb-admin", name: "dynamodb-admin", category: "databases", about: "Web GUI for DynamoDB Local: run dynamodb-admin, then open http://localhost:8001.", size: "5 MB", source: { npm: "dynamodb-admin" }, exe: "dynamodb-admin.cmd", cli: true },
|
|
25746
|
+
// ---- API clients
|
|
25747
|
+
{ id: "bruno", name: "Bruno", category: "api", about: "Offline, git-friendly API client (collections are files in your repo).", size: "175 MB", source: gh("usebruno/bruno", "^bruno_[\\d.]+_x64_win\\.zip$"), exe: "Bruno.exe" },
|
|
25748
|
+
{ id: "insomnia", name: "Insomnia", category: "api", about: "REST, GraphQL and gRPC client.", size: "590 MB", source: gh("Kong/insomnia", "^insomnia-[\\d.]+-full\\.nupkg$"), exe: "Insomnia.exe" },
|
|
25749
|
+
// ---- git
|
|
25750
|
+
{ id: "sourcegit", name: "SourceGit", category: "git", about: "Fast open-source Git GUI.", size: "26 MB", source: gh("sourcegit-scm/sourcegit", "^sourcegit_[\\d.]+\\.win-x64\\.zip$"), exe: "SourceGit.exe" },
|
|
25751
|
+
{ id: "githubdesktop", name: "GitHub Desktop", category: "git", about: 'Simple Git GUI from GitHub. It has its own git, so it needs provisioning.git.access "full".', size: "300 MB", source: gh("desktop/desktop", "^GitHubDesktop-[\\d.]+-x64-full\\.nupkg$"), exe: "GitHubDesktop.exe", fullGitOnly: true },
|
|
25752
|
+
{ id: "lazygit", name: "lazygit", category: "git", about: "Git in the terminal.", size: "7 MB", source: gh("jesseduffield/lazygit", "^lazygit_[\\d.]+_windows_x86_64\\.zip$"), exe: "lazygit.exe", cli: true },
|
|
25753
|
+
// ---- utilities
|
|
25754
|
+
{ id: "7zip", name: "7-Zip", category: "utilities", about: "Archive manager (7z, zip, rar, tar ...). Also unpacks installers for other apps.", size: "2 MB", source: { resolve: "7zip" }, exe: "7zFM.exe" },
|
|
25755
|
+
{ id: "winmerge", name: "WinMerge", category: "utilities", about: "Compare and merge files and folders.", size: "23 MB", source: gh("WinMerge/winmerge", "^winmerge-[\\d.]+-x64-exe\\.zip$"), exe: "WinMergeU.exe" },
|
|
25756
|
+
{ id: "everything", name: "Everything", category: "utilities", about: "Finds files by name instantly.", size: "2 MB", source: { resolve: "everything" }, exe: "Everything.exe" },
|
|
25757
|
+
{ id: "sysinternals", name: "Sysinternals Suite", category: "utilities", about: "Process Explorer, Process Monitor, Autoruns, TCPView and more (on PATH).", size: "200 MB", source: { url: "https://download.sysinternals.com/files/SysinternalsSuite.zip" }, exe: "procexp64.exe" },
|
|
25758
|
+
{ id: "imhex", name: "ImHex", category: "utilities", about: "Hex editor for reverse engineering and binary formats.", size: "80 MB", source: gh("WerWolv/ImHex", "^imhex-[\\d.]+-Windows-Portable-NoGPU-x86_64\\.zip$"), exe: "imhex-gui.exe" },
|
|
25759
|
+
// ---- browsers
|
|
25760
|
+
{ id: "chrome", name: "Chrome for Testing", category: "browsers", about: "Chrome build made for automated tests (with chromedriver on PATH).", size: "170 MB", source: { resolve: "chrome" }, exe: "chrome.exe" },
|
|
25761
|
+
{ id: "firefox", name: "Firefox", category: "browsers", about: "Firefox for testing.", size: "75 MB", source: { resolve: "firefox" }, unpack: "nsis", exe: "firefox.exe", needs: ["7zip"] },
|
|
25762
|
+
// ---- game development
|
|
25763
|
+
{ id: "godot", name: "Godot", category: "gamedev", about: "Open-source 2D and 3D game engine.", size: "80 MB", source: gh("godotengine/godot", "^Godot_v[\\d.]+-stable_win64\\.exe\\.zip$"), exe: "/^Godot_v[\\d.]+-stable_win64\\.exe$/", needs: ["mesa"] },
|
|
25764
|
+
{ id: "blender", name: "Blender", category: "gamedev", about: "3D modelling, animation and rendering.", size: "400 MB", source: { resolve: "blender" }, exe: "blender.exe", needs: ["mesa"] },
|
|
25765
|
+
{ id: "love", name: "L\xD6VE", category: "gamedev", about: "Lua framework for 2D games.", size: "4 MB", source: gh("love2d/love", "^love-[\\d.]+-win64\\.zip$"), exe: "love.exe", needs: ["mesa"] },
|
|
25766
|
+
{ id: "defold", name: "Defold", category: "gamedev", about: "Free 2D/3D game engine and editor.", size: "330 MB", source: gh("defold/defold", "^Defold-x86_64-win32\\.zip$"), exe: "Defold.exe", needs: ["mesa"] },
|
|
25767
|
+
{ id: "pixelorama", name: "Pixelorama", category: "gamedev", about: "Pixel-art and sprite editor.", size: "40 MB", source: gh("Orama-Interactive/Pixelorama", "^Pixelorama-Windows-64bit\\.zip$"), exe: "Pixelorama.exe", needs: ["mesa"] },
|
|
25768
|
+
{ id: "tiled", name: "Tiled", category: "gamedev", about: "Tile map editor.", size: "22 MB", source: gh("mapeditor/tiled", "^Tiled-[\\d.]+_Windows-10\\+_x86_64\\.msi$"), unpack: "msi", exe: "tiled.exe" },
|
|
25769
|
+
// ---- cloud
|
|
25770
|
+
{ id: "kubectl", name: "kubectl", category: "cloud", about: "Kubernetes command line.", size: "60 MB", source: { resolve: "kubectl" }, unpack: "file", exe: "kubectl.exe", cli: true },
|
|
25771
|
+
{ id: "helm", name: "Helm", category: "cloud", about: "Kubernetes package manager.", size: "20 MB", source: { resolve: "helm" }, exe: "helm.exe", cli: true },
|
|
25772
|
+
{ id: "k9s", name: "k9s", category: "cloud", about: "Kubernetes cluster UI in the terminal.", size: "40 MB", source: gh("derailed/k9s", "^k9s_Windows_amd64\\.zip$"), exe: "k9s.exe", cli: true },
|
|
25773
|
+
{ id: "terraform", name: "Terraform", category: "cloud", about: "Infrastructure as code.", size: "30 MB", source: { resolve: "terraform" }, exe: "terraform.exe", cli: true },
|
|
25774
|
+
{ id: "azure-cli", name: "Azure CLI", category: "cloud", about: "Manage Azure (az).", size: "90 MB", source: { url: "https://aka.ms/installazurecliwindowszipx64" }, exe: "az.cmd", cli: true },
|
|
25775
|
+
{ id: "gcloud", name: "Google Cloud CLI", category: "cloud", about: "Manage Google Cloud (gcloud, gsutil, bq).", size: "120 MB", source: { url: "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-windows-x86_64-bundled-python.zip" }, exe: "gcloud.cmd", cli: true },
|
|
25776
|
+
{ id: "aws-cli", name: "AWS CLI", category: "cloud", about: "Manage AWS (aws).", size: "50 MB", source: { url: "https://awscli.amazonaws.com/AWSCLIV2.msi" }, unpack: "msi", exe: "aws.exe", cli: true },
|
|
25777
|
+
// ---- diagrams and docs
|
|
25778
|
+
{ id: "drawio", name: "draw.io", category: "diagrams", about: "Diagram editor (flowcharts, architecture, UML).", size: "170 MB", source: gh("jgraph/drawio-desktop", "^draw\\.io-[\\d.]+-windows\\.zip$"), exe: "draw.io.exe" },
|
|
25779
|
+
{ id: "pandoc", name: "Pandoc", category: "diagrams", about: "Convert documents between Markdown, HTML, Word, PDF ...", size: "35 MB", source: gh("jgm/pandoc", "^pandoc-[\\d.]+-windows-x86_64\\.zip$"), exe: "pandoc.exe", cli: true },
|
|
25780
|
+
// ---- network
|
|
25781
|
+
{ id: "mitmproxy", name: "mitmproxy", category: "network", about: "Intercepting HTTPS proxy (mitmweb opens a web UI).", size: "90 MB", source: { resolve: "mitmproxy" }, exe: "mitmweb.exe", cli: true },
|
|
25782
|
+
{ id: "xh", name: "xh", category: "network", about: "Friendly HTTP requests from the terminal (like HTTPie).", size: "5 MB", source: gh("ducaale/xh", "^xh-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "xh.exe", cli: true },
|
|
25783
|
+
{ id: "grpcurl", name: "grpcurl", category: "network", about: "curl for gRPC servers.", size: "10 MB", source: gh("fullstorydev/grpcurl", "^grpcurl_[\\d.]+_windows_x86_64\\.zip$"), exe: "grpcurl.exe", cli: true },
|
|
25784
|
+
{ id: "k6", name: "k6", category: "network", about: "Load testing with JavaScript scripts.", size: "30 MB", source: gh("grafana/k6", "^k6-v[\\d.]+-windows-amd64\\.zip$"), exe: "k6.exe", cli: true },
|
|
25785
|
+
// ---- command-line tools
|
|
25786
|
+
{ id: "jq", name: "jq", category: "cli", about: "Query and reshape JSON.", size: "1 MB", source: gh("jqlang/jq", "^jq-windows-amd64\\.exe$"), unpack: "file", exe: "jq.exe", cli: true },
|
|
25787
|
+
{ id: "yq", name: "yq", category: "cli", about: "Query and edit YAML, JSON, XML and TOML.", size: "10 MB", source: gh("mikefarah/yq", "^yq_windows_amd64\\.exe$"), unpack: "file", exe: "yq.exe", cli: true },
|
|
25788
|
+
{ id: "ripgrep", name: "ripgrep", category: "cli", about: "Very fast recursive search (rg).", size: "2 MB", source: gh("BurntSushi/ripgrep", "^ripgrep-[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "rg.exe", cli: true },
|
|
25789
|
+
{ id: "fd", name: "fd", category: "cli", about: "Fast, friendly alternative to find.", size: "2 MB", source: gh("sharkdp/fd", "^fd-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "fd.exe", cli: true },
|
|
25790
|
+
{ id: "bat", name: "bat", category: "cli", about: "cat with syntax highlighting and git changes.", size: "3 MB", source: gh("sharkdp/bat", "^bat-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "bat.exe", cli: true },
|
|
25791
|
+
{ id: "fzf", name: "fzf", category: "cli", about: "Fuzzy finder for the terminal.", size: "2 MB", source: gh("junegunn/fzf", "^fzf-[\\d.]+-windows_amd64\\.zip$"), exe: "fzf.exe", cli: true },
|
|
25792
|
+
{ id: "delta", name: "delta", category: "cli", about: "Readable git diffs with syntax highlighting.", size: "3 MB", source: gh("dandavison/delta", "^delta-[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "delta.exe", cli: true },
|
|
25793
|
+
{ id: "just", name: "just", category: "cli", about: "Command runner (a simpler make for project tasks).", size: "2 MB", source: gh("casey/just", "^just-[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "just.exe", cli: true },
|
|
25794
|
+
{ id: "hyperfine", name: "hyperfine", category: "cli", about: "Benchmark commands.", size: "1 MB", source: gh("sharkdp/hyperfine", "^hyperfine-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "hyperfine.exe", cli: true },
|
|
25795
|
+
{ id: "watchexec", name: "watchexec", category: "cli", about: "Run a command when files change.", size: "5 MB", source: gh("watchexec/watchexec", "^watchexec-[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "watchexec.exe", cli: true },
|
|
25796
|
+
{ id: "protoc", name: "protoc", category: "cli", about: "Protocol Buffers compiler.", size: "3 MB", source: gh("protocolbuffers/protobuf", "^protoc-[\\d.]+-win64\\.zip$"), exe: "protoc.exe", cli: true },
|
|
25797
|
+
{ id: "ninja", name: "Ninja", category: "cli", about: "Small, fast build system (CMake's favourite generator).", size: "1 MB", source: gh("ninja-build/ninja", "^ninja-win\\.zip$"), exe: "ninja.exe", cli: true },
|
|
25798
|
+
{ id: "ffmpeg", name: "FFmpeg", category: "cli", about: "Convert and process audio and video.", size: "200 MB", source: gh("BtbN/FFmpeg-Builds", "^ffmpeg-master-latest-win64-gpl\\.zip$"), exe: "ffmpeg.exe", cli: true },
|
|
25799
|
+
{ id: "sqlite", name: "SQLite tools", category: "cli", about: "sqlite3 command-line shell.", size: "5 MB", source: { resolve: "sqlite" }, exe: "sqlite3.exe", cli: true },
|
|
25800
|
+
{ id: "shellcheck", name: "ShellCheck", category: "cli", about: "Finds bugs in shell scripts.", size: "10 MB", source: gh("koalaman/shellcheck", "^shellcheck-v[\\d.]+\\.zip$"), exe: "shellcheck.exe", cli: true },
|
|
25801
|
+
{ id: "gitleaks", name: "Gitleaks", category: "cli", about: "Finds secrets committed to git.", size: "8 MB", source: gh("gitleaks/gitleaks", "^gitleaks_[\\d.]+_windows_x64\\.zip$"), exe: "gitleaks.exe", cli: true }
|
|
25802
|
+
];
|
|
25803
|
+
|
|
25804
|
+
// src/core/schema/apps.ts
|
|
25805
|
+
var CATALOGUE_IDS = APPS.filter((a) => !a.setting).map((a) => a.id);
|
|
25806
|
+
var AppId = external_exports.enum(CATALOGUE_IDS);
|
|
25807
|
+
var CustomAppId = external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,40}$/, "lower-case letters, digits and - only");
|
|
25808
|
+
var compiles = (pattern) => {
|
|
25809
|
+
try {
|
|
25810
|
+
new RegExp(pattern);
|
|
25811
|
+
return true;
|
|
25812
|
+
} catch {
|
|
25813
|
+
return false;
|
|
25814
|
+
}
|
|
25815
|
+
};
|
|
25816
|
+
var CustomApp = external_exports.strictObject({
|
|
25817
|
+
name: external_exports.string().min(1).max(80).describe('The name on the desktop shortcut, e.g. "My Tool".'),
|
|
25818
|
+
github: external_exports.string().regex(/^[\w.-]+\/[\w.-]+$/, "owner/repo, e.g. sharkdp/bat").optional().describe("GitHub repository (owner/repo): the file comes from its latest release."),
|
|
25819
|
+
asset: external_exports.string().max(200).refine(compiles, "not a valid pattern").optional().describe("With github: a pattern (regular expression) matching the release file, e.g. windows.*x64\\.zip$"),
|
|
25820
|
+
url: external_exports.string().url().startsWith("https://", "https:// only").optional().describe("Or a direct https:// download link (zip, 7z, exe or msi)."),
|
|
25821
|
+
sha256: external_exports.string().regex(/^[0-9a-fA-F]{64}$/, "64 hex characters").optional().describe("SHA-256 of the download (GitHub releases usually publish one; then this is not needed)."),
|
|
25822
|
+
unpack: external_exports.enum(["archive", "file", "nsis", "msi"]).optional().describe("archive (zip, 7z, tar: the default), file (a single .exe), nsis (an installer 7-Zip can open) or msi."),
|
|
25823
|
+
exe: external_exports.string().min(1).max(200).describe("The program to start (found anywhere in the download), e.g. mytool.exe."),
|
|
25824
|
+
cli: external_exports.boolean().optional().describe("A command-line tool: its folder goes on PATH instead of getting a desktop shortcut.")
|
|
25825
|
+
}).refine((a) => a.github && a.asset ? !a.url : Boolean(a.url) && !a.github && !a.asset, { message: "give github + asset, or url (not both)" });
|
|
25826
|
+
var Apps = external_exports.strictObject({
|
|
25827
|
+
install: external_exports.array(AppId).describe("Apps from the catalogue to install in every sandbox (the Apps screen in the menu lists them by category). Each is downloaded once and kept."),
|
|
25828
|
+
custom: external_exports.record(CustomAppId, CustomApp).describe("Programs of your own: a GitHub release or a download link, and the program to start. Installed like the catalogue apps.")
|
|
25829
|
+
});
|
|
25830
|
+
|
|
25597
25831
|
// src/core/schema/projects.ts
|
|
25598
25832
|
var Projects = external_exports.strictObject({
|
|
25599
25833
|
all: external_exports.boolean().describe("Map every project you have started before (and every folder in your roots) at every start, except those in exclude, so all of them open in the running sandbox without a restart. Every agent in the sandbox can then see all of them. Each one is still only set up when you first use it."),
|
|
@@ -25610,14 +25844,18 @@ var Config = external_exports.strictObject({
|
|
|
25610
25844
|
defaultHarness: Name.describe("Agent started by `sandbox launch` (sandbox claude always runs claude)."),
|
|
25611
25845
|
guestWorkspace: GuestWorkspace,
|
|
25612
25846
|
readOnly: external_exports.boolean().describe("Map the workspace read-only."),
|
|
25847
|
+
workspaceFilter: WorkspaceFilter,
|
|
25848
|
+
audit: Audit,
|
|
25613
25849
|
mappings: external_exports.array(Mapping).describe("Extra folders from this PC."),
|
|
25614
25850
|
sandbox: Sandbox,
|
|
25615
25851
|
storage: external_exports.strictObject({
|
|
25616
25852
|
root: external_exports.string().min(1).describe("Where everything is kept on this PC (tools, databases, logins, launches)."),
|
|
25617
25853
|
persistAgentState: external_exports.boolean().describe("Keep agent logins and history between sandboxes."),
|
|
25618
|
-
dataName: Name.describe("Name of the persistent data set (C:\\AgentData) shared by all workspaces.")
|
|
25854
|
+
dataName: Name.describe("Name of the persistent data set (C:\\AgentData) shared by all workspaces."),
|
|
25855
|
+
markSandboxFiles: external_exports.boolean().describe(`Programs and scripts (.exe, .ps1, .bat, .cmd, .msi, .lnk ...) that appear or change in your project while a sandbox runs get Windows' "downloaded from the internet" mark, so SmartScreen and PowerShell ask before they run on this PC.`),
|
|
25856
|
+
warnDataGb: external_exports.number().min(0).describe("Say at launch when the persisted data (tools, databases, logins) is bigger than this many GB; 0 = never. sandbox data shows what takes the space.")
|
|
25619
25857
|
}),
|
|
25620
|
-
network:
|
|
25858
|
+
network: Network,
|
|
25621
25859
|
provisioning: Provisioning,
|
|
25622
25860
|
downloads: Downloads,
|
|
25623
25861
|
toolchains: Toolchains,
|
|
@@ -25626,6 +25864,7 @@ var Config = external_exports.strictObject({
|
|
|
25626
25864
|
agentCommands: AgentCommands,
|
|
25627
25865
|
secrets: Secrets,
|
|
25628
25866
|
personalization: toggle("Copy your desktop look (theme, accent, taskbar, wallpaper, ...) into the sandbox."),
|
|
25867
|
+
apps: Apps,
|
|
25629
25868
|
claudeProfile: ClaudeProfile,
|
|
25630
25869
|
harnesses: external_exports.record(Name, Harness).describe("Agents that can run in the sandbox."),
|
|
25631
25870
|
hooks: external_exports.strictObject({ failFast: external_exports.boolean().describe("Stop when a hook script fails.") }),
|
|
@@ -25636,10 +25875,14 @@ var LocalOverrides = external_exports.strictObject({
|
|
|
25636
25875
|
harness: Name.describe("Agent for this project."),
|
|
25637
25876
|
guestWorkspace: GuestWorkspace,
|
|
25638
25877
|
readOnly: external_exports.boolean().describe("Map this project read-only."),
|
|
25639
|
-
|
|
25878
|
+
// A project may turn the filter on and keep more out, never let ignored files in.
|
|
25879
|
+
workspaceFilter: WorkspaceFilter.pick({ enabled: true, exclude: true }).partial(),
|
|
25880
|
+
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."),
|
|
25640
25881
|
scripts: Strings.describe("Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent."),
|
|
25641
|
-
|
|
25642
|
-
|
|
25882
|
+
// This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
|
|
25883
|
+
sandbox: Sandbox.omit({ cpuLimit: true, pageFileMb: true }).partial(),
|
|
25884
|
+
// A project may block more (the LAN, hosts); it never opens hosts, ports, DNS or a proxy.
|
|
25885
|
+
network: Network.pick({ blockPrivateRanges: true, blockHosts: true }).partial(),
|
|
25643
25886
|
provisioning: Provisioning.pick({ copyFromHost: true, fallbackToInstall: true, refresh: true }).extend({ git: Git.partial(), terminal: toggle("Windows Terminal as the sandbox window.").partial() }).partial(),
|
|
25644
25887
|
downloads: Downloads.partial(),
|
|
25645
25888
|
// Every toolchain setting, each one optional (a project usually only switches languages on or pins a version).
|
|
@@ -25650,7 +25893,9 @@ var LocalOverrides = external_exports.strictObject({
|
|
|
25650
25893
|
databases: external_exports.strictObject(import_lodash.default.mapValues(Databases.shape, (shape) => shape.partial())).partial(),
|
|
25651
25894
|
attach: Attach.partial(),
|
|
25652
25895
|
personalization: toggle("Copy your desktop look into the sandbox.").partial(),
|
|
25653
|
-
|
|
25896
|
+
// Catalogue apps only (a vetted list): your own programs (apps.custom) download anything, so a repo cannot add them.
|
|
25897
|
+
apps: Apps.pick({ install: true }).partial(),
|
|
25898
|
+
claudeProfile: ClaudeProfile.pick({ enabled: true, overwrite: true, copyCredentials: true, syncHistory: true, syncClaudeMem: true }).partial(),
|
|
25654
25899
|
hooks: external_exports.strictObject({ failFast: external_exports.boolean() }).partial()
|
|
25655
25900
|
}).partial();
|
|
25656
25901
|
|
|
@@ -25670,7 +25915,9 @@ var SandboxStatus = external_exports.object({
|
|
|
25670
25915
|
updatedAt: Time,
|
|
25671
25916
|
workspaceHost: Text(),
|
|
25672
25917
|
guestWorkspace: Text(),
|
|
25673
|
-
headless: external_exports.boolean()
|
|
25918
|
+
headless: external_exports.boolean(),
|
|
25919
|
+
/** The sandbox's IPv4 addresses (for network.forwardPorts; the host only uses ones on its Hyper-V network). */
|
|
25920
|
+
addresses: external_exports.array(external_exports.string().max(45)).max(8).optional()
|
|
25674
25921
|
});
|
|
25675
25922
|
var Version = external_exports.strictObject({ spec: Text(128), source: Text(256) });
|
|
25676
25923
|
var DatabasePlan = external_exports.strictObject({
|
|
@@ -25748,6 +25995,8 @@ var runPaths = (dataDir2) => {
|
|
|
25748
25995
|
responses: path.join(run2, "responses"),
|
|
25749
25996
|
sessions: path.join(run2, "sessions"),
|
|
25750
25997
|
projects: path.join(run2, "projects"),
|
|
25998
|
+
/** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
|
|
25999
|
+
audit: path.join(run2, "audit"),
|
|
25751
26000
|
request: file2("requests"),
|
|
25752
26001
|
response: file2("responses"),
|
|
25753
26002
|
session: file2("sessions"),
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The audit log: kept small (repeats folded, a cap per minute, old days compressed, limits on days and size), read back
|
|
2
|
+
// filtered, and what the sandbox reports is checked like everything else that comes from it.
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import zlib from 'node:zlib';
|
|
7
|
+
import { AuditCollector } from '../host/audit/collect.js';
|
|
8
|
+
import { AuditLog, fold } from '../host/audit/log.js';
|
|
9
|
+
import { parseProcessEvents } from '../guest/audit-parse.js';
|
|
10
|
+
import { runPaths } from '../shared/rundir.js';
|
|
11
|
+
const ev = (t, extra = {}) => ({ t, k: 'proc', p: 'C:\\x\\cargo.exe', c: 'cargo build', ...extra });
|
|
12
|
+
export const auditSuite = {
|
|
13
|
+
name: 'audit',
|
|
14
|
+
title: 'audit log (folding, compression, limits, untrusted reports)',
|
|
15
|
+
run: (t) => {
|
|
16
|
+
t.section('small');
|
|
17
|
+
const folded = fold([ev('2026-01-01T10:00:01Z'), ev('2026-01-01T10:00:02Z'), ev('2026-01-01T10:00:03Z', { c: 'cargo test' }), ev('2026-01-01T10:01:00Z')]);
|
|
18
|
+
t.equal(folded.map((e) => [e.c, e.n ?? 1]), [['cargo build', 2], ['cargo test', 1], ['cargo build', 1]], 'repeats within a minute become one line with a count');
|
|
19
|
+
const flood = fold(Array.from({ length: 50 }, (_, i) => ev('2026-01-01T10:00:00Z', { c: 'step ' + i })), 10);
|
|
20
|
+
t.check(flood.length === 11 && flood[10].k === 'note' && flood[10].p.startsWith('40 more'), 'past the per-minute cap the rest is one note');
|
|
21
|
+
const dir = t.at('audit-log');
|
|
22
|
+
const log = new AuditLog(dir, { days: 3, maxMb: 5 });
|
|
23
|
+
log.append([ev('2026-01-01T10:00:00Z'), ev('2026-01-02T10:00:00Z'), ev('2026-01-04T10:00:00Z', { k: 'file', p: 'src/a.ts', a: 'change', w: 'C:\\app' })]);
|
|
24
|
+
log.maintain('2026-01-04');
|
|
25
|
+
t.equal(fs.readdirSync(dir).sort(), ['2026-01-02.jsonl.gz', '2026-01-04.jsonl'], 'finished days are compressed and days past audit.days are deleted');
|
|
26
|
+
t.includes(zlib.gunzipSync(fs.readFileSync(path.join(dir, '2026-01-02.jsonl.gz'))).toString(), 'cargo build', 'a compressed day still holds its events');
|
|
27
|
+
const big = new AuditLog(t.at('audit-big'), { days: 30, maxMb: 5 });
|
|
28
|
+
// Random text: compression must not make the test data fit after all.
|
|
29
|
+
const line = (day) => ev(day + 'T00:00:00Z', { c: crypto.randomBytes(900).toString('hex') });
|
|
30
|
+
for (const day of ['2026-02-01', '2026-02-02', '2026-02-03'])
|
|
31
|
+
big.append(Array.from({ length: 1400 }, () => line(day)));
|
|
32
|
+
big.maintain('2026-02-03');
|
|
33
|
+
t.check(big.bytes() <= 5 * 1024 * 1024 && fs.readdirSync(big.dir).length < 3, 'past audit.maxMb the oldest days go', String(big.bytes()));
|
|
34
|
+
t.section('reading');
|
|
35
|
+
t.equal(log.read({ days: 10000, kinds: ['file'] }).map((e) => e.p), ['src/a.ts'], 'events can be filtered by kind');
|
|
36
|
+
t.equal(log.read({ days: 10000, grep: 'SRC/A.TS' }).length, 1, '... and by text (any case)');
|
|
37
|
+
t.section('what the sandbox reports');
|
|
38
|
+
const xml = "<Event><System><TimeCreated SystemTime='2026-01-05T09:00:00.000Z'/><EventRecordID>41</EventRecordID></System><EventData><Data Name='NewProcessName'>C:\\AgentData\\tools\\git\\cmd\\git.exe</Data><Data Name='CommandLine'>git push --force "origin"</Data><Data Name='ParentProcessName'>C:\\nvm\\nodejs\\node.exe</Data></EventData></Event>"
|
|
39
|
+
+ "<Event><System><EventRecordID>42</EventRecordID></System><EventData><Data Name='NewProcessName'>C:\\Windows\\System32\\conhost.exe</Data></EventData></Event>";
|
|
40
|
+
const parsed = parseProcessEvents(xml);
|
|
41
|
+
t.equal({ last: parsed.last, lines: parsed.lines.map((l) => JSON.parse(l)).map((e) => [e.p, e.c, e.pp]) }, { last: 42, lines: [['C:\\AgentData\\tools\\git\\cmd\\git.exe', 'git push --force "origin"', 'C:\\nvm\\nodejs\\node.exe']] }, 'Windows process events become audit lines; Windows\' own background programs are left out');
|
|
42
|
+
const data = t.at('audit-data', 'data', 'd');
|
|
43
|
+
const storage = t.at('audit-data');
|
|
44
|
+
const reports = runPaths(data).audit;
|
|
45
|
+
t.write(path.join(reports, '1-1.jsonl'), [
|
|
46
|
+
JSON.stringify({ t: '2026-01-05T09:00:00Z', k: 'guard', p: 'fake: all clear' }),
|
|
47
|
+
JSON.stringify({ t: '2026-01-05T09:00:01Z', k: 'proc', p: 'C:\\x.exe', c: 'x --y', w: 'C:\\Users\\me' }),
|
|
48
|
+
'not json', JSON.stringify({ t: '2026-01-05T09:00:02Z', k: 'proc', p: 'x'.repeat(5000) }),
|
|
49
|
+
].join('\n'));
|
|
50
|
+
t.write(path.join(reports, 'not-a-report.txt'), 'ignored');
|
|
51
|
+
const collector = new AuditCollector(data, storage, { enabled: true, days: 14, maxMb: 50, processes: true, files: false, projects: [] });
|
|
52
|
+
collector.flush();
|
|
53
|
+
const got = new AuditLog(AuditLog.dirFor(storage, data), { days: 14, maxMb: 50 }).read({ days: 100000 });
|
|
54
|
+
t.equal(got.map((e) => [e.k, e.p, e.w]), [['proc', 'fake: all clear', undefined], ['proc', 'C:\\x.exe', undefined]], 'sandbox reports can only be programs: other kinds are recorded as such, fields it may not set are dropped, oversized lines are refused');
|
|
55
|
+
t.check(!fs.existsSync(path.join(reports, '1-1.jsonl')), 'reports are deleted once taken in');
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -27,6 +27,13 @@ export const channelSuite = {
|
|
|
27
27
|
filter.push(Buffer.from('2;c;cGF5\x1b\\d')),
|
|
28
28
|
]).toString('latin1');
|
|
29
29
|
t.equal(filtered, 'a' + 'b\x1b]0;title\x07' + 'c' + 'd', 'clipboard writes (OSC 52) from the sandbox are stripped, even split across chunks; titles pass');
|
|
30
|
+
const through = (...chunks) => { const f = new OutputFilter(); return Buffer.concat(chunks.map((s) => f.push(Buffer.from(s, 'latin1')))).toString('latin1'); };
|
|
31
|
+
t.equal(through('x\x1b]52;c;QUFB\x1b', '\\y'), 'xy', 'an OSC 52 whose terminator is split between chunks is still stripped');
|
|
32
|
+
t.equal(through('x\x1b]052;c;QUFB\x07y'), 'xy', 'OSC 052 (leading zero) is stripped too');
|
|
33
|
+
t.equal(through('x\x1b]52;c;' + 'A'.repeat(70000), 'B'.repeat(100), '\x07y'), 'xy', 'a huge OSC 52 stays stripped across chunks');
|
|
34
|
+
t.equal(through('x\xc2\x9d52;c;QUFB\xc2\x9cy\x1bP1$qm\x1b\\z'), 'xyz', 'C1 (UTF-8) OSC and DCS strings are stripped');
|
|
35
|
+
t.equal(through('\x1b]8;;https://a.b\x1b\\link\x1b]8;;\x1b\\ \x1b[31mred\x1b[0m é\xc2\xa0'), '\x1b]8;;https://a.b\x1b\\link\x1b]8;;\x1b\\ \x1b[31mred\x1b[0m é\xc2\xa0', 'links, colours and ordinary UTF-8 (incl. U+00A0) pass unchanged');
|
|
36
|
+
t.equal(through('\x1b]9;9;macro\x07\x1b]9;4;1;50\x07'), '\x1b]9;4;1;50\x07', 'only the progress form of OSC 9 passes (ConEmu macros do not)');
|
|
30
37
|
const oversizedHeader = Buffer.concat([Buffer.from([16]), Buffer.from([0, 0, 0, 0x7f])]);
|
|
31
38
|
await t.throws(() => new FrameReader().push(oversizedHeader), 'frame too large', 'a frame announcing too much data is rejected instead of buffered');
|
|
32
39
|
t.equal(new FrameReader().push(encodeFrame(16, Buffer.alloc(MAX_FRAME_BYTES))).length, 1, 'a frame of exactly the maximum size is accepted');
|
|
@@ -5,13 +5,14 @@ import path from 'node:path';
|
|
|
5
5
|
import { buildProgram, parseCli } from '../cli/args.js';
|
|
6
6
|
import { COMMANDS } from '../cli/manifest.js';
|
|
7
7
|
import { commandLine, readHistory, recordHistory } from '../cli/history.js';
|
|
8
|
-
import {
|
|
8
|
+
import { splitArgs } from '../cli/args.js';
|
|
9
|
+
import { optionProblem } from '../cli/menu/index.js';
|
|
9
10
|
import { cleanTargetOf } from '../cli/commands/data/collect.js';
|
|
10
11
|
import { ago, colorEnabled, shorten, shortenMiddle } from '../cli/colors.js';
|
|
11
12
|
import { hoistConfig, withoutConfig } from '../cli/index.js';
|
|
12
13
|
import { closest, validNodeSpec, validPythonSpec } from '../cli/validate.js';
|
|
13
|
-
import { editConfigText,
|
|
14
|
-
import {
|
|
14
|
+
import { CATEGORIES, editConfigText, nameOf, SchemaNode } from '../cli/menu/settings/index.js';
|
|
15
|
+
import { Frame, ListScreen, Text, TextScreen } from '../cli/tui/index.js';
|
|
15
16
|
import { Config, LocalOverrides } from '../core/schema.js';
|
|
16
17
|
export const cliSuite = {
|
|
17
18
|
name: 'cli',
|
|
@@ -99,20 +100,49 @@ export const cliSuite = {
|
|
|
99
100
|
const projectBad = editConfigText('{}', ['storage', 'root'], 'C:\\x', 'project');
|
|
100
101
|
t.check('text' in projectEdit && projectEdit.text.includes('requireChecksum'), 'settings screen: a project file accepts project settings');
|
|
101
102
|
t.check('problems' in projectBad, 'settings screen: a project file is validated against the project schema (storage cannot be set there)');
|
|
102
|
-
// Every setting must be reachable in the settings screen
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
103
|
+
// Every setting must be reachable in the settings screen, in a category, with an editor that fits it.
|
|
104
|
+
const userSchema = new SchemaNode(Config);
|
|
105
|
+
const projectSchema = new SchemaNode(LocalOverrides);
|
|
106
|
+
const userLeaves = userSchema.leaves();
|
|
107
|
+
const projectLeaves = projectSchema.leaves();
|
|
107
108
|
t.check(userLeaves.length > 100 && projectLeaves.length > 50, 'settings screen: every setting is a leaf (' + userLeaves.length + ' settings, ' + projectLeaves.length + ' in a project file)');
|
|
108
|
-
const
|
|
109
|
-
t.equal(
|
|
110
|
-
|
|
111
|
-
t.
|
|
112
|
-
|
|
113
|
-
t.
|
|
114
|
-
const
|
|
115
|
-
t.check(
|
|
116
|
-
t.
|
|
109
|
+
const uncategorised = [...userSchema.children(), ...projectSchema.children()].map(([name]) => name).filter((name) => !CATEGORIES.some((c) => c.keys.includes(name)));
|
|
110
|
+
t.equal(uncategorised, [], 'settings screen: every top-level setting belongs to a category');
|
|
111
|
+
const kinds = new Set(userLeaves.map((l) => l.node.editor));
|
|
112
|
+
t.equal(['boolean', 'choice', 'union', 'number', 'string', 'string-list', 'choice-list', 'object-list', 'string-map', 'object-map'].filter((k) => !kinds.has(k)), [], 'settings screen: every kind of editor is used');
|
|
113
|
+
t.equal(userLeaves.filter((l) => l.node.editor === 'json').map((l) => l.key.join('.')), [], 'settings screen: no setting falls back to typing JSON');
|
|
114
|
+
t.check(userLeaves.some((l) => l.key.join('.') === 'downloads.requireChecksum') && projectLeaves.some((l) => l.key.join('.') === 'downloads.requireChecksum'), 'settings screen: downloads.requireChecksum is reachable in both files');
|
|
115
|
+
const harness = userSchema.child('harnesses').element;
|
|
116
|
+
t.includes(String(harness.check(harness.initial())), 'npmPackage', 'settings screen: a new agent says what it still needs (a package to install)');
|
|
117
|
+
t.equal(harness.check({ ...harness.initial(), npmPackage: 'my-cli', npmBin: 'my-cli' }), true, '... and is valid once it has one');
|
|
118
|
+
const mapping = userSchema.child('mappings').element;
|
|
119
|
+
t.check(mapping.check(mapping.initial()) !== true, 'settings screen: a new mapped folder needs its folders filled in before it is valid');
|
|
120
|
+
t.equal([Text.humanize('memoryMb'), Text.humanize('vGpu'), nameOf('node')], ['Memory MB', 'V GPU', 'Node.js'], 'settings screen: readable names');
|
|
121
|
+
t.section('screens');
|
|
122
|
+
const fit = Text.fit('\x1b[31mred text\x1b[0m', 5);
|
|
123
|
+
t.equal([Text.width(fit), Text.strip(fit)], [5, 'red …'], 'a coloured label is cut to the width without breaking its colour');
|
|
124
|
+
t.equal(Text.width('日本'), 4, 'wide characters take two columns');
|
|
125
|
+
const screen = new ListScreen({ path: ['T'], items: [{ value: 'a', label: 'Alpha', category: 'x' }, { value: 'b', label: 'Beta', category: 'y', description: 'second' }], categories: [{ id: 'x', label: 'X' }, { id: 'y', label: 'Y' }] });
|
|
126
|
+
const key = (name, text = '') => ({ name, text, ctrl: false, meta: false, shift: false });
|
|
127
|
+
screen.handle(key('left'));
|
|
128
|
+
screen.handle(key('down'));
|
|
129
|
+
screen.handle(key('right'));
|
|
130
|
+
t.equal(screen.handle(key('return')), { done: { kind: 'pick', value: 'b' } }, 'list screen: arrows switch category, Enter picks');
|
|
131
|
+
const search = new ListScreen({ path: ['T'], items: [{ value: 'a', label: 'Alpha' }, { value: 'b', label: 'Beta' }] });
|
|
132
|
+
for (const k of [key('', '/'), key('b', 'b'), key('e', 'e')])
|
|
133
|
+
search.handle(k);
|
|
134
|
+
t.equal(search.handle(key('return')), { done: { kind: 'pick', value: 'b' } }, 'list screen: / searches');
|
|
135
|
+
const lines = Frame.render(screen.render({ width: 60, height: Frame.bodyHeight(20) }), 60, 20);
|
|
136
|
+
t.equal(lines.length, 20, 'a frame fills the terminal height exactly');
|
|
137
|
+
const multi = new ListScreen({ path: ['T'], multi: true, items: [{ value: 'a', label: 'A' }, { value: 'b', label: 'B', checked: true }] });
|
|
138
|
+
multi.handle(key('space'));
|
|
139
|
+
t.equal(multi.handle(key('return')), { done: { kind: 'multi', values: ['a', 'b'] } }, 'list screen: space ticks several');
|
|
140
|
+
const typed = new TextScreen({ path: ['T'], label: 'Port', validate: (v) => (Number(v) > 0 ? true : 'a number') });
|
|
141
|
+
for (const ch of 'x')
|
|
142
|
+
typed.handle(key(ch, ch));
|
|
143
|
+
t.equal(typed.handle(key('return')), undefined, 'text field: an invalid value is not accepted');
|
|
144
|
+
typed.handle(key('backspace'));
|
|
145
|
+
typed.handle(key('8', '8'));
|
|
146
|
+
t.equal(typed.handle(key('return')), { done: '8' }, 'text field: a valid value is');
|
|
117
147
|
},
|
|
118
148
|
};
|