@fitlab-ai/agent-infra 0.5.8 → 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 (67) hide show
  1. package/README.md +237 -5
  2. package/README.zh-CN.md +213 -5
  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 +56 -6
  12. package/lib/sandbox/config.js +9 -5
  13. package/lib/sandbox/constants.js +18 -3
  14. package/lib/sandbox/credentials.js +520 -0
  15. package/lib/sandbox/dotfiles.js +189 -0
  16. package/lib/sandbox/engine.js +135 -157
  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 +125 -3
  28. package/lib/sandbox/shell.js +53 -2
  29. package/lib/sandbox/tools.js +5 -5
  30. package/package.json +8 -4
  31. package/templates/.agents/rules/create-issue.en.md +5 -0
  32. package/templates/.agents/rules/create-issue.github.en.md +176 -0
  33. package/templates/.agents/rules/create-issue.github.zh-CN.md +176 -0
  34. package/templates/.agents/rules/create-issue.zh-CN.md +5 -0
  35. package/templates/.agents/rules/issue-pr-commands.github.en.md +29 -0
  36. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +29 -0
  37. package/templates/.agents/rules/issue-sync.github.en.md +1 -1
  38. package/templates/.agents/rules/issue-sync.github.zh-CN.md +1 -1
  39. package/templates/.agents/rules/milestone-inference.github.en.md +2 -2
  40. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +2 -2
  41. package/templates/.agents/scripts/{platform-adapters/find-existing-task.github.js → find-existing-task.js} +22 -79
  42. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +72 -42
  43. package/templates/.agents/skills/create-task/SKILL.en.md +69 -11
  44. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +70 -12
  45. package/templates/.agents/skills/create-task/config/verify.json +6 -1
  46. package/templates/.agents/skills/implement-task/reference/implementation-rules.en.md +7 -12
  47. package/templates/.agents/skills/implement-task/reference/implementation-rules.zh-CN.md +7 -12
  48. package/templates/.agents/skills/import-issue/SKILL.en.md +7 -9
  49. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +7 -9
  50. package/templates/.agents/skills/refine-task/reference/fix-workflow.en.md +2 -2
  51. package/templates/.agents/skills/refine-task/reference/fix-workflow.zh-CN.md +2 -2
  52. package/templates/.agents/skills/test/SKILL.en.md +45 -6
  53. package/templates/.agents/skills/test/SKILL.zh-CN.md +45 -6
  54. package/templates/.agents/scripts/platform-adapters/find-existing-task.js +0 -5
  55. package/templates/.agents/skills/create-issue/SKILL.en.md +0 -118
  56. package/templates/.agents/skills/create-issue/SKILL.zh-CN.md +0 -118
  57. package/templates/.agents/skills/create-issue/config/verify.json +0 -30
  58. package/templates/.agents/skills/create-issue/reference/label-and-type.en.md +0 -71
  59. package/templates/.agents/skills/create-issue/reference/label-and-type.zh-CN.md +0 -71
  60. package/templates/.agents/skills/create-issue/reference/template-matching.en.md +0 -17
  61. package/templates/.agents/skills/create-issue/reference/template-matching.zh-CN.md +0 -17
  62. package/templates/.claude/commands/create-issue.en.md +0 -8
  63. package/templates/.claude/commands/create-issue.zh-CN.md +0 -8
  64. package/templates/.gemini/commands/_project_/create-issue.en.toml +0 -8
  65. package/templates/.gemini/commands/_project_/create-issue.zh-CN.toml +0 -8
  66. package/templates/.opencode/commands/create-issue.en.md +0 -11
  67. package/templates/.opencode/commands/create-issue.zh-CN.md +0 -11
@@ -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,221 +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';
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
+ );
47
52
  }
48
- if (os === 'win32') {
49
- return 'wsl2';
50
- }
51
- if (os === 'darwin') {
52
- if (configured) {
53
- return configured;
54
- }
55
-
56
- return ENGINES.COLIMA;
57
- }
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 ensureDocker(config, onMessage) {
136
- const engine = detectEngine(config);
137
-
138
- if (engine === ENGINES.COLIMA) {
139
- await ensureColima(config, onMessage);
140
- return;
141
- }
110
+ function effectiveConfigFor(adapter, config) {
111
+ const userVm = config.vm ?? {};
112
+ return {
113
+ ...config,
114
+ userVm,
115
+ hasUserVmConfig,
116
+ vm: resolveEffectiveVm(adapter, userVm)
117
+ };
118
+ }
142
119
 
143
- if (engine === ENGINES.ORBSTACK) {
144
- await ensureOrbStack(config, onMessage);
145
- return;
146
- }
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);
147
124
 
148
- if (engine === ENGINES.DOCKER_DESKTOP) {
149
- await ensureDockerDesktop(config, onMessage);
150
- return;
151
- }
125
+ applyDockerContext(adapter);
126
+ const vmJustStarted = await adapter.ensure(effectiveConfig, onMessage, runFns(dependencies));
127
+ adapter.syncResources(effectiveConfig, onMessage, runFns(dependencies), { vmJustStarted });
128
+ }
152
129
 
