@fitlab-ai/agent-infra 0.5.10 → 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 +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/{cli.js → cli.ts} +21 -17
- 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} +48 -18
- 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.js → create.ts} +224 -118
- package/lib/sandbox/commands/{enter.js → enter.ts} +17 -14
- package/lib/sandbox/commands/{ls.js → ls.ts} +10 -10
- package/lib/sandbox/commands/{rebuild.js → rebuild.ts} +38 -21
- package/lib/sandbox/commands/{refresh.js → refresh.ts} +16 -7
- package/lib/sandbox/commands/{rm.js → rm.ts} +15 -13
- package/lib/sandbox/commands/{vm.js → vm.ts} +14 -11
- package/lib/sandbox/{config.js → config.ts} +55 -10
- package/lib/sandbox/{constants.js → constants.ts} +30 -18
- package/lib/sandbox/{credentials.js → credentials.ts} +160 -46
- package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
- package/lib/sandbox/{dotfiles.js → dotfiles.ts} +66 -19
- package/lib/sandbox/{engine.js → engine.ts} +57 -25
- package/lib/sandbox/engines/{colima.js → colima.ts} +9 -7
- package/lib/sandbox/engines/{docker-desktop.js → docker-desktop.ts} +5 -3
- package/lib/sandbox/engines/index.ts +74 -0
- package/lib/sandbox/engines/{native.js → native.ts} +25 -6
- package/lib/sandbox/engines/{orbstack.js → orbstack.ts} +7 -5
- package/lib/sandbox/engines/{selinux.js → selinux.ts} +11 -5
- package/lib/sandbox/engines/{wsl2-paths.js → wsl2-paths.ts} +15 -9
- package/lib/sandbox/engines/{wsl2.js → wsl2.ts} +9 -7
- package/lib/sandbox/{index.js → index.ts} +8 -8
- package/lib/sandbox/{shell.js → shell.ts} +30 -17
- package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
- package/lib/sandbox/{tools.js → tools.ts} +30 -26
- package/lib/{update.js → update.ts} +33 -10
- package/package.json +17 -9
- package/lib/paths.js +0 -9
- package/lib/sandbox/engines/index.js +0 -27
- /package/lib/{version.js → version.ts} +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { loadConfig } from '../config.
|
|
2
|
-
import { assertValidBranchName, containerNameCandidates } from '../constants.
|
|
3
|
-
import { detectEngine } from '../engine.
|
|
1
|
+
import { loadConfig } from '../config.ts';
|
|
2
|
+
import { assertValidBranchName, containerNameCandidates } from '../constants.ts';
|
|
3
|
+
import { detectEngine } from '../engine.ts';
|
|
4
4
|
import {
|
|
5
5
|
formatCredentialWarnings,
|
|
6
6
|
formatRemaining,
|
|
7
7
|
reconcileClaudeCredentials,
|
|
8
8
|
redactCommandError,
|
|
9
9
|
validateClaudeCredentialsEnvOverride
|
|
10
|
-
} from '../credentials.
|
|
11
|
-
import { runInteractiveEngine, runSafeEngine } from '../shell.
|
|
12
|
-
import { resolveTaskBranch } from '../task-resolver.
|
|
13
|
-
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.
|
|
10
|
+
} from '../credentials.ts';
|
|
11
|
+
import { runInteractiveEngine, runSafeEngine } from '../shell.ts';
|
|
12
|
+
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
13
|
+
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
|
|
14
14
|
|
|
15
15
|
const USAGE = `Usage: ai sandbox exec <branch> [cmd...]`;
|
|
16
16
|
const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
|
|
@@ -27,8 +27,8 @@ const FORWARDED_TERMINAL_ENV = [
|
|
|
27
27
|
'LC_TERMINAL_VERSION'
|
|
28
28
|
];
|
|
29
29
|
|
|
30
|
-
export function terminalEnvFlags(env = process.env) {
|
|
31
|
-
const flags = [];
|
|
30
|
+
export function terminalEnvFlags(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
31
|
+
const flags: string[] = [];
|
|
32
32
|
for (const name of FORWARDED_TERMINAL_ENV) {
|
|
33
33
|
const value = env[name];
|
|
34
34
|
if (value) {
|
|
@@ -38,7 +38,10 @@ export function terminalEnvFlags(env = process.env) {
|
|
|
38
38
|
return flags;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export function formatCredentialSyncStatus(
|
|
41
|
+
export function formatCredentialSyncStatus(
|
|
42
|
+
result: ReturnType<typeof reconcileClaudeCredentials>,
|
|
43
|
+
isTTY = process.stderr.isTTY
|
|
44
|
+
): string | null {
|
|
42
45
|
if (result.status === 'STALE_ACCESS') {
|
|
43
46
|
return 'Warning: Claude Code credentials on host appear stale. Run "ai sandbox refresh" or "claude /login" to renew.\n';
|
|
44
47
|
}
|
|
@@ -62,7 +65,7 @@ export function formatCredentialSyncStatus(result, isTTY = process.stderr.isTTY)
|
|
|
62
65
|
return null;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
export function enter(args) {
|
|
68
|
+
export function enter(args: string[]): number {
|
|
66
69
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
67
70
|
process.stdout.write(`${USAGE}\n`);
|
|
68
71
|
if (args.length === 0) {
|
|
@@ -74,7 +77,7 @@ export function enter(args) {
|
|
|
74
77
|
const config = loadConfig();
|
|
75
78
|
validateClaudeCredentialsEnvOverride();
|
|
76
79
|
const engine = detectEngine(config);
|
|
77
|
-
const [branchOrTaskId, ...cmd] = args;
|
|
80
|
+
const [branchOrTaskId = '', ...cmd] = args;
|
|
78
81
|
const branch = resolveTaskBranch(branchOrTaskId, config.repoRoot);
|
|
79
82
|
assertValidBranchName(branch);
|
|
80
83
|
const running = runSafeEngine(engine, 'docker', ['ps', '--format', '{{.Names}}']).split('\n');
|
|
@@ -93,7 +96,7 @@ export function enter(args) {
|
|
|
93
96
|
process.stderr.write(message);
|
|
94
97
|
}
|
|
95
98
|
} catch (error) {
|
|
96
|
-
process.stderr.write(`Warning: Failed to sync Claude Code credentials: ${redactCommandError(error
|
|
99
|
+
process.stderr.write(`Warning: Failed to sync Claude Code credentials: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
|
|
@@ -102,7 +105,7 @@ export function enter(args) {
|
|
|
102
105
|
try {
|
|
103
106
|
materializeDotfiles(config.dotfilesDir, dotfilesCacheDir(config.home, config.project));
|
|
104
107
|
} catch (error) {
|
|
105
|
-
process.stderr.write(`Warning: dotfiles snapshot rebuild failed: ${redactCommandError(error
|
|
108
|
+
process.stderr.write(`Warning: dotfiles snapshot rebuild failed: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
return runInteractiveEngine(engine, 'docker', ['exec', '-it', ...envFlags, container, 'bash', TMUX_ENTRY_PATH]);
|
|
@@ -2,26 +2,26 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import * as p from '@clack/prompts';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import { loadConfig } from '../config.
|
|
6
|
-
import { sandboxBranchLabel, sandboxLabel } from '../constants.
|
|
7
|
-
import { detectEngine } from '../engine.
|
|
8
|
-
import { runSafeEngine } from '../shell.
|
|
9
|
-
import { resolveTools, toolProjectDirCandidates } from '../tools.
|
|
5
|
+
import { loadConfig } from '../config.ts';
|
|
6
|
+
import { sandboxBranchLabel, sandboxLabel } from '../constants.ts';
|
|
7
|
+
import { detectEngine } from '../engine.ts';
|
|
8
|
+
import { runSafeEngine } from '../shell.ts';
|
|
9
|
+
import { resolveTools, toolProjectDirCandidates } from '../tools.ts';
|
|
10
10
|
|
|
11
11
|
const USAGE = 'Usage: ai sandbox ls';
|
|
12
12
|
const CONTAINER_LIST_HEADER = 'NAMES\tSTATUS\tBRANCH';
|
|
13
13
|
|
|
14
14
|
// Exported to lock the docker/podman-compatible format in unit tests.
|
|
15
|
-
export function containerListFormat() {
|
|
15
|
+
export function containerListFormat(): string {
|
|
16
16
|
return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function parseLabels(csv) {
|
|
19
|
+
export function parseLabels(csv: string): Record<string, string> {
|
|
20
20
|
if (!csv) {
|
|
21
21
|
return {};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const labels = {};
|
|
24
|
+
const labels: Record<string, string> = {};
|
|
25
25
|
for (const pair of csv.split(',')) {
|
|
26
26
|
if (!pair) {
|
|
27
27
|
continue;
|
|
@@ -35,7 +35,7 @@ export function parseLabels(csv) {
|
|
|
35
35
|
return labels;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function listChildren(dir) {
|
|
38
|
+
function listChildren(dir: string): string[] {
|
|
39
39
|
if (!fs.existsSync(dir)) {
|
|
40
40
|
return [];
|
|
41
41
|
}
|
|
@@ -43,7 +43,7 @@ function listChildren(dir) {
|
|
|
43
43
|
return fs.readdirSync(dir).sort().map((entry) => path.join(dir, entry));
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export function ls(args = []) {
|
|
46
|
+
export function ls(args: string[] = []): void {
|
|
47
47
|
if (args.length > 0 && (args[0] === '--help' || args[0] === '-h')) {
|
|
48
48
|
process.stdout.write(`${USAGE}\n`);
|
|
49
49
|
return;
|
|
@@ -2,18 +2,24 @@ import { parseArgs } from 'node:util';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import * as p from '@clack/prompts';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import { loadConfig } from '../config.
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
5
|
+
import { loadConfig } from '../config.ts';
|
|
6
|
+
import type { SandboxConfig } from '../config.ts';
|
|
7
|
+
import { prepareDockerfile } from '../dockerfile.ts';
|
|
8
|
+
import { sandboxImageConfigLabel, sandboxLabel } from '../constants.ts';
|
|
9
|
+
import { detectEngine, ensureDocker } from '../engine.ts';
|
|
10
|
+
import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shell.ts';
|
|
11
|
+
import { resolveTools, toolNpmPackagesArg } from '../tools.ts';
|
|
12
|
+
import type { SandboxTool } from '../tools.ts';
|
|
13
|
+
import { toEnginePath } from '../engines/wsl2-paths.ts';
|
|
14
|
+
import { resolveBuildUid } from '../engines/native.ts';
|
|
13
15
|
|
|
14
16
|
const USAGE = `Usage: ai sandbox rebuild [--quiet]`;
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
type PreparedDockerfile = ReturnType<typeof prepareDockerfile>;
|
|
19
|
+
type EngineRunFn = (engine: string, cmd: string, args: string[], opts?: { cwd?: string }) => string;
|
|
20
|
+
type EngineRunSafeFn = EngineRunFn;
|
|
21
|
+
|
|
22
|
+
function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTool[]): string {
|
|
17
23
|
return createHash('sha256')
|
|
18
24
|
.update(JSON.stringify({
|
|
19
25
|
dockerfile: preparedDockerfile.signature,
|
|
@@ -24,14 +30,25 @@ function buildSignature(preparedDockerfile, tools) {
|
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
export function buildArgs(
|
|
27
|
-
config,
|
|
28
|
-
tools,
|
|
29
|
-
dockerfilePath,
|
|
30
|
-
imageSignature,
|
|
31
|
-
{
|
|
32
|
-
) {
|
|
33
|
-
const { uid: hostUid, gid: hostGid } = resolveBuildUid({
|
|
33
|
+
config: SandboxConfig,
|
|
34
|
+
tools: SandboxTool[],
|
|
35
|
+
dockerfilePath: string,
|
|
36
|
+
imageSignature: string,
|
|
37
|
+
{
|
|
34
38
|
engine,
|
|
39
|
+
runFn = runEngine,
|
|
40
|
+
runSafeFn = runSafeEngine,
|
|
41
|
+
env = process.env
|
|
42
|
+
}: {
|
|
43
|
+
engine?: string;
|
|
44
|
+
runFn?: EngineRunFn;
|
|
45
|
+
runSafeFn?: EngineRunSafeFn;
|
|
46
|
+
env?: NodeJS.ProcessEnv;
|
|
47
|
+
} = {}
|
|
48
|
+
): string[] {
|
|
49
|
+
const selectedEngine = engine ?? detectEngine(config);
|
|
50
|
+
const { uid: hostUid, gid: hostGid } = resolveBuildUid({
|
|
51
|
+
engine: selectedEngine,
|
|
35
52
|
runFn,
|
|
36
53
|
runSafeFn,
|
|
37
54
|
env
|
|
@@ -52,18 +69,18 @@ export function buildArgs(
|
|
|
52
69
|
'--label',
|
|
53
70
|
`${sandboxImageConfigLabel(config)}=${imageSignature}`,
|
|
54
71
|
'-f',
|
|
55
|
-
toEnginePath(
|
|
56
|
-
toEnginePath(
|
|
72
|
+
toEnginePath(selectedEngine, dockerfilePath),
|
|
73
|
+
toEnginePath(selectedEngine, config.repoRoot)
|
|
57
74
|
];
|
|
58
75
|
}
|
|
59
76
|
|
|
60
|
-
function removeImageIfPresent(imageName, engine) {
|
|
77
|
+
function removeImageIfPresent(imageName: string, engine: string): void {
|
|
61
78
|
if (runOkEngine(engine, 'docker', ['image', 'inspect', imageName])) {
|
|
62
79
|
runEngine(engine, 'docker', ['rmi', imageName]);
|
|
63
80
|
}
|
|
64
81
|
}
|
|
65
82
|
|
|
66
|
-
export async function rebuild(args) {
|
|
83
|
+
export async function rebuild(args: string[]): Promise<void> {
|
|
67
84
|
const { values } = parseArgs({
|
|
68
85
|
args,
|
|
69
86
|
allowPositionals: true,
|
|
@@ -86,7 +103,7 @@ export async function rebuild(args) {
|
|
|
86
103
|
const quiet = values.quiet ?? false;
|
|
87
104
|
const engine = detectEngine(config);
|
|
88
105
|
|
|
89
|
-
await ensureDocker(config);
|
|
106
|
+
await ensureDocker(config, undefined);
|
|
90
107
|
p.intro(pc.cyan('Rebuilding sandbox image'));
|
|
91
108
|
|
|
92
109
|
try {
|
|
@@ -8,12 +8,21 @@ import {
|
|
|
8
8
|
reconcileClaudeCredentials,
|
|
9
9
|
redactCommandError,
|
|
10
10
|
validateClaudeCredentialsEnvOverride
|
|
11
|
-
} from '../credentials.
|
|
12
|
-
import { runProbe } from '../shell.
|
|
11
|
+
} from '../credentials.ts';
|
|
12
|
+
import { runProbe } from '../shell.ts';
|
|
13
13
|
|
|
14
14
|
const USAGE = 'Usage: ai sandbox refresh';
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
type ReconcileOptions = NonNullable<Parameters<typeof reconcileClaudeCredentials>[1]>;
|
|
17
|
+
|
|
18
|
+
type RefreshDeps = Partial<ReconcileOptions> & {
|
|
19
|
+
spawnFn?: typeof runProbe;
|
|
20
|
+
discoverFn?: typeof discoverProjects;
|
|
21
|
+
writeStdout?: (chunk: string) => unknown;
|
|
22
|
+
writeStderr?: (chunk: string) => unknown;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function probeClaudeStatus(spawnFn: typeof runProbe = runProbe): { ok: boolean; stderr: string; error: string | null } {
|
|
17
26
|
const result = spawnFn('claude', ['/status'], {
|
|
18
27
|
encoding: 'utf8',
|
|
19
28
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -21,12 +30,12 @@ export function probeClaudeStatus(spawnFn = runProbe) {
|
|
|
21
30
|
});
|
|
22
31
|
return {
|
|
23
32
|
ok: result.status === 0,
|
|
24
|
-
stderr: result.stderr ?? '',
|
|
33
|
+
stderr: result.stderr?.toString() ?? '',
|
|
25
34
|
error: result.error?.message ?? null
|
|
26
35
|
};
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
export async function refresh(args, deps = {}) {
|
|
38
|
+
export async function refresh(args: string[], deps: RefreshDeps = {}): Promise<number> {
|
|
30
39
|
const {
|
|
31
40
|
spawnFn = runProbe,
|
|
32
41
|
execFn,
|
|
@@ -35,8 +44,8 @@ export async function refresh(args, deps = {}) {
|
|
|
35
44
|
writeFn,
|
|
36
45
|
writeHostFn,
|
|
37
46
|
discoverFn = discoverProjects,
|
|
38
|
-
writeStdout = (chunk) => process.stdout.write(chunk),
|
|
39
|
-
writeStderr = (chunk) => process.stderr.write(chunk)
|
|
47
|
+
writeStdout = (chunk: string) => process.stdout.write(chunk),
|
|
48
|
+
writeStderr = (chunk: string) => process.stderr.write(chunk)
|
|
40
49
|
} = deps;
|
|
41
50
|
|
|
42
51
|
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
@@ -3,7 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
|
-
import { loadConfig } from '../config.
|
|
6
|
+
import { loadConfig } from '../config.ts';
|
|
7
|
+
import type { SandboxConfig } from '../config.ts';
|
|
7
8
|
import {
|
|
8
9
|
assertValidBranchName,
|
|
9
10
|
containerNameCandidates,
|
|
@@ -11,19 +12,20 @@ import {
|
|
|
11
12
|
sandboxLabel,
|
|
12
13
|
shareBranchDir,
|
|
13
14
|
worktreeDirCandidates
|
|
14
|
-
} from '../constants.
|
|
15
|
-
import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.
|
|
16
|
-
import { run, runOk, runSafe, runSafeEngine } from '../shell.
|
|
17
|
-
import { resolveTaskBranch } from '../task-resolver.
|
|
18
|
-
import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.
|
|
15
|
+
} from '../constants.ts';
|
|
16
|
+
import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.ts';
|
|
17
|
+
import { run, runOk, runSafe, runSafeEngine } from '../shell.ts';
|
|
18
|
+
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
19
|
+
import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.ts';
|
|
20
|
+
import type { SandboxTool } from '../tools.ts';
|
|
19
21
|
|
|
20
22
|
const USAGE = `Usage: ai sandbox rm <branch> [--all]`;
|
|
21
23
|
|
|
22
|
-
function projectToolDirs(config, tools) {
|
|
24
|
+
function projectToolDirs(config: SandboxConfig, tools: SandboxTool[]): string[] {
|
|
23
25
|
return tools.flatMap((tool) => toolProjectDirCandidates(tool, config.project));
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
export function assertManagedPath(root, target) {
|
|
28
|
+
export function assertManagedPath(root: string, target: string): void {
|
|
27
29
|
const resolvedRoot = path.resolve(root);
|
|
28
30
|
const resolvedTarget = path.resolve(target);
|
|
29
31
|
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
@@ -34,7 +36,7 @@ export function assertManagedPath(root, target) {
|
|
|
34
36
|
throw new Error(`Refusing to remove path outside managed sandbox root: ${target}`);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
async function rmOne(config, tools, branch) {
|
|
39
|
+
async function rmOne(config: SandboxConfig, tools: SandboxTool[], branch: string): Promise<void> {
|
|
38
40
|
assertValidBranchName(branch);
|
|
39
41
|
const engine = detectEngine(config);
|
|
40
42
|
let effectiveBranch = branch;
|
|
@@ -55,7 +57,7 @@ async function rmOne(config, tools, branch) {
|
|
|
55
57
|
'inspect',
|
|
56
58
|
'-f',
|
|
57
59
|
`{{ index .Config.Labels "${sandboxBranchLabel(config)}" }}`,
|
|
58
|
-
matchedContainers[0]
|
|
60
|
+
matchedContainers[0] ?? ''
|
|
59
61
|
]);
|
|
60
62
|
if (resolvedBranch) {
|
|
61
63
|
effectiveBranch = resolvedBranch;
|
|
@@ -136,7 +138,7 @@ async function rmOne(config, tools, branch) {
|
|
|
136
138
|
p.outro(pc.green('Sandbox removed'));
|
|
137
139
|
}
|
|
138
140
|
|
|
139
|
-
async function rmAll(config, tools) {
|
|
141
|
+
async function rmAll(config: SandboxConfig, tools: SandboxTool[]): Promise<void> {
|
|
140
142
|
const engine = detectEngine(config);
|
|
141
143
|
p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
|
|
142
144
|
|
|
@@ -228,7 +230,7 @@ async function rmAll(config, tools) {
|
|
|
228
230
|
p.outro(pc.green('All project sandboxes removed'));
|
|
229
231
|
}
|
|
230
232
|
|
|
231
|
-
export async function rm(args) {
|
|
233
|
+
export async function rm(args: string[]): Promise<void> {
|
|
232
234
|
const { values, positionals } = parseArgs({
|
|
233
235
|
args,
|
|
234
236
|
allowPositionals: true,
|
|
@@ -256,6 +258,6 @@ export async function rm(args) {
|
|
|
256
258
|
return;
|
|
257
259
|
}
|
|
258
260
|
|
|
259
|
-
const branch = resolveTaskBranch(positionals[0], config.repoRoot);
|
|
261
|
+
const branch = resolveTaskBranch(positionals[0] ?? '', config.repoRoot);
|
|
260
262
|
await rmOne(config, tools, branch);
|
|
261
263
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import * as p from '@clack/prompts';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import { loadConfig } from '../config.
|
|
5
|
-
import { parsePositiveIntegerOption } from '../constants.
|
|
4
|
+
import { loadConfig } from '../config.ts';
|
|
5
|
+
import { parsePositiveIntegerOption } from '../constants.ts';
|
|
6
6
|
import {
|
|
7
7
|
ENGINES,
|
|
8
8
|
detectEngine,
|
|
@@ -10,12 +10,12 @@ import {
|
|
|
10
10
|
isManagedEngine,
|
|
11
11
|
startManagedVm,
|
|
12
12
|
stopManagedVm
|
|
13
|
-
} from '../engine.
|
|
14
|
-
import { runOk, runSafe } from '../shell.
|
|
13
|
+
} from '../engine.ts';
|
|
14
|
+
import { runOk, runSafe } from '../shell.ts';
|
|
15
15
|
|
|
16
16
|
const USAGE = `Usage: ai sandbox vm <status|start|stop> [--cpu <n>] [--memory <n>]`;
|
|
17
17
|
|
|
18
|
-
export function ensureManagedVm(engine) {
|
|
18
|
+
export function ensureManagedVm(engine: string): void {
|
|
19
19
|
if (engine === ENGINES.NATIVE) {
|
|
20
20
|
throw new Error(
|
|
21
21
|
"Linux native Docker does not use a managed VM. Use 'ai sandbox create' directly."
|
|
@@ -32,14 +32,17 @@ export function ensureManagedVm(engine) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function wsl2BackendStatus({ runOkFn = runOk } = {}) {
|
|
35
|
+
export function wsl2BackendStatus({ runOkFn = runOk }: { runOkFn?: typeof runOk } = {}): {
|
|
36
|
+
wslAvailable: boolean;
|
|
37
|
+
dockerAvailable: boolean;
|
|
38
|
+
} {
|
|
36
39
|
const wslAvailable = runOkFn('wsl.exe', ['--status']) || runOkFn('wsl.exe', ['--', 'true']);
|
|
37
40
|
const dockerAvailable = wslAvailable && runOkFn('wsl.exe', ['--', 'docker', 'info']);
|
|
38
41
|
|
|
39
42
|
return { wslAvailable, dockerAvailable };
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
function status() {
|
|
45
|
+
function status(): void {
|
|
43
46
|
const config = loadConfig();
|
|
44
47
|
const engine = detectEngine(config);
|
|
45
48
|
const name = engineDisplayName(engine);
|
|
@@ -79,7 +82,7 @@ function status() {
|
|
|
79
82
|
process.stdout.write(`${runSafe('orb', ['status'])}\n`);
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
async function start(args) {
|
|
85
|
+
async function start(args: string[]): Promise<void> {
|
|
83
86
|
const { values } = parseArgs({
|
|
84
87
|
args,
|
|
85
88
|
allowPositionals: true,
|
|
@@ -118,7 +121,7 @@ async function start(args) {
|
|
|
118
121
|
}
|
|
119
122
|
};
|
|
120
123
|
|
|
121
|
-
const onMessage = (detail) => {
|
|
124
|
+
const onMessage = (detail: string) => {
|
|
122
125
|
p.log.info(detail);
|
|
123
126
|
};
|
|
124
127
|
|
|
@@ -126,7 +129,7 @@ async function start(args) {
|
|
|
126
129
|
p.outro(pc.green('VM ready'));
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
function stop() {
|
|
132
|
+
function stop(): void {
|
|
130
133
|
const config = loadConfig();
|
|
131
134
|
const engine = detectEngine(config);
|
|
132
135
|
const name = engineDisplayName(engine);
|
|
@@ -151,7 +154,7 @@ function stop() {
|
|
|
151
154
|
p.outro(pc.green('VM stopped'));
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
export async function vm(args) {
|
|
157
|
+
export async function vm(args: string[]): Promise<void> {
|
|
155
158
|
const [subcommand, ...rest] = args;
|
|
156
159
|
|
|
157
160
|
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
@@ -2,8 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { homedir, platform } from 'node:os';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import { validateSandboxEngine } from './engine.
|
|
6
|
-
import { hostJoin } from './engines/wsl2-paths.
|
|
5
|
+
import { validateSandboxEngine } from './engine.ts';
|
|
6
|
+
import { hostJoin } from './engines/wsl2-paths.ts';
|
|
7
7
|
|
|
8
8
|
const DEFAULTS = Object.freeze({
|
|
9
9
|
engine: null,
|
|
@@ -17,7 +17,47 @@ const DEFAULTS = Object.freeze({
|
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
type PlatformFn = typeof platform;
|
|
21
|
+
|
|
22
|
+
type SandboxConfigInput = {
|
|
23
|
+
engine?: string | null;
|
|
24
|
+
runtimes?: string[];
|
|
25
|
+
tools?: string[];
|
|
26
|
+
dockerfile?: string | null;
|
|
27
|
+
vm?: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type SandboxVmConfig = {
|
|
31
|
+
cpu: number | null;
|
|
32
|
+
memory: number | null;
|
|
33
|
+
disk: number | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type SandboxConfig = {
|
|
37
|
+
repoRoot: string;
|
|
38
|
+
configPath: string;
|
|
39
|
+
project: string;
|
|
40
|
+
org: string;
|
|
41
|
+
home: string;
|
|
42
|
+
containerPrefix: string;
|
|
43
|
+
imageName: string;
|
|
44
|
+
worktreeBase: string;
|
|
45
|
+
shareBase: string;
|
|
46
|
+
dotfilesDir: string;
|
|
47
|
+
engine: string | null;
|
|
48
|
+
runtimes: string[];
|
|
49
|
+
tools: string[];
|
|
50
|
+
dockerfile: string | null;
|
|
51
|
+
vm: SandboxVmConfig;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type AircConfig = {
|
|
55
|
+
project?: unknown;
|
|
56
|
+
org?: unknown;
|
|
57
|
+
sandbox?: SandboxConfigInput;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function detectRepoRoot(): string {
|
|
21
61
|
try {
|
|
22
62
|
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
23
63
|
encoding: 'utf8',
|
|
@@ -28,7 +68,11 @@ function detectRepoRoot() {
|
|
|
28
68
|
}
|
|
29
69
|
}
|
|
30
70
|
|
|
31
|
-
function
|
|
71
|
+
function asPositiveNumberOrNull(value: unknown): number | null {
|
|
72
|
+
return typeof value === 'number' ? value : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cloneDefaults(): SandboxConfigInput & { vm: SandboxVmConfig; runtimes: string[]; tools: string[] } {
|
|
32
76
|
return {
|
|
33
77
|
engine: DEFAULTS.engine,
|
|
34
78
|
runtimes: [...DEFAULTS.runtimes],
|
|
@@ -38,7 +82,7 @@ function cloneDefaults() {
|
|
|
38
82
|
};
|
|
39
83
|
}
|
|
40
84
|
|
|
41
|
-
export function loadConfig({ platformFn = platform } = {}) {
|
|
85
|
+
export function loadConfig({ platformFn = platform }: { platformFn?: PlatformFn } = {}): SandboxConfig {
|
|
42
86
|
const repoRoot = detectRepoRoot();
|
|
43
87
|
const home = homedir();
|
|
44
88
|
|
|
@@ -51,7 +95,7 @@ export function loadConfig({ platformFn = platform } = {}) {
|
|
|
51
95
|
throw new Error('No .agents/.airc.json found. Run "ai init" first.');
|
|
52
96
|
}
|
|
53
97
|
|
|
54
|
-
const airc = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
98
|
+
const airc = JSON.parse(fs.readFileSync(configPath, 'utf8')) as AircConfig;
|
|
55
99
|
const defaults = cloneDefaults();
|
|
56
100
|
const sandbox = airc.sandbox ?? {};
|
|
57
101
|
const engine = validateSandboxEngine(sandbox.engine ?? defaults.engine, { platformFn });
|
|
@@ -65,7 +109,7 @@ export function loadConfig({ platformFn = platform } = {}) {
|
|
|
65
109
|
repoRoot,
|
|
66
110
|
configPath,
|
|
67
111
|
project,
|
|
68
|
-
org: airc.org
|
|
112
|
+
org: typeof airc.org === 'string' ? airc.org : '',
|
|
69
113
|
home,
|
|
70
114
|
containerPrefix: `${project}-dev`,
|
|
71
115
|
imageName: `${project}-sandbox:latest`,
|
|
@@ -79,10 +123,11 @@ export function loadConfig({ platformFn = platform } = {}) {
|
|
|
79
123
|
tools: Array.isArray(sandbox.tools) && sandbox.tools.length > 0
|
|
80
124
|
? [...sandbox.tools]
|
|
81
125
|
: defaults.tools,
|
|
82
|
-
dockerfile: sandbox.dockerfile
|
|
126
|
+
dockerfile: typeof sandbox.dockerfile === 'string' ? sandbox.dockerfile : defaults.dockerfile ?? null,
|
|
83
127
|
vm: {
|
|
84
|
-
|
|
85
|
-
|
|
128
|
+
cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
|
|
129
|
+
memory: asPositiveNumberOrNull(sandbox.vm?.memory) ?? defaults.vm.memory,
|
|
130
|
+
disk: asPositiveNumberOrNull(sandbox.vm?.disk) ?? defaults.vm.disk
|
|
86
131
|
}
|
|
87
132
|
};
|
|
88
133
|
}
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { hostJoin } from './engines/wsl2-paths.
|
|
3
|
+
import { hostJoin } from './engines/wsl2-paths.ts';
|
|
4
4
|
|
|
5
5
|
const validatedBranches = new Set();
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type SandboxPathConfig = {
|
|
8
|
+
project: string;
|
|
9
|
+
containerPrefix: string;
|
|
10
|
+
worktreeBase: string;
|
|
11
|
+
shareBase: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type HostResources = {
|
|
15
|
+
cpu: number;
|
|
16
|
+
memory: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function dedupe(items: string[]): string[] {
|
|
8
20
|
return [...new Set(items)];
|
|
9
21
|
}
|
|
10
22
|
|
|
11
|
-
export function assertValidBranchName(branch) {
|
|
23
|
+
export function assertValidBranchName(branch: string): void {
|
|
12
24
|
if (validatedBranches.has(branch)) {
|
|
13
25
|
return;
|
|
14
26
|
}
|
|
@@ -32,61 +44,61 @@ export function assertValidBranchName(branch) {
|
|
|
32
44
|
validatedBranches.add(branch);
|
|
33
45
|
}
|
|
34
46
|
|
|
35
|
-
export function sanitizeBranchName(branch) {
|
|
47
|
+
export function sanitizeBranchName(branch: string): string {
|
|
36
48
|
assertValidBranchName(branch);
|
|
37
49
|
return branch.replace(/\//g, '..');
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
export function legacySanitizeBranchName(branch) {
|
|
52
|
+
export function legacySanitizeBranchName(branch: string): string {
|
|
41
53
|
assertValidBranchName(branch);
|
|
42
54
|
return branch.replace(/\//g, '-');
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
export function safeNameCandidates(branch) {
|
|
57
|
+
export function safeNameCandidates(branch: string): string[] {
|
|
46
58
|
return dedupe([sanitizeBranchName(branch), legacySanitizeBranchName(branch)]);
|
|
47
59
|
}
|
|
48
60
|
|
|
49
|
-
export function containerName(config, branch) {
|
|
61
|
+
export function containerName(config: Pick<SandboxPathConfig, 'containerPrefix'>, branch: string): string {
|
|
50
62
|
return `${config.containerPrefix}-${sanitizeBranchName(branch)}`;
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
export function containerNameCandidates(config, branch) {
|
|
65
|
+
export function containerNameCandidates(config: Pick<SandboxPathConfig, 'containerPrefix'>, branch: string): string[] {
|
|
54
66
|
return safeNameCandidates(branch).map((name) => `${config.containerPrefix}-${name}`);
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
export function worktreeDir(config, branch) {
|
|
69
|
+
export function worktreeDir(config: Pick<SandboxPathConfig, 'worktreeBase'>, branch: string): string {
|
|
58
70
|
return hostJoin(config.worktreeBase, sanitizeBranchName(branch));
|
|
59
71
|
}
|
|
60
72
|
|
|
61
|
-
export function worktreeDirCandidates(config, branch) {
|
|
73
|
+
export function worktreeDirCandidates(config: Pick<SandboxPathConfig, 'worktreeBase'>, branch: string): string[] {
|
|
62
74
|
return safeNameCandidates(branch).map((name) => hostJoin(config.worktreeBase, name));
|
|
63
75
|
}
|
|
64
76
|
|
|
65
|
-
export function shareDir(config) {
|
|
77
|
+
export function shareDir(config: Pick<SandboxPathConfig, 'shareBase'>): string {
|
|
66
78
|
return config.shareBase;
|
|
67
79
|
}
|
|
68
80
|
|
|
69
|
-
export function shareCommonDir(config) {
|
|
81
|
+
export function shareCommonDir(config: Pick<SandboxPathConfig, 'shareBase'>): string {
|
|
70
82
|
return hostJoin(config.shareBase, 'common');
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
export function shareBranchDir(config, branch) {
|
|
85
|
+
export function shareBranchDir(config: Pick<SandboxPathConfig, 'shareBase'>, branch: string): string {
|
|
74
86
|
return hostJoin(config.shareBase, 'branches', sanitizeBranchName(branch));
|
|
75
87
|
}
|
|
76
88
|
|
|
77
|
-
export function sandboxLabel(config) {
|
|
89
|
+
export function sandboxLabel(config: Pick<SandboxPathConfig, 'project'>): string {
|
|
78
90
|
return `${config.project}.sandbox`;
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
export function sandboxBranchLabel(config) {
|
|
93
|
+
export function sandboxBranchLabel(config: Pick<SandboxPathConfig, 'project'>): string {
|
|
82
94
|
return `${sandboxLabel(config)}.branch`;
|
|
83
95
|
}
|
|
84
96
|
|
|
85
|
-
export function sandboxImageConfigLabel(config) {
|
|
97
|
+
export function sandboxImageConfigLabel(config: Pick<SandboxPathConfig, 'project'>): string {
|
|
86
98
|
return `${sandboxLabel(config)}.image-config`;
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
export function parsePositiveIntegerOption(value, optionName) {
|
|
101
|
+
export function parsePositiveIntegerOption(value: unknown, optionName: string): number | undefined {
|
|
90
102
|
if (value === undefined || value === null) {
|
|
91
103
|
return undefined;
|
|
92
104
|
}
|
|
@@ -99,7 +111,7 @@ export function parsePositiveIntegerOption(value, optionName) {
|
|
|
99
111
|
return parsed;
|
|
100
112
|
}
|
|
101
113
|
|
|
102
|
-
export function detectHostResources() {
|
|
114
|
+
export function detectHostResources(): HostResources {
|
|
103
115
|
// Resource hints are for engines that pre-allocate a managed VM. macOS uses
|
|
104
116
|
// sysctl for Colima defaults, while the generic fallback supports WSL2 or
|
|
105
117
|
// other direct callers that need conservative CPU and memory defaults.
|