@fitlab-ai/agent-infra 0.9.14 → 0.10.0
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 +10 -0
- package/README.zh-CN.md +10 -0
- package/bin/internal-cli.sh +45 -0
- package/dist/bin/internal-cli.js +93 -30
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +9 -4
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
- package/dist/lib/defaults.json +103 -103
- package/dist/lib/fs/durable-write.js +39 -0
- package/dist/lib/fs/file-lock.js +111 -0
- package/dist/lib/host-control/audit.js +19 -0
- package/dist/lib/host-control/client.js +142 -0
- package/dist/lib/host-control/command.js +49 -0
- package/dist/lib/host-control/path.js +160 -0
- package/dist/lib/host-control/secure-fs.js +140 -0
- package/dist/lib/host-control/server.js +159 -0
- package/dist/lib/host-control/service.js +112 -0
- package/dist/lib/init.js +11 -2
- package/dist/lib/internal/cli-route-inventory.js +4 -2
- package/dist/lib/internal/codex-lifecycle.js +6 -6
- package/dist/lib/internal/demo-promotion.js +322 -0
- package/dist/lib/internal/demo-transcript.js +454 -0
- package/dist/lib/internal/host-control.js +28 -0
- package/dist/lib/internal/manual-validation.js +331 -0
- package/dist/lib/internal/platform-pr.js +18 -4
- package/dist/lib/internal/release-workflow.js +63 -84
- package/dist/lib/internal/sandbox-control.js +149 -2
- package/dist/lib/internal/task-artifact.js +16 -263
- package/dist/lib/internal/task-create.js +6 -5
- package/dist/lib/internal/task-event.js +37 -37
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-operation-registry.js +101 -4
- package/dist/lib/internal/task-review.js +11 -56
- package/dist/lib/internal/task-validate.js +30 -10
- package/dist/lib/owned-path.js +33 -0
- package/dist/lib/platform/check-status.js +11 -0
- package/dist/lib/platform/comment-safety.js +4 -1
- package/dist/lib/platform/completion-backfill.js +1 -1
- package/dist/lib/platform/github-data.js +241 -0
- package/dist/lib/platform/github-provider.js +15 -20
- package/dist/lib/platform/issue-comments.js +18 -12
- package/dist/lib/platform/issues.js +2 -180
- package/dist/lib/platform/pr-change-report.js +5 -4
- package/dist/lib/platform/pr-checks.js +12 -65
- package/dist/lib/platform/pr-summary.js +117 -33
- package/dist/lib/platform/pull-requests.js +5 -108
- package/dist/lib/platform/snapshots.js +2 -0
- package/dist/lib/platform/task-identities.js +2 -9
- package/dist/lib/platform/verification-sync.js +132 -143
- package/dist/lib/pr-review/evidence-grading.js +4 -8
- package/dist/lib/release/workflow.js +1 -1
- package/dist/lib/sandbox/commands/create.js +7 -2
- package/dist/lib/sandbox/commands/rm.js +19 -1511
- package/dist/lib/sandbox/control/audit.js +203 -0
- package/dist/lib/sandbox/control/client.js +93 -54
- package/dist/lib/sandbox/control/executor.js +129 -65
- package/dist/lib/sandbox/control/finalization-response.js +27 -0
- package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
- package/dist/lib/sandbox/control/lifecycle.js +36 -2
- package/dist/lib/sandbox/control/native-file-lock.js +6 -98
- package/dist/lib/sandbox/control/protocol.js +55 -4
- package/dist/lib/sandbox/control/server.js +518 -127
- package/dist/lib/sandbox/control/state.js +64 -0
- package/dist/lib/sandbox/control/task-workflow.js +74 -0
- package/dist/lib/sandbox/control/workflow-executor.js +89 -0
- package/dist/lib/sandbox/index.js +1 -2
- package/dist/lib/sandbox/recovery.js +28 -4
- package/dist/lib/sandbox/removal.js +822 -0
- package/dist/lib/sandbox/task-cleanup.js +217 -0
- package/dist/lib/sandbox/workspace-identity.js +17 -1
- package/dist/lib/sandbox/workspace-view.js +13 -14
- package/dist/lib/task/activity-intent.js +2 -1
- package/dist/lib/task/activity-log.js +30 -24
- package/dist/lib/task/artifact-command.js +79 -0
- package/dist/lib/task/artifact-lifecycle.js +30 -28
- package/dist/lib/task/artifact-name.js +36 -0
- package/dist/lib/task/artifact-operations.js +29 -289
- package/dist/lib/task/artifact-receipts.js +3 -15
- package/dist/lib/task/artifact-recovery.js +430 -0
- package/dist/lib/task/artifact-repair-intent.js +134 -29
- package/dist/lib/task/artifact-schema.js +6 -14
- package/dist/lib/task/capabilities.js +12 -19
- package/dist/lib/task/codex-orchestration.js +71 -25
- package/dist/lib/task/commit-identity.js +28 -0
- package/dist/lib/task/commit-operation.js +33 -20
- package/dist/lib/task/control-authority.js +403 -6
- package/dist/lib/task/control-recovery.js +140 -0
- package/dist/lib/task/create-service.js +1 -8
- package/dist/lib/task/create.js +62 -9
- package/dist/lib/task/decision-details.js +2 -46
- package/dist/lib/task/delegation-receipts.js +64 -3
- package/dist/lib/task/events.js +275 -22
- package/dist/lib/task/finalization-state.js +9 -0
- package/dist/lib/task/human-override.js +8 -14
- package/dist/lib/task/implementation-inputs.js +3 -3
- package/dist/lib/task/intermediate-cleanup.js +441 -0
- package/dist/lib/task/invalidation.js +18 -65
- package/dist/lib/task/ledger-intents.js +7 -22
- package/dist/lib/task/ledger.js +1 -3
- package/dist/lib/task/lifecycle-recovery.js +549 -0
- package/dist/lib/task/lifecycle.js +31 -4
- package/dist/lib/task/local-artifact-finalization.js +197 -197
- package/dist/lib/task/manual-validation-completion.js +26 -0
- package/dist/lib/task/manual-validation-evidence.js +130 -0
- package/dist/lib/task/manual-validation-receipt.js +105 -0
- package/dist/lib/task/manual-validation-shared.js +40 -0
- package/dist/lib/task/manual-validation-transaction.js +243 -0
- package/dist/lib/task/markdown.js +47 -0
- package/dist/lib/task/orchestration.js +66 -19
- package/dist/lib/task/pr-delivery-fact.js +12 -56
- package/dist/lib/task/qualification-audit.js +20 -34
- package/dist/lib/task/recovery-warning.js +19 -0
- package/dist/lib/task/review-artifacts.js +2 -18
- package/dist/lib/task/review-command.js +26 -0
- package/dist/lib/task/review-finalization.js +175 -216
- package/dist/lib/task/review-fingerprint.js +2 -2
- package/dist/lib/task/sections.js +34 -56
- package/dist/lib/task/short-id.js +15 -7
- package/dist/lib/task/task-execution-lock.js +1 -1
- package/dist/lib/task/verification-engine.js +119 -337
- package/dist/lib/task/verification.js +1 -1
- package/dist/lib/task/workflow-command.js +16 -0
- package/dist/lib/task/workflow-warning-intents.js +0 -16
- package/dist/lib/task/workflow-warnings.js +28 -2
- package/dist/lib/update.js +2 -0
- package/dist/lib/version.js +1 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +10 -4
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
- package/lib/fs/durable-write.ts +30 -0
- package/lib/fs/file-lock.ts +113 -0
- package/lib/host-control/audit.ts +22 -0
- package/lib/host-control/client.ts +172 -0
- package/lib/host-control/command.ts +60 -0
- package/lib/host-control/path.ts +167 -0
- package/lib/host-control/secure-fs.ts +135 -0
- package/lib/host-control/server.ts +180 -0
- package/lib/host-control/service.ts +133 -0
- package/lib/init.ts +15 -2
- package/lib/internal/cli-route-inventory.ts +4 -2
- package/lib/internal/codex-lifecycle.ts +6 -6
- package/lib/internal/demo-promotion.ts +354 -0
- package/lib/internal/demo-transcript.ts +419 -0
- package/lib/internal/host-control.ts +28 -0
- package/lib/internal/manual-validation.ts +315 -0
- package/lib/internal/platform-pr.ts +15 -4
- package/lib/internal/release-workflow.ts +74 -82
- package/lib/internal/sandbox-control.ts +162 -1
- package/lib/internal/task-artifact.ts +14 -217
- package/lib/internal/task-create.ts +5 -5
- package/lib/internal/task-event.ts +20 -11
- package/lib/internal/task-lifecycle.ts +4 -3
- package/lib/internal/task-operation-registry.ts +111 -4
- package/lib/internal/task-review.ts +9 -54
- package/lib/internal/task-validate.ts +26 -10
- package/lib/owned-path.ts +26 -0
- package/lib/platform/check-status.ts +9 -0
- package/lib/platform/comment-safety.ts +5 -0
- package/lib/platform/completion-backfill.ts +1 -1
- package/lib/platform/github-data.ts +322 -0
- package/lib/platform/github-provider.ts +15 -20
- package/lib/platform/issue-comments.ts +21 -14
- package/lib/platform/issues.ts +5 -246
- package/lib/platform/pr-change-report.ts +7 -5
- package/lib/platform/pr-checks.ts +13 -77
- package/lib/platform/pr-summary.ts +123 -35
- package/lib/platform/pull-requests.ts +6 -173
- package/lib/platform/snapshots.ts +53 -0
- package/lib/platform/task-identities.ts +3 -14
- package/lib/platform/verification-sync.ts +150 -156
- package/lib/pr-review/evidence-grading.ts +4 -9
- package/lib/release/workflow.ts +3 -3
- package/lib/sandbox/commands/create.ts +7 -6
- package/lib/sandbox/commands/rm.ts +21 -1895
- package/lib/sandbox/control/audit.ts +253 -0
- package/lib/sandbox/control/client.ts +109 -66
- package/lib/sandbox/control/executor.ts +134 -74
- package/lib/sandbox/control/finalization-response.ts +29 -0
- package/lib/sandbox/control/identity-sentinel.ts +137 -0
- package/lib/sandbox/control/lifecycle.ts +45 -3
- package/lib/sandbox/control/native-file-lock.ts +6 -107
- package/lib/sandbox/control/protocol.ts +80 -6
- package/lib/sandbox/control/server.ts +567 -132
- package/lib/sandbox/control/state.ts +86 -0
- package/lib/sandbox/control/task-workflow.ts +95 -0
- package/lib/sandbox/control/workflow-executor.ts +92 -0
- package/lib/sandbox/index.ts +1 -2
- package/lib/sandbox/recovery.ts +28 -5
- package/lib/sandbox/removal.ts +1142 -0
- package/lib/sandbox/task-cleanup.ts +209 -0
- package/lib/sandbox/workspace-identity.ts +19 -3
- package/lib/sandbox/workspace-view.ts +17 -15
- package/lib/task/activity-intent.ts +2 -2
- package/lib/task/activity-log.ts +27 -19
- package/lib/task/artifact-command.ts +85 -0
- package/lib/task/artifact-lifecycle.ts +31 -26
- package/lib/task/artifact-name.ts +55 -0
- package/lib/task/artifact-operations.ts +34 -310
- package/lib/task/artifact-receipts.ts +3 -14
- package/lib/task/artifact-recovery.ts +519 -0
- package/lib/task/artifact-repair-intent.ts +162 -33
- package/lib/task/artifact-schema.ts +5 -34
- package/lib/task/capabilities.ts +13 -22
- package/lib/task/codex-orchestration.ts +85 -24
- package/lib/task/commit-identity.ts +31 -0
- package/lib/task/commit-operation.ts +37 -20
- package/lib/task/control-authority.ts +540 -10
- package/lib/task/control-recovery.ts +186 -0
- package/lib/task/create-service.ts +1 -10
- package/lib/task/create.ts +63 -8
- package/lib/task/decision-details.ts +3 -71
- package/lib/task/delegation-receipts.ts +73 -2
- package/lib/task/events.ts +309 -24
- package/lib/task/finalization-state.ts +11 -0
- package/lib/task/human-override.ts +8 -16
- package/lib/task/implementation-inputs.ts +3 -3
- package/lib/task/intermediate-cleanup.ts +493 -0
- package/lib/task/invalidation.ts +18 -62
- package/lib/task/ledger-intents.ts +7 -24
- package/lib/task/ledger.ts +1 -3
- package/lib/task/lifecycle-recovery.ts +691 -0
- package/lib/task/lifecycle.ts +30 -5
- package/lib/task/local-artifact-finalization.ts +244 -234
- package/lib/task/manual-validation-completion.ts +40 -0
- package/lib/task/manual-validation-evidence.ts +209 -0
- package/lib/task/manual-validation-receipt.ts +156 -0
- package/lib/task/manual-validation-shared.ts +50 -0
- package/lib/task/manual-validation-transaction.ts +304 -0
- package/lib/task/markdown.ts +70 -0
- package/lib/task/orchestration.ts +84 -19
- package/lib/task/pr-delivery-fact.ts +15 -62
- package/lib/task/qualification-audit.ts +22 -33
- package/lib/task/recovery-warning.ts +21 -0
- package/lib/task/review-artifacts.ts +1 -20
- package/lib/task/review-command.ts +24 -0
- package/lib/task/review-finalization.ts +183 -292
- package/lib/task/review-fingerprint.ts +2 -2
- package/lib/task/sections.ts +33 -54
- package/lib/task/short-id.ts +27 -5
- package/lib/task/task-execution-lock.ts +1 -1
- package/lib/task/verification-engine.ts +113 -383
- package/lib/task/verification-types.ts +15 -0
- package/lib/task/verification.ts +1 -1
- package/lib/task/workflow-command.ts +17 -0
- package/lib/task/workflow-warning-intents.ts +0 -13
- package/lib/task/workflow-warnings.ts +21 -2
- package/lib/update.ts +6 -0
- package/lib/version.ts +1 -15
- package/package.json +6 -6
- package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +4 -2
- package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
- package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
- package/templates/.agents/rules/testing-discipline.en.md +2 -0
- package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
- package/templates/.agents/scripts/package.json +3 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +33 -21
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +33 -21
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/config/verify.json +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-issue/config/verify.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/release/SKILL.en.md +7 -3
- package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
- package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +17 -6
- package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/import-issue.en.md +1 -1
- package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/restore-task.en.md +1 -1
- package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
- package/templates/.claude/commands/run-manual-validation.en.md +1 -1
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
- package/dist/lib/platform/adapters.js +0 -56
- package/lib/platform/adapters.ts +0 -197
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel, sandboxTaskIdLabel, sandboxWorkspaceModeLabel, shareBranchDir, shellConfigDirCandidates, worktreeDirCandidates } from './constants.js';
|
|
7
|
+
import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from './engine.js';
|
|
8
|
+
import { pruneSandboxDanglingImages } from './image-prune.js';
|
|
9
|
+
import { assertManagedPath, removeManagedDir, removeWorktreeDir } from './managed-fs.js';
|
|
10
|
+
import { run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from './shell.js';
|
|
11
|
+
import { parseSandboxWorkspaceIdentity, resolveSandboxCleanupTarget, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
|
|
12
|
+
import { sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
|
|
13
|
+
import { advanceSandboxRemovalJournalToPhase, advanceSandboxRemovalJournalPhase, claimSandboxRemovalJournal, clearSandboxRemovalJournalRecord, isDefaultSandboxRemovalJournal, listSandboxRemovalJournals, removeSandboxControlRoot, readSandboxControlManifest, sandboxRemovalPhaseIndex } from './control/lifecycle.js';
|
|
14
|
+
import { inspectSandboxControlContainer } from './control/container-identity.js';
|
|
15
|
+
import { commandForSandboxAuthority } from './engines/authority.js';
|
|
16
|
+
import { acquireSandboxResourceLock } from './control/native-file-lock.js';
|
|
17
|
+
import { toolConfigDirCandidates, toolProjectDirCandidates } from './tools.js';
|
|
18
|
+
import { getProcessStartTime } from '../server/process-state.js';
|
|
19
|
+
import { releaseStaleShortIdRegistry } from '../task/short-id.js';
|
|
20
|
+
import { fetchSandboxRows } from './commands/list-running.js';
|
|
21
|
+
import { formatIntermediateCleanupReport, cleanupIntermediateUnderRemovalCoordinator, protectIntermediateCleanupReport, mergeIntermediateCleanupReports, scanIntermediateCleanup } from '../task/intermediate-cleanup.js';
|
|
22
|
+
import { createSandboxControlBindingEvidence } from './task-cleanup.js';
|
|
23
|
+
import { createCleanPermit, createDiscardPermit, formatWorktreeSnapshot, inspectRecoveredWorktree, inspectWorktrees, verifyWorktreePermit } from './worktree-safety.js';
|
|
24
|
+
function projectToolDirs(config, tools) {
|
|
25
|
+
return tools.flatMap((tool) => toolProjectDirCandidates(tool, config.project));
|
|
26
|
+
}
|
|
27
|
+
function isMissingPathError(error) {
|
|
28
|
+
return Boolean(error && typeof error === 'object' && 'code' in error
|
|
29
|
+
&& error.code === 'ENOENT');
|
|
30
|
+
}
|
|
31
|
+
function canonicalPath(input) {
|
|
32
|
+
try {
|
|
33
|
+
return fs.realpathSync.native(input);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return path.resolve(input);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function isMissingTaskRecordError(error) {
|
|
40
|
+
return error instanceof Error && /^Task not found: TASK-\d{8}-\d{6}$/.test(error.message);
|
|
41
|
+
}
|
|
42
|
+
function digestCleanupValue(value) {
|
|
43
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
function removalTargetDigest(config, target) {
|
|
46
|
+
return digestCleanupValue({
|
|
47
|
+
project: config.project,
|
|
48
|
+
branch: target.effectiveBranch,
|
|
49
|
+
workspace: target.workspace,
|
|
50
|
+
controlRoots: target.controlRoots.map((candidate) => path.resolve(candidate)),
|
|
51
|
+
workspaceViewRoots: target.workspaceViewRoots.map((candidate) => path.resolve(candidate)),
|
|
52
|
+
managedPathCandidates: (target.managedPathCandidates ?? []).map((candidate) => path.resolve(candidate)).sort()
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function removalPermitDigest(permits) {
|
|
56
|
+
return digestCleanupValue([...permits.entries()]
|
|
57
|
+
.map(([worktree, permit]) => [path.resolve(worktree), permit.mode, permit.snapshot.identity])
|
|
58
|
+
.sort(([left], [right]) => String(left).localeCompare(String(right))));
|
|
59
|
+
}
|
|
60
|
+
function removalPermitCommits(permits) {
|
|
61
|
+
return [...permits.entries()]
|
|
62
|
+
.map(([worktree, permit]) => ({
|
|
63
|
+
path: path.resolve(worktree),
|
|
64
|
+
mode: permit.mode,
|
|
65
|
+
snapshot: permit.snapshot
|
|
66
|
+
}))
|
|
67
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
68
|
+
}
|
|
69
|
+
function removalTargetCommit(config, target, permits, removeWorktree, removeBranch, removeShare) {
|
|
70
|
+
return {
|
|
71
|
+
branch: target.effectiveBranch,
|
|
72
|
+
project: config.project,
|
|
73
|
+
controlRoot: path.resolve(target.controlRoots[0] ?? path.join(config.controlBase, config.project)),
|
|
74
|
+
targetDigest: removalTargetDigest(config, target),
|
|
75
|
+
permitDigest: removalPermitDigest(permits),
|
|
76
|
+
removeWorktree,
|
|
77
|
+
removeBranch,
|
|
78
|
+
removeShare,
|
|
79
|
+
worktreePaths: target.existingWorktrees.map((candidate) => path.resolve(candidate)).sort(),
|
|
80
|
+
workspaceViewPaths: target.workspaceViewRoots.map((candidate) => path.resolve(candidate)).sort(),
|
|
81
|
+
toolPaths: target.toolCandidates.flatMap(({ candidates }) => candidates.map((candidate) => path.resolve(candidate))).sort(),
|
|
82
|
+
shellPaths: shellConfigDirCandidates(config, target.effectiveBranch).map((candidate) => path.resolve(candidate)).sort(),
|
|
83
|
+
sharePath: path.resolve(shareBranchDir(config, target.effectiveBranch)),
|
|
84
|
+
permits: removalPermitCommits(permits)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function assertRemovalSelectionMatches(expected, actual) {
|
|
88
|
+
const normalize = (value) => ({
|
|
89
|
+
...value,
|
|
90
|
+
controlRoot: '',
|
|
91
|
+
permits: value.permits
|
|
92
|
+
});
|
|
93
|
+
if (JSON.stringify(normalize(expected)) !== JSON.stringify(normalize(actual))) {
|
|
94
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function advanceRemovalJournals(project, target, targetDigest, phase, resourceLocks) {
|
|
98
|
+
return listSandboxRemovalJournals({ branch: target.effectiveBranch, project, targetDigest }).map((journal) => {
|
|
99
|
+
if (sandboxRemovalPhaseIndex(journal.phase) >= sandboxRemovalPhaseIndex(phase))
|
|
100
|
+
return journal;
|
|
101
|
+
const lock = resourceLocks.get(journal.target.controlRoot);
|
|
102
|
+
if (!lock)
|
|
103
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_LOCK_MISMATCH');
|
|
104
|
+
return advanceSandboxRemovalJournalToPhase(journal, phase, lock);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function prepareRemovalAction(project, target, targetDigest, startPhase, completedPhase, resourceLocks) {
|
|
108
|
+
const journals = listSandboxRemovalJournals({
|
|
109
|
+
branch: target.effectiveBranch,
|
|
110
|
+
project,
|
|
111
|
+
targetDigest
|
|
112
|
+
});
|
|
113
|
+
const startIndex = sandboxRemovalPhaseIndex(startPhase);
|
|
114
|
+
const completedIndex = sandboxRemovalPhaseIndex(completedPhase);
|
|
115
|
+
const shouldRun = journals.length === 0
|
|
116
|
+
|| journals.some((journal) => sandboxRemovalPhaseIndex(journal.phase) < completedIndex);
|
|
117
|
+
const recovering = journals.some((journal) => {
|
|
118
|
+
const index = sandboxRemovalPhaseIndex(journal.phase);
|
|
119
|
+
return index >= startIndex && index < completedIndex;
|
|
120
|
+
});
|
|
121
|
+
advanceRemovalJournals(project, target, targetDigest, startPhase, resourceLocks);
|
|
122
|
+
return { run: shouldRun, recovering };
|
|
123
|
+
}
|
|
124
|
+
function assertRemovalPathsAbsent(paths) {
|
|
125
|
+
const remaining = paths.filter((candidate) => fs.existsSync(candidate));
|
|
126
|
+
if (remaining.length > 0) {
|
|
127
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${remaining[0]}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function assertBranchRemovalIdentity(config, branch, permits) {
|
|
131
|
+
if (!runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', `refs/heads/${branch}`]))
|
|
132
|
+
return;
|
|
133
|
+
const expectedHeads = new Set([...permits.values()]
|
|
134
|
+
.filter((permit) => permit.snapshot.branch === branch)
|
|
135
|
+
.map((permit) => permit.snapshot.head));
|
|
136
|
+
if (expectedHeads.size === 0) {
|
|
137
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
138
|
+
}
|
|
139
|
+
const actualHead = runSafe('git', ['-C', config.repoRoot, 'rev-parse', `refs/heads/${branch}`]);
|
|
140
|
+
if (!expectedHeads.has(actualHead)) {
|
|
141
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function removalTombstonePath(targetDigest, kind, source) {
|
|
145
|
+
const sourceDigest = createHash('sha256')
|
|
146
|
+
.update(`${kind}\0${path.resolve(source)}`)
|
|
147
|
+
.digest('hex')
|
|
148
|
+
.slice(0, 24);
|
|
149
|
+
return path.join(path.dirname(path.resolve(source)), `.agent-infra-removal-${targetDigest.slice(0, 16)}-${kind}-${sourceDigest}`);
|
|
150
|
+
}
|
|
151
|
+
const REMOVAL_TOMBSTONE_MARKER = '.agent-infra-removal-ownership.json';
|
|
152
|
+
const REMOVAL_TOMBSTONE_PAYLOAD = 'payload';
|
|
153
|
+
function removalSourceClaimPath(source, ownership) {
|
|
154
|
+
const claimDigest = createHash('sha256')
|
|
155
|
+
.update(JSON.stringify(ownership))
|
|
156
|
+
.digest('hex')
|
|
157
|
+
.slice(0, 24);
|
|
158
|
+
return path.join(source, `.agent-infra-removal-source-${claimDigest}.json`);
|
|
159
|
+
}
|
|
160
|
+
function removalSourceIdentity(source) {
|
|
161
|
+
const stat = fs.lstatSync(source, { bigint: true });
|
|
162
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev === 0n || stat.ino === 0n) {
|
|
163
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
164
|
+
}
|
|
165
|
+
return { dev: String(stat.dev), ino: String(stat.ino) };
|
|
166
|
+
}
|
|
167
|
+
function sameRemovalSourceIdentity(left, right) {
|
|
168
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
169
|
+
}
|
|
170
|
+
function removalSourceClaimMatches(source, ownership, identity) {
|
|
171
|
+
try {
|
|
172
|
+
const claimPath = removalSourceClaimPath(source, ownership);
|
|
173
|
+
const claimStat = fs.lstatSync(claimPath);
|
|
174
|
+
if (!claimStat.isFile() || claimStat.isSymbolicLink())
|
|
175
|
+
return false;
|
|
176
|
+
const record = JSON.parse(fs.readFileSync(claimPath, 'utf8'));
|
|
177
|
+
return record.version === ownership.version
|
|
178
|
+
&& record.targetDigest === ownership.targetDigest
|
|
179
|
+
&& record.permitDigest === ownership.permitDigest
|
|
180
|
+
&& record.kind === ownership.kind
|
|
181
|
+
&& typeof record.source === 'string'
|
|
182
|
+
&& path.resolve(record.source) === path.resolve(ownership.source)
|
|
183
|
+
&& record.sourceIdentity?.dev === identity.dev
|
|
184
|
+
&& record.sourceIdentity?.ino === identity.ino;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function isOwnedRemovalSourceClaim(source, ownership) {
|
|
191
|
+
try {
|
|
192
|
+
const claimPath = removalSourceClaimPath(source, ownership);
|
|
193
|
+
const claimStat = fs.lstatSync(claimPath);
|
|
194
|
+
if (!claimStat.isFile() || claimStat.isSymbolicLink())
|
|
195
|
+
return false;
|
|
196
|
+
const record = JSON.parse(fs.readFileSync(claimPath, 'utf8'));
|
|
197
|
+
return record.version === ownership.version
|
|
198
|
+
&& record.targetDigest === ownership.targetDigest
|
|
199
|
+
&& record.permitDigest === ownership.permitDigest
|
|
200
|
+
&& record.kind === ownership.kind
|
|
201
|
+
&& typeof record.source === 'string'
|
|
202
|
+
&& path.resolve(record.source) === path.resolve(ownership.source);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function removeOwnedSourceClaim(source, ownership) {
|
|
209
|
+
try {
|
|
210
|
+
const claimPath = removalSourceClaimPath(source, ownership);
|
|
211
|
+
if (isOwnedRemovalSourceClaim(source, ownership))
|
|
212
|
+
fs.unlinkSync(claimPath);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Preserve the source when the claim cannot be revalidated.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function readRemovalTombstoneRecord(root, tombstone) {
|
|
219
|
+
assertManagedPath(root, tombstone);
|
|
220
|
+
try {
|
|
221
|
+
const tombstoneStat = fs.lstatSync(tombstone);
|
|
222
|
+
const markerStat = fs.lstatSync(removalTombstoneMarker(tombstone));
|
|
223
|
+
if (!tombstoneStat.isDirectory() || tombstoneStat.isSymbolicLink()
|
|
224
|
+
|| !markerStat.isFile() || markerStat.isSymbolicLink())
|
|
225
|
+
return null;
|
|
226
|
+
const record = JSON.parse(fs.readFileSync(removalTombstoneMarker(tombstone), 'utf8'));
|
|
227
|
+
if (record.version !== 1
|
|
228
|
+
|| typeof record.targetDigest !== 'string'
|
|
229
|
+
|| typeof record.permitDigest !== 'string'
|
|
230
|
+
|| typeof record.kind !== 'string'
|
|
231
|
+
|| typeof record.source !== 'string'
|
|
232
|
+
|| record.sourceIdentity?.dev === undefined
|
|
233
|
+
|| record.sourceIdentity.ino === undefined)
|
|
234
|
+
return null;
|
|
235
|
+
return record;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function removalTombstonePayload(tombstone) {
|
|
242
|
+
return path.join(tombstone, REMOVAL_TOMBSTONE_PAYLOAD);
|
|
243
|
+
}
|
|
244
|
+
function removalTombstoneMarker(tombstone) {
|
|
245
|
+
return path.join(tombstone, REMOVAL_TOMBSTONE_MARKER);
|
|
246
|
+
}
|
|
247
|
+
function isOwnedRemovalTombstone(root, tombstone, ownership) {
|
|
248
|
+
const record = readRemovalTombstoneRecord(root, tombstone);
|
|
249
|
+
return record !== null
|
|
250
|
+
&& record.version === 1
|
|
251
|
+
&& record.targetDigest === ownership.targetDigest
|
|
252
|
+
&& record.permitDigest === ownership.permitDigest
|
|
253
|
+
&& record.kind === ownership.kind
|
|
254
|
+
&& typeof record.source === 'string'
|
|
255
|
+
&& path.resolve(record.source) === path.resolve(ownership.source);
|
|
256
|
+
}
|
|
257
|
+
function isOwnedRemovalPayload(root, tombstone, ownership) {
|
|
258
|
+
const record = readRemovalTombstoneRecord(root, tombstone);
|
|
259
|
+
if (!record || record.targetDigest !== ownership.targetDigest
|
|
260
|
+
|| record.permitDigest !== ownership.permitDigest
|
|
261
|
+
|| record.kind !== ownership.kind
|
|
262
|
+
|| path.resolve(record.source) !== path.resolve(ownership.source))
|
|
263
|
+
return false;
|
|
264
|
+
try {
|
|
265
|
+
const payload = removalTombstonePayload(tombstone);
|
|
266
|
+
const payloadIdentity = removalSourceIdentity(payload);
|
|
267
|
+
return sameRemovalSourceIdentity(payloadIdentity, record.sourceIdentity)
|
|
268
|
+
&& removalSourceClaimMatches(payload, ownership, record.sourceIdentity);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function assertOwnedRemovalTombstone(root, tombstone, ownership) {
|
|
275
|
+
if (!isOwnedRemovalTombstone(root, tombstone, ownership)) {
|
|
276
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${ownership.source}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function shouldStageManagedRemoval(source, tombstone, recovering) {
|
|
280
|
+
return fs.existsSync(source) || (recovering && fs.existsSync(tombstone));
|
|
281
|
+
}
|
|
282
|
+
function stageManagedRemoval(root, source, tombstone, recovering, ownership) {
|
|
283
|
+
assertManagedPath(root, source);
|
|
284
|
+
assertManagedPath(root, tombstone);
|
|
285
|
+
const sourceExists = fs.existsSync(source);
|
|
286
|
+
const tombstoneExists = fs.existsSync(tombstone);
|
|
287
|
+
if (tombstoneExists) {
|
|
288
|
+
assertOwnedRemovalTombstone(root, tombstone, ownership);
|
|
289
|
+
if (sourceExists) {
|
|
290
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
291
|
+
}
|
|
292
|
+
if (recovering) {
|
|
293
|
+
if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
|
|
294
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
299
|
+
}
|
|
300
|
+
if (recovering) {
|
|
301
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${source}`);
|
|
302
|
+
}
|
|
303
|
+
if (!sourceExists)
|
|
304
|
+
return;
|
|
305
|
+
// The operator authorizes cleanup of this managed target after execution stops.
|
|
306
|
+
// Path checks detect conflicts; they do not provide an atomic source-instance move.
|
|
307
|
+
const expectedIdentity = removalSourceIdentity(source);
|
|
308
|
+
const claimPath = removalSourceClaimPath(source, ownership);
|
|
309
|
+
try {
|
|
310
|
+
fs.writeFileSync(claimPath, `${JSON.stringify({ ...ownership, sourceIdentity: expectedIdentity })}\n`, {
|
|
311
|
+
encoding: 'utf8',
|
|
312
|
+
mode: 0o600,
|
|
313
|
+
flag: 'wx'
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
318
|
+
}
|
|
319
|
+
if (!sameRemovalSourceIdentity(expectedIdentity, removalSourceIdentity(source))) {
|
|
320
|
+
removeOwnedSourceClaim(source, ownership);
|
|
321
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
322
|
+
}
|
|
323
|
+
let moved = false;
|
|
324
|
+
try {
|
|
325
|
+
fs.mkdirSync(tombstone, { mode: 0o700 });
|
|
326
|
+
fs.writeFileSync(removalTombstoneMarker(tombstone), `${JSON.stringify({ ...ownership, sourceIdentity: expectedIdentity })}\n`, {
|
|
327
|
+
encoding: 'utf8',
|
|
328
|
+
mode: 0o600,
|
|
329
|
+
flag: 'wx'
|
|
330
|
+
});
|
|
331
|
+
fs.renameSync(source, removalTombstonePayload(tombstone));
|
|
332
|
+
moved = true;
|
|
333
|
+
if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
|
|
334
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
if (!moved)
|
|
339
|
+
removeOwnedSourceClaim(source, ownership);
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function cleanupManagedTombstones(root, targetDigest, permitDigest, kind, sources) {
|
|
344
|
+
for (const source of sources) {
|
|
345
|
+
const tombstone = removalTombstonePath(targetDigest, kind, source);
|
|
346
|
+
if (!fs.existsSync(tombstone))
|
|
347
|
+
continue;
|
|
348
|
+
const ownership = { version: 1, targetDigest, permitDigest, kind, source };
|
|
349
|
+
if (!isOwnedRemovalTombstone(root, tombstone, ownership))
|
|
350
|
+
continue;
|
|
351
|
+
if (!isOwnedRemovalPayload(root, tombstone, ownership)) {
|
|
352
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${source}`);
|
|
353
|
+
}
|
|
354
|
+
removeManagedDir(root, tombstone);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function branchRemovalTombstoneRef(targetDigest) {
|
|
358
|
+
return `refs/agent-infra/sandbox-removal/${targetDigest}/branch`;
|
|
359
|
+
}
|
|
360
|
+
function branchPermitHead(branch, permits) {
|
|
361
|
+
const heads = new Set([...permits.values()]
|
|
362
|
+
.filter((permit) => permit.snapshot.branch === branch)
|
|
363
|
+
.map((permit) => permit.snapshot.head));
|
|
364
|
+
return heads.size === 1 ? [...heads][0] : null;
|
|
365
|
+
}
|
|
366
|
+
function stageBranchRemoval(config, branch, targetDigest, permits, recovering) {
|
|
367
|
+
const expectedHead = branchPermitHead(branch, permits);
|
|
368
|
+
if (!expectedHead)
|
|
369
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
370
|
+
const branchName = `refs/heads/${branch}`;
|
|
371
|
+
const tombstone = branchRemovalTombstoneRef(targetDigest);
|
|
372
|
+
const branchExists = runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', branchName]);
|
|
373
|
+
const tombstoneExists = runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', tombstone]);
|
|
374
|
+
if (branchExists && tombstoneExists) {
|
|
375
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
376
|
+
}
|
|
377
|
+
if (recovering) {
|
|
378
|
+
if (branchExists && !tombstoneExists) {
|
|
379
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${branch}`);
|
|
380
|
+
}
|
|
381
|
+
if (tombstoneExists && runSafe('git', ['-C', config.repoRoot, 'rev-parse', tombstone]) !== expectedHead) {
|
|
382
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (!branchExists)
|
|
387
|
+
return;
|
|
388
|
+
assertBranchRemovalIdentity(config, branch, permits);
|
|
389
|
+
const actualHead = runSafe('git', ['-C', config.repoRoot, 'rev-parse', branchName]);
|
|
390
|
+
if (!runOk('git', ['-C', config.repoRoot, 'update-ref', tombstone, actualHead, '0'.repeat(40)])) {
|
|
391
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
392
|
+
}
|
|
393
|
+
if (!runOk('git', ['-C', config.repoRoot, 'update-ref', '-d', branchName, actualHead])) {
|
|
394
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function cleanupBranchTombstone(config, branch, targetDigest, permits) {
|
|
398
|
+
const tombstone = branchRemovalTombstoneRef(targetDigest);
|
|
399
|
+
if (!runOk('git', ['-C', config.repoRoot, 'show-ref', '--verify', tombstone]))
|
|
400
|
+
return;
|
|
401
|
+
const expectedHead = branchPermitHead(branch, permits);
|
|
402
|
+
if (!expectedHead || runSafe('git', ['-C', config.repoRoot, 'rev-parse', tombstone]) !== expectedHead) {
|
|
403
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
404
|
+
}
|
|
405
|
+
if (!runOk('git', ['-C', config.repoRoot, 'update-ref', '-d', tombstone, expectedHead])) {
|
|
406
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${branch}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function worktreeRegistration(config, worktree) {
|
|
410
|
+
const lines = runSafe('git', ['-C', config.repoRoot, 'worktree', 'list', '--porcelain']).split('\n');
|
|
411
|
+
let current = null;
|
|
412
|
+
const flush = () => {
|
|
413
|
+
if (!current || path.resolve(current.path) !== path.resolve(worktree))
|
|
414
|
+
return null;
|
|
415
|
+
return { head: current.head, branch: current.branch };
|
|
416
|
+
};
|
|
417
|
+
for (const line of lines) {
|
|
418
|
+
if (line.startsWith('worktree ')) {
|
|
419
|
+
const previous = flush();
|
|
420
|
+
if (previous)
|
|
421
|
+
return previous;
|
|
422
|
+
current = { path: line.slice('worktree '.length), head: '', branch: null };
|
|
423
|
+
}
|
|
424
|
+
else if (current && line.startsWith('HEAD ')) {
|
|
425
|
+
current.head = line.slice('HEAD '.length);
|
|
426
|
+
}
|
|
427
|
+
else if (current && line.startsWith('branch ')) {
|
|
428
|
+
current.branch = line.slice('branch '.length);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return flush();
|
|
432
|
+
}
|
|
433
|
+
function assertWorktreeRegistrationIdentity(config, worktree, permit) {
|
|
434
|
+
const registration = worktreeRegistration(config, worktree);
|
|
435
|
+
if (!registration)
|
|
436
|
+
return;
|
|
437
|
+
if (registration.head !== permit.snapshot.head
|
|
438
|
+
|| registration.branch !== `refs/heads/${permit.snapshot.branch}`) {
|
|
439
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${worktree}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function pruneWorktreeRegistration(config, worktree) {
|
|
443
|
+
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
444
|
+
if (worktreeRegistration(config, worktree)) {
|
|
445
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${worktree}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function stageWorktreeRemoval(config, worktree, permit, tombstone, recovering, ownership) {
|
|
449
|
+
assertManagedPath(config.worktreeBase, worktree);
|
|
450
|
+
assertManagedPath(config.worktreeBase, tombstone);
|
|
451
|
+
const worktreeExists = fs.existsSync(worktree);
|
|
452
|
+
const tombstoneExists = fs.existsSync(tombstone);
|
|
453
|
+
if (worktreeExists || !recovering)
|
|
454
|
+
verifyWorktreePermit(permit);
|
|
455
|
+
if (recovering) {
|
|
456
|
+
if (worktreeExists || !tombstoneExists) {
|
|
457
|
+
if (worktreeExists && tombstoneExists) {
|
|
458
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH: ${worktree}`);
|
|
459
|
+
}
|
|
460
|
+
throw new Error(`SANDBOX_CONTROL_REMOVAL_ACTION_OUTCOME_UNKNOWN: ${worktree}`);
|
|
461
|
+
}
|
|
462
|
+
assertOwnedRemovalTombstone(config.worktreeBase, tombstone, ownership);
|
|
463
|
+
assertWorktreeRegistrationIdentity(config, worktree, permit);
|
|
464
|
+
pruneWorktreeRegistration(config, worktree);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!worktreeExists)
|
|
468
|
+
return;
|
|
469
|
+
stageManagedRemoval(config.worktreeBase, worktree, tombstone, false, ownership);
|
|
470
|
+
pruneWorktreeRegistration(config, worktree);
|
|
471
|
+
}
|
|
472
|
+
function cleanupCompletedRemovalArtifacts(config, target, committedTarget, targetDigest) {
|
|
473
|
+
const journals = listSandboxRemovalJournals({
|
|
474
|
+
branch: target.effectiveBranch,
|
|
475
|
+
project: config.project,
|
|
476
|
+
targetDigest
|
|
477
|
+
});
|
|
478
|
+
if (journals.length === 0)
|
|
479
|
+
return;
|
|
480
|
+
const completed = (phase) => journals.every((journal) => (sandboxRemovalPhaseIndex(journal.phase) >= sandboxRemovalPhaseIndex(phase)));
|
|
481
|
+
if (completed('workspace-removed')) {
|
|
482
|
+
cleanupManagedTombstones(config.workspaceViewBase, targetDigest, committedTarget.permitDigest, 'workspace', target.workspaceViewRoots);
|
|
483
|
+
cleanupManagedTombstones(path.join(config.controlBase, config.project), targetDigest, committedTarget.permitDigest, 'workspace', target.controlRoots);
|
|
484
|
+
cleanupManagedTombstones(config.worktreeBase, targetDigest, committedTarget.permitDigest, 'worktree', committedTarget.worktreePaths);
|
|
485
|
+
}
|
|
486
|
+
if (completed('branch-removed')) {
|
|
487
|
+
cleanupBranchTombstone(config, target.effectiveBranch, targetDigest, new Map(committedTarget.permits.map((permit) => [path.resolve(permit.path), {
|
|
488
|
+
mode: permit.mode,
|
|
489
|
+
snapshot: permit.snapshot
|
|
490
|
+
}])));
|
|
491
|
+
}
|
|
492
|
+
if (completed('tool-removed')) {
|
|
493
|
+
cleanupManagedTombstones(config.home, targetDigest, committedTarget.permitDigest, 'tool', committedTarget.toolPaths);
|
|
494
|
+
}
|
|
495
|
+
if (completed('shell-removed')) {
|
|
496
|
+
cleanupManagedTombstones(config.shellConfigBase, targetDigest, committedTarget.permitDigest, 'shell', committedTarget.shellPaths);
|
|
497
|
+
}
|
|
498
|
+
if (completed('share-removed')) {
|
|
499
|
+
cleanupManagedTombstones(config.shareBase, targetDigest, committedTarget.permitDigest, 'share', [committedTarget.sharePath]);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function removalActionPaths(target, config, phase) {
|
|
503
|
+
if (phase === 'workspace-removed') {
|
|
504
|
+
return [...target.workspaceViewRoots, ...target.controlRoots];
|
|
505
|
+
}
|
|
506
|
+
if (phase === 'tool-removed') {
|
|
507
|
+
return target.toolCandidates.flatMap(({ candidates }) => candidates);
|
|
508
|
+
}
|
|
509
|
+
if (phase === 'shell-removed') {
|
|
510
|
+
return shellConfigDirCandidates(config, target.effectiveBranch);
|
|
511
|
+
}
|
|
512
|
+
if (phase === 'share-removed') {
|
|
513
|
+
return [shareBranchDir(config, target.effectiveBranch)];
|
|
514
|
+
}
|
|
515
|
+
return [];
|
|
516
|
+
}
|
|
517
|
+
function permitsFromJournal(journals, target, expectedTargetDigest) {
|
|
518
|
+
const permits = new Map();
|
|
519
|
+
for (const journal of journals) {
|
|
520
|
+
if (journal.target.targetDigest !== expectedTargetDigest)
|
|
521
|
+
continue;
|
|
522
|
+
if (journal.target.branch !== target.effectiveBranch) {
|
|
523
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
|
|
524
|
+
}
|
|
525
|
+
for (const entry of journal.target.permits) {
|
|
526
|
+
if (path.resolve(entry.path) !== path.resolve(entry.snapshot.worktree)) {
|
|
527
|
+
throw new Error('SANDBOX_CONTROL_REMOVAL_PERMIT_MISMATCH');
|
|
528
|
+
}
|
|
529
|
+
permits.set(path.resolve(entry.path), {
|
|
530
|
+
mode: entry.mode,
|
|
531
|
+
snapshot: entry.snapshot
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return permits;
|
|
536
|
+
}
|
|
537
|
+
export function sandboxManagedPathKey(candidate, platform = process.platform, resolveExistingPath = (resolved) => fs.realpathSync.native(resolved)) {
|
|
538
|
+
const resolved = path.resolve(candidate);
|
|
539
|
+
let identity = resolved;
|
|
540
|
+
try {
|
|
541
|
+
identity = resolveExistingPath(resolved);
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
if (!isMissingPathError(error)) {
|
|
545
|
+
throw new Error(`SANDBOX_CLEANUP_BATCH_PREFLIGHT_FAILED: unable to determine managed path identity for '${resolved}'`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return platform === 'win32' ? identity.toLowerCase() : identity;
|
|
549
|
+
}
|
|
550
|
+
function resolveRmTarget(config, tools, cleanupTarget, options = {}) {
|
|
551
|
+
assertValidBranchName(cleanupTarget.branch);
|
|
552
|
+
const engine = detectEngine(config);
|
|
553
|
+
const branch = cleanupTarget.branch;
|
|
554
|
+
const effectiveBranch = branch;
|
|
555
|
+
const worktreeCandidates = worktreeDirCandidates(config, branch);
|
|
556
|
+
const toolCandidates = tools.map((tool) => ({
|
|
557
|
+
tool,
|
|
558
|
+
candidates: toolConfigDirCandidates(tool, config.project, branch)
|
|
559
|
+
}));
|
|
560
|
+
const shellCandidates = shellConfigDirCandidates(config, branch);
|
|
561
|
+
const existing = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
562
|
+
const matchedContainers = options.discoveredContainers
|
|
563
|
+
? [...options.discoveredContainers]
|
|
564
|
+
: containerNameCandidates(config, branch).filter((name) => existing.includes(name));
|
|
565
|
+
const workspace = cleanupTarget.workspace;
|
|
566
|
+
const identities = [workspace];
|
|
567
|
+
const containers = options.discoveredContainers
|
|
568
|
+
? matchedContainers
|
|
569
|
+
: [...new Set([...containerNameCandidates(config, effectiveBranch), ...matchedContainers])];
|
|
570
|
+
const controlRoots = containers.flatMap((container) => identities.map((identity) => sandboxControlPaths({
|
|
571
|
+
base: config.controlBase, project: config.project, container, identity
|
|
572
|
+
}).root));
|
|
573
|
+
const workspaceViewRoots = containers.flatMap((container) => identities.map((identity) => sandboxWorkspaceViewPaths({
|
|
574
|
+
base: config.workspaceViewBase, project: config.project, container, identity
|
|
575
|
+
}).root));
|
|
576
|
+
const managedPathCandidates = [...new Set([
|
|
577
|
+
...worktreeCandidates,
|
|
578
|
+
...toolCandidates.flatMap(({ candidates }) => candidates),
|
|
579
|
+
...shellCandidates,
|
|
580
|
+
shareBranchDir(config, branch),
|
|
581
|
+
...controlRoots,
|
|
582
|
+
...workspaceViewRoots
|
|
583
|
+
].map((candidate) => path.resolve(candidate)))];
|
|
584
|
+
return {
|
|
585
|
+
branch,
|
|
586
|
+
effectiveBranch,
|
|
587
|
+
engine,
|
|
588
|
+
matchedContainers,
|
|
589
|
+
existingWorktrees: worktreeCandidates.filter((candidate) => fs.existsSync(candidate)),
|
|
590
|
+
toolCandidates,
|
|
591
|
+
managedPathCandidates,
|
|
592
|
+
workspace,
|
|
593
|
+
controlRoots: [...new Set(controlRoots)],
|
|
594
|
+
workspaceViewRoots: [...new Set(workspaceViewRoots)]
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function assertRemoved(target, label) {
|
|
598
|
+
if (fs.existsSync(target))
|
|
599
|
+
throw new Error(`${label} still exists after removal: ${target}`);
|
|
600
|
+
}
|
|
601
|
+
function sandboxContainersRemoved(engine, containers) {
|
|
602
|
+
if (containers.length === 0)
|
|
603
|
+
return true;
|
|
604
|
+
const remaining = new Set(runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}'])
|
|
605
|
+
.split('\n')
|
|
606
|
+
.filter(Boolean));
|
|
607
|
+
return containers.every((container) => !remaining.has(container));
|
|
608
|
+
}
|
|
609
|
+
function removeEmptyManagedParent(base, directory) {
|
|
610
|
+
const parent = path.dirname(directory);
|
|
611
|
+
if (path.resolve(parent) === path.resolve(base))
|
|
612
|
+
return;
|
|
613
|
+
assertManagedPath(base, parent);
|
|
614
|
+
try {
|
|
615
|
+
fs.rmdirSync(parent);
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
if (['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code ?? ''))
|
|
619
|
+
return;
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
assertRemoved(parent, 'Empty sandbox container directory');
|
|
623
|
+
}
|
|
624
|
+
function blockerMessage(blockers) {
|
|
625
|
+
return blockers.map((blocker) => {
|
|
626
|
+
if (blocker.status === 'failed')
|
|
627
|
+
return `${JSON.stringify(blocker.worktree)}: ${blocker.message}`;
|
|
628
|
+
return `${formatWorktreeSnapshot(blocker.snapshot)}\n Action: commit, stash, clean, or remove this sandbox interactively.`;
|
|
629
|
+
}).join('\n');
|
|
630
|
+
}
|
|
631
|
+
function cleanPermits(inspections) {
|
|
632
|
+
const permits = new Map();
|
|
633
|
+
for (const inspection of inspections) {
|
|
634
|
+
if (inspection.status === 'clean')
|
|
635
|
+
permits.set(inspection.snapshot.worktree, createCleanPermit(inspection.snapshot));
|
|
636
|
+
}
|
|
637
|
+
return permits;
|
|
638
|
+
}
|
|
639
|
+
async function authorizeWorktrees(worktrees, { allowDirtyDiscard, assumeYes }, { interactive = Boolean(process.stdin.isTTY), confirm = p.confirm, isCancel = p.isCancel, recovery = new Map() } = {}) {
|
|
640
|
+
const inspections = worktrees.map((worktree) => {
|
|
641
|
+
const inspection = inspectWorktrees([worktree])[0];
|
|
642
|
+
const context = recovery.get(path.resolve(worktree));
|
|
643
|
+
return inspection.status === 'failed' && context
|
|
644
|
+
? inspectRecoveredWorktree(worktree, context)
|
|
645
|
+
: inspection;
|
|
646
|
+
});
|
|
647
|
+
const failures = inspections.filter((inspection) => inspection.status === 'failed');
|
|
648
|
+
if (failures.length > 0)
|
|
649
|
+
throw new Error(`Unable to inspect worktree(s):\n${blockerMessage(failures)}`);
|
|
650
|
+
const permits = cleanPermits(inspections);
|
|
651
|
+
for (const inspection of inspections) {
|
|
652
|
+
if (inspection.status !== 'dirty')
|
|
653
|
+
continue;
|
|
654
|
+
if (!allowDirtyDiscard || assumeYes || !interactive) {
|
|
655
|
+
throw new Error(`Refusing to remove dirty worktree(s):\n${blockerMessage([inspection])}`);
|
|
656
|
+
}
|
|
657
|
+
p.log.warn(formatWorktreeSnapshot(inspection.snapshot));
|
|
658
|
+
const confirmed = await confirm({
|
|
659
|
+
message: 'Discard this worktree and all its uncommitted changes, including later changes?',
|
|
660
|
+
initialValue: false
|
|
661
|
+
});
|
|
662
|
+
if (isCancel(confirmed) || !confirmed)
|
|
663
|
+
throw new Error('Dirty worktree removal cancelled; nothing was deleted');
|
|
664
|
+
permits.set(inspection.snapshot.worktree, createDiscardPermit(inspection.snapshot));
|
|
665
|
+
}
|
|
666
|
+
return permits;
|
|
667
|
+
}
|
|
668
|
+
async function runRmOneUnderRepositoryLock(config, tools, branch, options = {}) {
|
|
669
|
+
const target = options.target ?? resolveRmTarget(config, tools, options.cleanupTarget ?? resolveSandboxCleanupTarget(branch, config.repoRoot));
|
|
670
|
+
return rmOneCore(config, tools, branch, { ...options, target });
|
|
671
|
+
}
|
|
672
|
+
async function rmOne(config, tools, branch, options = {}) {
|
|
673
|
+
await runRmOneUnderRepositoryLock(config, tools, branch, options);
|
|
674
|
+
}
|
|
675
|
+
async function removeUncheckedSandbox(config, target, options) {
|
|
676
|
+
const confirm = options.prompt?.confirm ?? p.confirm;
|
|
677
|
+
const isCancel = options.prompt?.isCancel ?? p.isCancel;
|
|
678
|
+
const { effectiveBranch, engine, matchedContainers, existingWorktrees, toolCandidates, controlRoots, workspaceViewRoots } = target;
|
|
679
|
+
if (!options.quiet)
|
|
680
|
+
p.intro(pc.cyan(`Removing sandbox for ${target.branch}`));
|
|
681
|
+
const shouldRemoveWorktree = existingWorktrees.length > 0 && !options.assumeYes
|
|
682
|
+
? await confirm({ message: `Remove worktree(s): ${existingWorktrees.join(', ')}?`, initialValue: true })
|
|
683
|
+
: existingWorktrees.length > 0;
|
|
684
|
+
if (isCancel(shouldRemoveWorktree)) {
|
|
685
|
+
if (!options.quiet)
|
|
686
|
+
p.outro('Cancelled');
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
const shouldDeleteBranch = Boolean(shouldRemoveWorktree) && existingWorktrees.length > 0 && !options.assumeYes
|
|
690
|
+
? await confirm({ message: `Also delete local branch '${effectiveBranch}'?`, initialValue: true })
|
|
691
|
+
: Boolean(shouldRemoveWorktree) && existingWorktrees.length > 0;
|
|
692
|
+
if (isCancel(shouldDeleteBranch)) {
|
|
693
|
+
if (!options.quiet)
|
|
694
|
+
p.outro('Cancelled');
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
const sharePath = path.resolve(shareBranchDir(config, effectiveBranch));
|
|
698
|
+
const shouldRemoveShare = fs.existsSync(sharePath) && !options.assumeYes
|
|
699
|
+
? await confirm({ message: `Remove share dir for branch '${effectiveBranch}' (${sharePath})?`, initialValue: true })
|
|
700
|
+
: fs.existsSync(sharePath);
|
|
701
|
+
if (isCancel(shouldRemoveShare)) {
|
|
702
|
+
if (!options.quiet)
|
|
703
|
+
p.outro('Cancelled');
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
for (const container of matchedContainers)
|
|
707
|
+
runSafeEngine(engine, 'docker', ['rm', '-f', container]);
|
|
708
|
+
for (const root of controlRoots) {
|
|
709
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
710
|
+
removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
|
|
711
|
+
}
|
|
712
|
+
for (const root of workspaceViewRoots) {
|
|
713
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
714
|
+
removeEmptyManagedParent(path.join(config.workspaceViewBase, config.project), root);
|
|
715
|
+
}
|
|
716
|
+
for (const candidate of toolCandidates.flatMap(({ candidates }) => candidates))
|
|
717
|
+
fs.rmSync(candidate, { recursive: true, force: true });
|
|
718
|
+
for (const shell of shellConfigDirCandidates(config, effectiveBranch))
|
|
719
|
+
fs.rmSync(shell, { recursive: true, force: true });
|
|
720
|
+
if (shouldRemoveShare)
|
|
721
|
+
fs.rmSync(sharePath, { recursive: true, force: true });
|
|
722
|
+
if (shouldRemoveWorktree) {
|
|
723
|
+
for (const worktree of existingWorktrees)
|
|
724
|
+
fs.rmSync(worktree, { recursive: true, force: true });
|
|
725
|
+
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
726
|
+
}
|
|
727
|
+
if (shouldDeleteBranch)
|
|
728
|
+
runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
|
|
729
|
+
if (!sandboxContainersRemoved(engine, matchedContainers)) {
|
|
730
|
+
throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${matchedContainers.join(', ')}`);
|
|
731
|
+
}
|
|
732
|
+
if (target.workspace.mode === 'task-bound') {
|
|
733
|
+
releaseStaleShortIdRegistry(config.repoRoot, target.workspace.taskId);
|
|
734
|
+
}
|
|
735
|
+
if (!options.quiet)
|
|
736
|
+
p.outro('Sandbox removed');
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
async function rmOneCore(config, tools, branch, options = {}) {
|
|
740
|
+
const target = options.target ?? resolveRmTarget(config, tools, options.cleanupTarget ?? resolveSandboxCleanupTarget(branch, config.repoRoot));
|
|
741
|
+
return removeUncheckedSandbox(config, target, options);
|
|
742
|
+
}
|
|
743
|
+
async function rmPurge(config, tools, prompt = {}) {
|
|
744
|
+
return rmPurgeCore(config, tools, prompt);
|
|
745
|
+
}
|
|
746
|
+
async function rmPurgeCore(config, tools, prompt = {}) {
|
|
747
|
+
const engine = detectEngine(config);
|
|
748
|
+
const confirm = prompt.confirm ?? p.confirm;
|
|
749
|
+
const isCancel = prompt.isCancel ?? p.isCancel;
|
|
750
|
+
p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
|
|
751
|
+
const containers = runEngine(engine, 'docker', [
|
|
752
|
+
'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
|
|
753
|
+
]).split('\n').filter(Boolean);
|
|
754
|
+
for (const name of containers)
|
|
755
|
+
runSafeEngine(engine, 'docker', ['rm', '-f', name]);
|
|
756
|
+
const worktrees = fs.existsSync(config.worktreeBase)
|
|
757
|
+
? fs.readdirSync(config.worktreeBase).map((entry) => path.join(config.worktreeBase, entry))
|
|
758
|
+
: [];
|
|
759
|
+
if (worktrees.length > 0) {
|
|
760
|
+
const selected = await confirm({ message: `Remove all worktrees in ${config.worktreeBase}?`, initialValue: true });
|
|
761
|
+
if (!isCancel(selected) && selected) {
|
|
762
|
+
for (const worktree of worktrees)
|
|
763
|
+
fs.rmSync(worktree, { recursive: true, force: true });
|
|
764
|
+
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
for (const dir of projectToolDirs(config, tools))
|
|
768
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
769
|
+
if (fs.existsSync(config.shellConfigBase)) {
|
|
770
|
+
const selected = await confirm({ message: `Remove all shell config dirs in ${config.shellConfigBase}?`, initialValue: true });
|
|
771
|
+
if (!isCancel(selected) && selected)
|
|
772
|
+
fs.rmSync(config.shellConfigBase, { recursive: true, force: true });
|
|
773
|
+
}
|
|
774
|
+
if (fs.existsSync(config.shareBase)) {
|
|
775
|
+
const selected = await confirm({ message: `Remove all share dirs for project (${config.shareBase})?`, initialValue: true });
|
|
776
|
+
if (!isCancel(selected) && selected)
|
|
777
|
+
fs.rmSync(config.shareBase, { recursive: true, force: true });
|
|
778
|
+
}
|
|
779
|
+
for (const base of [config.workspaceViewBase, config.controlBase]) {
|
|
780
|
+
fs.rmSync(path.join(base, config.project), { recursive: true, force: true });
|
|
781
|
+
}
|
|
782
|
+
const removeImage = await confirm({ message: `Remove image ${config.imageName}?`, initialValue: false });
|
|
783
|
+
if (!isCancel(removeImage) && removeImage)
|
|
784
|
+
runSafeEngine(engine, 'docker', ['rmi', config.imageName]);
|
|
785
|
+
p.outro(pc.green('All project sandboxes removed'));
|
|
786
|
+
}
|
|
787
|
+
async function rmUnbound(config, tools, options) {
|
|
788
|
+
return rmUnboundCore(config, tools, options);
|
|
789
|
+
}
|
|
790
|
+
async function rmUnboundCore(config, tools, options) {
|
|
791
|
+
const engine = detectEngine(config);
|
|
792
|
+
const listed = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
|
|
793
|
+
const rows = [...listed.running, ...listed.nonRunning];
|
|
794
|
+
p.intro(pc.cyan(`Removing sandboxes for ${config.project}`));
|
|
795
|
+
if (options.dryRun) {
|
|
796
|
+
p.outro(`Dry run: ${rows.length} sandbox(es) found, nothing deleted`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
for (const row of rows) {
|
|
800
|
+
if (!row.branch) {
|
|
801
|
+
runSafeEngine(engine, 'docker', ['rm', '-f', row.name]);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const cleanupTarget = {
|
|
805
|
+
requestedRef: row.branch,
|
|
806
|
+
branch: row.branch,
|
|
807
|
+
workspace: row.workspaceMode === 'task-bound' && row.taskId
|
|
808
|
+
? { mode: 'task-bound', taskId: row.taskId }
|
|
809
|
+
: { mode: 'branch-only' },
|
|
810
|
+
taskState: 'branch-only'
|
|
811
|
+
};
|
|
812
|
+
const target = resolveRmTarget(config, tools, cleanupTarget, { discoveredContainers: [row.name] });
|
|
813
|
+
await rmOne(config, tools, cleanupTarget.branch, {
|
|
814
|
+
assumeYes: options.assumeYes,
|
|
815
|
+
target,
|
|
816
|
+
cleanupTarget
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
p.outro(pc.green(`Removed ${rows.length} sandbox(es)`));
|
|
820
|
+
}
|
|
821
|
+
export { authorizeWorktrees, rmOne, rmPurge, rmUnbound };
|
|
822
|
+
//# sourceMappingURL=removal.js.map
|