@fitlab-ai/agent-infra 0.5.9 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +200 -8
  2. package/README.zh-CN.md +176 -8
  3. package/bin/{cli.js → cli.ts} +23 -19
  4. package/dist/bin/cli.js +116 -0
  5. package/dist/lib/defaults.json +61 -0
  6. package/dist/lib/init.js +238 -0
  7. package/dist/lib/log.js +18 -0
  8. package/dist/lib/merge.js +747 -0
  9. package/dist/lib/paths.js +18 -0
  10. package/dist/lib/prompt.js +85 -0
  11. package/dist/lib/render.js +139 -0
  12. package/dist/lib/sandbox/commands/create.js +1173 -0
  13. package/dist/lib/sandbox/commands/enter.js +98 -0
  14. package/dist/lib/sandbox/commands/ls.js +93 -0
  15. package/dist/lib/sandbox/commands/rebuild.js +101 -0
  16. package/dist/lib/sandbox/commands/refresh.js +85 -0
  17. package/dist/lib/sandbox/commands/rm.js +226 -0
  18. package/dist/lib/sandbox/commands/vm.js +144 -0
  19. package/dist/lib/sandbox/config.js +85 -0
  20. package/dist/lib/sandbox/constants.js +104 -0
  21. package/dist/lib/sandbox/credentials.js +437 -0
  22. package/dist/lib/sandbox/dockerfile.js +76 -0
  23. package/dist/lib/sandbox/dotfiles.js +170 -0
  24. package/dist/lib/sandbox/engine.js +155 -0
  25. package/dist/lib/sandbox/engines/colima.js +64 -0
  26. package/dist/lib/sandbox/engines/docker-desktop.js +27 -0
  27. package/dist/lib/sandbox/engines/index.js +25 -0
  28. package/dist/lib/sandbox/engines/native.js +96 -0
  29. package/dist/lib/sandbox/engines/orbstack.js +63 -0
  30. package/dist/lib/sandbox/engines/selinux.js +48 -0
  31. package/dist/lib/sandbox/engines/wsl2-paths.js +47 -0
  32. package/dist/lib/sandbox/engines/wsl2.js +57 -0
  33. package/dist/lib/sandbox/index.js +70 -0
  34. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +39 -0
  35. package/dist/lib/sandbox/runtimes/base.dockerfile +178 -0
  36. package/dist/lib/sandbox/runtimes/java17.dockerfile +3 -0
  37. package/dist/lib/sandbox/runtimes/java21.dockerfile +3 -0
  38. package/dist/lib/sandbox/runtimes/node20.dockerfile +3 -0
  39. package/dist/lib/sandbox/runtimes/node22.dockerfile +3 -0
  40. package/dist/lib/sandbox/runtimes/python3.dockerfile +3 -0
  41. package/dist/lib/sandbox/shell.js +148 -0
  42. package/dist/lib/sandbox/task-resolver.js +35 -0
  43. package/dist/lib/sandbox/tools.js +115 -0
  44. package/dist/lib/update.js +186 -0
  45. package/dist/lib/version.js +5 -0
  46. package/dist/package.json +5 -0
  47. package/lib/{init.js → init.ts} +64 -20
  48. package/lib/{log.js → log.ts} +4 -4
  49. package/lib/{merge.js → merge.ts} +129 -63
  50. package/lib/paths.ts +18 -0
  51. package/lib/{prompt.js → prompt.ts} +12 -12
  52. package/lib/{render.js → render.ts} +30 -17
  53. package/lib/sandbox/commands/create.ts +1507 -0
  54. package/lib/sandbox/commands/enter.ts +115 -0
  55. package/lib/sandbox/commands/{ls.js → ls.ts} +41 -10
  56. package/lib/sandbox/commands/rebuild.ts +135 -0
  57. package/lib/sandbox/commands/refresh.ts +128 -0
  58. package/lib/sandbox/commands/{rm.js → rm.ts} +71 -21
  59. package/lib/sandbox/commands/{vm.js → vm.ts} +62 -15
  60. package/lib/sandbox/config.ts +133 -0
  61. package/lib/sandbox/{constants.js → constants.ts} +41 -17
  62. package/lib/sandbox/credentials.ts +634 -0
  63. package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
  64. package/lib/sandbox/dotfiles.ts +236 -0
  65. package/lib/sandbox/engine.ts +231 -0
  66. package/lib/sandbox/engines/colima.ts +81 -0
  67. package/lib/sandbox/engines/docker-desktop.ts +36 -0
  68. package/lib/sandbox/engines/index.ts +74 -0
  69. package/lib/sandbox/engines/native.ts +131 -0
  70. package/lib/sandbox/engines/orbstack.ts +78 -0
  71. package/lib/sandbox/engines/selinux.ts +66 -0
  72. package/lib/sandbox/engines/wsl2-paths.ts +65 -0
  73. package/lib/sandbox/engines/wsl2.ts +74 -0
  74. package/lib/sandbox/{index.js → index.ts} +17 -8
  75. package/lib/sandbox/runtimes/ai-tools.dockerfile +14 -1
  76. package/lib/sandbox/runtimes/base.dockerfile +116 -1
  77. package/lib/sandbox/shell.ts +186 -0
  78. package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
  79. package/lib/sandbox/{tools.js → tools.ts} +33 -29
  80. package/lib/{update.js → update.ts} +33 -10
  81. package/package.json +22 -12
  82. package/templates/.agents/rules/create-issue.github.en.md +2 -4
  83. package/templates/.agents/rules/create-issue.github.zh-CN.md +2 -4
  84. package/templates/.agents/rules/issue-pr-commands.github.en.md +29 -0
  85. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +29 -0
  86. package/templates/.agents/scripts/{platform-adapters/find-existing-task.github.js → find-existing-task.js} +22 -79
  87. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +26 -41
  88. package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
  89. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  90. package/templates/.agents/skills/import-issue/SKILL.en.md +6 -8
  91. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +6 -8
  92. package/lib/paths.js +0 -9
  93. package/lib/sandbox/commands/create.js +0 -1174
  94. package/lib/sandbox/commands/enter.js +0 -79
  95. package/lib/sandbox/commands/rebuild.js +0 -102
  96. package/lib/sandbox/config.js +0 -84
  97. package/lib/sandbox/engine.js +0 -256
  98. package/lib/sandbox/shell.js +0 -122
  99. package/templates/.agents/scripts/platform-adapters/find-existing-task.js +0 -5
  100. /package/lib/{version.js → version.ts} +0 -0
