@fitlab-ai/agent-infra 0.5.9 → 0.5.10

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 (41) hide show
  1. package/README.md +200 -8
  2. package/README.zh-CN.md +176 -8
  3. package/bin/cli.js +2 -2
  4. package/lib/init.js +18 -4
  5. package/lib/sandbox/commands/create.js +467 -240
  6. package/lib/sandbox/commands/enter.js +59 -26
  7. package/lib/sandbox/commands/ls.js +37 -6
  8. package/lib/sandbox/commands/rebuild.js +31 -15
  9. package/lib/sandbox/commands/refresh.js +119 -0
  10. package/lib/sandbox/commands/rm.js +59 -11
  11. package/lib/sandbox/commands/vm.js +50 -6
  12. package/lib/sandbox/config.js +9 -5
  13. package/lib/sandbox/constants.js +15 -3
  14. package/lib/sandbox/credentials.js +520 -0
  15. package/lib/sandbox/dotfiles.js +189 -0
  16. package/lib/sandbox/engine.js +135 -192
  17. package/lib/sandbox/engines/colima.js +79 -0
  18. package/lib/sandbox/engines/docker-desktop.js +34 -0
  19. package/lib/sandbox/engines/index.js +27 -0
  20. package/lib/sandbox/engines/native.js +112 -0
  21. package/lib/sandbox/engines/orbstack.js +76 -0
  22. package/lib/sandbox/engines/selinux.js +60 -0
  23. package/lib/sandbox/engines/wsl2-paths.js +59 -0
  24. package/lib/sandbox/engines/wsl2.js +72 -0
  25. package/lib/sandbox/index.js +10 -1
  26. package/lib/sandbox/runtimes/ai-tools.dockerfile +14 -1
  27. package/lib/sandbox/runtimes/base.dockerfile +116 -1
  28. package/lib/sandbox/shell.js +53 -2
  29. package/lib/sandbox/tools.js +5 -5
  30. package/package.json +6 -4
  31. package/templates/.agents/rules/create-issue.github.en.md +2 -4
  32. package/templates/.agents/rules/create-issue.github.zh-CN.md +2 -4
  33. package/templates/.agents/rules/issue-pr-commands.github.en.md +29 -0
  34. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +29 -0
  35. package/templates/.agents/scripts/{platform-adapters/find-existing-task.github.js → find-existing-task.js} +22 -79
  36. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +26 -41
  37. package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
  38. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  39. package/templates/.agents/skills/import-issue/SKILL.en.md +6 -8
  40. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +6 -8
  41. package/templates/.agents/scripts/platform-adapters/find-existing-task.js +0 -5
