@fitlab-ai/agent-infra 0.7.3 → 0.7.5
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 +32 -790
- package/README.zh-CN.md +32 -763
- package/bin/cli.ts +13 -11
- package/dist/bin/cli.js +13 -11
- package/dist/lib/init.js +1 -1
- package/dist/lib/merge.js +1 -1
- package/dist/lib/sandbox/commands/create.js +44 -3
- package/dist/lib/sandbox/commands/rm.js +99 -19
- package/dist/lib/sandbox/index.js +24 -22
- package/dist/lib/sandbox/readme-scaffold.js +6 -6
- package/dist/lib/task/artifacts.js +58 -0
- package/dist/lib/task/commands/cat.js +38 -0
- package/dist/lib/task/commands/files.js +47 -0
- package/dist/lib/task/commands/grep.js +143 -0
- package/dist/lib/task/commands/log.js +75 -0
- package/dist/lib/task/commands/show.js +5 -114
- package/dist/lib/task/commands/status.js +239 -0
- package/dist/lib/task/index.js +37 -0
- package/dist/lib/task/resolve-ref.js +150 -0
- package/dist/lib/update.js +1 -1
- package/lib/init.ts +1 -1
- package/lib/merge.ts +1 -1
- package/lib/sandbox/commands/create.ts +47 -4
- package/lib/sandbox/commands/rm.ts +128 -19
- package/lib/sandbox/index.ts +24 -22
- package/lib/sandbox/readme-scaffold.ts +6 -6
- package/lib/task/artifacts.ts +72 -0
- package/lib/task/commands/cat.ts +39 -0
- package/lib/task/commands/files.ts +53 -0
- package/lib/task/commands/grep.ts +147 -0
- package/lib/task/commands/log.ts +80 -0
- package/lib/task/commands/show.ts +5 -117
- package/lib/task/commands/status.ts +302 -0
- package/lib/task/index.ts +37 -0
- package/lib/task/resolve-ref.ts +160 -0
- package/lib/update.ts +1 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +1 -0
- package/templates/.agents/README.zh-CN.md +1 -0
- package/templates/.agents/rules/README.en.md +45 -0
- package/templates/.agents/rules/README.zh-CN.md +44 -0
- package/templates/.agents/rules/cli-help-format.en.md +49 -0
- package/templates/.agents/rules/cli-help-format.zh-CN.md +49 -0
- package/templates/.agents/rules/debugging-guide.en.md +25 -0
- package/templates/.agents/rules/debugging-guide.zh-CN.md +25 -0
- package/templates/.agents/rules/no-mid-flow-questions.en.md +14 -2
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +14 -2
- package/templates/.agents/rules/pr-sync.github.en.md +8 -6
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +8 -6
- package/templates/.agents/rules/review-handshake.en.md +83 -0
- package/templates/.agents/rules/review-handshake.zh-CN.md +83 -0
- package/templates/.agents/scripts/lib/post-review-commit.js +56 -0
- package/templates/.agents/scripts/lib/review-artifacts.js +117 -0
- package/templates/.agents/scripts/review-diff-fingerprint.js +99 -0
- package/templates/.agents/scripts/validate-artifact.js +240 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +52 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +52 -6
- package/templates/.agents/skills/code-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/code-task/config/verify.en.json +3 -0
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +3 -0
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -3
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -3
- package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/scripts/detect-mode.js +2 -107
- package/templates/.agents/skills/commit/SKILL.en.md +6 -0
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -0
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +8 -0
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +8 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -0
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -0
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/config/verify.en.json +3 -0
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +3 -0
- package/templates/.agents/skills/review-analysis/config/verify.en.json +2 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +2 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -4
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -4
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/review-code/config/verify.en.json +5 -2
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +5 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -4
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -4
- package/templates/.agents/skills/review-code/reference/report-template.en.md +6 -0
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +6 -0
- package/templates/.agents/skills/review-plan/config/verify.en.json +2 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +2 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -4
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -4
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +4 -0
- package/templates/.agents/skills/watch-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +7 -0
- package/templates/.agents/templates/task.zh-CN.md +7 -0
- package/templates/.github/workflows/metadata-sync.yml +1 -1
- package/templates/.github/workflows/pr-label.yml +1 -1
- package/templates/.github/workflows/status-label.yml +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
|
+
import { normalizeShortIdInput } from "./short-id.js";
|
|
5
|
+
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
6
|
+
// Flat-structured workspace dirs that hold tasks under `{dir}/{taskId}/task.md`.
|
|
7
|
+
// Note: `archive` uses a three-level YYYY/MM/DD layout and is handled separately.
|
|
8
|
+
const FLAT_WORKSPACE_DIRS = ['active', 'blocked', 'completed'];
|
|
9
|
+
function detectRepoRoot() {
|
|
10
|
+
try {
|
|
11
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
14
|
+
}).trim();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error('ai task: current directory is not inside a git repository');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function readShortIdLength(repoRoot) {
|
|
21
|
+
try {
|
|
22
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
23
|
+
const v = cfg?.task?.shortIdLength;
|
|
24
|
+
if (typeof v === 'number' && Number.isFinite(v) && v >= 1)
|
|
25
|
+
return v;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// fall through to default
|
|
29
|
+
}
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
function resolveShortIdToTaskId(arg, repoRoot) {
|
|
33
|
+
const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
|
|
34
|
+
if (!fs.existsSync(scriptPath)) {
|
|
35
|
+
throw new Error(`task-short-id.js not found at ${scriptPath}`);
|
|
36
|
+
}
|
|
37
|
+
const result = spawnSync('node', [scriptPath, 'resolve', arg], {
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
cwd: repoRoot
|
|
40
|
+
});
|
|
41
|
+
if (result.status !== 0) {
|
|
42
|
+
throw new Error((result.stderr || '').trim() || `failed to resolve '${arg}'`);
|
|
43
|
+
}
|
|
44
|
+
return result.stdout.trim();
|
|
45
|
+
}
|
|
46
|
+
function listSortedNumeric(dir, width) {
|
|
47
|
+
if (!fs.existsSync(dir))
|
|
48
|
+
return [];
|
|
49
|
+
const pattern = new RegExp(`^\\d{${width}}$`);
|
|
50
|
+
return fs
|
|
51
|
+
.readdirSync(dir)
|
|
52
|
+
.filter((entry) => pattern.test(entry))
|
|
53
|
+
.sort()
|
|
54
|
+
.reverse();
|
|
55
|
+
}
|
|
56
|
+
function findInArchive(repoRoot, taskId) {
|
|
57
|
+
// archive-tasks SKILL writes to .agents/workspace/archive/YYYY/MM/DD/{taskId}/task.md
|
|
58
|
+
// where YYYY/MM/DD comes from completed_at (or updated_at fallback) — NOT from
|
|
59
|
+
// the task id's creation date. So we cannot derive the path from taskId alone;
|
|
60
|
+
// walk the bounded YYYY/MM/DD tree instead. Newest-first to favor recent archives.
|
|
61
|
+
const archiveDir = path.join(repoRoot, '.agents', 'workspace', 'archive');
|
|
62
|
+
for (const year of listSortedNumeric(archiveDir, 4)) {
|
|
63
|
+
const yearDir = path.join(archiveDir, year);
|
|
64
|
+
for (const month of listSortedNumeric(yearDir, 2)) {
|
|
65
|
+
const monthDir = path.join(yearDir, month);
|
|
66
|
+
for (const day of listSortedNumeric(monthDir, 2)) {
|
|
67
|
+
const candidate = path.join(monthDir, day, taskId, 'task.md');
|
|
68
|
+
if (fs.existsSync(candidate))
|
|
69
|
+
return candidate;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function findTaskMd(repoRoot, taskId) {
|
|
76
|
+
for (const sub of FLAT_WORKSPACE_DIRS) {
|
|
77
|
+
const candidate = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
|
|
78
|
+
if (fs.existsSync(candidate))
|
|
79
|
+
return candidate;
|
|
80
|
+
}
|
|
81
|
+
return findInArchive(repoRoot, taskId);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Enumerate every task directory under the flat workspace states
|
|
85
|
+
* (active / blocked / completed) — archive is intentionally excluded so a
|
|
86
|
+
* full-tree scan never pulls in cold data. Ordered by state, then task id
|
|
87
|
+
* ascending, giving callers a deterministic traversal.
|
|
88
|
+
*/
|
|
89
|
+
function enumerateTaskDirs(repoRoot) {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const sub of FLAT_WORKSPACE_DIRS) {
|
|
92
|
+
const base = path.join(repoRoot, '.agents', 'workspace', sub);
|
|
93
|
+
if (!fs.existsSync(base))
|
|
94
|
+
continue;
|
|
95
|
+
for (const entry of fs.readdirSync(base).sort()) {
|
|
96
|
+
if (!TASK_ID_RE.test(entry))
|
|
97
|
+
continue;
|
|
98
|
+
const taskDir = path.join(base, entry);
|
|
99
|
+
if (!fs.existsSync(path.join(taskDir, 'task.md')))
|
|
100
|
+
continue;
|
|
101
|
+
out.push({ taskId: entry, taskDir });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolve a task ref (bare short id, `#N`, or `TASK-YYYYMMDD-HHMMSS`) to its
|
|
108
|
+
* task directory across active / blocked / completed / archive.
|
|
109
|
+
*
|
|
110
|
+
* The returned `message` on failure is command-agnostic (no `ai task <cmd>:`
|
|
111
|
+
* prefix); callers prepend their own prefix so each command keeps its existing
|
|
112
|
+
* stderr wording byte-for-byte.
|
|
113
|
+
*/
|
|
114
|
+
function resolveTaskRef(arg) {
|
|
115
|
+
const repoRoot = detectRepoRoot();
|
|
116
|
+
let taskId;
|
|
117
|
+
if (TASK_ID_RE.test(arg)) {
|
|
118
|
+
taskId = arg;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const shortIdLength = readShortIdLength(repoRoot);
|
|
122
|
+
const normalized = normalizeShortIdInput(arg, { shortIdLength });
|
|
123
|
+
if (normalized.kind === 'error') {
|
|
124
|
+
return { ok: false, message: normalized.message };
|
|
125
|
+
}
|
|
126
|
+
if (normalized.kind === 'pass') {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
message: `'${arg}' is not a valid short id or TASK-id; ` +
|
|
130
|
+
`expected bare digits, '#N', or 'TASK-YYYYMMDD-HHMMSS'`
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
taskId = resolveShortIdToTaskId(normalized.value, repoRoot);
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
return { ok: false, message: e.message };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const taskMdPath = findTaskMd(repoRoot, taskId);
|
|
141
|
+
if (!taskMdPath) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
message: `task ${taskId} not found in active / blocked / completed / archive`
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { ok: true, repoRoot, taskId, taskDir: path.dirname(taskMdPath), taskMdPath };
|
|
148
|
+
}
|
|
149
|
+
export { resolveTaskRef, detectRepoRoot, enumerateTaskDirs, TASK_ID_RE };
|
|
150
|
+
//# sourceMappingURL=resolve-ref.js.map
|
package/dist/lib/update.js
CHANGED
|
@@ -83,7 +83,7 @@ function syncFileRegistry(config, platformType, enabledTUIs) {
|
|
|
83
83
|
}
|
|
84
84
|
async function cmdUpdate() {
|
|
85
85
|
console.log('');
|
|
86
|
-
console.log('
|
|
86
|
+
console.log(' ai update');
|
|
87
87
|
console.log(' ==================================');
|
|
88
88
|
console.log('');
|
|
89
89
|
// check config exists
|
package/lib/init.ts
CHANGED
|
@@ -119,7 +119,7 @@ function parseLocalSources(input: string): SourceEntry[] {
|
|
|
119
119
|
|
|
120
120
|
async function cmdInit(): Promise<void> {
|
|
121
121
|
console.log('');
|
|
122
|
-
console.log('
|
|
122
|
+
console.log(' ai init');
|
|
123
123
|
console.log(' ================================');
|
|
124
124
|
console.log(' Optional template and skill sources can be added now or later in .agents/.airc.json.');
|
|
125
125
|
console.log('');
|
package/lib/merge.ts
CHANGED
|
@@ -901,7 +901,7 @@ function printReport(report: MergeReport): void {
|
|
|
901
901
|
async function cmdMerge(args: string[]): Promise<void> {
|
|
902
902
|
const sourcePath = args[0];
|
|
903
903
|
if (!sourcePath) {
|
|
904
|
-
throw new Error('Usage:
|
|
904
|
+
throw new Error('Usage: ai merge <source-path>');
|
|
905
905
|
}
|
|
906
906
|
|
|
907
907
|
const resolvedSource = path.resolve(sourcePath);
|
|
@@ -840,7 +840,34 @@ export function ensureClaudeSettings(toolDir: string, hostHomeDir?: string): voi
|
|
|
840
840
|
}
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
-
|
|
843
|
+
function resolveHostCatalogPath(value: unknown, hostHomeDir: string): string | null {
|
|
844
|
+
if (typeof value !== 'string' || value === '') {
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
let resolved: string;
|
|
848
|
+
if (value === '~' || value.startsWith('~/') || value.startsWith('~\\')) {
|
|
849
|
+
resolved = path.join(hostHomeDir, value.slice(1).replace(/^[/\\]+/, ''));
|
|
850
|
+
} else if (path.isAbsolute(value)) {
|
|
851
|
+
resolved = value;
|
|
852
|
+
} else {
|
|
853
|
+
resolved = path.join(hostHomeDir, '.codex', value);
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
if (!fs.statSync(resolved).isFile()) {
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
fs.accessSync(resolved, fs.constants.R_OK);
|
|
860
|
+
return resolved;
|
|
861
|
+
} catch {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
export function ensureCodexModelInheritance(
|
|
867
|
+
toolDir: string,
|
|
868
|
+
hostHomeDir?: string,
|
|
869
|
+
containerCodexDir: string = '/home/devuser/.codex'
|
|
870
|
+
): void {
|
|
844
871
|
if (!hostHomeDir) {
|
|
845
872
|
return;
|
|
846
873
|
}
|
|
@@ -890,6 +917,22 @@ export function ensureCodexModelInheritance(toolDir: string, hostHomeDir?: strin
|
|
|
890
917
|
changed = true;
|
|
891
918
|
}
|
|
892
919
|
|
|
920
|
+
if (!Object.hasOwn(sandboxParsed, 'model_catalog_json')) {
|
|
921
|
+
const hostCatalogPath = resolveHostCatalogPath(hostParsed['model_catalog_json'], hostHomeDir);
|
|
922
|
+
if (hostCatalogPath) {
|
|
923
|
+
try {
|
|
924
|
+
const basename = path.basename(hostCatalogPath);
|
|
925
|
+
const destDir = path.join(toolDir, 'model-catalogs');
|
|
926
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
927
|
+
fs.copyFileSync(hostCatalogPath, path.join(destDir, basename));
|
|
928
|
+
sandboxParsed['model_catalog_json'] = path.posix.join(containerCodexDir, 'model-catalogs', basename);
|
|
929
|
+
changed = true;
|
|
930
|
+
} catch {
|
|
931
|
+
// Copy failed (e.g. permissions): skip catalog, keep scalar inheritance intact.
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
893
936
|
if (changed) {
|
|
894
937
|
fs.writeFileSync(sandboxConfigPath, `${toml.stringify(sandboxParsed)}\n`, 'utf8');
|
|
895
938
|
}
|
|
@@ -1042,7 +1085,7 @@ function runEngineTaskCommand(engine: string, cmd: string, args: string[], opts:
|
|
|
1042
1085
|
}
|
|
1043
1086
|
|
|
1044
1087
|
export function buildImage(
|
|
1045
|
-
config: SandboxCreateConfig,
|
|
1088
|
+
config: Pick<SandboxCreateConfig, 'project' | 'imageName' | 'repoRoot'> & { engine?: string | null },
|
|
1046
1089
|
tools: SandboxTool[],
|
|
1047
1090
|
dockerfilePath: string,
|
|
1048
1091
|
imageSignature: string,
|
|
@@ -1060,7 +1103,7 @@ export function buildImage(
|
|
|
1060
1103
|
env?: NodeJS.ProcessEnv;
|
|
1061
1104
|
} = {}
|
|
1062
1105
|
): void {
|
|
1063
|
-
const selectedEngine = engine ?? detectEngine(config);
|
|
1106
|
+
const selectedEngine = engine ?? detectEngine({ engine: config.engine });
|
|
1064
1107
|
const { uid: hostUid, gid: hostGid } = resolveBuildUid({
|
|
1065
1108
|
engine: selectedEngine,
|
|
1066
1109
|
runFn,
|
|
@@ -1342,7 +1385,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1342
1385
|
}
|
|
1343
1386
|
const codexEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'codex');
|
|
1344
1387
|
if (codexEntry) {
|
|
1345
|
-
ensureCodexModelInheritance(codexEntry.dir, effectiveConfig.home);
|
|
1388
|
+
ensureCodexModelInheritance(codexEntry.dir, effectiveConfig.home, codexEntry.tool.containerMount);
|
|
1346
1389
|
ensureCodexWorkspaceTrust(codexEntry.dir);
|
|
1347
1390
|
}
|
|
1348
1391
|
const geminiEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'gemini-cli');
|
|
@@ -21,15 +21,27 @@ import { runOk, runSafe, runSafeEngine } from '../shell.ts';
|
|
|
21
21
|
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
22
22
|
import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.ts';
|
|
23
23
|
import type { SandboxTool } from '../tools.ts';
|
|
24
|
+
import { fetchSandboxRows } from './list-running.ts';
|
|
25
|
+
import { lookupShortIdByBranch } from '../../task/short-id.ts';
|
|
24
26
|
|
|
25
|
-
const USAGE = `Usage:
|
|
27
|
+
const USAGE = `Usage:
|
|
28
|
+
ai sandbox rm <branch> Remove one sandbox (branch | TASK-id | short id)
|
|
29
|
+
ai sandbox rm --all [--dry-run] [--yes] Remove every sandbox not bound to an active task
|
|
30
|
+
ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
|
|
26
31
|
export { assertManagedPath } from '../managed-fs.ts';
|
|
27
32
|
|
|
28
33
|
function projectToolDirs(config: SandboxConfig, tools: SandboxTool[]): string[] {
|
|
29
34
|
return tools.flatMap((tool) => toolProjectDirCandidates(tool, config.project));
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
type RmOneOptions = { assumeYes?: boolean; quiet?: boolean };
|
|
38
|
+
|
|
39
|
+
async function rmOne(
|
|
40
|
+
config: SandboxConfig,
|
|
41
|
+
tools: SandboxTool[],
|
|
42
|
+
branch: string,
|
|
43
|
+
options: RmOneOptions = {}
|
|
44
|
+
): Promise<void> {
|
|
33
45
|
assertValidBranchName(branch);
|
|
34
46
|
const engine = detectEngine(config);
|
|
35
47
|
let effectiveBranch = branch;
|
|
@@ -39,7 +51,9 @@ async function rmOne(config: SandboxConfig, tools: SandboxTool[], branch: string
|
|
|
39
51
|
candidates: toolConfigDirCandidates(tool, config.project, branch)
|
|
40
52
|
}));
|
|
41
53
|
|
|
42
|
-
|
|
54
|
+
if (!options.quiet) {
|
|
55
|
+
p.intro(pc.cyan(`Removing sandbox for ${branch}`));
|
|
56
|
+
}
|
|
43
57
|
|
|
44
58
|
const existing = runSafeEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
45
59
|
const matchedContainers = containerNameCandidates(config, branch)
|
|
@@ -74,10 +88,12 @@ async function rmOne(config: SandboxConfig, tools: SandboxTool[], branch: string
|
|
|
74
88
|
|
|
75
89
|
const existingWorktrees = worktreeCandidates.filter((candidate) => fs.existsSync(candidate));
|
|
76
90
|
if (existingWorktrees.length > 0) {
|
|
77
|
-
const shouldRemoveWorktree =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
const shouldRemoveWorktree = options.assumeYes
|
|
92
|
+
? true
|
|
93
|
+
: await p.confirm({
|
|
94
|
+
message: `Remove worktree(s): ${existingWorktrees.join(', ')}?`,
|
|
95
|
+
initialValue: true
|
|
96
|
+
});
|
|
81
97
|
|
|
82
98
|
if (p.isCancel(shouldRemoveWorktree)) {
|
|
83
99
|
p.outro('Cancelled');
|
|
@@ -89,10 +105,12 @@ async function rmOne(config: SandboxConfig, tools: SandboxTool[], branch: string
|
|
|
89
105
|
removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree);
|
|
90
106
|
}
|
|
91
107
|
|
|
92
|
-
const shouldDeleteBranch =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
const shouldDeleteBranch = options.assumeYes
|
|
109
|
+
? true
|
|
110
|
+
: await p.confirm({
|
|
111
|
+
message: `Also delete local branch '${effectiveBranch}'?`,
|
|
112
|
+
initialValue: true
|
|
113
|
+
});
|
|
96
114
|
|
|
97
115
|
if (!p.isCancel(shouldDeleteBranch) && shouldDeleteBranch) {
|
|
98
116
|
if (!runOk('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch])) {
|
|
@@ -116,20 +134,24 @@ async function rmOne(config: SandboxConfig, tools: SandboxTool[], branch: string
|
|
|
116
134
|
|
|
117
135
|
const shareBranch = shareBranchDir(config, effectiveBranch);
|
|
118
136
|
if (fs.existsSync(shareBranch)) {
|
|
119
|
-
const shouldRemoveShare =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
137
|
+
const shouldRemoveShare = options.assumeYes
|
|
138
|
+
? true
|
|
139
|
+
: await p.confirm({
|
|
140
|
+
message: `Remove share dir for branch '${effectiveBranch}' (${shareBranch})?`,
|
|
141
|
+
initialValue: true
|
|
142
|
+
});
|
|
123
143
|
if (!p.isCancel(shouldRemoveShare) && shouldRemoveShare) {
|
|
124
144
|
removeManagedDir(config.shareBase, shareBranch);
|
|
125
145
|
p.log.success(`Share dir removed: ${shareBranch}`);
|
|
126
146
|
}
|
|
127
147
|
}
|
|
128
148
|
|
|
129
|
-
|
|
149
|
+
if (!options.quiet) {
|
|
150
|
+
p.outro(pc.green('Sandbox removed'));
|
|
151
|
+
}
|
|
130
152
|
}
|
|
131
153
|
|
|
132
|
-
async function
|
|
154
|
+
async function rmPurge(config: SandboxConfig, tools: SandboxTool[]): Promise<void> {
|
|
133
155
|
const engine = detectEngine(config);
|
|
134
156
|
p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
|
|
135
157
|
|
|
@@ -231,6 +253,70 @@ async function rmAll(config: SandboxConfig, tools: SandboxTool[]): Promise<void>
|
|
|
231
253
|
p.outro(pc.green('All project sandboxes removed'));
|
|
232
254
|
}
|
|
233
255
|
|
|
256
|
+
async function rmUnbound(
|
|
257
|
+
config: SandboxConfig,
|
|
258
|
+
tools: SandboxTool[],
|
|
259
|
+
options: { dryRun: boolean; assumeYes: boolean }
|
|
260
|
+
): Promise<void> {
|
|
261
|
+
const engine = detectEngine(config);
|
|
262
|
+
const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
263
|
+
const removable = [...running, ...nonRunning].filter(
|
|
264
|
+
(row) => row.branch && lookupShortIdByBranch(row.branch, config.repoRoot) === null
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
p.intro(pc.cyan(`Removing sandboxes not bound to an active task for ${config.project}`));
|
|
268
|
+
|
|
269
|
+
if (removable.length === 0) {
|
|
270
|
+
p.outro('No removable sandboxes: every container is bound to an active task (or none exist)');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const row of removable) {
|
|
275
|
+
p.log.message(`${row.name} ${row.branch}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (options.dryRun) {
|
|
279
|
+
p.outro(`Dry run: ${removable.length} sandbox(es) would be removed, nothing deleted`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!options.assumeYes) {
|
|
284
|
+
if (!process.stdin.isTTY) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
'Refusing to remove sandboxes without confirmation in a non-interactive shell; pass --yes to proceed.'
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const confirmed = await p.confirm({
|
|
290
|
+
message: `Remove these ${removable.length} sandbox(es)?`,
|
|
291
|
+
initialValue: false
|
|
292
|
+
});
|
|
293
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
294
|
+
p.outro('Cancelled');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const failures: { branch: string; message: string }[] = [];
|
|
300
|
+
for (const row of removable) {
|
|
301
|
+
try {
|
|
302
|
+
await rmOne(config, tools, row.branch, { assumeYes: true, quiet: true });
|
|
303
|
+
} catch (error) {
|
|
304
|
+
failures.push({ branch: row.branch, message: error instanceof Error ? error.message : String(error) });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (failures.length > 0) {
|
|
309
|
+
for (const failure of failures) {
|
|
310
|
+
p.log.error(`Failed to remove '${failure.branch}': ${failure.message}`);
|
|
311
|
+
}
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Removed ${removable.length - failures.length}/${removable.length} sandbox(es); ${failures.length} failed`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
p.outro(pc.green(`Removed ${removable.length} sandbox(es)`));
|
|
318
|
+
}
|
|
319
|
+
|
|
234
320
|
export async function rm(args: string[]): Promise<void> {
|
|
235
321
|
const { values, positionals } = parseArgs({
|
|
236
322
|
args,
|
|
@@ -238,6 +324,9 @@ export async function rm(args: string[]): Promise<void> {
|
|
|
238
324
|
strict: true,
|
|
239
325
|
options: {
|
|
240
326
|
all: { type: 'boolean' },
|
|
327
|
+
purge: { type: 'boolean' },
|
|
328
|
+
'dry-run': { type: 'boolean' },
|
|
329
|
+
yes: { type: 'boolean', short: 'y' },
|
|
241
330
|
help: { type: 'boolean', short: 'h' }
|
|
242
331
|
}
|
|
243
332
|
});
|
|
@@ -247,15 +336,35 @@ export async function rm(args: string[]): Promise<void> {
|
|
|
247
336
|
return;
|
|
248
337
|
}
|
|
249
338
|
|
|
250
|
-
if (
|
|
339
|
+
if (values.all && values.purge) {
|
|
340
|
+
throw new Error('--all and --purge are mutually exclusive');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if ((values['dry-run'] || values.yes) && !values.all) {
|
|
344
|
+
throw new Error('--dry-run and --yes only apply to --all');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if ((values.all || values.purge) && positionals.length > 0) {
|
|
348
|
+
throw new Error(`${values.all ? '--all' : '--purge'} does not take a branch argument`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!values.all && !values.purge && positionals.length !== 1) {
|
|
251
352
|
throw new Error(USAGE);
|
|
252
353
|
}
|
|
253
354
|
|
|
254
355
|
const config = loadConfig();
|
|
255
356
|
const tools = resolveTools(config);
|
|
256
357
|
|
|
358
|
+
if (values.purge) {
|
|
359
|
+
await rmPurge(config, tools);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
257
363
|
if (values.all) {
|
|
258
|
-
await
|
|
364
|
+
await rmUnbound(config, tools, {
|
|
365
|
+
dryRun: Boolean(values['dry-run']),
|
|
366
|
+
assumeYes: Boolean(values.yes)
|
|
367
|
+
});
|
|
259
368
|
return;
|
|
260
369
|
}
|
|
261
370
|
|
package/lib/sandbox/index.ts
CHANGED
|
@@ -6,9 +6,6 @@ Commands:
|
|
|
6
6
|
Enter sandbox or run a command. N (bare) is the
|
|
7
7
|
recommended form for task short ids (e.g.
|
|
8
8
|
'ai sandbox exec 11'); '#N' is also accepted.
|
|
9
|
-
start <branch | TASK-id | N | '#N'>
|
|
10
|
-
Start an existing stopped sandbox container
|
|
11
|
-
(e.g. after the Docker daemon restarted)
|
|
12
9
|
ls List sandboxes for the current project (the '#'
|
|
13
10
|
column is a display-only row number; the 'SHORT'
|
|
14
11
|
column shows the active task short id, '-' if none)
|
|
@@ -16,7 +13,12 @@ Commands:
|
|
|
16
13
|
rebuild [--quiet] [--refresh]
|
|
17
14
|
Rebuild the sandbox image (--refresh pulls base + tools)
|
|
18
15
|
refresh Sync host Claude Code credentials to all sandbox copies
|
|
19
|
-
rm <branch>
|
|
16
|
+
rm <branch> | --all | --purge
|
|
17
|
+
Remove one sandbox, all sandboxes not bound to an
|
|
18
|
+
active task (--all), or tear down everything (--purge)
|
|
19
|
+
start <branch | TASK-id | N | '#N'>
|
|
20
|
+
Start an existing stopped sandbox container
|
|
21
|
+
(e.g. after the Docker daemon restarted)
|
|
20
22
|
vm status|start|stop Manage the sandbox VM (macOS) or check the backend (Windows)
|
|
21
23
|
|
|
22
24
|
Run 'ai sandbox <command> --help' for details.`;
|
|
@@ -49,6 +51,21 @@ export async function runSandbox(args: string[]): Promise<void> {
|
|
|
49
51
|
}
|
|
50
52
|
break;
|
|
51
53
|
}
|
|
54
|
+
case 'ls': {
|
|
55
|
+
const { ls } = await import('./commands/ls.ts');
|
|
56
|
+
ls(rest);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case 'prune': {
|
|
60
|
+
const { prune } = await import('./commands/prune.ts');
|
|
61
|
+
await prune(rest);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case 'rebuild': {
|
|
65
|
+
const { rebuild } = await import('./commands/rebuild.ts');
|
|
66
|
+
await rebuild(rest);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
52
69
|
case 'refresh': {
|
|
53
70
|
const { refresh } = await import('./commands/refresh.ts');
|
|
54
71
|
const exitCode = await refresh(rest);
|
|
@@ -57,24 +74,14 @@ export async function runSandbox(args: string[]): Promise<void> {
|
|
|
57
74
|
}
|
|
58
75
|
break;
|
|
59
76
|
}
|
|
60
|
-
case 'start': {
|
|
61
|
-
const { start } = await import('./commands/start.ts');
|
|
62
|
-
await start(rest);
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
case 'ls': {
|
|
66
|
-
const { ls } = await import('./commands/ls.ts');
|
|
67
|
-
ls(rest);
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
77
|
case 'rm': {
|
|
71
78
|
const { rm } = await import('./commands/rm.ts');
|
|
72
79
|
await rm(rest);
|
|
73
80
|
break;
|
|
74
81
|
}
|
|
75
|
-
case '
|
|
76
|
-
const {
|
|
77
|
-
await
|
|
82
|
+
case 'start': {
|
|
83
|
+
const { start } = await import('./commands/start.ts');
|
|
84
|
+
await start(rest);
|
|
78
85
|
break;
|
|
79
86
|
}
|
|
80
87
|
case 'vm': {
|
|
@@ -82,11 +89,6 @@ export async function runSandbox(args: string[]): Promise<void> {
|
|
|
82
89
|
await vm(rest);
|
|
83
90
|
break;
|
|
84
91
|
}
|
|
85
|
-
case 'rebuild': {
|
|
86
|
-
const { rebuild } = await import('./commands/rebuild.ts');
|
|
87
|
-
await rebuild(rest);
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
92
|
default:
|
|
91
93
|
throw new Error(`Unknown sandbox command: ${subcommand}`);
|
|
92
94
|
}
|
|
@@ -18,7 +18,7 @@ symlink under \`$HOME\` (e.g. \`.tmux.conf\` -> \`/home/devuser/.tmux.conf\`),
|
|
|
18
18
|
overriding image defaults so your editor, shell, and tool preferences follow
|
|
19
19
|
you across \`ai sandbox destroy + create\`.
|
|
20
20
|
|
|
21
|
-
See: https://github.com/fitlab-ai/agent-infra/blob/main/
|
|
21
|
+
See: https://github.com/fitlab-ai/agent-infra/blob/main/docs/en/sandbox.md#user-level-dotfiles-channel
|
|
22
22
|
|
|
23
23
|
Common usage - drop files or symlinks here:
|
|
24
24
|
|
|
@@ -46,7 +46,7 @@ only writes \`README.md\` when it is missing, never when it already exists.
|
|
|
46
46
|
(例如 \`.tmux.conf -> /home/devuser/.tmux.conf\`),覆盖镜像默认值,让你的编辑器、
|
|
47
47
|
shell、工具偏好跨 \`ai sandbox destroy + create\` 持久存在。
|
|
48
48
|
|
|
49
|
-
参考:https://github.com/fitlab-ai/agent-infra/blob/main/
|
|
49
|
+
参考:https://github.com/fitlab-ai/agent-infra/blob/main/docs/zh-CN/sandbox.md#用户级-dotfiles-通道
|
|
50
50
|
|
|
51
51
|
常见用法:把文件或符号链接放进来:
|
|
52
52
|
|
|
@@ -72,7 +72,7 @@ This directory is mounted **read-write** into every sandbox container of this
|
|
|
72
72
|
project at \`/share/common\`, regardless of branch. Drop files here to share
|
|
73
73
|
between host and any sandbox without polluting the git worktree.
|
|
74
74
|
|
|
75
|
-
See: https://github.com/fitlab-ai/agent-infra/blob/main/
|
|
75
|
+
See: https://github.com/fitlab-ai/agent-infra/blob/main/docs/en/sandbox.md#host-sandbox-file-exchange
|
|
76
76
|
|
|
77
77
|
This file is safe to delete; the next \`ai sandbox create\` will re-create it.
|
|
78
78
|
|
|
@@ -83,7 +83,7 @@ This file is safe to delete; the next \`ai sandbox create\` will re-create it.
|
|
|
83
83
|
该目录被以**读写**方式挂载到本项目所有 sandbox 容器的 \`/share/common\`,
|
|
84
84
|
跨分支可见。可用来在宿主和任意 sandbox 之间传文件,无需弄脏 git worktree。
|
|
85
85
|
|
|
86
|
-
参考:https://github.com/fitlab-ai/agent-infra/blob/main/
|
|
86
|
+
参考:https://github.com/fitlab-ai/agent-infra/blob/main/docs/zh-CN/sandbox.md#宿主-沙箱文件交换
|
|
87
87
|
|
|
88
88
|
该文件可以安全删除;下一次 \`ai sandbox create\` 会重新生成。
|
|
89
89
|
`;
|
|
@@ -94,7 +94,7 @@ This directory is mounted **read-write** into the sandbox container of this
|
|
|
94
94
|
project's current branch at \`/share/branch\`. Files here are exclusive to this
|
|
95
95
|
branch's sandbox and do not leak across branches.
|
|
96
96
|
|
|
97
|
-
See: https://github.com/fitlab-ai/agent-infra/blob/main/
|
|
97
|
+
See: https://github.com/fitlab-ai/agent-infra/blob/main/docs/en/sandbox.md#host-sandbox-file-exchange
|
|
98
98
|
|
|
99
99
|
This file is safe to delete; the next \`ai sandbox create\` will re-create it.
|
|
100
100
|
|
|
@@ -105,7 +105,7 @@ This file is safe to delete; the next \`ai sandbox create\` will re-create it.
|
|
|
105
105
|
该目录被以**读写**方式挂载到本项目当前分支 sandbox 容器的 \`/share/branch\`,
|
|
106
106
|
仅当前分支可见,不会跨分支泄漏。
|
|
107
107
|
|
|
108
|
-
参考:https://github.com/fitlab-ai/agent-infra/blob/main/
|
|
108
|
+
参考:https://github.com/fitlab-ai/agent-infra/blob/main/docs/zh-CN/sandbox.md#宿主-沙箱文件交换
|
|
109
109
|
|
|
110
110
|
该文件可以安全删除;下一次 \`ai sandbox create\` 会重新生成。
|
|
111
111
|
`;
|