@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
|
@@ -16,18 +16,28 @@ import {
|
|
|
16
16
|
} from '../constants.ts';
|
|
17
17
|
import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.ts';
|
|
18
18
|
import { pruneSandboxDanglingImages } from '../image-prune.ts';
|
|
19
|
-
import { removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
|
|
20
|
-
import { runOk, runSafe, runSafeEngine } from '../shell.ts';
|
|
21
|
-
import { resolveSandboxTarget } from '../workspace-identity.ts';
|
|
19
|
+
import { assertManagedPath, removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
|
|
20
|
+
import { runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.ts';
|
|
21
|
+
import { resolveSandboxTarget, type SandboxWorkspaceIdentity } from '../workspace-identity.ts';
|
|
22
|
+
import { sandboxControlPaths, sandboxWorkspaceViewPaths } from '../workspace-view.ts';
|
|
23
|
+
import { quiesceSandboxControlRoot, readSandboxControlManifest } from '../control/lifecycle.ts';
|
|
22
24
|
import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.ts';
|
|
23
25
|
import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
|
|
24
26
|
import type { SandboxTool } from '../tools.ts';
|
|
25
27
|
import { fetchSandboxRows } from './list-running.ts';
|
|
26
28
|
import { lookupShortIdByBranch } from '../../task/short-id.ts';
|
|
29
|
+
import {
|
|
30
|
+
createCleanPermit,
|
|
31
|
+
createDiscardPermit,
|
|
32
|
+
formatWorktreeSnapshot,
|
|
33
|
+
inspectWorktrees,
|
|
34
|
+
verifyWorktreePermit
|
|
35
|
+
} from '../worktree-safety.ts';
|
|
36
|
+
import type { WorktreeInspection, WorktreeRemovalPermit } from '../worktree-safety.ts';
|
|
27
37
|
|
|
28
38
|
const USAGE = `Usage:
|
|
29
39
|
ai sandbox rm <branch> Remove one sandbox (branch | TASK-id | short id)
|
|
30
|
-
ai sandbox rm --
|
|
40
|
+
ai sandbox rm --unbound [--dry-run] [--yes] Remove every sandbox not bound to an active task
|
|
31
41
|
ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
|
|
32
42
|
export { assertManagedPath } from '../managed-fs.ts';
|
|
33
43
|
|
|
@@ -35,14 +45,33 @@ function projectToolDirs(config: SandboxConfig, tools: SandboxTool[]): string[]
|
|
|
35
45
|
return tools.flatMap((tool) => toolProjectDirCandidates(tool, config.project));
|
|
36
46
|
}
|
|
37
47
|
|
|
38
|
-
type
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
type RmTarget = {
|
|
49
|
+
branch: string;
|
|
50
|
+
effectiveBranch: string;
|
|
51
|
+
engine: string;
|
|
52
|
+
matchedContainers: string[];
|
|
53
|
+
existingWorktrees: string[];
|
|
54
|
+
toolCandidates: Array<{ tool: SandboxTool; candidates: string[] }>;
|
|
55
|
+
workspace: SandboxWorkspaceIdentity;
|
|
56
|
+
controlRoots: string[];
|
|
57
|
+
workspaceViewRoots: string[];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type RmOneOptions = {
|
|
61
|
+
assumeYes?: boolean;
|
|
62
|
+
quiet?: boolean;
|
|
63
|
+
target?: RmTarget;
|
|
64
|
+
permits?: ReadonlyMap<string, WorktreeRemovalPermit>;
|
|
65
|
+
allowDirtyDiscard?: boolean;
|
|
66
|
+
prompt?: PromptDependencies;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type PromptDependencies = {
|
|
70
|
+
confirm?: typeof p.confirm;
|
|
71
|
+
isCancel?: typeof p.isCancel;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function resolveRmTarget(config: SandboxConfig, tools: SandboxTool[], branch: string): RmTarget {
|
|
46
75
|
assertValidBranchName(branch);
|
|
47
76
|
const engine = detectEngine(config);
|
|
48
77
|
let effectiveBranch = branch;
|
|
@@ -51,17 +80,11 @@ async function rmOne(
|
|
|
51
80
|
tool,
|
|
52
81
|
candidates: toolConfigDirCandidates(tool, config.project, branch)
|
|
53
82
|
}));
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
p.intro(pc.cyan(`Removing sandbox for ${branch}`));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const existing = runSafeEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
60
|
-
const matchedContainers = containerNameCandidates(config, branch)
|
|
61
|
-
.filter((name) => existing.includes(name));
|
|
83
|
+
const existing = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
84
|
+
const matchedContainers = containerNameCandidates(config, branch).filter((name) => existing.includes(name));
|
|
62
85
|
|
|
63
86
|
if (matchedContainers.length > 0) {
|
|
64
|
-
const resolvedBranch =
|
|
87
|
+
const resolvedBranch = runEngine(engine, 'docker', [
|
|
65
88
|
'inspect',
|
|
66
89
|
'-f',
|
|
67
90
|
`{{ index .Config.Labels "${sandboxBranchLabel(config)}" }}`,
|
|
@@ -75,58 +98,246 @@ async function rmOne(
|
|
|
75
98
|
candidates: toolConfigDirCandidates(tool, config.project, effectiveBranch)
|
|
76
99
|
}));
|
|
77
100
|
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const workspace = resolveSandboxTarget(effectiveBranch, config.repoRoot).workspace;
|
|
104
|
+
const identities: SandboxWorkspaceIdentity[] = workspace.mode === 'task-bound'
|
|
105
|
+
? [workspace, { mode: 'branch-only' }]
|
|
106
|
+
: [workspace];
|
|
107
|
+
const containers = [...new Set([...containerNameCandidates(config, effectiveBranch), ...matchedContainers])];
|
|
108
|
+
const controlRoots = containers.flatMap((container) => identities.map((identity) => sandboxControlPaths({
|
|
109
|
+
base: config.controlBase, project: config.project, container, identity
|
|
110
|
+
}).root));
|
|
111
|
+
const workspaceViewRoots = containers.flatMap((container) => identities.map((identity) => sandboxWorkspaceViewPaths({
|
|
112
|
+
base: config.workspaceViewBase, project: config.project, container, identity
|
|
113
|
+
}).root));
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
branch,
|
|
117
|
+
effectiveBranch,
|
|
118
|
+
engine,
|
|
119
|
+
matchedContainers,
|
|
120
|
+
existingWorktrees: worktreeCandidates.filter((candidate) => fs.existsSync(candidate)),
|
|
121
|
+
toolCandidates,
|
|
122
|
+
workspace,
|
|
123
|
+
controlRoots: [...new Set(controlRoots)],
|
|
124
|
+
workspaceViewRoots: [...new Set(workspaceViewRoots)]
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function assertControlRootMatchesTarget(root: string, effectiveBranch: string, workspace: SandboxWorkspaceIdentity): void {
|
|
129
|
+
const manifestPath = path.join(root, 'manifest.json');
|
|
130
|
+
if (!fs.existsSync(manifestPath)) return;
|
|
131
|
+
const manifest = readSandboxControlManifest(manifestPath);
|
|
132
|
+
const identityMatches = workspace.mode === 'task-bound'
|
|
133
|
+
? manifest.mode === 'task-bound' && manifest.taskId === workspace.taskId
|
|
134
|
+
: manifest.mode === 'branch-only';
|
|
135
|
+
const branchOnlyFallback = workspace.mode === 'task-bound' && manifest.mode === 'branch-only';
|
|
136
|
+
if (manifest.branch !== effectiveBranch || (!identityMatches && !branchOnlyFallback)) {
|
|
137
|
+
throw new Error(`SANDBOX_CONTROL_TARGET_MISMATCH: ${root}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function assertLegacyCandidateEvidence(
|
|
142
|
+
candidate: string,
|
|
143
|
+
base: string,
|
|
144
|
+
config: SandboxConfig,
|
|
145
|
+
effectiveBranch: string,
|
|
146
|
+
matchedContainers: readonly string[]
|
|
147
|
+
): void {
|
|
148
|
+
const relative = path.relative(path.join(base, config.project), candidate);
|
|
149
|
+
const [container, identity] = relative.split(path.sep);
|
|
150
|
+
const [canonical, legacy] = containerNameCandidates(config, effectiveBranch);
|
|
151
|
+
if (!container || !identity || legacy === canonical || container !== legacy || matchedContainers.includes(container)) return;
|
|
152
|
+
const manifestPath = path.join(config.controlBase, config.project, container, identity, 'manifest.json');
|
|
153
|
+
if (!fs.existsSync(manifestPath)) {
|
|
154
|
+
throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: ${candidate}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function assertRemoved(target: string, label: string): void {
|
|
159
|
+
if (fs.existsSync(target)) throw new Error(`${label} still exists after removal: ${target}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function removeEmptyManagedParent(base: string, directory: string): void {
|
|
163
|
+
const parent = path.dirname(directory);
|
|
164
|
+
if (path.resolve(parent) === path.resolve(base)) return;
|
|
165
|
+
assertManagedPath(base, parent);
|
|
166
|
+
try {
|
|
167
|
+
fs.rmdirSync(parent);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes((error as NodeJS.ErrnoException).code ?? '')) return;
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
assertRemoved(parent, 'Empty sandbox container directory');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function quiesceProjectControlRoots(config: SandboxConfig): Promise<void> {
|
|
176
|
+
const projectRoot = path.join(config.controlBase, config.project);
|
|
177
|
+
if (!fs.existsSync(projectRoot)) return;
|
|
178
|
+
assertManagedPath(config.controlBase, projectRoot);
|
|
179
|
+
const projectStat = fs.lstatSync(projectRoot);
|
|
180
|
+
if (!projectStat.isDirectory() || projectStat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
181
|
+
for (const container of fs.readdirSync(projectRoot, { withFileTypes: true })) {
|
|
182
|
+
if (!container.isDirectory() || container.isSymbolicLink()) continue;
|
|
183
|
+
const containerRoot = path.join(projectRoot, container.name);
|
|
184
|
+
for (const identity of fs.readdirSync(containerRoot, { withFileTypes: true })) {
|
|
185
|
+
if (!identity.isDirectory() || identity.isSymbolicLink()) continue;
|
|
186
|
+
await quiesceSandboxControlRoot(path.join(containerRoot, identity.name));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function inspectionBlockers(inspections: readonly WorktreeInspection[]): WorktreeInspection[] {
|
|
192
|
+
return inspections.filter((inspection) => inspection.status !== 'clean');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function blockerMessage(blockers: readonly WorktreeInspection[]): string {
|
|
196
|
+
return blockers.map((blocker) => {
|
|
197
|
+
if (blocker.status === 'failed') return `${JSON.stringify(blocker.worktree)}: ${blocker.message}`;
|
|
198
|
+
return `${formatWorktreeSnapshot(blocker.snapshot)}\n Action: commit, stash, clean, or remove this sandbox interactively.`;
|
|
199
|
+
}).join('\n');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function cleanPermits(inspections: readonly WorktreeInspection[]): Map<string, WorktreeRemovalPermit> {
|
|
203
|
+
const permits = new Map<string, WorktreeRemovalPermit>();
|
|
204
|
+
for (const inspection of inspections) {
|
|
205
|
+
if (inspection.status === 'clean') permits.set(inspection.snapshot.worktree, createCleanPermit(inspection.snapshot));
|
|
206
|
+
}
|
|
207
|
+
return permits;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function authorizeWorktrees(
|
|
211
|
+
worktrees: readonly string[],
|
|
212
|
+
{ allowDirtyDiscard, assumeYes }: { allowDirtyDiscard: boolean; assumeYes: boolean },
|
|
213
|
+
{
|
|
214
|
+
interactive = Boolean(process.stdin.isTTY),
|
|
215
|
+
confirm = p.confirm,
|
|
216
|
+
isCancel = p.isCancel
|
|
217
|
+
}: PromptDependencies & { interactive?: boolean } = {}
|
|
218
|
+
): Promise<Map<string, WorktreeRemovalPermit>> {
|
|
219
|
+
const inspections = inspectWorktrees(worktrees);
|
|
220
|
+
const failures = inspections.filter((inspection) => inspection.status === 'failed');
|
|
221
|
+
if (failures.length > 0) throw new Error(`Unable to inspect worktree(s):\n${blockerMessage(failures)}`);
|
|
222
|
+
const permits = cleanPermits(inspections);
|
|
223
|
+
for (const inspection of inspections) {
|
|
224
|
+
if (inspection.status !== 'dirty') continue;
|
|
225
|
+
if (!allowDirtyDiscard || assumeYes || !interactive) {
|
|
226
|
+
throw new Error(`Refusing to remove dirty worktree(s):\n${blockerMessage([inspection])}`);
|
|
227
|
+
}
|
|
228
|
+
p.log.warn(formatWorktreeSnapshot(inspection.snapshot));
|
|
229
|
+
const confirmed = await confirm({
|
|
230
|
+
message: 'Discard these exact uncommitted changes?',
|
|
231
|
+
initialValue: false
|
|
232
|
+
});
|
|
233
|
+
if (isCancel(confirmed) || !confirmed) throw new Error('Dirty worktree removal cancelled; nothing was deleted');
|
|
234
|
+
permits.set(inspection.snapshot.worktree, createDiscardPermit(inspection.snapshot));
|
|
235
|
+
}
|
|
236
|
+
return permits;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function rmOne(
|
|
240
|
+
config: SandboxConfig,
|
|
241
|
+
tools: SandboxTool[],
|
|
242
|
+
branch: string,
|
|
243
|
+
options: RmOneOptions = {}
|
|
244
|
+
): Promise<void> {
|
|
245
|
+
const target = options.target ?? resolveRmTarget(config, tools, branch);
|
|
246
|
+
const { effectiveBranch, engine, matchedContainers, existingWorktrees, toolCandidates } = target;
|
|
247
|
+
const { workspace, controlRoots, workspaceViewRoots } = target;
|
|
248
|
+
const confirm = options.prompt?.confirm ?? p.confirm;
|
|
249
|
+
const isCancel = options.prompt?.isCancel ?? p.isCancel;
|
|
78
250
|
|
|
251
|
+
if (!options.quiet) {
|
|
252
|
+
p.intro(pc.cyan(`Removing sandbox for ${branch}`));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const permits = options.permits
|
|
256
|
+
? new Map(options.permits)
|
|
257
|
+
: await authorizeWorktrees(existingWorktrees, {
|
|
258
|
+
allowDirtyDiscard: options.allowDirtyDiscard ?? true,
|
|
259
|
+
assumeYes: Boolean(options.assumeYes)
|
|
260
|
+
}, options.prompt);
|
|
261
|
+
for (const worktree of existingWorktrees) {
|
|
262
|
+
const permit = permits.get(path.resolve(worktree));
|
|
263
|
+
if (!permit) throw new Error(`Missing worktree removal permit: ${worktree}`);
|
|
264
|
+
verifyWorktreePermit(permit);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const shouldRemoveWorktree = existingWorktrees.length === 0
|
|
268
|
+
? false
|
|
269
|
+
: options.assumeYes
|
|
270
|
+
? true
|
|
271
|
+
: await confirm({
|
|
272
|
+
message: `Remove worktree(s): ${existingWorktrees.join(', ')}?`,
|
|
273
|
+
initialValue: true
|
|
274
|
+
});
|
|
275
|
+
if (isCancel(shouldRemoveWorktree)) {
|
|
276
|
+
p.outro('Cancelled');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const root of controlRoots.filter((candidate) => fs.existsSync(candidate))) {
|
|
281
|
+
assertLegacyCandidateEvidence(root, config.controlBase, config, effectiveBranch, matchedContainers);
|
|
282
|
+
assertControlRootMatchesTarget(root, effectiveBranch, workspace);
|
|
283
|
+
await quiesceSandboxControlRoot(root);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (matchedContainers.length > 0) {
|
|
79
287
|
const spinner = p.spinner();
|
|
80
288
|
spinner.start(`Stopping container(s): ${matchedContainers.join(', ')}`);
|
|
81
289
|
for (const name of matchedContainers) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (const [base, label] of [
|
|
85
|
-
[path.join(config.workspaceViewBase, config.project), 'Workspace view'],
|
|
86
|
-
[path.join(config.controlBase, config.project), 'Control channel']
|
|
87
|
-
] as const) {
|
|
88
|
-
const directory = path.join(base, name);
|
|
89
|
-
if (fs.existsSync(directory)) {
|
|
90
|
-
removeManagedDir(base, directory);
|
|
91
|
-
if (!options.quiet) p.log.success(`${label} removed: ${directory}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
290
|
+
if (!runOkEngine(engine, 'docker', ['stop', name])) throw new Error(`Failed to stop sandbox container: ${name}`);
|
|
291
|
+
if (!runOkEngine(engine, 'docker', ['rm', name])) throw new Error(`Failed to remove sandbox container: ${name}`);
|
|
94
292
|
}
|
|
293
|
+
const remaining = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
294
|
+
const leftovers = matchedContainers.filter((name) => remaining.includes(name));
|
|
295
|
+
if (leftovers.length > 0) throw new Error(`Sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
|
|
95
296
|
spinner.stop(pc.green(`Removed container(s): ${matchedContainers.join(', ')}`));
|
|
96
297
|
} else {
|
|
97
298
|
p.log.warn(`No sandbox container found for '${branch}'`);
|
|
98
299
|
}
|
|
99
300
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
301
|
+
for (const [roots, base, label] of [
|
|
302
|
+
[workspaceViewRoots, path.join(config.workspaceViewBase, config.project), 'Workspace view'],
|
|
303
|
+
[controlRoots, path.join(config.controlBase, config.project), 'Control channel']
|
|
304
|
+
] as const) {
|
|
305
|
+
for (const directory of roots.filter((candidate) => fs.existsSync(candidate))) {
|
|
306
|
+
assertLegacyCandidateEvidence(directory, base === path.join(config.controlBase, config.project)
|
|
307
|
+
? config.controlBase : config.workspaceViewBase, config, effectiveBranch, matchedContainers);
|
|
308
|
+
removeManagedDir(base, directory);
|
|
309
|
+
assertRemoved(directory, label);
|
|
310
|
+
removeEmptyManagedParent(base, directory);
|
|
311
|
+
if (!options.quiet) p.log.success(`${label} removed: ${directory}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (shouldRemoveWorktree) {
|
|
316
|
+
for (const worktree of existingWorktrees) {
|
|
317
|
+
const permit = permits.get(path.resolve(worktree));
|
|
318
|
+
if (!permit) throw new Error(`Missing worktree removal permit: ${worktree}`);
|
|
319
|
+
removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree, permit, {
|
|
320
|
+
allowRegisteredPathFallback: engine === ENGINES.WSL2
|
|
321
|
+
});
|
|
322
|
+
assertRemoved(worktree, 'Worktree');
|
|
323
|
+
const registered = runSafe('git', ['-C', config.repoRoot, 'worktree', 'list', '--porcelain'])
|
|
324
|
+
.split('\n').filter((line) => line.startsWith('worktree ')).map((line) => path.resolve(line.slice(9)));
|
|
325
|
+
if (registered.includes(path.resolve(worktree))) throw new Error(`Worktree is still registered after removal: ${worktree}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const shouldDeleteBranch = options.assumeYes
|
|
103
329
|
? true
|
|
104
|
-
: await
|
|
105
|
-
message: `
|
|
330
|
+
: await confirm({
|
|
331
|
+
message: `Also delete local branch '${effectiveBranch}'?`,
|
|
106
332
|
initialValue: true
|
|
107
333
|
});
|
|
108
334
|
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (shouldRemoveWorktree) {
|
|
115
|
-
for (const worktree of existingWorktrees) {
|
|
116
|
-
removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree);
|
|
335
|
+
if (!isCancel(shouldDeleteBranch) && shouldDeleteBranch) {
|
|
336
|
+
if (!runOk('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch])) {
|
|
337
|
+
throw new Error(`Local branch '${effectiveBranch}' was not deleted`);
|
|
117
338
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
? true
|
|
121
|
-
: await p.confirm({
|
|
122
|
-
message: `Also delete local branch '${effectiveBranch}'?`,
|
|
123
|
-
initialValue: true
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
if (!p.isCancel(shouldDeleteBranch) && shouldDeleteBranch) {
|
|
127
|
-
if (!runOk('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch])) {
|
|
128
|
-
p.log.warn(`Local branch '${effectiveBranch}' was not deleted`);
|
|
129
|
-
}
|
|
339
|
+
if (runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', `refs/heads/${effectiveBranch}`])) {
|
|
340
|
+
throw new Error(`Local branch '${effectiveBranch}' still exists after removal`);
|
|
130
341
|
}
|
|
131
342
|
}
|
|
132
343
|
}
|
|
@@ -134,12 +345,14 @@ async function rmOne(
|
|
|
134
345
|
for (const { tool, candidates } of toolCandidates) {
|
|
135
346
|
for (const dir of candidates.filter((candidate) => fs.existsSync(candidate))) {
|
|
136
347
|
removeManagedDir(tool.sandboxBase, dir);
|
|
348
|
+
assertRemoved(dir, `${tool.name} state`);
|
|
137
349
|
p.log.success(`${tool.name} state removed: ${dir}`);
|
|
138
350
|
}
|
|
139
351
|
}
|
|
140
352
|
|
|
141
353
|
for (const dir of shellConfigDirCandidates(config, effectiveBranch).filter((candidate) => fs.existsSync(candidate))) {
|
|
142
354
|
removeManagedDir(config.shellConfigBase, dir);
|
|
355
|
+
assertRemoved(dir, 'Shell config');
|
|
143
356
|
p.log.success(`Shell config removed: ${dir}`);
|
|
144
357
|
}
|
|
145
358
|
|
|
@@ -147,12 +360,13 @@ async function rmOne(
|
|
|
147
360
|
if (fs.existsSync(shareBranch)) {
|
|
148
361
|
const shouldRemoveShare = options.assumeYes
|
|
149
362
|
? true
|
|
150
|
-
: await
|
|
363
|
+
: await confirm({
|
|
151
364
|
message: `Remove share dir for branch '${effectiveBranch}' (${shareBranch})?`,
|
|
152
365
|
initialValue: true
|
|
153
366
|
});
|
|
154
|
-
if (!
|
|
367
|
+
if (!isCancel(shouldRemoveShare) && shouldRemoveShare) {
|
|
155
368
|
removeManagedDir(config.shareBase, shareBranch);
|
|
369
|
+
assertRemoved(shareBranch, 'Share dir');
|
|
156
370
|
p.log.success(`Share dir removed: ${shareBranch}`);
|
|
157
371
|
}
|
|
158
372
|
}
|
|
@@ -162,11 +376,33 @@ async function rmOne(
|
|
|
162
376
|
}
|
|
163
377
|
}
|
|
164
378
|
|
|
165
|
-
async function rmPurge(
|
|
379
|
+
async function rmPurge(
|
|
380
|
+
config: SandboxConfig,
|
|
381
|
+
tools: SandboxTool[],
|
|
382
|
+
prompt: PromptDependencies = {}
|
|
383
|
+
): Promise<void> {
|
|
166
384
|
const engine = detectEngine(config);
|
|
385
|
+
const confirm = prompt.confirm ?? p.confirm;
|
|
386
|
+
const isCancel = prompt.isCancel ?? p.isCancel;
|
|
167
387
|
p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
|
|
168
388
|
|
|
169
|
-
const
|
|
389
|
+
const worktrees = fs.existsSync(config.worktreeBase)
|
|
390
|
+
? fs.readdirSync(config.worktreeBase)
|
|
391
|
+
.map((entry) => path.join(config.worktreeBase, entry))
|
|
392
|
+
.filter((entry) => {
|
|
393
|
+
try { return fs.statSync(entry).isDirectory(); } catch { return false; }
|
|
394
|
+
})
|
|
395
|
+
: [];
|
|
396
|
+
const inspections = inspectWorktrees(worktrees);
|
|
397
|
+
const blockers = inspectionBlockers(inspections);
|
|
398
|
+
if (blockers.length > 0) {
|
|
399
|
+
throw new Error(`Refusing to purge because worktree preflight found blocker(s):\n${blockerMessage(blockers)}`);
|
|
400
|
+
}
|
|
401
|
+
const permits = cleanPermits(inspections);
|
|
402
|
+
|
|
403
|
+
await quiesceProjectControlRoots(config);
|
|
404
|
+
|
|
405
|
+
const containers = runEngine(engine, 'docker', [
|
|
170
406
|
'ps',
|
|
171
407
|
'-a',
|
|
172
408
|
'--filter',
|
|
@@ -178,58 +414,73 @@ async function rmPurge(config: SandboxConfig, tools: SandboxTool[]): Promise<voi
|
|
|
178
414
|
const spinner = p.spinner();
|
|
179
415
|
spinner.start('Stopping project sandbox containers...');
|
|
180
416
|
for (const name of containers.split('\n').filter(Boolean)) {
|
|
181
|
-
|
|
182
|
-
|
|
417
|
+
if (!runOkEngine(engine, 'docker', ['stop', name])) throw new Error(`Failed to stop sandbox container: ${name}`);
|
|
418
|
+
if (!runOkEngine(engine, 'docker', ['rm', name])) throw new Error(`Failed to remove sandbox container: ${name}`);
|
|
183
419
|
}
|
|
420
|
+
const remaining = runEngine(engine, 'docker', [
|
|
421
|
+
'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
|
|
422
|
+
]);
|
|
423
|
+
if (remaining) throw new Error(`Project sandbox container(s) still exist after removal: ${remaining.replaceAll('\n', ', ')}`);
|
|
184
424
|
spinner.stop(pc.green('Project sandbox containers removed'));
|
|
185
425
|
} else {
|
|
186
426
|
p.log.warn('No project sandbox containers found');
|
|
187
427
|
}
|
|
188
428
|
|
|
189
|
-
if (
|
|
190
|
-
const shouldRemoveWorktrees = await
|
|
429
|
+
if (worktrees.length > 0) {
|
|
430
|
+
const shouldRemoveWorktrees = await confirm({
|
|
191
431
|
message: `Remove all worktrees in ${config.worktreeBase}?`,
|
|
192
432
|
initialValue: true
|
|
193
433
|
});
|
|
194
434
|
|
|
195
|
-
if (!
|
|
196
|
-
for (const
|
|
197
|
-
const
|
|
198
|
-
|
|
435
|
+
if (!isCancel(shouldRemoveWorktrees) && shouldRemoveWorktrees) {
|
|
436
|
+
for (const dir of worktrees) {
|
|
437
|
+
const permit = permits.get(path.resolve(dir));
|
|
438
|
+
if (!permit) throw new Error(`Missing worktree removal permit: ${dir}`);
|
|
439
|
+
removeWorktreeDir(config.repoRoot, config.worktreeBase, dir, permit, {
|
|
440
|
+
allowRegisteredPathFallback: engine === ENGINES.WSL2
|
|
441
|
+
});
|
|
442
|
+
assertRemoved(dir, 'Worktree');
|
|
199
443
|
}
|
|
200
444
|
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
445
|
+
const registered = new Set(runSafe('git', ['-C', config.repoRoot, 'worktree', 'list', '--porcelain'])
|
|
446
|
+
.split('\n').filter((line) => line.startsWith('worktree ')).map((line) => path.resolve(line.slice(9))));
|
|
447
|
+
const leftovers = worktrees.filter((dir) => registered.has(path.resolve(dir)));
|
|
448
|
+
if (leftovers.length > 0) throw new Error(`Worktree(s) still registered after removal: ${leftovers.join(', ')}`);
|
|
201
449
|
}
|
|
202
450
|
}
|
|
203
451
|
|
|
204
452
|
for (const dir of projectToolDirs(config, tools)) {
|
|
205
453
|
if (fs.existsSync(dir)) {
|
|
206
454
|
removeManagedDir(path.dirname(dir), dir);
|
|
455
|
+
assertRemoved(dir, 'Tool state');
|
|
207
456
|
p.log.success(`Removed tool state: ${dir}`);
|
|
208
457
|
}
|
|
209
458
|
}
|
|
210
459
|
|
|
211
460
|
if (fs.existsSync(config.shellConfigBase) && fs.readdirSync(config.shellConfigBase).length > 0) {
|
|
212
|
-
const shouldRemoveShellConfigs = await
|
|
461
|
+
const shouldRemoveShellConfigs = await confirm({
|
|
213
462
|
message: `Remove all shell config dirs in ${config.shellConfigBase}?`,
|
|
214
463
|
initialValue: true
|
|
215
464
|
});
|
|
216
465
|
|
|
217
|
-
if (!
|
|
466
|
+
if (!isCancel(shouldRemoveShellConfigs) && shouldRemoveShellConfigs) {
|
|
218
467
|
for (const entry of fs.readdirSync(config.shellConfigBase)) {
|
|
219
468
|
const dir = path.join(config.shellConfigBase, entry);
|
|
220
469
|
removeManagedDir(config.shellConfigBase, dir);
|
|
470
|
+
assertRemoved(dir, 'Shell config');
|
|
221
471
|
}
|
|
222
472
|
p.log.success(`Project shell config dirs removed: ${config.shellConfigBase}`);
|
|
223
473
|
}
|
|
224
474
|
}
|
|
225
475
|
|
|
226
476
|
if (fs.existsSync(config.shareBase) && fs.readdirSync(config.shareBase).length > 0) {
|
|
227
|
-
const shouldRemoveAllShares = await
|
|
477
|
+
const shouldRemoveAllShares = await confirm({
|
|
228
478
|
message: `Remove all share dirs for project (${config.shareBase})?`,
|
|
229
479
|
initialValue: true
|
|
230
480
|
});
|
|
231
|
-
if (!
|
|
481
|
+
if (!isCancel(shouldRemoveAllShares) && shouldRemoveAllShares) {
|
|
232
482
|
removeManagedDir(path.dirname(config.shareBase), config.shareBase);
|
|
483
|
+
assertRemoved(config.shareBase, 'Project share dir');
|
|
233
484
|
p.log.success(`Project share dirs removed: ${config.shareBase}`);
|
|
234
485
|
}
|
|
235
486
|
}
|
|
@@ -241,15 +492,16 @@ async function rmPurge(config: SandboxConfig, tools: SandboxTool[]): Promise<voi
|
|
|
241
492
|
const projectDir = path.join(base, config.project);
|
|
242
493
|
if (fs.existsSync(projectDir)) {
|
|
243
494
|
removeManagedDir(base, projectDir);
|
|
495
|
+
assertRemoved(projectDir, `Project ${label}`);
|
|
244
496
|
p.log.success(`Removed project ${label}: ${projectDir}`);
|
|
245
497
|
}
|
|
246
498
|
}
|
|
247
499
|
|
|
248
|
-
const shouldRemoveImage = await
|
|
500
|
+
const shouldRemoveImage = await confirm({
|
|
249
501
|
message: `Remove image ${config.imageName}?`,
|
|
250
502
|
initialValue: false
|
|
251
503
|
});
|
|
252
|
-
if (!
|
|
504
|
+
if (!isCancel(shouldRemoveImage) && shouldRemoveImage) {
|
|
253
505
|
runSafeEngine(engine, 'docker', ['rmi', config.imageName]);
|
|
254
506
|
}
|
|
255
507
|
|
|
@@ -263,11 +515,11 @@ async function rmPurge(config: SandboxConfig, tools: SandboxTool[]): Promise<voi
|
|
|
263
515
|
}
|
|
264
516
|
|
|
265
517
|
const name = engineDisplayName(engine);
|
|
266
|
-
const shouldStopVm = await
|
|
518
|
+
const shouldStopVm = await confirm({
|
|
267
519
|
message: `Stop ${name} VM?`,
|
|
268
520
|
initialValue: false
|
|
269
521
|
});
|
|
270
|
-
if (!
|
|
522
|
+
if (!isCancel(shouldStopVm) && shouldStopVm) {
|
|
271
523
|
stopManagedVm(config);
|
|
272
524
|
}
|
|
273
525
|
}
|
|
@@ -293,35 +545,41 @@ async function rmUnbound(
|
|
|
293
545
|
return;
|
|
294
546
|
}
|
|
295
547
|
|
|
548
|
+
const targets = removable.map((row) => resolveRmTarget(config, tools, row.branch));
|
|
549
|
+
const inspections = inspectWorktrees(targets.flatMap((target) => target.existingWorktrees));
|
|
550
|
+
const blockers = inspectionBlockers(inspections);
|
|
551
|
+
const permits = cleanPermits(inspections);
|
|
552
|
+
|
|
296
553
|
for (const row of removable) {
|
|
297
554
|
p.log.message(`${row.name} ${row.branch}`);
|
|
298
555
|
}
|
|
556
|
+
if (blockers.length > 0) p.log.error(blockerMessage(blockers));
|
|
299
557
|
|
|
300
558
|
if (options.dryRun) {
|
|
301
|
-
p.outro(`Dry run: ${removable.length} sandbox(es)
|
|
559
|
+
p.outro(`Dry run: ${removable.length} sandbox(es) inspected, nothing deleted`);
|
|
302
560
|
return;
|
|
303
561
|
}
|
|
304
562
|
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
initialValue: false
|
|
314
|
-
});
|
|
315
|
-
if (p.isCancel(confirmed) || !confirmed) {
|
|
316
|
-
p.outro('Cancelled');
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
563
|
+
if (blockers.length > 0) {
|
|
564
|
+
throw new Error(`Refusing batch removal because worktree preflight found blocker(s):\n${blockerMessage(blockers)}`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!options.assumeYes && !process.stdin.isTTY) {
|
|
568
|
+
throw new Error(
|
|
569
|
+
'Refusing to remove sandboxes without confirmation in a non-interactive shell; pass --yes to proceed.'
|
|
570
|
+
);
|
|
319
571
|
}
|
|
320
572
|
|
|
321
573
|
const failures: { branch: string; message: string }[] = [];
|
|
322
|
-
for (const row of removable) {
|
|
574
|
+
for (const [index, row] of removable.entries()) {
|
|
323
575
|
try {
|
|
324
|
-
await rmOne(config, tools, row.branch, {
|
|
576
|
+
await rmOne(config, tools, row.branch, {
|
|
577
|
+
assumeYes: options.assumeYes,
|
|
578
|
+
quiet: true,
|
|
579
|
+
target: targets[index],
|
|
580
|
+
permits,
|
|
581
|
+
allowDirtyDiscard: false
|
|
582
|
+
});
|
|
325
583
|
} catch (error) {
|
|
326
584
|
failures.push({ branch: row.branch, message: error instanceof Error ? error.message : String(error) });
|
|
327
585
|
}
|
|
@@ -346,6 +604,7 @@ export async function rm(args: string[]): Promise<void> {
|
|
|
346
604
|
strict: true,
|
|
347
605
|
options: {
|
|
348
606
|
all: { type: 'boolean' },
|
|
607
|
+
unbound: { type: 'boolean' },
|
|
349
608
|
purge: { type: 'boolean' },
|
|
350
609
|
'dry-run': { type: 'boolean' },
|
|
351
610
|
yes: { type: 'boolean', short: 'y' },
|
|
@@ -353,24 +612,28 @@ export async function rm(args: string[]): Promise<void> {
|
|
|
353
612
|
}
|
|
354
613
|
});
|
|
355
614
|
|
|
615
|
+
if (values.all) {
|
|
616
|
+
throw new Error('CLI_FLAG_REMOVED: --all was removed; use --unbound');
|
|
617
|
+
}
|
|
618
|
+
|
|
356
619
|
if (values.help) {
|
|
357
620
|
process.stdout.write(`${USAGE}\n`);
|
|
358
621
|
return;
|
|
359
622
|
}
|
|
360
623
|
|
|
361
|
-
if (values.
|
|
362
|
-
throw new Error('--
|
|
624
|
+
if (values.unbound && values.purge) {
|
|
625
|
+
throw new Error('--unbound and --purge are mutually exclusive');
|
|
363
626
|
}
|
|
364
627
|
|
|
365
|
-
if ((values['dry-run'] || values.yes) && !values.
|
|
366
|
-
throw new Error('--dry-run and --yes only apply to --
|
|
628
|
+
if ((values['dry-run'] || values.yes) && !values.unbound) {
|
|
629
|
+
throw new Error('--dry-run and --yes only apply to --unbound');
|
|
367
630
|
}
|
|
368
631
|
|
|
369
|
-
if ((values.
|
|
370
|
-
throw new Error(`${values.
|
|
632
|
+
if ((values.unbound || values.purge) && positionals.length > 0) {
|
|
633
|
+
throw new Error(`${values.unbound ? '--unbound' : '--purge'} does not take a branch argument`);
|
|
371
634
|
}
|
|
372
635
|
|
|
373
|
-
if (!values.
|
|
636
|
+
if (!values.unbound && !values.purge && positionals.length !== 1) {
|
|
374
637
|
throw new Error(USAGE);
|
|
375
638
|
}
|
|
376
639
|
|
|
@@ -382,7 +645,7 @@ export async function rm(args: string[]): Promise<void> {
|
|
|
382
645
|
return;
|
|
383
646
|
}
|
|
384
647
|
|
|
385
|
-
if (values.
|
|
648
|
+
if (values.unbound) {
|
|
386
649
|
await rmUnbound(config, tools, {
|
|
387
650
|
dryRun: Boolean(values['dry-run']),
|
|
388
651
|
assumeYes: Boolean(values.yes)
|
|
@@ -393,3 +656,5 @@ export async function rm(args: string[]): Promise<void> {
|
|
|
393
656
|
const target = resolveSandboxTarget(positionals[0] ?? '', config.repoRoot);
|
|
394
657
|
await rmOne(config, tools, target.branch);
|
|
395
658
|
}
|
|
659
|
+
|
|
660
|
+
export { authorizeWorktrees, rmOne, rmPurge };
|