@@ -0,0 +1,189 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { hostJoin } from './engines/wsl2-paths.js';
4
+
5
+ export function dotfilesCacheDir(home, project) {
6
+ return hostJoin(home, '.agent-infra', '.cache', 'dotfiles-resolved', project);
7
+ }
8
+
9
+ function dotfilesWarning(warnings, writeStderr, relPath, reason, detail = '') {
10
+ const warning = { rel: relPath, reason };
11
+ if (detail) {
12
+ warning.detail = detail;
13
+ }
14
+ warnings.push(warning);
15
+
16
+ const suffix = detail ? `: ${detail}` : '';
17
+ writeStderr(`sandbox-dotfiles (host): skipping ${relPath} (${reason}${suffix})\n`);
18
+ }
19
+
20
+ function copyDotfile(srcPath, dstPath, context) {
21
+ const { fsModule, relPath, warnings, writeStderr } = context;
22
+ try {
23
+ fsModule.mkdirSync(path.dirname(dstPath), { recursive: true });
24
+ fsModule.copyFileSync(srcPath, dstPath);
25
+ } catch (error) {
26
+ dotfilesWarning(warnings, writeStderr, relPath, 'copy failed', error?.code ?? error?.message ?? 'unknown error');
27
+ }
28
+ }
29
+
30
+ function walkAndMaterializeDotfiles(context) {
31
+ const {
32
+ srcDir,
33
+ dstDir,
34
+ relParts,
35
+ depth,
36
+ maxDepth,
37
+ activeDirs,
38
+ warnings,
39
+ writeStderr,
40
+ fsModule
41
+ } = context;
42
+ const relPath = relParts.length > 0 ? relParts.join('/') : '.';
43
+
44
+ if (depth > maxDepth) {
45
+ dotfilesWarning(warnings, writeStderr, relPath, 'depth exceeds limit', String(maxDepth));
46
+ return;
47
+ }
48
+
49
+ let entries;
50
+ try {
51
+ entries = fsModule.readdirSync(srcDir, { withFileTypes: true });
52
+ } catch (error) {
53
+ dotfilesWarning(warnings, writeStderr, relPath, 'read failed', error?.code ?? error?.message ?? 'unknown error');
54
+ return;
55
+ }
56
+
57
+ for (const entry of entries) {
58
+ const childSrc = path.join(srcDir, entry.name);
59
+ const childDst = path.join(dstDir, entry.name);
60
+ const childRelParts = [...relParts, entry.name];
61
+ const childRelPath = childRelParts.join('/');
62
+
63
+ if (entry.isSymbolicLink()) {
64
+ let resolvedTarget;
65
+ try {
66
+ resolvedTarget = fsModule.realpathSync(childSrc);
67
+ } catch (error) {
68
+ const reason = error?.code === 'ELOOP' ? 'symlink loop' : 'dangling symlink';
69
+ dotfilesWarning(warnings, writeStderr, childRelPath, reason, error?.code ?? 'unresolved');
70
+ continue;
71
+ }
72
+
73
+ let targetStat;
74
+ try {
75
+ targetStat = fsModule.statSync(resolvedTarget);
76
+ } catch (error) {
77
+ dotfilesWarning(warnings, writeStderr, childRelPath, 'target stat failed', error?.code ?? error?.message ?? 'unknown error');
78
+ continue;
79
+ }
80
+
81
+ if (targetStat.isDirectory()) {
82
+ if (activeDirs.has(resolvedTarget)) {
83
+ dotfilesWarning(warnings, writeStderr, childRelPath, 'symlink loop');
84
+ continue;
85
+ }
86
+
87
+ activeDirs.add(resolvedTarget);
88
+ walkAndMaterializeDotfiles({
89
+ srcDir: resolvedTarget,
90
+ dstDir: childDst,
91
+ relParts: childRelParts,
92
+ depth: depth + 1,
93
+ maxDepth,
94
+ activeDirs,
95
+ warnings,
96
+ writeStderr,
97
+ fsModule
98
+ });
99
+ activeDirs.delete(resolvedTarget);
100
+ continue;
101
+ }
102
+
103
+ if (targetStat.isFile()) {
104
+ copyDotfile(resolvedTarget, childDst, {
105
+ fsModule,
106
+ relPath: childRelPath,
107
+ warnings,
108
+ writeStderr
109
+ });
110
+ }
111
+ continue;
112
+ }
113
+
114
+ if (entry.isDirectory()) {
115
+ let childRealPath = null;
116
+ try {
117
+ childRealPath = fsModule.realpathSync(childSrc);
118
+ } catch {
119
+ // A real directory may disappear during traversal; readdir will warn below.
120
+ }
121
+ if (childRealPath) {
122
+ activeDirs.add(childRealPath);
123
+ }
124
+ walkAndMaterializeDotfiles({
125
+ srcDir: childSrc,
126
+ dstDir: childDst,
127
+ relParts: childRelParts,
128
+ depth: depth + 1,
129
+ maxDepth,
130
+ activeDirs,
131
+ warnings,
132
+ writeStderr,
133
+ fsModule
134
+ });
135
+ if (childRealPath) {
136
+ activeDirs.delete(childRealPath);
137
+ }
138
+ continue;
139
+ }
140
+
141
+ if (entry.isFile()) {
142
+ copyDotfile(childSrc, childDst, {
143
+ fsModule,
144
+ relPath: childRelPath,
145
+ warnings,
146
+ writeStderr
147
+ });
148
+ }
149
+ }
150
+ }
151
+
152
+ export function materializeDotfiles(srcDir, cacheDir, options = {}) {
153
+ const {
154
+ writeStderr = (message) => process.stderr.write(message),
155
+ maxDepth = 32,
156
+ fsModule = fs
157
+ } = options;
158
+
159
+ if (!srcDir || !fsModule.existsSync(srcDir)) {
160
+ return null;
161
+ }
162
+
163
+ fsModule.mkdirSync(cacheDir, { recursive: true });
164
+ for (const entry of fsModule.readdirSync(cacheDir)) {
165
+ fsModule.rmSync(path.join(cacheDir, entry), { recursive: true, force: true });
166
+ }
167
+
168
+ const warnings = [];
169
+ const activeDirs = new Set();
170
+ try {
171
+ activeDirs.add(fsModule.realpathSync(srcDir));
172
+ } catch {
173
+ activeDirs.add(srcDir);
174
+ }
175
+
176
+ walkAndMaterializeDotfiles({
177
+ srcDir,
178
+ dstDir: cacheDir,
179
+ relParts: [],
180
+ depth: 0,
181
+ maxDepth,
182
+ activeDirs,
183
+ warnings,
184
+ writeStderr,
185
+ fsModule
186
+ });
187
+
188
+ return { cacheDir, warnings };
189
+ }
@@ -1,256 +1,199 @@
1
1
  import { platform } from 'node:os';
