windows-development-sandbox 0.0.0-stage → 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/.sandbox-config.example.json +14 -0
- package/LICENSE +21 -0
- package/README.md +94 -2
- package/config.example.json +349 -0
- package/dist/api.d.ts +31 -0
- package/dist/api.js +31 -0
- package/dist/bin/agent-sandbox.d.ts +2 -0
- package/dist/bin/agent-sandbox.js +3 -0
- package/dist/bin/claude-host.d.ts +2 -0
- package/dist/bin/claude-host.js +5 -0
- package/dist/bin/claude-sbx.d.ts +2 -0
- package/dist/bin/claude-sbx.js +3 -0
- package/dist/bin/codex-sbx.d.ts +2 -0
- package/dist/bin/codex-sbx.js +3 -0
- package/dist/bin/gemini-sbx.d.ts +2 -0
- package/dist/bin/gemini-sbx.js +3 -0
- package/dist/bin/sandbox.d.ts +2 -0
- package/dist/bin/sandbox.js +4 -0
- package/dist/bin/sbx-hide.d.ts +2 -0
- package/dist/bin/sbx-hide.js +3 -0
- package/dist/bin/sbx-show.d.ts +2 -0
- package/dist/bin/sbx-show.js +3 -0
- package/dist/cli/args.d.ts +52 -0
- package/dist/cli/args.js +143 -0
- package/dist/cli/colors.d.ts +24 -0
- package/dist/cli/colors.js +42 -0
- package/dist/cli/commands/accounts.d.ts +2 -0
- package/dist/cli/commands/accounts.js +52 -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 +23 -0
- package/dist/cli/commands/context.js +57 -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 +10 -0
- package/dist/cli/commands/index.js +10 -0
- package/dist/cli/commands/launch.d.ts +29 -0
- package/dist/cli/commands/launch.js +333 -0
- package/dist/cli/commands/maintenance.d.ts +8 -0
- package/dist/cli/commands/maintenance.js +154 -0
- 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/checks.js +170 -0
- package/dist/cli/commands/repair/finders.d.ts +30 -0
- 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 +12 -0
- package/dist/cli/commands/sessions.js +257 -0
- package/dist/cli/commands/setup/checks.d.ts +13 -0
- package/dist/cli/commands/setup/checks.js +71 -0
- package/dist/cli/commands/setup/facts.d.ts +31 -0
- 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 +20 -0
- package/dist/cli/index.js +121 -0
- package/dist/cli/manifest.d.ts +103 -0
- package/dist/cli/manifest.js +107 -0
- package/dist/cli/menu-multi.d.ts +12 -0
- package/dist/cli/menu-multi.js +82 -0
- package/dist/cli/menu.d.ts +14 -0
- package/dist/cli/menu.js +290 -0
- 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.d.ts +17 -0
- package/dist/core/config.js +106 -0
- package/dist/core/detect-databases.d.ts +15 -0
- package/dist/core/detect-databases.js +147 -0
- package/dist/core/detect-pwsh.d.ts +18 -0
- package/dist/core/detect-pwsh.js +111 -0
- package/dist/core/detect-toolchains.d.ts +30 -0
- package/dist/core/detect-toolchains.js +58 -0
- package/dist/core/disk.d.ts +16 -0
- package/dist/core/disk.js +55 -0
- package/dist/core/local-config.d.ts +20 -0
- package/dist/core/local-config.js +77 -0
- 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 -0
- package/dist/core/schema.js +8 -0
- package/dist/core/types.d.ts +190 -0
- package/dist/core/types.js +1 -0
- package/dist/core/util.d.ts +60 -0
- package/dist/core/util.js +154 -0
- package/dist/core/workspace.d.ts +18 -0
- package/dist/core/workspace.js +128 -0
- 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.d.ts +1 -0
- package/dist/guest/daemon.js +53 -0
- package/dist/guest/db.d.ts +1 -0
- package/dist/guest/db.js +75 -0
- package/dist/guest/env.d.ts +51 -0
- package/dist/guest/env.js +101 -0
- package/dist/guest/gitguard.d.ts +7 -0
- package/dist/guest/gitguard.js +60 -0
- package/dist/guest/gitrules.d.ts +2 -0
- package/dist/guest/gitrules.js +11 -0
- package/dist/guest/guide.d.ts +16 -0
- package/dist/guest/guide.js +69 -0
- package/dist/guest/helpers.d.ts +3 -0
- package/dist/guest/helpers.js +26 -0
- package/dist/guest/main.d.ts +1 -0
- package/dist/guest/main.js +130 -0
- 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 +22 -0
- package/dist/guest/ptyhost.js +36 -0
- package/dist/guest/setup/agents.d.ts +11 -0
- package/dist/guest/setup/agents.js +87 -0
- package/dist/guest/setup/claude.d.ts +21 -0
- package/dist/guest/setup/claude.js +146 -0
- package/dist/guest/setup/environment.d.ts +16 -0
- package/dist/guest/setup/environment.js +69 -0
- package/dist/guest/setup/hooks.d.ts +9 -0
- package/dist/guest/setup/hooks.js +50 -0
- package/dist/guest/setup/network.d.ts +10 -0
- package/dist/guest/setup/network.js +99 -0
- package/dist/guest/setup/policy.d.ts +3 -0
- package/dist/guest/setup/policy.js +18 -0
- package/dist/guest/setup/user.d.ts +7 -0
- package/dist/guest/setup/user.js +47 -0
- package/dist/guest/toolchains/catalogue.d.ts +55 -0
- package/dist/guest/toolchains/catalogue.js +31 -0
- package/dist/guest/toolchains/cli.d.ts +10 -0
- package/dist/guest/toolchains/cli.js +72 -0
- package/dist/guest/toolchains/common.d.ts +54 -0
- package/dist/guest/toolchains/common.js +147 -0
- package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
- package/dist/guest/toolchains/databases/dynamodb.js +52 -0
- package/dist/guest/toolchains/databases/index.d.ts +6 -0
- 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.d.ts +3 -0
- package/dist/guest/toolchains/git.js +121 -0
- package/dist/guest/toolchains/go.d.ts +3 -0
- package/dist/guest/toolchains/go.js +33 -0
- package/dist/guest/toolchains/index.d.ts +17 -0
- package/dist/guest/toolchains/index.js +80 -0
- package/dist/guest/toolchains/java.d.ts +8 -0
- package/dist/guest/toolchains/java.js +96 -0
- package/dist/guest/toolchains/leftovers.d.ts +2 -0
- package/dist/guest/toolchains/leftovers.js +26 -0
- package/dist/guest/toolchains/managers.d.ts +16 -0
- package/dist/guest/toolchains/managers.js +84 -0
- package/dist/guest/toolchains/native/binstall.d.ts +7 -0
- package/dist/guest/toolchains/native/binstall.js +52 -0
- 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.d.ts +3 -0
- package/dist/guest/toolchains/node.js +152 -0
- package/dist/guest/toolchains/plan.d.ts +22 -0
- package/dist/guest/toolchains/plan.js +96 -0
- package/dist/guest/toolchains/pwsh.d.ts +4 -0
- package/dist/guest/toolchains/pwsh.js +14 -0
- package/dist/guest/toolchains/python.d.ts +3 -0
- package/dist/guest/toolchains/python.js +54 -0
- package/dist/guest/toolchains/runtimes.d.ts +19 -0
- package/dist/guest/toolchains/runtimes.js +38 -0
- 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/toolchains/versions.d.ts +47 -0
- package/dist/guest/toolchains/versions.js +77 -0
- package/dist/guest/toolchains/vscode.d.ts +2 -0
- package/dist/guest/toolchains/vscode.js +53 -0
- package/dist/guest/view.d.ts +1 -0
- package/dist/guest/view.js +42 -0
- package/dist/host/accounts.d.ts +32 -0
- package/dist/host/accounts.js +128 -0
- 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 +38 -0
- package/dist/host/channel.js +162 -0
- package/dist/host/claude-profile.d.ts +15 -0
- package/dist/host/claude-profile.js +107 -0
- package/dist/host/cleanup.d.ts +23 -0
- package/dist/host/cleanup.js +57 -0
- package/dist/host/database-plan.d.ts +12 -0
- package/dist/host/database-plan.js +52 -0
- package/dist/host/generate.d.ts +7 -0
- package/dist/host/generate.js +98 -0
- 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/personalize.d.ts +10 -0
- package/dist/host/personalize.js +97 -0
- package/dist/host/platform.d.ts +19 -0
- package/dist/host/platform.js +126 -0
- package/dist/host/project-scan.d.ts +7 -0
- package/dist/host/project-scan.js +89 -0
- package/dist/host/projects.d.ts +100 -0
- package/dist/host/projects.js +147 -0
- package/dist/host/repo-guard.d.ts +36 -0
- package/dist/host/repo-guard.js +219 -0
- package/dist/host/secrets.d.ts +14 -0
- package/dist/host/secrets.js +133 -0
- package/dist/host/terminal.d.ts +32 -0
- package/dist/host/terminal.js +130 -0
- package/dist/host/test-guard.d.ts +6 -0
- package/dist/host/test-guard.js +15 -0
- package/dist/host/window.d.ts +4 -0
- package/dist/host/window.js +72 -0
- package/dist/host/wsb.d.ts +37 -0
- package/dist/host/wsb.js +101 -0
- package/dist/runtime/daemon.js +28494 -0
- package/dist/runtime/db.js +26387 -0
- package/dist/runtime/gitguard.js +84 -0
- package/dist/runtime/main.js +28622 -0
- package/dist/runtime/view.js +25841 -0
- 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 -0
- package/dist/selftest/cli.test.js +118 -0
- 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 -0
- package/dist/selftest/config.test.js +38 -0
- package/dist/selftest/data.test.d.ts +2 -0
- package/dist/selftest/data.test.js +80 -0
- package/dist/selftest/detect.test.d.ts +2 -0
- package/dist/selftest/detect.test.js +58 -0
- package/dist/selftest/fixtures.d.ts +272 -0
- package/dist/selftest/fixtures.js +80 -0
- 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 +51 -0
- package/dist/selftest/harness.js +77 -0
- 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 -0
- package/dist/selftest/index.js +155 -0
- 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 -0
- package/dist/selftest/mirror.test.js +101 -0
- 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 -0
- package/dist/selftest/setup.test.js +46 -0
- package/dist/selftest/suite.d.ts +13 -0
- package/dist/selftest/suite.js +1 -0
- package/dist/selftest/sync.test.d.ts +2 -0
- package/dist/selftest/sync.test.js +94 -0
- 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 +60 -0
- package/dist/shared/download.js +215 -0
- package/dist/shared/guest-paths.d.ts +30 -0
- package/dist/shared/guest-paths.js +61 -0
- package/dist/shared/netrules.d.ts +18 -0
- package/dist/shared/netrules.js +98 -0
- package/dist/shared/paths.d.ts +14 -0
- package/dist/shared/paths.js +28 -0
- package/dist/shared/projects.d.ts +53 -0
- package/dist/shared/projects.js +67 -0
- package/dist/shared/pty-client.d.ts +86 -0
- package/dist/shared/pty-client.js +227 -0
- package/dist/shared/rundir.d.ts +39 -0
- package/dist/shared/rundir.js +94 -0
- package/dist/shared/safe-remove.d.ts +10 -0
- package/dist/shared/safe-remove.js +66 -0
- package/dist/shared/schema.d.ts +527 -0
- package/dist/shared/schema.js +90 -0
- package/dist/shared/system.d.ts +15 -0
- package/dist/shared/system.js +19 -0
- package/dist/shared/test-guard.d.ts +4 -0
- package/dist/shared/test-guard.js +37 -0
- package/dist/sync/claude-mem.d.ts +8 -0
- package/dist/sync/claude-mem.js +79 -0
- package/dist/sync/history.d.ts +41 -0
- package/dist/sync/history.js +161 -0
- package/dist/sync/watcher.d.ts +21 -0
- package/dist/sync/watcher.js +135 -0
- package/hooks/post-agent/README.txt +1 -0
- package/hooks/pre-provision/README.txt +1 -0
- package/package.json +91 -4
- package/sandbox/bootstrap.ps1 +258 -0
- package/sandbox/cpucap.ps1 +51 -0
- package/sandbox/gitshim.cs +35 -0
- package/sandbox/logon.cmd +18 -0
- package/sandbox/openssl.cs +42 -0
- package/sandbox/ptyhost.cs +614 -0
- package/sandbox/trust.cmd +35 -0
- package/sandbox/vswhere.cs +83 -0
- package/schemas/config.schema.json +1161 -0
- package/schemas/sandbox-config.schema.json +968 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Claude accounts: saving logins from the sandbox or this PC, switching, and the names that are refused.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { activateAccount, activeAccount, defaultAccount, listAccounts, readAccount, removeAccount, saveAccount, setDefaultAccount } from '../host/accounts.js';
|
|
5
|
+
export const accountsSuite = {
|
|
6
|
+
name: 'accounts',
|
|
7
|
+
title: 'Claude accounts (save, switch, remove, safe names)',
|
|
8
|
+
run: async (t, f) => {
|
|
9
|
+
t.section('saving and switching');
|
|
10
|
+
const storage = t.at('acct-storage');
|
|
11
|
+
const dataA = path.join(storage, 'data', 'x');
|
|
12
|
+
const sandboxClaude = path.join(dataA, 'state', 'claude');
|
|
13
|
+
t.write(path.join(sandboxClaude, '.credentials.json'), '{"claudeAiOauth":{"refreshToken":"personal-1"}}');
|
|
14
|
+
t.write(path.join(sandboxClaude, '.claude.json'), '{"oauthAccount":{"emailAddress":"me@personal"},"other":1}');
|
|
15
|
+
saveAccount(storage, dataA, 'personal', null);
|
|
16
|
+
t.write(t.at('host-login', '.credentials.json'), '{"claudeAiOauth":{"refreshToken":"work-1"}}');
|
|
17
|
+
t.write(t.at('host-login.json'), '{"oauthAccount":{"emailAddress":"me@work"}}');
|
|
18
|
+
saveAccount(storage, dataA, 'work', { claudeDir: t.at('host-login'), globalStateFile: t.at('host-login.json') });
|
|
19
|
+
t.equal(listAccounts(storage, dataA).length, 2, 'accounts can be saved from the sandbox and from this PC');
|
|
20
|
+
t.equal(defaultAccount(storage), 'personal', 'the first account saved becomes the default');
|
|
21
|
+
t.equal(activeAccount(dataA), 'personal', 'the first account saved is the active one');
|
|
22
|
+
t.check(!fs.existsSync(path.join(sandboxClaude, '.agent-sandbox-account')), 'which account is active is not recorded inside the sandbox...');
|
|
23
|
+
t.check(fs.existsSync(path.join(storage, 'accounts', 'index.json')), '... it is recorded host-side');
|
|
24
|
+
t.write(path.join(sandboxClaude, '.credentials.json'), '{"claudeAiOauth":{"refreshToken":"personal-2"}}');
|
|
25
|
+
activateAccount(storage, dataA, 'work');
|
|
26
|
+
const stateAfter = f.readJson(path.join(sandboxClaude, '.claude.json'));
|
|
27
|
+
t.includes(fs.readFileSync(path.join(sandboxClaude, '.credentials.json'), 'utf8'), 'work-1', 'switching swaps in the other account\'s credentials');
|
|
28
|
+
t.equal(stateAfter.oauthAccount.emailAddress, 'me@work', 'switching swaps the signed-in account');
|
|
29
|
+
t.equal(stateAfter.other, 1, 'switching keeps the rest of the sandbox state');
|
|
30
|
+
t.equal(activeAccount(dataA), 'work', 'the new account is active');
|
|
31
|
+
t.includes(JSON.stringify(readAccount(storage, 'personal')?.credentials), 'personal-2', 'the outgoing account\'s refreshed tokens are saved back before switching');
|
|
32
|
+
t.section('removing and refusing');
|
|
33
|
+
setDefaultAccount(storage, 'work');
|
|
34
|
+
removeAccount(storage, 'work');
|
|
35
|
+
t.equal(defaultAccount(storage), null, 'removing the default account clears the default');
|
|
36
|
+
await t.throws(() => activateAccount(storage, dataA, 'nope'), 'Unknown account', 'an unknown account cannot be activated');
|
|
37
|
+
await t.throws(() => saveAccount(storage, dataA, '../x', null), 'Invalid account name', 'a path-like account name is rejected');
|
|
38
|
+
const victim = t.dir('victim');
|
|
39
|
+
const otherData = t.at('acct-storage', 'data', 'd');
|
|
40
|
+
const fromHost = { claudeDir: victim, globalStateFile: victim };
|
|
41
|
+
await t.throws(() => saveAccount(t.at('acct-storage'), otherData, 'index', fromHost), 'Invalid account name', 'an account cannot be called "index" (it would overwrite the index)');
|
|
42
|
+
await t.throws(() => saveAccount(t.at('acct-storage'), otherData, 'con', fromHost), 'Invalid account name', 'an account cannot be named after a device (it would open a device)');
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Agent commands (Beta): which agents get a forwarding command, what the files contain, and that "off" changes nothing.
|
|
2
|
+
// Everything happens in the test's temp folder with a made-up PATH; your real PATH is never read or written.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { DEFAULTS } from '../core/config.js';
|
|
6
|
+
import { agentCommands, commandsDir, forwardingCmd, forwardingSh, realAgent, reconcileAgentCommands } from '../host/agent-commands.js';
|
|
7
|
+
export const agentCommandsSuite = {
|
|
8
|
+
name: 'agent-commands',
|
|
9
|
+
title: 'agent commands (Beta): claude, codex ... forwarded into a hidden sandbox',
|
|
10
|
+
run: async (t) => {
|
|
11
|
+
const storage = t.at('agent-cmd-storage');
|
|
12
|
+
const npmDir = t.at('agent-cmd-npm');
|
|
13
|
+
const localBin = t.at('agent-cmd-local');
|
|
14
|
+
t.write(path.join(npmDir, 'codex.cmd'), '@echo codex');
|
|
15
|
+
t.write(path.join(npmDir, 'gemini.cmd'), '@echo gemini');
|
|
16
|
+
t.write(path.join(localBin, 'claude.exe'), 'MZ');
|
|
17
|
+
// A forwarding file of our own, first on PATH, must never count as the real agent.
|
|
18
|
+
t.write(path.join(commandsDir(storage), 'claude.cmd'), '@rem windows-development-sandbox agent command');
|
|
19
|
+
const PATH = [commandsDir(storage), npmDir, localBin].join(path.delimiter);
|
|
20
|
+
const on = { ...DEFAULTS, agentCommands: { enabled: true, agents: 'auto' } };
|
|
21
|
+
t.section('finding agents');
|
|
22
|
+
t.equal(realAgent('claude', commandsDir(storage), PATH), path.join(localBin, 'claude.exe'), 'the real agent is found on PATH, skipping this tool\'s own forwarding files');
|
|
23
|
+
t.equal(realAgent('nope', commandsDir(storage), PATH), null, 'an agent that is not installed is not found');
|
|
24
|
+
const commandsOf = (config, searchPath) => agentCommands(config, storage, searchPath).map((a) => a.command).sort();
|
|
25
|
+
t.equal(commandsOf(on, PATH), ['claude', 'codex', 'gemini'], '"auto": every agent installed on this PC gets a command, agents not installed none');
|
|
26
|
+
t.equal(commandsOf({ ...on, agentCommands: { enabled: true, agents: ['codex'] } }, PATH), ['codex'], 'a list limits it to the named agents');
|
|
27
|
+
const unsafeDir = t.at('agent%cmd^dir');
|
|
28
|
+
t.write(path.join(unsafeDir, 'aider.cmd'), '@echo');
|
|
29
|
+
t.check(!agentCommands(on, storage, unsafeDir).some((a) => a.command === 'aider'), 'an agent in a folder whose path has % or ^ (which cmd.exe would expand) is left alone');
|
|
30
|
+
t.section('the forwarding files');
|
|
31
|
+
const claude = { harness: 'claude', command: 'claude', real: path.join(localBin, 'claude.exe') };
|
|
32
|
+
const cmd = forwardingCmd(claude, 'C:\\node\\node.exe', 'C:\\tool\\dist\\bin\\sandbox.js');
|
|
33
|
+
t.includes(cmd, '"C:\\node\\node.exe" "C:\\tool\\dist\\bin\\sandbox.js" launch --harness claude --headless --terminal -- %*', 'the .cmd starts the agent in a hidden sandbox attached to this terminal...');
|
|
34
|
+
t.includes(cmd, 'if "%SANDBOX_AGENT_DIRECT%"=="1" goto direct', '... unless SANDBOX_AGENT_DIRECT=1...');
|
|
35
|
+
t.includes(cmd, 'call "' + claude.real + '" %*', '... which runs the real agent');
|
|
36
|
+
const sh = forwardingSh({ ...claude, real: 'C:\\Users\\me\\it\'s\\claude.exe' }, 'C:\\node\\node.exe', 'C:\\tool\\sandbox.js');
|
|
37
|
+
t.check(sh.startsWith('#!/bin/sh'), 'Git Bash gets a shell script');
|
|
38
|
+
t.includes(sh, "exec '/c/node/node.exe' '/c/tool/sandbox.js' launch --harness claude --headless --terminal -- \"$@\"", 'the shell script starts the same sandbox, with POSIX paths');
|
|
39
|
+
t.includes(sh, "'/c/Users/me/it'\\''s/claude.exe'", 'the shell script quotes awkward paths safely');
|
|
40
|
+
t.section('off changes nothing');
|
|
41
|
+
// Off with nothing set up: nothing to do, no PATH edit (which would be refused during the self-test anyway).
|
|
42
|
+
const fresh = t.at('agent-cmd-fresh');
|
|
43
|
+
const off = reconcileAgentCommands(DEFAULTS, fresh, 'C:\\node\\node.exe', 'C:\\tool\\sandbox.js');
|
|
44
|
+
t.equal({ on: off.on, changed: off.changed, wrote: fs.existsSync(commandsDir(fresh)) }, { on: false, changed: false, wrote: false }, 'off (the default) writes nothing and leaves your PATH alone');
|
|
45
|
+
await t.throws(() => reconcileAgentCommands(on, t.at('agent-cmd-on'), 'C:\\node\\node.exe', 'C:\\tool\\sandbox.js'), /self-test|PATH/i, 'turning it on edits your PATH, which the self-test refuses (it never changes this PC)');
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The host <-> sandbox channel and the terminal protocol: everything arriving from the sandbox is validated, sized and
|
|
2
|
+
// filtered before this PC acts on it.
|
|
3
|
+
import { safeId } from '../shared/rundir.js';
|
|
4
|
+
import { GuestRequest, SessionStatus } from '../shared/schema.js';
|
|
5
|
+
import { encodeFrame, FrameReader, FrameType, fixWindowsKeys, helloFrame, MAX_FRAME_BYTES, OutputFilter } from '../shared/pty-client.js';
|
|
6
|
+
export const channelSuite = {
|
|
7
|
+
name: 'channel',
|
|
8
|
+
title: 'request/status validation and the terminal protocol (frames, OSC 52, keys)',
|
|
9
|
+
run: async (t) => {
|
|
10
|
+
t.section('requests and session status');
|
|
11
|
+
const validRequest = (r) => GuestRequest.safeParse(r).success;
|
|
12
|
+
const validSession = (s) => SessionStatus.safeParse(s).success;
|
|
13
|
+
await t.throws(() => safeId('../evil'), 'invalid id', 'a path-like id is rejected');
|
|
14
|
+
t.check(!validRequest({ kind: 'shell', id: 'a' }), 'unknown request kinds are rejected');
|
|
15
|
+
t.check(!validRequest({ kind: 'exec', id: 'a/b', command: ['x'], cwd: 'C:\\', timeoutSeconds: 1 }), 'request ids must be safe file names');
|
|
16
|
+
t.check(validRequest({ kind: 'ping', id: 'abc', createdAt: 'x' }), 'a known request kind with a safe id is accepted');
|
|
17
|
+
t.check(!validRequest({ kind: 'ping', id: 'abc', createdAt: 'x', extra: 1 }), 'requests with unknown keys are rejected');
|
|
18
|
+
const goodSession = { id: 's1', launchId: 'l', harness: 'claude', cwd: 'C:\\Workspace', message: 'ok', phase: 'running', updatedAt: 'x', startedAt: 'x', attach: { addresses: ['172.20.1.5'], port: 5000, token: 't' } };
|
|
19
|
+
t.check(validSession(goodSession), 'a well-formed session status is accepted');
|
|
20
|
+
t.check(!validSession({ ...goodSession, attach: { ...goodSession.attach, port: 70000 } }), 'a session status with an impossible port is rejected');
|
|
21
|
+
t.check(!validSession({ ...goodSession, phase: 'pwned' }), 'a session status with an unknown phase is rejected');
|
|
22
|
+
t.section('terminal frames');
|
|
23
|
+
const filter = new OutputFilter();
|
|
24
|
+
const filtered = Buffer.concat([
|
|
25
|
+
filter.push(Buffer.from('a\x1b]52;c;ZXZpbA==\x07b\x1b]0;title\x07')),
|
|
26
|
+
filter.push(Buffer.from('c\x1b]5')),
|
|
27
|
+
filter.push(Buffer.from('2;c;cGF5\x1b\\d')),
|
|
28
|
+
]).toString('latin1');
|
|
29
|
+
t.equal(filtered, 'a' + 'b\x1b]0;title\x07' + 'c' + 'd', 'clipboard writes (OSC 52) from the sandbox are stripped, even split across chunks; titles pass');
|
|
30
|
+
const oversizedHeader = Buffer.concat([Buffer.from([16]), Buffer.from([0, 0, 0, 0x7f])]);
|
|
31
|
+
await t.throws(() => new FrameReader().push(oversizedHeader), 'frame too large', 'a frame announcing too much data is rejected instead of buffered');
|
|
32
|
+
t.equal(new FrameReader().push(encodeFrame(16, Buffer.alloc(MAX_FRAME_BYTES))).length, 1, 'a frame of exactly the maximum size is accepted');
|
|
33
|
+
const hello = new FrameReader().push(helloFrame('tok', 100, 30, 2))[0];
|
|
34
|
+
t.equal({ type: hello.type, cols: hello.payload.readInt16LE(0), rows: hello.payload.readInt16LE(2), flags: hello.payload[4], token: hello.payload.subarray(5).toString() }, { type: FrameType.Hello, cols: 100, rows: 30, flags: 2, token: 'tok' }, 'a hello frame carries size, flags and token');
|
|
35
|
+
t.equal(fixWindowsKeys(Buffer.from([0x61, 0x08, 0x7f, 0x1b])), Buffer.from([0x61, 0x7f, 0x08, 0x1b]), 'Windows console Backspace (^H) reaches the agent as DEL, so it deletes one character, not a word');
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// The command manifest, the argument parser, the interactive menu helpers and the settings screen. One definition
|
|
2
|
+
// (cli/manifest.ts) drives both the flags and the menu.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { buildProgram, parseCli } from '../cli/args.js';
|
|
6
|
+
import { COMMANDS } from '../cli/manifest.js';
|
|
7
|
+
import { commandLine, readHistory, recordHistory } from '../cli/history.js';
|
|
8
|
+
import { optionProblem, splitArgs } from '../cli/menu.js';
|
|
9
|
+
import { cleanTargetOf } from '../cli/commands/data/collect.js';
|
|
10
|
+
import { ago, colorEnabled, shorten, shortenMiddle } from '../cli/colors.js';
|
|
11
|
+
import { hoistConfig, withoutConfig } from '../cli/index.js';
|
|
12
|
+
import { closest, validNodeSpec, validPythonSpec } from '../cli/validate.js';
|
|
13
|
+
import { editConfigText, SECTION_TITLES } from '../cli/settings/index.js';
|
|
14
|
+
import { childrenOf, isSection, jsonExample, leavesOf, promptKind } from '../cli/settings/schema.js';
|
|
15
|
+
import { Config, LocalOverrides } from '../core/schema.js';
|
|
16
|
+
export const cliSuite = {
|
|
17
|
+
name: 'cli',
|
|
18
|
+
title: 'command manifest, argument parser, menu helpers, settings screen',
|
|
19
|
+
run: async (t) => {
|
|
20
|
+
t.section('manifest and parser');
|
|
21
|
+
const program = buildProgram(() => undefined);
|
|
22
|
+
const visible = COMMANDS.filter((c) => !c.hidden).map((c) => c.name);
|
|
23
|
+
const missingFromParser = visible.filter((name) => !program.commands.some((c) => c.name() === name));
|
|
24
|
+
t.equal(missingFromParser, [], 'every manifest command exists in the parser');
|
|
25
|
+
t.check(program.commands.find((c) => c.name() === 'launch').options.some((o) => o.long === '--terminal'), 'every manifest option exists in the parser (launch --terminal)');
|
|
26
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(t.packageRoot, 'package.json'), 'utf8'));
|
|
27
|
+
t.equal(program.version(), packageJson.version, '--version prints the package version');
|
|
28
|
+
t.check(program.options.some((o) => o.short === '-v' && o.long === '--version'), '-v is a short form of --version');
|
|
29
|
+
const start = parseCli(['start', '--headless']);
|
|
30
|
+
t.equal({ command: start.command, noAgent: start.noAgent, headless: start.headless }, { command: 'launch', noAgent: true, headless: true }, '`start` is launch without an agent');
|
|
31
|
+
const clean = parseCli(['data', 'clean', 'tools/go', 'logs', '--yes']);
|
|
32
|
+
t.equal({ command: clean.command, positionals: clean.positionals, yes: clean.yes }, { command: 'data', positionals: ['clean', 'tools/go', 'logs'], yes: true }, 'data clean takes targets and --yes');
|
|
33
|
+
t.equal(parseCli(['start', '--database', 'postgres,redis', '--database', 'mongodb']).databases, ['postgres', 'redis', 'mongodb'], '--database takes lists and can repeat');
|
|
34
|
+
t.equal(parseCli(['db', 'start', 'redis']).positionals, ['start', 'redis'], '`db` takes an action and a name');
|
|
35
|
+
t.equal(parseCli(['launch', '--harness', 'opencode', '--', '--continue']).passthrough, ['--continue'], 'agent arguments after -- reach the agent');
|
|
36
|
+
t.section('parser: agent wrappers (claude-sbx ...) and agent-sandbox');
|
|
37
|
+
const wrapped = parseCli(['--terminal', '--continue', '--headless', '--node', '20', '--', '--terminal'], true);
|
|
38
|
+
t.equal({ command: wrapped.command, terminal: wrapped.terminal, headless: wrapped.headless, node: wrapped.node, passthrough: wrapped.passthrough }, { command: 'launch', terminal: true, headless: true, node: '20', passthrough: ['--continue', '--terminal'] }, 'claude-sbx: sandbox flags are consumed, everything else goes to the agent');
|
|
39
|
+
await t.throws(() => parseCli(['status', '--bogus']), 'unknown option', 'agent-sandbox: unknown options are errors');
|
|
40
|
+
t.equal(parseCli(['account', 'use', 'work', '--no-restart']).positionals, ['use', 'work'], 'agent-sandbox: positionals are collected');
|
|
41
|
+
t.equal(parseCli(['clean', '--days=3']).days, 3, 'agent-sandbox: --opt=value works');
|
|
42
|
+
const agentFlags = parseCli(['--model', 'opus', '-p', 'fix it', '--json', '-f', 'x', '--timeout', '5', '--resume', 'abc'], true);
|
|
43
|
+
t.equal(agentFlags.passthrough, ['--model', 'opus', '-p', 'fix it', '--json', '-f', 'x', '--timeout', '5', '--resume', 'abc'], 'claude-sbx: any flag that is not a launch option goes to the agent, in order (even ones agent-sandbox uses elsewhere)');
|
|
44
|
+
t.equal({ json: agentFlags.json, follow: agentFlags.follow, timeout: agentFlags.timeout }, { json: false, follow: false, timeout: 600 }, 'claude-sbx: the agent\'s flags do not set agent-sandbox options');
|
|
45
|
+
const reserved = parseCli(['--sbx-terminal', '--terminal', '--sbx-node=18'], true);
|
|
46
|
+
t.equal({ terminal: reserved.terminal, node: reserved.node, passthrough: reserved.passthrough }, { terminal: true, node: '18', passthrough: [] }, '--sbx-<option> always means agent-sandbox');
|
|
47
|
+
t.equal(parseCli(['launch', '--continue', '--sbx-headless']).passthrough, ['--continue'], 'agent-sandbox launch passes agent flags too');
|
|
48
|
+
await t.throws(() => parseCli(['--sbx-nope'], true), 'Unknown option: --sbx-nope', 'an unknown --sbx-<option> is an error');
|
|
49
|
+
t.section('menu helpers');
|
|
50
|
+
t.equal(splitArgs('--model "big one" -c \'x y\''), ['--model', 'big one', '-c', 'x y'], 'menu: arguments split like a shell');
|
|
51
|
+
t.equal(commandLine(['exec', 'echo', 'a b']), 'sandbox exec echo "a b"', 'menu: command lines are quoted');
|
|
52
|
+
const optionOf = (flag) => COMMANDS.flatMap((cmd) => cmd.options).find((o) => o.flag === flag);
|
|
53
|
+
t.check(optionProblem(optionOf('--node'), 'abc') !== undefined, 'menu: a bad Node version is rejected while typing');
|
|
54
|
+
t.check(optionProblem(optionOf('--node'), '22') === undefined, 'menu: a good Node version is accepted');
|
|
55
|
+
t.check(optionProblem(optionOf('--workspace'), path.join(t.packageRoot, 'no-such-folder-here')) !== undefined, 'menu: a missing folder is rejected');
|
|
56
|
+
t.check(optionProblem(optionOf('--workspace'), t.packageRoot) === undefined, 'menu: an existing folder is accepted');
|
|
57
|
+
t.check(optionProblem(optionOf('--days'), '0') !== undefined, 'menu: a number below the minimum is rejected');
|
|
58
|
+
const rowOf = (area, name) => ({ area, name, path: 'x', bytes: 1 });
|
|
59
|
+
t.equal([cleanTargetOf(rowOf('tools', 'go')), cleanTargetOf(rowOf('tools', 'agents/claude')), cleanTargetOf(rowOf('databases', 'databases/pg')), cleanTargetOf(rowOf('host', 'launches'))], ['tools/go', 'tools/agents/claude', 'databases/pg', 'launches'], 'data menu: removable rows become checkbox targets');
|
|
60
|
+
t.equal([cleanTargetOf(rowOf('host', 'accounts')), cleanTargetOf(rowOf('tools', '(files in tools)'))], [null, null], 'data menu: accounts and loose files are never offered');
|
|
61
|
+
t.equal(COMMANDS.find((cmd) => cmd.name === 'data').options.find((o) => o.flag === '--yes')?.noMenu, true, 'data menu: --yes is never offered');
|
|
62
|
+
t.section('--config placement, history, validation, text helpers');
|
|
63
|
+
t.equal(hoistConfig(['--config', 'a.json', 'status', '--json']), ['status', '--config', 'a.json', '--json'], '--config before the command is moved behind it');
|
|
64
|
+
t.equal(hoistConfig(['--config=a.json']), ['menu', '--config', 'a.json'], '--config alone opens the menu with it');
|
|
65
|
+
t.equal(withoutConfig(['status', '--config', 'a.json', '--json']), ['status', '--json'], 'the recorded history drops --config');
|
|
66
|
+
recordHistory(['launch', '--harness', 'claude']);
|
|
67
|
+
recordHistory(['status']);
|
|
68
|
+
recordHistory(['launch', '--harness', 'claude']);
|
|
69
|
+
recordHistory(['menu']);
|
|
70
|
+
t.equal(readHistory().map((h) => h.argv.join(' ')), ['launch --harness claude', 'status'], 'history: newest first, no duplicates, the menu itself is not recorded');
|
|
71
|
+
t.check(validNodeSpec('22') && validNodeSpec('v22.1.0') && validNodeSpec('lts/*') && !validNodeSpec('abc'), 'validation: Node version syntax');
|
|
72
|
+
t.check(validPythonSpec('3.12') && !validPythonSpec('2.7'), 'validation: Python version syntax');
|
|
73
|
+
t.equal(closest('cluade', ['claude', 'codex']), 'claude', 'validation: did-you-mean for agent names');
|
|
74
|
+
t.equal(closest('zzzzzz', ['claude']), undefined, 'validation: nothing is suggested for an unrelated name');
|
|
75
|
+
const env = (vars, isTty) => colorEnabled(vars, isTty);
|
|
76
|
+
t.check(env({}, true), 'colours: on for a terminal');
|
|
77
|
+
t.check(!env({}, false), 'colours: off for a pipe');
|
|
78
|
+
t.check(!env({ NO_COLOR: '1' }, true) && !env({ TERM: 'dumb' }, true), 'colours: off for NO_COLOR and TERM=dumb');
|
|
79
|
+
t.check(env({ FORCE_COLOR: '1' }, false) && !env({ FORCE_COLOR: '0' }, true), 'colours: FORCE_COLOR overrides');
|
|
80
|
+
t.equal(shorten('abcdefghij', 6), 'abcde…', 'text helpers: shorten');
|
|
81
|
+
t.equal(shortenMiddle('C:\\Users\\me\\project', 11), 'C:\\Us…oject', 'text helpers: shorten in the middle');
|
|
82
|
+
t.equal([ago(0, 10 * 60 * 1000), ago(0, 5000), ago(0, 3 * 86400 * 1000)], ['10 min ago', 'just now', '3 d ago'], 'text helpers: relative time');
|
|
83
|
+
t.section('settings screen');
|
|
84
|
+
const original = '{\n // my notes stay\n "sandbox": { "headless": true }\n}\n';
|
|
85
|
+
const set = editConfigText(original, ['attach', 'auto'], true);
|
|
86
|
+
const nested = 'text' in set ? editConfigText(set.text, ['databases', 'postgres', 'port'], 6543) : set;
|
|
87
|
+
const reset = 'text' in nested ? editConfigText(nested.text, ['sandbox', 'headless'], undefined) : nested;
|
|
88
|
+
t.check('text' in reset, 'editing a config file gives new text');
|
|
89
|
+
const resetText = 'text' in reset ? reset.text : '';
|
|
90
|
+
t.includes(resetText, '// my notes stay', 'settings screen: changes keep the file\'s comments');
|
|
91
|
+
t.includes(resetText, '"auto": true', 'settings screen: a new setting is added');
|
|
92
|
+
t.includes(resetText, '"port": 6543', 'settings screen: nested keys are created');
|
|
93
|
+
t.excludes(resetText, 'headless', 'settings screen: reset removes a key');
|
|
94
|
+
const invalid = editConfigText(original, ['sandbox', 'memoryMb'], 12);
|
|
95
|
+
t.check('problems' in invalid && invalid.problems.join().includes('memoryMb'), 'settings screen: invalid values are not saved', JSON.stringify(invalid));
|
|
96
|
+
const missing = editConfigText('{ "storage": { "root": "C:\\\\x" } }', ['agentCommands', 'enabled'], undefined);
|
|
97
|
+
t.check('text' in missing && missing.text.includes('"root"'), 'removing a setting the file does not have changes nothing (no crash)');
|
|
98
|
+
const projectEdit = editConfigText('{}', ['downloads', 'requireChecksum'], true, 'project');
|
|
99
|
+
const projectBad = editConfigText('{}', ['storage', 'root'], 'C:\\x', 'project');
|
|
100
|
+
t.check('text' in projectEdit && projectEdit.text.includes('requireChecksum'), 'settings screen: a project file accepts project settings');
|
|
101
|
+
t.check('problems' in projectBad, 'settings screen: a project file is validated against the project schema (storage cannot be set there)');
|
|
102
|
+
// Every setting must be reachable in the settings screen: each leaf has a prompt, each top-level section a title.
|
|
103
|
+
const userLeaves = leavesOf(Config);
|
|
104
|
+
const projectLeaves = leavesOf(LocalOverrides);
|
|
105
|
+
const sectionNames = (schema) => childrenOf(schema).filter(([, s]) => isSection(s)).map(([name]) => name);
|
|
106
|
+
const kinds = new Set(userLeaves.map((l) => promptKind(l.schema)));
|
|
107
|
+
t.check(userLeaves.length > 100 && projectLeaves.length > 50, 'settings screen: every setting is a leaf (' + userLeaves.length + ' settings, ' + projectLeaves.length + ' in a project file)');
|
|
108
|
+
const missingKinds = ['boolean', 'enum', 'union', 'number', 'string', 'string-list', 'json'].filter((k) => !kinds.has(k));
|
|
109
|
+
t.equal(missingKinds, [], 'settings screen: every kind of setting has a prompt');
|
|
110
|
+
t.check(userLeaves.some((l) => l.key.join('.') === 'downloads.requireChecksum'), 'settings screen: downloads.requireChecksum is reachable in your settings');
|
|
111
|
+
t.check(projectLeaves.some((l) => l.key.join('.') === 'downloads.requireChecksum'), 'settings screen: downloads.requireChecksum is reachable in a project file');
|
|
112
|
+
const untitled = [...sectionNames(Config), ...sectionNames(LocalOverrides)].filter((name) => !SECTION_TITLES[name]);
|
|
113
|
+
t.equal(untitled, [], 'settings screen: every top-level section has a title');
|
|
114
|
+
const jsonLeaves = userLeaves.filter((l) => promptKind(l.schema) === 'json');
|
|
115
|
+
t.check(jsonLeaves.length > 0 && jsonLeaves.every((l) => jsonExample(l.key, l.schema).length > 5), 'settings screen: JSON settings show an example of the expected shape');
|
|
116
|
+
t.includes(jsonExample(['harnesses'], jsonLeaves[0].schema), 'npmPackage', 'settings screen: the agent definition example shows its fields');
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// How the sandbox builds command lines for npm-installed agents (.cmd files run through cmd.exe): & | < > ^ % and quotes
|
|
2
|
+
// must reach the agent unchanged.
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { isWin } from '../core/util.js';
|
|
7
|
+
import { runNode } from './fixtures.js';
|
|
8
|
+
// Odd quote counts flip cmd's quote state for every later argument: those come after them on purpose.
|
|
9
|
+
const TRICKY_ARGUMENTS = ['https://x?a=1&b=2', '50% off', 'a|b<c>d', 'x^y', 'say "hi" & go', 'plain', '%PATH%', 'a"b&c', 'q"x|y^z"w', 'e"5%', 'k&v', '100%'];
|
|
10
|
+
export const cmdlineSuite = {
|
|
11
|
+
name: 'cmdline',
|
|
12
|
+
title: 'cmd.exe quoting for npm-installed agents',
|
|
13
|
+
run: async (t) => {
|
|
14
|
+
t.section('command lines');
|
|
15
|
+
// guest/cmdline.ts is loaded in a child process with a throwaway launch/data folder (it reads session.json on load).
|
|
16
|
+
const quoteDir = t.at('quote');
|
|
17
|
+
t.write(path.join(quoteDir, 'launch', 'session.json'), '{}');
|
|
18
|
+
const agentCmd = t.write(path.join(quoteDir, 'agent.cmd'), '@"' + process.execPath + '" -e "console.log(JSON.stringify(process.argv.slice(1)))" %*\r\n');
|
|
19
|
+
const script = [
|
|
20
|
+
'const m = await import(process.argv[1]); const [cmd, tricky] = JSON.parse(process.argv[2]);',
|
|
21
|
+
'console.log(JSON.stringify({ normal: m.commandLine("C:\\\\W\\\\cmd.exe", ["/d", "/c", "C:\\\\a\\\\agent.cmd", "--model", "big one", "plain", ""]), direct: m.commandLine("C:\\\\a\\\\agent.exe", ["a&b", "50%"]),',
|
|
22
|
+
' url: m.cmdArgs(["https://x?a=1&b=2"]), pct: m.cmdArgs(["50%"]), live: m.commandLine("cmd.exe", ["/d", "/c", cmd, ...tricky]).slice(8) }));',
|
|
23
|
+
].join('\n');
|
|
24
|
+
const probe = await runNode(['--input-type=module', '-e', script, pathToFileURL(path.join(t.packageRoot, 'dist', 'guest', 'cmdline.js')).href, JSON.stringify([agentCmd, TRICKY_ARGUMENTS])], {
|
|
25
|
+
env: { ...process.env, AGENT_LAUNCH_DIR: path.join(quoteDir, 'launch'), AGENT_DATA_DIR: path.join(quoteDir, 'data') },
|
|
26
|
+
});
|
|
27
|
+
let quoting = {};
|
|
28
|
+
try {
|
|
29
|
+
quoting = JSON.parse(probe.out);
|
|
30
|
+
}
|
|
31
|
+
catch { /* reported below */ }
|
|
32
|
+
const whyEmpty = quoting.normal ? '' : 'the helper printed: ' + probe.out.trim().slice(0, 300);
|
|
33
|
+
t.check(quoting.normal === 'C:\\W\\cmd.exe /d /c C:\\a\\agent.cmd --model "big one" plain ""', 'ordinary arguments are quoted as before', whyEmpty + '\nactual: ' + quoting.normal);
|
|
34
|
+
t.check(quoting.direct === 'C:\\a\\agent.exe a&b 50%', 'a program started directly gets & and % unchanged', 'actual: ' + quoting.direct);
|
|
35
|
+
t.equal(quoting.url, '"https://x?a=1&b=2"', 'cmd /c arguments: & | < > ^ are quoted');
|
|
36
|
+
t.equal(quoting.pct, '"50"%""', 'cmd /c arguments: % is split out so no %name% expands');
|
|
37
|
+
// (A temp folder with spaces would also need cmd's own quote rules, so this end-to-end run skips those.)
|
|
38
|
+
if (isWin && quoting.live && !/\s/.test(agentCmd)) {
|
|
39
|
+
const echoed = spawnSync(process.env.ComSpec ?? 'cmd.exe', [quoting.live], { encoding: 'utf8', windowsVerbatimArguments: true, windowsHide: true });
|
|
40
|
+
t.equal(echoed.stdout.trim(), JSON.stringify(TRICKY_ARGUMENTS), 'cmd /d /c <agent>.cmd passes &, |, <, >, ^, % and quotes through to the agent unchanged');
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
t.skip('end-to-end cmd.exe quoting run (not Windows, or the temp path has spaces)');
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Loading the settings: built-in defaults, a user config file, and the example config that ships with the tool.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
import { DEFAULTS, loadConfig } from '../core/config.js';
|
|
5
|
+
import { readJsonc } from '../core/util.js';
|
|
6
|
+
export const configSuite = {
|
|
7
|
+
name: 'config',
|
|
8
|
+
title: 'built-in defaults, user config file, config.example.json',
|
|
9
|
+
run: async (t, f) => {
|
|
10
|
+
t.section('built-in defaults');
|
|
11
|
+
const builtIn = loadConfig();
|
|
12
|
+
t.equal({
|
|
13
|
+
configPath: builtIn.configPath,
|
|
14
|
+
guestWorkspace: builtIn.config.guestWorkspace,
|
|
15
|
+
headless: builtIn.config.sandbox.headless,
|
|
16
|
+
vGpu: builtIn.config.sandbox.vGpu,
|
|
17
|
+
gitAccess: builtIn.config.provisioning.git.access,
|
|
18
|
+
}, { configPath: null, guestWorkspace: 'auto', headless: false, vGpu: false, gitAccess: 'full' }, 'with no user config the built-in defaults apply (auto guest folder, headless off, vGPU off, git full)');
|
|
19
|
+
t.section('a user config file');
|
|
20
|
+
const customFile = t.writeJson(t.at('cfg.json'), {
|
|
21
|
+
sandbox: { headless: true },
|
|
22
|
+
toolchains: { rust: { enabled: false } },
|
|
23
|
+
mappings: [{ host: 'rel-folder', guest: 'C:\\Rel', readOnly: true }],
|
|
24
|
+
});
|
|
25
|
+
const custom = loadConfig(customFile);
|
|
26
|
+
t.equal({ headless: custom.config.sandbox.headless, rust: custom.config.toolchains.rust.enabled, python: custom.config.toolchains.python.enabled }, { headless: true, rust: false, python: 'auto' }, 'a config file only needs the keys it changes (deep merge over the defaults)');
|
|
27
|
+
await t.throws(() => loadConfig(t.at('missing.json')), 'Config file not found', 'an explicit --config that does not exist is an error');
|
|
28
|
+
t.dir('rel-folder');
|
|
29
|
+
const relativeRun = await f.run({}, { ...custom.config, storage: f.baseConfig.storage });
|
|
30
|
+
const mappedHosts = relativeRun.config.mappings.map((m) => m.host);
|
|
31
|
+
t.check(mappedHosts.includes(t.at('rel-folder')), 'relative mappings in the user config resolve against the config file\'s folder', 'mapped hosts: ' + JSON.stringify(mappedHosts));
|
|
32
|
+
t.section('config.example.json');
|
|
33
|
+
const examplePath = path.join(t.packageRoot, 'config.example.json');
|
|
34
|
+
t.equal(loadConfig(examplePath).config.defaultHarness, 'claude', 'config.example.json is valid');
|
|
35
|
+
const example = _.omit(readJsonc(examplePath), '$schema');
|
|
36
|
+
t.check(_.isEqual(example, DEFAULTS), 'config.example.json lists exactly the built-in defaults', 'the differences are in: ' + Object.keys(DEFAULTS).filter((key) => !_.isEqual(example[key], DEFAULTS[key])).join(', '));
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// `sandbox data` and `sandbox clean`: what a clean target may name, how sizes and removal treat links, and which old
|
|
2
|
+
// launch folders, logs and downloads are pruned.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { resolveCleanTargets } from '../cli/commands/data/clean.js';
|
|
6
|
+
import { removeTree } from '../cli/commands/data/remove.js';
|
|
7
|
+
import { findLinks } from '../cli/commands/repair/finders.js';
|
|
8
|
+
import { humanBytes, sizeOf } from '../core/disk.js';
|
|
9
|
+
import { pruneStorage } from '../host/cleanup.js';
|
|
10
|
+
export const dataSuite = {
|
|
11
|
+
name: 'data',
|
|
12
|
+
title: 'data clean targets, sizes and removal with links, storage cleanup',
|
|
13
|
+
run: async (t) => {
|
|
14
|
+
t.section('clean targets');
|
|
15
|
+
const storage = t.at('data-store');
|
|
16
|
+
const dataPath = path.join(storage, 'data', 'default');
|
|
17
|
+
const resolve = (...inputs) => resolveCleanTargets(inputs, dataPath, storage).map((r) => path.relative(storage, r.path));
|
|
18
|
+
const inData = (...parts) => path.join('data', 'default', ...parts);
|
|
19
|
+
t.equal(resolve('tools/go'), [inData('tools', 'go')], 'tools/<name> resolves inside the data folder');
|
|
20
|
+
t.equal(resolve('tools\\go'), [inData('tools', 'go')], 'either slash works');
|
|
21
|
+
t.equal(resolve('Tools/agents/claude'), [inData('tools', 'agents', 'claude')], 'tools/agents/<name> resolves, whatever the case');
|
|
22
|
+
t.equal(resolve('databases/postgres-18'), [inData('databases', 'postgres-18')], 'databases/<name> resolves');
|
|
23
|
+
t.equal(resolve('logs'), [inData('logs')], 'logs resolves');
|
|
24
|
+
t.equal(resolve('launches'), ['launches'], 'launches resolves to the storage folder\'s launches');
|
|
25
|
+
t.equal(resolve('state/claude'), [inData('state', 'claude')], 'state/<agent> resolves');
|
|
26
|
+
t.equal(resolve('tools', 'tools/go', 'tools/go'), [inData('tools')], 'nested and repeated targets collapse');
|
|
27
|
+
t.equal(resolve('all', 'logs'), [inData(), 'launches'], '`all` is the data folder plus launches');
|
|
28
|
+
t.check(Boolean(resolveCleanTargets(['state/claude'], dataPath, storage)[0].warning?.includes('login')), 'cleaning state warns about the login');
|
|
29
|
+
t.check(Boolean(resolveCleanTargets(['all'], dataPath, storage)[0].warning), 'cleaning everything carries a warning');
|
|
30
|
+
const badTargets = ['', '..', 'tools/..', 'tools/../../x', '../data', 'C:\\Windows', 'C:tools', '/etc', '\\\\server\\share', 'tools/*', 'tools/go:stream', 'tools/a/b', 'tools/agents/a/b', 'logs/guest.log', 'accounts', 'sync', 'windows', 'tools/.', 'tools/go.'];
|
|
31
|
+
const accepted = badTargets.filter((target) => {
|
|
32
|
+
try {
|
|
33
|
+
resolveCleanTargets([target], dataPath, storage);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
t.equal(accepted, [], 'rejected: .., absolute paths, drive letters, UNC, wildcards, streams, too deep, unknown areas');
|
|
41
|
+
await t.throws(() => resolveCleanTargets([], dataPath, storage), '', 'rejected: no target at all (that is not "everything")');
|
|
42
|
+
t.section('sizes and removal never follow links');
|
|
43
|
+
const sized = t.at('sized');
|
|
44
|
+
t.write(path.join(sized, 'a.bin'), 'x'.repeat(1000));
|
|
45
|
+
t.write(path.join(sized, 'sub', 'b.bin'), 'y'.repeat(24));
|
|
46
|
+
const outside = t.at('outside');
|
|
47
|
+
t.write(path.join(outside, 'keep.txt'), 'z'.repeat(5000));
|
|
48
|
+
let linked = false;
|
|
49
|
+
try {
|
|
50
|
+
fs.symlinkSync(outside, path.join(sized, 'sub', 'escape'), 'junction');
|
|
51
|
+
linked = true;
|
|
52
|
+
}
|
|
53
|
+
catch { /* no junctions here */ }
|
|
54
|
+
t.equal(sizeOf(sized), 1024, 'sizes count real files only' + (linked ? ' (a junction counts 0)' : ''));
|
|
55
|
+
t.equal([humanBytes(1536), humanBytes(0), humanBytes(3 * 1024 ** 3)], ['1.5 KB', '0 B', '3.0 GB'], 'sizes are shown in readable units');
|
|
56
|
+
if (linked)
|
|
57
|
+
t.equal(findLinks(sized).length, 1, 'repair finds a junction planted in the data folder');
|
|
58
|
+
removeTree(sized, t.temp);
|
|
59
|
+
t.check(!fs.existsSync(sized), 'a folder is removed');
|
|
60
|
+
t.check(fs.existsSync(path.join(outside, 'keep.txt')), 'removing a folder never deletes what a link inside points to');
|
|
61
|
+
t.section('storage cleanup');
|
|
62
|
+
const store = t.at('prune');
|
|
63
|
+
const aged = (p, days) => { const d = new Date(Date.now() - days * 86400000); fs.utimesSync(p, d, d); };
|
|
64
|
+
for (const [name, days] of [['old', 9], ['kept', 30], ['new', 1]]) {
|
|
65
|
+
aged(t.write(path.join(store, 'launches', name, 'agent.wsb'), 'x'), days);
|
|
66
|
+
aged(path.join(store, 'launches', name), days);
|
|
67
|
+
}
|
|
68
|
+
aged(t.write(path.join(store, 'data', 'd', 'logs', 'guest-old.log'), 'x'), 20);
|
|
69
|
+
t.write(path.join(store, 'data', 'd', 'logs', 'guest-new.log'), 'x');
|
|
70
|
+
const staging = t.dir('prune', 'data', 'd', 'tools', 'git.staging-ab12');
|
|
71
|
+
aged(staging, 2);
|
|
72
|
+
t.dir('prune', 'data', 'd', 'tools', 'node');
|
|
73
|
+
const pruned = pruneStorage(store, { keep: ['kept'] });
|
|
74
|
+
const left = (p) => fs.existsSync(path.join(store, p));
|
|
75
|
+
t.equal(pruned.launches, 1, 'cleanup removes one launch folder');
|
|
76
|
+
t.equal({ old: left('launches/old'), kept: left('launches/kept'), new: left('launches/new') }, { old: false, kept: true, new: true }, 'cleanup: launch folders older than 7 days go; the running launch and recent ones stay');
|
|
77
|
+
t.equal({ oldLog: left('data/d/logs/guest-old.log'), newLog: left('data/d/logs/guest-new.log') }, { oldLog: false, newLog: true }, 'cleanup: logs older than 14 days go');
|
|
78
|
+
t.equal({ interrupted: left('data/d/tools/git.staging-ab12'), realTool: left('data/d/tools/node') }, { interrupted: false, realTool: true }, 'cleanup: interrupted downloads go; real tools stay');
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// "What does this project need?": databases and toolchains detected from project files, and the plans built from them.
|
|
2
|
+
import { databasesFromServices, detectDatabases } from '../core/detect-databases.js';
|
|
3
|
+
import { DEFAULTS } from '../core/config.js';
|
|
4
|
+
import { databaseCatalog, planDatabases } from '../host/database-plan.js';
|
|
5
|
+
import { decideToolchains, resolveToolchains } from '../core/detect-toolchains.js';
|
|
6
|
+
const found = (files, host = []) => detectDatabases(files, host).map((d) => d.database);
|
|
7
|
+
const file = (path, text) => ({ path, text });
|
|
8
|
+
export const detectSuite = {
|
|
9
|
+
name: 'detect',
|
|
10
|
+
title: 'database and toolchain detection from project files',
|
|
11
|
+
run: (t) => {
|
|
12
|
+
t.section('databases from project files');
|
|
13
|
+
const compose = file('docker-compose.yml', 'services:\n db:\n image: postgres:16-alpine\n cache:\n image: "redis:7"\n dynamo:\n image: amazon/dynamodb-local:latest\n');
|
|
14
|
+
t.equal(found([compose]), ['postgres', 'redis', 'dynamodb'], 'compose services: postgres, redis and DynamoDB Local images');
|
|
15
|
+
t.equal(found([file('package.json', '{"dependencies":{"pg":"^8.11.0","ioredis":"5","@aws-sdk/client-dynamodb":"3"}}')]), ['postgres', 'redis', 'dynamodb'], 'package.json: pg, ioredis, @aws-sdk/client-dynamodb');
|
|
16
|
+
t.equal(found([file('package.json', '{"dependencies":{"pgadmin-ui":"1","mysql-logger":"2","express":"4"}}')]), [], 'package.json: names that only contain a driver name do not count');
|
|
17
|
+
t.equal(found([file('prisma/schema.prisma', 'datasource db {\n provider = "mysql"\n url = env("DATABASE_URL")\n}')]), ['mysql'], 'Prisma: the datasource provider decides');
|
|
18
|
+
t.equal(found([file('requirements.txt', 'django==5.0\npsycopg2-binary==2.9\npymongo>=4\n')]), ['postgres', 'mongodb'], 'Python requirements: psycopg2 and pymongo');
|
|
19
|
+
t.equal(found([file('Api/Api.csproj', '<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />')]), ['sqlserver'], '.NET: EF Core SQL Server provider');
|
|
20
|
+
t.equal(found([file('.env.example', 'DATABASE_URL=postgresql://user:pass@localhost:5432/app\nMONGO_URL=mongodb+srv://cluster/x\n')]), ['postgres', 'mongodb'], '.env templates: connection-string schemes');
|
|
21
|
+
t.equal(found([file('template.yaml', 'Resources:\n Table:\n Type: AWS::DynamoDB::Table\n')]), ['dynamodb'], 'SAM/CloudFormation: a DynamoDB table resource');
|
|
22
|
+
t.equal(found([file('go.mod', 'require (\n\tgithub.com/jackc/pgx/v5 v5.5.0\n\tgithub.com/aws/aws-sdk-go-v2/service/dynamodb v1.0.0\n)')]), ['postgres', 'dynamodb'], 'go.mod: pgx and the DynamoDB SDK');
|
|
23
|
+
const knex = [file('package.json', '{"dependencies":{"knex":"3"}}')];
|
|
24
|
+
t.equal(found(knex), [], 'an ORM that names no database uses nothing when this PC has none');
|
|
25
|
+
t.equal(found(knex, ['mysql']), ['mysql'], 'an ORM that names no database uses the one installed on this PC');
|
|
26
|
+
t.equal(databasesFromServices(['postgresql-x64-16', 'MySQL80', 'MSSQL$SQLEXPRESS', 'Spooler', 'Redis']), ['postgres', 'mysql', 'sqlserver', 'redis'], 'database services on this PC are recognised by name');
|
|
27
|
+
// Hostile workspace files (a cloned repository is untrusted): detection must stay linear.
|
|
28
|
+
const hostile = [
|
|
29
|
+
file('.env', 'Server=a;Database=a;'.repeat(12000)),
|
|
30
|
+
file('compose.yaml', 'services:\n x:\n image: ' + 'postgres'.repeat(20000) + '!\n'),
|
|
31
|
+
file('appsettings.json', 'Host=a;'.repeat(30000)),
|
|
32
|
+
];
|
|
33
|
+
const started = Date.now();
|
|
34
|
+
detectDatabases(hostile);
|
|
35
|
+
const took = Date.now() - started;
|
|
36
|
+
t.check(took < 2000, 'database detection stays fast on hostile workspace files (no catastrophic backtracking)', 'took ' + took + ' ms');
|
|
37
|
+
t.section('the database plan');
|
|
38
|
+
const plan = planDatabases({ ...DEFAULTS.databases, mongodb: { ...DEFAULTS.databases.mongodb, enabled: true }, redis: { ...DEFAULTS.databases.redis, enabled: false } }, 'unused', { files: [compose], host: [] });
|
|
39
|
+
t.equal(plan.map((p) => p.database + '@' + p.version + ':' + p.port), ['postgres@16:5432', 'mongodb@latest:27017', 'dynamodb@latest:8000'], 'plan: "auto" from the project (compose pins Postgres 16), true always, false never');
|
|
40
|
+
t.equal(plan.find((p) => p.database === 'dynamodb')?.adminPort, 8001, 'plan: DynamoDB gets its admin UI port');
|
|
41
|
+
const catalog = databaseCatalog({ ...DEFAULTS.databases, postgres: { ...DEFAULTS.databases.postgres, enabled: false, port: 6543 } });
|
|
42
|
+
t.equal(catalog.map((p) => p.database), ['postgres', 'mysql', 'mongodb', 'redis', 'dynamodb'], 'sbx-db catalog: every database that can run (not SQL Server), even ones off at boot');
|
|
43
|
+
t.equal(catalog[0].port, 6543, 'sbx-db catalog: configured ports are used');
|
|
44
|
+
t.section('toolchains from project files');
|
|
45
|
+
const tc = DEFAULTS.toolchains;
|
|
46
|
+
const goProject = decideToolchains(tc, ['README.md', 'go.mod', 'api/main.go'], ['python', 'rust']);
|
|
47
|
+
t.equal({ go: goProject.go.on, reason: goProject.go.reason }, { go: true, reason: 'go.mod' }, 'toolchains: a Go project gets Go (and says why)');
|
|
48
|
+
t.equal({ python: goProject.python.on, rust: goProject.rust.on, dotnet: goProject.dotnet.on, java: goProject.java.on }, { python: false, rust: false, dotnet: false, java: false }, 'toolchains: a Go project gets nothing it does not use, whatever this PC has');
|
|
49
|
+
const fresh = decideToolchains(tc, ['notes.txt'], ['python', 'dotnet']);
|
|
50
|
+
t.equal({ python: fresh.python.on, dotnet: fresh.dotnet.on, go: fresh.go.on }, { python: true, dotnet: true, go: false }, 'toolchains: a folder that is no project yet gets the languages this PC has');
|
|
51
|
+
t.includes(fresh.python.reason, 'this PC', 'toolchains: the reason names this PC');
|
|
52
|
+
const rusty = decideToolchains({ ...tc, python: { ...tc.python, enabled: true } }, ['Cargo.toml'], []);
|
|
53
|
+
t.equal({ rust: rusty.rust.on, cpp: rusty.cpp.on }, { rust: true, cpp: true }, 'toolchains: Rust brings its C/C++ linker');
|
|
54
|
+
t.equal({ python: rusty.python.on, reason: rusty.python.reason }, { python: true, reason: 'enabled in config' }, 'toolchains: true in config always wins');
|
|
55
|
+
const resolved = resolveToolchains(tc, goProject);
|
|
56
|
+
t.equal({ go: resolved.go.enabled, python: resolved.python.enabled }, { go: true, python: false }, 'the sandbox gets plain decisions instead of "auto"');
|
|
57
|
+
},
|
|
58
|
+
};
|