windows-development-sandbox 0.0.0-stage → 1.0.5

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.
Files changed (231) hide show
  1. package/.sandbox-config.example.json +14 -0
  2. package/README.md +78 -2
  3. package/config.example.json +332 -0
  4. package/dist/api.d.ts +31 -0
  5. package/dist/api.js +30 -0
  6. package/dist/bin/agent-sandbox.d.ts +2 -0
  7. package/dist/bin/agent-sandbox.js +3 -0
  8. package/dist/bin/claude-host.d.ts +2 -0
  9. package/dist/bin/claude-host.js +5 -0
  10. package/dist/bin/claude-sbx.d.ts +2 -0
  11. package/dist/bin/claude-sbx.js +3 -0
  12. package/dist/bin/codex-sbx.d.ts +2 -0
  13. package/dist/bin/codex-sbx.js +3 -0
  14. package/dist/bin/gemini-sbx.d.ts +2 -0
  15. package/dist/bin/gemini-sbx.js +3 -0
  16. package/dist/bin/sandbox.d.ts +2 -0
  17. package/dist/bin/sandbox.js +4 -0
  18. package/dist/bin/sbx-hide.d.ts +2 -0
  19. package/dist/bin/sbx-hide.js +3 -0
  20. package/dist/bin/sbx-show.d.ts +2 -0
  21. package/dist/bin/sbx-show.js +3 -0
  22. package/dist/cli/args.d.ts +32 -0
  23. package/dist/cli/args.js +111 -0
  24. package/dist/cli/commands/accounts.d.ts +2 -0
  25. package/dist/cli/commands/accounts.js +52 -0
  26. package/dist/cli/commands/context.d.ts +21 -0
  27. package/dist/cli/commands/context.js +54 -0
  28. package/dist/cli/commands/data.d.ts +46 -0
  29. package/dist/cli/commands/data.js +249 -0
  30. package/dist/cli/commands/index.d.ts +8 -0
  31. package/dist/cli/commands/index.js +8 -0
  32. package/dist/cli/commands/launch.d.ts +5 -0
  33. package/dist/cli/commands/launch.js +152 -0
  34. package/dist/cli/commands/maintenance.d.ts +8 -0
  35. package/dist/cli/commands/maintenance.js +125 -0
  36. package/dist/cli/commands/repair.d.ts +59 -0
  37. package/dist/cli/commands/repair.js +332 -0
  38. package/dist/cli/commands/sessions.d.ts +11 -0
  39. package/dist/cli/commands/sessions.js +113 -0
  40. package/dist/cli/commands/setup.d.ts +45 -0
  41. package/dist/cli/commands/setup.js +152 -0
  42. package/dist/cli/index.d.ts +14 -0
  43. package/dist/cli/index.js +96 -0
  44. package/dist/cli/manifest.d.ts +95 -0
  45. package/dist/cli/manifest.js +87 -0
  46. package/dist/cli/menu.d.ts +22 -0
  47. package/dist/cli/menu.js +187 -0
  48. package/dist/cli/settings.d.ts +11 -0
  49. package/dist/cli/settings.js +156 -0
  50. package/dist/core/config.d.ts +17 -0
  51. package/dist/core/config.js +103 -0
  52. package/dist/core/detect-databases.d.ts +15 -0
  53. package/dist/core/detect-databases.js +147 -0
  54. package/dist/core/detect-pwsh.d.ts +18 -0
  55. package/dist/core/detect-pwsh.js +111 -0
  56. package/dist/core/detect-toolchains.d.ts +30 -0
  57. package/dist/core/detect-toolchains.js +58 -0
  58. package/dist/core/local-config.d.ts +12 -0
  59. package/dist/core/local-config.js +64 -0
  60. package/dist/core/schema.d.ts +604 -0
  61. package/dist/core/schema.js +264 -0
  62. package/dist/core/types.d.ts +162 -0
  63. package/dist/core/types.js +1 -0
  64. package/dist/core/util.d.ts +53 -0
  65. package/dist/core/util.js +145 -0
  66. package/dist/core/workspace.d.ts +23 -0
  67. package/dist/core/workspace.js +162 -0
  68. package/dist/guest/daemon.d.ts +1 -0
  69. package/dist/guest/daemon.js +186 -0
  70. package/dist/guest/db.d.ts +1 -0
  71. package/dist/guest/db.js +60 -0
  72. package/dist/guest/env.d.ts +49 -0
  73. package/dist/guest/env.js +99 -0
  74. package/dist/guest/gitguard.d.ts +7 -0
  75. package/dist/guest/gitguard.js +60 -0
  76. package/dist/guest/gitrules.d.ts +2 -0
  77. package/dist/guest/gitrules.js +11 -0
  78. package/dist/guest/guide.d.ts +12 -0
  79. package/dist/guest/guide.js +68 -0
  80. package/dist/guest/main.d.ts +1 -0
  81. package/dist/guest/main.js +120 -0
  82. package/dist/guest/ptyhost.d.ts +40 -0
  83. package/dist/guest/ptyhost.js +94 -0
  84. package/dist/guest/setup/agents.d.ts +8 -0
  85. package/dist/guest/setup/agents.js +73 -0
  86. package/dist/guest/setup/claude.d.ts +21 -0
  87. package/dist/guest/setup/claude.js +138 -0
  88. package/dist/guest/setup/environment.d.ts +6 -0
  89. package/dist/guest/setup/environment.js +65 -0
  90. package/dist/guest/setup/hooks.d.ts +1 -0
  91. package/dist/guest/setup/hooks.js +36 -0
  92. package/dist/guest/setup/network.d.ts +10 -0
  93. package/dist/guest/setup/network.js +99 -0
  94. package/dist/guest/setup/policy.d.ts +3 -0
  95. package/dist/guest/setup/policy.js +18 -0
  96. package/dist/guest/setup/user.d.ts +7 -0
  97. package/dist/guest/setup/user.js +47 -0
  98. package/dist/guest/toolchains/binstall.d.ts +7 -0
  99. package/dist/guest/toolchains/binstall.js +55 -0
  100. package/dist/guest/toolchains/catalogue.d.ts +51 -0
  101. package/dist/guest/toolchains/catalogue.js +31 -0
  102. package/dist/guest/toolchains/cli.d.ts +10 -0
  103. package/dist/guest/toolchains/cli.js +64 -0
  104. package/dist/guest/toolchains/common.d.ts +37 -0
  105. package/dist/guest/toolchains/common.js +124 -0
  106. package/dist/guest/toolchains/databases.d.ts +7 -0
  107. package/dist/guest/toolchains/databases.js +226 -0
  108. package/dist/guest/toolchains/git.d.ts +3 -0
  109. package/dist/guest/toolchains/git.js +112 -0
  110. package/dist/guest/toolchains/go.d.ts +3 -0
  111. package/dist/guest/toolchains/go.js +33 -0
  112. package/dist/guest/toolchains/index.d.ts +13 -0
  113. package/dist/guest/toolchains/index.js +122 -0
  114. package/dist/guest/toolchains/java.d.ts +8 -0
  115. package/dist/guest/toolchains/java.js +96 -0
  116. package/dist/guest/toolchains/managers.d.ts +16 -0
  117. package/dist/guest/toolchains/managers.js +82 -0
  118. package/dist/guest/toolchains/msvc.d.ts +10 -0
  119. package/dist/guest/toolchains/msvc.js +216 -0
  120. package/dist/guest/toolchains/native.d.ts +12 -0
  121. package/dist/guest/toolchains/native.js +220 -0
  122. package/dist/guest/toolchains/node.d.ts +3 -0
  123. package/dist/guest/toolchains/node.js +132 -0
  124. package/dist/guest/toolchains/pwsh.d.ts +4 -0
  125. package/dist/guest/toolchains/pwsh.js +14 -0
  126. package/dist/guest/toolchains/python.d.ts +3 -0
  127. package/dist/guest/toolchains/python.js +47 -0
  128. package/dist/guest/toolchains/runtimes.d.ts +17 -0
  129. package/dist/guest/toolchains/runtimes.js +40 -0
  130. package/dist/guest/toolchains/scripting.d.ts +10 -0
  131. package/dist/guest/toolchains/scripting.js +137 -0
  132. package/dist/guest/toolchains/versions.d.ts +47 -0
  133. package/dist/guest/toolchains/versions.js +77 -0
  134. package/dist/guest/toolchains/vscode.d.ts +2 -0
  135. package/dist/guest/toolchains/vscode.js +53 -0
  136. package/dist/guest/view.d.ts +1 -0
  137. package/dist/guest/view.js +22 -0
  138. package/dist/host/accounts.d.ts +32 -0
  139. package/dist/host/accounts.js +119 -0
  140. package/dist/host/channel.d.ts +37 -0
  141. package/dist/host/channel.js +180 -0
  142. package/dist/host/claude-profile.d.ts +15 -0
  143. package/dist/host/claude-profile.js +103 -0
  144. package/dist/host/cleanup.d.ts +23 -0
  145. package/dist/host/cleanup.js +66 -0
  146. package/dist/host/generate.d.ts +15 -0
  147. package/dist/host/generate.js +185 -0
  148. package/dist/host/personalize.d.ts +10 -0
  149. package/dist/host/personalize.js +97 -0
  150. package/dist/host/platform.d.ts +17 -0
  151. package/dist/host/platform.js +114 -0
  152. package/dist/host/project-scan.d.ts +23 -0
  153. package/dist/host/project-scan.js +173 -0
  154. package/dist/host/repo-guard.d.ts +23 -0
  155. package/dist/host/repo-guard.js +128 -0
  156. package/dist/host/secrets.d.ts +14 -0
  157. package/dist/host/secrets.js +133 -0
  158. package/dist/host/terminal.d.ts +32 -0
  159. package/dist/host/terminal.js +127 -0
  160. package/dist/host/window.d.ts +4 -0
  161. package/dist/host/window.js +68 -0
  162. package/dist/runtime/daemon.js +20291 -0
  163. package/dist/runtime/db.js +20423 -0
  164. package/dist/runtime/gitguard.js +84 -0
  165. package/dist/runtime/main.js +22398 -0
  166. package/dist/runtime/view.js +19966 -0
  167. package/dist/selftest/cli.test.d.ts +2 -0
  168. package/dist/selftest/cli.test.js +30 -0
  169. package/dist/selftest/config.test.d.ts +3 -0
  170. package/dist/selftest/config.test.js +74 -0
  171. package/dist/selftest/data.test.d.ts +2 -0
  172. package/dist/selftest/data.test.js +73 -0
  173. package/dist/selftest/detect.test.d.ts +2 -0
  174. package/dist/selftest/detect.test.js +42 -0
  175. package/dist/selftest/fixtures.d.ts +232 -0
  176. package/dist/selftest/fixtures.js +63 -0
  177. package/dist/selftest/guest.test.d.ts +3 -0
  178. package/dist/selftest/guest.test.js +61 -0
  179. package/dist/selftest/harness.d.ts +14 -0
  180. package/dist/selftest/harness.js +36 -0
  181. package/dist/selftest/helpers.test.d.ts +2 -0
  182. package/dist/selftest/helpers.test.js +125 -0
  183. package/dist/selftest/host.test.d.ts +2 -0
  184. package/dist/selftest/host.test.js +71 -0
  185. package/dist/selftest/index.d.ts +1 -0
  186. package/dist/selftest/index.js +56 -0
  187. package/dist/selftest/live.test.d.ts +7 -0
  188. package/dist/selftest/live.test.js +127 -0
  189. package/dist/selftest/mirror.test.d.ts +3 -0
  190. package/dist/selftest/mirror.test.js +74 -0
  191. package/dist/selftest/security.test.d.ts +3 -0
  192. package/dist/selftest/security.test.js +143 -0
  193. package/dist/selftest/setup.test.d.ts +2 -0
  194. package/dist/selftest/setup.test.js +23 -0
  195. package/dist/selftest/sync.test.d.ts +3 -0
  196. package/dist/selftest/sync.test.js +82 -0
  197. package/dist/shared/download.d.ts +58 -0
  198. package/dist/shared/download.js +200 -0
  199. package/dist/shared/guest-paths.d.ts +30 -0
  200. package/dist/shared/guest-paths.js +61 -0
  201. package/dist/shared/netrules.d.ts +18 -0
  202. package/dist/shared/netrules.js +98 -0
  203. package/dist/shared/paths.d.ts +9 -0
  204. package/dist/shared/paths.js +22 -0
  205. package/dist/shared/pty-client.d.ts +86 -0
  206. package/dist/shared/pty-client.js +227 -0
  207. package/dist/shared/rundir.d.ts +30 -0
  208. package/dist/shared/rundir.js +86 -0
  209. package/dist/shared/safe-remove.d.ts +10 -0
  210. package/dist/shared/safe-remove.js +55 -0
  211. package/dist/shared/schema.d.ts +84 -0
  212. package/dist/shared/schema.js +51 -0
  213. package/dist/shared/system.d.ts +15 -0
  214. package/dist/shared/system.js +19 -0
  215. package/dist/sync/claude-mem.d.ts +8 -0
  216. package/dist/sync/claude-mem.js +77 -0
  217. package/dist/sync/history.d.ts +30 -0
  218. package/dist/sync/history.js +129 -0
  219. package/dist/sync/watcher.d.ts +13 -0
  220. package/dist/sync/watcher.js +116 -0
  221. package/hooks/post-agent/README.txt +1 -0
  222. package/hooks/pre-provision/README.txt +1 -0
  223. package/package.json +91 -4
  224. package/sandbox/bootstrap.ps1 +258 -0
  225. package/sandbox/gitshim.cs +35 -0
  226. package/sandbox/logon.cmd +16 -0
  227. package/sandbox/ptyhost.cs +603 -0
  228. package/sandbox/trust.cmd +35 -0
  229. package/sandbox/vswhere.cs +83 -0
  230. package/schemas/config.schema.json +1062 -0
  231. package/schemas/sandbox-config.schema.json +952 -0
