@fitlab-ai/agent-infra 0.6.2 → 0.6.4
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 +13 -3
- package/README.zh-CN.md +10 -3
- package/bin/cli.ts +6 -1
- package/dist/bin/cli.js +6 -1
- package/dist/lib/sandbox/clipboard/bridge.js +218 -0
- package/dist/lib/sandbox/clipboard/darwin.js +66 -0
- package/dist/lib/sandbox/clipboard/index.js +9 -0
- package/dist/lib/sandbox/clipboard/keys.js +58 -0
- package/dist/lib/sandbox/clipboard/node-pty.js +13 -0
- package/dist/lib/sandbox/clipboard/paths.js +59 -0
- package/dist/lib/sandbox/commands/create.js +15 -2
- package/dist/lib/sandbox/commands/enter.js +14 -3
- package/dist/lib/sandbox/commands/ls.js +19 -4
- package/dist/lib/sandbox/commands/prune.js +176 -0
- package/dist/lib/sandbox/commands/rm.js +27 -33
- package/dist/lib/sandbox/config.js +1 -0
- package/dist/lib/sandbox/constants.js +6 -0
- package/dist/lib/sandbox/host-timezone.js +33 -0
- package/dist/lib/sandbox/index.js +7 -1
- package/dist/lib/sandbox/managed-fs.js +25 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +21 -16
- package/dist/lib/sandbox/tools.js +1 -1
- package/dist/lib/version.js +9 -2
- package/lib/sandbox/clipboard/bridge.ts +286 -0
- package/lib/sandbox/clipboard/darwin.ts +91 -0
- package/lib/sandbox/clipboard/index.ts +13 -0
- package/lib/sandbox/clipboard/keys.ts +78 -0
- package/lib/sandbox/clipboard/node-pty.d.ts +19 -0
- package/lib/sandbox/clipboard/node-pty.ts +34 -0
- package/lib/sandbox/clipboard/paths.ts +71 -0
- package/lib/sandbox/commands/create.ts +19 -2
- package/lib/sandbox/commands/enter.ts +15 -3
- package/lib/sandbox/commands/ls.ts +28 -4
- package/lib/sandbox/commands/prune.ts +211 -0
- package/lib/sandbox/commands/rm.ts +30 -32
- package/lib/sandbox/config.ts +2 -0
- package/lib/sandbox/constants.ts +9 -0
- package/lib/sandbox/host-timezone.ts +42 -0
- package/lib/sandbox/index.ts +7 -1
- package/lib/sandbox/managed-fs.ts +27 -0
- package/lib/sandbox/runtimes/base.dockerfile +21 -16
- package/lib/sandbox/tools.ts +1 -1
- package/lib/version.ts +11 -4
- package/package.json +10 -6
- package/templates/.agents/README.en.md +19 -0
- package/templates/.agents/README.zh-CN.md +19 -0
- package/templates/.agents/rules/create-issue.github.en.md +19 -1
- package/templates/.agents/rules/create-issue.github.zh-CN.md +19 -1
- package/templates/.agents/rules/milestone-inference.github.en.md +12 -0
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +12 -0
- package/templates/.agents/rules/testing-discipline.en.md +44 -0
- package/templates/.agents/rules/testing-discipline.zh-CN.md +44 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +26 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +26 -0
- package/templates/.agents/skills/analyze-task/config/verify.en.json +51 -0
- package/templates/.agents/skills/analyze-task/config/{verify.json → verify.zh-CN.json} +6 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +15 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +15 -0
- package/templates/.agents/skills/complete-task/config/{verify.json → verify.en.json} +10 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +48 -0
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/create-task/config/verify.json +1 -0
- package/templates/.agents/skills/implement-task/SKILL.en.md +14 -0
- package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +14 -0
- package/templates/.agents/skills/implement-task/config/verify.en.json +51 -0
- package/templates/.agents/skills/implement-task/config/{verify.json → verify.zh-CN.json} +7 -2
- package/templates/.agents/skills/implement-task/reference/report-template.en.md +15 -0
- package/templates/.agents/skills/implement-task/reference/report-template.zh-CN.md +15 -0
- package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +22 -0
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +22 -0
- package/templates/.agents/skills/plan-task/config/verify.en.json +52 -0
- package/templates/.agents/skills/plan-task/config/{verify.json → verify.zh-CN.json} +6 -2
- package/templates/.agents/skills/post-release/SKILL.en.md +1 -0
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/refine-task/SKILL.en.md +14 -0
- package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +14 -0
- package/templates/.agents/skills/refine-task/config/verify.en.json +47 -0
- package/templates/.agents/skills/refine-task/config/{verify.json → verify.zh-CN.json} +7 -2
- package/templates/.agents/skills/refine-task/reference/report-template.en.md +15 -0
- package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +15 -0
- package/templates/.agents/skills/review-task/SKILL.en.md +14 -0
- package/templates/.agents/skills/review-task/SKILL.zh-CN.md +14 -0
- package/templates/.agents/skills/review-task/config/verify.en.json +50 -0
- package/templates/.agents/skills/review-task/config/{verify.json → verify.zh-CN.json} +5 -2
- package/templates/.agents/skills/review-task/reference/report-template.en.md +15 -0
- package/templates/.agents/skills/review-task/reference/report-template.zh-CN.md +15 -0
- package/dist/package.json +0 -5
|
@@ -5,6 +5,8 @@ import { formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials,
|
|
|
5
5
|
import { runInteractiveEngine, runSafeEngine } from "../shell.js";
|
|
6
6
|
import { resolveTaskBranch } from "../task-resolver.js";
|
|
7
7
|
import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
|
|
8
|
+
import { runInteractiveWithClipboardBridge } from "../clipboard/bridge.js";
|
|
9
|
+
import { detectHostTimezone } from "../host-timezone.js";
|
|
8
10
|
const USAGE = `Usage: ai sandbox exec <branch> [cmd...]`;
|
|
9
11
|
const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
|
|
10
12
|
// Terminal-detection variables that interactive TUIs (e.g. claude-code)
|
|
@@ -28,6 +30,10 @@ export function terminalEnvFlags(env = process.env) {
|
|
|
28
30
|
}
|
|
29
31
|
return flags;
|
|
30
32
|
}
|
|
33
|
+
export function hostTimezoneEnvFlags(detect = detectHostTimezone) {
|
|
34
|
+
const tz = detect();
|
|
35
|
+
return tz ? ['-e', `TZ=${tz}`] : [];
|
|
36
|
+
}
|
|
31
37
|
export function formatCredentialSyncStatus(result, isTTY = process.stderr.isTTY) {
|
|
32
38
|
if (result.status === 'STALE_ACCESS') {
|
|
33
39
|
return 'Warning: Claude Code credentials on host appear stale. Run "ai sandbox refresh" or "claude /login" to renew.\n';
|
|
@@ -51,7 +57,7 @@ export function formatCredentialSyncStatus(result, isTTY = process.stderr.isTTY)
|
|
|
51
57
|
}
|
|
52
58
|
return null;
|
|
53
59
|
}
|
|
54
|
-
export function enter(args) {
|
|
60
|
+
export async function enter(args) {
|
|
55
61
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
56
62
|
process.stdout.write(`${USAGE}\n`);
|
|
57
63
|
if (args.length === 0) {
|
|
@@ -83,7 +89,7 @@ export function enter(args) {
|
|
|
83
89
|
process.stderr.write(`Warning: Failed to sync Claude Code credentials: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
|
|
84
90
|
}
|
|
85
91
|
}
|
|
86
|
-
const envFlags = terminalEnvFlags();
|
|
92
|
+
const envFlags = [...terminalEnvFlags(), ...hostTimezoneEnvFlags()];
|
|
87
93
|
if (cmd.length === 0) {
|
|
88
94
|
try {
|
|
89
95
|
materializeDotfiles(config.dotfilesDir, dotfilesCacheDir(config.home, config.project));
|
|
@@ -91,7 +97,12 @@ export function enter(args) {
|
|
|
91
97
|
catch (error) {
|
|
92
98
|
process.stderr.write(`Warning: dotfiles snapshot rebuild failed: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
|
|
93
99
|
}
|
|
94
|
-
return
|
|
100
|
+
return runInteractiveWithClipboardBridge({
|
|
101
|
+
engine,
|
|
102
|
+
dockerArgs: ['exec', '-it', ...envFlags, container, 'bash', TMUX_ENTRY_PATH],
|
|
103
|
+
container,
|
|
104
|
+
home: config.home
|
|
105
|
+
});
|
|
95
106
|
}
|
|
96
107
|
return runInteractiveEngine(engine, 'docker', ['exec', '-it', ...envFlags, container, ...cmd]);
|
|
97
108
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
const ZONEINFO_MARK = '/zoneinfo/';
|
|
4
|
+
const IANA_ZONE_RE = /^[A-Za-z][A-Za-z0-9_+-]*(\/[A-Za-z0-9_+-]+)*$/;
|
|
5
|
+
function safeTimezone(value) {
|
|
6
|
+
if (!value || !IANA_ZONE_RE.test(value)) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
export function detectHostTimezone(options = {}) {
|
|
12
|
+
const platform = options.platform ?? os.platform();
|
|
13
|
+
const env = options.env ?? process.env;
|
|
14
|
+
if (env.TZ) {
|
|
15
|
+
return safeTimezone(env.TZ);
|
|
16
|
+
}
|
|
17
|
+
if (platform !== 'darwin' && platform !== 'linux') {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const readlink = options.readlink ?? fs.readlinkSync;
|
|
21
|
+
try {
|
|
22
|
+
const target = readlink('/etc/localtime');
|
|
23
|
+
const idx = target.indexOf(ZONEINFO_MARK);
|
|
24
|
+
if (idx < 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return safeTimezone(target.slice(idx + ZONEINFO_MARK.length));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=host-timezone.js.map
|
|
@@ -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
|
|
@@ -3,7 +3,6 @@ FROM ubuntu:22.04
|
|
|
3
3
|
LABEL description="AI coding sandbox"
|
|
4
4
|
|
|
5
5
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
6
|
-
ENV TZ=Asia/Shanghai
|
|
7
6
|
|
|
8
7
|
ARG HOST_UID=1000
|
|
9
8
|
ARG HOST_GID=1000
|
|
@@ -22,7 +21,7 @@ RUN apt-get update && apt-get install -y \
|
|
|
22
21
|
build-essential ca-certificates gnupg lsb-release \
|
|
23
22
|
libevent-core-2.1-7 libncursesw6 libtinfo6 \
|
|
24
23
|
pkg-config bison libevent-dev libncurses-dev \
|
|
25
|
-
locales \
|
|
24
|
+
locales tzdata \
|
|
26
25
|
&& locale-gen en_US.UTF-8 \
|
|
27
26
|
&& (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|
28
27
|
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg) \
|
|
@@ -45,13 +44,13 @@ RUN apt-get update && apt-get install -y \
|
|
|
45
44
|
&& rm -rf /var/lib/apt/lists/*
|
|
46
45
|
|
|
47
46
|
# Enable extended keys in CSI u format so Shift+Enter and other modified
|
|
48
|
-
# keys are forwarded through tmux. Preserve terminal
|
|
47
|
+
# keys are forwarded through tmux. Preserve terminal/timezone variables
|
|
49
48
|
# injected at `docker exec` time when new tmux sessions are created.
|
|
50
49
|
RUN printf '%s\n' \
|
|
51
50
|
'set -g extended-keys always' \
|
|
52
51
|
'set -g extended-keys-format csi-u' \
|
|
53
52
|
"set -as terminal-features 'xterm*:extkeys'" \
|
|
54
|
-
"set -ga update-environment 'TERM_PROGRAM TERM_PROGRAM_VERSION LC_TERMINAL LC_TERMINAL_VERSION'" \
|
|
53
|
+
"set -ga update-environment 'TERM_PROGRAM TERM_PROGRAM_VERSION LC_TERMINAL LC_TERMINAL_VERSION TZ'" \
|
|
55
54
|
'set -g mouse on' \
|
|
56
55
|
'set -g status-interval 1' \
|
|
57
56
|
'set -g status-right-length 80' \
|
|
@@ -146,7 +145,7 @@ RUN cat > /usr/local/bin/sandbox-tmux-entry <<'SCRIPT' && chmod +x /usr/local/bi
|
|
|
146
145
|
#!/bin/sh
|
|
147
146
|
set -eu
|
|
148
147
|
|
|
149
|
-
sandbox-dotfiles-link >/dev/null || true
|
|
148
|
+
sandbox-dotfiles-link >/dev/null 2>&1 || true
|
|
150
149
|
|
|
151
150
|
SESSION=work
|
|
152
151
|
|
|
@@ -154,20 +153,26 @@ if ! command -v tmux >/dev/null 2>&1; then
|
|
|
154
153
|
exec bash
|
|
155
154
|
fi
|
|
156
155
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null | \
|
|
162
|
-
while read -r name attached; do
|
|
163
|
-
[ "$name" = "$SESSION" ] && continue
|
|
156
|
+
# Drop stale grouped sessions left by older entry-script versions (the windows
|
|
157
|
+
# live on $SESSION, so killing the group members only removes view entries).
|
|
158
|
+
tmux list-sessions -F '#{session_name}' 2>/dev/null | while IFS= read -r name; do
|
|
164
159
|
case "$name" in
|
|
165
|
-
|
|
160
|
+
"$SESSION"-*) tmux kill-session -t "$name" 2>/dev/null || true ;;
|
|
166
161
|
esac
|
|
167
|
-
|
|
168
|
-
|
|
162
|
+
done
|
|
163
|
+
|
|
164
|
+
# Reuse the single $SESSION; -d detaches any pre-existing client so the new
|
|
165
|
+
# one becomes the sole owner of window-size, eliminating size races.
|
|
166
|
+
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
167
|
+
# Push the per-exec TZ into the running session's env so new
|
|
168
|
+
# windows/panes pick up the host timezone without a session kill.
|
|
169
|
+
if [ -n "${TZ:-}" ]; then
|
|
170
|
+
tmux set-environment -t "$SESSION" TZ "$TZ" 2>/dev/null || true
|
|
171
|
+
fi
|
|
172
|
+
exec tmux attach -d -t "$SESSION"
|
|
173
|
+
fi
|
|
169
174
|
|
|
170
|
-
exec tmux new-session -
|
|
175
|
+
exec tmux new-session -s "$SESSION"
|
|
171
176
|
SCRIPT
|
|
172
177
|
|
|
173
178
|
ENV LANG=en_US.UTF-8
|
|
@@ -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',
|
package/dist/lib/version.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
const
|
|
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
|