@@ -0,0 +1,236 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { hostJoin } from './engines/wsl2-paths.ts';
4
+
5
+ type DotfilesWarning = {
6
+ rel: string;
7
+ reason: string;
8
+ detail?: string;
9
+ };
10
+
11
+ type DotfilesFs = Pick<typeof fs, 'copyFileSync' | 'existsSync' | 'mkdirSync' | 'readdirSync' | 'realpathSync' | 'rmSync' | 'statSync'>;
12
+
13
+ type MaterializeOptions = {
14
+ writeStderr?: (message: string) => void;
15
+ maxDepth?: number;
16
+ fsModule?: DotfilesFs;
17
+ };
18
+
19
+ type WalkContext = {
20
+ srcDir: string;
21
+ dstDir: string;
22
+ relParts: string[];
23
+ depth: number;
24
+ maxDepth: number;
25
+ activeDirs: Set<string>;
26
+ warnings: DotfilesWarning[];
27
+ writeStderr: (message: string) => void;
28
+ fsModule: DotfilesFs;
29
+ };
30
+
31
+ export function dotfilesCacheDir(home: string, project: string): string {
32
+ return hostJoin(home, '.agent-infra', '.cache', 'dotfiles-resolved', project);
33
+ }
34
+
35
+ function dotfilesWarning(
36
+ warnings: DotfilesWarning[],
37
+ writeStderr: (message: string) => void,
38
+ relPath: string,
39
+ reason: string,
40
+ detail = ''
41
+ ): void {
42
+ const warning: DotfilesWarning = { rel: relPath, reason };
43
+ if (detail) {
44
+ warning.detail = detail;
45
+ }
46
+ warnings.push(warning);
47
+
48
+ const suffix = detail ? `: ${detail}` : '';
49
+ writeStderr(`sandbox-dotfiles (host): skipping ${relPath} (${reason}${suffix})\n`);
50
+ }
51
+
52
+ function errorDetail(error: unknown): string {
53
+ return error instanceof Error ? error.message : 'unknown error';
54
+ }
55
+
56
+ function errorCodeOrDetail(error: unknown): string {
57
+ return typeof error === 'object' && error !== null && 'code' in error
58
+ ? String(error.code)
59
+ : errorDetail(error);
60
+ }
61
+
62
+ function copyDotfile(
63
+ srcPath: string,
64
+ dstPath: string,
65
+ context: Pick<WalkContext, 'fsModule' | 'warnings' | 'writeStderr'> & { relPath: string }
66
+ ): void {
67
+ const { fsModule, relPath, warnings, writeStderr } = context;
68
+ try {
69
+ fsModule.mkdirSync(path.dirname(dstPath), { recursive: true });
70
+ fsModule.copyFileSync(srcPath, dstPath);
71
+ } catch (error) {
72
+ dotfilesWarning(warnings, writeStderr, relPath, 'copy failed', errorCodeOrDetail(error));
73
+ }
74
+ }
75
+
76
+ function walkAndMaterializeDotfiles(context: WalkContext): void {
77
+ const {
78
+ srcDir,
79
+ dstDir,
80
+ relParts,
81
+ depth,
82
+ maxDepth,
83
+ activeDirs,
84
+ warnings,
85
+ writeStderr,
86
+ fsModule
87
+ } = context;
88
+ const relPath = relParts.length > 0 ? relParts.join('/') : '.';
89
+
90
+ if (depth > maxDepth) {
91
+ dotfilesWarning(warnings, writeStderr, relPath, 'depth exceeds limit', String(maxDepth));
92
+ return;
93
+ }
94
+
95
+ let entries: fs.Dirent[];
96
+ try {
97
+ entries = fsModule.readdirSync(srcDir, { withFileTypes: true });
98
+ } catch (error) {
99
+ dotfilesWarning(warnings, writeStderr, relPath, 'read failed', errorCodeOrDetail(error));
100
+ return;
101
+ }
102
+
103
+ for (const entry of entries) {
104
+ const childSrc = path.join(srcDir, entry.name);
105
+ const childDst = path.join(dstDir, entry.name);
106
+ const childRelParts = [...relParts, entry.name];
107
+ const childRelPath = childRelParts.join('/');
108
+
109
+ if (entry.isSymbolicLink()) {
110
+ let resolvedTarget: string;
111
+ try {
112
+ resolvedTarget = fsModule.realpathSync(childSrc);
113
+ } catch (error) {
114
+ const code = errorCodeOrDetail(error);
115
+ const reason = code === 'ELOOP' ? 'symlink loop' : 'dangling symlink';
116
+ dotfilesWarning(warnings, writeStderr, childRelPath, reason, code || 'unresolved');
117
+ continue;
118
+ }
119
+
120
+ let targetStat: fs.Stats;
121
+ try {
122
+ targetStat = fsModule.statSync(resolvedTarget);
123
+ } catch (error) {
124
+ dotfilesWarning(warnings, writeStderr, childRelPath, 'target stat failed', errorCodeOrDetail(error));
125
+ continue;
126
+ }
127
+
128
+ if (targetStat.isDirectory()) {
129
+ if (activeDirs.has(resolvedTarget)) {
130
+ dotfilesWarning(warnings, writeStderr, childRelPath, 'symlink loop');
131
+ continue;
132
+ }
133
+
134
+ activeDirs.add(resolvedTarget);
135
+ walkAndMaterializeDotfiles({
136
+ srcDir: resolvedTarget,
137
+ dstDir: childDst,
138
+ relParts: childRelParts,
139
+ depth: depth + 1,
140
+ maxDepth,
141
+ activeDirs,
142
+ warnings,
143
+ writeStderr,
144
+ fsModule
145
+ });
146
+ activeDirs.delete(resolvedTarget);
147
+ continue;
148
+ }
149
+
150
+ if (targetStat.isFile()) {
151
+ copyDotfile(resolvedTarget, childDst, {
152
+ fsModule,
153
+ relPath: childRelPath,
154
+ warnings,
155
+ writeStderr
156
+ });
157
+ }
158
+ continue;
159
+ }
160
+
161
+ if (entry.isDirectory()) {
162
+ let childRealPath: string | null = null;
163
+ try {
164
+ childRealPath = fsModule.realpathSync(childSrc);
165
+ } catch {
166
+ // A real directory may disappear during traversal; readdir will warn below.
167
+ }
168
+ if (childRealPath) {
169
+ activeDirs.add(childRealPath);
170
+ }
171
+ walkAndMaterializeDotfiles({
172
+ srcDir: childSrc,
173
+ dstDir: childDst,
174
+ relParts: childRelParts,
175
+ depth: depth + 1,
176
+ maxDepth,
177
+ activeDirs,
178
+ warnings,
179
+ writeStderr,
180
+ fsModule
181
+ });
182
+ if (childRealPath) {
183
+ activeDirs.delete(childRealPath);
184
+ }
185
+ continue;
186
+ }
187
+
188
+ if (entry.isFile()) {
189
+ copyDotfile(childSrc, childDst, {
190
+ fsModule,
191
+ relPath: childRelPath,
192
+ warnings,
193
+ writeStderr
194
+ });
195
+ }
196
+ }
197
+ }
198
+
199
+ export function materializeDotfiles(srcDir: string, cacheDir: string, options: MaterializeOptions = {}) {
200
+ const {
201
+ writeStderr = (message) => process.stderr.write(message),
202
+ maxDepth = 32,
203
+ fsModule = fs
204
+ } = options;
205
+
206
+ if (!srcDir || !fsModule.existsSync(srcDir)) {
207
+ return null;
208
+ }
209
+
210
+ fsModule.mkdirSync(cacheDir, { recursive: true });
211
+ for (const entry of fsModule.readdirSync(cacheDir)) {
212
+ fsModule.rmSync(path.join(cacheDir, entry), { recursive: true, force: true });
213
+ }
214
+
215
+ const warnings: DotfilesWarning[] = [];
216
+ const activeDirs = new Set<string>();
217
+ try {
218
+ activeDirs.add(fsModule.realpathSync(srcDir));
219
+ } catch {
220
+ activeDirs.add(srcDir);
221
+ }
222
+
223
+ walkAndMaterializeDotfiles({
224
+ srcDir,
225
+ dstDir: cacheDir,
226
+ relParts: [],
227
+ depth: 0,
228
+ maxDepth,
229
+ activeDirs,
230
+ warnings,
231
+ writeStderr,
232
+ fsModule
233
+ });
234
+
235
+ return { cacheDir, warnings };
236
+ }
@@ -0,0 +1,231 @@
1
+ import { platform } from 'node:os';
2
+ import { detectHostResources } from './constants.ts';
3
+ import { ADAPTERS, enginesForPlatform, getAdapter } from './engines/index.ts';
4
+ import type { EffectiveSandboxConfig, OnMessage, RunFns, SandboxAdapter, SandboxVmConfig } from './engines/index.ts';
5
+ import { run, runOk, runSafe, runVerbose } from './shell.ts';
6
+
7
+ export const ENGINES = Object.freeze({
8
+ COLIMA: 'colima',
9
+ ORBSTACK: 'orbstack',
10
+ DOCKER_DESKTOP: 'docker-desktop',
11
+ NATIVE: 'native',
12
+ WSL2: 'wsl2'
13
+ });
14
+
15
+ const PLATFORM_DEFAULTS = Object.freeze({
16
+ linux: ENGINES.NATIVE,
17
+ darwin: ENGINES.COLIMA,
18
+ win32: ENGINES.WSL2
19
+ });
20
+
21
+ type EngineConfig = EffectiveSandboxConfig & {
22
+ engine?: string | null;
23
+ };
24
+
25
+ type EngineDependencies = {
26
+ platformFn?: typeof platform;
27
+ runFn?: RunFns['run'];
28
+ runOkFn?: RunFns['runOk'];
29
+ runSafeFn?: RunFns['runSafe'];
30
+ runVerboseFn?: RunFns['runVerbose'];
31
+ };
32
+
33
+ function runFns({
34
+ runFn = run,
35
+ runOkFn = runOk,
36
+ runSafeFn = runSafe,
37
+ runVerboseFn = runVerbose
38
+ }: EngineDependencies = {}): RunFns {
39
+ return {
40
+ run: runFn,
41
+ runOk: runOkFn,
42
+ runSafe: runSafeFn,
43
+ runVerbose: runVerboseFn
44
+ };
45
+ }
46
+
47
+ function applyDockerContext(adapter: SandboxAdapter): void {
48
+ if (adapter.dockerContext) {
49
+ process.env.DOCKER_CONTEXT = adapter.dockerContext;
50
+ }
51
+ }
52
+
53
+ export function validateSandboxEngine(
54
+ engine: string | null | undefined,
55
+ { platformFn = platform }: Pick<EngineDependencies, 'platformFn'> = {}
56
+ ): string | null {
57
+ if (engine === null || engine === undefined) {
58
+ return null;
59
+ }
60
+
61
+ const os = platformFn();
62
+ if (!(engine in ADAPTERS)) {
63
+ const known = Object.keys(ADAPTERS).join(', ');
64
+ throw new Error(
65
+ `sandbox: invalid "sandbox.engine" value "${engine}" (unknown sandbox engine). `
66
+ + `Valid engines: ${known}.`
67
+ );
68
+ }
69
+
70
+ const adapter = ADAPTERS[engine as keyof typeof ADAPTERS];
71
+ if (!adapter.supportedPlatforms.includes(os)) {
72
+ const supported = enginesForPlatform(os);
73
+ const supportedList = supported.length > 0 ? supported.join(', ') : 'none';
74
+ throw new Error(
75
+ `sandbox: "sandbox.engine" value "${engine}" is not supported on ${os}. `
76
+ + `Supported engines on ${os}: ${supportedList}.`
77
+ );
78
+ }
79
+
80
+ return engine;
81
+ }
82
+
83
+ export function detectEngine(
84
+ config: EngineConfig = {},
85
+ { platformFn = platform }: Pick<EngineDependencies, 'platformFn'> = {}
86
+ ): string {
87
+ const configured = validateSandboxEngine(config.engine, { platformFn });
88
+ if (configured) {
89
+ return configured;
90
+ }
91
+
92
+ const os = platformFn();
93
+ const fallback = PLATFORM_DEFAULTS[os as keyof typeof PLATFORM_DEFAULTS];
94
+ if (fallback) {
95
+ return fallback;
96
+ }
97
+
98
+ throw new Error(
99
+ `sandbox: platform "${os}" is not supported. `
100
+ + 'Supported platforms: linux (native), darwin (colima/orbstack/docker-desktop), win32 (wsl2). '
101
+ + 'Please open an issue at https://github.com/fitlab-ai/agent-infra/issues/new '
102
+ + 'with your platform details if you need this added.'
103
+ );
104
+ }
105
+
106
+ export function hasUserVmConfig(vm: SandboxVmConfig = {}): boolean {
107
+ return vm.cpu != null || vm.memory != null || vm.disk != null;
108
+ }
109
+
110
+ export function resolveEffectiveVm(
111
+ adapter: SandboxAdapter,
112
+ userVm: SandboxVmConfig = {},
113
+ { detectHostResourcesFn = detectHostResources }: { detectHostResourcesFn?: typeof detectHostResources } = {}
114
+ ): SandboxVmConfig {
115
+ let host: ReturnType<typeof detectHostResources> | null = null;
116
+ const getHost = () => {
117
+ host ??= detectHostResourcesFn();
118
+ return host;
119
+ };
120
+ const defaults = adapter.defaultResources?.(getHost) ?? {};
121
+
122
+ return {
123
+ cpu: userVm.cpu ?? defaults.cpu ?? null,
124
+ memory: userVm.memory ?? defaults.memory ?? null,
125
+ disk: userVm.disk ?? defaults.disk ?? null
126
+ };
127
+ }
128
+
129
+ function effectiveConfigFor(adapter: SandboxAdapter, config: EngineConfig): EffectiveSandboxConfig {
130
+ const userVm = config.vm ?? {};
131
+ return {
132
+ ...config,
133
+ userVm,
134
+ hasUserVmConfig,
135
+ vm: resolveEffectiveVm(adapter, userVm)
136
+ };
137
+ }
138
+
139
+ export async function ensureDocker(
140
+ config: EngineConfig,
141
+ onMessage: OnMessage,
142
+ dependencies: EngineDependencies = {}
143
+ ): Promise<void> {
144
+ const engine = detectEngine(config, dependencies);
145
+ const adapter = getAdapter(engine);
146
+ const effectiveConfig = effectiveConfigFor(adapter, config);
147
+
148
+ applyDockerContext(adapter);
149
+ const vmJustStarted = await adapter.ensure(effectiveConfig, onMessage, runFns(dependencies));
150
+ adapter.syncResources(effectiveConfig, onMessage, runFns(dependencies), { vmJustStarted });
151
+ }
152
+
153
+ export function isVmManaged(config: EngineConfig = {}, dependencies: EngineDependencies = {}): boolean {
154
+ try {
155
+ const engine = detectEngine(config, dependencies);
156
+ return isManagedEngine(engine);
157
+ } catch (error) {
158
+ const message = error instanceof Error ? error.message : '';
159
+ if (
160
+ message.startsWith('sandbox: platform "')
161
+ || / is not supported on [^.]+\. Supported engines on [^:]+: none\./.test(message)
162
+ ) {
163
+ return false;
164
+ }
165
+
166
+ throw error;
167
+ }
168
+ }
169
+
170
+ export function isManagedEngine(engine: string): boolean {
171
+ try {
172
+ return getAdapter(engine).managed;
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+
178
+ export function engineDisplayName(engine: string): string {
179
+ try {
180
+ return getAdapter(engine).displayName;
181
+ } catch {
182
+ return engine;
183
+ }
184
+ }
185
+
186
+ export function startManagedVm(
187
+ config: EngineConfig,
188
+ { platformFn = platform, runOkFn = runOk, runSafeFn = runSafe, runVerboseFn = runVerbose, onMessage }: EngineDependencies & { onMessage?: OnMessage } = {}
189
+ ): 'already-running' | 'started' {
190
+ const engine = detectEngine(config, { platformFn });
191
+ const adapter = getAdapter(engine);
192
+ if (!adapter.managed) {
193
+ throw new Error(`VM management is unavailable for engine '${adapter.displayName}'.`);
194
+ }
195
+
196
+ const effectiveConfig = effectiveConfigFor(adapter, config);
197
+ applyDockerContext(adapter);
198
+ if (!adapter.startVm) {
199
+ throw new Error(`VM management is unavailable for engine '${adapter.displayName}'.`);
200
+ }
201
+ const result = adapter.startVm(
202
+ effectiveConfig,
203
+ onMessage,
204
+ runFns({ runOkFn, runSafeFn, runVerboseFn })
205
+ );
206
+ adapter.syncResources(
207
+ effectiveConfig,
208
+ onMessage,
209
+ runFns({ runOkFn, runSafeFn, runVerboseFn }),
210
+ { vmJustStarted: result === 'started' }
211
+ );
212
+ return result;
213
+ }
214
+
215
+ export function stopManagedVm(
216
+ config: EngineConfig,
217
+ { platformFn = platform, runFn = run }: Pick<EngineDependencies, 'platformFn' | 'runFn'> = {}
218
+ ): 'stopped' {
219
+ const engine = detectEngine(config, { platformFn });
220
+ const adapter = getAdapter(engine);
221
+ if (!adapter.managed) {
222
+ throw new Error(`VM management is unavailable for engine '${adapter.displayName}'.`);
223
+ }
224
+ if (!adapter.stopVm) {
225
+ throw new Error(`VM management is unavailable for engine '${adapter.displayName}'.`);
226
+ }
227
+
228
+ // Stop commands do not read Docker context or VM resource values; keep the
229
+ // previous environment unchanged and pass the original config intentionally.
230
+ return adapter.stopVm(config, null, runFns({ runFn }));
231
+ }
@@ -0,0 +1,81 @@
1
+ import type { EffectiveSandboxConfig, HostResources, OnMessage, RunFns, SandboxAdapter } from './index.ts';
2
+
3
+ function colimaArgs(config: EffectiveSandboxConfig, runSafeFn: RunFns['runSafe']): string[] {
4
+ const arch = runSafeFn('uname', ['-m']);
5
+ const vm = config.vm ?? {};
6
+ const args = ['start', '--cpu', String(vm.cpu), '--memory', String(vm.memory), '--disk', String(vm.disk)];
7
+
8
+ if (arch === 'arm64') {
9
+ args.push('--arch', 'aarch64', '--vm-type=vz', '--mount-type=virtiofs');
10
+ } else {
11
+ args.push('--arch', 'x86_64');
12
+ }
13
+
14
+ return args;
15
+ }
16
+
17
+ export const colimaAdapter: SandboxAdapter = {
18
+ id: 'colima',
19
+ displayName: 'Colima',
20
+ supportedPlatforms: ['darwin'],
21
+ dockerContext: 'colima',
22
+ managed: true,
23
+ canApplyResources: 'on-start',
24
+
25
+ defaultResources(getHost: () => HostResources) {
26
+ const host = getHost();
27
+ return {
28
+ cpu: host.cpu,
29
+ memory: host.memory,
30
+ disk: 60
31
+ };
32
+ },
33
+
34
+ async ensure(config, onMessage: OnMessage, { runOk, runSafe, runVerbose }) {
35
+ let started = false;
36
+
37
+ if (!runOk('which', ['colima'])) {
38
+ onMessage?.('Installing colima + docker via Homebrew...');
39
+ runVerbose('brew', ['install', 'colima', 'docker']);
40
+ }
41
+
42
+ if (!runOk('colima', ['status'])) {
43
+ onMessage?.('Starting Colima VM...');
44
+ runVerbose('colima', colimaArgs(config, runSafe));
45
+ started = true;
46
+ }
47
+
48
+ if (!runOk('docker', ['info'])) {
49
+ throw new Error('Docker daemon is not available after starting Colima');
50
+ }
51
+
52
+ return started;
53
+ },
54
+
55
+ startVm(config, _onMessage: OnMessage, { runOk, runSafe, runVerbose }) {
56
+ if (runOk('colima', ['status'])) {
57
+ return 'already-running';
58
+ }
59
+
60
+ runVerbose('colima', colimaArgs(config, runSafe));
61
+ return 'started';
62
+ },
63
+
64
+ stopVm(_config, _onMessage: OnMessage, { run }) {
65
+ run('colima', ['stop']);
66
+ return 'stopped';
67
+ },
68
+
69
+ syncResources(config, onMessage: OnMessage, _runFns, { vmJustStarted } = {}) {
70
+ if (vmJustStarted || !config.hasUserVmConfig?.(config.userVm)) {
71
+ return;
72
+ }
73
+
74
+ onMessage?.(
75
+ 'Warning: Colima VM is already running; restart with '
76
+ + '`ai sandbox vm stop && ai sandbox vm start` to apply new sandbox.vm.* values.'
77
+ );
78
+ }
79
+ };
80
+
81
+ export default colimaAdapter;
@@ -0,0 +1,36 @@
1
+ import type { OnMessage, SandboxAdapter } from './index.ts';
2
+
3
+ export const dockerDesktopAdapter: SandboxAdapter = {
4
+ id: 'docker-desktop',
5
+ displayName: 'Docker Desktop',
6
+ supportedPlatforms: ['darwin', 'linux', 'win32'],
7
+ dockerContext: 'desktop-linux',
8
+ managed: false,
9
+ canApplyResources: 'never',
10
+
11
+ defaultResources() {
12
+ return null;
13
+ },
14
+
15
+ async ensure(_config, _onMessage: OnMessage, { runOk }) {
16
+ if (!runOk('docker', ['info'])) {
17
+ throw new Error('Docker Desktop is not running. Please start Docker Desktop manually.');
18
+ }
19
+
20
+ return false;
21
+ },
22
+
23
+ syncResources(config, onMessage: OnMessage) {
24
+ if (!config.hasUserVmConfig?.(config.userVm)) {
25
+ return;
26
+ }
27
+
28
+ onMessage?.(
29
+ 'Warning: Docker Desktop manages CPU/memory/disk via Settings -> Resources. '
30
+ + 'sandbox.vm.* values and --cpu/--memory flags are not applied for this engine. '
31
+ + 'Please configure resources in Docker Desktop GUI to match.'
32
+ );
33
+ }
34
+ };
35
+
36
+ export default dockerDesktopAdapter;
@@ -0,0 +1,74 @@
1
+ import { colimaAdapter } from './colima.ts';
2
+ import { dockerDesktopAdapter } from './docker-desktop.ts';
3
+ import { nativeAdapter } from './native.ts';
4
+ import { orbstackAdapter } from './orbstack.ts';
5
+ import { wsl2Adapter } from './wsl2.ts';
6
+
7
+ export type SandboxVmConfig = {
8
+ cpu?: number | null;
9
+ memory?: number | null;
10
+ disk?: number | null;
11
+ };
12
+
13
+ export type EffectiveSandboxConfig = {
14
+ vm?: SandboxVmConfig;
15
+ userVm?: SandboxVmConfig;
16
+ hasUserVmConfig?: (vm?: SandboxVmConfig) => boolean;
17
+ };
18
+
19
+ export type HostResources = {
20
+ cpu: number;
21
+ memory: number;
22
+ };
23
+
24
+ export type RunFns = {
25
+ run: (cmd: string, args: string[]) => string;
26
+ runOk: (cmd: string, args: string[]) => boolean;
27
+ runSafe: (cmd: string, args: string[]) => string;
28
+ runVerbose: (cmd: string, args: string[]) => void;
29
+ };
30
+
31
+ export type OnMessage = ((message: string) => void) | undefined | null;
32
+
33
+ export type SandboxAdapter = {
34
+ id: string;
35
+ displayName: string;
36
+ supportedPlatforms: string[];
37
+ dockerContext: string | null;
38
+ managed: boolean;
39
+ canApplyResources: string;
40
+ defaultResources: (getHost: () => HostResources) => SandboxVmConfig | null;
41
+ ensure: (config: EffectiveSandboxConfig, onMessage: OnMessage, runFns: RunFns) => Promise<boolean>;
42
+ startVm?: (config: EffectiveSandboxConfig, onMessage: OnMessage, runFns: RunFns) => 'already-running' | 'started';
43
+ stopVm?: (config: EffectiveSandboxConfig, onMessage: OnMessage, runFns: Pick<RunFns, 'run'>) => 'stopped';
44
+ syncResources: (
45
+ config: EffectiveSandboxConfig,
46
+ onMessage: OnMessage,
47
+ runFns: RunFns,
48
+ options?: { vmJustStarted?: boolean }
49
+ ) => void;
50
+ };
51
+
52
+ export const ADAPTERS = Object.freeze({
53
+ colima: colimaAdapter,
54
+ orbstack: orbstackAdapter,
55
+ 'docker-desktop': dockerDesktopAdapter,
56
+ native: nativeAdapter,
57
+ wsl2: wsl2Adapter
58
+ });
59
+
60
+ type SandboxEngineId = keyof typeof ADAPTERS;
61
+
62
+ export function getAdapter(engineId: string): SandboxAdapter {
63
+ const adapter = ADAPTERS[engineId as SandboxEngineId];
64
+ if (!adapter) {
65
+ throw new Error(`No adapter registered for engine '${engineId}'`);
66
+ }
67
+ return adapter;
68
+ }
69
+
70
+ export function enginesForPlatform(platformName: string): string[] {
71
+ return Object.values(ADAPTERS)
72
+ .filter((adapter) => adapter.supportedPlatforms.includes(platformName))
73
+ .map((adapter) => adapter.id);
74
+ }