@@ -0,0 +1,23 @@
1
+ import type { Config, Overrides, ResolvedRun, RunRequest, ToolVersions } from './types.js';
2
+ export declare const askYesNo: (question: string) => Promise<boolean>;
3
+ /** A read/write folder mapped into the sandbox must not contain, or sit inside, any protected folder. */
4
+ export declare const assertSafeMapping: (folder: string, packageRoot: string, storageRoot: string) => void;
5
+ /** The agent gets read/write access to the workspace, so refuse folders that would let it reach the tool itself, its storage, or the OS. */
6
+ export declare const assertSafeWorkspace: (workspace: string, packageRoot: string, storageRoot: string) => void;
7
+ /**
8
+ * 'auto' guest workspace: C:\Workspace\<folder name>. Keeping the real folder name means Claude's per-project history
9
+ * and claude-mem's project name match this PC instead of every project being "Workspace".
10
+ */
11
+ export declare const autoGuestWorkspace: (workspaceHost: string) => string;
12
+ /** The Node version a Volta project pins ("volta": { "node": "20.11.1" } in package.json), so nvm defaults to it too. */
13
+ export declare const voltaNode: (workspace: string) => string | null;
14
+ /**
15
+ * Picks tool versions. Highest priority first: command line, the folder's .sandbox-config.json,
16
+ * version files in the workspace (.nvmrc, .node-version, .python-version), the tool config, this PC's node version.
17
+ */
18
+ export declare const resolveVersions: (config: Config, workspace: string, local: Overrides, req: {
19
+ nodeVersion?: string;
20
+ pythonVersion?: string;
21
+ }, hostNodeVersion: string) => ToolVersions;
22
+ /** Precedence (lowest to highest): built-in defaults, user config, .sandbox-config.json in the workspace, command-line flags. */
23
+ export declare const resolveRun: (req: RunRequest, hostNodeVersion?: string) => Promise<ResolvedRun>;
@@ -0,0 +1,162 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline';
4
+ import { z } from 'zod';
5
+ import { userDir } from './config.js';
6
+ import { planMirrorWorkspace } from '../shared/guest-paths.js';
7
+ import { DATABASES } from './detect-databases.js';
8
+ import { applyOverrides, describeOverrides, isTrusted, normalizeOverrides, readLocalConfig, saveTrust, trustStorePath } from './local-config.js';
9
+ import { dirExists, expandPath, fail, fileExists, isInsideOrEqual, isRootPath, isUserProfile } from './util.js';
10
+ export const askYesNo = async (question) => {
11
+ if (!process.stdin.isTTY)
12
+ return false;
13
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
14
+ const answer = await new Promise((resolve) => rl.question(question + ' [y/N] ', resolve));
15
+ rl.close();
16
+ return /^y(es)?$/i.test(answer.trim());
17
+ };
18
+ /** Folders the agent must never be able to write: the tool itself, its storage and your settings for it. */
19
+ const protectedFolders = (packageRoot, storageRoot) => [
20
+ ['this tool\'s install folder', packageRoot],
21
+ ['the sandbox storage folder', storageRoot],
22
+ ['your sandbox settings folder', userDir()],
23
+ ];
24
+ /** A read/write folder mapped into the sandbox must not contain, or sit inside, any protected folder. */
25
+ export const assertSafeMapping = (folder, packageRoot, storageRoot) => {
26
+ if (isRootPath(folder) || isUserProfile(folder))
27
+ fail('A whole drive or user profile cannot be mapped into the sandbox: ' + folder, 'Map a specific folder instead.');
28
+ for (const [label, dir] of protectedFolders(packageRoot, storageRoot)) {
29
+ if (isInsideOrEqual(dir, folder) || isInsideOrEqual(folder, dir))
30
+ fail(folder + ' cannot be mapped into the sandbox: it overlaps ' + label + ' (' + dir + ').', 'Map a folder that is not inside (and does not contain) ' + dir + '.');
31
+ }
32
+ };
33
+ /** The agent gets read/write access to the workspace, so refuse folders that would let it reach the tool itself, its storage, or the OS. */
34
+ export const assertSafeWorkspace = (workspace, packageRoot, storageRoot) => {
35
+ if (isRootPath(workspace))
36
+ fail('A whole drive cannot be the workspace: ' + workspace, 'Run sandbox claude from a project folder (or pass --workspace <folder>).');
37
+ if (isUserProfile(workspace))
38
+ fail('Your whole user profile cannot be the workspace: ' + workspace, 'Run sandbox claude from a project folder (or pass --workspace <folder>).');
39
+ for (const [label, dir] of protectedFolders(packageRoot, storageRoot)) {
40
+ if (isInsideOrEqual(dir, workspace))
41
+ fail(workspace + ' cannot be the workspace: it is inside ' + label + ' (' + dir + '), so the agent could change the code that runs on this PC.', dir === packageRoot ? 'Developing windows-development-sandbox itself? Install it as a copy instead of a link: run `npm run install:global` in the repository, then sandbox claude works there.' : 'Run sandbox claude from a project folder somewhere else.');
42
+ if (isInsideOrEqual(workspace, dir))
43
+ fail(workspace + ' cannot be the workspace: it contains ' + label + ' (' + dir + '), which the agent must not be able to change.', 'Run sandbox claude from a project folder that does not contain it.');
44
+ }
45
+ for (const name of ['WINDIR', 'ProgramFiles', 'ProgramFiles(x86)', 'ProgramData']) {
46
+ const dir = process.env[name];
47
+ if (dir && isInsideOrEqual(dir, workspace))
48
+ fail('A system folder cannot be the workspace: ' + workspace, 'Run sandbox claude from a project folder.');
49
+ }
50
+ };
51
+ /**
52
+ * 'auto' guest workspace: C:\Workspace\<folder name>. Keeping the real folder name means Claude's per-project history
53
+ * and claude-mem's project name match this PC instead of every project being "Workspace".
54
+ */
55
+ export const autoGuestWorkspace = (workspaceHost) => {
56
+ const name = path.basename(workspaceHost).replace(/[^A-Za-z0-9._ -]/g, '_').replace(/^[. ]+|[. ]+$/g, '');
57
+ return 'C:\\Workspace\\' + (name || 'project');
58
+ };
59
+ const firstLine = (file) => {
60
+ if (!fileExists(file))
61
+ return null;
62
+ const line = fs.readFileSync(file, 'utf8').replace(/^/, '').split(/\r?\n/).map((l) => l.replace(/#.*$/, '').trim()).find((l) => l !== '');
63
+ return line ?? null;
64
+ };
65
+ const VOLTA_PIN = 'package.json volta.node';
66
+ const PackageJson = z.object({ volta: z.object({ node: z.string().regex(/^\d+(\.\d+){0,2}$/).optional() }).optional() });
67
+ /** The Node version a Volta project pins ("volta": { "node": "20.11.1" } in package.json), so nvm defaults to it too. */
68
+ export const voltaNode = (workspace) => {
69
+ const file = path.join(workspace, 'package.json');
70
+ if (!fileExists(file))
71
+ return null;
72
+ try {
73
+ return PackageJson.safeParse(JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, ''))).data?.volta?.node ?? null;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ };
79
+ /**
80
+ * Picks tool versions. Highest priority first: command line, the folder's .sandbox-config.json,
81
+ * version files in the workspace (.nvmrc, .node-version, .python-version), the tool config, this PC's node version.
82
+ */
83
+ export const resolveVersions = (config, workspace, local, req, hostNodeVersion) => {
84
+ const localTools = (local.toolchains ?? {});
85
+ const pick = (cli, localValue, files, configured) => {
86
+ if (cli)
87
+ return { spec: cli, source: 'command line' };
88
+ if (localValue)
89
+ return { spec: localValue, source: '.sandbox-config.json' };
90
+ for (const name of files) {
91
+ const value = name === VOLTA_PIN ? voltaNode(workspace) : firstLine(path.join(workspace, name));
92
+ if (value)
93
+ return { spec: value, source: name };
94
+ }
95
+ return { spec: configured, source: 'config' };
96
+ };
97
+ let node = pick(req.nodeVersion, localTools.node?.version, ['.nvmrc', '.node-version', VOLTA_PIN], config.toolchains.node.version);
98
+ if (node.spec === 'host')
99
+ node = { spec: hostNodeVersion.replace(/^v/, ''), source: 'this PC' };
100
+ const python = pick(req.pythonVersion, localTools.python?.version, ['.python-version'], config.toolchains.python.version);
101
+ return { node, nodeExtra: config.toolchains.node.extraVersions, python };
102
+ };
103
+ /** Precedence (lowest to highest): built-in defaults, user config, .sandbox-config.json in the workspace, command-line flags. */
104
+ export const resolveRun = async (req, hostNodeVersion = process.version) => {
105
+ const notes = [];
106
+ // Mappings from the user config are relative to that config file; expand them before anything else uses them.
107
+ let config = { ...req.loaded.config, mappings: req.loaded.config.mappings.map((m) => ({ ...m, host: expandPath(m.host, req.loaded.base) })) };
108
+ const storageRoot = expandPath(config.storage.root, req.loaded.base);
109
+ const workspaceHost = path.resolve(req.workspace ?? req.cwd);
110
+ if (!dirExists(workspaceHost))
111
+ fail('The workspace folder does not exist: ' + workspaceHost, 'Check the --workspace path, or cd into the project first.');
112
+ assertSafeWorkspace(workspaceHost, req.packageRoot, storageRoot);
113
+ const scripts = [];
114
+ let localConfigPath = null;
115
+ let overrides = {};
116
+ const local = req.noLocalConfig ? null : readLocalConfig(workspaceHost);
117
+ if (local) {
118
+ overrides = normalizeOverrides(local.data, workspaceHost, local.path);
119
+ const store = trustStorePath(storageRoot);
120
+ if (!isTrusted(store, local.path, local.hash)) {
121
+ const summary = describeOverrides(overrides);
122
+ let trusted = req.trustConfig;
123
+ if (!trusted && req.ask) {
124
+ console.log('\nFound ' + local.path + ' which is not trusted yet. It would apply:');
125
+ for (const line of summary)
126
+ console.log(' ' + line);
127
+ trusted = await req.ask('Trust and use this file?');
128
+ }
129
+ if (!trusted)
130
+ fail(local.path + ' has not been approved yet (a repo can ship this file, so it needs your OK). It would apply:\n ' + (summary.join('\n ') || '(no changes)'), 'Review it, then run again in an interactive terminal and answer y, or add --trust-config. To ignore the file: --no-local-config');
131
+ saveTrust(store, local.path, local.hash);
132
+ notes.push('Trusted ' + local.path + ' (you will only be asked again if the file changes).');
133
+ }
134
+ config = applyOverrides(config, overrides);
135
+ scripts.push(...(overrides.scripts ?? []));
136
+ localConfigPath = local.path;
137
+ notes.push('Using ' + local.path);
138
+ }
139
+ if (req.headless !== undefined)
140
+ config = { ...config, sandbox: { ...config.sandbox, headless: req.headless } };
141
+ for (const name of req.databases ?? []) {
142
+ const db = name.toLowerCase();
143
+ if (!DATABASES.includes(db))
144
+ fail('Unknown database "' + name + '".', 'Choose from: ' + DATABASES.join(', '));
145
+ config = { ...config, databases: { ...config.databases, [db]: { ...config.databases[db], enabled: true } } };
146
+ }
147
+ if (config.guestWorkspace === 'auto')
148
+ config = { ...config, guestWorkspace: autoGuestWorkspace(workspaceHost) };
149
+ else if (config.guestWorkspace === 'mirror') {
150
+ const plan = planMirrorWorkspace(workspaceHost, autoGuestWorkspace(workspaceHost));
151
+ if (plan.note)
152
+ notes.push(plan.note);
153
+ config = { ...config, guestWorkspace: plan.guest };
154
+ }
155
+ scripts.push(...req.scripts.map((s) => path.resolve(s)));
156
+ const harnessName = req.noAgent ? 'none' : (req.harness ?? config.defaultHarness);
157
+ if (harnessName !== 'none' && !config.harnesses[harnessName])
158
+ fail('Unknown agent "' + harnessName + '".', 'Configured agents: ' + Object.keys(config.harnesses).join(', ') + ' (add more under "harnesses" in your config).');
159
+ const versions = resolveVersions(config, workspaceHost, overrides, req, hostNodeVersion);
160
+ const dataPath = path.join(storageRoot, 'data', config.storage.dataName);
161
+ return { config, storageRoot, dataPath, workspaceHost, harnessName, scripts, localConfigPath, notes, versions };
162
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,186 @@
1
+ // Session daemon (runs detached inside the sandbox for its whole life). It is the only thing that acts on host
2
+ // requests, and it only knows four request kinds: start a session, run a command, restart sessions, ping.
3
+ // It must never die quietly: every iteration is guarded, and anything unexpected is written to its log.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import crypto from 'node:crypto';
7
+ import { spawn, spawnSync } from 'node:child_process';
8
+ import { FrameType, sendControl } from '../shared/pty-client.js';
9
+ import { SYSTEM } from '../shared/system.js';
10
+ import { commandLine, ensurePtyHost, freePort, ptyHostLogTail, startPtyHost, waitForPtyHost } from './ptyhost.js';
11
+ import { provisionHarness, resolveLaunch } from './setup/agents.js';
12
+ import { saveClaudeMem } from './setup/claude.js';
13
+ import { runHooks } from './setup/hooks.js';
14
+ import { guestAddresses } from './setup/network.js';
15
+ import { now, readSession, scanRequests, writeJsonAtomic } from '../shared/rundir.js';
16
+ import { dataDir, errMsg, exists, isWin, log, run, session, toolsDir, warn, workspace } from './env.js';
17
+ const MAX_OUTPUT = 1024 * 1024;
18
+ const MAX_MESSAGE = 3500;
19
+ const live = new Map();
20
+ let starting = 0;
21
+ let lastMemSave = Date.now();
22
+ process.on('uncaughtException', (e) => warn('daemon: unexpected error (continuing): ' + (e.stack ?? errMsg(e))));
23
+ process.on('unhandledRejection', (e) => warn('daemon: unexpected error (continuing): ' + errMsg(e)));
24
+ const insideWorkspace = (cwd) => {
25
+ const rel = path.relative(workspace.toLowerCase(), path.resolve(cwd).toLowerCase());
26
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
27
+ };
28
+ /** Status/response writes are best-effort: a briefly locked file (antivirus, the host's sync) must not kill the daemon. */
29
+ const tryWrite = (file, value) => { try {
30
+ writeJsonAtomic(file, value);
31
+ }
32
+ catch (e) {
33
+ warn('Could not write ' + file + ': ' + errMsg(e));
34
+ } };
35
+ const respond = (id, result) => tryWrite(run.response(id), { id, ...result, message: result.message.slice(0, MAX_MESSAGE), finishedAt: now() });
36
+ const saveStatus = (status) => tryWrite(run.session(status.id), { ...status, message: status.message.slice(0, MAX_MESSAGE), updatedAt: now() });
37
+ const safely = (what, work) => { try {
38
+ work();
39
+ }
40
+ catch (e) {
41
+ warn(what + ' failed: ' + errMsg(e));
42
+ } };
43
+ const terminalTab = (id, title, cwd) => {
44
+ const view = [process.execPath, path.join(path.dirname(process.argv[1]), 'view.js'), id];
45
+ const wt = path.join(toolsDir, 'terminal', 'wt.exe');
46
+ if (exists(wt))
47
+ spawn(wt, ['-w', '0', 'nt', '--title', title, '-d', cwd, ...view], { detached: true, stdio: 'ignore' }).unref();
48
+ else
49
+ spawn(SYSTEM.cmd(), ['/d', '/c', 'start', title, ...view], { detached: true, stdio: 'ignore', cwd }).unref();
50
+ };
51
+ const onExit = (running) => (code) => {
52
+ live.delete(running.id);
53
+ const tail = code === 0 ? '' : ptyHostLogTail(running.id, 6);
54
+ saveStatus({ ...running, phase: 'exited', message: running.harness + ' exited with code ' + code + (tail ? '. Last output:\n' + tail : ''), exitCode: code ?? 1, attach: undefined });
55
+ log('Session ' + running.id + ' ended (exit ' + code + ')');
56
+ safely('post-agent hooks', () => runHooks('post-agent', process.env));
57
+ safely('claude-mem save', saveClaudeMem);
58
+ maybeShutdown();
59
+ };
60
+ const startSession = async (req) => {
61
+ const status = { id: req.id, launchId: session.launchId, harness: req.harness, cwd: req.cwd, phase: 'starting', message: 'Starting ' + req.harness, updatedAt: now(), startedAt: now() };
62
+ saveStatus(status);
63
+ starting++;
64
+ try {
65
+ if (!insideWorkspace(req.cwd) || !exists(req.cwd))
66
+ throw new Error('Folder is not inside the sandbox workspace ' + workspace + ': ' + req.cwd);
67
+ const harness = req.harness === session.harnessName && session.harness ? session.harness : null;
68
+ if (!harness)
69
+ throw new Error('Agent "' + req.harness + '" is not configured for this sandbox (it runs ' + session.harnessName + ').');
70
+ // Never refresh here: main already did, and replacing a running agent's executable would fail.
71
+ provisionHarness(req.harness, harness, process.env, false);
72
+ const launch = resolveLaunch(req.harness, harness);
73
+ const base = [...launch.args, ...harness.arguments];
74
+ const port = await freePort();
75
+ const token = crypto.randomBytes(24).toString('base64url');
76
+ const remote = session.networking && session.attach.enabled;
77
+ const child = startPtyHost({
78
+ exe: ensurePtyHost(), port, token, cols: 120, rows: 32, id: req.id, cwd: req.cwd, env: { ...process.env, ...harness.environment }, remote,
79
+ command: commandLine(launch.file, [...base, ...req.args]), restartCommand: commandLine(launch.file, [...base, ...(harness.resumeArguments ?? [])]),
80
+ });
81
+ try {
82
+ await waitForPtyHost(child, port, 15000);
83
+ }
84
+ catch (e) {
85
+ const tail = ptyHostLogTail(req.id);
86
+ throw new Error(errMsg(e) + (tail ? '. From its log:\n' + tail : '') + '\nLog: ' + path.join(dataDir, 'logs', 'ptyhost-' + req.id + '.log'));
87
+ }
88
+ const running = { ...status, phase: 'running', message: req.harness + ' is running', attach: { addresses: remote ? guestAddresses() : [], port, token } };
89
+ live.set(req.id, { status: running, child });
90
+ child.once('exit', onExit(running));
91
+ saveStatus(running);
92
+ log('Session ' + req.id + ' started (' + req.harness + ' in ' + req.cwd + ', port ' + port + ')');
93
+ if (req.id !== session.launchId && process.env.AGENT_NO_TABS !== '1')
94
+ terminalTab(req.id, req.harness + ' ' + path.basename(req.cwd), req.cwd);
95
+ }
96
+ catch (e) {
97
+ saveStatus({ ...status, phase: 'failed', message: errMsg(e) });
98
+ warn('Session ' + req.id + ' failed: ' + errMsg(e));
99
+ maybeShutdown();
100
+ }
101
+ finally {
102
+ starting--;
103
+ }
104
+ };
105
+ const quoteForCmd = (a) => (/[\s"&|<>^]/.test(a) ? '"' + a.replace(/"/g, '""') + '"' : a);
106
+ /** Runs a command for the host without blocking the daemon (other requests and pings keep being answered). */
107
+ const execCommand = (req) => {
108
+ if (!insideWorkspace(req.cwd) || !exists(req.cwd)) {
109
+ respond(req.id, { ok: false, message: 'Folder is not inside the sandbox workspace: ' + req.cwd });
110
+ return;
111
+ }
112
+ // chcp 65001 first: console programs then write UTF-8 (not the OEM code page) into the pipes.
113
+ const child = spawn(SYSTEM.cmd(), ['/d', '/s', '/c', '"chcp 65001>nul & ' + req.command.map(quoteForCmd).join(' ') + '"'], { cwd: req.cwd, env: process.env, windowsHide: true, windowsVerbatimArguments: true });
114
+ const output = { stdout: '', stderr: '' };
115
+ // setEncoding decodes across chunks: a multi-byte character split between two reads is not mangled.
116
+ const collect = (key) => (d) => { if (output[key].length < MAX_OUTPUT)
117
+ output[key] += d; };
118
+ child.stdout.setEncoding('utf8').on('data', collect('stdout'));
119
+ child.stderr.setEncoding('utf8').on('data', collect('stderr'));
120
+ const timer = setTimeout(() => child.kill(), Math.min(req.timeoutSeconds, 3600) * 1000);
121
+ const cap = (s) => (s.length > MAX_OUTPUT ? s.slice(0, MAX_OUTPUT) + '\n[output truncated]' : s);
122
+ child.on('error', (e) => { clearTimeout(timer); respond(req.id, { ok: false, message: 'Could not run the command: ' + errMsg(e) }); });
123
+ child.on('close', (code) => {
124
+ clearTimeout(timer);
125
+ respond(req.id, { ok: code === 0, message: code === null ? 'stopped after ' + req.timeoutSeconds + ' s' : 'exit ' + code, exitCode: code ?? -1, stdout: cap(output.stdout), stderr: cap(output.stderr) });
126
+ });
127
+ };
128
+ const restart = async (req) => {
129
+ const targets = [...live.values()].filter((s) => s.status.attach && (req.harness === null || s.status.harness === req.harness));
130
+ for (const s of targets)
131
+ await sendControl({ addresses: ['127.0.0.1'], port: s.status.attach.port, token: s.status.attach.token }, FrameType.Restart);
132
+ respond(req.id, { ok: true, message: 'restarted ' + targets.length + ' session(s)' });
133
+ };
134
+ const handle = (req) => {
135
+ switch (req.kind) {
136
+ case 'session': return startSession(req);
137
+ case 'exec': return execCommand(req);
138
+ case 'restart': return restart(req);
139
+ case 'ping': return respond(req.id, { ok: true, message: 'pong (' + live.size + ' session(s) running)' });
140
+ }
141
+ };
142
+ /** A headless sandbox with nothing running (or starting) only wastes RAM, so it shuts itself down. */
143
+ const maybeShutdown = () => {
144
+ if (!session.headless || !isWin)
145
+ return;
146
+ const idle = () => live.size === 0 && starting === 0 && scanRequests(dataDir).valid.length === 0;
147
+ if (!idle())
148
+ return;
149
+ setTimeout(() => {
150
+ if (!idle())
151
+ return;
152
+ log('Headless sandbox has no sessions left; shutting down.');
153
+ safely('claude-mem save', saveClaudeMem);
154
+ spawnSync(SYSTEM.shutdown(), ['/s', '/t', '5'], { windowsHide: true });
155
+ }, 15000);
156
+ };
157
+ const tick = () => {
158
+ const { valid, invalid } = scanRequests(dataDir);
159
+ // Anything in requests/ that is not a valid request (wrong kind, bad id, junk) is deleted unread.
160
+ for (const file of invalid)
161
+ safely('removing an invalid request', () => fs.rmSync(file, { recursive: true, force: true }));
162
+ for (const req of valid) {
163
+ safely('claiming a request', () => fs.rmSync(run.request(req.id), { force: true }));
164
+ if (readSession(dataDir, req.id))
165
+ continue;
166
+ // Not awaited: starting a session or running a command must not hold up other requests.
167
+ void Promise.resolve(handle(req)).catch((e) => respond(req.id, { ok: false, message: errMsg(e) }));
168
+ }
169
+ if (Date.now() - lastMemSave > 120000) {
170
+ lastMemSave = Date.now();
171
+ safely('claude-mem save', saveClaudeMem);
172
+ }
173
+ };
174
+ const loop = () => {
175
+ try {
176
+ tick();
177
+ }
178
+ catch (e) {
179
+ warn('daemon loop: ' + errMsg(e));
180
+ }
181
+ finally {
182
+ setTimeout(loop, 400);
183
+ }
184
+ };
185
+ log('Session daemon started (pid ' + process.pid + ').');
186
+ loop();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ // `sbx-db`: list, start and stop the sandbox's databases from a terminal inside it (`sandbox db` on the PC runs it too).
2
+ // Usage: sbx-db [list] | sbx-db start <name> | sbx-db stop <name>
3
+ import { DATABASE_NAMES, portOpen, setupDatabase, stopDatabase } from './toolchains/databases.js';
4
+ import { emptyEnv } from './toolchains/common.js';
5
+ import { addMachineEnvironment } from './setup/environment.js';
6
+ import { errMsg, session } from './env.js';
7
+ const USAGE = 'Usage: sbx-db [list] | sbx-db start <name> | sbx-db stop <name>';
8
+ /** Boot's own plans first (their versions and ports were chosen for this project), then everything else. */
9
+ const plans = () => {
10
+ const boot = session.databases ?? [];
11
+ return [...boot, ...(session.databaseCatalog ?? []).filter((c) => !boot.some((b) => b.database === c.database))];
12
+ };
13
+ const find = (name) => {
14
+ const all = plans();
15
+ const plan = all.find((p) => p.database === name?.toLowerCase());
16
+ if (!plan)
17
+ throw new Error((name ? 'Unknown database "' + name + '". ' : '') + 'Choose one of: ' + all.map((p) => p.database).join(', '));
18
+ if (plan.database === 'sqlserver')
19
+ throw new Error('SQL Server cannot run inside Windows Sandbox.');
20
+ return plan;
21
+ };
22
+ const list = async () => {
23
+ for (const p of plans().filter((x) => x.database !== 'sqlserver')) {
24
+ const state = (await portOpen(p.port)) ? 'running' : 'stopped';
25
+ console.log(p.database.padEnd(10) + state.padEnd(9) + 'port ' + p.port + ' ' + DATABASE_NAMES[p.database]);
26
+ }
27
+ return 0;
28
+ };
29
+ const start = async (name) => {
30
+ const plan = find(name);
31
+ if (await portOpen(plan.port)) {
32
+ console.log(plan.database + ' is already running on port ' + plan.port + '.');
33
+ return 0;
34
+ }
35
+ console.log('Starting ' + DATABASE_NAMES[plan.database] + ' (the first time downloads it) ...');
36
+ const env = emptyEnv();
37
+ await setupDatabase(env, plan);
38
+ addMachineEnvironment(env.vars, env.prepend);
39
+ for (const line of env.summary)
40
+ console.log(' ' + line);
41
+ console.log('New terminal tabs have its variables and tools; this one does not.');
42
+ return 0;
43
+ };
44
+ const stop = async (name) => {
45
+ const plan = find(name);
46
+ console.log((await stopDatabase(plan)) ? 'Stopped ' + plan.database + '.' : plan.database + ' was not running.');
47
+ return 0;
48
+ };
49
+ const main = (args) => {
50
+ const [action = 'list', name] = args;
51
+ if (action === 'list')
52
+ return list();
53
+ if (action === 'start')
54
+ return start(name);
55
+ if (action === 'stop')
56
+ return stop(name);
57
+ console.error(USAGE);
58
+ return Promise.resolve(2);
59
+ };
60
+ main(process.argv.slice(2)).then((code) => { process.exitCode = code; }, (e) => { console.error(errMsg(e)); process.exitCode = 1; });
@@ -0,0 +1,49 @@
1
+ import { type SpawnOptions, type SpawnSyncOptions } from 'node:child_process';
2
+ import type { GuestSession, SandboxStatus } from '../core/types.js';
3
+ export declare const isWin: boolean;
4
+ export declare const dryRun: boolean;
5
+ /** Real provisioning, but nothing machine-wide (environment variables): for end-to-end tests outside a sandbox. */
6
+ export declare const testMode: boolean;
7
+ export declare const launchDir: string;
8
+ export declare const dataDir: string;
9
+ export declare const hostToolsDir: string;
10
+ /** Folders that must stay on the sandbox's own disk (symlinks, SQLite) rather than in a mapped folder. */
11
+ export declare const localDir: string;
12
+ export declare const toolsDir: string;
13
+ export declare const logsDir: string;
14
+ export declare const run: {
15
+ run: string;
16
+ sandbox: string;
17
+ requests: string;
18
+ responses: string;
19
+ sessions: string;
20
+ request: (id: string) => string;
21
+ response: (id: string) => string;
22
+ session: (id: string) => string;
23
+ };
24
+ export declare const session: GuestSession;
25
+ export declare const workspace: string;
26
+ export declare const stateDir: string;
27
+ export declare const claudeDir: string;
28
+ export declare const log: (message: string) => void;
29
+ export declare const warn: (message: string) => void;
30
+ export declare const errMsg: (e: unknown) => string;
31
+ export declare const exists: (p: string) => boolean;
32
+ export declare const mkdirp: (p: string) => string;
33
+ export declare const logPath: string;
34
+ /** Best-effort: progress reporting must never be what breaks the boot. */
35
+ export declare const setSandboxStatus: (phase: SandboxStatus["phase"], message: string) => void;
36
+ /** Runs a program, streaming output, and throws with a readable message when it fails. */
37
+ export declare const exec: (file: string, args: string[], options?: SpawnSyncOptions) => void;
38
+ /** exec without blocking: other toolchains keep downloading while this one runs an installer. */
39
+ export declare const execAsync: (file: string, args: string[], options?: SpawnOptions) => Promise<void>;
40
+ export declare const execOutput: (file: string, args: string[], options?: SpawnSyncOptions) => string;
41
+ export declare const powershell: (script: string) => void;
42
+ /** Copies a folder with robocopy (fast, restartable), or just the named files in it; exit codes below 8 mean success. */
43
+ export declare const robocopy: (from: string, to: string, files?: string[]) => void;
44
+ /**
45
+ * A Node install without its global packages (nvm keeps those per version: often tens of thousands of small files,
46
+ * which is what made copying from this PC slow). Same file list as Copy-NodeFromHost in sandbox/bootstrap.ps1.
47
+ * Copied into a staging folder and renamed into place: node.exe alone (an interrupted copy) must not look like a full install.
48
+ */
49
+ export declare const copyNode: (from: string, to: string) => void;
@@ -0,0 +1,99 @@
1
+ // Shared state for the guest scripts (main, daemon, view): fixed paths, the session description, logging, status.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { moveIntoPlace, spawnAsync, stagingFolder } from '../shared/download.js';
6
+ import { now, runPaths, writeJsonAtomic } from '../shared/rundir.js';
7
+ import { SYSTEM } from '../shared/system.js';
8
+ export const isWin = process.platform === 'win32';
9
+ export const dryRun = process.env.AGENT_DRY_RUN === '1';
10
+ /** Real provisioning, but nothing machine-wide (environment variables): for end-to-end tests outside a sandbox. */
11
+ export const testMode = process.env.AGENT_SANDBOX_TEST === '1';
12
+ export const launchDir = process.env.AGENT_LAUNCH_DIR ?? 'C:\\AgentLaunch';
13
+ export const dataDir = process.env.AGENT_DATA_DIR ?? 'C:\\AgentData';
14
+ export const hostToolsDir = process.env.AGENT_HOST_TOOLS_DIR ?? 'C:\\HostTools';
15
+ /** Folders that must stay on the sandbox's own disk (symlinks, SQLite) rather than in a mapped folder. */
16
+ export const localDir = process.env.AGENT_LOCAL_DIR ?? 'C:\\nvm';
17
+ export const toolsDir = path.join(dataDir, 'tools');
18
+ export const logsDir = path.join(dataDir, 'logs');
19
+ export const run = runPaths(dataDir);
20
+ export const session = JSON.parse(fs.readFileSync(path.join(launchDir, 'session.json'), 'utf8').replace(/^\uFEFF/, ''));
21
+ export const workspace = process.env.AGENT_WORKSPACE ?? session.workspace;
22
+ export const stateDir = path.join(dataDir, session.persistAgentState ? 'state' : 'state-session');
23
+ export const claudeDir = path.join(stateDir, 'claude');
24
+ fs.mkdirSync(logsDir, { recursive: true });
25
+ const logFile = path.join(logsDir, (process.env.AGENT_LOG_PREFIX ?? 'guest') + '-' + new Date().toISOString().replace(/[:.]/g, '-') + '.log');
26
+ const append = (line) => { try {
27
+ fs.appendFileSync(logFile, line + '\n');
28
+ }
29
+ catch { /* logging must never break the run */ } };
30
+ export const log = (message) => { console.log(message); append(message); };
31
+ export const warn = (message) => { console.warn('\x1b[33mWARNING: ' + message + '\x1b[0m'); append('WARNING: ' + message); };
32
+ export const errMsg = (e) => (e instanceof Error ? e.message : String(e));
33
+ export const exists = (p) => fs.existsSync(p);
34
+ export const mkdirp = (p) => { fs.mkdirSync(p, { recursive: true }); return p; };
35
+ export const logPath = logFile;
36
+ /** Best-effort: progress reporting must never be what breaks the boot. */
37
+ export const setSandboxStatus = (phase, message) => {
38
+ try {
39
+ writeJsonAtomicChecked(phase, message);
40
+ }
41
+ catch (e) {
42
+ warn('Could not update sandbox status: ' + errMsg(e));
43
+ }
44
+ };
45
+ const writeJsonAtomicChecked = (phase, message) => {
46
+ writeJsonAtomic(run.sandbox, { launchId: session.launchId, phase, message, updatedAt: now(), workspaceHost: session.workspaceHost, guestWorkspace: workspace, headless: session.headless });
47
+ };
48
+ /** Runs a program, streaming output, and throws with a readable message when it fails. */
49
+ export const exec = (file, args, options = {}) => {
50
+ const result = spawnSync(file, args, { stdio: 'inherit', windowsHide: true, ...options });
51
+ if (result.error)
52
+ throw result.error;
53
+ if (result.status !== 0)
54
+ throw new Error(path.basename(file) + ' ' + args.slice(0, 3).join(' ') + ' failed with exit code ' + result.status);
55
+ };
56
+ /** exec without blocking: other toolchains keep downloading while this one runs an installer. */
57
+ export const execAsync = async (file, args, options = {}) => {
58
+ const code = await spawnAsync(file, args, { stdio: 'inherit', ...options });
59
+ if (code !== 0)
60
+ throw new Error(path.basename(file) + ' ' + args.slice(0, 3).join(' ') + ' failed with exit code ' + code);
61
+ };
62
+ export const execOutput = (file, args, options = {}) => {
63
+ const result = spawnSync(file, args, { encoding: 'utf8', windowsHide: true, ...options });
64
+ return result.status === 0 ? String(result.stdout ?? '').trim() : '';
65
+ };
66
+ export const powershell = (script) => exec(SYSTEM.powershell(), ['-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script]);
67
+ /** Copies a folder with robocopy (fast, restartable), or just the named files in it; exit codes below 8 mean success. */
68
+ export const robocopy = (from, to, files = []) => {
69
+ if (!isWin) {
70
+ if (!files.length) {
71
+ fs.cpSync(from, to, { recursive: true });
72
+ return;
73
+ }
74
+ fs.mkdirSync(to, { recursive: true });
75
+ for (const f of files.filter((f) => fs.existsSync(path.join(from, f))))
76
+ fs.copyFileSync(path.join(from, f), path.join(to, f));
77
+ return;
78
+ }
79
+ const what = files.length ? files : ['/E', '/MT:16'];
80
+ const result = spawnSync(SYSTEM.robocopy(), [from, to, ...what, '/NFL', '/NDL', '/NJH', '/NJS', '/NP', '/R:1', '/W:1', '/XJ'], { stdio: 'ignore', windowsHide: true });
81
+ if ((result.status ?? 16) >= 8)
82
+ throw new Error('robocopy ' + from + ' -> ' + to + ' failed (exit ' + result.status + ')');
83
+ };
84
+ /**
85
+ * A Node install without its global packages (nvm keeps those per version: often tens of thousands of small files,
86
+ * which is what made copying from this PC slow). Same file list as Copy-NodeFromHost in sandbox/bootstrap.ps1.
87
+ * Copied into a staging folder and renamed into place: node.exe alone (an interrupted copy) must not look like a full install.
88
+ */
89
+ export const copyNode = (from, to) => {
90
+ const work = stagingFolder(to);
91
+ try {
92
+ robocopy(from, work, ['node.exe', 'npm', 'npm.cmd', 'npx', 'npx.cmd']);
93
+ robocopy(path.join(from, 'node_modules', 'npm'), path.join(work, 'node_modules', 'npm'));
94
+ moveIntoPlace(work, to);
95
+ }
96
+ finally {
97
+ fs.rmSync(work, { recursive: true, force: true });
98
+ }
99
+ };
@@ -0,0 +1,7 @@
1
+ export type Decision = {
2
+ allow: true;
3
+ } | {
4
+ allow: false;
5
+ reason: string;
6
+ };
7
+ export declare const gitDecision: (args: string[], access: string) => Decision;