@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.
- 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 +216 -0
- package/dist/lib/sandbox/clipboard/darwin.js +73 -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 +11 -2
- package/dist/lib/sandbox/commands/enter.js +8 -2
- 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/index.js +7 -1
- package/dist/lib/sandbox/managed-fs.js +25 -0
- package/dist/lib/sandbox/tools.js +1 -1
- package/dist/lib/version.js +9 -2
- package/lib/sandbox/clipboard/bridge.ts +285 -0
- package/lib/sandbox/clipboard/darwin.ts +90 -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 +15 -2
- package/lib/sandbox/commands/enter.ts +8 -2
- 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/index.ts +7 -1
- package/lib/sandbox/managed-fs.ts +27 -0
- package/lib/sandbox/tools.ts +1 -1
- package/lib/version.ts +11 -4
- package/package.json +5 -1
- package/templates/.agents/README.en.md +19 -0
- package/templates/.agents/README.zh-CN.md +19 -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/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/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
|
@@ -9,7 +9,13 @@ import { runSafeEngine } from '../shell.ts';
|
|
|
9
9
|
import { resolveTools, toolProjectDirCandidates } from '../tools.ts';
|
|
10
10
|
|
|
11
11
|
const USAGE = 'Usage: ai sandbox ls';
|
|
12
|
-
const
|
|
12
|
+
const CONTAINER_TABLE_HEADERS = ['NAMES', 'STATUS', 'BRANCH'] as const;
|
|
13
|
+
|
|
14
|
+
type ContainerTableRow = {
|
|
15
|
+
name: string;
|
|
16
|
+
status: string;
|
|
17
|
+
branch: string;
|
|
18
|
+
};
|
|
13
19
|
|
|
14
20
|
// Exported to lock the docker/podman-compatible format in unit tests.
|
|
15
21
|
export function containerListFormat(): string {
|
|
@@ -35,6 +41,22 @@ export function parseLabels(csv: string): Record<string, string> {
|
|
|
35
41
|
return labels;
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
export function formatContainerTable(rows: ContainerTableRow[]): string[] {
|
|
45
|
+
const columns = rows.map((row) => [row.name, row.status, row.branch] as const);
|
|
46
|
+
const widths = [
|
|
47
|
+
Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.name.length)),
|
|
48
|
+
Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.status.length)),
|
|
49
|
+
Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.branch.length))
|
|
50
|
+
] as const;
|
|
51
|
+
const renderRow = (values: readonly [string, string, string]): string =>
|
|
52
|
+
`${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2]}`.trimEnd();
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
renderRow(CONTAINER_TABLE_HEADERS),
|
|
56
|
+
...columns.map((column) => renderRow(column))
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
function listChildren(dir: string): string[] {
|
|
39
61
|
if (!fs.existsSync(dir)) {
|
|
40
62
|
return [];
|
|
@@ -69,11 +91,13 @@ export function ls(args: string[] = []): void {
|
|
|
69
91
|
p.log.warn(' No sandbox containers');
|
|
70
92
|
} else {
|
|
71
93
|
const branchKey = sandboxBranchLabel(config);
|
|
72
|
-
|
|
73
|
-
for (const line of containers.split('\n')) {
|
|
94
|
+
const rows = containers.split('\n').map((line) => {
|
|
74
95
|
const [name = '', status = '', labelsCsv = ''] = line.split('\t');
|
|
75
96
|
const branch = parseLabels(labelsCsv)[branchKey] ?? '';
|
|
76
|
-
|
|
97
|
+
return { name, status, branch };
|
|
98
|
+
});
|
|
99
|
+
for (const line of formatContainerTable(rows)) {
|
|
100
|
+
process.stdout.write(` ${line}\n`);
|
|
77
101
|
}
|
|
78
102
|
}
|
|
79
103
|
|
|
@@ -0,0 +1,211 @@
|
|
|
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.ts';
|
|
7
|
+
import type { SandboxConfig } from '../config.ts';
|
|
8
|
+
import { safeNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.ts';
|
|
9
|
+
import { detectEngine } from '../engine.ts';
|
|
10
|
+
import { hostJoin } from '../engines/wsl2-paths.ts';
|
|
11
|
+
import { removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
|
|
12
|
+
import { parseLabels } from './ls.ts';
|
|
13
|
+
import { runEngine, runSafe } from '../shell.ts';
|
|
14
|
+
import { resolveTools } from '../tools.ts';
|
|
15
|
+
import type { SandboxTool } from '../tools.ts';
|
|
16
|
+
|
|
17
|
+
const USAGE = `Usage: ai sandbox prune [--dry-run]`;
|
|
18
|
+
|
|
19
|
+
type OrphanKind = 'shell' | 'worktree' | 'share' | 'tool';
|
|
20
|
+
|
|
21
|
+
export type OrphanGroup = {
|
|
22
|
+
kind: OrphanKind;
|
|
23
|
+
label: string;
|
|
24
|
+
base: string;
|
|
25
|
+
dirs: string[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function listChildDirs(base: string): string[] {
|
|
29
|
+
if (!fs.existsSync(base)) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return fs.readdirSync(base)
|
|
34
|
+
.sort()
|
|
35
|
+
.map((entry) => path.join(base, entry))
|
|
36
|
+
.filter((entry) => {
|
|
37
|
+
try {
|
|
38
|
+
return fs.statSync(entry).isDirectory();
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function activeSafeNames(activeBranches: string[]): Set<string> {
|
|
46
|
+
const names = new Set<string>();
|
|
47
|
+
for (const branch of activeBranches) {
|
|
48
|
+
try {
|
|
49
|
+
for (const name of safeNameCandidates(branch)) {
|
|
50
|
+
names.add(name);
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
names.add(branch);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return names;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function orphanDirs(base: string, activeNames: Set<string>): string[] {
|
|
60
|
+
return listChildDirs(base).filter((dir) => !activeNames.has(path.basename(dir)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function addGroup(groups: OrphanGroup[], group: OrphanGroup): void {
|
|
64
|
+
if (group.dirs.length > 0) {
|
|
65
|
+
groups.push(group);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function collectOrphanGroups(
|
|
70
|
+
config: SandboxConfig,
|
|
71
|
+
tools: SandboxTool[],
|
|
72
|
+
activeBranches: string[]
|
|
73
|
+
): OrphanGroup[] {
|
|
74
|
+
const activeNames = activeSafeNames(activeBranches);
|
|
75
|
+
const groups: OrphanGroup[] = [];
|
|
76
|
+
const shareBranchesBase = hostJoin(config.shareBase, 'branches');
|
|
77
|
+
|
|
78
|
+
addGroup(groups, {
|
|
79
|
+
kind: 'shell',
|
|
80
|
+
label: 'Shell config dirs',
|
|
81
|
+
base: config.shellConfigBase,
|
|
82
|
+
dirs: orphanDirs(config.shellConfigBase, activeNames)
|
|
83
|
+
});
|
|
84
|
+
addGroup(groups, {
|
|
85
|
+
kind: 'worktree',
|
|
86
|
+
label: 'Worktrees',
|
|
87
|
+
base: config.worktreeBase,
|
|
88
|
+
dirs: orphanDirs(config.worktreeBase, activeNames)
|
|
89
|
+
});
|
|
90
|
+
addGroup(groups, {
|
|
91
|
+
kind: 'share',
|
|
92
|
+
label: 'Share branch dirs',
|
|
93
|
+
base: shareBranchesBase,
|
|
94
|
+
dirs: orphanDirs(shareBranchesBase, activeNames)
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
for (const tool of tools) {
|
|
98
|
+
const base = hostJoin(tool.sandboxBase, config.project);
|
|
99
|
+
addGroup(groups, {
|
|
100
|
+
kind: 'tool',
|
|
101
|
+
label: `${tool.name} state`,
|
|
102
|
+
base,
|
|
103
|
+
dirs: orphanDirs(base, activeNames)
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return groups;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function removeOrphanGroups(config: SandboxConfig, groups: OrphanGroup[]): boolean {
|
|
111
|
+
let removedWorktrees = false;
|
|
112
|
+
for (const group of groups) {
|
|
113
|
+
for (const dir of group.dirs) {
|
|
114
|
+
if (group.kind === 'worktree') {
|
|
115
|
+
removeWorktreeDir(config.repoRoot, group.base, dir);
|
|
116
|
+
removedWorktrees = true;
|
|
117
|
+
} else {
|
|
118
|
+
removeManagedDir(group.base, dir);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return removedWorktrees;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function activeBranchesFromLabels(config: SandboxConfig, labelsOutput: string): string[] {
|
|
126
|
+
const branchKey = sandboxBranchLabel(config);
|
|
127
|
+
return labelsOutput.split('\n')
|
|
128
|
+
.map((line) => parseLabels(line)[branchKey] ?? '')
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function orphanCount(groups: OrphanGroup[]): number {
|
|
133
|
+
return groups.reduce((sum, group) => sum + group.dirs.length, 0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function writeGroups(groups: OrphanGroup[]): void {
|
|
137
|
+
for (const group of groups) {
|
|
138
|
+
p.log.step(group.label);
|
|
139
|
+
for (const dir of group.dirs) {
|
|
140
|
+
process.stdout.write(` ${dir}\n`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function prune(args: string[]): Promise<void> {
|
|
146
|
+
const { values } = parseArgs({
|
|
147
|
+
args,
|
|
148
|
+
strict: true,
|
|
149
|
+
options: {
|
|
150
|
+
'dry-run': { type: 'boolean' },
|
|
151
|
+
help: { type: 'boolean', short: 'h' }
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (values.help) {
|
|
156
|
+
process.stdout.write(`${USAGE}\n`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const config = loadConfig();
|
|
161
|
+
const tools = resolveTools(config);
|
|
162
|
+
const engine = detectEngine(config);
|
|
163
|
+
const psArgs = [
|
|
164
|
+
'ps',
|
|
165
|
+
'-a',
|
|
166
|
+
'--filter',
|
|
167
|
+
`label=${sandboxLabel(config)}`,
|
|
168
|
+
'--format',
|
|
169
|
+
'{{.Labels}}'
|
|
170
|
+
];
|
|
171
|
+
let labelsOutput: string;
|
|
172
|
+
try {
|
|
173
|
+
labelsOutput = runEngine(engine, 'docker', psArgs);
|
|
174
|
+
} catch {
|
|
175
|
+
throw new Error('Unable to determine active sandbox branches: docker ps failed');
|
|
176
|
+
}
|
|
177
|
+
const groups = collectOrphanGroups(config, tools, activeBranchesFromLabels(config, labelsOutput));
|
|
178
|
+
const count = orphanCount(groups);
|
|
179
|
+
|
|
180
|
+
p.intro(pc.cyan(`Pruning orphaned sandbox state for ${config.project}`));
|
|
181
|
+
|
|
182
|
+
if (count === 0) {
|
|
183
|
+
p.log.success('No orphaned sandbox state dirs found');
|
|
184
|
+
p.outro(pc.green('Sandbox prune complete'));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
writeGroups(groups);
|
|
189
|
+
|
|
190
|
+
if (values['dry-run']) {
|
|
191
|
+
p.outro(pc.green('Dry run complete'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const shouldRemove = await p.confirm({
|
|
196
|
+
message: `Remove ${count} orphaned sandbox state dirs?`,
|
|
197
|
+
initialValue: true
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (p.isCancel(shouldRemove) || !shouldRemove) {
|
|
201
|
+
p.outro('Cancelled');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const removedWorktrees = removeOrphanGroups(config, groups);
|
|
206
|
+
if (removedWorktrees) {
|
|
207
|
+
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
p.outro(pc.green('Orphaned sandbox state dirs removed'));
|
|
211
|
+
}
|
|
@@ -11,31 +11,23 @@ import {
|
|
|
11
11
|
sandboxBranchLabel,
|
|
12
12
|
sandboxLabel,
|
|
13
13
|
shareBranchDir,
|
|
14
|
+
shellConfigDirCandidates,
|
|
14
15
|
worktreeDirCandidates
|
|
15
16
|
} from '../constants.ts';
|
|
16
17
|
import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.ts';
|
|
17
|
-
import {
|
|
18
|
+
import { removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
|
|
19
|
+
import { runOk, runSafe, runSafeEngine } from '../shell.ts';
|
|
18
20
|
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
19
21
|
import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.ts';
|
|
20
22
|
import type { SandboxTool } from '../tools.ts';
|
|
21
23
|
|
|
22
24
|
const USAGE = `Usage: ai sandbox rm <branch> [--all]`;
|
|
25
|
+
export { assertManagedPath } from '../managed-fs.ts';
|
|
23
26
|
|
|
24
27
|
function projectToolDirs(config: SandboxConfig, tools: SandboxTool[]): string[] {
|
|
25
28
|
return tools.flatMap((tool) => toolProjectDirCandidates(tool, config.project));
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
export function assertManagedPath(root: string, target: string): void {
|
|
29
|
-
const resolvedRoot = path.resolve(root);
|
|
30
|
-
const resolvedTarget = path.resolve(target);
|
|
31
|
-
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
32
|
-
if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
throw new Error(`Refusing to remove path outside managed sandbox root: ${target}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
31
|
async function rmOne(config: SandboxConfig, tools: SandboxTool[], branch: string): Promise<void> {
|
|
40
32
|
assertValidBranchName(branch);
|
|
41
33
|
const engine = detectEngine(config);
|
|
@@ -93,12 +85,7 @@ async function rmOne(config: SandboxConfig, tools: SandboxTool[], branch: string
|
|
|
93
85
|
|
|
94
86
|
if (shouldRemoveWorktree) {
|
|
95
87
|
for (const worktree of existingWorktrees) {
|
|
96
|
-
|
|
97
|
-
run('git', ['-C', config.repoRoot, 'worktree', 'remove', worktree, '--force']);
|
|
98
|
-
} catch {
|
|
99
|
-
assertManagedPath(config.worktreeBase, worktree);
|
|
100
|
-
fs.rmSync(worktree, { recursive: true, force: true });
|
|
101
|
-
}
|
|
88
|
+
removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree);
|
|
102
89
|
}
|
|
103
90
|
|
|
104
91
|
const shouldDeleteBranch = await p.confirm({
|
|
@@ -116,12 +103,16 @@ async function rmOne(config: SandboxConfig, tools: SandboxTool[], branch: string
|
|
|
116
103
|
|
|
117
104
|
for (const { tool, candidates } of toolCandidates) {
|
|
118
105
|
for (const dir of candidates.filter((candidate) => fs.existsSync(candidate))) {
|
|
119
|
-
|
|
120
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
106
|
+
removeManagedDir(tool.sandboxBase, dir);
|
|
121
107
|
p.log.success(`${tool.name} state removed: ${dir}`);
|
|
122
108
|
}
|
|
123
109
|
}
|
|
124
110
|
|
|
111
|
+
for (const dir of shellConfigDirCandidates(config, effectiveBranch).filter((candidate) => fs.existsSync(candidate))) {
|
|
112
|
+
removeManagedDir(config.shellConfigBase, dir);
|
|
113
|
+
p.log.success(`Shell config removed: ${dir}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
125
116
|
const shareBranch = shareBranchDir(config, effectiveBranch);
|
|
126
117
|
if (fs.existsSync(shareBranch)) {
|
|
127
118
|
const shouldRemoveShare = await p.confirm({
|
|
@@ -129,8 +120,7 @@ async function rmOne(config: SandboxConfig, tools: SandboxTool[], branch: string
|
|
|
129
120
|
initialValue: true
|
|
130
121
|
});
|
|
131
122
|
if (!p.isCancel(shouldRemoveShare) && shouldRemoveShare) {
|
|
132
|
-
|
|
133
|
-
fs.rmSync(shareBranch, { recursive: true, force: true });
|
|
123
|
+
removeManagedDir(config.shareBase, shareBranch);
|
|
134
124
|
p.log.success(`Share dir removed: ${shareBranch}`);
|
|
135
125
|
}
|
|
136
126
|
}
|
|
@@ -171,12 +161,7 @@ async function rmAll(config: SandboxConfig, tools: SandboxTool[]): Promise<void>
|
|
|
171
161
|
if (!p.isCancel(shouldRemoveWorktrees) && shouldRemoveWorktrees) {
|
|
172
162
|
for (const entry of fs.readdirSync(config.worktreeBase)) {
|
|
173
163
|
const dir = path.join(config.worktreeBase, entry);
|
|
174
|
-
|
|
175
|
-
run('git', ['-C', config.repoRoot, 'worktree', 'remove', dir, '--force']);
|
|
176
|
-
} catch {
|
|
177
|
-
assertManagedPath(config.worktreeBase, dir);
|
|
178
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
179
|
-
}
|
|
164
|
+
removeWorktreeDir(config.repoRoot, config.worktreeBase, dir);
|
|
180
165
|
}
|
|
181
166
|
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
182
167
|
}
|
|
@@ -184,20 +169,33 @@ async function rmAll(config: SandboxConfig, tools: SandboxTool[]): Promise<void>
|
|
|
184
169
|
|
|
185
170
|
for (const dir of projectToolDirs(config, tools)) {
|
|
186
171
|
if (fs.existsSync(dir)) {
|
|
187
|
-
|
|
188
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
172
|
+
removeManagedDir(path.dirname(dir), dir);
|
|
189
173
|
p.log.success(`Removed tool state: ${dir}`);
|
|
190
174
|
}
|
|
191
175
|
}
|
|
192
176
|
|
|
177
|
+
if (fs.existsSync(config.shellConfigBase) && fs.readdirSync(config.shellConfigBase).length > 0) {
|
|
178
|
+
const shouldRemoveShellConfigs = await p.confirm({
|
|
179
|
+
message: `Remove all shell config dirs in ${config.shellConfigBase}?`,
|
|
180
|
+
initialValue: true
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!p.isCancel(shouldRemoveShellConfigs) && shouldRemoveShellConfigs) {
|
|
184
|
+
for (const entry of fs.readdirSync(config.shellConfigBase)) {
|
|
185
|
+
const dir = path.join(config.shellConfigBase, entry);
|
|
186
|
+
removeManagedDir(config.shellConfigBase, dir);
|
|
187
|
+
}
|
|
188
|
+
p.log.success(`Project shell config dirs removed: ${config.shellConfigBase}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
193
192
|
if (fs.existsSync(config.shareBase) && fs.readdirSync(config.shareBase).length > 0) {
|
|
194
193
|
const shouldRemoveAllShares = await p.confirm({
|
|
195
194
|
message: `Remove all share dirs for project (${config.shareBase})?`,
|
|
196
195
|
initialValue: true
|
|
197
196
|
});
|
|
198
197
|
if (!p.isCancel(shouldRemoveAllShares) && shouldRemoveAllShares) {
|
|
199
|
-
|
|
200
|
-
fs.rmSync(config.shareBase, { recursive: true, force: true });
|
|
198
|
+
removeManagedDir(path.dirname(config.shareBase), config.shareBase);
|
|
201
199
|
p.log.success(`Project share dirs removed: ${config.shareBase}`);
|
|
202
200
|
}
|
|
203
201
|
}
|
package/lib/sandbox/config.ts
CHANGED
|
@@ -46,6 +46,7 @@ export type SandboxConfig = {
|
|
|
46
46
|
imageName: string;
|
|
47
47
|
worktreeBase: string;
|
|
48
48
|
shareBase: string;
|
|
49
|
+
shellConfigBase: string;
|
|
49
50
|
dotfilesDir: string;
|
|
50
51
|
engine: string | null;
|
|
51
52
|
runtimes: string[];
|
|
@@ -145,6 +146,7 @@ export function loadConfig({
|
|
|
145
146
|
imageName: `${project}-sandbox:latest`,
|
|
146
147
|
worktreeBase: hostJoin(home, '.agent-infra', 'worktrees', project),
|
|
147
148
|
shareBase: hostJoin(home, '.agent-infra', 'share', project),
|
|
149
|
+
shellConfigBase: hostJoin(home, '.agent-infra', 'config', project),
|
|
148
150
|
dotfilesDir: hostJoin(home, '.agent-infra', 'dotfiles'),
|
|
149
151
|
engine,
|
|
150
152
|
runtimes,
|
package/lib/sandbox/constants.ts
CHANGED
|
@@ -9,6 +9,7 @@ type SandboxPathConfig = {
|
|
|
9
9
|
containerPrefix: string;
|
|
10
10
|
worktreeBase: string;
|
|
11
11
|
shareBase: string;
|
|
12
|
+
shellConfigBase: string;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
type HostResources = {
|
|
@@ -86,6 +87,14 @@ export function shareBranchDir(config: Pick<SandboxPathConfig, 'shareBase'>, bra
|
|
|
86
87
|
return hostJoin(config.shareBase, 'branches', sanitizeBranchName(branch));
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
export function shellConfigDir(config: Pick<SandboxPathConfig, 'shellConfigBase'>, branch: string): string {
|
|
91
|
+
return hostJoin(config.shellConfigBase, sanitizeBranchName(branch));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function shellConfigDirCandidates(config: Pick<SandboxPathConfig, 'shellConfigBase'>, branch: string): string[] {
|
|
95
|
+
return safeNameCandidates(branch).map((name) => hostJoin(config.shellConfigBase, name));
|
|
96
|
+
}
|
|
97
|
+
|
|
89
98
|
export function sandboxLabel(config: Pick<SandboxPathConfig, 'project'>): string {
|
|
90
99
|
return `${config.project}.sandbox`;
|
|
91
100
|
}
|
package/lib/sandbox/index.ts
CHANGED
|
@@ -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
|
|
|
@@ -33,7 +34,7 @@ export async function runSandbox(args: string[]): Promise<void> {
|
|
|
33
34
|
}
|
|
34
35
|
case 'exec': {
|
|
35
36
|
const { enter } = await import('./commands/enter.ts');
|
|
36
|
-
const exitCode = enter(rest);
|
|
37
|
+
const exitCode = await enter(rest);
|
|
37
38
|
if (typeof exitCode === 'number' && exitCode !== 0) {
|
|
38
39
|
process.exitCode = exitCode;
|
|
39
40
|
}
|
|
@@ -57,6 +58,11 @@ export async function runSandbox(args: string[]): Promise<void> {
|
|
|
57
58
|
await rm(rest);
|
|
58
59
|
break;
|
|
59
60
|
}
|
|
61
|
+
case 'prune': {
|
|
62
|
+
const { prune } = await import('./commands/prune.ts');
|
|
63
|
+
await prune(rest);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
60
66
|
case 'vm': {
|
|
61
67
|
const { vm } = await import('./commands/vm.ts');
|
|
62
68
|
await vm(rest);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { run } from './shell.ts';
|
|
4
|
+
|
|
5
|
+
export function assertManagedPath(root: string, target: string): void {
|
|
6
|
+
const resolvedRoot = path.resolve(root);
|
|
7
|
+
const resolvedTarget = path.resolve(target);
|
|
8
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
9
|
+
if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
throw new Error(`Refusing to remove path outside managed sandbox root: ${target}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function removeManagedDir(root: string, dir: string): void {
|
|
17
|
+
assertManagedPath(root, dir);
|
|
18
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function removeWorktreeDir(repoRoot: string, worktreeBase: string, dir: string): void {
|
|
22
|
+
try {
|
|
23
|
+
run('git', ['-C', repoRoot, 'worktree', 'remove', dir, '--force']);
|
|
24
|
+
} catch {
|
|
25
|
+
removeManagedDir(worktreeBase, dir);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/lib/sandbox/tools.ts
CHANGED
|
@@ -28,7 +28,7 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
|
|
|
28
28
|
'claude-code': {
|
|
29
29
|
id: 'claude-code',
|
|
30
30
|
name: 'Claude Code',
|
|
31
|
-
npmPackage: '@anthropic-ai/claude-code',
|
|
31
|
+
npmPackage: '@anthropic-ai/claude-code@stable',
|
|
32
32
|
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
|
|
33
33
|
containerMount: '/home/devuser/.claude',
|
|
34
34
|
versionCmd: 'claude --version',
|
package/lib/version.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
)
|
|
3
|
+
const packageJsonUrl = [
|
|
4
|
+
new URL('../package.json', import.meta.url),
|
|
5
|
+
new URL('../../package.json', import.meta.url),
|
|
6
|
+
].find((url) => existsSync(url));
|
|
7
|
+
|
|
8
|
+
if (!packageJsonUrl) {
|
|
9
|
+
throw new Error('Unable to locate package.json for agent-infra version');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { version } = JSON.parse(readFileSync(packageJsonUrl, 'utf8'));
|
|
6
13
|
const VERSION = `v${version}`;
|
|
7
14
|
|
|
8
15
|
export { VERSION };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Gemini CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"test:smoke": "npm run build && node --experimental-strip-types --no-warnings --test tests/templates/*.test.ts tests/core/airc.test.ts tests/core/release.test.ts tests/core/metadata-sync-workflow.test.ts tests/core/pr-label-workflow.test.ts tests/core/status-label-workflow.test.ts tests/core/test-tier-coverage.test.ts tests/cli/lib.test.ts tests/cli/sync-templates.test.ts tests/scripts/sync-templates-platform-gating.test.ts",
|
|
58
58
|
"test:core": "npm run build && node --experimental-strip-types --no-warnings --test tests/templates/*.test.ts tests/core/airc.test.ts tests/core/release.test.ts tests/core/metadata-sync-workflow.test.ts tests/core/pr-label-workflow.test.ts tests/core/status-label-workflow.test.ts tests/core/test-tier-coverage.test.ts tests/cli/lib.test.ts tests/cli/sync-templates.test.ts tests/scripts/sync-templates-platform-gating.test.ts tests/cli/cli.test.ts tests/cli/merge.test.ts tests/cli/sandbox.test.ts tests/core/custom-skills.test.ts tests/core/custom-tuis.test.ts tests/core/demo-regen.test.ts tests/scripts/find-existing-task.test.ts tests/scripts/platform-adapter-defaults.test.ts",
|
|
59
59
|
"test": "npm run build && node --experimental-strip-types --no-warnings --test tests/cli/*.test.ts tests/templates/*.test.ts tests/core/*.test.ts tests/scripts/*.test.ts",
|
|
60
|
+
"test:coverage": "npm run build && node --experimental-strip-types --no-warnings --test --experimental-test-coverage --test-coverage-exclude='tests/**' --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage.lcov tests/cli/*.test.ts tests/templates/*.test.ts tests/core/*.test.ts tests/scripts/*.test.ts",
|
|
60
61
|
"prepublishOnly": "npm run build && node --experimental-strip-types --no-warnings --test tests/cli/*.test.ts tests/templates/*.test.ts tests/core/*.test.ts tests/scripts/*.test.ts"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
@@ -64,5 +65,8 @@
|
|
|
64
65
|
"@types/node": "^25.9.1",
|
|
65
66
|
"@types/semver": "^7.7.1",
|
|
66
67
|
"typescript": "~6.0"
|
|
68
|
+
},
|
|
69
|
+
"optionalDependencies": {
|
|
70
|
+
"@lydell/node-pty": "^1.2.0-beta.12"
|
|
67
71
|
}
|
|
68
72
|
}
|
|
@@ -193,6 +193,23 @@ Each source should mirror the `.agents/skills/` layout and include `SKILL.md` at
|
|
|
193
193
|
- Built-in skills are not overridable by custom sources; if a source skill name conflicts with a built-in skill, the source copy is skipped
|
|
194
194
|
- Use `files.ejected` if the project must take ownership of a built-in skill or command
|
|
195
195
|
|
|
196
|
+
## File Ownership and Sync Strategy
|
|
197
|
+
|
|
198
|
+
The `files` field in `.agents/.airc.json` groups project files into three categories:
|
|
199
|
+
|
|
200
|
+
| Category | When the template has the file | When the template does not have the file | Cleanup behavior |
|
|
201
|
+
|----------|--------------------------------|------------------------------------------|------------------|
|
|
202
|
+
| `managed` | Write from the template and overwrite | Treat as removed from the template | Delete the local project copy |
|
|
203
|
+
| `merged` | Merge semantically by AI or humans | Do not write from the template | Keep the local project copy |
|
|
204
|
+
| `ejected` | May be created from the template first; skip overwrite once it exists | Do not write from the template | Keep the local project copy |
|
|
205
|
+
|
|
206
|
+
`ejected` has two common uses:
|
|
207
|
+
|
|
208
|
+
1. **Taking over a built-in file**: the project needs full control over a rule, command, or config file that originally came from the template.
|
|
209
|
+
2. **Declaring a project-only file**: the project owns a file under a managed directory wildcard, but the template does not contain that file; list it in `files.ejected` so sync does not treat it as a removed template file.
|
|
210
|
+
|
|
211
|
+
`ejected` entries support literal paths or globs, using the same matching rules as `merged`.
|
|
212
|
+
|
|
196
213
|
## Custom TUI Configuration
|
|
197
214
|
|
|
198
215
|
Use the top-level `.agents/.airc.json` `customTUIs` array when your team uses an AI TUI that is not one of the built-in command targets. This config lets agent-infra show the correct next-step commands and generate command files for project custom skills by learning from an existing command in the custom TUI directory.
|
|
@@ -257,6 +274,7 @@ When writing or updating `.agents/skills/*/SKILL.md` files and their templates,
|
|
|
257
274
|
|
|
258
275
|
- Keep SKILL.md as concise as possible; move detailed rules, long templates, and large script blocks into a sibling `reference/` or `scripts/` directory.
|
|
259
276
|
- Store declarative configuration in a sibling `config/` directory, for example `config/verify.json`.
|
|
277
|
+
When `required_sections` or `required_patterns` contain language-specific text, provide `config/verify.en.json` and `config/verify.zh-CN.json`; sync strips the selected language variant back to `config/verify.json`.
|
|
260
278
|
- Use explicit navigation in the skeleton, such as: `Read reference/xxx.md before executing this step.`
|
|
261
279
|
- Keep scripts in `scripts/` and execute them instead of inlining long bash blocks.
|
|
262
280
|
|
|
@@ -269,6 +287,7 @@ node .agents/scripts/validate-artifact.js gate <skill-name> <task-dir> [artifact
|
|
|
269
287
|
```
|
|
270
288
|
|
|
271
289
|
- Each skill declares its own checks in `config/verify.json`; keep the file focused on what that skill must validate
|
|
290
|
+
- For language-specific artifact headings or anchors, keep only `required_sections` and language-specific `required_patterns` different between `config/verify.en.json` and `config/verify.zh-CN.json`
|
|
272
291
|
- If a skill also prints next-step guidance, run the gate first and only show those instructions after the gate passes
|
|
273
292
|
- For user-facing final validation, prefer `--format text` so the reply contains a readable summary instead of raw JSON
|
|
274
293
|
- Shared validation logic belongs in `.agents/scripts/validate-artifact.js`; do not move detailed rules back into SKILL.md
|
|
@@ -193,6 +193,23 @@ args: "<task-id>" # 可选
|
|
|
193
193
|
- 自定义 source 不能覆盖内置 skill;如果与内置 skill 同名,会跳过该 source skill
|
|
194
194
|
- 如果项目必须接管某个内置 skill 或命令,请使用 `files.ejected`
|
|
195
195
|
|
|
196
|
+
## 文件归属与同步策略
|
|
197
|
+
|
|
198
|
+
`.agents/.airc.json` 的 `files` 字段把项目文件分为三类:
|
|
199
|
+
|
|
200
|
+
| 类别 | 模板中存在时 | 模板中不存在时 | 清理行为 |
|
|
201
|
+
|------|--------------|----------------|----------|
|
|
202
|
+
| `managed` | 从模板写入并覆盖 | 视为模板已下线 | 删除项目本地副本 |
|
|
203
|
+
| `merged` | 由 AI 或人工语义合并 | 不从模板写入 | 保留项目本地副本 |
|
|
204
|
+
| `ejected` | 首次可从模板创建,已存在时跳过覆盖 | 不从模板写入 | 保留项目本地副本 |
|
|
205
|
+
|
|
206
|
+
`ejected` 有两种常见用法:
|
|
207
|
+
|
|
208
|
+
1. **接管内置文件**:项目需要完全控制原本来自模板的规则、命令或配置文件,避免后续同步覆盖本地内容。
|
|
209
|
+
2. **声明项目独占文件**:项目自己的文件落在 managed 目录通配下,但模板中没有同名文件;把它列入 `files.ejected`,避免同步时被当作模板已下线文件删除。
|
|
210
|
+
|
|
211
|
+
`ejected` 条目支持字面路径或 glob,匹配规则与 `merged` 相同。
|
|
212
|
+
|
|
196
213
|
## 自定义 TUI 配置
|
|
197
214
|
|
|
198
215
|
当团队使用的 AI TUI 不属于内置命令目标时,可以在 `.agents/.airc.json` 顶层配置 `customTUIs` 数组。该配置用于让 agent-infra 输出正确的下一步命令,并通过学习自定义 TUI 目录中的既有命令文件,为项目自定义 skill 生成同格式命令。
|
|
@@ -257,6 +274,7 @@ args: "<task-id>" # 可选
|
|
|
257
274
|
|
|
258
275
|
- SKILL.md 正文尽可能精简,把详细规则、长模板和大段脚本拆分到同级 `reference/` 或 `scripts/` 目录。
|
|
259
276
|
- 声明式配置统一放在同级 `config/` 目录,例如 `config/verify.json`。
|
|
277
|
+
当 `required_sections` 或 `required_patterns` 包含语言相关文案时,提供 `config/verify.en.json` 和 `config/verify.zh-CN.json`;sync 会把选中的语言变体剥离为 `config/verify.json`。
|
|
260
278
|
- 骨架中使用明确导航,例如:`执行此步骤前,先读取 reference/xxx.md。`
|
|
261
279
|
- 长脚本继续放在 `scripts/` 目录,优先执行脚本而不是内联大段 bash。
|
|
262
280
|
|
|
@@ -269,6 +287,7 @@ node .agents/scripts/validate-artifact.js gate <skill-name> <task-dir> [artifact
|
|
|
269
287
|
```
|
|
270
288
|
|
|
271
289
|
- 每个 skill 在自己的 `config/verify.json` 中声明需要检查的事项
|
|
290
|
+
- 对语言相关的产物标题或锚点,`config/verify.en.json` 和 `config/verify.zh-CN.json` 之间只应让 `required_sections` 与语言相关的 `required_patterns` 不同
|
|
272
291
|
- 如果 skill 还会展示“下一步”提示,必须先通过完成校验,再输出这些指引
|
|
273
292
|
- 面向用户展示最终校验结果时,优先使用 `--format text` 输出可读摘要,而不是原始 JSON
|
|
274
293
|
- 共享逻辑集中在 `.agents/scripts/validate-artifact.js`,不要把详细校验规则重新塞回 SKILL.md
|