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