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,156 @@
1
+ // The settings screen in the menu: every setting, generated from the zod schema (so nothing needs to be listed by
2
+ // hand), shown with its current value and description, edited with a fitting prompt and saved to the user config
3
+ // file. Edits go through jsonc-parser, so comments and formatting in the file are kept, and nothing is written unless
4
+ // the whole config is still valid.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { input, select, Separator } from '@inquirer/prompts';
8
+ import { applyEdits, modify, parse } from 'jsonc-parser';
9
+ import _ from 'lodash';
10
+ import { DEFAULTS, loadConfig, mergeConfig, userConfigPath } from '../core/config.js';
11
+ import { Config as ConfigSchema, describeIssues } from '../core/schema.js';
12
+ const c = { dim: (s) => '\x1b[2m' + s + '\x1b[0m', cyan: (s) => '\x1b[36m' + s + '\x1b[0m', green: (s) => '\x1b[32m' + s + '\x1b[0m', yellow: (s) => '\x1b[33m' + s + '\x1b[0m', red: (s) => '\x1b[31m' + s + '\x1b[0m' };
13
+ /** optional()/default() wrappers hide the real type. */
14
+ const unwrap = (s) => (['optional', 'default', 'nullable'].includes(s._zod.def.type) && s._zod.def.innerType ? unwrap(s._zod.def.innerType) : s);
15
+ const kind = (s) => unwrap(s)._zod.def.type;
16
+ const isSection = (s) => kind(s) === 'object' && Boolean(unwrap(s).shape);
17
+ /** Settings not worth editing here (the schema link) or better edited in the file (agent definitions). */
18
+ const HIDDEN = new Set(['$schema']);
19
+ /** A value in one short line: maps as their keys, long lists shortened. */
20
+ const show = (value) => {
21
+ if (value === undefined)
22
+ return c.dim('(not set)');
23
+ if (typeof value === 'string')
24
+ return value;
25
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
26
+ const keys = Object.keys(value);
27
+ return keys.length ? '{ ' + _.truncate(keys.join(', '), { length: 60 }) + ' }' : '{}';
28
+ }
29
+ return _.truncate(JSON.stringify(value), { length: 70 });
30
+ };
31
+ /** What the value is in the file the user owns (undefined when it comes from the built-in defaults). */
32
+ const fileValue = (text, key) => _.get(parse(text) ?? {}, key);
33
+ /**
34
+ * Applies one change (undefined = back to the default) to the config text and returns the new text, or the problems
35
+ * that make the result invalid. Pure: the caller writes the file.
36
+ */
37
+ export const editConfigText = (text, key, value) => {
38
+ const edits = modify(text || '{}', key, value, { formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' } });
39
+ const next = applyEdits(text || '{}', edits);
40
+ const parsed = ConfigSchema.safeParse(mergeConfig(DEFAULTS, parse(next) ?? {}));
41
+ return parsed.success ? { text: next.endsWith('\n') ? next : next + '\n' } : { problems: describeIssues(parsed.error) };
42
+ };
43
+ /** A prompt that fits the setting's type; returns the new value, or undefined to change nothing. */
44
+ const askValue = async (schema, label, current, config) => {
45
+ const s = unwrap(schema);
46
+ // The default agent is one of the configured ones: a list, not free text.
47
+ if (label === 'defaultHarness' && config) {
48
+ const v = await select({ message: label, choices: [...Object.keys(config.harnesses).map((h) => ({ name: h, value: h })), { name: c.dim('< cancel'), value: '@cancel' }], default: current });
49
+ return v === '@cancel' ? undefined : { value: v };
50
+ }
51
+ const t = s._zod.def.type;
52
+ const back = { name: c.dim('< cancel'), value: '@cancel' };
53
+ const pick = async (choices) => {
54
+ const v = await select({ message: label, choices: [...choices, back], default: current });
55
+ return v === '@cancel' ? undefined : { value: v };
56
+ };
57
+ if (t === 'boolean')
58
+ return pick([{ name: 'true', value: true }, { name: 'false', value: false }]);
59
+ if (t === 'enum')
60
+ return pick(Object.values(s._zod.def.entries ?? {}).map((v) => ({ name: String(v), value: v })));
61
+ if (t === 'union') {
62
+ const options = s._zod.def.options ?? [];
63
+ const literals = options.filter((o) => kind(o) === 'literal').flatMap((o) => unwrap(o)._zod.def.values ?? []);
64
+ const hasBool = options.some((o) => kind(o) === 'boolean');
65
+ const other = options.find((o) => !['literal', 'boolean'].includes(kind(o)));
66
+ const choices = [...(hasBool ? [{ name: 'true', value: true }, { name: 'false', value: false }] : []), ...literals.map((v) => ({ name: String(v), value: v }))];
67
+ if (!other)
68
+ return pick(choices);
69
+ const chosen = await pick([...choices, { name: 'a ' + kind(other) + ' ...', value: '@other' }]);
70
+ return chosen?.value === '@other' ? askValue(other, label, current) : chosen;
71
+ }
72
+ const typed = async (validate, convert) => {
73
+ const answer = await input({ message: label + c.dim(' (empty = cancel)'), default: current === undefined ? '' : typeof current === 'string' ? current : JSON.stringify(current), validate: (v) => (v === '' ? true : validate(v)) });
74
+ return answer === '' ? undefined : { value: convert(answer) };
75
+ };
76
+ if (t === 'number')
77
+ return typed((v) => (Number.isFinite(Number(v)) ? true : 'a number'), Number);
78
+ if (t === 'string')
79
+ return typed(() => true, (v) => v);
80
+ if (t === 'array' && kind(s._zod.def.element) === 'string') {
81
+ return typed(() => true, (v) => (v.trim() === '[]' ? [] : v.split(',').map((x) => x.trim()).filter(Boolean)));
82
+ }
83
+ // Lists of objects and maps (mappings, harnesses, environment): edited as JSON.
84
+ return typed((v) => { try {
85
+ JSON.parse(v);
86
+ return true;
87
+ }
88
+ catch {
89
+ return 'valid JSON';
90
+ } }, (v) => JSON.parse(v));
91
+ };
92
+ /** Edits one setting: shows it, asks, saves (or resets it to the default). */
93
+ const editSetting = async (schema, key, config, file) => {
94
+ const text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
95
+ const inFile = fileValue(text, key);
96
+ console.log('\n' + c.cyan(key.join('.')) + (schema.description ? '\n' + c.dim(schema.description) : ''));
97
+ console.log('Now: ' + show(_.get(config, key)) + (inFile === undefined ? c.dim(' (built-in default)') : c.dim(' (set in ' + file + ')')));
98
+ const action = inFile === undefined ? 'change' : await select({ message: 'What now?', choices: [{ name: 'Change it', value: 'change' }, { name: 'Reset to the default (' + show(_.get(DEFAULTS, key)) + ')', value: 'reset' }, { name: c.dim('< back'), value: 'back' }] });
99
+ if (action === 'back')
100
+ return;
101
+ const answer = action === 'reset' ? { value: undefined } : await askValue(schema, key.join('.'), _.get(config, key), config);
102
+ if (!answer)
103
+ return;
104
+ // The default needs no line in the file; an unchanged value needs no save.
105
+ const value = _.isEqual(answer.value, _.get(DEFAULTS, key)) ? undefined : answer.value;
106
+ if (value === inFile && _.isEqual(answer.value, _.get(config, key))) {
107
+ console.log(c.dim('Unchanged.'));
108
+ return;
109
+ }
110
+ const result = editConfigText(text, key, value);
111
+ if ('problems' in result) {
112
+ console.log(c.red('Not saved: ') + result.problems.join('; '));
113
+ return;
114
+ }
115
+ fs.mkdirSync(path.dirname(file), { recursive: true });
116
+ fs.writeFileSync(file, result.text, 'utf8');
117
+ console.log(c.green('Saved') + c.dim(' to ' + file + '. New sandboxes use it.'));
118
+ };
119
+ const SECTION_TITLES = {
120
+ toolchains: 'Languages and tools', databases: 'Databases', sandbox: 'The sandbox machine (memory, window, GPU, network)', attach: 'Attaching (auto attach, auto hide)',
121
+ provisioning: 'Provisioning and git', secrets: 'Secrets', claudeProfile: 'Your Claude settings and history', storage: 'Storage', network: 'Network', hooks: 'Hooks', personalization: 'Desktop look',
122
+ };
123
+ /** Browses one level of settings; sections open, leaves are edited. */
124
+ const browse = async (schema, key, file) => {
125
+ for (;;) {
126
+ const config = loadConfig().config;
127
+ const entries = Object.entries(unwrap(schema).shape ?? {}).filter(([name]) => !HIDDEN.has(name));
128
+ const text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
129
+ const pickName = await select({
130
+ message: key.length ? key.join('.') : 'Settings ' + c.dim('(saved to ' + file + ')'),
131
+ pageSize: 18,
132
+ choices: [
133
+ ...entries.map(([name, s]) => {
134
+ const full = [...key, name];
135
+ const changed = fileValue(text, full) !== undefined;
136
+ const label = isSection(s) ? (key.length ? name : SECTION_TITLES[name] ?? name) + c.dim(' ...') : name.padEnd(18) + ' ' + (changed ? c.yellow(show(_.get(config, full))) : show(_.get(config, full)));
137
+ return { name: label, value: name, description: s.description };
138
+ }),
139
+ new Separator(),
140
+ { name: c.dim('< back'), value: '@back' },
141
+ ],
142
+ });
143
+ if (pickName === '@back')
144
+ return;
145
+ const child = unwrap(schema).shape[pickName];
146
+ if (isSection(child))
147
+ await browse(child, [...key, pickName], file);
148
+ else
149
+ await editSetting(child, [...key, pickName], config, file);
150
+ }
151
+ };
152
+ /** The settings screen. Changes are saved to the user config file (created when missing). */
153
+ export const settingsMenu = async (configPath) => {
154
+ const file = configPath ?? userConfigPath();
155
+ await browse(ConfigSchema, [], file);
156
+ };
@@ -0,0 +1,17 @@
1
+ import type { Config, LoadedConfig } from './types.js';
2
+ export declare const DEFAULTS: Config;
3
+ /** Per-user folder for config and hooks. Survives reinstalling the npm package. */
4
+ export declare const userDir: () => string;
5
+ export declare const userConfigPath: () => string;
6
+ /** Deep merge where arrays (and anything that is not a plain object) replace instead of merging element by element. */
7
+ export declare const mergeConfig: <T>(base: T, extra: unknown) => T;
8
+ /** Validates a full config; throws one readable error listing every problem. */
9
+ export declare const validateConfig: (config: unknown, source: string) => Config;
10
+ /**
11
+ * Lookup order: --config, %AGENT_SANDBOX_CONFIG%, ~/.agent-sandbox/config.json, built-in defaults.
12
+ * Whatever file is found (JSON with comments) is merged over the defaults, so it only needs the keys you change.
13
+ */
14
+ export declare const loadConfig: (explicitPath?: string) => LoadedConfig;
15
+ export declare const assertNoGuestOverlap: (mappings: {
16
+ guest: string;
17
+ }[]) => void;
@@ -0,0 +1,103 @@
1
+ // Built-in defaults and loading the user config (~/.agent-sandbox/config.json). Shapes live in schema.ts.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import _ from 'lodash';
5
+ import { Config as ConfigSchema, describeIssues } from './schema.js';
6
+ import { fail, homeDir, readJsonc } from './util.js';
7
+ export const DEFAULTS = {
8
+ defaultHarness: 'claude',
9
+ guestWorkspace: 'auto',
10
+ readOnly: false,
11
+ mappings: [],
12
+ sandbox: { memoryMb: 'auto', maxMemoryMb: 32768, reserveHostMb: 8192, vGpu: false, networking: true, clipboard: true, audioInput: false, videoInput: false, printerRedirection: false, protectedClient: true, headless: false },
13
+ storage: { root: '%LOCALAPPDATA%\\AgentSandbox', persistAgentState: true, dataName: 'default' },
14
+ network: { blockPrivateRanges: true },
15
+ provisioning: { copyFromHost: true, fallbackToInstall: true, refresh: false, git: { enabled: true, access: 'full' }, terminal: { enabled: true } },
16
+ toolchains: {
17
+ node: { version: 'host', extraVersions: [], corepack: true, volta: 'auto', globalPackages: [] },
18
+ python: { enabled: 'auto', version: '3', python2: true, pyenv: 'auto', tools: [] },
19
+ rust: { enabled: 'auto', toolchain: 'stable', host: 'auto', components: [], targets: [], crates: [], targetDir: 'separate' },
20
+ msvc: { enabled: 'auto', acceptLicense: false, msvcVersion: 'latest', sdkVersion: 'latest' },
21
+ cpp: { enabled: 'auto', flexBison: true },
22
+ dotnet: { enabled: 'auto', channels: ['LTS'], tools: [], workloads: [], frameworkTargets: ['net48', 'net472'], netfx35: false },
23
+ // Bigger downloads only for projects that use them.
24
+ go: { enabled: 'auto', version: 'stable', tools: [] },
25
+ java: { enabled: 'auto', version: 'lts', maven: true, gradle: true },
26
+ ruby: { enabled: 'auto', version: '3', gems: [] },
27
+ php: { enabled: 'auto', version: '8', composer: true },
28
+ deno: { enabled: 'auto' },
29
+ pwsh: { enabled: 'auto' },
30
+ cli: { gh: 'auto', zstd: false, openssl: 'auto' },
31
+ bun: { enabled: true },
32
+ uv: { enabled: true },
33
+ vcRuntime: { enabled: false },
34
+ mesa: { enabled: false, vulkan: false },
35
+ vscode: { enabled: true },
36
+ environment: {},
37
+ },
38
+ databases: {
39
+ postgres: { enabled: 'auto', version: 'latest', port: 5432 },
40
+ mysql: { enabled: 'auto', version: 'lts', port: 3306 },
41
+ mongodb: { enabled: 'auto', version: 'latest', port: 27017 },
42
+ redis: { enabled: 'auto', port: 6379 },
43
+ dynamodb: { enabled: 'auto', port: 8000, adminPort: 8001, admin: true },
44
+ sqlserver: { enabled: 'auto' },
45
+ },
46
+ attach: { enabled: true, reuseSession: true, auto: false, autoHide: false },
47
+ secrets: { strip: true, allow: [], patterns: [], warnFiles: true },
48
+ personalization: { enabled: true },
49
+ claudeProfile: { enabled: true, hostDir: 'auto', include: ['settings.json', 'CLAUDE.md', 'commands', 'agents', 'skills', 'plugins'], globalStateFile: 'auto', copyCredentials: false, overwrite: true, syncHistory: true, syncClaudeMem: true },
50
+ harnesses: {
51
+ claude: { hostExecutable: 'claude.exe', hostDir: 'auto', npmPackage: '@anthropic-ai/claude-code@latest', npmBin: 'claude', arguments: [], environment: { DISABLE_AUTOUPDATER: '1' }, resumeArguments: ['--continue'] },
52
+ codex: { npmPackage: '@openai/codex@latest', npmBin: 'codex', arguments: [], environment: {}, resumeArguments: ['resume', '--last'] },
53
+ gemini: { npmPackage: '@google/gemini-cli@latest', npmBin: 'gemini', arguments: [], environment: {}, resumeArguments: ['--resume'] },
54
+ // Not tested by us; installed from each project's official npm / PyPI package.
55
+ opencode: { npmPackage: 'opencode-ai@latest', npmBin: 'opencode', arguments: [], environment: {}, resumeArguments: ['--continue'] },
56
+ qwen: { npmPackage: '@qwen-code/qwen-code@latest', npmBin: 'qwen', arguments: [], environment: {}, resumeArguments: ['--continue'] },
57
+ crush: { npmPackage: '@charmland/crush@latest', npmBin: 'crush', arguments: [], environment: {}, resumeArguments: ['--continue'] },
58
+ copilot: { npmPackage: '@github/copilot@latest', npmBin: 'copilot', arguments: [], environment: {}, resumeArguments: ['--continue'] },
59
+ kilo: { npmPackage: '@kilocode/cli@latest', npmBin: 'kilo', arguments: [], environment: {}, resumeArguments: ['--continue'] },
60
+ amp: { npmPackage: '@ampcode/cli@latest', npmBin: 'amp', arguments: [], environment: {} },
61
+ cline: { npmPackage: 'cline@latest', npmBin: 'cline', arguments: [], environment: {} },
62
+ continue: { npmPackage: '@continuedev/cli@latest', npmBin: 'cn', arguments: [], environment: {}, resumeArguments: ['--resume'] },
63
+ kimi: { npmPackage: '@moonshot-ai/kimi-code@latest', npmBin: 'kimi', arguments: [], environment: {}, resumeArguments: ['-c'] },
64
+ // DeepSeek has no official coding CLI; these two community agents are built for it.
65
+ reasonix: { npmPackage: 'reasonix@latest', npmBin: 'reasonix', arguments: [], environment: {} },
66
+ deepseek: { npmPackage: 'codewhale@latest', npmBin: 'codewhale', arguments: [], environment: {} },
67
+ aider: { uvPackage: 'aider-chat', uvBin: 'aider', arguments: [], environment: {}, resumeArguments: ['--restore-chat-history'] },
68
+ },
69
+ hooks: { failFast: false },
70
+ };
71
+ /** Per-user folder for config and hooks. Survives reinstalling the npm package. */
72
+ export const userDir = () => process.env.AGENT_SANDBOX_HOME ?? path.join(homeDir(), '.agent-sandbox');
73
+ export const userConfigPath = () => path.join(userDir(), 'config.json');
74
+ /** Deep merge where arrays (and anything that is not a plain object) replace instead of merging element by element. */
75
+ export const mergeConfig = (base, extra) => _.mergeWith(_.cloneDeep(base), extra, (_target, source) => (Array.isArray(source) ? source : undefined));
76
+ /** Validates a full config; throws one readable error listing every problem. */
77
+ export const validateConfig = (config, source) => {
78
+ const parsed = ConfigSchema.safeParse(config);
79
+ if (!parsed.success)
80
+ fail(source + ' is not valid:\n - ' + describeIssues(parsed.error).join('\n - '));
81
+ return parsed.data;
82
+ };
83
+ /**
84
+ * Lookup order: --config, %AGENT_SANDBOX_CONFIG%, ~/.agent-sandbox/config.json, built-in defaults.
85
+ * Whatever file is found (JSON with comments) is merged over the defaults, so it only needs the keys you change.
86
+ */
87
+ export const loadConfig = (explicitPath) => {
88
+ const requested = explicitPath ?? process.env.AGENT_SANDBOX_CONFIG;
89
+ const full = path.resolve(requested ?? userConfigPath());
90
+ if (!fs.existsSync(full)) {
91
+ if (requested)
92
+ fail('Config file not found: ' + full, 'Check the --config path or AGENT_SANDBOX_CONFIG, or create one with: sandbox config init');
93
+ return { config: structuredClone(DEFAULTS), configPath: null, base: userDir() };
94
+ }
95
+ return { config: validateConfig(mergeConfig(DEFAULTS, readJsonc(full)), full), configPath: full, base: path.dirname(full) };
96
+ };
97
+ export const assertNoGuestOverlap = (mappings) => {
98
+ const normal = mappings.map((m) => m.guest.replace(/\\+$/, '').toLowerCase());
99
+ normal.forEach((a, i) => normal.slice(i + 1).forEach((b, j) => {
100
+ if (a === b || a.startsWith(b + '\\') || b.startsWith(a + '\\'))
101
+ fail('Two folders map to overlapping places in the sandbox: ' + mappings[i].guest + ' and ' + mappings[i + 1 + j].guest + '.', 'Give each mapping its own guest folder (one must not be inside the other).');
102
+ }));
103
+ };
@@ -0,0 +1,15 @@
1
+ export declare const DATABASES: readonly ["postgres", "mysql", "sqlserver", "mongodb", "redis", "dynamodb"];
2
+ export type Database = (typeof DATABASES)[number];
3
+ export interface ProjectFile {
4
+ path: string;
5
+ text: string;
6
+ }
7
+ /** Files worth reading, by name (lower case). Everything else is ignored, so a scan stays fast on big repos. */
8
+ export declare const INTERESTING_FILE: RegExp;
9
+ export interface Detection {
10
+ database: Database;
11
+ reasons: string[];
12
+ }
13
+ export declare const detectDatabases: (input: ProjectFile[], hostHas?: Database[] | (() => Database[])) => Detection[];
14
+ /** Windows service names of databases installed on this PC (from `sc query`), e.g. postgresql-x64-16, MySQL80. */
15
+ export declare const databasesFromServices: (serviceNames: string[]) => Database[];
@@ -0,0 +1,147 @@
1
+ // Which databases a project needs, worked out from its files: compose files, dependency manifests of every ecosystem,
2
+ // ORM schemas, connection strings and .env templates. Pure (file contents in, verdicts out), so the host can run it at
3
+ // launch and the tests can feed it fixtures.
4
+ export const DATABASES = ['postgres', 'mysql', 'sqlserver', 'mongodb', 'redis', 'dynamodb'];
5
+ /** Files worth reading, by name (lower case). Everything else is ignored, so a scan stays fast on big repos. */
6
+ export const INTERESTING_FILE = /^(package\.json|requirements[\w.-]{0,40}\.txt|pyproject\.toml|pipfile|setup\.py|setup\.cfg|gemfile|go\.mod|cargo\.toml|composer\.json|pom\.xml|build\.gradle(\.kts)?|[\w.-]+\.(cs|fs|vb)proj|directory\.packages\.props|packages\.config|schema\.prisma|(docker-)?compose[\w.-]{0,40}\.ya?ml|\.env(\.[\w.-]+)?|appsettings[\w.-]{0,40}\.json|database\.yml|application[\w.-]{0,40}\.(properties|ya?ml)|knexfile\.[jt]s|ormconfig\.(json|[jt]s)|drizzle\.config\.[jt]s|alembic\.ini|settings\.py|mix\.exs|deno\.json|serverless\.ya?ml|template\.ya?ml|samconfig\.toml|[\w.-]+\.tf|cdk\.json)$/i;
7
+ const manifests = /^(package\.json|deno\.json)$/i;
8
+ const python = /^(requirements[\w.-]{0,40}\.txt|pyproject\.toml|pipfile|setup\.py|setup\.cfg)$/i;
9
+ const dotnet = /\.(cs|fs|vb)proj$|^directory\.packages\.props$|^packages\.config$/i;
10
+ const jvm = /^(pom\.xml|build\.gradle(\.kts)?)$/i;
11
+ const compose = /^(docker-)?compose[\w.-]{0,40}\.ya?ml$/i;
12
+ const config = /^(\.env(\.[\w.-]+)?|appsettings[\w.-]{0,40}\.json|database\.yml|application[\w.-]{0,40}\.(properties|ya?ml)|knexfile\.[jt]s|ormconfig\.(json|[jt]s)|drizzle\.config\.[jt]s|alembic\.ini|settings\.py)$/i;
13
+ const infra = /^(serverless\.ya?ml|template\.ya?ml|[\w.-]+\.tf)$/i;
14
+ const dep = (names) => new RegExp('["\'\\s/=]?(' + names + ')["\'\\s@=<>~^:,\\]\\n]', 'i');
15
+ const RULES = {
16
+ postgres: [
17
+ [compose, /image:\s*["']?[\w./-]{0,80}(postgres|postgis|timescaledb|supabase\/postgres|pgvector)[\w.-]{0,40}(:|\s|["']|$)/im, 'a compose service runs Postgres'],
18
+ [manifests, dep('pg|postgres|pg-promise|@neondatabase/serverless|@vercel/postgres|slonik|postgres-js|@electric-sql/pglite'), 'a Postgres driver in package.json'],
19
+ [python, dep('psycopg2?(-binary)?|psycopg\\[[\\w,]+\\]|asyncpg|pg8000|django\\.db\\.backends\\.postgresql'), 'a Postgres driver in the Python dependencies'],
20
+ [/^gemfile$/i, /gem\s+["']pg["']/, 'the pg gem'],
21
+ [/^go\.mod$/i, /github\.com\/(jackc\/pgx|lib\/pq)/, 'a Postgres driver in go.mod'],
22
+ [/^cargo\.toml$/i, /tokio-postgres|postgres\s*=|sqlx[^\n]{0,200}postgres|diesel[^\n]{0,200}postgres|sea-orm[^\n]{0,200}postgres/i, 'a Postgres driver in Cargo.toml'],
23
+ [dotnet, /Npgsql/i, 'Npgsql in a .NET project'],
24
+ [jvm, /org\.postgresql|postgresql:postgresql|r2dbc-postgresql/i, 'the Postgres JDBC driver'],
25
+ [/^composer\.json$/i, /ext-pgsql|ext-pdo_pgsql/i, 'the PHP Postgres extension'],
26
+ [/^mix\.exs$/i, /:postgrex/, 'Postgrex'],
27
+ [/^schema\.prisma$/i, /provider\s*=\s*"(postgresql|postgres|cockroachdb)"/, 'Prisma uses PostgreSQL'],
28
+ [config, /postgres(ql)?(\+\w+)?:\/\/|DB_CONNECTION\s*=\s*pgsql|adapter:\s*postgres|\bHost=[^;\s]{1,200};\s*(Port=\d{1,5};\s*)?Database=|jdbc:postgresql:/i, 'a Postgres connection string'],
29
+ ],
30
+ mysql: [
31
+ [compose, /image:\s*["']?[\w./-]{0,80}(mysql|mariadb|percona)[\w.-]{0,40}(:|\s|["']|$)/im, 'a compose service runs MySQL / MariaDB'],
32
+ [manifests, dep('mysql2?|mariadb|@planetscale/database'), 'a MySQL driver in package.json'],
33
+ [python, dep('pymysql|mysqlclient|mysql-connector-python|aiomysql|asyncmy|mariadb'), 'a MySQL driver in the Python dependencies'],
34
+ [/^gemfile$/i, /gem\s+["'](mysql2|trilogy)["']/, 'a MySQL gem'],
35
+ [/^go\.mod$/i, /github\.com\/go-sql-driver\/mysql/, 'the MySQL driver in go.mod'],
36
+ [/^cargo\.toml$/i, /mysql_async|mysql\s*=|sqlx[^\n]{0,200}mysql|diesel[^\n]{0,200}mysql/i, 'a MySQL driver in Cargo.toml'],
37
+ [dotnet, /MySqlConnector|MySql\.Data|Pomelo\.EntityFrameworkCore\.MySql/i, 'a MySQL driver in a .NET project'],
38
+ [jvm, /mysql-connector|mariadb-java-client|com\.mysql/i, 'the MySQL JDBC driver'],
39
+ [/^composer\.json$/i, /ext-mysqli|ext-pdo_mysql/i, 'the PHP MySQL extension'],
40
+ [/^mix\.exs$/i, /:myxql/, 'MyXQL'],
41
+ [/^schema\.prisma$/i, /provider\s*=\s*"mysql"/, 'Prisma uses MySQL'],
42
+ [config, /(mysql|mariadb)(\+\w+)?:\/\/|DB_CONNECTION\s*=\s*(mysql|mariadb)|adapter:\s*(mysql2|trilogy)|jdbc:(mysql|mariadb):/i, 'a MySQL connection string'],
43
+ ],
44
+ sqlserver: [
45
+ [compose, /image:\s*["']?(mcr\.microsoft\.com\/(mssql|azure-sql-edge)|[\w./-]{0,80}mssql)[\w./:-]{0,80}/im, 'a compose service runs SQL Server'],
46
+ [manifests, dep('mssql|tedious|msnodesqlv8'), 'a SQL Server driver in package.json'],
47
+ [python, dep('pyodbc|pymssql|aioodbc|mssql-django'), 'a SQL Server driver in the Python dependencies'],
48
+ [/^go\.mod$/i, /(denisenkom|microsoft)\/go-mssqldb/, 'the SQL Server driver in go.mod'],
49
+ [/^cargo\.toml$/i, /tiberius|sqlx[^\n]{0,200}mssql/i, 'a SQL Server driver in Cargo.toml'],
50
+ [dotnet, /Microsoft\.Data\.SqlClient|System\.Data\.SqlClient|EntityFrameworkCore\.SqlServer|Dapper\.SqlBuilder|LocalDb/i, 'SQL Server in a .NET project'],
51
+ [jvm, /mssql-jdbc|com\.microsoft\.sqlserver/i, 'the SQL Server JDBC driver'],
52
+ [/^composer\.json$/i, /ext-sqlsrv|ext-pdo_sqlsrv/i, 'the PHP SQL Server extension'],
53
+ [/^schema\.prisma$/i, /provider\s*=\s*"sqlserver"/, 'Prisma uses SQL Server'],
54
+ [config, /\(localdb\)|mssql(\+\w+)?:\/\/|sqlserver:\/\/|DB_CONNECTION\s*=\s*sqlsrv|jdbc:sqlserver:|Initial Catalog=|Trusted_Connection=|TrustServerCertificate=|Integrated Security=/i, 'a SQL Server connection string'],
55
+ ],
56
+ mongodb: [
57
+ [compose, /image:\s*["']?[\w./-]{0,80}mongo(db)?[\w.-]{0,40}(:|\s|["']|$)/im, 'a compose service runs MongoDB'],
58
+ [manifests, dep('mongodb|mongoose|@typegoose/typegoose|mongodb-memory-server'), 'a MongoDB driver in package.json'],
59
+ [python, dep('pymongo|motor|mongoengine|beanie|djongo'), 'a MongoDB driver in the Python dependencies'],
60
+ [/^gemfile$/i, /gem\s+["'](mongoid|mongo)["']/, 'a MongoDB gem'],
61
+ [/^go\.mod$/i, /go\.mongodb\.org\/mongo-driver/, 'the MongoDB driver in go.mod'],
62
+ [/^cargo\.toml$/i, /^\s*mongodb\s*=/m, 'the MongoDB crate'],
63
+ [dotnet, /MongoDB\.Driver|MongoDB\.EntityFrameworkCore/i, 'the MongoDB driver in a .NET project'],
64
+ [jvm, /mongodb-driver|spring-boot-starter-data-mongodb/i, 'the MongoDB Java driver'],
65
+ [/^schema\.prisma$/i, /provider\s*=\s*"mongodb"/, 'Prisma uses MongoDB'],
66
+ [config, /mongodb(\+srv)?:\/\//i, 'a MongoDB connection string'],
67
+ ],
68
+ dynamodb: [
69
+ [compose, /image:\s*["']?(amazon\/dynamodb-local|[\w./-]{0,80}dynamodb-local|instructure\/dynamo-local-admin)[\w.:-]{0,40}/im, 'a compose service runs DynamoDB Local'],
70
+ [manifests, dep('@aws-sdk/client-dynamodb|@aws-sdk/lib-dynamodb|dynamoose|electrodb|dynamodb-toolbox|@typedorm/core|dynamodb-onetable'), 'a DynamoDB client in package.json'],
71
+ [python, dep('pynamodb|boto3-stubs\\[[^\\]]{0,80}dynamodb[^\\]]{0,80}\\]|types-aiobotocore-dynamodb|mypy-boto3-dynamodb|dynamorm'), 'a DynamoDB client in the Python dependencies'],
72
+ [/^gemfile$/i, /gem\s+["'](aws-sdk-dynamodb|dynamoid|aws-record)["']/, 'a DynamoDB gem'],
73
+ [/^go\.mod$/i, /aws-sdk-go-v2\/service\/dynamodb|github\.com\/guregu\/dynamo/, 'a DynamoDB client in go.mod'],
74
+ [/^cargo\.toml$/i, /^\s*(aws-sdk-dynamodb|serde_dynamo|modyne)\s*=/m, 'a DynamoDB crate'],
75
+ [dotnet, /AWSSDK\.DynamoDBv2/i, 'AWSSDK.DynamoDBv2 in a .NET project'],
76
+ [jvm, /software\.amazon\.awssdk[^\n]{0,200}dynamodb|aws-java-sdk-dynamodb|dynamodb-enhanced/i, 'the DynamoDB Java SDK'],
77
+ [infra, /AWS::DynamoDB::(Global)?Table|aws_dynamodb_table/, 'a DynamoDB table in the infrastructure code'],
78
+ [config, /AWS_ENDPOINT_URL_DYNAMODB|DYNAMO(DB)?_(ENDPOINT|URL|TABLE)|dynamodb\.[\w-]+\.amazonaws\.com|localhost:8000/i, 'DynamoDB settings in the config'],
79
+ ],
80
+ redis: [
81
+ [compose, /image:\s*["']?[\w./-]{0,80}(redis|valkey|keydb|dragonfly)[\w.-]{0,40}(:|\s|["']|$)/im, 'a compose service runs Redis'],
82
+ [manifests, dep('redis|ioredis|bullmq|bull|@upstash/redis|connect-redis|rate-limit-redis'), 'a Redis client in package.json'],
83
+ [python, dep('redis|aioredis|rq|celery\\[redis\\]|django-redis|arq'), 'a Redis client in the Python dependencies'],
84
+ [/^gemfile$/i, /gem\s+["'](redis|sidekiq|resque)["']/, 'a Redis gem'],
85
+ [/^go\.mod$/i, /github\.com\/(redis\/go-redis|go-redis\/redis|gomodule\/redigo)/, 'a Redis client in go.mod'],
86
+ [/^cargo\.toml$/i, /^\s*(redis|deadpool-redis|bb8-redis)\s*=/m, 'a Redis crate'],
87
+ [dotnet, /StackExchange\.Redis|Microsoft\.Extensions\.Caching\.StackExchangeRedis/i, 'StackExchange.Redis in a .NET project'],
88
+ [jvm, /jedis|lettuce-core|spring-boot-starter-data-redis|redisson/i, 'a Redis Java client'],
89
+ [/^composer\.json$/i, /predis\/predis|ext-redis/i, 'a PHP Redis client'],
90
+ [config, /rediss?:\/\/|REDIS_(URL|HOST)\s*=|CACHE_DRIVER\s*=\s*redis/i, 'a Redis connection string'],
91
+ ],
92
+ };
93
+ /** Libraries that talk to "a SQL database" without saying which: then this PC's own database decides (see below). */
94
+ const GENERIC_SQL = [
95
+ [manifests, dep('knex|sequelize|typeorm|drizzle-orm|kysely|@mikro-orm/core|objection|prisma'), 'an SQL ORM'],
96
+ [python, dep('sqlalchemy|django|peewee|tortoise-orm|sqlmodel|alembic'), 'an SQL ORM'],
97
+ [dotnet, /Microsoft\.EntityFrameworkCore(?!\.(Sqlite|InMemory))|Dapper/i, 'Entity Framework / Dapper'],
98
+ [/^gemfile$/i, /gem\s+["'](rails|activerecord|sequel)["']/, 'ActiveRecord'],
99
+ [jvm, /spring-boot-starter-data-jpa|hibernate-core|jooq/i, 'JPA / Hibernate'],
100
+ [/^go\.mod$/i, /gorm\.io\/gorm|entgo\.io\/ent|github\.com\/jmoiron\/sqlx/, 'an SQL ORM'],
101
+ [/^cargo\.toml$/i, /^\s*(sqlx|diesel|sea-orm)\s*=/m, 'an SQL ORM'],
102
+ [/^composer\.json$/i, /laravel\/framework|doctrine\/orm/i, 'Laravel / Doctrine'],
103
+ ];
104
+ const fileName = (p) => p.split(/[\\/]/).pop() ?? p;
105
+ const SQL = ['postgres', 'mysql', 'sqlserver'];
106
+ /**
107
+ * The databases a project needs, each with the evidence ("compose.yaml: a compose service runs Postgres"). `hostHas` is
108
+ * what this PC has installed: when the project only shows a database-agnostic ORM, the SQL database you use here is
109
+ * taken (Postgres first if several), so an ORM project still gets a database.
110
+ */
111
+ /** Longest line and file looked at: bounded work per file whatever a (sandbox-written) file contains. */
112
+ const MAX_LINE = 1000;
113
+ const MAX_TEXT = 256 * 1024;
114
+ const bounded = (text) => text.slice(0, MAX_TEXT).split('\n').filter((line) => line.length <= MAX_LINE).join('\n');
115
+ export const detectDatabases = (input, hostHas = []) => {
116
+ const files = input.map((f) => ({ path: f.path, text: bounded(f.text) }));
117
+ const found = new Map();
118
+ const add = (db, reason) => { const list = found.get(db) ?? []; if (!list.includes(reason))
119
+ list.push(reason); found.set(db, list); };
120
+ for (const file of files) {
121
+ const name = fileName(file.path);
122
+ for (const db of DATABASES)
123
+ for (const [test, pattern, reason] of RULES[db])
124
+ if (test.test(name) && pattern.test(file.text))
125
+ add(db, file.path + ': ' + reason);
126
+ }
127
+ if (!SQL.some((db) => found.has(db))) {
128
+ const orm = files.flatMap((file) => GENERIC_SQL.filter(([test, pattern]) => test.test(fileName(file.path)) && pattern.test(file.text)).map(([, , reason]) => file.path + ': ' + reason));
129
+ const host = orm.length ? (typeof hostHas === 'function' ? hostHas() : hostHas) : [];
130
+ const mine = SQL.find((db) => host.includes(db));
131
+ if (orm.length && mine)
132
+ add(mine, orm[0] + ', and this PC has ' + mine + ' installed');
133
+ }
134
+ return DATABASES.filter((db) => found.has(db)).map((database) => ({ database, reasons: found.get(database) }));
135
+ };
136
+ /** Windows service names of databases installed on this PC (from `sc query`), e.g. postgresql-x64-16, MySQL80. */
137
+ export const databasesFromServices = (serviceNames) => {
138
+ const has = (r) => serviceNames.some((s) => r.test(s));
139
+ return DATABASES.filter((db) => ({
140
+ postgres: has(/^postgresql/i),
141
+ mysql: has(/^(mysql|mariadb)/i),
142
+ sqlserver: has(/^(MSSQLSERVER|MSSQL\$|SQLServer)/i),
143
+ mongodb: has(/^mongodb/i),
144
+ redis: has(/^(redis|memurai)/i),
145
+ dynamodb: false,
146
+ })[db]);
147
+ };
@@ -0,0 +1,18 @@
1
+ import type { ProjectFile } from './detect-databases.js';
2
+ /** Scripts looked at: the workspace root and one folder down, at most this many, only their start. */
3
+ export declare const PWSH_MAX_SCRIPTS = 40;
4
+ export declare const PWSH_MAX_BYTES: number;
5
+ /** The first script that needs PowerShell 7 and why ("moh.ps1: #Requires -Version 7"), or null. */
6
+ export declare const usesPwsh: (files: readonly ProjectFile[]) => string | null;
7
+ /** *.ps1 / *.psm1 / *.psd1 in the workspace root and its direct sub-folders (root first), capped. Links are skipped. */
8
+ export declare const readPwshScripts: (root: string) => ProjectFile[];
9
+ /** PowerShell 7 installed on this PC (the MSI's folder, or a pwsh.exe on PATH other than a Store alias). */
10
+ export declare const pcHasPwsh: (env?: NodeJS.ProcessEnv, exists?: (p: string) => boolean) => boolean;
11
+ /**
12
+ * The decision for toolchains.pwsh.enabled: true/false are kept; "auto" is on when a script needs PowerShell 7, or, in
13
+ * a folder that is no project yet (`fresh`), when this PC has it.
14
+ */
15
+ export declare const decidePwsh: (setting: boolean | "auto", scripts: readonly ProjectFile[], fresh: boolean, pcHas: boolean) => {
16
+ on: boolean;
17
+ reason: string;
18
+ };
@@ -0,0 +1,111 @@
1
+ // Does a project need PowerShell 7 (pwsh)? Windows PowerShell 5.1 ships with Windows, so a plain *.ps1 says nothing;
2
+ // a script that asks for 7 (#Requires -Version 7 / -PSEdition Core) or tells you to run it with `pwsh` does.
3
+ // usesPwsh is pure; readPwshScripts reads a bounded sample of the workspace (no links followed, files and bytes capped).
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ /** Scripts looked at: the workspace root and one folder down, at most this many, only their start. */
7
+ export const PWSH_MAX_SCRIPTS = 40;
8
+ export const PWSH_MAX_BYTES = 64 * 1024;
9
+ const MAX_LINES = 400;
10
+ const MAX_LINE = 400;
11
+ const SKIP = /^(node_modules|\.git|vendor|target|bin|obj|dist|build|out|\.venv|venv|packages)$/i;
12
+ /** Why one line asks for PowerShell 7, or null. Plain string work per line (no regex that can backtrack). */
13
+ const pwshLine = (raw) => {
14
+ const line = raw.trimStart().slice(0, MAX_LINE).toLowerCase();
15
+ if (line.startsWith('#requires')) {
16
+ const words = line.slice('#requires'.length).split(/\s+/).filter(Boolean);
17
+ const after = (flag) => words[words.indexOf(flag) + 1];
18
+ const version = words.includes('-version') ? Number.parseInt(after('-version') ?? '', 10) : NaN;
19
+ if (version >= 7)
20
+ return '#Requires -Version ' + version;
21
+ if (words.includes('-psedition') && after('-psedition') === 'core')
22
+ return '#Requires -PSEdition Core';
23
+ return null;
24
+ }
25
+ // "Usage: pwsh scripts/build.ps1", "& pwsh -File x.ps1", "pwsh.exe -NoProfile ...": the word pwsh, then an option
26
+ // or a script (so prose like "works in pwsh too" does not count).
27
+ // At most a few occurrences per line, each looking at a short window: linear in the (capped) input.
28
+ for (let at = line.indexOf('pwsh'), n = 0; at !== -1 && n < 4; at = line.indexOf('pwsh', at + 4), n++) {
29
+ const end = at + (line.startsWith('pwsh.exe', at) ? 8 : 4);
30
+ if (at > 0 && /[\w.$-]/.test(line[at - 1]))
31
+ continue;
32
+ if (line[end] !== ' ' && line[end] !== '\t')
33
+ continue;
34
+ const next = line.slice(end, end + 80).trimStart().split(/\s/, 1)[0] ?? '';
35
+ if (next.startsWith('-') || next.includes('.ps1'))
36
+ return 'pwsh usage';
37
+ }
38
+ return null;
39
+ };
40
+ /** The first script that needs PowerShell 7 and why ("moh.ps1: #Requires -Version 7"), or null. */
41
+ export const usesPwsh = (files) => {
42
+ for (const file of files.slice(0, PWSH_MAX_SCRIPTS)) {
43
+ if (!/\.ps[dm]?1$/i.test(file.path))
44
+ continue;
45
+ const lines = file.text.slice(0, PWSH_MAX_BYTES).split(/\r?\n/, MAX_LINES);
46
+ for (const line of lines) {
47
+ const why = pwshLine(line);
48
+ if (why)
49
+ return file.path.replace(/\\/g, '/') + ': ' + why;
50
+ }
51
+ }
52
+ return null;
53
+ };
54
+ /** The start of a file, through one handle, only when that handle is a regular file. */
55
+ const readStart = (file) => {
56
+ let fd = null;
57
+ try {
58
+ fd = fs.openSync(file, 'r');
59
+ const stat = fs.fstatSync(fd);
60
+ if (!stat.isFile())
61
+ return null;
62
+ const buffer = Buffer.alloc(Math.min(stat.size, PWSH_MAX_BYTES));
63
+ const read = fs.readSync(fd, buffer, 0, buffer.length, 0);
64
+ return buffer.subarray(0, read).toString('utf8');
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ finally {
70
+ if (fd !== null)
71
+ fs.closeSync(fd);
72
+ }
73
+ };
74
+ /** *.ps1 / *.psm1 / *.psd1 in the workspace root and its direct sub-folders (root first), capped. Links are skipped. */
75
+ export const readPwshScripts = (root) => {
76
+ const list = (dir) => { try {
77
+ return fs.readdirSync(dir, { withFileTypes: true }).slice(0, 2000);
78
+ }
79
+ catch {
80
+ return [];
81
+ } };
82
+ const isScript = (d) => d.isFile() && /\.ps[dm]?1$/i.test(d.name);
83
+ const top = list(root);
84
+ const names = [
85
+ ...top.filter(isScript).map((d) => d.name),
86
+ ...top.filter((d) => d.isDirectory() && !d.name.startsWith('.') && !SKIP.test(d.name)).slice(0, 200)
87
+ .flatMap((d) => list(path.join(root, d.name)).filter(isScript).map((f) => d.name + '/' + f.name)),
88
+ ].slice(0, PWSH_MAX_SCRIPTS);
89
+ return names.flatMap((name) => { const text = readStart(path.join(root, name)); return text === null ? [] : [{ path: name, text }]; });
90
+ };
91
+ /** PowerShell 7 installed on this PC (the MSI's folder, or a pwsh.exe on PATH other than a Store alias). */
92
+ export const pcHasPwsh = (env = process.env, exists = fs.existsSync) => {
93
+ const programFiles = env.ProgramFiles ?? 'C:\\Program Files';
94
+ if (exists(path.win32.join(programFiles, 'PowerShell', '7', 'pwsh.exe')))
95
+ return true;
96
+ return (env.PATH ?? env.Path ?? '').split(';').some((d) => d && !/WindowsApps/i.test(d) && exists(path.win32.join(d, 'pwsh.exe')));
97
+ };
98
+ /**
99
+ * The decision for toolchains.pwsh.enabled: true/false are kept; "auto" is on when a script needs PowerShell 7, or, in
100
+ * a folder that is no project yet (`fresh`), when this PC has it.
101
+ */
102
+ export const decidePwsh = (setting, scripts, fresh, pcHas) => {
103
+ if (setting !== 'auto')
104
+ return { on: setting, reason: setting ? 'enabled in config' : 'disabled in config' };
105
+ const why = usesPwsh(scripts);
106
+ if (why)
107
+ return { on: true, reason: why };
108
+ if (fresh && pcHas)
109
+ return { on: true, reason: 'installed on this PC (new folder)' };
110
+ return { on: false, reason: 'no script needs PowerShell 7' };
111
+ };
@@ -0,0 +1,30 @@
1
+ import type { Toolchains } from './types.js';
2
+ /** Files that show a project uses a language (looked for in the workspace and one folder down). */
3
+ export declare const MARKERS: {
4
+ readonly python: readonly ["pyproject.toml", "requirements.txt", "setup.py", "setup.cfg", "Pipfile", "uv.lock", "poetry.lock", ".python-version", "*.py", "*.ipynb"];
5
+ readonly rust: readonly ["Cargo.toml", "rust-toolchain", "rust-toolchain.toml"];
6
+ readonly msvc: readonly ["*.vcxproj", "CMakePresets.json", "CMakeUserPresets.json"];
7
+ readonly cpp: readonly ["CMakeLists.txt", "Makefile", "meson.build", "premake5.lua", "Cargo.toml", "*.vcxproj", "*.c", "*.cpp", "*.cc", "*.h", "*.hpp"];
8
+ readonly dotnet: readonly ["*.sln", "*.slnx", "*.csproj", "*.fsproj", "*.vbproj", "global.json", "*.cs"];
9
+ readonly go: readonly ["go.mod", "go.work", "*.go"];
10
+ readonly java: readonly ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts", "mvnw", "gradlew", ".java-version", ".sdkmanrc", "*.java"];
11
+ readonly ruby: readonly ["Gemfile", "*.gemspec", ".ruby-version", "*.rb"];
12
+ readonly php: readonly ["composer.json", "artisan", ".php-version", "*.php"];
13
+ readonly deno: readonly ["deno.json", "deno.jsonc", "deno.lock"];
14
+ readonly pwsh: readonly [];
15
+ };
16
+ export type AutoToolchain = keyof typeof MARKERS;
17
+ export declare const AUTO_TOOLCHAINS: AutoToolchain[];
18
+ export interface ToolchainDecision {
19
+ on: boolean;
20
+ reason: string;
21
+ }
22
+ /**
23
+ * The decision per toolchain. true/false are kept; "auto" is on when the project has one of its marker files, or, in
24
+ * a folder that is no project yet, when this PC has the language installed. Rust needs a linker (MSVC or C/C++).
25
+ * hints: reasons found by reading files (a script that calls vcvars/vswhere) or about this PC ("builds Rust with MSVC"),
26
+ * which turn an "auto" toolchain on like a marker file does.
27
+ */
28
+ export declare const decideToolchains: (settings: Toolchains, entries: string[], pcHas: AutoToolchain[], hints?: Partial<Record<AutoToolchain, string>>) => Record<AutoToolchain, ToolchainDecision>;
29
+ /** The settings with every "auto" replaced by the decision (what the sandbox receives). */
30
+ export declare const resolveToolchains: (settings: Toolchains, decisions: Record<AutoToolchain, ToolchainDecision>) => Toolchains;