@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.
Files changed (82) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/bin/{cli.js → cli.ts} +21 -17
  4. package/dist/bin/cli.js +116 -0
  5. package/dist/lib/defaults.json +61 -0
  6. package/dist/lib/init.js +238 -0
  7. package/dist/lib/log.js +18 -0
  8. package/dist/lib/merge.js +747 -0
  9. package/dist/lib/paths.js +18 -0
  10. package/dist/lib/prompt.js +85 -0
  11. package/dist/lib/render.js +139 -0
  12. package/dist/lib/sandbox/commands/create.js +1173 -0
  13. package/dist/lib/sandbox/commands/enter.js +98 -0
  14. package/dist/lib/sandbox/commands/ls.js +93 -0
  15. package/dist/lib/sandbox/commands/rebuild.js +101 -0
  16. package/dist/lib/sandbox/commands/refresh.js +85 -0
  17. package/dist/lib/sandbox/commands/rm.js +226 -0
  18. package/dist/lib/sandbox/commands/vm.js +144 -0
  19. package/dist/lib/sandbox/config.js +85 -0
  20. package/dist/lib/sandbox/constants.js +104 -0
  21. package/dist/lib/sandbox/credentials.js +437 -0
  22. package/dist/lib/sandbox/dockerfile.js +76 -0
  23. package/dist/lib/sandbox/dotfiles.js +170 -0
  24. package/dist/lib/sandbox/engine.js +155 -0
  25. package/dist/lib/sandbox/engines/colima.js +64 -0
  26. package/dist/lib/sandbox/engines/docker-desktop.js +27 -0
  27. package/dist/lib/sandbox/engines/index.js +25 -0
  28. package/dist/lib/sandbox/engines/native.js +96 -0
  29. package/dist/lib/sandbox/engines/orbstack.js +63 -0
  30. package/dist/lib/sandbox/engines/selinux.js +48 -0
  31. package/dist/lib/sandbox/engines/wsl2-paths.js +47 -0
  32. package/dist/lib/sandbox/engines/wsl2.js +57 -0
  33. package/dist/lib/sandbox/index.js +70 -0
  34. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +39 -0
  35. package/dist/lib/sandbox/runtimes/base.dockerfile +178 -0
  36. package/dist/lib/sandbox/runtimes/java17.dockerfile +3 -0
  37. package/dist/lib/sandbox/runtimes/java21.dockerfile +3 -0
  38. package/dist/lib/sandbox/runtimes/node20.dockerfile +3 -0
  39. package/dist/lib/sandbox/runtimes/node22.dockerfile +3 -0
  40. package/dist/lib/sandbox/runtimes/python3.dockerfile +3 -0
  41. package/dist/lib/sandbox/shell.js +148 -0
  42. package/dist/lib/sandbox/task-resolver.js +35 -0
  43. package/dist/lib/sandbox/tools.js +115 -0
  44. package/dist/lib/update.js +186 -0
  45. package/dist/lib/version.js +5 -0
  46. package/dist/package.json +5 -0
  47. package/lib/{init.js → init.ts} +48 -18
  48. package/lib/{log.js → log.ts} +4 -4
  49. package/lib/{merge.js → merge.ts} +129 -63
  50. package/lib/paths.ts +18 -0
  51. package/lib/{prompt.js → prompt.ts} +12 -12
  52. package/lib/{render.js → render.ts} +30 -17
  53. package/lib/sandbox/commands/{create.js → create.ts} +224 -118
  54. package/lib/sandbox/commands/{enter.js → enter.ts} +17 -14
  55. package/lib/sandbox/commands/{ls.js → ls.ts} +10 -10
  56. package/lib/sandbox/commands/{rebuild.js → rebuild.ts} +38 -21
  57. package/lib/sandbox/commands/{refresh.js → refresh.ts} +16 -7
  58. package/lib/sandbox/commands/{rm.js → rm.ts} +15 -13
  59. package/lib/sandbox/commands/{vm.js → vm.ts} +14 -11
  60. package/lib/sandbox/{config.js → config.ts} +55 -10
  61. package/lib/sandbox/{constants.js → constants.ts} +30 -18
  62. package/lib/sandbox/{credentials.js → credentials.ts} +160 -46
  63. package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
  64. package/lib/sandbox/{dotfiles.js → dotfiles.ts} +66 -19
  65. package/lib/sandbox/{engine.js → engine.ts} +57 -25
  66. package/lib/sandbox/engines/{colima.js → colima.ts} +9 -7
  67. package/lib/sandbox/engines/{docker-desktop.js → docker-desktop.ts} +5 -3
  68. package/lib/sandbox/engines/index.ts +74 -0
  69. package/lib/sandbox/engines/{native.js → native.ts} +25 -6
  70. package/lib/sandbox/engines/{orbstack.js → orbstack.ts} +7 -5
  71. package/lib/sandbox/engines/{selinux.js → selinux.ts} +11 -5
  72. package/lib/sandbox/engines/{wsl2-paths.js → wsl2-paths.ts} +15 -9
  73. package/lib/sandbox/engines/{wsl2.js → wsl2.ts} +9 -7
  74. package/lib/sandbox/{index.js → index.ts} +8 -8
  75. package/lib/sandbox/{shell.js → shell.ts} +30 -17
  76. package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
  77. package/lib/sandbox/{tools.js → tools.ts} +30 -26
  78. package/lib/{update.js → update.ts} +33 -10
  79. package/package.json +17 -9
  80. package/lib/paths.js +0 -9
  81. package/lib/sandbox/engines/index.js +0 -27
  82. /package/lib/{version.js → version.ts} +0 -0
