@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.
- package/README.md +200 -8
- package/README.zh-CN.md +176 -8
- package/bin/cli.js +2 -2
- package/lib/init.js +18 -4
- package/lib/sandbox/commands/create.js +467 -240
- package/lib/sandbox/commands/enter.js +59 -26
- package/lib/sandbox/commands/ls.js +37 -6
- package/lib/sandbox/commands/rebuild.js +31 -15
- package/lib/sandbox/commands/refresh.js +119 -0
- package/lib/sandbox/commands/rm.js +59 -11
- package/lib/sandbox/commands/vm.js +50 -6
- package/lib/sandbox/config.js +9 -5
- package/lib/sandbox/constants.js +15 -3
- package/lib/sandbox/credentials.js +520 -0
- package/lib/sandbox/dotfiles.js +189 -0
- package/lib/sandbox/engine.js +135 -192
- package/lib/sandbox/engines/colima.js +79 -0
- package/lib/sandbox/engines/docker-desktop.js +34 -0
- package/lib/sandbox/engines/index.js +27 -0
- package/lib/sandbox/engines/native.js +112 -0
- package/lib/sandbox/engines/orbstack.js +76 -0
- package/lib/sandbox/engines/selinux.js +60 -0
- package/lib/sandbox/engines/wsl2-paths.js +59 -0
- package/lib/sandbox/engines/wsl2.js +72 -0
- package/lib/sandbox/index.js +10 -1
- package/lib/sandbox/runtimes/ai-tools.dockerfile +14 -1
- package/lib/sandbox/runtimes/base.dockerfile +116 -1
- package/lib/sandbox/shell.js +53 -2
- package/lib/sandbox/tools.js +5 -5
- package/package.json +6 -4
- package/templates/.agents/rules/create-issue.github.en.md +2 -4
- package/templates/.agents/rules/create-issue.github.zh-CN.md +2 -4
- package/templates/.agents/rules/issue-pr-commands.github.en.md +29 -0
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +29 -0
- package/templates/.agents/scripts/{platform-adapters/find-existing-task.github.js → find-existing-task.js} +22 -79
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +26 -41
- package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +6 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +6 -8
- 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
|
+
}
|
package/lib/sandbox/engine.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const PLATFORM_DEFAULTS = Object.freeze({
|
|
15
|
+
linux: ENGINES.NATIVE,
|
|
16
|
+
darwin: ENGINES.COLIMA,
|
|
17
|
+
win32: ENGINES.WSL2
|
|
15
18
|
});
|
|
16
19
|
|
|
17
|
-
|
|
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(
|
|
20
|
-
|
|
21
|
-
|
|
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 (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
64
|
+
return engine;
|
|
77
65
|
}
|
|
78
66
|
|
|
79
|
-
export
|
|
80
|
-
config,
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
73
|
+
const os = platformFn();
|
|
74
|
+
const fallback = PLATFORM_DEFAULTS[os];
|
|
75
|
+
if (fallback) {
|
|
76
|
+
return fallback;
|
|
94
77
|
}
|
|
95
78
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
102
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{
|
|
91
|
+
export function resolveEffectiveVm(
|
|
92
|
+
adapter,
|
|
93
|
+
userVm = {},
|
|
94
|
+
{ detectHostResourcesFn = detectHostResources } = {}
|
|
127
95
|
) {
|
|
128
|
-
|
|
96
|
+
let host = null;
|
|
97
|
+
const getHost = () => {
|
|
98
|
+
host ??= detectHostResourcesFn();
|
|
99
|
+
return host;
|
|
100
|
+
};
|
|
101
|
+
const defaults = adapter.defaultResources?.(getHost) ?? {};
|
|
129
102
|
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
148
|
+
try {
|
|
149
|
+
return getAdapter(engine).managed;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
209
153
|
}
|
|
210
154
|
|
|
211
155
|
export function engineDisplayName(engine) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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;
|