@fitlab-ai/agent-infra 0.6.2 → 0.6.3

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 (75) hide show
  1. package/README.md +13 -3
  2. package/README.zh-CN.md +10 -3
  3. package/bin/cli.ts +6 -1
  4. package/dist/bin/cli.js +6 -1
  5. package/dist/lib/sandbox/clipboard/bridge.js +216 -0
  6. package/dist/lib/sandbox/clipboard/darwin.js +73 -0
  7. package/dist/lib/sandbox/clipboard/index.js +9 -0
  8. package/dist/lib/sandbox/clipboard/keys.js +58 -0
  9. package/dist/lib/sandbox/clipboard/node-pty.js +13 -0
  10. package/dist/lib/sandbox/clipboard/paths.js +59 -0
  11. package/dist/lib/sandbox/commands/create.js +11 -2
  12. package/dist/lib/sandbox/commands/enter.js +8 -2
  13. package/dist/lib/sandbox/commands/ls.js +19 -4
  14. package/dist/lib/sandbox/commands/prune.js +176 -0
  15. package/dist/lib/sandbox/commands/rm.js +27 -33
  16. package/dist/lib/sandbox/config.js +1 -0
  17. package/dist/lib/sandbox/constants.js +6 -0
  18. package/dist/lib/sandbox/index.js +7 -1
  19. package/dist/lib/sandbox/managed-fs.js +25 -0
  20. package/dist/lib/sandbox/tools.js +1 -1
  21. package/dist/lib/version.js +9 -2
  22. package/lib/sandbox/clipboard/bridge.ts +285 -0
  23. package/lib/sandbox/clipboard/darwin.ts +90 -0
  24. package/lib/sandbox/clipboard/index.ts +13 -0
  25. package/lib/sandbox/clipboard/keys.ts +78 -0
  26. package/lib/sandbox/clipboard/node-pty.d.ts +19 -0
  27. package/lib/sandbox/clipboard/node-pty.ts +34 -0
  28. package/lib/sandbox/clipboard/paths.ts +71 -0
  29. package/lib/sandbox/commands/create.ts +15 -2
  30. package/lib/sandbox/commands/enter.ts +8 -2
  31. package/lib/sandbox/commands/ls.ts +28 -4
  32. package/lib/sandbox/commands/prune.ts +211 -0
  33. package/lib/sandbox/commands/rm.ts +30 -32
  34. package/lib/sandbox/config.ts +2 -0
  35. package/lib/sandbox/constants.ts +9 -0
  36. package/lib/sandbox/index.ts +7 -1
  37. package/lib/sandbox/managed-fs.ts +27 -0
  38. package/lib/sandbox/tools.ts +1 -1
  39. package/lib/version.ts +11 -4
  40. package/package.json +5 -1
  41. package/templates/.agents/README.en.md +19 -0
  42. package/templates/.agents/README.zh-CN.md +19 -0
  43. package/templates/.agents/skills/analyze-task/SKILL.en.md +26 -0
  44. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +26 -0
  45. package/templates/.agents/skills/analyze-task/config/verify.en.json +51 -0
  46. package/templates/.agents/skills/analyze-task/config/{verify.json → verify.zh-CN.json} +6 -2
  47. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -0
  48. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +15 -0
  49. package/templates/.agents/skills/complete-task/config/{verify.json → verify.en.json} +10 -0
  50. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +48 -0
  51. package/templates/.agents/skills/implement-task/SKILL.en.md +14 -0
  52. package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +14 -0
  53. package/templates/.agents/skills/implement-task/config/verify.en.json +51 -0
  54. package/templates/.agents/skills/implement-task/config/{verify.json → verify.zh-CN.json} +7 -2
  55. package/templates/.agents/skills/implement-task/reference/report-template.en.md +15 -0
  56. package/templates/.agents/skills/implement-task/reference/report-template.zh-CN.md +15 -0
  57. package/templates/.agents/skills/plan-task/SKILL.en.md +22 -0
  58. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +22 -0
  59. package/templates/.agents/skills/plan-task/config/verify.en.json +52 -0
  60. package/templates/.agents/skills/plan-task/config/{verify.json → verify.zh-CN.json} +6 -2
  61. package/templates/.agents/skills/post-release/SKILL.en.md +1 -0
  62. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +1 -0
  63. package/templates/.agents/skills/refine-task/SKILL.en.md +14 -0
  64. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +14 -0
  65. package/templates/.agents/skills/refine-task/config/verify.en.json +47 -0
  66. package/templates/.agents/skills/refine-task/config/{verify.json → verify.zh-CN.json} +7 -2
  67. package/templates/.agents/skills/refine-task/reference/report-template.en.md +15 -0
  68. package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +15 -0
  69. package/templates/.agents/skills/review-task/SKILL.en.md +14 -0
  70. package/templates/.agents/skills/review-task/SKILL.zh-CN.md +14 -0
  71. package/templates/.agents/skills/review-task/config/verify.en.json +50 -0
  72. package/templates/.agents/skills/review-task/config/{verify.json → verify.zh-CN.json} +5 -2
  73. package/templates/.agents/skills/review-task/reference/report-template.en.md +15 -0
  74. package/templates/.agents/skills/review-task/reference/report-template.zh-CN.md +15 -0
  75. package/dist/package.json +0 -5
