@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.
- package/README.md +237 -5
- package/README.zh-CN.md +213 -5
- 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 +56 -6
- package/lib/sandbox/config.js +9 -5
- package/lib/sandbox/constants.js +18 -3
- package/lib/sandbox/credentials.js +520 -0
- package/lib/sandbox/dotfiles.js +189 -0
- package/lib/sandbox/engine.js +135 -157
- 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 +125 -3
- package/lib/sandbox/shell.js +53 -2
- package/lib/sandbox/tools.js +5 -5
- package/package.json +8 -4
- package/templates/.agents/rules/create-issue.en.md +5 -0
- package/templates/.agents/rules/create-issue.github.en.md +176 -0
- package/templates/.agents/rules/create-issue.github.zh-CN.md +176 -0
- package/templates/.agents/rules/create-issue.zh-CN.md +5 -0
- 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/rules/issue-sync.github.en.md +1 -1
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +1 -1
- package/templates/.agents/rules/milestone-inference.github.en.md +2 -2
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +2 -2
- 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 +72 -42
- package/templates/.agents/skills/create-task/SKILL.en.md +69 -11
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +70 -12
- package/templates/.agents/skills/create-task/config/verify.json +6 -1
- package/templates/.agents/skills/implement-task/reference/implementation-rules.en.md +7 -12
- package/templates/.agents/skills/implement-task/reference/implementation-rules.zh-CN.md +7 -12
- package/templates/.agents/skills/import-issue/SKILL.en.md +7 -9
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +7 -9
- package/templates/.agents/skills/refine-task/reference/fix-workflow.en.md +2 -2
- package/templates/.agents/skills/refine-task/reference/fix-workflow.zh-CN.md +2 -2
- package/templates/.agents/skills/test/SKILL.en.md +45 -6
- package/templates/.agents/skills/test/SKILL.zh-CN.md +45 -6
- package/templates/.agents/scripts/platform-adapters/find-existing-task.js +0 -5
- package/templates/.agents/skills/create-issue/SKILL.en.md +0 -118
- package/templates/.agents/skills/create-issue/SKILL.zh-CN.md +0 -118
- package/templates/.agents/skills/create-issue/config/verify.json +0 -30
- package/templates/.agents/skills/create-issue/reference/label-and-type.en.md +0 -71
- package/templates/.agents/skills/create-issue/reference/label-and-type.zh-CN.md +0 -71
- package/templates/.agents/skills/create-issue/reference/template-matching.en.md +0 -17
- package/templates/.agents/skills/create-issue/reference/template-matching.zh-CN.md +0 -17
- package/templates/.claude/commands/create-issue.en.md +0 -8
- package/templates/.claude/commands/create-issue.zh-CN.md +0 -8
- package/templates/.gemini/commands/_project_/create-issue.en.toml +0 -8
- package/templates/.gemini/commands/_project_/create-issue.zh-CN.toml +0 -8
- package/templates/.opencode/commands/create-issue.en.md +0 -11
- 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
|
+
}
|
package/lib/sandbox/engine.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
+
try {
|
|
149
|
+
return getAdapter(engine).managed;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
174
153
|
}
|
|
175
154
|
|
|
176
155
|
export function engineDisplayName(engine) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
+
}
|