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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// `sandbox repair`: finds and fixes common problems. Each check (checks.ts) either confirms all is well (OK), fixes the
|
|
2
|
+
// problem itself (FIXED), or says what you need to do (NEEDS YOU). Toolchain and run-state fixes wait until no
|
|
3
|
+
// sandbox is running.
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import _ from 'lodash';
|
|
6
|
+
import { DEFAULTS, loadConfig, userDir } from '../../../core/config.js';
|
|
7
|
+
import { errMsg, expandPath, UserError } from '../../../core/util.js';
|
|
8
|
+
import { isSandboxRunning } from '../../../host/platform.js';
|
|
9
|
+
import { createContext, printJson, table } from '../context.js';
|
|
10
|
+
import { CHECKS } from './checks.js';
|
|
11
|
+
const LABEL = { ok: 'OK', fixed: 'FIXED', 'needs-you': 'NEEDS YOU', warn: 'WARNING', skipped: 'SKIPPED' };
|
|
12
|
+
/** Runs every check in order; a check that throws is reported, never stops the rest. */
|
|
13
|
+
export const runChecks = (env, checks = CHECKS) => checks.map((check) => {
|
|
14
|
+
try {
|
|
15
|
+
return { name: check.name, ...check.run(env) };
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
const fix = e instanceof UserError && e.fix ? ' ' + e.fix : '';
|
|
19
|
+
return { name: check.name, status: 'needs-you', detail: 'check failed: ' + errMsg(e) + fix };
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
export const repair = (ctx, configError = null) => {
|
|
23
|
+
const env = { ctx, running: isSandboxRunning(), dataPath: ctx.dataPath, storageRoot: ctx.storageRoot, toolsDir: path.join(ctx.dataPath, 'tools'), configError };
|
|
24
|
+
// Re-check the config file even when the context loaded (it may have been edited since, or come from --config).
|
|
25
|
+
if (!env.configError) {
|
|
26
|
+
try {
|
|
27
|
+
loadConfig(ctx.options.config);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
if (e instanceof UserError)
|
|
31
|
+
env.configError = e;
|
|
32
|
+
else
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const results = runChecks(env);
|
|
37
|
+
if (printJson(ctx, results))
|
|
38
|
+
return results.some((r) => r.status === 'needs-you') ? 1 : 0;
|
|
39
|
+
table(results.map((r) => [LABEL[r.status], r.name, r.detail]));
|
|
40
|
+
const counts = _.countBy(results, 'status');
|
|
41
|
+
console.log('\n' + (counts['needs-you'] ? counts['needs-you'] + ' thing(s) need you (see NEEDS YOU above).' : 'All good' + (counts.fixed ? ': fixed ' + counts.fixed + ' problem(s).' : '.')));
|
|
42
|
+
return counts['needs-you'] ? 1 : 0;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Entry point that works even when the config file is broken (createContext would throw): falls back to the built-in
|
|
46
|
+
* defaults so the other checks still run, and reports the config problem as the first check.
|
|
47
|
+
*/
|
|
48
|
+
export const repairCommand = (packageRoot, entry, options) => {
|
|
49
|
+
let ctx;
|
|
50
|
+
try {
|
|
51
|
+
ctx = createContext(packageRoot, entry, options);
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
if (!(e instanceof UserError))
|
|
55
|
+
throw e;
|
|
56
|
+
const loaded = { config: structuredClone(DEFAULTS), configPath: null, base: userDir() };
|
|
57
|
+
const storageRoot = expandPath(loaded.config.storage.root, loaded.base);
|
|
58
|
+
return repair({ packageRoot, entry, options, loaded, storageRoot, dataPath: path.join(storageRoot, 'data', loaded.config.storage.dataName) }, e);
|
|
59
|
+
}
|
|
60
|
+
return repair(ctx);
|
|
61
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { UserError } from '../../../core/util.js';
|
|
2
|
+
import type { Context } from '../context.js';
|
|
3
|
+
export type RepairStatus = 'ok' | 'fixed' | 'needs-you' | 'warn' | 'skipped';
|
|
4
|
+
export interface RepairResult {
|
|
5
|
+
name: string;
|
|
6
|
+
status: RepairStatus;
|
|
7
|
+
detail: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RepairEnv {
|
|
10
|
+
ctx: Context;
|
|
11
|
+
running: boolean;
|
|
12
|
+
dataPath: string;
|
|
13
|
+
storageRoot: string;
|
|
14
|
+
toolsDir: string;
|
|
15
|
+
configError: UserError | null;
|
|
16
|
+
}
|
|
17
|
+
export interface RepairCheck {
|
|
18
|
+
name: string;
|
|
19
|
+
run: (env: RepairEnv) => Omit<RepairResult, 'name'>;
|
|
20
|
+
}
|
|
21
|
+
export declare const ok: (detail: string) => Omit<RepairResult, "name">;
|
|
22
|
+
/** For fixes that must not touch folders a running sandbox may be using. */
|
|
23
|
+
export declare const whileRunning: {
|
|
24
|
+
status: "skipped";
|
|
25
|
+
detail: string;
|
|
26
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export const ok = (detail) => ({ status: 'ok', detail });
|
|
2
|
+
/** For fixes that must not touch folders a running sandbox may be using. */
|
|
3
|
+
export const whileRunning = { status: 'skipped', detail: 'a sandbox is running and may be using it; run repair again once it is closed' };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SessionStatus } from '../../core/types.js';
|
|
2
|
+
import { type NeedsRestart } from '../../host/projects.js';
|
|
3
|
+
import { type Context } from './context.js';
|
|
4
|
+
/** "2 sessions (claude in api, codex in web)". */
|
|
5
|
+
export declare const describeSessions: (sessions: Pick<SessionStatus, "harness" | "cwd" | "project">[]) => string;
|
|
6
|
+
export declare const offerRestart: (ctx: Context, needs: NeedsRestart[], then: () => Promise<number>) => Promise<number>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The last way in for a folder the running sandbox cannot show (not in a project root, no `wsb share`): restart it
|
|
2
|
+
// with that folder mapped as well. Only ever with your consent (a prompt, or --yes): it lists what will end, stops
|
|
3
|
+
// the sandbox cleanly (history and claude-mem sync back as on any close), starts it again from the same launch
|
|
4
|
+
// folder with the projects it had plus the new one, reopens the sessions it ended (continuing their conversations
|
|
5
|
+
// where the agent supports it) and then starts the new project's session.
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import _ from 'lodash';
|
|
8
|
+
import { askYesNo } from '../../core/prompt.js';
|
|
9
|
+
import { fail, safeText, sleep } from '../../core/util.js';
|
|
10
|
+
import { currentSessions, execInSandbox } from '../../host/channel.js';
|
|
11
|
+
import { isSandboxRunning } from '../../host/platform.js';
|
|
12
|
+
import { readRegistry } from '../../host/projects.js';
|
|
13
|
+
import { hostDirFor, projectForGuestDir } from '../../shared/projects.js';
|
|
14
|
+
import { readSandboxStatus } from '../../shared/rundir.js';
|
|
15
|
+
import { watcherRunning } from '../../sync/watcher.js';
|
|
16
|
+
import { c } from '../colors.js';
|
|
17
|
+
import { working } from '../progress.js';
|
|
18
|
+
import { sandboxAlive } from './context.js';
|
|
19
|
+
/** "2 sessions (claude in api, codex in web)". */
|
|
20
|
+
export const describeSessions = (sessions) => sessions.length === 0 ? 'no sessions' : sessions.length + ' session' + (sessions.length === 1 ? '' : 's') + ' (' + sessions.map((s) => safeText(s.harness) + ' in ' + safeText(s.project ?? path.win32.basename(s.cwd))).join(', ') + ')';
|
|
21
|
+
/** The command to run again with consent, for a non-interactive terminal. */
|
|
22
|
+
const againWithYes = (ctx) => 'sandbox ' + (ctx.options.noAgent ? 'start' : ctx.options.harness ?? ctx.loaded.config.defaultHarness) + (ctx.options.terminal ? ' -t' : '') + ' --yes';
|
|
23
|
+
/** Where each running session is on this PC, so it can be reopened after the restart. */
|
|
24
|
+
const sessionsToReopen = (registry, sessions) => sessions.flatMap((s) => {
|
|
25
|
+
const project = projectForGuestDir([...registry.projects, ...registry.roots.map((r) => ({ ...r }))], s.cwd);
|
|
26
|
+
return project ? [{ harness: s.harness, hostDir: hostDirFor(project, s.cwd) }] : [];
|
|
27
|
+
});
|
|
28
|
+
/** Shuts the sandbox down and waits until it (and the background sync's final pass) is gone. */
|
|
29
|
+
const stopAndWait = async (ctx) => {
|
|
30
|
+
const status = readSandboxStatus(ctx.dataPath);
|
|
31
|
+
await working('Stopping the sandbox (history syncs back)', async () => {
|
|
32
|
+
await execInSandbox(ctx.dataPath, ['C:\\Windows\\System32\\shutdown.exe', '/s', '/t', '0'], status?.guestWorkspace ?? 'C:\\', 30, sandboxAlive).catch(() => null);
|
|
33
|
+
for (let i = 0; i < 180 && isSandboxRunning(); i++)
|
|
34
|
+
await sleep(1000);
|
|
35
|
+
if (isSandboxRunning())
|
|
36
|
+
fail('The sandbox did not shut down within 3 minutes.', 'Close its window yourself, then run the command again.');
|
|
37
|
+
// The watcher syncs history back a few seconds after the window closes; the next start needs it gone.
|
|
38
|
+
for (let i = 0; i < 60 && watcherRunning(ctx.dataPath); i++)
|
|
39
|
+
await sleep(1000);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
export const offerRestart = async (ctx, needs, then) => {
|
|
43
|
+
const folders = needs.map((n) => n.hostDir);
|
|
44
|
+
const registry = readRegistry(ctx.storageRoot) ?? fail('The running sandbox was started by an older version, so it cannot take another folder.', 'Stop it (sandbox stop) and start it again from ' + folders[0] + '.');
|
|
45
|
+
const primary = registry.projects.find((p) => p.primary) ?? fail('The running sandbox has no launch folder on record.', 'Stop it (sandbox stop) and start it again.');
|
|
46
|
+
const sessions = currentSessions(ctx.dataPath);
|
|
47
|
+
for (const need of needs)
|
|
48
|
+
console.log(c.yellow('Windows Sandbox cannot add ' + need.hostDir + ' while it runs: ' + need.reason + '.'));
|
|
49
|
+
console.log('A restart maps ' + (needs.length === 1 ? 'it' : 'them') + ' as well (and keeps ' + registry.projects.map((p) => p.host).join(', ') + '). ' +
|
|
50
|
+
(sessions.length ? describeSessions(sessions) + ' will end' : 'No session is running') + '; your files, tools, databases and logins are kept. It takes about a minute.');
|
|
51
|
+
for (const parent of _.uniq(folders.map((f) => path.dirname(f))))
|
|
52
|
+
console.log(c.dim('No restart next time for folders in ' + parent + ': sandbox projects roots add "' + parent + '"'));
|
|
53
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
54
|
+
if (!ctx.options.yes) {
|
|
55
|
+
if (!interactive)
|
|
56
|
+
fail('Not restarting without your consent.', 'Run it again with --yes to restart: ' + againWithYes(ctx));
|
|
57
|
+
if (!(await askYesNo('Restart the sandbox now?'))) {
|
|
58
|
+
console.log('Nothing changed.');
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const reopen = sessionsToReopen(registry, sessions);
|
|
63
|
+
const resume = reopen.length > 0 && (ctx.options.yes || (await askYesNo('Reopen the ' + reopen.length + ' session(s) afterwards (continuing their conversations where the agent supports it)?', true)));
|
|
64
|
+
await stopAndWait(ctx);
|
|
65
|
+
// Imported here: launch.ts imports this file.
|
|
66
|
+
const { launch, openSession, readySandbox } = await import('./launch.js');
|
|
67
|
+
const { joinHere } = await import('./project.js');
|
|
68
|
+
const keep = registry.projects.filter((p) => !p.primary && p.mechanism !== 'root').map((p) => p.host);
|
|
69
|
+
const relaunch = { ...ctx, options: { ...ctx.options, workspace: primary.host, harness: ctx.options.harness ?? ctx.options.sessions[0]?.harness, terminal: false, noAgent: false, newSession: true, passthrough: [], sessions: [] } };
|
|
70
|
+
await launch(relaunch, false, { extraProjects: [...keep, ...folders], autoSession: false, waitUntilReady: true });
|
|
71
|
+
const ready = await readySandbox(ctx.dataPath);
|
|
72
|
+
if (resume) {
|
|
73
|
+
for (const s of reopen) {
|
|
74
|
+
const at = { ...ctx, options: { ...ctx.options, workspace: s.hostDir, harness: s.harness, terminal: false, newSession: true, passthrough: [], sessions: [] } };
|
|
75
|
+
try {
|
|
76
|
+
const joined = await joinHere(at, ready);
|
|
77
|
+
await openSession(at, ready, joined, { attach: false, harness: s.harness, args: ctx.loaded.config.harnesses[s.harness]?.resumeArguments ?? [], reuse: false });
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
console.warn(c.yellow('Could not reopen ' + s.harness + ' in ' + s.hostDir + ': ' + (e instanceof Error ? e.message : String(e))));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return then();
|
|
85
|
+
};
|
|
@@ -3,8 +3,9 @@ export declare const attach: (ctx: Context) => Promise<number>;
|
|
|
3
3
|
export declare const sessions: (ctx: Context) => Promise<number>;
|
|
4
4
|
export declare const status: (ctx: Context) => Promise<number>;
|
|
5
5
|
export declare const exec: (ctx: Context) => Promise<number>;
|
|
6
|
-
/** `sandbox db`: runs the sandbox's own `sbx-db
|
|
6
|
+
/** `sandbox db`: runs the sandbox's own `sbx-db` in this folder's project, which knows its database versions and ports. */
|
|
7
7
|
export declare const db: (ctx: Context) => Promise<number>;
|
|
8
|
+
/** Restarts this folder's project's sessions (--all: every session). */
|
|
8
9
|
export declare const restart: (ctx: Context) => Promise<number>;
|
|
9
10
|
export declare const stop: (ctx: Context) => Promise<number>;
|
|
10
11
|
export declare const show: (ctx: Context) => number;
|
|
@@ -1,110 +1,254 @@
|
|
|
1
|
-
// Working with a running sandbox: attach, sessions, status, exec, restart, stop, show/hide.
|
|
1
|
+
// Working with a running sandbox: attach, sessions, status, exec, restart, stop, show/hide. Each knows which project
|
|
2
|
+
// you run it from (project.ts): attach and sessions put that project's sessions first, exec and db run in it, restart
|
|
3
|
+
// restarts its sessions, stop warns about the other projects' sessions.
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { select } from '@inquirer/prompts';
|
|
2
7
|
import _ from 'lodash';
|
|
8
|
+
import { askYesNo } from '../../core/prompt.js';
|
|
3
9
|
import { activeAccount } from '../../host/accounts.js';
|
|
4
|
-
import { execInSandbox, pickSession, pingSandbox, restartSessions, currentSessions, runningSessions,
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
10
|
+
import { execInSandbox, pickSession, pingSandbox, restartSessions, currentSessions, runningSessions, sessionsFirstIn } from '../../host/channel.js';
|
|
11
|
+
import { sessionViewers } from '../../host/attach.js';
|
|
12
|
+
import { insideSandbox, isSandboxRunning } from '../../host/platform.js';
|
|
13
|
+
import { NeedsRestart, readRegistry } from '../../host/projects.js';
|
|
14
|
+
import { guardedProjects, repoFindings } from '../../host/repo-guard.js';
|
|
7
15
|
import { hideSandboxWindow, showSandboxWindow } from '../../host/window.js';
|
|
8
|
-
import {
|
|
16
|
+
import { isWithin, projectForGuestDir } from '../../shared/projects.js';
|
|
17
|
+
import { listProjectStatus, listSessions, readSandboxStatus } from '../../shared/rundir.js';
|
|
9
18
|
import { watcherRunning } from '../../sync/watcher.js';
|
|
10
19
|
import { errMsg, fail, safeText, sleep } from '../../core/util.js';
|
|
11
|
-
import {
|
|
12
|
-
import { attachHere } from './launch.js';
|
|
20
|
+
import { printJson, requireOnPc, requireRunning, sandboxAlive, table } from './context.js';
|
|
21
|
+
import { attachHere, readySandbox } from './launch.js';
|
|
22
|
+
import { hereDir, joinHere, locateHere, provisionJoined } from './project.js';
|
|
23
|
+
import { working } from '../progress.js';
|
|
24
|
+
import { ago, c, shortenMiddle, termWidth } from '../colors.js';
|
|
25
|
+
const viewerText = (v) => (v === undefined ? '-' : v === null ? 'unknown (older session)' : v.remote + ' here, ' + (v.viewers - v.remote) + ' in sandbox');
|
|
26
|
+
const projectOf = (projects, s) => s.project ?? projectForGuestDir(projects, s.cwd)?.name ?? '-';
|
|
13
27
|
export const attach = async (ctx) => {
|
|
28
|
+
requireOnPc('attach');
|
|
14
29
|
requireRunning('Start one with: sandbox claude --terminal');
|
|
15
|
-
await
|
|
30
|
+
const ready = await readySandbox(ctx.dataPath);
|
|
16
31
|
const id = ctx.options.positionals[0];
|
|
17
32
|
if (!id && runningSessions(ctx.dataPath).length === 0)
|
|
18
33
|
await sleep(1500);
|
|
19
|
-
|
|
34
|
+
const here = locateHere(ctx, ready);
|
|
35
|
+
if (id)
|
|
36
|
+
return attachHere(ctx, pickSession(ctx.dataPath, id));
|
|
37
|
+
// Several sessions: choose, this folder's project first (the newest is the default).
|
|
38
|
+
const running = sessionsFirstIn(runningSessions(ctx.dataPath), here.guestDir);
|
|
39
|
+
if (running.length > 1 && process.stdin.isTTY && process.stdout.isTTY) {
|
|
40
|
+
const projects = readRegistry(ctx.storageRoot)?.projects ?? [];
|
|
41
|
+
const chosen = await select({
|
|
42
|
+
message: 'Which session?' + (here.project ? ' (' + here.project.name + ' first)' : ''),
|
|
43
|
+
choices: running.map((s) => ({ value: s.id, name: (here.guestDir && isWithin(here.guestDir, s.cwd) ? '* ' : ' ') + safeText(s.harness).padEnd(9) + ' ' + projectOf(projects, s).padEnd(16) + ' ' + ago(s.startedAt).padEnd(11) + ' ' + s.id })),
|
|
44
|
+
});
|
|
45
|
+
return attachHere(ctx, pickSession(ctx.dataPath, chosen));
|
|
46
|
+
}
|
|
47
|
+
return attachHere(ctx, pickSession(ctx.dataPath, undefined, here.guestDir));
|
|
20
48
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const sessionRow = (s) => [s.id, safeText(s.harness), s.phase + (s.exitCode !== undefined ? ' (' + s.exitCode + ')' : ''), viewerText(s.viewers), safeText(s.cwd), safeText(new Date(s.startedAt).toLocaleTimeString())];
|
|
24
|
-
export const sessions = async (ctx) => {
|
|
49
|
+
/** Sessions with who is watching them; `here` marks the ones in the project of the current folder. */
|
|
50
|
+
const listedSessions = async (ctx) => {
|
|
25
51
|
// Who is attached comes from each session itself, so a terminal that was closed is never counted.
|
|
26
52
|
const live = isSandboxRunning();
|
|
27
53
|
// Only this sandbox's sessions are asked (and at most MAX_QUERIED_SESSIONS): the sandbox writes these files.
|
|
28
54
|
const ask = new Set(live ? currentSessions(ctx.dataPath).map((s) => s.id) : []);
|
|
29
|
-
const
|
|
55
|
+
const here = live ? locateHere(ctx, readSandboxStatus(ctx.dataPath)) : { guestDir: null };
|
|
56
|
+
const projects = readRegistry(ctx.storageRoot)?.projects ?? [];
|
|
57
|
+
return Promise.all(listSessions(ctx.dataPath).map(async (s) => ({
|
|
58
|
+
...s, projectName: projectOf(projects, s), here: Boolean(here.guestDir && isWithin(here.guestDir, s.cwd)),
|
|
59
|
+
...(ask.has(s.id) ? { viewers: await sessionViewers(s) } : {}),
|
|
60
|
+
})));
|
|
61
|
+
};
|
|
62
|
+
export const sessions = async (ctx) => {
|
|
63
|
+
const list = await listedSessions(ctx);
|
|
30
64
|
if (printJson(ctx, list.map((s) => _.omit(s, 'attach'))))
|
|
31
65
|
return 0;
|
|
32
|
-
if (list.length === 0)
|
|
33
|
-
console.log('No sessions.');
|
|
34
|
-
|
|
35
|
-
|
|
66
|
+
if (list.length === 0) {
|
|
67
|
+
console.log('No sessions. Start one with: sandbox claude');
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
// This folder's project first, newest first within each group; harness, cwd and times are written by the sandbox.
|
|
71
|
+
const ordered = [..._.sortBy(list.filter((s) => s.here), (s) => s.startedAt).reverse(), ..._.sortBy(list.filter((s) => !s.here), (s) => s.startedAt).reverse()];
|
|
72
|
+
const folderWidth = Math.max(16, termWidth() - 80);
|
|
73
|
+
table([['', 'ID', 'AGENT', 'PROJECT', 'STATE', 'ATTACHED', 'FOLDER', 'STARTED'], ...ordered.map((s) => [
|
|
74
|
+
s.here ? '*' : '', s.id, safeText(s.harness), safeText(s.projectName ?? '-'), s.phase + (s.exitCode !== undefined ? ' (' + s.exitCode + ')' : ''), viewerText(s.viewers), shortenMiddle(safeText(s.cwd), folderWidth), ago(s.startedAt),
|
|
75
|
+
])]);
|
|
76
|
+
if (list.some((s) => s.here))
|
|
77
|
+
console.log(c.dim('* = the project of this folder'));
|
|
36
78
|
return 0;
|
|
37
79
|
};
|
|
80
|
+
/** Running databases by port, from the sandbox's own `sbx-db list` (best effort: unknown when it does not answer). */
|
|
81
|
+
const runningPorts = async (ctx, cwd) => {
|
|
82
|
+
try {
|
|
83
|
+
const r = await execInSandbox(ctx.dataPath, ['sbx-db', 'list'], cwd, 30, sandboxAlive);
|
|
84
|
+
return new Set((r.stdout ?? '').split(/\r?\n/).filter((l) => /\brunning\b/.test(l)).map((l) => Number(/port (\d+)/.exec(l)?.[1])).filter((n) => n > 0));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
38
90
|
export const status = async (ctx) => {
|
|
39
91
|
const running = isSandboxRunning();
|
|
40
92
|
const sandbox = readSandboxStatus(ctx.dataPath);
|
|
93
|
+
const registry = running ? readRegistry(ctx.storageRoot) : null;
|
|
94
|
+
const ready = running && sandbox?.phase === 'ready';
|
|
95
|
+
const daemon = ready ? await pingSandbox(ctx.dataPath, sandboxAlive).then((r) => r.message, (e) => 'no answer (' + errMsg(e) + ')') : '-';
|
|
96
|
+
const here = running ? locateHere(ctx, sandbox) : { project: null, guestDir: null };
|
|
97
|
+
const sessionList = running ? await listedSessions(ctx) : [];
|
|
98
|
+
const provisioning = running ? listProjectStatus(ctx.dataPath) : [];
|
|
99
|
+
const ports = ready && sandbox ? await runningPorts(ctx, sandbox.guestWorkspace) : null;
|
|
41
100
|
const findings = repoFindings(ctx.dataPath);
|
|
42
|
-
const
|
|
101
|
+
const startedAt = Number(/^(\d+)-/.exec(registry?.launchId ?? sandbox?.launchId ?? '')?.[1]) || null;
|
|
102
|
+
const projects = (registry?.projects ?? []).map((p) => {
|
|
103
|
+
const prov = provisioning.find((s) => s.name.toLowerCase() === p.name.toLowerCase());
|
|
104
|
+
const own = sessionList.filter((s) => s.phase === 'running' && projectForGuestDir(registry?.projects ?? [], s.cwd)?.name === p.name);
|
|
105
|
+
return {
|
|
106
|
+
name: p.name, host: p.host, guest: p.guest, mechanism: p.mechanism, here: here.project?.name === p.name,
|
|
107
|
+
setup: prov ? { phase: prov.phase, message: safeText(prov.message) } : { phase: 'not set up yet', message: 'set up when you first start a session or command there' },
|
|
108
|
+
databases: (prov?.databases ?? []).map((d) => ({ database: safeText(d.database), port: d.port, state: ports ? (ports.has(d.port) ? 'running' : 'stopped') : 'unknown' })),
|
|
109
|
+
sessions: own.map((s) => ({ id: s.id, harness: safeText(s.harness), viewers: viewerText(s.viewers), started: s.startedAt })),
|
|
110
|
+
// With several guarded folders each finding starts with its folder.
|
|
111
|
+
repoGuard: findings.filter((f) => f.detail.toLowerCase().startsWith(p.host.toLowerCase() + ':')).map((f) => f.kind + ': ' + safeText(f.detail)),
|
|
112
|
+
};
|
|
113
|
+
});
|
|
43
114
|
const info = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
115
|
+
sandbox: {
|
|
116
|
+
running, phase: running ? sandbox?.phase ?? 'unknown' : 'stopped', message: sandbox?.message, headless: sandbox?.headless,
|
|
117
|
+
uptime: running && startedAt ? ago(startedAt).replace(' ago', '') : null, daemon, account: activeAccount(ctx.dataPath), syncWatcher: watcherRunning(ctx.dataPath),
|
|
118
|
+
},
|
|
119
|
+
here: { folder: hereDir(ctx), project: here.project?.name ?? null, guest: here.guestDir },
|
|
120
|
+
roots: registry?.roots ?? [],
|
|
121
|
+
projects,
|
|
122
|
+
otherSessions: sessionList.filter((s) => s.phase === 'running' && !projects.some((p) => p.sessions.some((x) => x.id === s.id))).map((s) => ({ id: s.id, harness: safeText(s.harness), cwd: safeText(s.cwd) })),
|
|
123
|
+
repoGuard: { guarded: guardedProjects(ctx.dataPath).length, findings: findings.map((f) => f.kind + ': ' + f.detail) },
|
|
47
124
|
};
|
|
48
|
-
if (
|
|
49
|
-
|
|
125
|
+
if (printJson(ctx, info))
|
|
126
|
+
return 0;
|
|
127
|
+
const yesNo = (v) => (typeof v === 'boolean' ? (v ? 'yes' : 'no') : String(v ?? '-'));
|
|
128
|
+
table(Object.entries(info.sandbox).map(([k, v]) => [_.startCase(k), safeText(yesNo(v))]));
|
|
129
|
+
if (!running)
|
|
130
|
+
return 0;
|
|
131
|
+
console.log('\n' + c.bold('Projects') + c.dim(' (* = this folder: ' + info.here.folder + (info.here.project ? '' : ', not in this sandbox') + ')'));
|
|
132
|
+
for (const p of projects) {
|
|
133
|
+
console.log((p.here ? '* ' : ' ') + c.bold(p.name) + ' ' + p.host + ' -> ' + p.guest + c.dim(' (' + (p.mechanism === 'launch' ? 'started from here' : p.mechanism === 'root' ? 'in a project root' : p.mechanism === 'boot' ? 'mapped at start' : 'wsb share') + ')'));
|
|
134
|
+
console.log(' setup: ' + p.setup.phase + (p.setup.phase === 'ready' ? '' : ' - ' + shortenMiddle(p.setup.message, termWidth() - 20)));
|
|
135
|
+
if (p.databases.length)
|
|
136
|
+
console.log(' databases: ' + p.databases.map((d) => d.database + ' :' + d.port + ' ' + d.state).join(', '));
|
|
137
|
+
for (const s of p.sessions)
|
|
138
|
+
console.log(' session ' + s.id + ' ' + s.harness + ' ' + s.viewers + ' ' + ago(s.started));
|
|
139
|
+
for (const f of p.repoGuard)
|
|
140
|
+
console.log(' ' + c.yellow('guard: ' + f));
|
|
141
|
+
}
|
|
142
|
+
for (const r of info.roots)
|
|
143
|
+
console.log(' root ' + r.host + ' -> ' + r.guest + c.dim(' (every project in it opens without a restart)'));
|
|
144
|
+
for (const s of info.otherSessions)
|
|
145
|
+
console.log(' session ' + s.id + ' ' + s.harness + ' ' + s.cwd);
|
|
146
|
+
console.log('\n' + c.bold('Repo guard') + ' ' + info.repoGuard.guarded + ' folder(s) guarded; ' + (findings.length ? c.yellow(findings.length + ' change(s) neutralised:') : 'nothing from the sandbox needed neutralising'));
|
|
147
|
+
for (const f of info.repoGuard.findings.slice(-10))
|
|
148
|
+
console.log(' ' + c.yellow(safeText(f)));
|
|
50
149
|
return 0;
|
|
51
150
|
};
|
|
151
|
+
/** What a command run in the sandbox printed (text from the sandbox: through safeText), or why it failed. */
|
|
152
|
+
const printExecResult = (result) => {
|
|
153
|
+
if (result.stdout)
|
|
154
|
+
process.stdout.write(safeText(result.stdout));
|
|
155
|
+
if (result.stderr)
|
|
156
|
+
process.stderr.write(safeText(result.stderr));
|
|
157
|
+
if (!result.ok && !result.stdout && !result.stderr)
|
|
158
|
+
console.error(result.message);
|
|
159
|
+
};
|
|
160
|
+
/** The guest folder for this folder, joining the running sandbox (and setting the project up) when needed. */
|
|
161
|
+
const guestFolderHere = async (ctx, what) => {
|
|
162
|
+
const ready = await readySandbox(ctx.dataPath);
|
|
163
|
+
try {
|
|
164
|
+
const joined = await joinHere(ctx, ready);
|
|
165
|
+
for (const note of joined.notes)
|
|
166
|
+
console.error(note);
|
|
167
|
+
await provisionJoined(ctx, joined);
|
|
168
|
+
return joined.guestDir;
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
if (!(e instanceof NeedsRestart))
|
|
172
|
+
throw e;
|
|
173
|
+
return fail(hereDir(ctx) + ' is not in the running sandbox (' + e.reason + '), so "sandbox ' + what + '" cannot run there.', 'sandbox claude (or sandbox start) from that folder offers a restart that adds it; or add its parent folder as a root: sandbox projects roots add "' + path.dirname(e.hostDir) + '"');
|
|
174
|
+
}
|
|
175
|
+
};
|
|
52
176
|
export const exec = async (ctx) => {
|
|
53
177
|
const command = [...ctx.options.positionals, ...ctx.options.passthrough];
|
|
54
178
|
if (command.length === 0)
|
|
55
179
|
fail('No command given.', 'Usage: sandbox exec [--timeout <s>] -- <command> [args...]');
|
|
180
|
+
requireOnPc('exec');
|
|
56
181
|
requireRunning();
|
|
57
|
-
const
|
|
58
|
-
const result = await execInSandbox(ctx.dataPath, command,
|
|
59
|
-
if (!printJson(ctx, result))
|
|
60
|
-
|
|
61
|
-
process.stdout.write(safeText(result.stdout));
|
|
62
|
-
if (result.stderr)
|
|
63
|
-
process.stderr.write(safeText(result.stderr));
|
|
64
|
-
if (!result.ok && !result.stdout && !result.stderr)
|
|
65
|
-
console.error(result.message);
|
|
66
|
-
}
|
|
182
|
+
const cwd = await guestFolderHere(ctx, 'exec');
|
|
183
|
+
const result = await execInSandbox(ctx.dataPath, command, cwd, ctx.options.timeout, sandboxAlive);
|
|
184
|
+
if (!printJson(ctx, result))
|
|
185
|
+
printExecResult(result);
|
|
67
186
|
return result.exitCode ?? 1;
|
|
68
187
|
};
|
|
69
|
-
/** `sandbox db`: runs the sandbox's own `sbx-db
|
|
188
|
+
/** `sandbox db`: runs the sandbox's own `sbx-db` in this folder's project, which knows its database versions and ports. */
|
|
70
189
|
export const db = async (ctx) => {
|
|
71
190
|
const [action = 'list', name] = ctx.options.positionals;
|
|
191
|
+
if (!['list', 'start', 'stop'].includes(action))
|
|
192
|
+
fail('Unknown db action "' + action + '".', 'Use: sandbox db (list) or sandbox db start|stop <postgres|mysql|mongodb|redis|dynamodb>');
|
|
72
193
|
if (action !== 'list' && !name)
|
|
73
194
|
fail('Which database?', 'Usage: sandbox db ' + action + ' <postgres|mysql|mongodb|redis|dynamodb>');
|
|
195
|
+
// Already inside the sandbox: run the database tool here instead of asking the sandbox from outside.
|
|
196
|
+
if (insideSandbox())
|
|
197
|
+
return spawnSync(process.execPath, [path.join(ctx.packageRoot, 'dist', 'runtime', 'db.js'), action, ...(name ? [name] : [])], { stdio: 'inherit', windowsHide: true }).status ?? 1;
|
|
74
198
|
requireRunning('Start one with: sandbox start --database <name>');
|
|
75
|
-
const
|
|
199
|
+
const cwd = await guestFolderHere(ctx, 'db');
|
|
76
200
|
// Starting can download the database the first time.
|
|
77
|
-
const result = await execInSandbox(ctx.dataPath, ['sbx-db', action, ...(name ? [name] : [])],
|
|
78
|
-
|
|
79
|
-
process.stdout.write(safeText(result.stdout));
|
|
80
|
-
if (result.stderr)
|
|
81
|
-
process.stderr.write(safeText(result.stderr));
|
|
82
|
-
if (!result.ok && !result.stdout && !result.stderr)
|
|
83
|
-
console.error(result.message);
|
|
201
|
+
const result = await working((action === 'list' ? 'Asking' : action === 'start' ? 'Starting' : 'Stopping') + ' the database' + (name ? ' ' + name : 's') + ' in the sandbox', () => execInSandbox(ctx.dataPath, ['sbx-db', action, ...(name ? [name] : [])], cwd, 1800, sandboxAlive));
|
|
202
|
+
printExecResult(result);
|
|
84
203
|
return result.exitCode ?? 1;
|
|
85
204
|
};
|
|
205
|
+
/** Restarts this folder's project's sessions (--all: every session). */
|
|
86
206
|
export const restart = async (ctx) => {
|
|
207
|
+
requireOnPc('restart');
|
|
87
208
|
requireRunning();
|
|
88
|
-
|
|
209
|
+
const here = locateHere(ctx, readSandboxStatus(ctx.dataPath));
|
|
210
|
+
const scope = ctx.options.all || !here.project ? undefined : here.project.guest;
|
|
211
|
+
if (!scope && !ctx.options.all)
|
|
212
|
+
console.log(c.dim(hereDir(ctx) + ' is not a project of the running sandbox: restarting every session.'));
|
|
213
|
+
console.log((await working('Restarting the agents' + (scope ? ' of ' + here.project?.name : ''), () => restartSessions(ctx.dataPath, ctx.options.harness ?? null, sandboxAlive, scope))).message);
|
|
89
214
|
return 0;
|
|
90
215
|
};
|
|
91
216
|
export const stop = async (ctx) => {
|
|
217
|
+
requireOnPc('stop');
|
|
92
218
|
if (!isSandboxRunning()) {
|
|
93
219
|
console.log('No Windows Sandbox is running.');
|
|
94
220
|
return 0;
|
|
95
221
|
}
|
|
96
222
|
const sandbox = readSandboxStatus(ctx.dataPath) ?? fail('This sandbox was not started by this tool.', 'Close its window instead.');
|
|
97
|
-
|
|
223
|
+
// Stopping ends every project's sessions, not just this folder's: say which, and ask when others would end too.
|
|
224
|
+
const here = locateHere(ctx, sandbox);
|
|
225
|
+
const running = currentSessions(ctx.dataPath);
|
|
226
|
+
const others = running.filter((s) => !(here.guestDir && isWithin(here.project?.guest ?? here.guestDir, s.cwd)));
|
|
227
|
+
if (running.length)
|
|
228
|
+
console.log('Stopping ends ' + running.length + ' session(s): ' + running.map((s) => safeText(s.harness) + ' in ' + safeText(s.project ?? path.win32.basename(s.cwd))).join(', ') + '.');
|
|
229
|
+
if (others.length && !ctx.options.yes) {
|
|
230
|
+
// A script must never end other projects' sessions by accident: without a terminal to ask in, it needs --yes.
|
|
231
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
232
|
+
if (!interactive)
|
|
233
|
+
fail(others.length + ' of these sessions belong to other projects, so the sandbox was not stopped.', 'Stop it anyway with: sandbox stop --yes');
|
|
234
|
+
if (!(await askYesNo(others.length + ' of them belong to other projects. Stop the whole sandbox?'))) {
|
|
235
|
+
console.log('Nothing changed.');
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
await working('Asking the sandbox to shut down', () => execInSandbox(ctx.dataPath, ['C:\\Windows\\System32\\shutdown.exe', '/s', '/t', '2'], sandbox.guestWorkspace, 30, sandboxAlive));
|
|
98
240
|
console.log('Sandbox is shutting down; history and claude-mem sync back when it closes.');
|
|
99
241
|
return 0;
|
|
100
242
|
};
|
|
101
243
|
export const show = (ctx) => {
|
|
244
|
+
requireOnPc('show');
|
|
102
245
|
requireRunning();
|
|
103
246
|
if (!showSandboxWindow(ctx.storageRoot))
|
|
104
247
|
fail('Could not find the Windows Sandbox window.', 'It may still be starting; try again in a few seconds.');
|
|
105
248
|
return 0;
|
|
106
249
|
};
|
|
107
250
|
export const hide = async (ctx) => {
|
|
251
|
+
requireOnPc('hide');
|
|
108
252
|
requireRunning();
|
|
109
253
|
if (!(await hideSandboxWindow(ctx.storageRoot, 5)))
|
|
110
254
|
fail('Could not find a visible Windows Sandbox window.', 'It may already be hidden (sbx-show brings it back).');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type EditionSupport, type MachineFacts } from './facts.js';
|
|
2
|
+
export type CheckStatus = 'ok' | 'warn' | 'action';
|
|
3
|
+
export interface Check {
|
|
4
|
+
name: string;
|
|
5
|
+
status: CheckStatus;
|
|
6
|
+
detail: string;
|
|
7
|
+
}
|
|
8
|
+
/** Windows Sandbox needs Pro, Enterprise or Education (build 18305+); Home ("Core*") and Server cannot run it. */
|
|
9
|
+
export declare const editionSupport: (edition: string | null) => EditionSupport;
|
|
10
|
+
export declare const nodeMajor: (version: string) => number;
|
|
11
|
+
/** Offer to enable the feature only when it is off and this edition can have it. */
|
|
12
|
+
export declare const canEnableFeature: (f: MachineFacts) => boolean;
|
|
13
|
+
export declare const buildChecks: (f: MachineFacts) => Check[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Turns what `sandbox setup` learnt about the PC into a checklist. Pure, so it can be tested.
|
|
2
|
+
import { GB } from '../../../core/disk.js';
|
|
3
|
+
import { ENABLE_COMMAND } from './facts.js';
|
|
4
|
+
/** Windows Sandbox needs Pro, Enterprise or Education (build 18305+); Home ("Core*") and Server cannot run it. */
|
|
5
|
+
export const editionSupport = (edition) => {
|
|
6
|
+
if (!edition)
|
|
7
|
+
return 'unknown';
|
|
8
|
+
if (/^Core/i.test(edition))
|
|
9
|
+
return 'home';
|
|
10
|
+
if (/^Server/i.test(edition))
|
|
11
|
+
return 'unsupported';
|
|
12
|
+
return /^(Professional|Enterprise|Education|IoTEnterprise)/i.test(edition) ? 'supported' : 'unknown';
|
|
13
|
+
};
|
|
14
|
+
export const nodeMajor = (version) => Number(/^v?(\d+)/.exec(version.trim())?.[1] ?? 0);
|
|
15
|
+
/** Offer to enable the feature only when it is off and this edition can have it. */
|
|
16
|
+
export const canEnableFeature = (f) => f.feature !== 'enabled' && f.feature !== 'pending' && !f.sandboxExe && ['supported', 'unknown'].includes(editionSupport(f.edition));
|
|
17
|
+
const formatGb = (bytes) => (bytes / GB).toFixed(1) + ' GB';
|
|
18
|
+
const check = (name, status, detail) => ({ name, status, detail });
|
|
19
|
+
const editionCheck = (f) => {
|
|
20
|
+
const support = editionSupport(f.edition);
|
|
21
|
+
const edition = (f.edition ?? 'unknown edition') + (f.build ? ', build ' + f.build : '');
|
|
22
|
+
const name = 'Windows edition';
|
|
23
|
+
if (support === 'supported' && (f.build ?? 18305) >= 18305)
|
|
24
|
+
return check(name, 'ok', edition);
|
|
25
|
+
if (support === 'home')
|
|
26
|
+
return check(name, 'action', edition + ': Windows Home cannot run Windows Sandbox. Upgrade to Pro (Settings > System > Activation).');
|
|
27
|
+
if (support === 'unsupported')
|
|
28
|
+
return check(name, 'action', edition + ': Windows Sandbox needs Windows 10/11 Pro, Enterprise or Education.');
|
|
29
|
+
if (support === 'supported')
|
|
30
|
+
return check(name, 'action', edition + ': too old; Windows Sandbox needs Windows 10 version 1903 or newer.');
|
|
31
|
+
return check(name, 'warn', edition + ': not sure this edition has Windows Sandbox (it needs Pro, Enterprise or Education).');
|
|
32
|
+
};
|
|
33
|
+
const virtualizationCheck = (f) => {
|
|
34
|
+
const name = 'CPU virtualization';
|
|
35
|
+
const virt = f.hypervisorPresent || f.virtualizationFirmware;
|
|
36
|
+
if (virt)
|
|
37
|
+
return check(name, 'ok', f.hypervisorPresent ? 'on (hypervisor running)' : 'on');
|
|
38
|
+
if (virt === false)
|
|
39
|
+
return check(name, 'action', 'off. Turn on Intel VT-x / AMD-V (SVM) in your PC\'s BIOS/UEFI settings, then run this again.');
|
|
40
|
+
return check(name, 'warn', 'could not tell; if Windows Sandbox will not start, turn on VT-x / AMD-V in the BIOS/UEFI.');
|
|
41
|
+
};
|
|
42
|
+
const featureCheck = (f) => {
|
|
43
|
+
const name = 'Windows Sandbox feature';
|
|
44
|
+
if (f.feature === 'enabled' && f.sandboxExe)
|
|
45
|
+
return check(name, 'ok', 'enabled');
|
|
46
|
+
if (f.feature === 'pending' || f.feature === 'enabled')
|
|
47
|
+
return check(name, 'action', 'enabled, but Windows needs a restart to finish.');
|
|
48
|
+
if (f.sandboxExe)
|
|
49
|
+
return check(name, 'ok', 'installed');
|
|
50
|
+
return check(name, 'action', 'not enabled. In an admin PowerShell: ' + ENABLE_COMMAND + ', then restart.');
|
|
51
|
+
};
|
|
52
|
+
const memoryCheck = (f) => {
|
|
53
|
+
const gb = formatGb(f.totalRamBytes);
|
|
54
|
+
if (f.totalRamBytes < 4 * GB)
|
|
55
|
+
return check('Memory', 'action', gb + ': Windows Sandbox needs at least 4 GB.');
|
|
56
|
+
if (f.totalRamBytes < 8 * GB)
|
|
57
|
+
return check('Memory', 'warn', gb + ': works, but 8 GB or more is recommended.');
|
|
58
|
+
return check('Memory', 'ok', gb);
|
|
59
|
+
};
|
|
60
|
+
const diskCheck = (f) => {
|
|
61
|
+
if (f.freeDiskBytes === null)
|
|
62
|
+
return check('Free disk', 'warn', 'could not check the drive of ' + f.storageRoot);
|
|
63
|
+
const free = formatGb(f.freeDiskBytes) + ' free for ' + f.storageRoot;
|
|
64
|
+
if (f.freeDiskBytes < 5 * GB)
|
|
65
|
+
return check('Free disk', 'action', free + ': free up space (toolchains need several GB).');
|
|
66
|
+
if (f.freeDiskBytes < 20 * GB)
|
|
67
|
+
return check('Free disk', 'warn', free + ': getting low; toolchains and caches can use several GB.');
|
|
68
|
+
return check('Free disk', 'ok', free);
|
|
69
|
+
};
|
|
70
|
+
const nodeCheck = (f) => nodeMajor(f.nodeVersion) >= 20 ? check('Node.js', 'ok', f.nodeVersion) : check('Node.js', 'action', f.nodeVersion + ': needs 20 or newer (winget install OpenJS.NodeJS.LTS).');
|
|
71
|
+
export const buildChecks = (f) => [editionCheck(f), virtualizationCheck(f), featureCheck(f), memoryCheck(f), diskCheck(f), nodeCheck(f)];
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { type Context } from './context.js';
|
|
3
|
-
export type CheckStatus = 'ok' | 'warn' | 'action';
|
|
4
|
-
export interface Check {
|
|
5
|
-
name: string;
|
|
6
|
-
status: CheckStatus;
|
|
7
|
-
detail: string;
|
|
8
|
-
}
|
|
9
2
|
export type FeatureState = 'enabled' | 'pending' | 'disabled' | 'unknown';
|
|
10
3
|
export type EditionSupport = 'supported' | 'home' | 'unsupported' | 'unknown';
|
|
11
4
|
export interface MachineFacts {
|
|
@@ -24,22 +17,15 @@ export interface MachineFacts {
|
|
|
24
17
|
export declare const ENABLE_COMMAND: string;
|
|
25
18
|
/** `reg query <key>` output to { valueName: data }. */
|
|
26
19
|
export declare const parseRegQuery: (text: string) => Record<string, string>;
|
|
27
|
-
/** Windows Sandbox needs Pro, Enterprise or Education (build 18305+); Home ("Core*") and Server cannot run it. */
|
|
28
|
-
export declare const editionSupport: (edition: string | null) => EditionSupport;
|
|
29
20
|
/** The "State : ..." line of `Dism /Get-FeatureInfo` (English output only; anything else is unknown). */
|
|
30
21
|
export declare const parseDismState: (text: string) => FeatureState;
|
|
31
22
|
/** Win32_OptionalFeature.InstallState: 1 enabled, 2 disabled, 3 absent. */
|
|
32
23
|
export declare const featureFromInstallState: (state: number | null | undefined) => FeatureState;
|
|
33
|
-
export declare const nodeMajor: (version: string) => number;
|
|
34
|
-
/** Offer to enable the feature only when it is off and this edition can have it. */
|
|
35
|
-
export declare const canEnableFeature: (f: MachineFacts) => boolean;
|
|
36
|
-
/** Turns what we learnt about the PC into the checklist (pure, so it can be tested). */
|
|
37
|
-
export declare const buildChecks: (f: MachineFacts) => Check[];
|
|
38
24
|
declare const Probe: z.ZodObject<{
|
|
39
25
|
virtualizationFirmware: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
40
26
|
hypervisorPresent: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
41
27
|
installState: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
42
28
|
}, z.core.$strip>;
|
|
43
29
|
export declare const parseProbe: (text: string) => z.infer<typeof Probe>;
|
|
44
|
-
export declare const
|
|
30
|
+
export declare const gatherFacts: (storageRoot: string) => MachineFacts;
|
|
45
31
|
export {};
|