153
- if (engine === 'native') {
154
- if (!runOk('docker', ['info'])) {
155
- throw new Error('Docker daemon is not running. Please start Docker first.');
130
+ export function isVmManaged(config = {}, dependencies = {}) {
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;
156
141
  }
157
- return;
158
- }
159
142
 
160
- if (engine === 'wsl2') {
161
- throw new Error('Windows sandbox support is reserved for a future WSL2 implementation.');
143
+ throw error;
162
144
  }
163
-
164
- throw new Error(`Unsupported sandbox engine: ${engine}`);
165
- }
166
-
167
- export function isVmManaged(config = {}, dependencies = {}) {
168
- const engine = detectEngine(config, dependencies);
169
- return isManagedEngine(engine);
170
145
  }
171
146
 
172
147
  export function isManagedEngine(engine) {
173
- return engine === ENGINES.COLIMA || engine === ENGINES.ORBSTACK;
148
+ try {
149
+ return getAdapter(engine).managed;
150
+ } catch {
151
+ return false;
152
+ }
174
153
  }
175
154
 
176
155
  export function engineDisplayName(engine) {
177
- const names = {
178
- [ENGINES.COLIMA]: 'Colima',
179
- [ENGINES.ORBSTACK]: 'OrbStack',
180
- [ENGINES.DOCKER_DESKTOP]: 'Docker Desktop',
181
- native: 'native Docker',
182
- wsl2: 'WSL2'
183
- };
184
- return names[engine] ?? engine;
156
+ try {
157
+ return getAdapter(engine).displayName;
158
+ } catch {
159
+ return engine;
160
+ }
185
161
  }
186
162
 
187
163
  export function startManagedVm(
188
164
  config,
189
- { platformFn = platform, runOkFn = runOk, runVerboseFn = runVerbose } = {}
165
+ { platformFn = platform, runOkFn = runOk, runSafeFn = runSafe, runVerboseFn = runVerbose, onMessage } = {}
190
166
  ) {
191
167
  const engine = detectEngine(config, { platformFn });
192
- if (!isManagedEngine(engine)) {
193
- throw new Error(`VM management is unavailable for engine '${engineDisplayName(engine)}'.`);
168
+ const adapter = getAdapter(engine);
169
+ if (!adapter.managed) {
170
+ throw new Error(`VM management is unavailable for engine '${adapter.displayName}'.`);
194
171
  }
195
172
 
196
- if (engine === ENGINES.COLIMA && runOkFn('colima', ['status'])) {
197
- return 'already-running';
198
- }
199
- if (engine === ENGINES.ORBSTACK && runOkFn('orb', ['status'])) {
200
- return 'already-running';
201
- }
202
-
203
- if (engine === ENGINES.COLIMA) {
204
- runVerboseFn('colima', colimaArgs(config));
205
- } else if (engine === ENGINES.ORBSTACK) {
206
- runVerboseFn('orb', ['start']);
207
- }
208
- 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;
209
187
  }
210
188
 
211
189
  export function stopManagedVm(config, { platformFn = platform, runFn = run } = {}) {
212
190
  const engine = detectEngine(config, { platformFn });
213
- if (engine === ENGINES.COLIMA) {
214
- runFn('colima', ['stop']);
215
- return 'stopped';
216
- } else if (engine === ENGINES.ORBSTACK) {
217
- runFn('orb', ['stop']);
218
- return 'stopped';
191
+ const adapter = getAdapter(engine);
192
+ if (!adapter.managed) {
193
+ throw new Error(`VM management is unavailable for engine '${adapter.displayName}'.`);
219
194
  }
220
- 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 }));
221
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;
@@ -0,0 +1,27 @@
1
+ import { colimaAdapter } from './colima.js';
2
+ import { dockerDesktopAdapter } from './docker-desktop.js';
3
+ import { nativeAdapter } from './native.js';
4
+ import { orbstackAdapter } from './orbstack.js';
5
+ import { wsl2Adapter } from './wsl2.js';
6
+
7
+ export const ADAPTERS = Object.freeze({
8
+ colima: colimaAdapter,
9
+ orbstack: orbstackAdapter,
10
+ 'docker-desktop': dockerDesktopAdapter,
11
+ native: nativeAdapter,
12
+ wsl2: wsl2Adapter
13
+ });
14
+
15
+ export function getAdapter(engineId) {
16
+ const adapter = ADAPTERS[engineId];
17
+ if (!adapter) {
18
+ throw new Error(`No adapter registered for engine '${engineId}'`);
19
+ }
20
+ return adapter;
21
+ }
22
+
23
+ export function enginesForPlatform(platformName) {
24
+ return Object.values(ADAPTERS)
25
+ .filter((adapter) => adapter.supportedPlatforms.includes(platformName))
26
+ .map((adapter) => adapter.id);
27
+ }