@fitlab-ai/agent-infra 0.5.9 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +200 -8
- package/README.zh-CN.md +176 -8
- package/bin/{cli.js → cli.ts} +23 -19
- package/dist/bin/cli.js +116 -0
- package/dist/lib/defaults.json +61 -0
- package/dist/lib/init.js +238 -0
- package/dist/lib/log.js +18 -0
- package/dist/lib/merge.js +747 -0
- package/dist/lib/paths.js +18 -0
- package/dist/lib/prompt.js +85 -0
- package/dist/lib/render.js +139 -0
- package/dist/lib/sandbox/commands/create.js +1173 -0
- package/dist/lib/sandbox/commands/enter.js +98 -0
- package/dist/lib/sandbox/commands/ls.js +93 -0
- package/dist/lib/sandbox/commands/rebuild.js +101 -0
- package/dist/lib/sandbox/commands/refresh.js +85 -0
- package/dist/lib/sandbox/commands/rm.js +226 -0
- package/dist/lib/sandbox/commands/vm.js +144 -0
- package/dist/lib/sandbox/config.js +85 -0
- package/dist/lib/sandbox/constants.js +104 -0
- package/dist/lib/sandbox/credentials.js +437 -0
- package/dist/lib/sandbox/dockerfile.js +76 -0
- package/dist/lib/sandbox/dotfiles.js +170 -0
- package/dist/lib/sandbox/engine.js +155 -0
- package/dist/lib/sandbox/engines/colima.js +64 -0
- package/dist/lib/sandbox/engines/docker-desktop.js +27 -0
- package/dist/lib/sandbox/engines/index.js +25 -0
- package/dist/lib/sandbox/engines/native.js +96 -0
- package/dist/lib/sandbox/engines/orbstack.js +63 -0
- package/dist/lib/sandbox/engines/selinux.js +48 -0
- package/dist/lib/sandbox/engines/wsl2-paths.js +47 -0
- package/dist/lib/sandbox/engines/wsl2.js +57 -0
- package/dist/lib/sandbox/index.js +70 -0
- package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +39 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +178 -0
- package/dist/lib/sandbox/runtimes/java17.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/java21.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/node20.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/node22.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/python3.dockerfile +3 -0
- package/dist/lib/sandbox/shell.js +148 -0
- package/dist/lib/sandbox/task-resolver.js +35 -0
- package/dist/lib/sandbox/tools.js +115 -0
- package/dist/lib/update.js +186 -0
- package/dist/lib/version.js +5 -0
- package/dist/package.json +5 -0
- package/lib/{init.js → init.ts} +64 -20
- package/lib/{log.js → log.ts} +4 -4
- package/lib/{merge.js → merge.ts} +129 -63
- package/lib/paths.ts +18 -0
- package/lib/{prompt.js → prompt.ts} +12 -12
- package/lib/{render.js → render.ts} +30 -17
- package/lib/sandbox/commands/create.ts +1507 -0
- package/lib/sandbox/commands/enter.ts +115 -0
- package/lib/sandbox/commands/{ls.js → ls.ts} +41 -10
- package/lib/sandbox/commands/rebuild.ts +135 -0
- package/lib/sandbox/commands/refresh.ts +128 -0
- package/lib/sandbox/commands/{rm.js → rm.ts} +71 -21
- package/lib/sandbox/commands/{vm.js → vm.ts} +62 -15
- package/lib/sandbox/config.ts +133 -0
- package/lib/sandbox/{constants.js → constants.ts} +41 -17
- package/lib/sandbox/credentials.ts +634 -0
- package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
- package/lib/sandbox/dotfiles.ts +236 -0
- package/lib/sandbox/engine.ts +231 -0
- package/lib/sandbox/engines/colima.ts +81 -0
- package/lib/sandbox/engines/docker-desktop.ts +36 -0
- package/lib/sandbox/engines/index.ts +74 -0
- package/lib/sandbox/engines/native.ts +131 -0
- package/lib/sandbox/engines/orbstack.ts +78 -0
- package/lib/sandbox/engines/selinux.ts +66 -0
- package/lib/sandbox/engines/wsl2-paths.ts +65 -0
- package/lib/sandbox/engines/wsl2.ts +74 -0
- package/lib/sandbox/{index.js → index.ts} +17 -8
- package/lib/sandbox/runtimes/ai-tools.dockerfile +14 -1
- package/lib/sandbox/runtimes/base.dockerfile +116 -1
- package/lib/sandbox/shell.ts +186 -0
- package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
- package/lib/sandbox/{tools.js → tools.ts} +33 -29
- package/lib/{update.js → update.ts} +33 -10
- package/package.json +22 -12
- 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/lib/paths.js +0 -9
- package/lib/sandbox/commands/create.js +0 -1174
- package/lib/sandbox/commands/enter.js +0 -79
- package/lib/sandbox/commands/rebuild.js +0 -102
- package/lib/sandbox/config.js +0 -84
- package/lib/sandbox/engine.js +0 -256
- package/lib/sandbox/shell.js +0 -122
- package/templates/.agents/scripts/platform-adapters/find-existing-task.js +0 -5
- /package/lib/{version.js → version.ts} +0 -0
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { loadConfig } from '../config.js';
|
|
2
|
-
import { assertValidBranchName, containerNameCandidates } from '../constants.js';
|
|
3
|
-
import { runInteractive, runSafe } from '../shell.js';
|
|
4
|
-
import { resolveTaskBranch } from '../task-resolver.js';
|
|
5
|
-
|
|
6
|
-
const USAGE = `Usage: ai sandbox exec <branch> [cmd...]`;
|
|
7
|
-
export const TMUX_ENTRY_SCRIPT = `
|
|
8
|
-
SESSION=work
|
|
9
|
-
|
|
10
|
-
if ! command -v tmux >/dev/null 2>&1; then
|
|
11
|
-
exec bash
|
|
12
|
-
fi
|
|
13
|
-
|
|
14
|
-
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
15
|
-
exec tmux new-session -s "$SESSION"
|
|
16
|
-
fi
|
|
17
|
-
|
|
18
|
-
tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null | \\
|
|
19
|
-
while read -r name attached; do
|
|
20
|
-
[ "$name" = "$SESSION" ] && continue
|
|
21
|
-
case "$name" in
|
|
22
|
-
''|*[!0-9]*) continue ;;
|
|
23
|
-
esac
|
|
24
|
-
[ "$attached" = "0" ] && tmux kill-session -t "$name" 2>/dev/null || true
|
|
25
|
-
done
|
|
26
|
-
|
|
27
|
-
exec tmux new-session -t "$SESSION"
|
|
28
|
-
`.trim();
|
|
29
|
-
|
|
30
|
-
// Terminal-detection variables that interactive TUIs (e.g. claude-code)
|
|
31
|
-
// inspect to enable progressive enhancements such as the kitty keyboard
|
|
32
|
-
// protocol, which is what makes Shift+Enter distinguishable from Enter.
|
|
33
|
-
// `docker exec` does not forward these by default, so we must pass them
|
|
34
|
-
// through explicitly.
|
|
35
|
-
const FORWARDED_TERMINAL_ENV = [
|
|
36
|
-
'TERM_PROGRAM',
|
|
37
|
-
'TERM_PROGRAM_VERSION',
|
|
38
|
-
'LC_TERMINAL',
|
|
39
|
-
'LC_TERMINAL_VERSION'
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
export function terminalEnvFlags(env = process.env) {
|
|
43
|
-
const flags = [];
|
|
44
|
-
for (const name of FORWARDED_TERMINAL_ENV) {
|
|
45
|
-
const value = env[name];
|
|
46
|
-
if (value) {
|
|
47
|
-
flags.push('-e', `${name}=${value}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return flags;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function enter(args) {
|
|
54
|
-
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
55
|
-
process.stdout.write(`${USAGE}\n`);
|
|
56
|
-
if (args.length === 0) {
|
|
57
|
-
return 1;
|
|
58
|
-
}
|
|
59
|
-
return 0;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const config = loadConfig();
|
|
63
|
-
const [branchOrTaskId, ...cmd] = args;
|
|
64
|
-
const branch = resolveTaskBranch(branchOrTaskId, config.repoRoot);
|
|
65
|
-
assertValidBranchName(branch);
|
|
66
|
-
const running = runSafe('docker', ['ps', '--format', '{{.Names}}']).split('\n');
|
|
67
|
-
const container = containerNameCandidates(config, branch).find((name) => running.includes(name));
|
|
68
|
-
|
|
69
|
-
if (!container) {
|
|
70
|
-
throw new Error(`No running sandbox found for branch '${branch}'`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const envFlags = terminalEnvFlags();
|
|
74
|
-
if (cmd.length === 0) {
|
|
75
|
-
return runInteractive('docker', ['exec', '-it', ...envFlags, container, 'bash', '-c', TMUX_ENTRY_SCRIPT]);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return runInteractive('docker', ['exec', '-it', ...envFlags, container, ...cmd]);
|
|
79
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { parseArgs } from 'node:util';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import * as p from '@clack/prompts';
|
|
4
|
-
import pc from 'picocolors';
|
|
5
|
-
import { loadConfig } from '../config.js';
|
|
6
|
-
import { prepareDockerfile } from '../dockerfile.js';
|
|
7
|
-
import { sandboxImageConfigLabel, sandboxLabel } from '../constants.js';
|
|
8
|
-
import { ensureDocker } from '../engine.js';
|
|
9
|
-
import { run, runOk, runVerbose } from '../shell.js';
|
|
10
|
-
import { resolveTools, toolNpmPackagesArg } from '../tools.js';
|
|
11
|
-
|
|
12
|
-
const USAGE = `Usage: ai sandbox rebuild [--quiet]`;
|
|
13
|
-
|
|
14
|
-
function buildSignature(preparedDockerfile, tools) {
|
|
15
|
-
return createHash('sha256')
|
|
16
|
-
.update(JSON.stringify({
|
|
17
|
-
dockerfile: preparedDockerfile.signature,
|
|
18
|
-
tools: tools.map((tool) => tool.npmPackage)
|
|
19
|
-
}))
|
|
20
|
-
.digest('hex')
|
|
21
|
-
.slice(0, 12);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function buildArgs(config, tools, dockerfilePath, imageSignature) {
|
|
25
|
-
const hostUid = run('id', ['-u']);
|
|
26
|
-
const hostGid = run('id', ['-g']);
|
|
27
|
-
|
|
28
|
-
return [
|
|
29
|
-
'build',
|
|
30
|
-
'-t',
|
|
31
|
-
config.imageName,
|
|
32
|
-
'--build-arg',
|
|
33
|
-
`HOST_UID=${hostUid}`,
|
|
34
|
-
'--build-arg',
|
|
35
|
-
`HOST_GID=${hostGid}`,
|
|
36
|
-
'--build-arg',
|
|
37
|
-
`AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
|
|
38
|
-
'--label',
|
|
39
|
-
sandboxLabel(config),
|
|
40
|
-
'--label',
|
|
41
|
-
`${sandboxImageConfigLabel(config)}=${imageSignature}`,
|
|
42
|
-
'-f',
|
|
43
|
-
dockerfilePath,
|
|
44
|
-
config.repoRoot
|
|
45
|
-
];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function removeImageIfPresent(imageName) {
|
|
49
|
-
if (runOk('docker', ['image', 'inspect', imageName])) {
|
|
50
|
-
run('docker', ['rmi', imageName]);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function rebuild(args) {
|
|
55
|
-
const { values } = parseArgs({
|
|
56
|
-
args,
|
|
57
|
-
allowPositionals: true,
|
|
58
|
-
strict: true,
|
|
59
|
-
options: {
|
|
60
|
-
quiet: { type: 'boolean', short: 'q' },
|
|
61
|
-
help: { type: 'boolean', short: 'h' }
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
if (values.help) {
|
|
66
|
-
process.stdout.write(`${USAGE}\n`);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const config = loadConfig();
|
|
71
|
-
const tools = resolveTools(config);
|
|
72
|
-
const preparedDockerfile = prepareDockerfile(config);
|
|
73
|
-
const imageSignature = buildSignature(preparedDockerfile, tools);
|
|
74
|
-
const quiet = values.quiet ?? false;
|
|
75
|
-
|
|
76
|
-
await ensureDocker(config);
|
|
77
|
-
p.intro(pc.cyan('Rebuilding sandbox image'));
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
if (quiet) {
|
|
81
|
-
const spinner = p.spinner();
|
|
82
|
-
spinner.start(`Removing old image ${config.imageName}...`);
|
|
83
|
-
removeImageIfPresent(config.imageName);
|
|
84
|
-
spinner.stop('Old image removed');
|
|
85
|
-
spinner.start('Building image...');
|
|
86
|
-
run('docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature), { cwd: config.repoRoot });
|
|
87
|
-
spinner.stop(pc.green('Sandbox image rebuilt'));
|
|
88
|
-
} else {
|
|
89
|
-
p.log.step(`Removing old image ${config.imageName}`);
|
|
90
|
-
removeImageIfPresent(config.imageName);
|
|
91
|
-
p.log.step('Building image');
|
|
92
|
-
runVerbose(
|
|
93
|
-
'docker',
|
|
94
|
-
buildArgs(config, tools, preparedDockerfile.path, imageSignature),
|
|
95
|
-
{ cwd: config.repoRoot }
|
|
96
|
-
);
|
|
97
|
-
p.log.success(pc.green('Sandbox image rebuilt'));
|
|
98
|
-
}
|
|
99
|
-
} finally {
|
|
100
|
-
preparedDockerfile.cleanup();
|
|
101
|
-
}
|
|
102
|
-
}
|
package/lib/sandbox/config.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import { validateSandboxEngine } from './engine.js';
|
|
5
|
-
|
|
6
|
-
const DEFAULTS = Object.freeze({
|
|
7
|
-
engine: null,
|
|
8
|
-
runtimes: ['node20'],
|
|
9
|
-
tools: ['claude-code', 'codex', 'opencode', 'gemini-cli'],
|
|
10
|
-
dockerfile: null,
|
|
11
|
-
vm: {
|
|
12
|
-
cpu: null,
|
|
13
|
-
memory: null,
|
|
14
|
-
disk: null
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
function detectRepoRoot() {
|
|
19
|
-
try {
|
|
20
|
-
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
21
|
-
encoding: 'utf8',
|
|
22
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
23
|
-
}).trim();
|
|
24
|
-
} catch {
|
|
25
|
-
throw new Error('sandbox: current directory is not inside a git repository');
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function cloneDefaults() {
|
|
30
|
-
return {
|
|
31
|
-
engine: DEFAULTS.engine,
|
|
32
|
-
runtimes: [...DEFAULTS.runtimes],
|
|
33
|
-
tools: [...DEFAULTS.tools],
|
|
34
|
-
dockerfile: DEFAULTS.dockerfile,
|
|
35
|
-
vm: { ...DEFAULTS.vm }
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function loadConfig() {
|
|
40
|
-
const repoRoot = detectRepoRoot();
|
|
41
|
-
const home = process.env.HOME;
|
|
42
|
-
|
|
43
|
-
if (!home) {
|
|
44
|
-
throw new Error('sandbox: HOME environment variable is required');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const configPath = path.join(repoRoot, '.agents', '.airc.json');
|
|
48
|
-
if (!fs.existsSync(configPath)) {
|
|
49
|
-
throw new Error('No .agents/.airc.json found. Run "ai init" first.');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const airc = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
53
|
-
const defaults = cloneDefaults();
|
|
54
|
-
const sandbox = airc.sandbox ?? {};
|
|
55
|
-
const engine = validateSandboxEngine(sandbox.engine ?? defaults.engine);
|
|
56
|
-
const project = airc.project;
|
|
57
|
-
|
|
58
|
-
if (!project || typeof project !== 'string') {
|
|
59
|
-
throw new Error('sandbox: .agents/.airc.json is missing a valid "project" field');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
repoRoot,
|
|
64
|
-
configPath,
|
|
65
|
-
project,
|
|
66
|
-
org: airc.org ?? '',
|
|
67
|
-
home,
|
|
68
|
-
containerPrefix: `${project}-dev`,
|
|
69
|
-
imageName: `${project}-sandbox:latest`,
|
|
70
|
-
worktreeBase: path.join(home, '.agent-infra', 'worktrees', project),
|
|
71
|
-
engine,
|
|
72
|
-
runtimes: Array.isArray(sandbox.runtimes) && sandbox.runtimes.length > 0
|
|
73
|
-
? [...sandbox.runtimes]
|
|
74
|
-
: defaults.runtimes,
|
|
75
|
-
tools: Array.isArray(sandbox.tools) && sandbox.tools.length > 0
|
|
76
|
-
? [...sandbox.tools]
|
|
77
|
-
: defaults.tools,
|
|
78
|
-
dockerfile: sandbox.dockerfile ?? defaults.dockerfile,
|
|
79
|
-
vm: {
|
|
80
|
-
...defaults.vm,
|
|
81
|
-
...(sandbox.vm ?? {})
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
}
|
package/lib/sandbox/engine.js
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { platform } from 'node:os';
|
|
2
|
-
import { detectHostResources } from './constants.js';
|
|
3
|
-
import { run, runOk, runSafe, runVerbose } from './shell.js';
|
|
4
|
-
|
|
5
|
-
export const ENGINES = Object.freeze({
|
|
6
|
-
COLIMA: 'colima',
|
|
7
|
-
ORBSTACK: 'orbstack',
|
|
8
|
-
DOCKER_DESKTOP: 'docker-desktop'
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
export const ENGINE_DOCKER_CONTEXT = Object.freeze({
|
|
12
|
-
[ENGINES.COLIMA]: 'colima',
|
|
13
|
-
[ENGINES.ORBSTACK]: 'orbstack',
|
|
14
|
-
[ENGINES.DOCKER_DESKTOP]: 'desktop-linux'
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const VALID_CONFIG_ENGINES = new Set(Object.values(ENGINES));
|
|
18
|
-
|
|
19
|
-
function applyDockerContext(engine) {
|
|
20
|
-
const context = ENGINE_DOCKER_CONTEXT[engine];
|
|
21
|
-
if (context) {
|
|
22
|
-
process.env.DOCKER_CONTEXT = context;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function validateSandboxEngine(engine) {
|
|
27
|
-
if (engine === null || engine === undefined) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
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
|
-
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;
|
|
57
|
-
}
|
|
58
|
-
return 'unsupported';
|
|
59
|
-
}
|
|
60
|
-
|
|
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');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return args;
|
|
77
|
-
}
|
|
78
|
-
|
|
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']);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!runOkFn('colima', ['status'])) {
|
|
92
|
-
onMessage?.('Starting Colima VM...');
|
|
93
|
-
runVerboseFn('colima', colimaArgs(config, runSafeFn));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!runOkFn('docker', ['info'])) {
|
|
97
|
-
throw new Error('Docker daemon is not available after starting Colima');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
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
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export async function ensureDockerDesktop(
|
|
124
|
-
_config,
|
|
125
|
-
onMessage,
|
|
126
|
-
{ runOkFn = runOk } = {}
|
|
127
|
-
) {
|
|
128
|
-
applyDockerContext(ENGINES.DOCKER_DESKTOP);
|
|
129
|
-
|
|
130
|
-
if (!runOkFn('docker', ['info'])) {
|
|
131
|
-
throw new Error('Docker Desktop is not running. Please start Docker Desktop manually.');
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
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'));
|
|
170
|
-
}
|
|
171
|
-
|
|
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
|
-
}
|
|
198
|
-
|
|
199
|
-
throw new Error(`Unsupported sandbox engine: ${engine}`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export function isVmManaged(config = {}, dependencies = {}) {
|
|
203
|
-
const engine = detectEngine(config, dependencies);
|
|
204
|
-
return isManagedEngine(engine);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function isManagedEngine(engine) {
|
|
208
|
-
return engine === ENGINES.COLIMA || engine === ENGINES.ORBSTACK;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
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;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function startManagedVm(
|
|
223
|
-
config,
|
|
224
|
-
{ platformFn = platform, runOkFn = runOk, runVerboseFn = runVerbose } = {}
|
|
225
|
-
) {
|
|
226
|
-
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';
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (engine === ENGINES.COLIMA) {
|
|
239
|
-
runVerboseFn('colima', colimaArgs(config));
|
|
240
|
-
} else if (engine === ENGINES.ORBSTACK) {
|
|
241
|
-
runVerboseFn('orb', ['start']);
|
|
242
|
-
}
|
|
243
|
-
return 'started';
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function stopManagedVm(config, { platformFn = platform, runFn = run } = {}) {
|
|
247
|
-
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';
|
|
254
|
-
}
|
|
255
|
-
throw new Error(`VM management is unavailable for engine '${engineDisplayName(engine)}'.`);
|
|
256
|
-
}
|
package/lib/sandbox/shell.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
6
|
-
|
|
7
|
-
function normalizeOptions(opts = {}, stdio) {
|
|
8
|
-
return {
|
|
9
|
-
cwd: opts.cwd,
|
|
10
|
-
encoding: opts.encoding,
|
|
11
|
-
stdio,
|
|
12
|
-
timeout: opts.timeout ?? DEFAULT_TIMEOUT_MS
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function resolveCommand(cmd) {
|
|
17
|
-
if (process.platform !== 'win32' || path.extname(cmd)) {
|
|
18
|
-
return cmd;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const pathValue = process.env.Path || process.env.PATH || '';
|
|
22
|
-
const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
23
|
-
.split(';')
|
|
24
|
-
.filter(Boolean);
|
|
25
|
-
|
|
26
|
-
for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
27
|
-
for (const extension of extensions) {
|
|
28
|
-
const candidate = path.join(dir, `${cmd}${extension.toLowerCase()}`);
|
|
29
|
-
if (fs.existsSync(candidate)) {
|
|
30
|
-
return candidate;
|
|
31
|
-
}
|
|
32
|
-
const upperCandidate = path.join(dir, `${cmd}${extension.toUpperCase()}`);
|
|
33
|
-
if (fs.existsSync(upperCandidate)) {
|
|
34
|
-
return upperCandidate;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return cmd;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function commandOptions(cmd, opts) {
|
|
43
|
-
if (process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(cmd)) {
|
|
44
|
-
return { ...opts, shell: true };
|
|
45
|
-
}
|
|
46
|
-
return opts;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function run(cmd, args, opts = {}) {
|
|
50
|
-
const resolved = resolveCommand(cmd);
|
|
51
|
-
return execFileSync(resolved, args, commandOptions(resolved, {
|
|
52
|
-
...normalizeOptions(opts, ['pipe', 'pipe', 'pipe']),
|
|
53
|
-
encoding: 'utf8'
|
|
54
|
-
})).trim();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function runOk(cmd, args, opts = {}) {
|
|
58
|
-
const resolved = resolveCommand(cmd);
|
|
59
|
-
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'pipe')));
|
|
60
|
-
return result.status === 0;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function restoreTerminal() {
|
|
64
|
-
if (!process.stdout.isTTY) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
process.stdout.write([
|
|
70
|
-
'\x1b[?1049l',
|
|
71
|
-
'\x1b[?25h',
|
|
72
|
-
'\x1b>',
|
|
73
|
-
'\x1b[?1000l',
|
|
74
|
-
'\x1b[?1002l',
|
|
75
|
-
'\x1b[?1003l',
|
|
76
|
-
'\x1b[?1006l'
|
|
77
|
-
].join(''));
|
|
78
|
-
} catch {
|
|
79
|
-
// Best-effort cleanup only; preserve the original command result.
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (process.platform === 'win32') {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
execFileSync('stty', ['sane'], { stdio: 'inherit' });
|
|
88
|
-
} catch {
|
|
89
|
-
// Some environments do not provide stty or reject sane; ANSI reset still helps.
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function runInteractive(cmd, args, opts = {}) {
|
|
94
|
-
const resolved = resolveCommand(cmd);
|
|
95
|
-
try {
|
|
96
|
-
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
|
|
97
|
-
return result.status ?? 1;
|
|
98
|
-
} finally {
|
|
99
|
-
restoreTerminal();
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function runVerbose(cmd, args, opts = {}) {
|
|
104
|
-
const resolved = resolveCommand(cmd);
|
|
105
|
-
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
|
|
106
|
-
|
|
107
|
-
if (result.status !== 0) {
|
|
108
|
-
if (result.signal === 'SIGTERM') {
|
|
109
|
-
throw new Error(`Command timed out after ${opts.timeout ?? DEFAULT_TIMEOUT_MS}ms: ${cmd} ${args.join(' ')}`);
|
|
110
|
-
}
|
|
111
|
-
throw new Error(`Command failed with exit code ${result.status}: ${cmd} ${args.join(' ')}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function runSafe(cmd, args, opts = {}) {
|
|
116
|
-
const resolved = resolveCommand(cmd);
|
|
117
|
-
const result = spawnSync(resolved, args, commandOptions(resolved, {
|
|
118
|
-
...normalizeOptions(opts, ['pipe', 'pipe', 'pipe']),
|
|
119
|
-
encoding: 'utf8',
|
|
120
|
-
}));
|
|
121
|
-
return (result.stdout ?? '').trim();
|
|
122
|
-
}
|
|
File without changes
|