@@ -8,7 +8,7 @@ import { detectEngine } from "../engine.js";
8
8
  import { runSafeEngine } from "../shell.js";
9
9
  import { resolveTools, toolProjectDirCandidates } from "../tools.js";
10
10
  const USAGE = 'Usage: ai sandbox ls';
11
- const CONTAINER_LIST_HEADER = 'NAMES\tSTATUS\tBRANCH';
11
+ const CONTAINER_TABLE_HEADERS = ['NAMES', 'STATUS', 'BRANCH'];
12
12
  // Exported to lock the docker/podman-compatible format in unit tests.
13
13
  export function containerListFormat() {
14
14
  return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
@@ -30,6 +30,19 @@ export function parseLabels(csv) {
30
30
  }
31
31
  return labels;
32
32
  }
33
+ export function formatContainerTable(rows) {
34
+ const columns = rows.map((row) => [row.name, row.status, row.branch]);
35
+ const widths = [
36
+ Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.name.length)),
37
+ Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.status.length)),
38
+ Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.branch.length))
39
+ ];
40
+ const renderRow = (values) => `${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2]}`.trimEnd();
41
+ return [
42
+ renderRow(CONTAINER_TABLE_HEADERS),
43
+ ...columns.map((column) => renderRow(column))
44
+ ];
45
+ }
33
46
  function listChildren(dir) {
34
47
  if (!fs.existsSync(dir)) {
35
48
  return [];
@@ -60,11 +73,13 @@ export function ls(args = []) {
60
73
  }
61
74
  else {
62
75
  const branchKey = sandboxBranchLabel(config);
63
- process.stdout.write(` ${CONTAINER_LIST_HEADER}\n`);
64
- for (const line of containers.split('\n')) {
76
+ const rows = containers.split('\n').map((line) => {
65
77
  const [name = '', status = '', labelsCsv = ''] = line.split('\t');
66
78
  const branch = parseLabels(labelsCsv)[branchKey] ?? '';
67
- process.stdout.write(` ${name}\t${status}\t${branch}\n`);
79
+ return { name, status, branch };
80
+ });
81
+ for (const line of formatContainerTable(rows)) {
82
+ process.stdout.write(` ${line}\n`);
68
83
  }
69
84
  }
70
85
  p.log.step('Worktrees');
@@ -0,0 +1,176 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import * as p from '@clack/prompts';
5
+ import pc from 'picocolors';
6
+ import { loadConfig } from "../config.js";
7
+ import { safeNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
8
+ import { detectEngine } from "../engine.js";
9
+ import { hostJoin } from "../engines/wsl2-paths.js";
10
+ import { removeManagedDir, removeWorktreeDir } from "../managed-fs.js";
11
+ import { parseLabels } from "./ls.js";
12
+ import { runEngine, runSafe } from "../shell.js";
13
+ import { resolveTools } from "../tools.js";
14
+ const USAGE = `Usage: ai sandbox prune [--dry-run]`;
15
+ function listChildDirs(base) {
16
+ if (!fs.existsSync(base)) {
17
+ return [];
18
+ }
19
+ return fs.readdirSync(base)
20
+ .sort()
21
+ .map((entry) => path.join(base, entry))
22
+ .filter((entry) => {
23
+ try {
24
+ return fs.statSync(entry).isDirectory();
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ });
30
+ }
31
+ function activeSafeNames(activeBranches) {
32
+ const names = new Set();
33
+ for (const branch of activeBranches) {
34
+ try {
35
+ for (const name of safeNameCandidates(branch)) {
36
+ names.add(name);
37
+ }
38
+ }
39
+ catch {
40
+ names.add(branch);
41
+ }
42
+ }
43
+ return names;
44
+ }
45
+ function orphanDirs(base, activeNames) {
46
+ return listChildDirs(base).filter((dir) => !activeNames.has(path.basename(dir)));
47
+ }
48
+ function addGroup(groups, group) {
49
+ if (group.dirs.length > 0) {
50
+ groups.push(group);
51
+ }
52
+ }
53
+ export function collectOrphanGroups(config, tools, activeBranches) {
54
+ const activeNames = activeSafeNames(activeBranches);
55
+ const groups = [];
56
+ const shareBranchesBase = hostJoin(config.shareBase, 'branches');
57
+ addGroup(groups, {
58
+ kind: 'shell',
59
+ label: 'Shell config dirs',
60
+ base: config.shellConfigBase,
61
+ dirs: orphanDirs(config.shellConfigBase, activeNames)
62
+ });
63
+ addGroup(groups, {
64
+ kind: 'worktree',
65
+ label: 'Worktrees',
66
+ base: config.worktreeBase,
67
+ dirs: orphanDirs(config.worktreeBase, activeNames)
68
+ });
69
+ addGroup(groups, {
70
+ kind: 'share',
71
+ label: 'Share branch dirs',
72
+ base: shareBranchesBase,
73
+ dirs: orphanDirs(shareBranchesBase, activeNames)
74
+ });
75
+ for (const tool of tools) {
76
+ const base = hostJoin(tool.sandboxBase, config.project);
77
+ addGroup(groups, {
78
+ kind: 'tool',
79
+ label: `${tool.name} state`,
80
+ base,
81
+ dirs: orphanDirs(base, activeNames)
82
+ });
83
+ }
84
+ return groups;
85
+ }
86
+ export function removeOrphanGroups(config, groups) {
87
+ let removedWorktrees = false;
88
+ for (const group of groups) {
89
+ for (const dir of group.dirs) {
90
+ if (group.kind === 'worktree') {
91
+ removeWorktreeDir(config.repoRoot, group.base, dir);
92
+ removedWorktrees = true;
93
+ }
94
+ else {
95
+ removeManagedDir(group.base, dir);
96
+ }
97
+ }
98
+ }
99
+ return removedWorktrees;
100
+ }
101
+ function activeBranchesFromLabels(config, labelsOutput) {
102
+ const branchKey = sandboxBranchLabel(config);
103
+ return labelsOutput.split('\n')
104
+ .map((line) => parseLabels(line)[branchKey] ?? '')
105
+ .filter(Boolean);
106
+ }
107
+ function orphanCount(groups) {
108
+ return groups.reduce((sum, group) => sum + group.dirs.length, 0);
109
+ }
110
+ function writeGroups(groups) {
111
+ for (const group of groups) {
112
+ p.log.step(group.label);
113
+ for (const dir of group.dirs) {
114
+ process.stdout.write(` ${dir}\n`);
115
+ }
116
+ }
117
+ }
118
+ export async function prune(args) {
119
+ const { values } = parseArgs({
120
+ args,
121
+ strict: true,
122
+ options: {
123
+ 'dry-run': { type: 'boolean' },
124
+ help: { type: 'boolean', short: 'h' }
125
+ }
126
+ });
127
+ if (values.help) {
128
+ process.stdout.write(`${USAGE}\n`);
129
+ return;
130
+ }
131
+ const config = loadConfig();
132
+ const tools = resolveTools(config);
133
+ const engine = detectEngine(config);
134
+ const psArgs = [
135
+ 'ps',
136
+ '-a',
137
+ '--filter',
138
+ `label=${sandboxLabel(config)}`,
139
+ '--format',
140
+ '{{.Labels}}'
141
+ ];
142
+ let labelsOutput;
143
+ try {
144
+ labelsOutput = runEngine(engine, 'docker', psArgs);
145
+ }
146
+ catch {
147
+ throw new Error('Unable to determine active sandbox branches: docker ps failed');
148
+ }
149
+ const groups = collectOrphanGroups(config, tools, activeBranchesFromLabels(config, labelsOutput));
150
+ const count = orphanCount(groups);
151
+ p.intro(pc.cyan(`Pruning orphaned sandbox state for ${config.project}`));
152
+ if (count === 0) {
153
+ p.log.success('No orphaned sandbox state dirs found');
154
+ p.outro(pc.green('Sandbox prune complete'));
155
+ return;
156
+ }
157
+ writeGroups(groups);
158
+ if (values['dry-run']) {
159
+ p.outro(pc.green('Dry run complete'));
160
+ return;
161
+ }
162
+ const shouldRemove = await p.confirm({
163
+ message: `Remove ${count} orphaned sandbox state dirs?`,
164
+ initialValue: true
165
+ });
166
+ if (p.isCancel(shouldRemove) || !shouldRemove) {
167
+ p.outro('Cancelled');
168
+ return;
169
+ }
170
+ const removedWorktrees = removeOrphanGroups(config, groups);
171
+ if (removedWorktrees) {
172
+ runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
173
+ }
174
+ p.outro(pc.green('Orphaned sandbox state dirs removed'));
175
+ }
176
+ //# sourceMappingURL=prune.js.map
@@ -4,24 +4,17 @@ import { parseArgs } from 'node:util';
4
4
  import * as p from '@clack/prompts';
5
5
  import pc from 'picocolors';
6
6
  import { loadConfig } from "../config.js";
7
- import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel, shareBranchDir, worktreeDirCandidates } from "../constants.js";
7
+ import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel, shareBranchDir, shellConfigDirCandidates, worktreeDirCandidates } from "../constants.js";
8
8
  import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from "../engine.js";
9
- import { run, runOk, runSafe, runSafeEngine } from "../shell.js";
9
+ import { removeManagedDir, removeWorktreeDir } from "../managed-fs.js";
10
+ import { runOk, runSafe, runSafeEngine } from "../shell.js";
10
11
  import { resolveTaskBranch } from "../task-resolver.js";
11
12
  import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from "../tools.js";
12
13
  const USAGE = `Usage: ai sandbox rm <branch> [--all]`;
14
+ export { assertManagedPath } from "../managed-fs.js";
13
15
  function projectToolDirs(config, tools) {
14
16
  return tools.flatMap((tool) => toolProjectDirCandidates(tool, config.project));
15
17
  }
16
- export function assertManagedPath(root, target) {
17
- const resolvedRoot = path.resolve(root);
18
- const resolvedTarget = path.resolve(target);
19
- const relative = path.relative(resolvedRoot, resolvedTarget);
20
- if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
21
- return;
22
- }
23
- throw new Error(`Refusing to remove path outside managed sandbox root: ${target}`);
24
- }
25
18
  async function rmOne(config, tools, branch) {
26
19
  assertValidBranchName(branch);
27
20
  const engine = detectEngine(config);
@@ -73,13 +66,7 @@ async function rmOne(config, tools, branch) {
73
66
  }
74
67
  if (shouldRemoveWorktree) {
75
68
  for (const worktree of existingWorktrees) {
76
- try {
77
- run('git', ['-C', config.repoRoot, 'worktree', 'remove', worktree, '--force']);
78
- }
79
- catch {
80
- assertManagedPath(config.worktreeBase, worktree);
81
- fs.rmSync(worktree, { recursive: true, force: true });
82
- }
69
+ removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree);
83
70
  }
84
71
  const shouldDeleteBranch = await p.confirm({
85
72
  message: `Also delete local branch '${effectiveBranch}'?`,
@@ -94,11 +81,14 @@ async function rmOne(config, tools, branch) {
94
81
  }
95
82
  for (const { tool, candidates } of toolCandidates) {
96
83
  for (const dir of candidates.filter((candidate) => fs.existsSync(candidate))) {
97
- assertManagedPath(tool.sandboxBase, dir);
98
- fs.rmSync(dir, { recursive: true, force: true });
84
+ removeManagedDir(tool.sandboxBase, dir);
99
85
  p.log.success(`${tool.name} state removed: ${dir}`);
100
86
  }
101
87
  }
88
+ for (const dir of shellConfigDirCandidates(config, effectiveBranch).filter((candidate) => fs.existsSync(candidate))) {
89
+ removeManagedDir(config.shellConfigBase, dir);
90
+ p.log.success(`Shell config removed: ${dir}`);
91
+ }
102
92
  const shareBranch = shareBranchDir(config, effectiveBranch);
103
93
  if (fs.existsSync(shareBranch)) {
104
94
  const shouldRemoveShare = await p.confirm({
@@ -106,8 +96,7 @@ async function rmOne(config, tools, branch) {
106
96
  initialValue: true
107
97
  });
108
98
  if (!p.isCancel(shouldRemoveShare) && shouldRemoveShare) {
109
- assertManagedPath(config.shareBase, shareBranch);
110
- fs.rmSync(shareBranch, { recursive: true, force: true });
99
+ removeManagedDir(config.shareBase, shareBranch);
111
100
  p.log.success(`Share dir removed: ${shareBranch}`);
112
101
  }
113
102
  }
@@ -144,32 +133,37 @@ async function rmAll(config, tools) {
144
133
  if (!p.isCancel(shouldRemoveWorktrees) && shouldRemoveWorktrees) {
145
134
  for (const entry of fs.readdirSync(config.worktreeBase)) {
146
135
  const dir = path.join(config.worktreeBase, entry);
147
- try {
148
- run('git', ['-C', config.repoRoot, 'worktree', 'remove', dir, '--force']);
149
- }
150
- catch {
151
- assertManagedPath(config.worktreeBase, dir);
152
- fs.rmSync(dir, { recursive: true, force: true });
153
- }
136
+ removeWorktreeDir(config.repoRoot, config.worktreeBase, dir);
154
137
  }
155
138
  runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
156
139
  }
157
140
  }
158
141
  for (const dir of projectToolDirs(config, tools)) {
159
142
  if (fs.existsSync(dir)) {
160
- assertManagedPath(path.dirname(dir), dir);
161
- fs.rmSync(dir, { recursive: true, force: true });
143
+ removeManagedDir(path.dirname(dir), dir);
162
144
  p.log.success(`Removed tool state: ${dir}`);
163
145
  }
164
146
  }
147
+ if (fs.existsSync(config.shellConfigBase) && fs.readdirSync(config.shellConfigBase).length > 0) {
148
+ const shouldRemoveShellConfigs = await p.confirm({
149
+ message: `Remove all shell config dirs in ${config.shellConfigBase}?`,
150
+ initialValue: true
151
+ });
152
+ if (!p.isCancel(shouldRemoveShellConfigs) && shouldRemoveShellConfigs) {
153
+ for (const entry of fs.readdirSync(config.shellConfigBase)) {
154
+ const dir = path.join(config.shellConfigBase, entry);
155
+ removeManagedDir(config.shellConfigBase, dir);
156
+ }
157
+ p.log.success(`Project shell config dirs removed: ${config.shellConfigBase}`);
158
+ }
159
+ }
165
160
  if (fs.existsSync(config.shareBase) && fs.readdirSync(config.shareBase).length > 0) {
166
161
  const shouldRemoveAllShares = await p.confirm({
167
162
  message: `Remove all share dirs for project (${config.shareBase})?`,
168
163
  initialValue: true
169
164
  });
170
165
  if (!p.isCancel(shouldRemoveAllShares) && shouldRemoveAllShares) {
171
- assertManagedPath(path.dirname(config.shareBase), config.shareBase);
172
- fs.rmSync(config.shareBase, { recursive: true, force: true });
166
+ removeManagedDir(path.dirname(config.shareBase), config.shareBase);
173
167
  p.log.success(`Project share dirs removed: ${config.shareBase}`);
174
168
  }
175
169
  }
@@ -86,6 +86,7 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
86
86
  imageName: `${project}-sandbox:latest`,
87
87
  worktreeBase: hostJoin(home, '.agent-infra', 'worktrees', project),
88
88
  shareBase: hostJoin(home, '.agent-infra', 'share', project),
89
+ shellConfigBase: hostJoin(home, '.agent-infra', 'config', project),
89
90
  dotfilesDir: hostJoin(home, '.agent-infra', 'dotfiles'),
90
91
  engine,
91
92
  runtimes,
@@ -57,6 +57,12 @@ export function shareCommonDir(config) {
57
57
  export function shareBranchDir(config, branch) {
58
58
  return hostJoin(config.shareBase, 'branches', sanitizeBranchName(branch));
59
59
  }
60
+ export function shellConfigDir(config, branch) {
61
+ return hostJoin(config.shellConfigBase, sanitizeBranchName(branch));
62
+ }
63
+ export function shellConfigDirCandidates(config, branch) {
64
+ return safeNameCandidates(branch).map((name) => hostJoin(config.shellConfigBase, name));
65
+ }
60
66
  export function sandboxLabel(config) {
61
67
  return `${config.project}.sandbox`;
62
68
  }
@@ -6,6 +6,7 @@ Commands:
6
6
  refresh Sync host Claude Code credentials to all sandbox copies
7
7
  ls List sandboxes for the current project
8
8
  rm <branch> [--all] Remove a sandbox or all sandboxes
9
+ prune [--dry-run] Remove orphaned per-branch state dirs
9
10
  vm status|start|stop Manage the sandbox VM (macOS) or check the backend (Windows)
10
11
  rebuild [--quiet] Rebuild the sandbox image
11
12
 
@@ -29,7 +30,7 @@ export async function runSandbox(args) {
29
30
  }
30
31
  case 'exec': {
31
32
  const { enter } = await import("./commands/enter.js");
32
- const exitCode = enter(rest);
33
+ const exitCode = await enter(rest);
33
34
  if (typeof exitCode === 'number' && exitCode !== 0) {
34
35
  process.exitCode = exitCode;
35
36
  }
@@ -53,6 +54,11 @@ export async function runSandbox(args) {
53
54
  await rm(rest);
54
55
  break;
55
56
  }
57
+ case 'prune': {
58
+ const { prune } = await import("./commands/prune.js");
59
+ await prune(rest);
60
+ break;
61
+ }
56
62
  case 'vm': {
57
63
  const { vm } = await import("./commands/vm.js");
58
64
  await vm(rest);
@@ -0,0 +1,25 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { run } from "./shell.js";
4
+ export function assertManagedPath(root, target) {
5
+ const resolvedRoot = path.resolve(root);
6
+ const resolvedTarget = path.resolve(target);
7
+ const relative = path.relative(resolvedRoot, resolvedTarget);
8
+ if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
9
+ return;
10
+ }
11
+ throw new Error(`Refusing to remove path outside managed sandbox root: ${target}`);
12
+ }
13
+ export function removeManagedDir(root, dir) {
14
+ assertManagedPath(root, dir);
15
+ fs.rmSync(dir, { recursive: true, force: true });
16
+ }
17
+ export function removeWorktreeDir(repoRoot, worktreeBase, dir) {
18
+ try {
19
+ run('git', ['-C', repoRoot, 'worktree', 'remove', dir, '--force']);
20
+ }
21
+ catch {
22
+ removeManagedDir(worktreeBase, dir);
23
+ }
24
+ }
25
+ //# sourceMappingURL=managed-fs.js.map
@@ -5,7 +5,7 @@ function createBuiltinTools(home, project) {
5
5
  'claude-code': {
6
6
  id: 'claude-code',
7
7
  name: 'Claude Code',
8
- npmPackage: '@anthropic-ai/claude-code',
8
+ npmPackage: '@anthropic-ai/claude-code@stable',
9
9
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
10
10
  containerMount: '/home/devuser/.claude',
11
11
  versionCmd: 'claude --version',
@@ -1,5 +1,12 @@
1
- import { readFileSync } from 'node:fs';
2
- const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ const packageJsonUrl = [
3
+ new URL('../package.json', import.meta.url),
4
+ new URL('../../package.json', import.meta.url),
5
+ ].find((url) => existsSync(url));
6
+ if (!packageJsonUrl) {
7
+ throw new Error('Unable to locate package.json for agent-infra version');
8
+ }
9
+ const { version } = JSON.parse(readFileSync(packageJsonUrl, 'utf8'));
3
10
  const VERSION = `v${version}`;
4
11
  export { VERSION };
5
12
  //# sourceMappingURL=version.js.map