2
2
  import { detectHostResources } from './constants.js';
3
+ import { ADAPTERS, enginesForPlatform, getAdapter } from './engines/index.js';
3
4
  import { run, runOk, runSafe, runVerbose } from './shell.js';
4
5
 
5
6
  export const ENGINES = Object.freeze({
6
7
  COLIMA: 'colima',
7
8
  ORBSTACK: 'orbstack',
8
- DOCKER_DESKTOP: 'docker-desktop'
9
+ DOCKER_DESKTOP: 'docker-desktop',
10
+ NATIVE: 'native',
11
+ WSL2: 'wsl2'
9
12
  });
10
13
 
11
- export const ENGINE_DOCKER_CONTEXT = Object.freeze({
12
- [ENGINES.COLIMA]: 'colima',
13
- [ENGINES.ORBSTACK]: 'orbstack',
14
- [ENGINES.DOCKER_DESKTOP]: 'desktop-linux'
14
+ const PLATFORM_DEFAULTS = Object.freeze({
15
+ linux: ENGINES.NATIVE,
16
+ darwin: ENGINES.COLIMA,
17
+ win32: ENGINES.WSL2
15
18
  });
16
19
 
17
- const VALID_CONFIG_ENGINES = new Set(Object.values(ENGINES));
20
+ function runFns({
21
+ runFn = run,
22
+ runOkFn = runOk,
23
+ runSafeFn = runSafe,
24
+ runVerboseFn = runVerbose
25
+ } = {}) {
26
+ return {
27
+ run: runFn,
28
+ runOk: runOkFn,
29
+ runSafe: runSafeFn,
30
+ runVerbose: runVerboseFn
31
+ };
32
+ }
18
33
 
19
- function applyDockerContext(engine) {
20
- const context = ENGINE_DOCKER_CONTEXT[engine];
21
- if (context) {
22
- process.env.DOCKER_CONTEXT = context;
34
+ function applyDockerContext(adapter) {
35
+ if (adapter.dockerContext) {
36
+ process.env.DOCKER_CONTEXT = adapter.dockerContext;
23
37
  }
24
38
  }
25
39
 
26
- export function validateSandboxEngine(engine) {
40
+ export function validateSandboxEngine(engine, { platformFn = platform } = {}) {
27
41
  if (engine === null || engine === undefined) {
28
42
  return null;
29
43
  }
30
44
 
31
- if (VALID_CONFIG_ENGINES.has(engine)) {
32
- return engine;
33
- }
34
-
35
- throw new Error(
36
- `sandbox: invalid "sandbox.engine" value "${engine}". `
37
- + 'Expected one of: null, colima, orbstack, docker-desktop. '
38
- + 'This setting only affects macOS sandbox engine selection.'
39
- );
40
- }
41
-
42
- export function detectEngine(config = {}, { platformFn = platform } = {}) {
43
- const configured = validateSandboxEngine(config.engine);
44
45
  const os = platformFn();
45
- if (os === 'linux') {
46
- return 'native';
47
- }
48
- if (os === 'win32') {
49
- return 'wsl2';
50
- }
51
- if (os === 'darwin') {
52
- if (configured) {
53
- return configured;
54
- }
55
-
56
- return ENGINES.COLIMA;
46
+ if (!(engine in ADAPTERS)) {
47
+ const known = Object.keys(ADAPTERS).join(', ');
48
+ throw new Error(
49
+ `sandbox: invalid "sandbox.engine" value "${engine}" (unknown sandbox engine). `
50
+ + `Valid engines: ${known}.`
51
+ );
57
52
  }
58
- return 'unsupported';
59
- }
60
53
 
61
- function colimaArgs(config, runSafeFn = runSafe) {
62
- const arch = runSafeFn('uname', ['-m']);
63
- const defaults = detectHostResources();
64
- const vm = config.vm ?? {};
65
- const cpu = vm.cpu ?? defaults.cpu;
66
- const memory = vm.memory ?? defaults.memory;
67
- const disk = vm.disk ?? 60;
68
- const args = ['start', '--cpu', String(cpu), '--memory', String(memory), '--disk', String(disk)];
69
-
70
- if (arch === 'arm64') {
71
- args.push('--arch', 'aarch64', '--vm-type=vz', '--mount-type=virtiofs');
72
- } else {
73
- args.push('--arch', 'x86_64');
54
+ const adapter = ADAPTERS[engine];
55
+ if (!adapter.supportedPlatforms.includes(os)) {
56
+ const supported = enginesForPlatform(os);
57
+ const supportedList = supported.length > 0 ? supported.join(', ') : 'none';
58
+ throw new Error(
59
+ `sandbox: "sandbox.engine" value "${engine}" is not supported on ${os}. `
60
+ + `Supported engines on ${os}: ${supportedList}.`
61
+ );
74
62
  }
75
63
 
76
- return args;
64
+ return engine;
77
65
  }
78
66
 
79
- export async function ensureColima(
80
- config,
81
- onMessage,
82
- { runOkFn = runOk, runSafeFn = runSafe, runVerboseFn = runVerbose } = {}
83
- ) {
84
- applyDockerContext(ENGINES.COLIMA);
85
-
86
- if (!runOkFn('which', ['colima'])) {
87
- onMessage?.('Installing colima + docker via Homebrew...');
88
- runVerboseFn('brew', ['install', 'colima', 'docker']);
67
+ export function detectEngine(config = {}, { platformFn = platform } = {}) {
68
+ const configured = validateSandboxEngine(config.engine, { platformFn });
69
+ if (configured) {
70
+ return configured;
89
71
  }
90
72
 
91
- if (!runOkFn('colima', ['status'])) {
92
- onMessage?.('Starting Colima VM...');
93
- runVerboseFn('colima', colimaArgs(config, runSafeFn));
73
+ const os = platformFn();
74
+ const fallback = PLATFORM_DEFAULTS[os];
75
+ if (fallback) {
76
+ return fallback;
94
77
  }
95
78
 
96
- if (!runOkFn('docker', ['info'])) {
97
- throw new Error('Docker daemon is not available after starting Colima');
98
- }
79
+ throw new Error(
80
+ `sandbox: platform "${os}" is not supported. `
81
+ + 'Supported platforms: linux (native), darwin (colima/orbstack/docker-desktop), win32 (wsl2). '
82
+ + 'Please open an issue at https://github.com/fitlab-ai/agent-infra/issues/new '
83
+ + 'with your platform details if you need this added.'
84
+ );
99
85
  }
100
86
 
101
- export async function ensureOrbStack(
102
- _config,
103
- onMessage,
104
- { runOkFn = runOk, runVerboseFn = runVerbose } = {}
105
- ) {
106
- applyDockerContext(ENGINES.ORBSTACK);
107
-
108
- if (!runOkFn('which', ['orb'])) {
109
- onMessage?.('Installing OrbStack via Homebrew...');
110
- runVerboseFn('brew', ['install', '--cask', 'orbstack']);
111
- }
112
-
113
- if (!runOkFn('docker', ['info'])) {
114
- onMessage?.('Starting OrbStack...');
115
- runVerboseFn('orb', ['start']);
116
- }
117
-
118
- if (!runOkFn('docker', ['info'])) {
119
- throw new Error('Docker daemon is not available after starting OrbStack');
120
- }
87
+ export function hasUserVmConfig(vm = {}) {
88
+ return vm.cpu != null || vm.memory != null || vm.disk != null;
121
89
  }
122
90
 
123
- export async function ensureDockerDesktop(
124
- _config,
125
- onMessage,
126
- { runOkFn = runOk } = {}
91
+ export function resolveEffectiveVm(
92
+ adapter,
93
+ userVm = {},
94
+ { detectHostResourcesFn = detectHostResources } = {}
127
95
  ) {
128
- applyDockerContext(ENGINES.DOCKER_DESKTOP);
96
+ let host = null;
97
+ const getHost = () => {
98
+ host ??= detectHostResourcesFn();
99
+ return host;
100
+ };
101
+ const defaults = adapter.defaultResources?.(getHost) ?? {};
129
102
 
130
- if (!runOkFn('docker', ['info'])) {
131
- throw new Error('Docker Desktop is not running. Please start Docker Desktop manually.');
132
- }
103
+ return {
104
+ cpu: userVm.cpu ?? defaults.cpu ?? null,
105
+ memory: userVm.memory ?? defaults.memory ?? null,
106
+ disk: userVm.disk ?? defaults.disk ?? null
107
+ };
133
108
  }
134
109
 
135
- export async function ensureNativeDocker(
136
- _config,
137
- _onMessage,
138
- { runOkFn = runOk, runSafeFn = runSafe } = {}
139
- ) {
140
- if (!runOkFn('which', ['docker'])) {
141
- throw new Error([
142
- 'Docker is not installed.',
143
- 'Install Docker Engine for your distribution: https://docs.docker.com/engine/install/',
144
- 'Then start the daemon with: sudo systemctl enable --now docker',
145
- 'If you want to run Docker without sudo, add your user to the docker group: sudo usermod -aG docker $USER'
146
- ].join('\n'));
147
- }
148
-
149
- if (runOkFn('docker', ['info'])) {
150
- return;
151
- }
152
-
153
- const serverVersion = runSafeFn('docker', ['version', '--format', '{{.Server.Version}}']);
154
- if (!serverVersion) {
155
- throw new Error([
156
- 'Docker daemon is not running or is unreachable.',
157
- 'Start it with: sudo systemctl start docker',
158
- 'Enable it on boot with: sudo systemctl enable docker',
159
- 'If you use rootless or remote Docker, verify DOCKER_HOST points at a reachable socket.',
160
- 'Then retry: ai sandbox create <branch>'
161
- ].join('\n'));
162
- }
163
-
164
- throw new Error([
165
- 'Docker is installed, but the current user may lack permission to use the daemon.',
166
- 'Add your user to the docker group: sudo usermod -aG docker $USER',
167
- 'Open a new login shell or run: newgrp docker',
168
- 'For rootless Docker, make sure DOCKER_HOST points at the rootless daemon socket.'
169
- ].join('\n'));
110
+ function effectiveConfigFor(adapter, config) {
111
+ const userVm = config.vm ?? {};
112
+ return {
113
+ ...config,
114
+ userVm,
115
+ hasUserVmConfig,
116
+ vm: resolveEffectiveVm(adapter, userVm)
117
+ };
170
118
  }
171
119
 
172
- export async function ensureDocker(config, onMessage) {
173
- const engine = detectEngine(config);
174
-
175
- if (engine === ENGINES.COLIMA) {
176
- await ensureColima(config, onMessage);
177
- return;
178
- }
179
-
180
- if (engine === ENGINES.ORBSTACK) {
181
- await ensureOrbStack(config, onMessage);
182
- return;
183
- }
184
-
185
- if (engine === ENGINES.DOCKER_DESKTOP) {
186
- await ensureDockerDesktop(config, onMessage);
187
- return;
188
- }
189
-
190
- if (engine === 'native') {
191
- await ensureNativeDocker(config, onMessage);
192
- return;
193
- }
194
-
195
- if (engine === 'wsl2') {
196
- throw new Error('Windows sandbox support is reserved for a future WSL2 implementation.');
197
- }
120
+ export async function ensureDocker(config, onMessage, dependencies = {}) {
121
+ const engine = detectEngine(config, dependencies);
122
+ const adapter = getAdapter(engine);
123
+ const effectiveConfig = effectiveConfigFor(adapter, config);
198
124
 
199
- throw new Error(`Unsupported sandbox engine: ${engine}`);
125
+ applyDockerContext(adapter);
126
+ const vmJustStarted = await adapter.ensure(effectiveConfig, onMessage, runFns(dependencies));
127
+ adapter.syncResources(effectiveConfig, onMessage, runFns(dependencies), { vmJustStarted });
200
128
  }
201
129
 
202
130
  export function isVmManaged(config = {}, dependencies = {}) {
203
- const engine = detectEngine(config, dependencies);
204
- return isManagedEngine(engine);
131
+ try {
132
+ const engine = detectEngine(config, dependencies);
133
+ return isManagedEngine(engine);
134
+ } catch (error) {
135
+ const message = error?.message ?? '';
136
+ if (
137
+ message.startsWith('sandbox: platform "')
138
+ || / is not supported on [^.]+\. Supported engines on [^:]+: none\./.test(message)
139
+ ) {
140
+ return false;
141
+ }
142
+
143
+ throw error;
144
+ }
205
145
  }
206
146
 
207
147
  export function isManagedEngine(engine) {
208
- return engine === ENGINES.COLIMA || engine === ENGINES.ORBSTACK;
148
+ try {
149
+ return getAdapter(engine).managed;
150
+ } catch {
151
+ return false;
152
+ }
209
153
  }
210
154
 
211
155
  export function engineDisplayName(engine) {
212
- const names = {
213
- [ENGINES.COLIMA]: 'Colima',
214
- [ENGINES.ORBSTACK]: 'OrbStack',
215
- [ENGINES.DOCKER_DESKTOP]: 'Docker Desktop',
216
- native: 'native Docker',
217
- wsl2: 'WSL2'
218
- };
219
- return names[engine] ?? engine;
156
+ try {
157
+ return getAdapter(engine).displayName;
158
+ } catch {
159
+ return engine;
160
+ }
220
161
  }
221
162
 
222
163
  export function startManagedVm(
223
164
  config,
224
- { platformFn = platform, runOkFn = runOk, runVerboseFn = runVerbose } = {}
165
+ { platformFn = platform, runOkFn = runOk, runSafeFn = runSafe, runVerboseFn = runVerbose, onMessage } = {}
225
166
  ) {
226
167
  const engine = detectEngine(config, { platformFn });
227
- if (!isManagedEngine(engine)) {
228
- throw new Error(`VM management is unavailable for engine '${engineDisplayName(engine)}'.`);
229
- }
230
-
231
- if (engine === ENGINES.COLIMA && runOkFn('colima', ['status'])) {
232
- return 'already-running';
233
- }
234
- if (engine === ENGINES.ORBSTACK && runOkFn('orb', ['status'])) {
235
- return 'already-running';
168
+ const adapter = getAdapter(engine);
169
+ if (!adapter.managed) {
170
+ throw new Error(`VM management is unavailable for engine '${adapter.displayName}'.`);
236
171
  }
237
172
 
238
- if (engine === ENGINES.COLIMA) {
239
- runVerboseFn('colima', colimaArgs(config));
240
- } else if (engine === ENGINES.ORBSTACK) {
241
- runVerboseFn('orb', ['start']);
242
- }
243
- return 'started';
173
+ const effectiveConfig = effectiveConfigFor(adapter, config);
174
+ applyDockerContext(adapter);
175
+ const result = adapter.startVm(
176
+ effectiveConfig,
177
+ onMessage,
178
+ runFns({ runOkFn, runSafeFn, runVerboseFn })
179
+ );
180
+ adapter.syncResources(
181
+ effectiveConfig,
182
+ onMessage,
183
+ runFns({ runOkFn, runSafeFn, runVerboseFn }),
184
+ { vmJustStarted: result === 'started' }
185
+ );
186
+ return result;
244
187
  }
245
188
 
246
189
  export function stopManagedVm(config, { platformFn = platform, runFn = run } = {}) {
247
190
  const engine = detectEngine(config, { platformFn });
248
- if (engine === ENGINES.COLIMA) {
249
- runFn('colima', ['stop']);
250
- return 'stopped';
251
- } else if (engine === ENGINES.ORBSTACK) {
252
- runFn('orb', ['stop']);
253
- return 'stopped';
191
+ const adapter = getAdapter(engine);
192
+ if (!adapter.managed) {
193
+ throw new Error(`VM management is unavailable for engine '${adapter.displayName}'.`);
254
194
  }
255
- throw new Error(`VM management is unavailable for engine '${engineDisplayName(engine)}'.`);
195
+
196
+ // Stop commands do not read Docker context or VM resource values; keep the
197
+ // previous environment unchanged and pass the original config intentionally.
198
+ return adapter.stopVm(config, null, runFns({ runFn }));
256
199
  }
@@ -0,0 +1,79 @@
1
+ function colimaArgs(config, runSafeFn) {
2
+ const arch = runSafeFn('uname', ['-m']);
3
+ const vm = config.vm ?? {};
4
+ const args = ['start', '--cpu', String(vm.cpu), '--memory', String(vm.memory), '--disk', String(vm.disk)];
5
+
6
+ if (arch === 'arm64') {
7
+ args.push('--arch', 'aarch64', '--vm-type=vz', '--mount-type=virtiofs');
8
+ } else {
9
+ args.push('--arch', 'x86_64');
10
+ }
11
+
12
+ return args;
13
+ }
14
+
15
+ export const colimaAdapter = {
16
+ id: 'colima',
17
+ displayName: 'Colima',
18
+ supportedPlatforms: ['darwin'],
19
+ dockerContext: 'colima',
20
+ managed: true,
21
+ canApplyResources: 'on-start',
22
+
23
+ defaultResources(getHost) {
24
+ const host = getHost();
25
+ return {
26
+ cpu: host.cpu,
27
+ memory: host.memory,
28
+ disk: 60
29
+ };
30
+ },
31
+
32
+ async ensure(config, onMessage, { runOk, runSafe, runVerbose }) {
33
+ let started = false;
34
+
35
+ if (!runOk('which', ['colima'])) {
36
+ onMessage?.('Installing colima + docker via Homebrew...');
37
+ runVerbose('brew', ['install', 'colima', 'docker']);
38
+ }
39
+
40
+ if (!runOk('colima', ['status'])) {
41
+ onMessage?.('Starting Colima VM...');
42
+ runVerbose('colima', colimaArgs(config, runSafe));
43
+ started = true;
44
+ }
45
+
46
+ if (!runOk('docker', ['info'])) {
47
+ throw new Error('Docker daemon is not available after starting Colima');
48
+ }
49
+
50
+ return started;
51
+ },
52
+
53
+ startVm(config, _onMessage, { runOk, runSafe, runVerbose }) {
54
+ if (runOk('colima', ['status'])) {
55
+ return 'already-running';
56
+ }
57
+
58
+ runVerbose('colima', colimaArgs(config, runSafe));
59
+ return 'started';
60
+ },
61
+
62
+ stopVm(_config, _onMessage, { run }) {
63
+ run('colima', ['stop']);
64
+ return 'stopped';
65
+ },
66
+
67
+ syncResources(config, onMessage, _runFns, { vmJustStarted } = {}) {
68
+ if (vmJustStarted || !config.hasUserVmConfig?.(config.userVm)) {
69
+ return;
70
+ }
71
+
72
+ onMessage?.(
73
+ 'Warning: Colima VM is already running; restart with '
74
+ + '`ai sandbox vm stop && ai sandbox vm start` to apply new sandbox.vm.* values.'
75
+ );
76
+ }
77
+ };
78
+
79
+ export default colimaAdapter;
@@ -0,0 +1,34 @@
1
+ export const dockerDesktopAdapter = {
2
+ id: 'docker-desktop',
3
+ displayName: 'Docker Desktop',
4
+ supportedPlatforms: ['darwin', 'linux', 'win32'],
5
+ dockerContext: 'desktop-linux',
6
+ managed: false,
7
+ canApplyResources: 'never',
8
+
9
+ defaultResources() {
10
+ return null;
11
+ },
12
+
13
+ async ensure(_config, _onMessage, { runOk }) {
14
+ if (!runOk('docker', ['info'])) {
15
+ throw new Error('Docker Desktop is not running. Please start Docker Desktop manually.');
16
+ }
17
+
18
+ return false;
19
+ },
20
+
21
+ syncResources(config, onMessage) {
22
+ if (!config.hasUserVmConfig?.(config.userVm)) {
23
+ return;
24
+ }
25
+
26
+ onMessage?.(
27
+ 'Warning: Docker Desktop manages CPU/memory/disk via Settings -> Resources. '
28
+ + 'sandbox.vm.* values and --cpu/--memory flags are not applied for this engine. '
29
+ + 'Please configure resources in Docker Desktop GUI to match.'
30
+ );
31
+ }
32
+ };
33
+
34
+ export default dockerDesktopAdapter;