windows-development-sandbox 1.0.5 → 1.1.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/LICENSE +21 -0
- package/README.md +22 -6
- package/config.example.json +17 -0
- package/dist/api.js +2 -1
- package/dist/cli/args.d.ts +20 -0
- package/dist/cli/args.js +35 -3
- package/dist/cli/colors.d.ts +24 -0
- package/dist/cli/colors.js +42 -0
- package/dist/cli/commands/agent-commands.d.ts +6 -0
- package/dist/cli/commands/agent-commands.js +61 -0
- package/dist/cli/commands/context.d.ts +2 -0
- package/dist/cli/commands/context.js +3 -0
- package/dist/cli/commands/data/clean.d.ts +15 -0
- package/dist/cli/commands/data/clean.js +101 -0
- package/dist/cli/commands/data/collect.d.ts +11 -0
- package/dist/cli/commands/data/collect.js +54 -0
- package/dist/cli/commands/data/index.d.ts +4 -0
- package/dist/cli/commands/data/index.js +45 -0
- package/dist/cli/commands/data/remove.d.ts +2 -0
- package/dist/cli/commands/data/remove.js +30 -0
- package/dist/cli/commands/index.d.ts +5 -3
- package/dist/cli/commands/index.js +5 -3
- package/dist/cli/commands/launch.d.ts +26 -2
- package/dist/cli/commands/launch.js +220 -39
- package/dist/cli/commands/maintenance.js +33 -4
- package/dist/cli/commands/project.d.ts +29 -0
- package/dist/cli/commands/project.js +63 -0
- package/dist/cli/commands/projects.d.ts +14 -0
- package/dist/cli/commands/projects.js +146 -0
- package/dist/cli/commands/repair/checks.d.ts +2 -0
- package/dist/cli/commands/{repair.js → repair/checks.js} +19 -181
- package/dist/cli/commands/{repair.d.ts → repair/finders.d.ts} +0 -29
- package/dist/cli/commands/repair/finders.js +108 -0
- package/dist/cli/commands/repair/index.d.ts +12 -0
- package/dist/cli/commands/repair/index.js +61 -0
- package/dist/cli/commands/repair/types.d.ts +26 -0
- package/dist/cli/commands/repair/types.js +3 -0
- package/dist/cli/commands/restart.d.ts +6 -0
- package/dist/cli/commands/restart.js +85 -0
- package/dist/cli/commands/sessions.d.ts +2 -1
- package/dist/cli/commands/sessions.js +189 -45
- package/dist/cli/commands/setup/checks.d.ts +13 -0
- package/dist/cli/commands/setup/checks.js +71 -0
- package/dist/cli/commands/{setup.d.ts → setup/facts.d.ts} +1 -15
- package/dist/cli/commands/setup/facts.js +60 -0
- package/dist/cli/commands/setup/index.d.ts +2 -0
- package/dist/cli/commands/setup/index.js +43 -0
- package/dist/cli/commands/user-config.d.ts +8 -0
- package/dist/cli/commands/user-config.js +41 -0
- package/dist/cli/history.d.ts +11 -0
- package/dist/cli/history.js +21 -0
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/index.js +38 -13
- package/dist/cli/manifest.d.ts +8 -0
- package/dist/cli/manifest.js +35 -15
- package/dist/cli/menu-multi.d.ts +12 -0
- package/dist/cli/menu-multi.js +82 -0
- package/dist/cli/menu.d.ts +5 -13
- package/dist/cli/menu.js +149 -46
- package/dist/cli/progress.d.ts +9 -0
- package/dist/cli/progress.js +49 -0
- package/dist/cli/settings/edit.d.ts +13 -0
- package/dist/cli/settings/edit.js +19 -0
- package/dist/cli/settings/index.d.ts +8 -0
- package/dist/cli/settings/index.js +131 -0
- package/dist/cli/settings/prompts.d.ts +7 -0
- package/dist/cli/settings/prompts.js +48 -0
- package/dist/cli/settings/schema.d.ts +33 -0
- package/dist/cli/settings/schema.js +29 -0
- package/dist/cli/validate.d.ts +17 -0
- package/dist/cli/validate.js +89 -0
- package/dist/core/config.js +4 -1
- package/dist/core/disk.d.ts +16 -0
- package/dist/core/disk.js +55 -0
- package/dist/core/local-config.d.ts +8 -0
- package/dist/core/local-config.js +18 -5
- package/dist/core/project-policy.d.ts +15 -0
- package/dist/core/project-policy.js +35 -0
- package/dist/core/prompt.d.ts +4 -0
- package/dist/core/prompt.js +15 -0
- package/dist/core/safe-paths.d.ts +4 -0
- package/dist/core/safe-paths.js +37 -0
- package/dist/core/schema/common.d.ts +17 -0
- package/dist/core/schema/common.js +15 -0
- package/dist/core/schema/config.d.ts +594 -0
- package/dist/core/schema/config.js +63 -0
- package/dist/core/schema/databases.d.ts +33 -0
- package/dist/core/schema/databases.js +35 -0
- package/dist/core/schema/harness.d.ts +12 -0
- package/dist/core/schema/harness.js +17 -0
- package/dist/core/schema/json-schema.d.ts +5 -0
- package/dist/core/schema/json-schema.js +25 -0
- package/dist/core/schema/profile.d.ts +27 -0
- package/dist/core/schema/profile.js +29 -0
- package/dist/core/schema/projects.d.ts +9 -0
- package/dist/core/schema/projects.js +11 -0
- package/dist/core/schema/sandbox.d.ts +50 -0
- package/dist/core/schema/sandbox.js +36 -0
- package/dist/core/schema/toolchains.d.ts +93 -0
- package/dist/core/schema/toolchains.js +83 -0
- package/dist/core/schema.d.ts +6 -604
- package/dist/core/schema.js +8 -264
- package/dist/core/types.d.ts +29 -1
- package/dist/core/util.d.ts +7 -0
- package/dist/core/util.js +12 -3
- package/dist/core/workspace.d.ts +0 -5
- package/dist/core/workspace.js +12 -46
- package/dist/guest/boot-env.d.ts +4 -0
- package/dist/guest/boot-env.js +25 -0
- package/dist/guest/cmdline.d.ts +12 -0
- package/dist/guest/cmdline.js +23 -0
- package/dist/guest/daemon/exec.d.ts +5 -0
- package/dist/guest/daemon/exec.js +32 -0
- package/dist/guest/daemon/project.d.ts +14 -0
- package/dist/guest/daemon/project.js +132 -0
- package/dist/guest/daemon/session.d.ts +13 -0
- package/dist/guest/daemon/session.js +139 -0
- package/dist/guest/daemon/shutdown.d.ts +1 -0
- package/dist/guest/daemon/shutdown.js +21 -0
- package/dist/guest/daemon/state.d.ts +17 -0
- package/dist/guest/daemon/state.js +31 -0
- package/dist/guest/daemon.js +8 -141
- package/dist/guest/db.js +18 -3
- package/dist/guest/env.d.ts +2 -0
- package/dist/guest/env.js +4 -2
- package/dist/guest/guide.d.ts +6 -2
- package/dist/guest/guide.js +5 -4
- package/dist/guest/helpers.d.ts +3 -0
- package/dist/guest/helpers.js +26 -0
- package/dist/guest/main.js +17 -7
- package/dist/guest/ports.d.ts +6 -0
- package/dist/guest/ports.js +22 -0
- package/dist/guest/project-context.d.ts +7 -0
- package/dist/guest/project-context.js +10 -0
- package/dist/guest/project-env.d.ts +20 -0
- package/dist/guest/project-env.js +20 -0
- package/dist/guest/project-status.d.ts +5 -0
- package/dist/guest/project-status.js +17 -0
- package/dist/guest/ptyhost.d.ts +0 -18
- package/dist/guest/ptyhost.js +5 -63
- package/dist/guest/setup/agents.d.ts +5 -2
- package/dist/guest/setup/agents.js +22 -8
- package/dist/guest/setup/claude.js +11 -3
- package/dist/guest/setup/environment.d.ts +13 -3
- package/dist/guest/setup/environment.js +14 -10
- package/dist/guest/setup/hooks.d.ts +9 -1
- package/dist/guest/setup/hooks.js +28 -14
- package/dist/guest/toolchains/catalogue.d.ts +4 -0
- package/dist/guest/toolchains/catalogue.js +2 -2
- package/dist/guest/toolchains/cli.js +25 -17
- package/dist/guest/toolchains/common.d.ts +18 -1
- package/dist/guest/toolchains/common.js +34 -11
- package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
- package/dist/guest/toolchains/databases/dynamodb.js +52 -0
- package/dist/guest/toolchains/{databases.d.ts → databases/index.d.ts} +2 -3
- package/dist/guest/toolchains/databases/index.js +62 -0
- package/dist/guest/toolchains/databases/mongodb.d.ts +3 -0
- package/dist/guest/toolchains/databases/mongodb.js +31 -0
- package/dist/guest/toolchains/databases/mysql.d.ts +3 -0
- package/dist/guest/toolchains/databases/mysql.js +38 -0
- package/dist/guest/toolchains/databases/postgres.d.ts +5 -0
- package/dist/guest/toolchains/databases/postgres.js +38 -0
- package/dist/guest/toolchains/databases/redis.d.ts +3 -0
- package/dist/guest/toolchains/databases/redis.js +16 -0
- package/dist/guest/toolchains/databases/server.d.ts +14 -0
- package/dist/guest/toolchains/databases/server.js +52 -0
- package/dist/guest/toolchains/git.js +25 -16
- package/dist/guest/toolchains/go.js +1 -1
- package/dist/guest/toolchains/index.d.ts +13 -9
- package/dist/guest/toolchains/index.js +40 -82
- package/dist/guest/toolchains/java.js +5 -5
- package/dist/guest/toolchains/leftovers.d.ts +2 -0
- package/dist/guest/toolchains/leftovers.js +26 -0
- package/dist/guest/toolchains/managers.js +12 -10
- package/dist/guest/toolchains/{binstall.js → native/binstall.js} +6 -9
- package/dist/guest/toolchains/native/cpp.d.ts +4 -0
- package/dist/guest/toolchains/native/cpp.js +38 -0
- package/dist/guest/toolchains/native/dotnet.d.ts +4 -0
- package/dist/guest/toolchains/native/dotnet.js +103 -0
- package/dist/guest/toolchains/native/mesa.d.ts +7 -0
- package/dist/guest/toolchains/native/mesa.js +46 -0
- package/dist/guest/toolchains/native/msvc/environment.d.ts +16 -0
- package/dist/guest/toolchains/native/msvc/environment.js +57 -0
- package/dist/guest/toolchains/native/msvc/index.d.ts +6 -0
- package/dist/guest/toolchains/native/msvc/index.js +48 -0
- package/dist/guest/toolchains/native/msvc/packages.d.ts +6 -0
- package/dist/guest/toolchains/native/msvc/packages.js +156 -0
- package/dist/guest/toolchains/native/msvc/tree.d.ts +15 -0
- package/dist/guest/toolchains/native/msvc/tree.js +21 -0
- package/dist/guest/toolchains/native/rust.d.ts +3 -0
- package/dist/guest/toolchains/native/rust.js +45 -0
- package/dist/guest/toolchains/node.js +26 -6
- package/dist/guest/toolchains/plan.d.ts +22 -0
- package/dist/guest/toolchains/plan.js +96 -0
- package/dist/guest/toolchains/pwsh.js +1 -1
- package/dist/guest/toolchains/python.js +17 -10
- package/dist/guest/toolchains/runtimes.d.ts +2 -0
- package/dist/guest/toolchains/runtimes.js +6 -8
- package/dist/guest/toolchains/scripting/php.d.ts +3 -0
- package/dist/guest/toolchains/scripting/php.js +71 -0
- package/dist/guest/toolchains/scripting/ruby.d.ts +3 -0
- package/dist/guest/toolchains/scripting/ruby.js +47 -0
- package/dist/guest/toolchains/scripting/vcruntime.d.ts +7 -0
- package/dist/guest/toolchains/scripting/vcruntime.js +45 -0
- package/dist/guest/view.js +21 -1
- package/dist/host/accounts.js +14 -5
- package/dist/host/agent-commands.d.ts +25 -0
- package/dist/host/agent-commands.js +145 -0
- package/dist/host/attach.d.ts +17 -0
- package/dist/host/attach.js +69 -0
- package/dist/host/channel.d.ts +23 -22
- package/dist/host/channel.js +66 -84
- package/dist/host/claude-profile.js +7 -3
- package/dist/host/cleanup.js +2 -11
- package/dist/host/database-plan.d.ts +12 -0
- package/dist/host/database-plan.js +52 -0
- package/dist/host/generate.d.ts +4 -12
- package/dist/host/generate.js +48 -135
- package/dist/host/join.d.ts +28 -0
- package/dist/host/join.js +76 -0
- package/dist/host/launch/mappings.d.ts +3 -0
- package/dist/host/launch/mappings.js +50 -0
- package/dist/host/launch/projects.d.ts +15 -0
- package/dist/host/launch/projects.js +56 -0
- package/dist/host/launch/stage.d.ts +11 -0
- package/dist/host/launch/stage.js +66 -0
- package/dist/host/launch/wsb.d.ts +10 -0
- package/dist/host/launch/wsb.js +34 -0
- package/dist/host/pc-scan.d.ts +6 -0
- package/dist/host/pc-scan.js +37 -0
- package/dist/host/platform.d.ts +2 -0
- package/dist/host/platform.js +13 -1
- package/dist/host/project-scan.d.ts +1 -17
- package/dist/host/project-scan.js +2 -86
- package/dist/host/projects.d.ts +100 -0
- package/dist/host/projects.js +147 -0
- package/dist/host/repo-guard.d.ts +15 -2
- package/dist/host/repo-guard.js +103 -12
- package/dist/host/terminal.js +3 -0
- package/dist/host/test-guard.d.ts +6 -0
- package/dist/host/test-guard.js +15 -0
- package/dist/host/window.js +7 -3
- package/dist/host/wsb.d.ts +37 -0
- package/dist/host/wsb.js +101 -0
- package/dist/runtime/daemon.js +10997 -2794
- package/dist/runtime/db.js +6428 -464
- package/dist/runtime/main.js +7359 -1135
- package/dist/runtime/view.js +5922 -47
- package/dist/selftest/accounts.test.d.ts +2 -0
- package/dist/selftest/accounts.test.js +44 -0
- package/dist/selftest/agent-commands.test.d.ts +2 -0
- package/dist/selftest/agent-commands.test.js +47 -0
- package/dist/selftest/channel.test.d.ts +2 -0
- package/dist/selftest/channel.test.js +37 -0
- package/dist/selftest/cli.test.d.ts +2 -2
- package/dist/selftest/cli.test.js +115 -27
- package/dist/selftest/cmdline.test.d.ts +2 -0
- package/dist/selftest/cmdline.test.js +45 -0
- package/dist/selftest/config.test.d.ts +2 -3
- package/dist/selftest/config.test.js +36 -72
- package/dist/selftest/data.test.d.ts +2 -2
- package/dist/selftest/data.test.js +77 -70
- package/dist/selftest/detect.test.d.ts +2 -2
- package/dist/selftest/detect.test.js +54 -38
- package/dist/selftest/fixtures.d.ts +44 -4
- package/dist/selftest/fixtures.js +40 -23
- package/dist/selftest/generate.test.d.ts +2 -0
- package/dist/selftest/generate.test.js +63 -0
- package/dist/selftest/git-access.test.d.ts +2 -0
- package/dist/selftest/git-access.test.js +26 -0
- package/dist/selftest/guest-boot.test.d.ts +2 -0
- package/dist/selftest/guest-boot.test.js +76 -0
- package/dist/selftest/harness.d.ts +44 -7
- package/dist/selftest/harness.js +62 -21
- package/dist/selftest/host-sim.test.d.ts +2 -0
- package/dist/selftest/host-sim.test.js +93 -0
- package/dist/selftest/hostile-input.test.d.ts +2 -0
- package/dist/selftest/hostile-input.test.js +54 -0
- package/dist/selftest/index.d.ts +8 -1
- package/dist/selftest/index.js +134 -35
- package/dist/selftest/links.test.d.ts +2 -0
- package/dist/selftest/links.test.js +50 -0
- package/dist/selftest/live-daemon.test.d.ts +2 -0
- package/dist/selftest/live-daemon.test.js +91 -0
- package/dist/selftest/live-git.test.d.ts +2 -0
- package/dist/selftest/live-git.test.js +60 -0
- package/dist/selftest/live-projects.test.d.ts +2 -0
- package/dist/selftest/live-projects.test.js +127 -0
- package/dist/selftest/live.d.ts +65 -0
- package/dist/selftest/live.js +109 -0
- package/dist/selftest/local-config.test.d.ts +2 -0
- package/dist/selftest/local-config.test.js +107 -0
- package/dist/selftest/mirror.test.d.ts +2 -3
- package/dist/selftest/mirror.test.js +92 -65
- package/dist/selftest/network.test.d.ts +2 -0
- package/dist/selftest/network.test.js +28 -0
- package/dist/selftest/personalization.test.d.ts +2 -0
- package/dist/selftest/personalization.test.js +53 -0
- package/dist/selftest/projects.test.d.ts +2 -0
- package/dist/selftest/projects.test.js +254 -0
- package/dist/selftest/repair.test.d.ts +2 -0
- package/dist/selftest/repair.test.js +42 -0
- package/dist/selftest/repo-guard.test.d.ts +2 -0
- package/dist/selftest/repo-guard.test.js +63 -0
- package/dist/selftest/secrets.test.d.ts +2 -0
- package/dist/selftest/secrets.test.js +71 -0
- package/dist/selftest/setup.test.d.ts +2 -2
- package/dist/selftest/setup.test.js +45 -22
- package/dist/selftest/suite.d.ts +13 -0
- package/dist/selftest/suite.js +1 -0
- package/dist/selftest/sync.test.d.ts +2 -3
- package/dist/selftest/sync.test.js +90 -78
- package/dist/selftest/test-guard.test.d.ts +2 -0
- package/dist/selftest/test-guard.test.js +49 -0
- package/dist/selftest/toolchains.test.d.ts +2 -0
- package/dist/selftest/toolchains.test.js +103 -0
- package/dist/selftest/workspace.test.d.ts +2 -0
- package/dist/selftest/workspace.test.js +53 -0
- package/dist/shared/download.d.ts +3 -1
- package/dist/shared/download.js +19 -4
- package/dist/shared/paths.d.ts +5 -0
- package/dist/shared/paths.js +6 -0
- package/dist/shared/projects.d.ts +53 -0
- package/dist/shared/projects.js +67 -0
- package/dist/shared/rundir.d.ts +10 -1
- package/dist/shared/rundir.js +12 -4
- package/dist/shared/safe-remove.js +11 -0
- package/dist/shared/schema.d.ts +443 -0
- package/dist/shared/schema.js +41 -2
- package/dist/shared/test-guard.d.ts +4 -0
- package/dist/shared/test-guard.js +37 -0
- package/dist/sync/claude-mem.js +3 -1
- package/dist/sync/history.d.ts +23 -12
- package/dist/sync/history.js +52 -20
- package/dist/sync/watcher.d.ts +8 -0
- package/dist/sync/watcher.js +23 -4
- package/package.json +6 -6
- package/sandbox/bootstrap.ps1 +258 -258
- package/sandbox/cpucap.ps1 +51 -0
- package/sandbox/logon.cmd +18 -16
- package/sandbox/openssl.cs +42 -0
- package/sandbox/ptyhost.cs +15 -4
- package/sandbox/trust.cmd +35 -35
- package/schemas/config.schema.json +100 -1
- package/schemas/sandbox-config.schema.json +17 -1
- package/dist/cli/commands/data.d.ts +0 -46
- package/dist/cli/commands/data.js +0 -249
- package/dist/cli/commands/setup.js +0 -152
- package/dist/cli/settings.d.ts +0 -11
- package/dist/cli/settings.js +0 -156
- package/dist/guest/toolchains/databases.js +0 -226
- package/dist/guest/toolchains/msvc.d.ts +0 -10
- package/dist/guest/toolchains/msvc.js +0 -216
- package/dist/guest/toolchains/native.d.ts +0 -12
- package/dist/guest/toolchains/native.js +0 -220
- package/dist/guest/toolchains/scripting.d.ts +0 -10
- package/dist/guest/toolchains/scripting.js +0 -137
- package/dist/selftest/guest.test.d.ts +0 -3
- package/dist/selftest/guest.test.js +0 -61
- package/dist/selftest/helpers.test.d.ts +0 -2
- package/dist/selftest/helpers.test.js +0 -125
- package/dist/selftest/host.test.d.ts +0 -2
- package/dist/selftest/host.test.js +0 -71
- package/dist/selftest/live.test.d.ts +0 -7
- package/dist/selftest/live.test.js +0 -127
- package/dist/selftest/security.test.d.ts +0 -3
- package/dist/selftest/security.test.js +0 -143
- /package/dist/guest/toolchains/{binstall.d.ts → native/binstall.d.ts} +0 -0
package/dist/host/repo-guard.js
CHANGED
|
@@ -11,7 +11,10 @@ import crypto from 'node:crypto';
|
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import _ from 'lodash';
|
|
14
|
-
import {
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { readValid, writeFileAtomic, writeJsonAtomic } from '../shared/rundir.js';
|
|
16
|
+
import { assertTestWritable } from './test-guard.js';
|
|
17
|
+
import { dirExists, fileExists, isLink } from '../core/util.js';
|
|
15
18
|
/** Config keys that make git run a program, or load more config. */
|
|
16
19
|
const DANGEROUS_KEY = /^(fsmonitor|hookspath|sshcommand|pager|editor|askpass|program|gpgprogram|textconv|command|cmd|clean|smudge|process|driver|helper|uploadpack|receivepack|path|attributesfile|excludesfile|worktree|worktreeconfig|gitproxy|proxycommand|sendemailvalidate|tool)\s*=/i;
|
|
17
20
|
const SECTION = /^\s*\[([^\]]*)\]\s*/;
|
|
@@ -30,7 +33,30 @@ const COMMANDS = new RegExp('^(' + [
|
|
|
30
33
|
'make', 'cmake', 'ninja', 'gcc', 'g\\+\\+', 'clang', 'psql', 'mysql', 'mariadb', 'mongosh', 'redis-cli', 'docker', 'kubectl', 'terraform', 'az', 'aws',
|
|
31
34
|
'powershell', 'pwsh', 'cmd', 'where', 'tasklist', 'tar', 'curl', 'ssh', 'scp',
|
|
32
35
|
].join('|') + ')\\.(exe|com|bat|cmd|vbs|vbe|js|jse|wsf|wsh|msc|ps1)$', 'i');
|
|
33
|
-
|
|
36
|
+
/** SHA-256 in small chunks: a sandbox can plant a multi-gigabyte "node.exe" to exhaust this PC's memory. */
|
|
37
|
+
const hash = (file) => {
|
|
38
|
+
if (isLink(file))
|
|
39
|
+
return 'link';
|
|
40
|
+
const sum = crypto.createHash('sha256');
|
|
41
|
+
const buffer = Buffer.alloc(1 << 20);
|
|
42
|
+
let fd;
|
|
43
|
+
// A file that cannot be read (locked by a program in the sandbox) never matches the snapshot, so it is dealt with.
|
|
44
|
+
try {
|
|
45
|
+
fd = fs.openSync(file, 'r');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return 'unreadable-' + crypto.randomBytes(4).toString('hex');
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
for (let n = fs.readSync(fd, buffer, 0, buffer.length, null); n > 0; n = fs.readSync(fd, buffer, 0, buffer.length, null))
|
|
52
|
+
sum.update(buffer.subarray(0, n));
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
fs.closeSync(fd);
|
|
56
|
+
}
|
|
57
|
+
return sum.digest('hex');
|
|
58
|
+
};
|
|
59
|
+
const MAX_CONFIG_BYTES = 4 * 1024 * 1024;
|
|
34
60
|
const plainGitDir = (workspace) => {
|
|
35
61
|
const dir = path.join(workspace, '.git');
|
|
36
62
|
return dirExists(dir) && !isLink(dir) ? dir : null;
|
|
@@ -40,7 +66,7 @@ const filesIn = (dir, accept) => dirExists(dir) && !isLink(dir)
|
|
|
40
66
|
: {};
|
|
41
67
|
const configLines = (dir) => {
|
|
42
68
|
const file = path.join(dir, 'config');
|
|
43
|
-
return fileExists(file) && !isLink(file) ? fs.readFileSync(file, 'utf8').split(/\r?\n/) : [];
|
|
69
|
+
return fileExists(file) && !isLink(file) && fs.statSync(file).size <= MAX_CONFIG_BYTES ? fs.readFileSync(file, 'utf8').split(/\r?\n/) : [];
|
|
44
70
|
};
|
|
45
71
|
export const snapshotRepo = (workspace) => {
|
|
46
72
|
const dir = plainGitDir(workspace);
|
|
@@ -59,7 +85,12 @@ const quarantine = (from, names, into, kind) => {
|
|
|
59
85
|
const target = path.join(into, 'agent-sandbox-quarantine', String(Date.now()));
|
|
60
86
|
fs.mkdirSync(target, { recursive: true });
|
|
61
87
|
return names.map((name) => {
|
|
62
|
-
|
|
88
|
+
try {
|
|
89
|
+
fs.renameSync(path.join(from, name), path.join(target, name));
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
return { kind, detail: name + ' (could not be moved: ' + e.code + '; delete it yourself before running anything here)' };
|
|
93
|
+
}
|
|
63
94
|
return { kind, detail: name + ' (moved to ' + target + ')' };
|
|
64
95
|
});
|
|
65
96
|
};
|
|
@@ -85,7 +116,12 @@ const disableConfig = (dir, before) => {
|
|
|
85
116
|
return MARK + line.trim();
|
|
86
117
|
});
|
|
87
118
|
if (findings.length)
|
|
88
|
-
|
|
119
|
+
writeFileAtomic(path.join(dir, 'config'), fixed.join('\n'));
|
|
120
|
+
const file = path.join(dir, 'config');
|
|
121
|
+
// A link or a huge file is not read at all, so say so instead of letting it pass as "nothing found".
|
|
122
|
+
const unchecked = isLink(file) ? '.git/config is a link' : lines.length === 0 && fileExists(file) && fs.statSync(file).size > MAX_CONFIG_BYTES ? '.git/config is over 4 MB' : null;
|
|
123
|
+
if (unchecked)
|
|
124
|
+
findings.push({ kind: 'git', detail: unchecked + ', so it cannot be checked. Do not run git in this folder until you have looked at it.' });
|
|
89
125
|
return findings;
|
|
90
126
|
};
|
|
91
127
|
/**
|
|
@@ -93,6 +129,7 @@ const disableConfig = (dir, before) => {
|
|
|
93
129
|
* Returns what was found (and fixed). Never touches what was already there before the sandbox ran.
|
|
94
130
|
*/
|
|
95
131
|
export const guardRepo = (workspace, before) => {
|
|
132
|
+
assertTestWritable(workspace);
|
|
96
133
|
const findings = [];
|
|
97
134
|
const commandsNow = filesIn(workspace, (n) => COMMANDS.test(n));
|
|
98
135
|
findings.push(...quarantine(workspace, Object.keys(commandsNow).filter((n) => before.commands[n] !== commandsNow[n]), workspace, 'command'));
|
|
@@ -114,15 +151,69 @@ export const guardRepo = (workspace, before) => {
|
|
|
114
151
|
const syncDir = (dataPath) => path.join(path.dirname(path.dirname(dataPath)), 'sync');
|
|
115
152
|
const snapshotFile = (dataPath) => path.join(syncDir(dataPath), path.basename(dataPath) + '-repo.json');
|
|
116
153
|
const findingsFile = (dataPath) => path.join(syncDir(dataPath), path.basename(dataPath) + '-repo-findings.json');
|
|
117
|
-
|
|
118
|
-
|
|
154
|
+
const Finding = z.object({ kind: z.enum(['hook', 'config', 'git', 'command']), detail: z.string().max(4096), at: z.string().max(64).optional() });
|
|
155
|
+
const Saved = z.object({
|
|
156
|
+
workspace: z.string().max(1024).refine((p) => path.isAbsolute(p)),
|
|
157
|
+
snapshot: z.object({ gitDir: z.boolean(), config: z.array(z.string()), hooks: z.record(z.string(), z.string()), redirects: z.array(z.string()), commands: z.record(z.string(), z.string()) }),
|
|
158
|
+
});
|
|
159
|
+
const Store = z.strictObject({ projects: z.array(Saved).max(4096), roots: z.array(z.string().max(1024).refine((p) => path.isAbsolute(p))).max(64) });
|
|
160
|
+
const StoreAny = z.union([Store, Saved.transform((s) => ({ projects: [s], roots: [] }))]);
|
|
161
|
+
const readStore = (dataPath) => readValid(StoreAny, snapshotFile(dataPath)) ?? { projects: [], roots: [] };
|
|
162
|
+
const EMPTY = { gitDir: false, config: [], hooks: {}, redirects: [], commands: {} };
|
|
163
|
+
const MAX_ROOT_PROJECTS = 2000;
|
|
164
|
+
/** The folders directly inside a project root (plain folders only; links are never followed). */
|
|
165
|
+
const rootProjects = (root) => {
|
|
166
|
+
if (!dirExists(root) || isLink(root))
|
|
167
|
+
return [];
|
|
168
|
+
return fs.readdirSync(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.isSymbolicLink()).slice(0, MAX_ROOT_PROJECTS).map((e) => path.join(root, e.name));
|
|
169
|
+
};
|
|
170
|
+
const has = (projects, workspace) => projects.some((s) => s.workspace.toLowerCase() === workspace.toLowerCase());
|
|
171
|
+
/**
|
|
172
|
+
* At launch: every folder the sandbox will be able to write. The launch folder, projects mapped with it and every
|
|
173
|
+
* project already inside a project root are snapshotted as they are now, before the sandbox can touch them.
|
|
174
|
+
*/
|
|
175
|
+
export const saveRepoSnapshot = (dataPath, workspace, more = [], roots = []) => {
|
|
176
|
+
const folders = _.uniqBy([workspace, ...more, ...roots.flatMap((r) => [r, ...rootProjects(r)])], (w) => w.toLowerCase());
|
|
177
|
+
writeJsonAtomic(snapshotFile(dataPath), { projects: folders.map((w) => ({ workspace: w, snapshot: snapshotRepo(w) })), roots });
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* A project that joins the running sandbox: snapshotted before its first session, then guarded like the launch
|
|
181
|
+
* folder. A project already guarded keeps its first snapshot (what was there before the sandbox could touch it).
|
|
182
|
+
*/
|
|
183
|
+
export const addRepoSnapshot = (dataPath, workspace, o = {}) => {
|
|
184
|
+
const store = readStore(dataPath);
|
|
185
|
+
// A folder inside one that is guarded already is covered by it only for its root files; it gets its own entry.
|
|
186
|
+
if (has(store.projects, workspace))
|
|
187
|
+
return;
|
|
188
|
+
writeJsonAtomic(snapshotFile(dataPath), { ...store, projects: [...store.projects, { workspace, snapshot: o.empty ? EMPTY : snapshotRepo(workspace) }] });
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Folders that appeared inside a project root after the launch: the sandbox could have written them from the start,
|
|
192
|
+
* so they get an EMPTY baseline: any git hook, git config that runs programs (core.hooksPath, core.fsmonitor, ...)
|
|
193
|
+
* or planted command in them counts as added inside the sandbox and is neutralised.
|
|
194
|
+
*/
|
|
195
|
+
const discoverRootProjects = (dataPath) => {
|
|
196
|
+
const store = readStore(dataPath);
|
|
197
|
+
const found = store.roots.flatMap(rootProjects).filter((w) => !has(store.projects, w)).map((workspace) => ({ workspace, snapshot: EMPTY }));
|
|
198
|
+
if (!found.length)
|
|
199
|
+
return store.projects;
|
|
200
|
+
const projects = [...store.projects, ...found].slice(0, 4096);
|
|
201
|
+
writeJsonAtomic(snapshotFile(dataPath), { ...store, projects });
|
|
202
|
+
return projects;
|
|
203
|
+
};
|
|
204
|
+
/** The folders being guarded (for status). */
|
|
205
|
+
export const guardedProjects = (dataPath) => readStore(dataPath).projects.map((s) => s.workspace);
|
|
206
|
+
/** Re-checks every project of the current/last launch; findings are kept in a host-only file for `status`. Returns the new ones. */
|
|
119
207
|
export const checkRepo = (dataPath) => {
|
|
120
|
-
const saved =
|
|
121
|
-
if (!saved
|
|
208
|
+
const saved = discoverRootProjects(dataPath);
|
|
209
|
+
if (!saved.length)
|
|
122
210
|
return [];
|
|
123
|
-
const
|
|
211
|
+
const old = repoFindings(dataPath);
|
|
212
|
+
// A finding that cannot be fixed (a file locked in the sandbox) is reported once, not every minute.
|
|
213
|
+
const findings = saved.flatMap((s) => guardRepo(s.workspace, s.snapshot).map((f) => (saved.length > 1 ? { ...f, detail: s.workspace + ': ' + f.detail } : f)))
|
|
214
|
+
.filter((f) => !old.some((o) => o.kind === f.kind && o.detail === f.detail));
|
|
124
215
|
if (findings.length)
|
|
125
|
-
|
|
216
|
+
writeJsonAtomic(findingsFile(dataPath), [...old, ...findings.map((f) => ({ ...f, at: new Date().toISOString() }))]);
|
|
126
217
|
return findings;
|
|
127
218
|
};
|
|
128
|
-
export const repoFindings = (dataPath) =>
|
|
219
|
+
export const repoFindings = (dataPath) => readValid(z.array(Finding).max(10000), findingsFile(dataPath)) ?? [];
|
package/dist/host/terminal.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { githubAsset, installArchive } from '../shared/download.js';
|
|
6
6
|
import { safeRemove } from '../shared/safe-remove.js';
|
|
7
|
+
import { assertTestWritable } from './test-guard.js';
|
|
7
8
|
import { z } from 'zod';
|
|
8
9
|
import { assertNoLinks, errMsg, fileExists, parseJsonc, writeJson } from '../core/util.js';
|
|
9
10
|
export const terminalDir = (dataPath) => path.join(dataPath, 'tools', 'terminal');
|
|
@@ -55,6 +56,7 @@ export const withPwshProfile = (settings, visible) => {
|
|
|
55
56
|
*/
|
|
56
57
|
export const syncPwshProfile = (dataPath, allowed) => {
|
|
57
58
|
const file = path.join(terminalDir(dataPath), 'settings', 'settings.json');
|
|
59
|
+
assertTestWritable(file);
|
|
58
60
|
assertNoLinks(dataPath, file);
|
|
59
61
|
if (!fileExists(file) || fs.statSync(file).size > MAX_SETTINGS_BYTES)
|
|
60
62
|
return;
|
|
@@ -89,6 +91,7 @@ export const ensureTerminal = async (dataPath, refresh, options = {}) => {
|
|
|
89
91
|
const installTerminal = async (dataPath, refresh) => {
|
|
90
92
|
const exe = terminalExe(dataPath);
|
|
91
93
|
const dir = terminalDir(dataPath);
|
|
94
|
+
assertTestWritable(dir);
|
|
92
95
|
const settings = path.join(dir, 'settings');
|
|
93
96
|
// installArchive replaces tools\terminal: the portable settings folder waits here meanwhile (and goes back also when
|
|
94
97
|
// the download fails). One still parked from an interrupted earlier run is the one to keep.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { selfTestRunning } from '../shared/test-guard.js';
|
|
2
|
+
export { selfTestRunning };
|
|
3
|
+
/** Refuses an action that reaches outside the test's own temp folder (a no-op outside the self-test). */
|
|
4
|
+
export declare const refuseInSelfTest: (what: string) => void;
|
|
5
|
+
/** A write, copy or delete on the host: during the self-test only the temp folder is allowed. */
|
|
6
|
+
export declare const assertTestWritable: (target: string) => void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Host side of the self-test guard (see shared/test-guard.ts): outward actions refuse, with a message the user can read.
|
|
2
|
+
import { testWriteRefusal, selfTestRunning } from '../shared/test-guard.js';
|
|
3
|
+
import { fail } from '../core/util.js';
|
|
4
|
+
export { selfTestRunning };
|
|
5
|
+
/** Refuses an action that reaches outside the test's own temp folder (a no-op outside the self-test). */
|
|
6
|
+
export const refuseInSelfTest = (what) => {
|
|
7
|
+
if (selfTestRunning())
|
|
8
|
+
fail('The self-test does not ' + what + '.', 'It only works in temp folders, so it can never change this PC.');
|
|
9
|
+
};
|
|
10
|
+
/** A write, copy or delete on the host: during the self-test only the temp folder is allowed. */
|
|
11
|
+
export const assertTestWritable = (target) => {
|
|
12
|
+
const refusal = testWriteRefusal(target);
|
|
13
|
+
if (refusal)
|
|
14
|
+
fail(refusal);
|
|
15
|
+
};
|
package/dist/host/window.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Headless mode: Windows Sandbox always opens its client window, so we hide it (ShowWindow SW_HIDE) as soon as it
|
|
2
2
|
// appears. The VM and the logged-in session keep running; `sbx-show` brings the window back.
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { writeJsonAtomic } from '../shared/rundir.js';
|
|
5
|
+
import { refuseInSelfTest } from './test-guard.js';
|
|
6
|
+
import { isWin, runPowerShell, runPowerShellAsync, tryReadJson } from '../core/util.js';
|
|
5
7
|
const PRELUDE = `
|
|
6
8
|
$ErrorActionPreference = 'Stop'
|
|
7
9
|
Add-Type -Namespace AgentSandbox -Name Win -MemberDefinition @'
|
|
@@ -34,6 +36,7 @@ function Get-SandboxWindows([bool]$visibleOnly) {
|
|
|
34
36
|
const stateFile = (storageRoot) => path.join(storageRoot, 'run', 'hidden-windows.json');
|
|
35
37
|
/** Waits for the sandbox window and hides it; keeps watching briefly in case Windows re-shows it while connecting. */
|
|
36
38
|
export const hideSandboxWindow = async (storageRoot, waitSeconds = 180) => {
|
|
39
|
+
refuseInSelfTest('hide windows');
|
|
37
40
|
if (!isWin)
|
|
38
41
|
return false;
|
|
39
42
|
const script = PRELUDE + `
|
|
@@ -48,11 +51,12 @@ while ((Get-Date) -lt $deadline) {
|
|
|
48
51
|
ConvertTo-Json -Compress @($hidden)`;
|
|
49
52
|
const stdout = await runPowerShellAsync(script);
|
|
50
53
|
const handles = JSON.parse(stdout.trim() || '[]').filter((h) => Number.isSafeInteger(h));
|
|
51
|
-
|
|
54
|
+
writeJsonAtomic(stateFile(storageRoot), { handles });
|
|
52
55
|
return handles.length > 0;
|
|
53
56
|
};
|
|
54
57
|
/** Shows the sandbox window again (the ones we hid, or any hidden Windows Sandbox window). */
|
|
55
58
|
export const showSandboxWindow = (storageRoot) => {
|
|
59
|
+
refuseInSelfTest('show windows');
|
|
56
60
|
if (!isWin)
|
|
57
61
|
return false;
|
|
58
62
|
// Only integers go into the script, whatever the file contains.
|
|
@@ -63,6 +67,6 @@ if (-not $handles) { $handles = Get-SandboxWindows $false }
|
|
|
63
67
|
foreach ($h in $handles) { [void][AgentSandbox.Win]::ShowWindow([IntPtr]$h, 5); [void][AgentSandbox.Win]::ShowWindow([IntPtr]$h, 9); [void][AgentSandbox.Win]::SetForegroundWindow([IntPtr]$h) }
|
|
64
68
|
@($handles).Count`;
|
|
65
69
|
const { stdout } = runPowerShell(script, true);
|
|
66
|
-
|
|
70
|
+
writeJsonAtomic(stateFile(storageRoot), { handles: [] });
|
|
67
71
|
return Number(stdout.trim()) > 0;
|
|
68
72
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Where wsb.exe can be: the Store app's alias, then System32 (never by bare name; cwd is often the workspace). */
|
|
2
|
+
export declare const wsbCandidates: (env?: NodeJS.ProcessEnv) => string[];
|
|
3
|
+
export declare const findWsb: (env?: NodeJS.ProcessEnv, exists?: (p: string) => boolean) => string | null;
|
|
4
|
+
/** The version from `wsb --version` ("0.1.32.0", "wsb 1.2.3+abc"). */
|
|
5
|
+
export declare const parseVersion: (out: string) => string | null;
|
|
6
|
+
/** Subcommand names from `wsb --help` (the lines under "Commands:"). */
|
|
7
|
+
export declare const parseCommands: (help: string) => string[];
|
|
8
|
+
export interface WsbSandbox {
|
|
9
|
+
id: string;
|
|
10
|
+
status: string;
|
|
11
|
+
}
|
|
12
|
+
/** `wsb list --raw` (JSON: an array, or an object holding one) or the plain table: the sandboxes and their status. */
|
|
13
|
+
export declare const parseList: (out: string) => WsbSandbox[];
|
|
14
|
+
/** `wsb ip --id <id>`: the first IPv4 address in the output. */
|
|
15
|
+
export declare const parseIp: (out: string) => string | null;
|
|
16
|
+
export interface WsbResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface Wsb {
|
|
23
|
+
exe: string;
|
|
24
|
+
version: string | null;
|
|
25
|
+
commands: string[];
|
|
26
|
+
has: (command: string) => boolean;
|
|
27
|
+
}
|
|
28
|
+
/** The CLI and what it offers, or null on Windows versions without it. */
|
|
29
|
+
export declare const detectWsb: (env?: NodeJS.ProcessEnv) => Wsb | null;
|
|
30
|
+
/** The running sandbox's id: the only running one (with several, none is guessed). */
|
|
31
|
+
export declare const runningSandboxId: (wsb: Wsb) => {
|
|
32
|
+
id: string | null;
|
|
33
|
+
error?: string;
|
|
34
|
+
};
|
|
35
|
+
/** Shares a folder of this PC into the running sandbox at `sandboxPath` (read/write unless readOnly). */
|
|
36
|
+
export declare const shareFolder: (wsb: Wsb, id: string, hostPath: string, sandboxPath: string, readOnly?: boolean) => WsbResult;
|
|
37
|
+
export declare const sandboxIp: (wsb: Wsb, id: string) => string | null;
|
package/dist/host/wsb.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// The Windows Sandbox command line (wsb.exe), Windows 11 24H2+ with the updated Windows Sandbox. Used only where it
|
|
2
|
+
// adds something the .wsb file cannot: `wsb share` puts a folder into the RUNNING sandbox. Launching always stays on
|
|
3
|
+
// the .wsb path, which works on every Windows version. UNTESTED: the machines this was built on predate the CLI, so
|
|
4
|
+
// every call is checked and any failure falls back (the caller offers a restart instead), with the error shown.
|
|
5
|
+
// Syntax from Microsoft's docs (learn.microsoft.com, "Windows Sandbox command line"):
|
|
6
|
+
// wsb start [--id <id>] [-c|--config <xml>] wsb list [--raw] wsb ip --id <id> wsb stop --id <id>
|
|
7
|
+
// wsb share --id <id> -f|--host-path <host> -s|--sandbox-path <sandbox> [-w|--allow-write]
|
|
8
|
+
// wsb exec --id <id> -c <command> -r System|ExistingLogin [-d <dir>] wsb connect --id <id> --raw = JSON output
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { refuseInSelfTest } from './test-guard.js';
|
|
13
|
+
const GUID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
|
|
14
|
+
const MAX_OUTPUT = 1024 * 1024;
|
|
15
|
+
/** Where wsb.exe can be: the Store app's alias, then System32 (never by bare name; cwd is often the workspace). */
|
|
16
|
+
export const wsbCandidates = (env = process.env) => [
|
|
17
|
+
env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps', 'wsb.exe') : '',
|
|
18
|
+
path.join(env.SystemRoot ?? env.WINDIR ?? 'C:\\Windows', 'System32', 'wsb.exe'),
|
|
19
|
+
].filter(Boolean);
|
|
20
|
+
export const findWsb = (env = process.env, exists = fs.existsSync) => process.platform === 'win32' || exists !== fs.existsSync ? wsbCandidates(env).find((p) => exists(p)) ?? null : null;
|
|
21
|
+
// ---- parsing (pure; the tests feed it recorded output) ---------------------------------------------------------
|
|
22
|
+
/** The version from `wsb --version` ("0.1.32.0", "wsb 1.2.3+abc"). */
|
|
23
|
+
export const parseVersion = (out) => /\d+\.\d+(?:\.\d+){0,2}/.exec(out)?.[0] ?? null;
|
|
24
|
+
/** Subcommand names from `wsb --help` (the lines under "Commands:"). */
|
|
25
|
+
export const parseCommands = (help) => {
|
|
26
|
+
const lines = help.split(/\r?\n/);
|
|
27
|
+
const start = lines.findIndex((l) => /^\s*commands:\s*$/i.test(l));
|
|
28
|
+
if (start < 0)
|
|
29
|
+
return [];
|
|
30
|
+
const names = [];
|
|
31
|
+
for (const line of lines.slice(start + 1)) {
|
|
32
|
+
if (/^\S/.test(line) && line.trim())
|
|
33
|
+
break;
|
|
34
|
+
const m = /^\s{1,8}([a-z][a-z-]*)\b/.exec(line);
|
|
35
|
+
if (m)
|
|
36
|
+
names.push(m[1].toLowerCase());
|
|
37
|
+
}
|
|
38
|
+
return names;
|
|
39
|
+
};
|
|
40
|
+
/** `wsb list --raw` (JSON: an array, or an object holding one) or the plain table: the sandboxes and their status. */
|
|
41
|
+
export const parseList = (out) => {
|
|
42
|
+
const fromObject = (o) => {
|
|
43
|
+
if (!o || typeof o !== 'object')
|
|
44
|
+
return null;
|
|
45
|
+
const entries = Object.entries(o);
|
|
46
|
+
const id = entries.find(([k, v]) => /^id$/i.test(k) && typeof v === 'string' && GUID.test(v))?.[1];
|
|
47
|
+
const status = entries.find(([k]) => /^(status|state)$/i.test(k))?.[1];
|
|
48
|
+
return id ? { id: GUID.exec(id)[0], status: typeof status === 'string' ? status : 'unknown' } : null;
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(out);
|
|
52
|
+
const list = Array.isArray(parsed) ? parsed : Object.values(parsed).find(Array.isArray) ?? [parsed];
|
|
53
|
+
return list.map(fromObject).filter((s) => s !== null);
|
|
54
|
+
}
|
|
55
|
+
catch { /* the table */ }
|
|
56
|
+
return out.split(/\r?\n/).flatMap((line) => {
|
|
57
|
+
const id = GUID.exec(line)?.[0];
|
|
58
|
+
if (!id)
|
|
59
|
+
return [];
|
|
60
|
+
return [{ id, status: /\brunning\b/i.test(line) ? 'Running' : /\bstopped\b/i.test(line) ? 'Stopped' : 'unknown' }];
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
/** `wsb ip --id <id>`: the first IPv4 address in the output. */
|
|
64
|
+
export const parseIp = (out) => /\b(?:\d{1,3}\.){3}\d{1,3}\b/.exec(out)?.[0] ?? null;
|
|
65
|
+
const call = (exe, args, timeoutMs = 60000) => {
|
|
66
|
+
const r = spawnSync(exe, args, { encoding: 'utf8', windowsHide: true, timeout: timeoutMs, maxBuffer: MAX_OUTPUT });
|
|
67
|
+
const stdout = String(r.stdout ?? '').slice(0, MAX_OUTPUT);
|
|
68
|
+
const stderr = String(r.stderr ?? '').slice(0, MAX_OUTPUT);
|
|
69
|
+
if (r.error)
|
|
70
|
+
return { ok: false, stdout, stderr, error: r.error.message };
|
|
71
|
+
return { ok: r.status === 0, stdout, stderr, ...(r.status === 0 ? {} : { error: (stderr || stdout).trim().split(/\r?\n/).slice(-3).join(' ') || 'exit code ' + r.status }) };
|
|
72
|
+
};
|
|
73
|
+
/** The CLI and what it offers, or null on Windows versions without it. */
|
|
74
|
+
export const detectWsb = (env = process.env) => {
|
|
75
|
+
const exe = findWsb(env);
|
|
76
|
+
if (!exe)
|
|
77
|
+
return null;
|
|
78
|
+
const version = parseVersion(call(exe, ['--version'], 15000).stdout);
|
|
79
|
+
const help = call(exe, ['--help'], 15000);
|
|
80
|
+
const commands = parseCommands(help.stdout + '\n' + help.stderr);
|
|
81
|
+
// An unreadable help text is not proof of anything: assume the documented commands.
|
|
82
|
+
const known = commands.length ? commands : ['start', 'list', 'exec', 'stop', 'share', 'connect', 'ip'];
|
|
83
|
+
return { exe, version, commands: known, has: (c) => known.includes(c) };
|
|
84
|
+
};
|
|
85
|
+
/** The running sandbox's id: the only running one (with several, none is guessed). */
|
|
86
|
+
export const runningSandboxId = (wsb) => {
|
|
87
|
+
const listed = call(wsb.exe, ['list', '--raw'], 20000);
|
|
88
|
+
const all = parseList(listed.ok ? listed.stdout : call(wsb.exe, ['list'], 20000).stdout);
|
|
89
|
+
const running = all.filter((s) => !/stopped/i.test(s.status));
|
|
90
|
+
if (running.length === 1)
|
|
91
|
+
return { id: running[0].id };
|
|
92
|
+
return { id: null, error: running.length ? running.length + ' sandboxes are running (wsb list); cannot tell which one is ours' : 'wsb list shows no running sandbox (one started from a .wsb file may not be listed)' };
|
|
93
|
+
};
|
|
94
|
+
/** Shares a folder of this PC into the running sandbox at `sandboxPath` (read/write unless readOnly). */
|
|
95
|
+
export const shareFolder = (wsb, id, hostPath, sandboxPath, readOnly = false) => {
|
|
96
|
+
refuseInSelfTest('share a folder into Windows Sandbox');
|
|
97
|
+
if (!GUID.test(id))
|
|
98
|
+
return { ok: false, stdout: '', stderr: '', error: 'not a sandbox id: ' + id };
|
|
99
|
+
return call(wsb.exe, ['share', '--id', id, '-f', hostPath, '-s', sandboxPath, ...(readOnly ? [] : ['--allow-write'])]);
|
|
100
|
+
};
|
|
101
|
+
export const sandboxIp = (wsb, id) => (GUID.test(id) ? parseIp(call(wsb.exe, ['ip', '--id', id], 15000).stdout) : null);
|