@@ -1,16 +1,16 @@
1
- import { loadConfig } from '../config.js';
2
- import { assertValidBranchName, containerNameCandidates } from '../constants.js';
3
- import { detectEngine } from '../engine.js';
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.js';
11
- import { runInteractiveEngine, runSafeEngine } from '../shell.js';
12
- import { resolveTaskBranch } from '../task-resolver.js';
13
- import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
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(result, isTTY = process.stderr.isTTY) {
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?.message ?? 'unknown error')}\n`);
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?.message ?? 'unknown error')}\n`);
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.js';
6
- import { sandboxBranchLabel, sandboxLabel } from '../constants.js';
7
- import { detectEngine } from '../engine.js';
8
- import { runSafeEngine } from '../shell.js';
9
- import { resolveTools, toolProjectDirCandidates } from '../tools.js';
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.js';
6
- import { prepareDockerfile } from '../dockerfile.js';
7
- import { sandboxImageConfigLabel, sandboxLabel } from '../constants.js';
8
- import { detectEngine, ensureDocker } from '../engine.js';
9
- import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shell.js';
10
- import { resolveTools, toolNpmPackagesArg } from '../tools.js';
11
- import { toEnginePath } from '../engines/wsl2-paths.js';
12
- import { resolveBuildUid } from '../engines/native.js';
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
- function buildSignature(preparedDockerfile, tools) {
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
- { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env } = {}
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(engine, dockerfilePath),
56
- toEnginePath(engine, config.repoRoot)
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.js';
12
- import { runProbe } from '../shell.js';
11
+ } from '../credentials.ts';
12
+ import { runProbe } from '../shell.ts';
13
13
 
14
14
  const USAGE = 'Usage: ai sandbox refresh';
15
15
 
16
- export function probeClaudeStatus(spawnFn = runProbe) {
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.js';
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.js';
15
- import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.js';
16
- import { run, runOk, runSafe, runSafeEngine } from '../shell.js';
17
- import { resolveTaskBranch } from '../task-resolver.js';
18
- import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.js';
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.js';
5
- import { parsePositiveIntegerOption } from '../constants.js';
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.js';
14
- import { runOk, runSafe } from '../shell.js';
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.js';
6
- import { hostJoin } from './engines/wsl2-paths.js';
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
- function detectRepoRoot() {
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 cloneDefaults() {
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 ?? defaults.dockerfile,
126
+ dockerfile: typeof sandbox.dockerfile === 'string' ? sandbox.dockerfile : defaults.dockerfile ?? null,
83
127
  vm: {
84
- ...defaults.vm,
85
- ...(sandbox.vm ?? {})
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.js';
3
+ import { hostJoin } from './engines/wsl2-paths.ts';
4
4
 
5
5
  const validatedBranches = new Set();
6
6
 
7
- function dedupe(items) {
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.