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
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { SYSTEM } from '../../shared/system.js';
|
|
6
|
+
import { spawnAsync } from '../../shared/download.js';
|
|
6
7
|
import { exists, isWin, launchDir, log, session, warn, workspace } from '../env.js';
|
|
7
8
|
const commandFor = (file) => {
|
|
8
9
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -16,21 +17,34 @@ const commandFor = (file) => {
|
|
|
16
17
|
return [SYSTEM.cmd(), ['/d', '/c', file]];
|
|
17
18
|
return null;
|
|
18
19
|
};
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
const BOOT = { root: path.join(launchDir, 'hooks'), cwd: workspace };
|
|
21
|
+
export const projectHooks = (name, cwd) => ({ root: path.join(launchDir, 'projects', name, 'hooks'), cwd });
|
|
22
|
+
const hooksOf = (stage, target) => {
|
|
23
|
+
const dir = path.join(target.root, stage);
|
|
21
24
|
if (!exists(dir))
|
|
22
|
-
return;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
return [];
|
|
26
|
+
return fs.readdirSync(dir).sort().flatMap((name) => { const command = commandFor(path.join(dir, name)); return command ? [{ name, command }] : []; });
|
|
27
|
+
};
|
|
28
|
+
const failed = (stage, name, status) => {
|
|
29
|
+
const message = 'Hook failed (' + stage + '/' + name + ', exit ' + status + ')';
|
|
30
|
+
if (session.hooks.failFast)
|
|
31
|
+
throw new Error(message);
|
|
32
|
+
warn(message);
|
|
33
|
+
};
|
|
34
|
+
export const runHooks = (stage, env, target = BOOT) => {
|
|
35
|
+
for (const { name, command } of hooksOf(stage, target)) {
|
|
36
|
+
log('Hook [' + stage + ']: ' + name);
|
|
37
|
+
const result = spawnSync(command[0], command[1], { stdio: 'inherit', cwd: target.cwd, env });
|
|
38
|
+
if (result.status !== 0)
|
|
39
|
+
failed(stage, name, result.status);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/** The same without blocking: the session daemon keeps answering other requests while a project's hooks run. */
|
|
43
|
+
export const runHooksAsync = async (stage, env, target) => {
|
|
44
|
+
for (const { name, command } of hooksOf(stage, target)) {
|
|
27
45
|
log('Hook [' + stage + ']: ' + name);
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
const message = 'Hook failed (' + stage + '/' + name + ', exit ' + result.status + ')';
|
|
32
|
-
if (session.hooks.failFast)
|
|
33
|
-
throw new Error(message);
|
|
34
|
-
warn(message);
|
|
46
|
+
const status = await spawnAsync(command[0], command[1], { stdio: 'ignore', cwd: target.cwd, env, windowsHide: true });
|
|
47
|
+
if (status !== 0)
|
|
48
|
+
failed(stage, name, status);
|
|
35
49
|
}
|
|
36
50
|
};
|
|
@@ -17,6 +17,8 @@ export interface CliTool {
|
|
|
17
17
|
subdir?: string;
|
|
18
18
|
/** What the tool is for (summary and docs). */
|
|
19
19
|
about: string;
|
|
20
|
+
/** Rough download size for the progress line. */
|
|
21
|
+
size: string;
|
|
20
22
|
}
|
|
21
23
|
export declare const CLI_TOOLS: readonly [{
|
|
22
24
|
readonly name: "gh";
|
|
@@ -24,12 +26,14 @@ export declare const CLI_TOOLS: readonly [{
|
|
|
24
26
|
readonly asset: (arch: Arch) => RegExp;
|
|
25
27
|
readonly exe: "gh.exe";
|
|
26
28
|
readonly about: "GitHub CLI";
|
|
29
|
+
readonly size: "15 MB";
|
|
27
30
|
}, {
|
|
28
31
|
readonly name: "zstd";
|
|
29
32
|
readonly repo: "facebook/zstd";
|
|
30
33
|
readonly asset: () => RegExp;
|
|
31
34
|
readonly exe: "zstd.exe";
|
|
32
35
|
readonly about: "Zstandard compression";
|
|
36
|
+
readonly size: "2 MB";
|
|
33
37
|
}];
|
|
34
38
|
export type CliName = (typeof CLI_TOOLS)[number]['name'] | 'openssl';
|
|
35
39
|
export declare const CLI_NAMES: readonly CliName[];
|
|
@@ -4,9 +4,9 @@ export const pwshAsset = (arch) => (arch === 'arm64' ? /^PowerShell-\d+\.\d+\.\d
|
|
|
4
4
|
export const binstallAsset = (arch) => (arch === 'arm64' ? /^cargo-binstall-aarch64-pc-windows-msvc\.zip$/ : /^cargo-binstall-x86_64-pc-windows-msvc\.zip$/);
|
|
5
5
|
export const CLI_TOOLS = [
|
|
6
6
|
// gh_2.102.0_windows_amd64.zip holds bin\gh.exe.
|
|
7
|
-
{ name: 'gh', repo: 'cli/cli', asset: (arch) => (arch === 'arm64' ? /^gh_\d+\.\d+\.\d+_windows_arm64\.zip$/ : /^gh_\d+\.\d+\.\d+_windows_amd64\.zip$/), exe: 'gh.exe', about: 'GitHub CLI' },
|
|
7
|
+
{ name: 'gh', repo: 'cli/cli', asset: (arch) => (arch === 'arm64' ? /^gh_\d+\.\d+\.\d+_windows_arm64\.zip$/ : /^gh_\d+\.\d+\.\d+_windows_amd64\.zip$/), exe: 'gh.exe', about: 'GitHub CLI', size: '15 MB' },
|
|
8
8
|
// zstd-v1.5.7-win64.zip; there is no arm64 build, and Windows on Arm runs the x64 one.
|
|
9
|
-
{ name: 'zstd', repo: 'facebook/zstd', asset: () => /^zstd-v\d+\.\d+\.\d+-win64\.zip$/, exe: 'zstd.exe', about: 'Zstandard compression' },
|
|
9
|
+
{ name: 'zstd', repo: 'facebook/zstd', asset: () => /^zstd-v\d+\.\d+\.\d+-win64\.zip$/, exe: 'zstd.exe', about: 'Zstandard compression', size: '2 MB' },
|
|
10
10
|
];
|
|
11
11
|
export const CLI_NAMES = [...CLI_TOOLS.map((t) => t.name), 'openssl'];
|
|
12
12
|
/**
|
|
@@ -2,39 +2,40 @@
|
|
|
2
2
|
// `openssl` shim onto the copy Git for Windows already ships.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { dryRun, errMsg, exists, log, mkdirp, stateDir, warn
|
|
5
|
+
import { dryRun, errMsg, exists, log, mkdirp, stateDir, warn } from '../env.js';
|
|
6
|
+
import { projectWorkspace } from '../project-context.js';
|
|
6
7
|
import { CLI_TOOLS, decideCli } from './catalogue.js';
|
|
7
8
|
import { arch, tool } from './common.js';
|
|
8
9
|
import { setupSingleExe } from './runtimes.js';
|
|
10
|
+
import { ensureCompiled } from '../helpers.js';
|
|
9
11
|
/** gh keeps `gh auth login` here, under the agent state (persists like Claude's own login; never copied from the PC). */
|
|
10
12
|
export const ghConfigDir = () => path.join(stateDir, 'gh');
|
|
11
13
|
const toolVars = {
|
|
12
14
|
gh: () => ({ GH_CONFIG_DIR: mkdirp(ghConfigDir()), GH_NO_UPDATE_NOTIFIER: '1', GH_NO_EXTENSION_UPDATE_NOTIFIER: '1' }),
|
|
13
15
|
};
|
|
14
16
|
/**
|
|
15
|
-
* openssl.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* by programs that use cmd /c; Rust's Command::new("openssl") looks only for openssl.exe.)
|
|
17
|
+
* tools\shims\openssl.exe (sandbox/openssl.cs), a launcher for Git's mingw64\bin\openssl.exe. A real .exe so programs
|
|
18
|
+
* that call CreateProcess directly (Rust's Command::new("openssl")) find it, and only the shim is on PATH: mingw64\bin
|
|
19
|
+
* itself would bring Git's runtime DLLs along and could shadow MinGW's gcc from the C/C++ step.
|
|
19
20
|
*/
|
|
20
21
|
const shimOpenssl = (env) => {
|
|
21
|
-
|
|
22
|
-
if (!exists(exe)) {
|
|
22
|
+
if (!exists(tool('git', 'mingw64', 'bin', 'openssl.exe'))) {
|
|
23
23
|
env.summary.push('openssl: not in this Git for Windows (skipped)');
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
env.
|
|
26
|
+
const shims = mkdirp(tool('shims'));
|
|
27
|
+
fs.copyFileSync(ensureCompiled('openssl'), path.join(shims, 'openssl.exe'));
|
|
28
|
+
// An earlier version wrote a .cmd; it would still win for shells that prefer it.
|
|
29
|
+
fs.rmSync(path.join(shims, 'openssl.cmd'), { force: true });
|
|
30
|
+
env.prepend.push(shims);
|
|
31
|
+
env.summary.push('openssl: Git for Windows\' copy');
|
|
31
32
|
};
|
|
32
33
|
/**
|
|
33
34
|
* Installs the tools toolchains.cli asks for. Settings still "auto" here (normally the host decides) use the same
|
|
34
35
|
* rules on the workspace as the host would. The gh rule is enforced here again, whatever the host sent.
|
|
35
36
|
*/
|
|
36
37
|
export const setupCli = async (env, settings, gitAccess, refresh = false) => {
|
|
37
|
-
const decisions = decideCli(settings, { gitAccess, hasGithubDir: exists(path.join(
|
|
38
|
+
const decisions = decideCli(settings, { gitAccess, hasGithubDir: exists(path.join(projectWorkspace(), '.github')) });
|
|
38
39
|
if (settings.gh && !decisions.gh.on) {
|
|
39
40
|
env.summary.push('gh: off (' + decisions.gh.reason + ')');
|
|
40
41
|
// A login from an earlier full-access sandbox is still on disk; the agent could read the token from there.
|
|
@@ -50,15 +51,22 @@ export const setupCli = async (env, settings, gitAccess, refresh = false) => {
|
|
|
50
51
|
if (!decisions[name].on)
|
|
51
52
|
continue;
|
|
52
53
|
try {
|
|
53
|
-
await setupSingleExe(env, name, entry.repo, entry.asset(arch), entry.exe, tool('cli', name), { refresh, subdir: entry.subdir });
|
|
54
|
+
await setupSingleExe(env, name, entry.repo, entry.asset(arch), entry.exe, tool('cli', name), { refresh, subdir: entry.subdir, size: entry.size });
|
|
54
55
|
Object.assign(env.vars, toolVars[name]?.() ?? {});
|
|
55
56
|
}
|
|
56
57
|
catch (e) {
|
|
57
58
|
// One tool failing never takes the others down.
|
|
58
|
-
warn(name + '
|
|
59
|
+
warn(name + ' could not be set up and is skipped; the other tools are not affected. Reason: ' + errMsg(e));
|
|
59
60
|
env.summary.push(entry.name + ': FAILED');
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
|
-
if (decisions.openssl.on)
|
|
63
|
-
|
|
63
|
+
if (decisions.openssl.on) {
|
|
64
|
+
try {
|
|
65
|
+
shimOpenssl(env);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
warn('openssl could not be set up and is skipped (Git Bash still has its own). Reason: ' + errMsg(e));
|
|
69
|
+
env.summary.push('openssl: FAILED');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
64
72
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReleaseAsset } from '../../shared/download.js';
|
|
1
2
|
/** What the toolchains contribute to the agent's environment. */
|
|
2
3
|
export interface ToolEnv {
|
|
3
4
|
vars: Record<string, string>;
|
|
@@ -16,9 +17,24 @@ export declare const versionOf: (exe: string, args?: string[], options?: {}) =>
|
|
|
16
17
|
* straight to the remembered versions instead of each waiting for the network; other errors (no such version) do not.
|
|
17
18
|
*/
|
|
18
19
|
export declare const resolveCached: (key: string, resolve: () => Promise<string>) => Promise<string>;
|
|
20
|
+
/** A single file download that lands as `file` only when complete (and checksum-verified): a cut-off one never looks finished. */
|
|
21
|
+
export declare const downloadFile: (url: string, file: string, sha256?: string | null) => Promise<void>;
|
|
22
|
+
interface GithubInstall {
|
|
23
|
+
/** The folder the archive is unpacked into (replaced atomically). */
|
|
24
|
+
dest: string;
|
|
25
|
+
/** What the progress line says is being downloaded, with its size; a function gets the release asset. */
|
|
26
|
+
what: string | ((asset: ReleaseAsset) => string);
|
|
27
|
+
/** Look past the latest release for a matching asset. */
|
|
28
|
+
searchOlder?: boolean;
|
|
29
|
+
/** installArchive's `locate` / `up`: keep only the folder holding this file. */
|
|
30
|
+
locate?: string;
|
|
31
|
+
up?: number;
|
|
32
|
+
}
|
|
33
|
+
/** The newest GitHub release asset matching `pattern`, checksum-verified, unpacked into `dest`. */
|
|
34
|
+
export declare const installFromGithub: (repo: string, pattern: RegExp, o: GithubInstall) => Promise<void>;
|
|
19
35
|
/** Runs one installer; a failure is reported and skipped so it never blocks the other toolchains. */
|
|
20
36
|
export declare const step: (name: string, env: ToolEnv, work: () => Promise<void>) => Promise<void>;
|
|
21
|
-
/** File names in the
|
|
37
|
+
/** File names in the project's folder and its direct sub-folders ("api/go.mod"); read once per project. */
|
|
22
38
|
export declare const workspaceEntries: () => string[];
|
|
23
39
|
/**
|
|
24
40
|
* true / false as configured. The host already turns "auto" into a decision at launch (core/detect-toolchains.ts);
|
|
@@ -35,3 +51,4 @@ export declare const versionFromFiles: (files: string[], parse?: (text: string,
|
|
|
35
51
|
* "npm@22.11.0"). One failing package is reported and skipped; it is tried again next boot.
|
|
36
52
|
*/
|
|
37
53
|
export declare const installOnce: (env: ToolEnv, group: string, packages: string[], install: (pkg: string) => void | Promise<void>) => Promise<void>;
|
|
54
|
+
export {};
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
import { download, githubAsset, installArchive } from '../../shared/download.js';
|
|
6
7
|
import { writeJsonAtomic } from '../../shared/rundir.js';
|
|
7
|
-
import { errMsg, execOutput, exists, log, session, toolsDir, warn
|
|
8
|
+
import { errMsg, execOutput, exists, log, session, toolsDir, warn } from '../env.js';
|
|
9
|
+
import { projectWorkspace } from '../project-context.js';
|
|
8
10
|
import { MARKERS } from '../../core/detect-toolchains.js';
|
|
9
11
|
import { projectUses, specFromVersionFile } from './versions.js';
|
|
10
12
|
export const emptyEnv = () => ({ vars: {}, prepend: [], append: [], summary: [] });
|
|
@@ -49,27 +51,47 @@ export const resolveCached = async (key, resolve) => {
|
|
|
49
51
|
offline = true;
|
|
50
52
|
if (!cached)
|
|
51
53
|
throw e;
|
|
52
|
-
warn('Could not look up ' + key + ' online (' + errMsg(e) + '); using ' + cached.value + ' from an earlier
|
|
54
|
+
warn('Could not look up the latest ' + key + ' online (' + errMsg(e) + '); using ' + cached.value + ' from an earlier start.');
|
|
53
55
|
return cached.value;
|
|
54
56
|
}
|
|
55
57
|
};
|
|
58
|
+
/** A single file download that lands as `file` only when complete (and checksum-verified): a cut-off one never looks finished. */
|
|
59
|
+
export const downloadFile = async (url, file, sha256 = null) => {
|
|
60
|
+
const part = file + '.part';
|
|
61
|
+
try {
|
|
62
|
+
await download(url, part, sha256);
|
|
63
|
+
fs.rmSync(file, { force: true });
|
|
64
|
+
fs.renameSync(part, file);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
fs.rmSync(part, { force: true });
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/** The newest GitHub release asset matching `pattern`, checksum-verified, unpacked into `dest`. */
|
|
71
|
+
export const installFromGithub = async (repo, pattern, o) => {
|
|
72
|
+
const asset = await githubAsset(repo, pattern, o.searchOlder);
|
|
73
|
+
log('Downloading ' + (typeof o.what === 'string' ? o.what : o.what(asset)) + ' ...');
|
|
74
|
+
await installArchive(asset.url, o.dest, { sha256: asset.sha256, fileName: asset.name, ...(o.locate ? { locate: o.locate } : {}), ...(o.up ? { up: o.up } : {}) });
|
|
75
|
+
};
|
|
56
76
|
/** Runs one installer; a failure is reported and skipped so it never blocks the other toolchains. */
|
|
57
77
|
export const step = async (name, env, work) => {
|
|
58
78
|
try {
|
|
59
79
|
await work();
|
|
60
80
|
}
|
|
61
81
|
catch (e) {
|
|
62
|
-
warn(name + '
|
|
82
|
+
warn(name + ' could not be set up and is skipped; everything else still works, and the next start tries again. Reason: ' + errMsg(e));
|
|
63
83
|
env.summary.push(name + ': FAILED');
|
|
64
84
|
}
|
|
65
85
|
};
|
|
66
86
|
// ---- which toolchains a project needs ------------------------------------------------------------------------------
|
|
67
87
|
const SKIP_DIRS = new Set(['node_modules', '.git', 'target', 'bin', 'obj', 'dist', 'build', 'vendor', '.venv', 'venv']);
|
|
68
|
-
|
|
69
|
-
/** File names in the
|
|
88
|
+
const entries = new Map();
|
|
89
|
+
/** File names in the project's folder and its direct sub-folders ("api/go.mod"); read once per project. */
|
|
70
90
|
export const workspaceEntries = () => {
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
const workspace = projectWorkspace();
|
|
92
|
+
const known = entries.get(workspace.toLowerCase());
|
|
93
|
+
if (known)
|
|
94
|
+
return known;
|
|
73
95
|
const list = (dir) => { try {
|
|
74
96
|
return fs.readdirSync(dir, { withFileTypes: true });
|
|
75
97
|
}
|
|
@@ -79,8 +101,9 @@ export const workspaceEntries = () => {
|
|
|
79
101
|
const top = list(workspace);
|
|
80
102
|
const below = top.filter((d) => d.isDirectory() && !d.name.startsWith('.') && !SKIP_DIRS.has(d.name.toLowerCase()))
|
|
81
103
|
.flatMap((d) => list(path.join(workspace, d.name)).filter((f) => f.isFile()).map((f) => d.name + '/' + f.name));
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
const found = [...top.filter((d) => d.isFile()).map((d) => d.name), ...below];
|
|
105
|
+
entries.set(workspace.toLowerCase(), found);
|
|
106
|
+
return found;
|
|
84
107
|
};
|
|
85
108
|
/**
|
|
86
109
|
* true / false as configured. The host already turns "auto" into a decision at launch (core/detect-toolchains.ts);
|
|
@@ -90,7 +113,7 @@ export const wanted = (setting, toolchain) => setting === 'auto' ? projectUses(w
|
|
|
90
113
|
/** The first of these version files in the workspace root, parsed (e.g. .ruby-version "3.3.1"). */
|
|
91
114
|
export const versionFromFiles = (files, parse = (t) => specFromVersionFile(t)) => {
|
|
92
115
|
for (const name of files) {
|
|
93
|
-
const file = path.join(
|
|
116
|
+
const file = path.join(projectWorkspace(), name);
|
|
94
117
|
if (!exists(file))
|
|
95
118
|
continue;
|
|
96
119
|
const spec = parse(fs.readFileSync(file, 'utf8'), name);
|
|
@@ -115,7 +138,7 @@ export const installOnce = async (env, group, packages, install) => {
|
|
|
115
138
|
writeJsonAtomic(installedFile(), { ...installed(), [group + ':' + pkg]: new Date().toISOString() });
|
|
116
139
|
}
|
|
117
140
|
catch (e) {
|
|
118
|
-
warn(
|
|
141
|
+
warn(pkg + ' (' + group + ') could not be installed and is skipped; the next start tries again. Reason: ' + errMsg(e));
|
|
119
142
|
}
|
|
120
143
|
}
|
|
121
144
|
const ok = packages.filter((p) => installed()[group + ':' + p]);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DatabasePlan } from '../../../core/types.js';
|
|
2
|
+
import { type ToolEnv } from '../common.js';
|
|
3
|
+
/**
|
|
4
|
+
* Amazon's DynamoDB Local (a Java program; the Java toolchain step runs first, else this installs the LTS JDK) and
|
|
5
|
+
* dynamodb-admin, the web UI to browse and edit tables, installed with the runner's own npm. Every AWS SDK picks the
|
|
6
|
+
* local endpoint up from AWS_ENDPOINT_URL_DYNAMODB, with dummy credentials (real ones never reach the sandbox).
|
|
7
|
+
*/
|
|
8
|
+
export declare const setupDynamo: (env: ToolEnv, plan: DatabasePlan) => Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// DynamoDB Local + dynamodb-admin.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { fetchText, installArchive } from '../../../shared/download.js';
|
|
6
|
+
import { execAsync, exists, log, logsDir, mkdirp } from '../../env.js';
|
|
7
|
+
import { ensureJdk } from '../java.js';
|
|
8
|
+
import { tool } from '../common.js';
|
|
9
|
+
import { portOpen, waitForPort } from '../../ports.js';
|
|
10
|
+
import { dataFor, startServer } from './server.js';
|
|
11
|
+
const DYNAMO_ZIP = 'https://d1ni2b6xgvw0s0.cloudfront.net/v2.x/dynamodb_local_latest.zip';
|
|
12
|
+
/**
|
|
13
|
+
* Amazon's DynamoDB Local (a Java program; the Java toolchain step runs first, else this installs the LTS JDK) and
|
|
14
|
+
* dynamodb-admin, the web UI to browse and edit tables, installed with the runner's own npm. Every AWS SDK picks the
|
|
15
|
+
* local endpoint up from AWS_ENDPOINT_URL_DYNAMODB, with dummy credentials (real ones never reach the sandbox).
|
|
16
|
+
*/
|
|
17
|
+
export const setupDynamo = async (env, plan) => {
|
|
18
|
+
const java = path.join(await ensureJdk(), 'bin', 'java.exe');
|
|
19
|
+
const home = tool('dynamodb-local');
|
|
20
|
+
if (!exists(path.join(home, 'DynamoDBLocal.jar'))) {
|
|
21
|
+
log('Downloading DynamoDB Local (~60 MB once) ...');
|
|
22
|
+
const sha256 = (await fetchText(DYNAMO_ZIP + '.sha256')).trim().split(/\s+/)[0];
|
|
23
|
+
await installArchive(DYNAMO_ZIP, home, { sha256, locate: 'DynamoDBLocal.jar', fileName: 'dynamodb_local.zip' });
|
|
24
|
+
}
|
|
25
|
+
const data = mkdirp(dataFor('dynamodb'));
|
|
26
|
+
// -sharedDb: one database whatever region/credentials a client uses, so the admin UI and the app see the same tables.
|
|
27
|
+
await startServer('dynamodb', java, ['-Djava.library.path=' + path.join(home, 'DynamoDBLocal_lib'), '-jar', path.join(home, 'DynamoDBLocal.jar'), '-sharedDb', '-dbPath', data, '-port', String(plan.port)], plan.port);
|
|
28
|
+
const endpoint = 'http://localhost:' + plan.port;
|
|
29
|
+
const aws = { AWS_ENDPOINT_URL_DYNAMODB: endpoint, DYNAMO_ENDPOINT: endpoint, AWS_ACCESS_KEY_ID: 'local', AWS_SECRET_ACCESS_KEY: 'local', AWS_REGION: 'us-east-1', AWS_DEFAULT_REGION: 'us-east-1' };
|
|
30
|
+
Object.assign(env.vars, aws);
|
|
31
|
+
env.summary.push('dynamodb (DynamoDB Local): ' + endpoint + ' (AWS SDKs use it automatically via AWS_ENDPOINT_URL_DYNAMODB; dummy credentials)');
|
|
32
|
+
if (!plan.adminPort)
|
|
33
|
+
return;
|
|
34
|
+
const prefix = tool('dynamodb-admin');
|
|
35
|
+
const script = path.join(prefix, 'node_modules', 'dynamodb-admin', 'dist', 'dynamodb-admin.js');
|
|
36
|
+
if (!exists(script)) {
|
|
37
|
+
log('Installing dynamodb-admin, the DynamoDB web UI (~20 MB once) ...');
|
|
38
|
+
const npmCli = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
|
|
39
|
+
await execAsync(process.execPath, [npmCli, 'install', '--global', '--prefix', prefix, 'dynamodb-admin', '--no-fund', '--no-audit'], { stdio: 'ignore' });
|
|
40
|
+
}
|
|
41
|
+
// The database matters more than its UI: a taken port is reported, not fatal.
|
|
42
|
+
if (await portOpen(plan.adminPort)) {
|
|
43
|
+
env.summary.push('dynamodb-admin: NOT started, port ' + plan.adminPort + ' is in use (change databases.dynamodb.adminPort)');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const out = fs.openSync(path.join(logsDir, 'dynamodb-admin.log'), 'a');
|
|
47
|
+
spawn(process.execPath, [script, '--port', String(plan.adminPort), '--dynamo-endpoint', endpoint, '--skip-default-credentials'], { detached: true, stdio: ['ignore', out, out], windowsHide: true, env: { ...process.env, ...aws } }).unref();
|
|
48
|
+
fs.closeSync(out);
|
|
49
|
+
if (!(await waitForPort(plan.adminPort, 30)))
|
|
50
|
+
throw new Error('dynamodb-admin did not start; see ' + path.join(logsDir, 'dynamodb-admin.log'));
|
|
51
|
+
env.summary.push('dynamodb-admin: http://localhost:' + plan.adminPort + ' (browse and edit tables in the sandbox\'s browser)');
|
|
52
|
+
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { DatabasePlan } from '
|
|
2
|
-
import { type ToolEnv } from '
|
|
3
|
-
export declare const portOpen: (port: number) => Promise<boolean>;
|
|
1
|
+
import type { DatabasePlan } from '../../../core/types.js';
|
|
2
|
+
import { type ToolEnv } from '../common.js';
|
|
4
3
|
export declare const DATABASE_NAMES: Record<DatabasePlan['database'], string>;
|
|
5
4
|
/** Stops a database (and DynamoDB's admin UI). PostgreSQL shuts down cleanly through pg_ctl; the others are ended. */
|
|
6
5
|
export declare const stopDatabase: (plan: DatabasePlan) => Promise<boolean>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Databases inside the sandbox: PostgreSQL, MariaDB (MySQL-compatible), MongoDB, Redis and DynamoDB Local. Binaries
|
|
2
|
+
// live in C:\AgentData\tools, data in C:\AgentData\databases (both persist), and every server listens on localhost only.
|
|
3
|
+
// Which ones run was decided on the host (session.databases): only what the project needs.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { SYSTEM } from '../../../shared/system.js';
|
|
8
|
+
import { dryRun, execAsync, exists, log } from '../../env.js';
|
|
9
|
+
import { tool } from '../common.js';
|
|
10
|
+
import { setupDynamo } from './dynamodb.js';
|
|
11
|
+
import { setupMongo } from './mongodb.js';
|
|
12
|
+
import { setupMysql } from './mysql.js';
|
|
13
|
+
import { postgresData, setupPostgres } from './postgres.js';
|
|
14
|
+
import { setupRedis } from './redis.js';
|
|
15
|
+
import { portOpen } from '../../ports.js';
|
|
16
|
+
import { duringSession } from '../../project-context.js';
|
|
17
|
+
const SETUP = { postgres: setupPostgres, mysql: setupMysql, mongodb: setupMongo, redis: setupRedis, dynamodb: setupDynamo };
|
|
18
|
+
export const DATABASE_NAMES = { postgres: 'PostgreSQL', mysql: 'MySQL (MariaDB)', mongodb: 'MongoDB', redis: 'Redis', dynamodb: 'DynamoDB Local', sqlserver: 'SQL Server' };
|
|
19
|
+
/** The process listening on a local TCP port (netstat: no PowerShell start-up cost), or null. */
|
|
20
|
+
const listenerPid = (port) => {
|
|
21
|
+
const out = spawnSync(SYSTEM.netstat(), ['-ano', '-p', 'TCP'], { encoding: 'utf8', windowsHide: true }).stdout ?? '';
|
|
22
|
+
const hit = out.split(/\r?\n/).map((l) => /^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)/.exec(l)).find((m) => m && Number(m[1]) === port);
|
|
23
|
+
return hit ? Number(hit[2]) : null;
|
|
24
|
+
};
|
|
25
|
+
/** Stops a database (and DynamoDB's admin UI). PostgreSQL shuts down cleanly through pg_ctl; the others are ended. */
|
|
26
|
+
export const stopDatabase = async (plan) => {
|
|
27
|
+
if (!(await portOpen(plan.port)))
|
|
28
|
+
return false;
|
|
29
|
+
if (plan.database === 'postgres' && exists(tool('postgres'))) {
|
|
30
|
+
for (const version of fs.readdirSync(tool('postgres'))) {
|
|
31
|
+
const data = postgresData(version);
|
|
32
|
+
const pgCtl = path.join(tool('postgres', version), 'bin', 'pg_ctl.exe');
|
|
33
|
+
if (exists(path.join(data, 'postmaster.pid')) && exists(pgCtl))
|
|
34
|
+
await execAsync(pgCtl, ['stop', '-D', data, '-m', 'fast', '-w'], { stdio: 'ignore' });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const port of [plan.port, plan.adminPort ?? 0]) {
|
|
38
|
+
const pid = port && listenerPid(port);
|
|
39
|
+
if (pid)
|
|
40
|
+
try {
|
|
41
|
+
process.kill(pid);
|
|
42
|
+
}
|
|
43
|
+
catch { /* already gone */ }
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
export const setupDatabase = async (env, plan) => {
|
|
48
|
+
if (plan.database === 'sqlserver') {
|
|
49
|
+
env.summary.push('sqlserver: NOT available (SQL Server cannot start inside Windows Sandbox). Switch the project to Postgres or SQLite for local work');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (dryRun) {
|
|
53
|
+
log('(dry run) would run ' + DATABASE_NAMES[plan.database] + ' on port ' + plan.port + ' (' + plan.reasons.join('; ') + ')');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// A project that joined later, while another project's server already listens there: they share it.
|
|
57
|
+
if (duringSession() && (await portOpen(plan.port))) {
|
|
58
|
+
env.summary.push(plan.database + ': shared, already running on port ' + plan.port);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await SETUP[plan.database](env, plan);
|
|
62
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// MongoDB Community, with mongosh.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fetchJson, installArchive } from '../../../shared/download.js';
|
|
4
|
+
import { exists, log, mkdirp } from '../../env.js';
|
|
5
|
+
import { installFromGithub, resolveCached, tool } from '../common.js';
|
|
6
|
+
import { clearStaleLocks, dataFor, startServer } from './server.js';
|
|
7
|
+
export const setupMongo = async (env, plan) => {
|
|
8
|
+
const find = async () => {
|
|
9
|
+
const { versions } = await fetchJson('https://downloads.mongodb.org/full.json');
|
|
10
|
+
const prod = versions.filter((v) => v.production_release);
|
|
11
|
+
const hit = plan.version === 'latest' ? prod[0] : prod.find((v) => v.version === plan.version || v.version.startsWith(plan.version + '.'));
|
|
12
|
+
const archive = hit?.downloads.find((d) => d.target === 'windows' && d.arch === 'x86_64' && d.edition === 'base')?.archive;
|
|
13
|
+
return hit && archive ? { version: hit.version, archive } : Promise.reject(new Error('No MongoDB ' + plan.version + ' for Windows'));
|
|
14
|
+
};
|
|
15
|
+
const version = await resolveCached('mongodb:' + plan.version, async () => (await find()).version);
|
|
16
|
+
const home = tool('mongodb', version);
|
|
17
|
+
if (!exists(path.join(home, 'bin', 'mongod.exe'))) {
|
|
18
|
+
const { archive } = await find();
|
|
19
|
+
log('Downloading MongoDB ' + version + ' (~400 MB once) ...');
|
|
20
|
+
await installArchive(archive.url, home, { sha256: archive.sha256, locate: 'mongod.exe', up: 1 });
|
|
21
|
+
}
|
|
22
|
+
const shell = tool('mongosh');
|
|
23
|
+
if (!exists(path.join(shell, 'mongosh.exe'))) {
|
|
24
|
+
await installFromGithub('mongodb-js/mongosh', /^mongosh-[\d.]+-win32-x64\.zip$/, { dest: shell, what: 'mongosh, the MongoDB shell (~50 MB once)', locate: 'mongosh.exe' });
|
|
25
|
+
}
|
|
26
|
+
const data = mkdirp(dataFor('mongodb'));
|
|
27
|
+
await clearStaleLocks(data, ['mongod.lock'], plan.port);
|
|
28
|
+
await startServer('mongodb', path.join(home, 'bin', 'mongod.exe'), ['--dbpath', data, '--bind_ip', '127.0.0.1', '--port', String(plan.port)], plan.port);
|
|
29
|
+
env.prepend.push(path.join(home, 'bin'), shell);
|
|
30
|
+
env.summary.push('mongodb ' + version + ': mongodb://127.0.0.1:' + plan.port + ' (mongosh on PATH)');
|
|
31
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// MariaDB, the MySQL-compatible server.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fetchJson, installArchive } from '../../../shared/download.js';
|
|
4
|
+
import { execAsync, exists, log } from '../../env.js';
|
|
5
|
+
import { resolveCached, tool } from '../common.js';
|
|
6
|
+
import { createOnce, dataFor, startServer } from './server.js';
|
|
7
|
+
const MARIA = 'https://downloads.mariadb.org/rest-api/mariadb/';
|
|
8
|
+
export const setupMysql = async (env, plan) => {
|
|
9
|
+
const series = await resolveCached('mariadb:' + plan.version, async () => {
|
|
10
|
+
if (plan.version !== 'lts')
|
|
11
|
+
return plan.version;
|
|
12
|
+
const { major_releases: all } = await fetchJson(MARIA);
|
|
13
|
+
return all.find((r) => r.release_status === 'Stable' && r.release_support_type === 'Long Term Support')?.release_id ?? Promise.reject(new Error('No MariaDB LTS release found'));
|
|
14
|
+
});
|
|
15
|
+
const latest = async () => {
|
|
16
|
+
const { releases } = await fetchJson(MARIA + series + '/latest/');
|
|
17
|
+
const [version, release] = Object.entries(releases)[0] ?? [];
|
|
18
|
+
const zip = release?.files.find((f) => /-winx64\.zip$/.test(f.file_name));
|
|
19
|
+
return zip ? { version, zip } : Promise.reject(new Error('No MariaDB ' + series + ' zip for Windows'));
|
|
20
|
+
};
|
|
21
|
+
const version = await resolveCached('mariadb-version:' + series, async () => (await latest()).version);
|
|
22
|
+
const home = tool('mariadb', version);
|
|
23
|
+
const bin = path.join(home, 'bin');
|
|
24
|
+
if (!exists(path.join(bin, 'mariadbd.exe'))) {
|
|
25
|
+
const { zip } = await latest();
|
|
26
|
+
log('Downloading MariaDB ' + version + ' (~90 MB once) ...');
|
|
27
|
+
await installArchive(zip.file_download_url.replace(/^http:/, 'https:'), home, { sha256: zip.checksum?.sha256sum ?? null, locate: 'mariadbd.exe', up: 1, fileName: zip.file_name });
|
|
28
|
+
}
|
|
29
|
+
const data = dataFor('mariadb');
|
|
30
|
+
await createOnce(data, 'mysql', async () => {
|
|
31
|
+
log('Creating the MariaDB database (first start) ...');
|
|
32
|
+
await execAsync(path.join(bin, 'mariadb-install-db.exe'), ['--datadir=' + data, '--port=' + plan.port], { stdio: 'ignore' });
|
|
33
|
+
});
|
|
34
|
+
await startServer('mysql', path.join(bin, 'mariadbd.exe'), ['--datadir=' + data, '--port=' + plan.port, '--bind-address=127.0.0.1', '--console'], plan.port);
|
|
35
|
+
Object.assign(env.vars, { MYSQL_HOST: '127.0.0.1', MYSQL_TCP_PORT: String(plan.port) });
|
|
36
|
+
env.prepend.push(bin);
|
|
37
|
+
env.summary.push('mysql (MariaDB ' + version + '): 127.0.0.1:' + plan.port + ', user root, no password (mysql://root@127.0.0.1:' + plan.port + ')');
|
|
38
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DatabasePlan } from '../../../core/types.js';
|
|
2
|
+
import { type ToolEnv } from '../common.js';
|
|
3
|
+
/** The data folder of a PostgreSQL major version (one cluster per major). */
|
|
4
|
+
export declare const postgresData: (version: string) => string;
|
|
5
|
+
export declare const setupPostgres: (env: ToolEnv, plan: DatabasePlan) => Promise<void>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// PostgreSQL from EnterpriseDB's Windows binaries (without pgAdmin).
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fetchJson, installArchive } from '../../../shared/download.js';
|
|
4
|
+
import { execAsync, exists, log, logsDir } from '../../env.js';
|
|
5
|
+
import { resolveCached, tool } from '../common.js';
|
|
6
|
+
import { portOpen } from '../../ports.js';
|
|
7
|
+
import { clearStaleLocks, createOnce, dataFor } from './server.js';
|
|
8
|
+
/** The data folder of a PostgreSQL major version (one cluster per major). */
|
|
9
|
+
export const postgresData = (version) => dataFor('postgres-' + version.split('.')[0]);
|
|
10
|
+
export const setupPostgres = async (env, plan) => {
|
|
11
|
+
const exact = await resolveCached('postgres:' + plan.version, async () => {
|
|
12
|
+
const versions = (await fetchJson('https://www.postgresql.org/versions.json')).filter((v) => v.supported);
|
|
13
|
+
const hit = plan.version === 'latest' ? versions.find((v) => v.current) ?? versions[versions.length - 1] : versions.find((v) => v.major === plan.version.split('.')[0]);
|
|
14
|
+
return hit ? hit.major + '.' + hit.latestMinor : Promise.reject(new Error('No supported PostgreSQL ' + plan.version));
|
|
15
|
+
});
|
|
16
|
+
const home = tool('postgres', exact);
|
|
17
|
+
const bin = path.join(home, 'bin');
|
|
18
|
+
if (!exists(path.join(bin, 'pg_ctl.exe'))) {
|
|
19
|
+
log('Downloading PostgreSQL ' + exact + ' (~330 MB once) ...');
|
|
20
|
+
// EDB publishes no checksums; the download is HTTPS from EnterpriseDB, PostgreSQL's official Windows packager.
|
|
21
|
+
await installArchive('https://get.enterprisedb.com/postgresql/postgresql-' + exact + '-1-windows-x64-binaries.zip', home, {
|
|
22
|
+
locate: 'pg_ctl.exe', up: 1, fileName: 'postgresql-' + exact + '.zip', members: ['pgsql/bin', 'pgsql/lib', 'pgsql/share', 'pgsql/include'],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const data = postgresData(exact);
|
|
26
|
+
await createOnce(data, 'PG_VERSION', async () => {
|
|
27
|
+
log('Creating the PostgreSQL database (first start) ...');
|
|
28
|
+
await execAsync(path.join(bin, 'initdb.exe'), ['-D', data, '-U', 'postgres', '-A', 'trust', '-E', 'UTF8', '--locale=C'], { stdio: 'ignore' });
|
|
29
|
+
});
|
|
30
|
+
if (await portOpen(plan.port))
|
|
31
|
+
throw new Error('port ' + plan.port + ' is already in use (change databases.postgres.port)');
|
|
32
|
+
await clearStaleLocks(data, ['postmaster.pid'], plan.port);
|
|
33
|
+
// postgres.exe refuses to run as an administrator (the sandbox user is one); pg_ctl drops the rights first.
|
|
34
|
+
await execAsync(path.join(bin, 'pg_ctl.exe'), ['start', '-D', data, '-w', '-t', '60', '-l', path.join(logsDir, 'postgres.log'), '-o', '-p ' + plan.port + ' -c listen_addresses=localhost'], { stdio: 'ignore' });
|
|
35
|
+
Object.assign(env.vars, { PGHOST: 'localhost', PGPORT: String(plan.port), PGUSER: 'postgres' });
|
|
36
|
+
env.prepend.push(bin);
|
|
37
|
+
env.summary.push('postgres ' + exact + ': localhost:' + plan.port + ', user postgres, no password (postgres://postgres@localhost:' + plan.port + '/postgres)');
|
|
38
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Redis (the maintained community Windows build of upstream Redis).
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { exists, mkdirp } from '../../env.js';
|
|
4
|
+
import { installFromGithub, tool } from '../common.js';
|
|
5
|
+
import { dataFor, startServer } from './server.js';
|
|
6
|
+
export const setupRedis = async (env, plan) => {
|
|
7
|
+
const home = tool('redis');
|
|
8
|
+
if (!exists(path.join(home, 'redis-server.exe'))) {
|
|
9
|
+
// Redis has no official Windows build; this one is built from upstream Redis with MSYS2.
|
|
10
|
+
await installFromGithub('redis-windows/redis-windows', /^Redis-[\d.]+-Windows-x64-msys2\.zip$/, { dest: home, what: (a) => 'Redis (' + a.name + ', ~10 MB once)', locate: 'redis-server.exe' });
|
|
11
|
+
}
|
|
12
|
+
const data = mkdirp(dataFor('redis'));
|
|
13
|
+
await startServer('redis', path.join(home, 'redis-server.exe'), ['--port', String(plan.port), '--bind', '127.0.0.1', '--dir', data.replace(/\\/g, '/'), '--appendonly', 'yes'], plan.port);
|
|
14
|
+
env.prepend.push(home);
|
|
15
|
+
env.summary.push('redis: redis://127.0.0.1:' + plan.port + ' (redis-cli on PATH)');
|
|
16
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** C:\AgentData\databases\<name>: the data survives the sandbox. */
|
|
2
|
+
export declare const dataFor: (name: string) => string;
|
|
3
|
+
/** Starts a server detached (it outlives the boot script) with its output in logs\<name>.log, and waits for its port. */
|
|
4
|
+
export declare const startServer: (name: string, exe: string, args: string[], port: number) => Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* Lock files a sandbox that was closed without shutting the server down leaves behind. Only when nothing listens on
|
|
7
|
+
* the port: a server already running in this VM keeps its lock.
|
|
8
|
+
*/
|
|
9
|
+
export declare const clearStaleLocks: (dir: string, files: string[], port: number) => Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Creates a database folder with `create` (initdb, install-db) unless `marker` is there. A failed or interrupted run
|
|
12
|
+
* removes the folder again when this call made it, so the half-made one is not taken for a finished database next boot.
|
|
13
|
+
*/
|
|
14
|
+
export declare const createOnce: (data: string, marker: string, create: () => Promise<void>) => Promise<void>;
|