@fitlab-ai/agent-infra 0.9.4 → 0.9.6
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +13 -3
- package/dist/lib/agent-clients/adapter.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
- package/dist/lib/agent-clients/adapters/codex.js +3 -3
- package/dist/lib/git/workflow.js +0 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +300 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +56 -0
- package/dist/lib/internal/task-orchestration.js +71 -31
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +16 -16
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +318 -89
- package/dist/lib/sandbox/control/client.js +152 -22
- package/dist/lib/sandbox/control/executor.js +136 -0
- package/dist/lib/sandbox/control/lifecycle.js +348 -0
- package/dist/lib/sandbox/control/protocol.js +73 -18
- package/dist/lib/sandbox/control/server.js +235 -80
- package/dist/lib/sandbox/control/state.js +181 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +35 -24
- package/dist/lib/sandbox/recovery.js +262 -45
- package/dist/lib/sandbox/workspace-view.js +80 -11
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/process-control.js +6 -0
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/artifact-lifecycle.js +5 -4
- package/dist/lib/task/codex-orchestration.js +161 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +50 -12
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/delegation-receipts.js +25 -0
- package/dist/lib/task/events.js +10 -4
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/orchestration.js +283 -18
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/verification-engine.js +38 -6
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
- package/lib/agent-clients/adapters/codex.ts +3 -3
- package/lib/git/workflow.ts +0 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +308 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +61 -0
- package/lib/internal/task-orchestration.ts +57 -31
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +18 -21
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +368 -103
- package/lib/sandbox/control/client.ts +173 -25
- package/lib/sandbox/control/executor.ts +163 -0
- package/lib/sandbox/control/lifecycle.ts +342 -0
- package/lib/sandbox/control/protocol.ts +110 -48
- package/lib/sandbox/control/server.ts +252 -85
- package/lib/sandbox/control/state.ts +196 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +31 -13
- package/lib/sandbox/recovery.ts +301 -60
- package/lib/sandbox/workspace-view.ts +89 -12
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/process-control.ts +11 -0
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/artifact-lifecycle.ts +5 -4
- package/lib/task/codex-orchestration.ts +233 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +75 -12
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/delegation-receipts.ts +52 -3
- package/lib/task/events.ts +9 -4
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/orchestration.ts +381 -22
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/verification-engine.ts +42 -8
- package/lib/task/verification.ts +2 -1
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/task-management.en.md +1 -0
- package/templates/.agents/rules/task-management.zh-CN.md +1 -0
- package/templates/.agents/skills/commit/SKILL.en.md +3 -9
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
|
@@ -7,22 +7,25 @@ import { loadConfig } from '../config.js';
|
|
|
7
7
|
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel, shareBranchDir, shellConfigDirCandidates, worktreeDirCandidates } from '../constants.js';
|
|
8
8
|
import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.js';
|
|
9
9
|
import { pruneSandboxDanglingImages } from '../image-prune.js';
|
|
10
|
-
import { removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
|
|
11
|
-
import { runOk, runSafe, runSafeEngine } from '../shell.js';
|
|
10
|
+
import { assertManagedPath, removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
|
|
11
|
+
import { runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.js';
|
|
12
12
|
import { resolveSandboxTarget } from '../workspace-identity.js';
|
|
13
|
+
import { sandboxControlPaths, sandboxWorkspaceViewPaths } from '../workspace-view.js';
|
|
14
|
+
import { quiesceSandboxControlRoot, readSandboxControlManifest } from '../control/lifecycle.js';
|
|
13
15
|
import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.js';
|
|
14
16
|
import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
|
|
15
17
|
import { fetchSandboxRows } from './list-running.js';
|
|
16
18
|
import { lookupShortIdByBranch } from '../../task/short-id.js';
|
|
19
|
+
import { createCleanPermit, createDiscardPermit, formatWorktreeSnapshot, inspectWorktrees, verifyWorktreePermit } from '../worktree-safety.js';
|
|
17
20
|
const USAGE = `Usage:
|
|
18
21
|
ai sandbox rm <branch> Remove one sandbox (branch | TASK-id | short id)
|
|
19
|
-
ai sandbox rm --
|
|
22
|
+
ai sandbox rm --unbound [--dry-run] [--yes] Remove every sandbox not bound to an active task
|
|
20
23
|
ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
|
|
21
24
|
export { assertManagedPath } from '../managed-fs.js';
|
|
22
25
|
function projectToolDirs(config, tools) {
|
|
23
26
|
return tools.flatMap((tool) => toolProjectDirCandidates(tool, config.project));
|
|
24
27
|
}
|
|
25
|
-
|
|
28
|
+
function resolveRmTarget(config, tools, branch) {
|
|
26
29
|
assertValidBranchName(branch);
|
|
27
30
|
const engine = detectEngine(config);
|
|
28
31
|
let effectiveBranch = branch;
|
|
@@ -31,14 +34,10 @@ async function rmOne(config, tools, branch, options = {}) {
|
|
|
31
34
|
tool,
|
|
32
35
|
candidates: toolConfigDirCandidates(tool, config.project, branch)
|
|
33
36
|
}));
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
const existing = runSafeEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
38
|
-
const matchedContainers = containerNameCandidates(config, branch)
|
|
39
|
-
.filter((name) => existing.includes(name));
|
|
37
|
+
const existing = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
38
|
+
const matchedContainers = containerNameCandidates(config, branch).filter((name) => existing.includes(name));
|
|
40
39
|
if (matchedContainers.length > 0) {
|
|
41
|
-
const resolvedBranch =
|
|
40
|
+
const resolvedBranch = runEngine(engine, 'docker', [
|
|
42
41
|
'inspect',
|
|
43
42
|
'-f',
|
|
44
43
|
`{{ index .Config.Labels "${sandboxBranchLabel(config)}" }}`,
|
|
@@ -52,77 +51,255 @@ async function rmOne(config, tools, branch, options = {}) {
|
|
|
52
51
|
candidates: toolConfigDirCandidates(tool, config.project, effectiveBranch)
|
|
53
52
|
}));
|
|
54
53
|
}
|
|
54
|
+
}
|
|
55
|
+
const workspace = resolveSandboxTarget(effectiveBranch, config.repoRoot).workspace;
|
|
56
|
+
const identities = workspace.mode === 'task-bound'
|
|
57
|
+
? [workspace, { mode: 'branch-only' }]
|
|
58
|
+
: [workspace];
|
|
59
|
+
const containers = [...new Set([...containerNameCandidates(config, effectiveBranch), ...matchedContainers])];
|
|
60
|
+
const controlRoots = containers.flatMap((container) => identities.map((identity) => sandboxControlPaths({
|
|
61
|
+
base: config.controlBase, project: config.project, container, identity
|
|
62
|
+
}).root));
|
|
63
|
+
const workspaceViewRoots = containers.flatMap((container) => identities.map((identity) => sandboxWorkspaceViewPaths({
|
|
64
|
+
base: config.workspaceViewBase, project: config.project, container, identity
|
|
65
|
+
}).root));
|
|
66
|
+
return {
|
|
67
|
+
branch,
|
|
68
|
+
effectiveBranch,
|
|
69
|
+
engine,
|
|
70
|
+
matchedContainers,
|
|
71
|
+
existingWorktrees: worktreeCandidates.filter((candidate) => fs.existsSync(candidate)),
|
|
72
|
+
toolCandidates,
|
|
73
|
+
workspace,
|
|
74
|
+
controlRoots: [...new Set(controlRoots)],
|
|
75
|
+
workspaceViewRoots: [...new Set(workspaceViewRoots)]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function assertControlRootMatchesTarget(root, effectiveBranch, workspace) {
|
|
79
|
+
const manifestPath = path.join(root, 'manifest.json');
|
|
80
|
+
if (!fs.existsSync(manifestPath))
|
|
81
|
+
return;
|
|
82
|
+
const manifest = readSandboxControlManifest(manifestPath);
|
|
83
|
+
const identityMatches = workspace.mode === 'task-bound'
|
|
84
|
+
? manifest.mode === 'task-bound' && manifest.taskId === workspace.taskId
|
|
85
|
+
: manifest.mode === 'branch-only';
|
|
86
|
+
const branchOnlyFallback = workspace.mode === 'task-bound' && manifest.mode === 'branch-only';
|
|
87
|
+
if (manifest.branch !== effectiveBranch || (!identityMatches && !branchOnlyFallback)) {
|
|
88
|
+
throw new Error(`SANDBOX_CONTROL_TARGET_MISMATCH: ${root}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function assertLegacyCandidateEvidence(candidate, base, config, effectiveBranch, matchedContainers) {
|
|
92
|
+
const relative = path.relative(path.join(base, config.project), candidate);
|
|
93
|
+
const [container, identity] = relative.split(path.sep);
|
|
94
|
+
const [canonical, legacy] = containerNameCandidates(config, effectiveBranch);
|
|
95
|
+
if (!container || !identity || legacy === canonical || container !== legacy || matchedContainers.includes(container))
|
|
96
|
+
return;
|
|
97
|
+
const manifestPath = path.join(config.controlBase, config.project, container, identity, 'manifest.json');
|
|
98
|
+
if (!fs.existsSync(manifestPath)) {
|
|
99
|
+
throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: ${candidate}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function assertRemoved(target, label) {
|
|
103
|
+
if (fs.existsSync(target))
|
|
104
|
+
throw new Error(`${label} still exists after removal: ${target}`);
|
|
105
|
+
}
|
|
106
|
+
function removeEmptyManagedParent(base, directory) {
|
|
107
|
+
const parent = path.dirname(directory);
|
|
108
|
+
if (path.resolve(parent) === path.resolve(base))
|
|
109
|
+
return;
|
|
110
|
+
assertManagedPath(base, parent);
|
|
111
|
+
try {
|
|
112
|
+
fs.rmdirSync(parent);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code ?? ''))
|
|
116
|
+
return;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
assertRemoved(parent, 'Empty sandbox container directory');
|
|
120
|
+
}
|
|
121
|
+
async function quiesceProjectControlRoots(config) {
|
|
122
|
+
const projectRoot = path.join(config.controlBase, config.project);
|
|
123
|
+
if (!fs.existsSync(projectRoot))
|
|
124
|
+
return;
|
|
125
|
+
assertManagedPath(config.controlBase, projectRoot);
|
|
126
|
+
const projectStat = fs.lstatSync(projectRoot);
|
|
127
|
+
if (!projectStat.isDirectory() || projectStat.isSymbolicLink())
|
|
128
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
129
|
+
for (const container of fs.readdirSync(projectRoot, { withFileTypes: true })) {
|
|
130
|
+
if (!container.isDirectory() || container.isSymbolicLink())
|
|
131
|
+
continue;
|
|
132
|
+
const containerRoot = path.join(projectRoot, container.name);
|
|
133
|
+
for (const identity of fs.readdirSync(containerRoot, { withFileTypes: true })) {
|
|
134
|
+
if (!identity.isDirectory() || identity.isSymbolicLink())
|
|
135
|
+
continue;
|
|
136
|
+
await quiesceSandboxControlRoot(path.join(containerRoot, identity.name));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function inspectionBlockers(inspections) {
|
|
141
|
+
return inspections.filter((inspection) => inspection.status !== 'clean');
|
|
142
|
+
}
|
|
143
|
+
function blockerMessage(blockers) {
|
|
144
|
+
return blockers.map((blocker) => {
|
|
145
|
+
if (blocker.status === 'failed')
|
|
146
|
+
return `${JSON.stringify(blocker.worktree)}: ${blocker.message}`;
|
|
147
|
+
return `${formatWorktreeSnapshot(blocker.snapshot)}\n Action: commit, stash, clean, or remove this sandbox interactively.`;
|
|
148
|
+
}).join('\n');
|
|
149
|
+
}
|
|
150
|
+
function cleanPermits(inspections) {
|
|
151
|
+
const permits = new Map();
|
|
152
|
+
for (const inspection of inspections) {
|
|
153
|
+
if (inspection.status === 'clean')
|
|
154
|
+
permits.set(inspection.snapshot.worktree, createCleanPermit(inspection.snapshot));
|
|
155
|
+
}
|
|
156
|
+
return permits;
|
|
157
|
+
}
|
|
158
|
+
async function authorizeWorktrees(worktrees, { allowDirtyDiscard, assumeYes }, { interactive = Boolean(process.stdin.isTTY), confirm = p.confirm, isCancel = p.isCancel } = {}) {
|
|
159
|
+
const inspections = inspectWorktrees(worktrees);
|
|
160
|
+
const failures = inspections.filter((inspection) => inspection.status === 'failed');
|
|
161
|
+
if (failures.length > 0)
|
|
162
|
+
throw new Error(`Unable to inspect worktree(s):\n${blockerMessage(failures)}`);
|
|
163
|
+
const permits = cleanPermits(inspections);
|
|
164
|
+
for (const inspection of inspections) {
|
|
165
|
+
if (inspection.status !== 'dirty')
|
|
166
|
+
continue;
|
|
167
|
+
if (!allowDirtyDiscard || assumeYes || !interactive) {
|
|
168
|
+
throw new Error(`Refusing to remove dirty worktree(s):\n${blockerMessage([inspection])}`);
|
|
169
|
+
}
|
|
170
|
+
p.log.warn(formatWorktreeSnapshot(inspection.snapshot));
|
|
171
|
+
const confirmed = await confirm({
|
|
172
|
+
message: 'Discard these exact uncommitted changes?',
|
|
173
|
+
initialValue: false
|
|
174
|
+
});
|
|
175
|
+
if (isCancel(confirmed) || !confirmed)
|
|
176
|
+
throw new Error('Dirty worktree removal cancelled; nothing was deleted');
|
|
177
|
+
permits.set(inspection.snapshot.worktree, createDiscardPermit(inspection.snapshot));
|
|
178
|
+
}
|
|
179
|
+
return permits;
|
|
180
|
+
}
|
|
181
|
+
async function rmOne(config, tools, branch, options = {}) {
|
|
182
|
+
const target = options.target ?? resolveRmTarget(config, tools, branch);
|
|
183
|
+
const { effectiveBranch, engine, matchedContainers, existingWorktrees, toolCandidates } = target;
|
|
184
|
+
const { workspace, controlRoots, workspaceViewRoots } = target;
|
|
185
|
+
const confirm = options.prompt?.confirm ?? p.confirm;
|
|
186
|
+
const isCancel = options.prompt?.isCancel ?? p.isCancel;
|
|
187
|
+
if (!options.quiet) {
|
|
188
|
+
p.intro(pc.cyan(`Removing sandbox for ${branch}`));
|
|
189
|
+
}
|
|
190
|
+
const permits = options.permits
|
|
191
|
+
? new Map(options.permits)
|
|
192
|
+
: await authorizeWorktrees(existingWorktrees, {
|
|
193
|
+
allowDirtyDiscard: options.allowDirtyDiscard ?? true,
|
|
194
|
+
assumeYes: Boolean(options.assumeYes)
|
|
195
|
+
}, options.prompt);
|
|
196
|
+
for (const worktree of existingWorktrees) {
|
|
197
|
+
const permit = permits.get(path.resolve(worktree));
|
|
198
|
+
if (!permit)
|
|
199
|
+
throw new Error(`Missing worktree removal permit: ${worktree}`);
|
|
200
|
+
verifyWorktreePermit(permit);
|
|
201
|
+
}
|
|
202
|
+
const shouldRemoveWorktree = existingWorktrees.length === 0
|
|
203
|
+
? false
|
|
204
|
+
: options.assumeYes
|
|
205
|
+
? true
|
|
206
|
+
: await confirm({
|
|
207
|
+
message: `Remove worktree(s): ${existingWorktrees.join(', ')}?`,
|
|
208
|
+
initialValue: true
|
|
209
|
+
});
|
|
210
|
+
if (isCancel(shouldRemoveWorktree)) {
|
|
211
|
+
p.outro('Cancelled');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
for (const root of controlRoots.filter((candidate) => fs.existsSync(candidate))) {
|
|
215
|
+
assertLegacyCandidateEvidence(root, config.controlBase, config, effectiveBranch, matchedContainers);
|
|
216
|
+
assertControlRootMatchesTarget(root, effectiveBranch, workspace);
|
|
217
|
+
await quiesceSandboxControlRoot(root);
|
|
218
|
+
}
|
|
219
|
+
if (matchedContainers.length > 0) {
|
|
55
220
|
const spinner = p.spinner();
|
|
56
221
|
spinner.start(`Stopping container(s): ${matchedContainers.join(', ')}`);
|
|
57
222
|
for (const name of matchedContainers) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
[path.join(config.controlBase, config.project), 'Control channel']
|
|
63
|
-
]) {
|
|
64
|
-
const directory = path.join(base, name);
|
|
65
|
-
if (fs.existsSync(directory)) {
|
|
66
|
-
removeManagedDir(base, directory);
|
|
67
|
-
if (!options.quiet)
|
|
68
|
-
p.log.success(`${label} removed: ${directory}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
223
|
+
if (!runOkEngine(engine, 'docker', ['stop', name]))
|
|
224
|
+
throw new Error(`Failed to stop sandbox container: ${name}`);
|
|
225
|
+
if (!runOkEngine(engine, 'docker', ['rm', name]))
|
|
226
|
+
throw new Error(`Failed to remove sandbox container: ${name}`);
|
|
71
227
|
}
|
|
228
|
+
const remaining = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
229
|
+
const leftovers = matchedContainers.filter((name) => remaining.includes(name));
|
|
230
|
+
if (leftovers.length > 0)
|
|
231
|
+
throw new Error(`Sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
|
|
72
232
|
spinner.stop(pc.green(`Removed container(s): ${matchedContainers.join(', ')}`));
|
|
73
233
|
}
|
|
74
234
|
else {
|
|
75
235
|
p.log.warn(`No sandbox container found for '${branch}'`);
|
|
76
236
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
237
|
+
for (const [roots, base, label] of [
|
|
238
|
+
[workspaceViewRoots, path.join(config.workspaceViewBase, config.project), 'Workspace view'],
|
|
239
|
+
[controlRoots, path.join(config.controlBase, config.project), 'Control channel']
|
|
240
|
+
]) {
|
|
241
|
+
for (const directory of roots.filter((candidate) => fs.existsSync(candidate))) {
|
|
242
|
+
assertLegacyCandidateEvidence(directory, base === path.join(config.controlBase, config.project)
|
|
243
|
+
? config.controlBase : config.workspaceViewBase, config, effectiveBranch, matchedContainers);
|
|
244
|
+
removeManagedDir(base, directory);
|
|
245
|
+
assertRemoved(directory, label);
|
|
246
|
+
removeEmptyManagedParent(base, directory);
|
|
247
|
+
if (!options.quiet)
|
|
248
|
+
p.log.success(`${label} removed: ${directory}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (shouldRemoveWorktree) {
|
|
252
|
+
for (const worktree of existingWorktrees) {
|
|
253
|
+
const permit = permits.get(path.resolve(worktree));
|
|
254
|
+
if (!permit)
|
|
255
|
+
throw new Error(`Missing worktree removal permit: ${worktree}`);
|
|
256
|
+
removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree, permit, {
|
|
257
|
+
allowRegisteredPathFallback: engine === ENGINES.WSL2
|
|
258
|
+
});
|
|
259
|
+
assertRemoved(worktree, 'Worktree');
|
|
260
|
+
const registered = runSafe('git', ['-C', config.repoRoot, 'worktree', 'list', '--porcelain'])
|
|
261
|
+
.split('\n').filter((line) => line.startsWith('worktree ')).map((line) => path.resolve(line.slice(9)));
|
|
262
|
+
if (registered.includes(path.resolve(worktree)))
|
|
263
|
+
throw new Error(`Worktree is still registered after removal: ${worktree}`);
|
|
264
|
+
}
|
|
265
|
+
const shouldDeleteBranch = options.assumeYes
|
|
80
266
|
? true
|
|
81
|
-
: await
|
|
82
|
-
message: `
|
|
267
|
+
: await confirm({
|
|
268
|
+
message: `Also delete local branch '${effectiveBranch}'?`,
|
|
83
269
|
initialValue: true
|
|
84
270
|
});
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
if (shouldRemoveWorktree) {
|
|
90
|
-
for (const worktree of existingWorktrees) {
|
|
91
|
-
removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree);
|
|
271
|
+
if (!isCancel(shouldDeleteBranch) && shouldDeleteBranch) {
|
|
272
|
+
if (!runOk('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch])) {
|
|
273
|
+
throw new Error(`Local branch '${effectiveBranch}' was not deleted`);
|
|
92
274
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
: await p.confirm({
|
|
96
|
-
message: `Also delete local branch '${effectiveBranch}'?`,
|
|
97
|
-
initialValue: true
|
|
98
|
-
});
|
|
99
|
-
if (!p.isCancel(shouldDeleteBranch) && shouldDeleteBranch) {
|
|
100
|
-
if (!runOk('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch])) {
|
|
101
|
-
p.log.warn(`Local branch '${effectiveBranch}' was not deleted`);
|
|
102
|
-
}
|
|
275
|
+
if (runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', `refs/heads/${effectiveBranch}`])) {
|
|
276
|
+
throw new Error(`Local branch '${effectiveBranch}' still exists after removal`);
|
|
103
277
|
}
|
|
104
278
|
}
|
|
105
279
|
}
|
|
106
280
|
for (const { tool, candidates } of toolCandidates) {
|
|
107
281
|
for (const dir of candidates.filter((candidate) => fs.existsSync(candidate))) {
|
|
108
282
|
removeManagedDir(tool.sandboxBase, dir);
|
|
283
|
+
assertRemoved(dir, `${tool.name} state`);
|
|
109
284
|
p.log.success(`${tool.name} state removed: ${dir}`);
|
|
110
285
|
}
|
|
111
286
|
}
|
|
112
287
|
for (const dir of shellConfigDirCandidates(config, effectiveBranch).filter((candidate) => fs.existsSync(candidate))) {
|
|
113
288
|
removeManagedDir(config.shellConfigBase, dir);
|
|
289
|
+
assertRemoved(dir, 'Shell config');
|
|
114
290
|
p.log.success(`Shell config removed: ${dir}`);
|
|
115
291
|
}
|
|
116
292
|
const shareBranch = shareBranchDir(config, effectiveBranch);
|
|
117
293
|
if (fs.existsSync(shareBranch)) {
|
|
118
294
|
const shouldRemoveShare = options.assumeYes
|
|
119
295
|
? true
|
|
120
|
-
: await
|
|
296
|
+
: await confirm({
|
|
121
297
|
message: `Remove share dir for branch '${effectiveBranch}' (${shareBranch})?`,
|
|
122
298
|
initialValue: true
|
|
123
299
|
});
|
|
124
|
-
if (!
|
|
300
|
+
if (!isCancel(shouldRemoveShare) && shouldRemoveShare) {
|
|
125
301
|
removeManagedDir(config.shareBase, shareBranch);
|
|
302
|
+
assertRemoved(shareBranch, 'Share dir');
|
|
126
303
|
p.log.success(`Share dir removed: ${shareBranch}`);
|
|
127
304
|
}
|
|
128
305
|
}
|
|
@@ -130,10 +307,31 @@ async function rmOne(config, tools, branch, options = {}) {
|
|
|
130
307
|
p.outro(pc.green('Sandbox removed'));
|
|
131
308
|
}
|
|
132
309
|
}
|
|
133
|
-
async function rmPurge(config, tools) {
|
|
310
|
+
async function rmPurge(config, tools, prompt = {}) {
|
|
134
311
|
const engine = detectEngine(config);
|
|
312
|
+
const confirm = prompt.confirm ?? p.confirm;
|
|
313
|
+
const isCancel = prompt.isCancel ?? p.isCancel;
|
|
135
314
|
p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
|
|
136
|
-
const
|
|
315
|
+
const worktrees = fs.existsSync(config.worktreeBase)
|
|
316
|
+
? fs.readdirSync(config.worktreeBase)
|
|
317
|
+
.map((entry) => path.join(config.worktreeBase, entry))
|
|
318
|
+
.filter((entry) => {
|
|
319
|
+
try {
|
|
320
|
+
return fs.statSync(entry).isDirectory();
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
: [];
|
|
327
|
+
const inspections = inspectWorktrees(worktrees);
|
|
328
|
+
const blockers = inspectionBlockers(inspections);
|
|
329
|
+
if (blockers.length > 0) {
|
|
330
|
+
throw new Error(`Refusing to purge because worktree preflight found blocker(s):\n${blockerMessage(blockers)}`);
|
|
331
|
+
}
|
|
332
|
+
const permits = cleanPermits(inspections);
|
|
333
|
+
await quiesceProjectControlRoots(config);
|
|
334
|
+
const containers = runEngine(engine, 'docker', [
|
|
137
335
|
'ps',
|
|
138
336
|
'-a',
|
|
139
337
|
'--filter',
|
|
@@ -145,53 +343,73 @@ async function rmPurge(config, tools) {
|
|
|
145
343
|
const spinner = p.spinner();
|
|
146
344
|
spinner.start('Stopping project sandbox containers...');
|
|
147
345
|
for (const name of containers.split('\n').filter(Boolean)) {
|
|
148
|
-
|
|
149
|
-
|
|
346
|
+
if (!runOkEngine(engine, 'docker', ['stop', name]))
|
|
347
|
+
throw new Error(`Failed to stop sandbox container: ${name}`);
|
|
348
|
+
if (!runOkEngine(engine, 'docker', ['rm', name]))
|
|
349
|
+
throw new Error(`Failed to remove sandbox container: ${name}`);
|
|
150
350
|
}
|
|
351
|
+
const remaining = runEngine(engine, 'docker', [
|
|
352
|
+
'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
|
|
353
|
+
]);
|
|
354
|
+
if (remaining)
|
|
355
|
+
throw new Error(`Project sandbox container(s) still exist after removal: ${remaining.replaceAll('\n', ', ')}`);
|
|
151
356
|
spinner.stop(pc.green('Project sandbox containers removed'));
|
|
152
357
|
}
|
|
153
358
|
else {
|
|
154
359
|
p.log.warn('No project sandbox containers found');
|
|
155
360
|
}
|
|
156
|
-
if (
|
|
157
|
-
const shouldRemoveWorktrees = await
|
|
361
|
+
if (worktrees.length > 0) {
|
|
362
|
+
const shouldRemoveWorktrees = await confirm({
|
|
158
363
|
message: `Remove all worktrees in ${config.worktreeBase}?`,
|
|
159
364
|
initialValue: true
|
|
160
365
|
});
|
|
161
|
-
if (!
|
|
162
|
-
for (const
|
|
163
|
-
const
|
|
164
|
-
|
|
366
|
+
if (!isCancel(shouldRemoveWorktrees) && shouldRemoveWorktrees) {
|
|
367
|
+
for (const dir of worktrees) {
|
|
368
|
+
const permit = permits.get(path.resolve(dir));
|
|
369
|
+
if (!permit)
|
|
370
|
+
throw new Error(`Missing worktree removal permit: ${dir}`);
|
|
371
|
+
removeWorktreeDir(config.repoRoot, config.worktreeBase, dir, permit, {
|
|
372
|
+
allowRegisteredPathFallback: engine === ENGINES.WSL2
|
|
373
|
+
});
|
|
374
|
+
assertRemoved(dir, 'Worktree');
|
|
165
375
|
}
|
|
166
376
|
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
377
|
+
const registered = new Set(runSafe('git', ['-C', config.repoRoot, 'worktree', 'list', '--porcelain'])
|
|
378
|
+
.split('\n').filter((line) => line.startsWith('worktree ')).map((line) => path.resolve(line.slice(9))));
|
|
379
|
+
const leftovers = worktrees.filter((dir) => registered.has(path.resolve(dir)));
|
|
380
|
+
if (leftovers.length > 0)
|
|
381
|
+
throw new Error(`Worktree(s) still registered after removal: ${leftovers.join(', ')}`);
|
|
167
382
|
}
|
|
168
383
|
}
|
|
169
384
|
for (const dir of projectToolDirs(config, tools)) {
|
|
170
385
|
if (fs.existsSync(dir)) {
|
|
171
386
|
removeManagedDir(path.dirname(dir), dir);
|
|
387
|
+
assertRemoved(dir, 'Tool state');
|
|
172
388
|
p.log.success(`Removed tool state: ${dir}`);
|
|
173
389
|
}
|
|
174
390
|
}
|
|
175
391
|
if (fs.existsSync(config.shellConfigBase) && fs.readdirSync(config.shellConfigBase).length > 0) {
|
|
176
|
-
const shouldRemoveShellConfigs = await
|
|
392
|
+
const shouldRemoveShellConfigs = await confirm({
|
|
177
393
|
message: `Remove all shell config dirs in ${config.shellConfigBase}?`,
|
|
178
394
|
initialValue: true
|
|
179
395
|
});
|
|
180
|
-
if (!
|
|
396
|
+
if (!isCancel(shouldRemoveShellConfigs) && shouldRemoveShellConfigs) {
|
|
181
397
|
for (const entry of fs.readdirSync(config.shellConfigBase)) {
|
|
182
398
|
const dir = path.join(config.shellConfigBase, entry);
|
|
183
399
|
removeManagedDir(config.shellConfigBase, dir);
|
|
400
|
+
assertRemoved(dir, 'Shell config');
|
|
184
401
|
}
|
|
185
402
|
p.log.success(`Project shell config dirs removed: ${config.shellConfigBase}`);
|
|
186
403
|
}
|
|
187
404
|
}
|
|
188
405
|
if (fs.existsSync(config.shareBase) && fs.readdirSync(config.shareBase).length > 0) {
|
|
189
|
-
const shouldRemoveAllShares = await
|
|
406
|
+
const shouldRemoveAllShares = await confirm({
|
|
190
407
|
message: `Remove all share dirs for project (${config.shareBase})?`,
|
|
191
408
|
initialValue: true
|
|
192
409
|
});
|
|
193
|
-
if (!
|
|
410
|
+
if (!isCancel(shouldRemoveAllShares) && shouldRemoveAllShares) {
|
|
194
411
|
removeManagedDir(path.dirname(config.shareBase), config.shareBase);
|
|
412
|
+
assertRemoved(config.shareBase, 'Project share dir');
|
|
195
413
|
p.log.success(`Project share dirs removed: ${config.shareBase}`);
|
|
196
414
|
}
|
|
197
415
|
}
|
|
@@ -202,14 +420,15 @@ async function rmPurge(config, tools) {
|
|
|
202
420
|
const projectDir = path.join(base, config.project);
|
|
203
421
|
if (fs.existsSync(projectDir)) {
|
|
204
422
|
removeManagedDir(base, projectDir);
|
|
423
|
+
assertRemoved(projectDir, `Project ${label}`);
|
|
205
424
|
p.log.success(`Removed project ${label}: ${projectDir}`);
|
|
206
425
|
}
|
|
207
426
|
}
|
|
208
|
-
const shouldRemoveImage = await
|
|
427
|
+
const shouldRemoveImage = await confirm({
|
|
209
428
|
message: `Remove image ${config.imageName}?`,
|
|
210
429
|
initialValue: false
|
|
211
430
|
});
|
|
212
|
-
if (!
|
|
431
|
+
if (!isCancel(shouldRemoveImage) && shouldRemoveImage) {
|
|
213
432
|
runSafeEngine(engine, 'docker', ['rmi', config.imageName]);
|
|
214
433
|
}
|
|
215
434
|
pruneSandboxDanglingImages(config, engine);
|
|
@@ -220,11 +439,11 @@ async function rmPurge(config, tools) {
|
|
|
220
439
|
return;
|
|
221
440
|
}
|
|
222
441
|
const name = engineDisplayName(engine);
|
|
223
|
-
const shouldStopVm = await
|
|
442
|
+
const shouldStopVm = await confirm({
|
|
224
443
|
message: `Stop ${name} VM?`,
|
|
225
444
|
initialValue: false
|
|
226
445
|
});
|
|
227
|
-
if (!
|
|
446
|
+
if (!isCancel(shouldStopVm) && shouldStopVm) {
|
|
228
447
|
stopManagedVm(config);
|
|
229
448
|
}
|
|
230
449
|
}
|
|
@@ -239,30 +458,35 @@ async function rmUnbound(config, tools, options) {
|
|
|
239
458
|
p.outro('No removable sandboxes: every container is bound to an active task (or none exist)');
|
|
240
459
|
return;
|
|
241
460
|
}
|
|
461
|
+
const targets = removable.map((row) => resolveRmTarget(config, tools, row.branch));
|
|
462
|
+
const inspections = inspectWorktrees(targets.flatMap((target) => target.existingWorktrees));
|
|
463
|
+
const blockers = inspectionBlockers(inspections);
|
|
464
|
+
const permits = cleanPermits(inspections);
|
|
242
465
|
for (const row of removable) {
|
|
243
466
|
p.log.message(`${row.name} ${row.branch}`);
|
|
244
467
|
}
|
|
468
|
+
if (blockers.length > 0)
|
|
469
|
+
p.log.error(blockerMessage(blockers));
|
|
245
470
|
if (options.dryRun) {
|
|
246
|
-
p.outro(`Dry run: ${removable.length} sandbox(es)
|
|
471
|
+
p.outro(`Dry run: ${removable.length} sandbox(es) inspected, nothing deleted`);
|
|
247
472
|
return;
|
|
248
473
|
}
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
message: `Remove these ${removable.length} sandbox(es)?`,
|
|
255
|
-
initialValue: false
|
|
256
|
-
});
|
|
257
|
-
if (p.isCancel(confirmed) || !confirmed) {
|
|
258
|
-
p.outro('Cancelled');
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
474
|
+
if (blockers.length > 0) {
|
|
475
|
+
throw new Error(`Refusing batch removal because worktree preflight found blocker(s):\n${blockerMessage(blockers)}`);
|
|
476
|
+
}
|
|
477
|
+
if (!options.assumeYes && !process.stdin.isTTY) {
|
|
478
|
+
throw new Error('Refusing to remove sandboxes without confirmation in a non-interactive shell; pass --yes to proceed.');
|
|
261
479
|
}
|
|
262
480
|
const failures = [];
|
|
263
|
-
for (const row of removable) {
|
|
481
|
+
for (const [index, row] of removable.entries()) {
|
|
264
482
|
try {
|
|
265
|
-
await rmOne(config, tools, row.branch, {
|
|
483
|
+
await rmOne(config, tools, row.branch, {
|
|
484
|
+
assumeYes: options.assumeYes,
|
|
485
|
+
quiet: true,
|
|
486
|
+
target: targets[index],
|
|
487
|
+
permits,
|
|
488
|
+
allowDirtyDiscard: false
|
|
489
|
+
});
|
|
266
490
|
}
|
|
267
491
|
catch (error) {
|
|
268
492
|
failures.push({ branch: row.branch, message: error instanceof Error ? error.message : String(error) });
|
|
@@ -283,26 +507,30 @@ export async function rm(args) {
|
|
|
283
507
|
strict: true,
|
|
284
508
|
options: {
|
|
285
509
|
all: { type: 'boolean' },
|
|
510
|
+
unbound: { type: 'boolean' },
|
|
286
511
|
purge: { type: 'boolean' },
|
|
287
512
|
'dry-run': { type: 'boolean' },
|
|
288
513
|
yes: { type: 'boolean', short: 'y' },
|
|
289
514
|
help: { type: 'boolean', short: 'h' }
|
|
290
515
|
}
|
|
291
516
|
});
|
|
517
|
+
if (values.all) {
|
|
518
|
+
throw new Error('CLI_FLAG_REMOVED: --all was removed; use --unbound');
|
|
519
|
+
}
|
|
292
520
|
if (values.help) {
|
|
293
521
|
process.stdout.write(`${USAGE}\n`);
|
|
294
522
|
return;
|
|
295
523
|
}
|
|
296
|
-
if (values.
|
|
297
|
-
throw new Error('--
|
|
524
|
+
if (values.unbound && values.purge) {
|
|
525
|
+
throw new Error('--unbound and --purge are mutually exclusive');
|
|
298
526
|
}
|
|
299
|
-
if ((values['dry-run'] || values.yes) && !values.
|
|
300
|
-
throw new Error('--dry-run and --yes only apply to --
|
|
527
|
+
if ((values['dry-run'] || values.yes) && !values.unbound) {
|
|
528
|
+
throw new Error('--dry-run and --yes only apply to --unbound');
|
|
301
529
|
}
|
|
302
|
-
if ((values.
|
|
303
|
-
throw new Error(`${values.
|
|
530
|
+
if ((values.unbound || values.purge) && positionals.length > 0) {
|
|
531
|
+
throw new Error(`${values.unbound ? '--unbound' : '--purge'} does not take a branch argument`);
|
|
304
532
|
}
|
|
305
|
-
if (!values.
|
|
533
|
+
if (!values.unbound && !values.purge && positionals.length !== 1) {
|
|
306
534
|
throw new Error(USAGE);
|
|
307
535
|
}
|
|
308
536
|
const config = loadConfig();
|
|
@@ -311,7 +539,7 @@ export async function rm(args) {
|
|
|
311
539
|
await rmPurge(config, tools);
|
|
312
540
|
return;
|
|
313
541
|
}
|
|
314
|
-
if (values.
|
|
542
|
+
if (values.unbound) {
|
|
315
543
|
await rmUnbound(config, tools, {
|
|
316
544
|
dryRun: Boolean(values['dry-run']),
|
|
317
545
|
assumeYes: Boolean(values.yes)
|
|
@@ -321,4 +549,5 @@ export async function rm(args) {
|
|
|
321
549
|
const target = resolveSandboxTarget(positionals[0] ?? '', config.repoRoot);
|
|
322
550
|
await rmOne(config, tools, target.branch);
|
|
323
551
|
}
|
|
552
|
+
export { authorizeWorktrees, rmOne, rmPurge };
|
|
324
553
|
//# sourceMappingURL=rm.js.map
|