chainlesschain 0.162.186 → 0.162.188
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/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-DTXiO-TB.js → AIOps-Brr1LVcE.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BbmDW-Pz.js → ActionButton-DQU74DKG.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CSfgXJuD.js → Analytics-CYp9178y.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BS0FFLb7.js → AppLayout-DW4SCYsW.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-B19WFPK2.js → Artifacts-CYlD_ed1.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CQhZBi7n.js → Audit-CWl1rZOi.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BOLZCpGH.js → BackgroundAgents-BOR-LPsa.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Dz3R_ePD.js → Backup-DLpG7j1X.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CYfgMpax.js → BaseInput-B35K9wdr.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-5CymMr1j.js → Chat-CwUJb9gB.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-B2A_TJtl.js → ChatBubbleRenderer-1v6aMS1m.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-CDXHVYcu.js → Checkbox-pi3-tPNW.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-uq9Z9oiS.js → Codegen-FyNF35eL.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BuW1K_65.js → Col-tGt_JDEr.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Bbg-PvRY.js → Community-CC7LonQX.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-co0pPPTw.js → Compact-BGXrHHjE.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-B-pFAv7z.js → Compliance-DSW0TBZ8.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-54ZQS4gC.js → Cowork-Bhk7rbvE.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-B1BF2Seq.js → Cron-DxyiQ2NZ.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Dey6qQTj.js → Crosschain-B2ZkxHok.js} +1 -1
- package/src/assets/web-panel/assets/{DID-ltERvfAj.js → DID-yOJV99eJ.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DuWxOcqN.js → Dashboard-CKsttz7D.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CrZ8ekTo.js → Dropdown-CVFldXSL.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-fIyHXhUS.js → EmailListRenderer-Bufv638J.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BtRyrRPm.js → FamilyGuardDashboard-4ncjoIjL.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-EUC6Wydj.js → Federation-DsknEDoX.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BOpka2G-.js → FormItemContext-Cqe5jWXA.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BXtrrYHn.js → GenericCardRenderer-NAJaFKxc.js} +1 -1
- package/src/assets/web-panel/assets/{Git-8WMb7PiR.js → Git-CszKQpnl.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DNWOeDTV.js → Governance-BXGodXi6.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Bne9b_Lb.js → Inference-CkoYuG2c.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Cp-6Us9b.js → KnowledgeGraph-DFlEr_T0.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Ck-uSPAx.js → Logs-CVaENddA.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-7iWGwZ-r.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-Vdq7brrJ.js → Marketplace-CDlLT0fw.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-ggLnw4hY.js → McpTools-DdwOyszN.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-BKIjNVCU.js → Memory-DKQnkKuG.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BElCY25a.js → MobileBridge-DGseqlzJ.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DpLgovdH.js → MobileProjects-COBbEOlj.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-bNdrF_Zh.js → Mtc-O61Vwyli.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Dw8w1Cqk.js → MtcAudit-Dw2SyasS.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CfGKvlqc.js → Multisig-hSu_l8kf.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-D6OJf7YN.js → NLProgramming-CX1Epurr.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B3n_LzIS.js → Notes-R9xX8o8_.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-Uu1V_ICM.js → NotificationSettings-jtSNLCuK.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-aGyWwWcp.js → OrderTableRenderer-BU2Yqq5H.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-qjFFmuwK.js → Organization-Pu2WO-U3.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-J4ZgnwZz.js → Overflow-BjnqUyFp.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-YEZF-uVH.js → P2P-jjkvz44c.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-yqx7fwqk.js → PdhVaultBrowser-69ca_WeR.js} +4 -4
- package/src/assets/web-panel/assets/{Permissions-DdEjlZ02.js → Permissions-D4yDpivd.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DbAPYvns.js → PersonalDataHub-0qsL6iH1.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-Bxlk_ZJU.js → Pipeline-Cx7Ybrk2.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CpE-05tz.js → Privacy-PbfR_M_V.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DWlhcGHT.js → ProjectInit-CyV6Gsub.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BnH6BiTQ.js → ProjectSettings-C7KZ2Wdk.js} +2 -2
- package/src/assets/web-panel/assets/Projects-DQDjO-yK.js +1 -0
- package/src/assets/web-panel/assets/{Providers-Bdz_sTyb.js → Providers-DyFVBBMX.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BHh2b3pH.js → QrScannerModal-CzB1BC9_.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-D84M09pe.js → QuickAsk-Bk636mC1.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Czc30K6M.js → Recommend-DoWxHIT3.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-q_TU5qxt.js → RemoteSession-oOVIv9wq.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CnTce4WM.js → Reputation-BucoReGG.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C60UFLay.js → Row-DzmOdJU7.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BOUFEu9_.js → RssFeed-C1tK7KGU.js} +3 -3
- package/src/assets/web-panel/assets/{Search-D4__blE-.js → Search-CSnInOqk.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BoOm7_JK.js → Security-dlejr15l.js} +3 -3
- package/src/assets/web-panel/assets/{Services-AvzrW6HA.js → Services-BioPZuNh.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BZlrHozi.js → Skeleton-DWMibwpC.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CKaMLuk0.js → Skills-BAAnx1B2.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DYCNJF8h.js → Sla-jWNp4hNO.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-y5kqKha-.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-B6tW8dVO.js → SyncSettings-B9GNsc3e.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-oNC_2NVz.js → Tasks-BDBYKRen.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DvLPvvjL.js → Templates-DRiE5eaN.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BDt9UBn_.js → Tenant-BGlAvExF.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-ddCUnH-X.js → Terminal-B8uzrbNY.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CA4sDL47.js → TimelineRenderer-Df48iveY.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-By03hnVs.js → Tokens-CcmHndN6.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dz2mH-ZU.js → Trigger-DPkWxmQ2.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DShSWHeJ.js → Trust-DTb5a5AV.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-JN8lP3gt.js → UkeySign-CBmg5vch.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BgPy4gV3.js → VideoEditing-B0mZrz4O.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CQGEsXw_.js → Wallet-Bxs7_mpd.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-D8xR-ds_.js → WebAuthn-C8x3cMxU.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-Df61_p1p.js → WorkflowEditor-DPrNznrP.js} +1 -1
- package/src/assets/web-panel/assets/{chat-fdPby427.js → chat-D3bYWu7S.js} +1 -1
- package/src/assets/web-panel/assets/{colors-Dimqhufo.js → colors-DY-PwMu4.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DHayKcqZ.js → compact-item-LDk10Kw6.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-hZUhypRH.js → createContext-C-2MZnFq.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-B4dXIJqD.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Dw7zC_dV.js → hasIn-BQIBjcnB.js} +1 -1
- package/src/assets/web-panel/assets/{index-y5LHQ5Ck.js → index-7xKu7Fns.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBQK-LEF.js → index-8JC9BJa1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuMSdwno.js → index-B5pgtZnY.js} +1 -1
- package/src/assets/web-panel/assets/{index-BM5BNqp9.js → index-BI4KgcYC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BwnUluxu.js → index-BN-xdSWZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7jQEwB6.js → index-BRI3ngNg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIxxvaGN.js → index-BX2b9Orc.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1yMYKi-.js → index-BdXnYMCZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CuaVa8tX.js → index-BrnRO9yE.js} +1 -1
- package/src/assets/web-panel/assets/index-C-C7DaG6.js +1 -0
- package/src/assets/web-panel/assets/{index-CIOE8tt6.js → index-C3GNvExG.js} +1 -1
- package/src/assets/web-panel/assets/{index-C8XXPz2n.js → index-C7FC8cP2.js} +1 -1
- package/src/assets/web-panel/assets/{index-cEnkyHtX.js → index-COG61sRv.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJP7TjWm.js → index-CQVb2dWp.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvKHpaao.js → index-CY4KnaO9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxoPzPHy.js → index-CaslYch6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSlda4-6.js → index-CbR6ROOa.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPZ3U5iH.js → index-CiTvBAr-.js} +1 -1
- package/src/assets/web-panel/assets/{index-j0pxzOu7.js → index-Cx6D_dx2.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzJXkoZs.js → index-Cz4b6Z_7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BVHdkz8F.js → index-D3Oq-DLL.js} +17 -17
- package/src/assets/web-panel/assets/{index-ulvBurtw.js → index-D4K1weKc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyEhrvpm.js → index-DSvo_bp0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC2R4zvM.js → index-DTIZpPtG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsCo_A0I.js → index-DY2FIrmz.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTiVrnpR.js → index-DYyLJPlF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHJUptSx.js → index-Dd2Km97U.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcpDboUd.js → index-Dh-tkpKQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnwDYBnG.js → index-DhRt9cYJ.js} +1 -1
- package/src/assets/web-panel/assets/index-DkfWzWtd.js +1 -0
- package/src/assets/web-panel/assets/{index--DVpUpz3.js → index-DlRYqt3S.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVM_KgzF.js → index-HasPt4-h.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX1DwDFI.js → index-KFZO_lcx.js} +1 -1
- package/src/assets/web-panel/assets/{index-lPjBQJCy.js → index-L3smcoQX.js} +1 -1
- package/src/assets/web-panel/assets/{index-Urt2R5aE.js → index-L6qixyLc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYdNwhkv.js → index-VCh5zNJo.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bhmpp3cm.js → index-_HRO_cIu.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAjvkQ4K.js → index-k7C_oDCO.js} +1 -1
- package/src/assets/web-panel/assets/{index-JE6Udz2r.js → index-lmm9p-jm.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-B4pR3px5.js → initDefaultProps-zQPSHf2C.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CE7dWPPs.js → motion-CKCStGVk.js} +1 -1
- package/src/assets/web-panel/assets/{move-Hw8cUObc.js → move-BufVDGYF.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CvRjE4Fa.js → mtc-parser-DcaFxy5D.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CkFeLVre.js → omit-EScpkGdo.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-5ZMqmbbb.js → pickAttrs-72hG_qfT.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BZ4oeZ0q.js → placementArrow-CMFWiH20.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-lwoiX_Us.js → responsiveObserve-DKQHS41p.js} +1 -1
- package/src/assets/web-panel/assets/{slide-D_as37l8.js → slide-QVAFHTIQ.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BsA-4Bs7.js → statusUtils-DIz38e2G.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BVar4sLQ.js → styleChecker-zK5cyLby.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-j1IwVpGd.js → useFlexGapSupport-DhZngk62.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-7Q9bEnUp.js → useFs-BvV2cvkg.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BBwTIRMd.js → usePersonalDataHub-CRmxb9l3.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C7Dk-M8w.js → vnode-CgQIbobP.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D3UHaTFB.js → zoom-D4XrrO0R.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/auto-mode.js +47 -0
- package/src/commands/team.js +1805 -473
- package/src/data/auto-mode-safety-eval-v1.json +3294 -0
- package/src/data/changelog.json +26 -0
- package/src/harness/worktree-isolator.js +671 -79
- package/src/lib/agent-team/task-lease.js +378 -78
- package/src/lib/agent-team/team-agent-stream.js +488 -0
- package/src/lib/agent-team/team-budget.js +216 -20
- package/src/lib/agent-team/team-mailbox.js +389 -15
- package/src/lib/agent-team/team-run-state-lock.js +209 -0
- package/src/lib/agent-team/team-runner.js +547 -63
- package/src/lib/agent-team/team-scope-lock.js +409 -0
- package/src/lib/agent-team/team-task-contract.js +277 -0
- package/src/lib/agent-team/team-worktree.js +1171 -62
- package/src/lib/agent-worktree.js +19 -20
- package/src/lib/auto-mode-safety-classifier.js +2034 -0
- package/src/lib/auto-mode-safety-eval.js +1022 -0
- package/src/lib/collaboration-run-store.js +983 -68
- package/src/lib/with-file-lock.js +190 -24
- package/src/lib/worktree-isolator.js +3 -0
- package/src/lib/worktree-sparse.js +23 -0
- package/src/runtime/headless-stream.js +6 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-DxjsdAXw.js +0 -1
- package/src/assets/web-panel/assets/Projects-CHw2fXDB.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-BvCSU6d2.js +0 -1
- package/src/assets/web-panel/assets/devWarning-ePxoBp92.js +0 -1
- package/src/assets/web-panel/assets/index-BTBDdCoo.js +0 -1
- package/src/assets/web-panel/assets/index-CfOGx8Ns.js +0 -1
|
@@ -7,8 +7,17 @@
|
|
|
7
7
|
* Feature-flag gated: WORKTREE_ISOLATION
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
existsSync,
|
|
12
|
+
lstatSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
rmdirSync,
|
|
15
|
+
symlinkSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
rmSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { resolve, dirname, relative, isAbsolute, basename } from "node:path";
|
|
12
21
|
import {
|
|
13
22
|
isGitRepo,
|
|
14
23
|
gitExec,
|
|
@@ -24,6 +33,347 @@ import {
|
|
|
24
33
|
|
|
25
34
|
const WORKTREE_DIR = ".worktrees";
|
|
26
35
|
|
|
36
|
+
function normalizeFilesystemPath(value) {
|
|
37
|
+
const absolute = resolve(value);
|
|
38
|
+
return process.platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function nativeRealpath(value) {
|
|
42
|
+
return realpathSync.native ? realpathSync.native(value) : realpathSync(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve filesystem aliases without requiring the complete target to exist.
|
|
47
|
+
* `realpathSync()` alone cannot validate a not-yet-created worktree, while a
|
|
48
|
+
* lexical `resolve()` treats macOS' `/var` and `/private/var` (or a repository
|
|
49
|
+
* directory symlink) as different locations. Resolve the deepest existing
|
|
50
|
+
* prefix physically, then append only the still-missing path segments.
|
|
51
|
+
*/
|
|
52
|
+
function canonicalExistingPrefixPath(value) {
|
|
53
|
+
let cursor = resolve(value);
|
|
54
|
+
const missing = [];
|
|
55
|
+
|
|
56
|
+
for (;;) {
|
|
57
|
+
try {
|
|
58
|
+
return resolve(nativeRealpath(cursor), ...missing);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error?.code !== "ENOENT") throw error;
|
|
61
|
+
const parent = dirname(cursor);
|
|
62
|
+
if (parent === cursor) throw error;
|
|
63
|
+
missing.unshift(basename(cursor));
|
|
64
|
+
cursor = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sameFilesystemPath(left, right) {
|
|
70
|
+
return (
|
|
71
|
+
normalizeFilesystemPath(canonicalExistingPrefixPath(left)) ===
|
|
72
|
+
normalizeFilesystemPath(canonicalExistingPrefixPath(right))
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertNotSymlink(target, label) {
|
|
77
|
+
let stat;
|
|
78
|
+
try {
|
|
79
|
+
stat = lstatSync(target);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error?.code === "ENOENT") return;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
if (stat.isSymbolicLink()) {
|
|
85
|
+
throw new Error(`${label} must not be a symbolic link or junction`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function gitRepositoryIdentity(repoDir) {
|
|
90
|
+
const [topLevel, commonDir, ...unexpected] = gitExecArgs(
|
|
91
|
+
["rev-parse", "--show-toplevel", "--git-common-dir"],
|
|
92
|
+
repoDir,
|
|
93
|
+
).split(/\r?\n/);
|
|
94
|
+
if (!topLevel || !commonDir || unexpected.length > 0) {
|
|
95
|
+
throw new Error("unable to determine git repository identity");
|
|
96
|
+
}
|
|
97
|
+
const absoluteCommonDir = isAbsolute(commonDir)
|
|
98
|
+
? commonDir
|
|
99
|
+
: resolve(repoDir, commonDir);
|
|
100
|
+
return {
|
|
101
|
+
topLevel,
|
|
102
|
+
topLevelIdentity: normalizeFilesystemPath(
|
|
103
|
+
canonicalExistingPrefixPath(topLevel),
|
|
104
|
+
),
|
|
105
|
+
commonDirIdentity: normalizeFilesystemPath(
|
|
106
|
+
canonicalExistingPrefixPath(absoluteCommonDir),
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function assertSameGitRepository(repoDir, expectedCommonDir, expectedRoot) {
|
|
112
|
+
const identity = gitRepositoryIdentity(repoDir);
|
|
113
|
+
if (
|
|
114
|
+
identity.commonDirIdentity !== expectedCommonDir ||
|
|
115
|
+
identity.topLevelIdentity !== expectedRoot
|
|
116
|
+
) {
|
|
117
|
+
throw new Error("worktree does not belong to the expected git repository");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseRegisteredWorktrees(output) {
|
|
122
|
+
const worktrees = [];
|
|
123
|
+
let current = {};
|
|
124
|
+
|
|
125
|
+
for (const line of output.split(/\r?\n/)) {
|
|
126
|
+
if (line.startsWith("worktree ")) {
|
|
127
|
+
if (current.path) worktrees.push(current);
|
|
128
|
+
current = { path: line.slice(9) };
|
|
129
|
+
} else if (line.startsWith("HEAD ")) {
|
|
130
|
+
current.head = line.slice(5);
|
|
131
|
+
} else if (line.startsWith("branch ")) {
|
|
132
|
+
current.branch = line.slice(7).replace("refs/heads/", "");
|
|
133
|
+
} else if (line === "bare") {
|
|
134
|
+
current.bare = true;
|
|
135
|
+
} else if (line === "detached") {
|
|
136
|
+
current.detached = true;
|
|
137
|
+
} else if (line === "locked") {
|
|
138
|
+
current.locked = true;
|
|
139
|
+
} else if (line.startsWith("locked ")) {
|
|
140
|
+
current.locked = true;
|
|
141
|
+
current.lockReason = line.slice(7);
|
|
142
|
+
} else if (line === "prunable") {
|
|
143
|
+
current.prunable = true;
|
|
144
|
+
} else if (line.startsWith("prunable ")) {
|
|
145
|
+
current.prunable = true;
|
|
146
|
+
current.prunableReason = line.slice(9);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (current.path) worktrees.push(current);
|
|
150
|
+
return worktrees;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readRegisteredWorktrees(repoDir) {
|
|
154
|
+
return parseRegisteredWorktrees(
|
|
155
|
+
gitExec("worktree list --porcelain", repoDir),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function repositoryManagementContext(repoDir) {
|
|
160
|
+
const repository = gitRepositoryIdentity(repoDir);
|
|
161
|
+
const registeredWorktrees = readRegisteredWorktrees(repoDir);
|
|
162
|
+
const invocationRegistered = registeredWorktrees.some((worktree) => {
|
|
163
|
+
if (worktree.bare || !worktree.path) return false;
|
|
164
|
+
try {
|
|
165
|
+
return (
|
|
166
|
+
normalizeFilesystemPath(canonicalExistingPrefixPath(worktree.path)) ===
|
|
167
|
+
repository.topLevelIdentity
|
|
168
|
+
);
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
if (!invocationRegistered) {
|
|
174
|
+
throw new Error("repository checkout is not a registered git worktree");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Git lists the primary checkout first. It is the sole management anchor:
|
|
178
|
+
// calls made from a linked worktree still create a sibling below the primary
|
|
179
|
+
// checkout's `.worktrees`, never below an arbitrary externally-located
|
|
180
|
+
// registered checkout.
|
|
181
|
+
const primary = registeredWorktrees[0];
|
|
182
|
+
if (!primary || primary.bare || !primary.path || !existsSync(primary.path)) {
|
|
183
|
+
throw new Error("primary git worktree is unavailable");
|
|
184
|
+
}
|
|
185
|
+
const primaryPath = canonicalExistingPrefixPath(primary.path);
|
|
186
|
+
const primaryIdentity = normalizeFilesystemPath(primaryPath);
|
|
187
|
+
assertSameGitRepository(
|
|
188
|
+
primary.path,
|
|
189
|
+
repository.commonDirIdentity,
|
|
190
|
+
primaryIdentity,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
repository,
|
|
195
|
+
registeredWorktrees,
|
|
196
|
+
primaryPath,
|
|
197
|
+
primaryIdentity,
|
|
198
|
+
managedRoot: resolve(primaryPath, WORKTREE_DIR),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function assertPhysicalDescendant(target, root, label) {
|
|
203
|
+
const fromRoot = relative(realpathSync(root), realpathSync(target));
|
|
204
|
+
if (
|
|
205
|
+
fromRoot === "" ||
|
|
206
|
+
isAbsolute(fromRoot) ||
|
|
207
|
+
fromRoot === ".." ||
|
|
208
|
+
fromRoot.startsWith(`..${pathSeparator()}`)
|
|
209
|
+
) {
|
|
210
|
+
throw new Error(`${label} escapes its physical root`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function assertNoSymlinkAncestors(root, target, label) {
|
|
215
|
+
const rootPath = resolve(root);
|
|
216
|
+
const targetPath = resolve(target);
|
|
217
|
+
const fromRoot = relative(rootPath, targetPath);
|
|
218
|
+
if (
|
|
219
|
+
isAbsolute(fromRoot) ||
|
|
220
|
+
fromRoot === ".." ||
|
|
221
|
+
fromRoot.startsWith(`..${pathSeparator()}`)
|
|
222
|
+
) {
|
|
223
|
+
throw new Error(`${label} escapes its root`);
|
|
224
|
+
}
|
|
225
|
+
assertNotSymlink(rootPath, `${label} root`);
|
|
226
|
+
let cursor = rootPath;
|
|
227
|
+
for (const segment of fromRoot.split(/[\\/]/).filter(Boolean)) {
|
|
228
|
+
cursor = resolve(cursor, segment);
|
|
229
|
+
assertNotSymlink(cursor, label);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function managedWorktreePath(repoDir, branchName) {
|
|
234
|
+
assertSafeGitRef(branchName, "branch name");
|
|
235
|
+
const { managedRoot } = repositoryManagementContext(repoDir);
|
|
236
|
+
return resolve(managedRoot, branchName.replace(/\//g, "-"));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Validate a path before it reaches Git cleanup or recursive filesystem
|
|
241
|
+
* removal. Managed worktrees are direct children of `<repo>/.worktrees` and,
|
|
242
|
+
* when a branch is known, have exactly the deterministic path created for it.
|
|
243
|
+
*/
|
|
244
|
+
export function assertManagedWorktreePath(
|
|
245
|
+
repoDir,
|
|
246
|
+
worktreePath,
|
|
247
|
+
{ branchName = null } = {},
|
|
248
|
+
) {
|
|
249
|
+
if (
|
|
250
|
+
typeof repoDir !== "string" ||
|
|
251
|
+
repoDir.length === 0 ||
|
|
252
|
+
typeof worktreePath !== "string" ||
|
|
253
|
+
worktreePath.length === 0 ||
|
|
254
|
+
worktreePath.includes("\0")
|
|
255
|
+
) {
|
|
256
|
+
throw new TypeError("managed worktree path must be a non-empty string");
|
|
257
|
+
}
|
|
258
|
+
if (branchName != null) {
|
|
259
|
+
assertSafeGitRef(branchName, "branch name");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const context = repositoryManagementContext(repoDir);
|
|
263
|
+
const candidate = resolve(worktreePath);
|
|
264
|
+
const candidatePath = canonicalExistingPrefixPath(candidate);
|
|
265
|
+
const candidateIdentity = normalizeFilesystemPath(candidatePath);
|
|
266
|
+
const registered = context.registeredWorktrees.find((worktree) => {
|
|
267
|
+
try {
|
|
268
|
+
return sameFilesystemPath(worktree.path, candidatePath);
|
|
269
|
+
} catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
const managedRootPath = canonicalExistingPrefixPath(context.managedRoot);
|
|
274
|
+
const managedRootIdentity = normalizeFilesystemPath(managedRootPath);
|
|
275
|
+
if (
|
|
276
|
+
candidateIdentity === context.primaryIdentity ||
|
|
277
|
+
candidateIdentity === context.repository.topLevelIdentity ||
|
|
278
|
+
normalizeFilesystemPath(dirname(candidateIdentity)) !==
|
|
279
|
+
normalizeFilesystemPath(managedRootIdentity)
|
|
280
|
+
) {
|
|
281
|
+
throw new Error("refusing unmanaged worktree path");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (registered && branchName && registered.branch !== branchName) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
"registered worktree branch does not match cleanup request",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const effectiveBranch = branchName || registered?.branch || null;
|
|
290
|
+
if (effectiveBranch != null) {
|
|
291
|
+
assertSafeGitRef(effectiveBranch, "branch name");
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
effectiveBranch != null &&
|
|
295
|
+
!sameFilesystemPath(
|
|
296
|
+
candidatePath,
|
|
297
|
+
resolve(managedRootPath, effectiveBranch.replace(/\//g, "-")),
|
|
298
|
+
)
|
|
299
|
+
) {
|
|
300
|
+
throw new Error("managed worktree path does not match its branch");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (registered && existsSync(candidatePath)) {
|
|
304
|
+
assertSameGitRepository(
|
|
305
|
+
candidatePath,
|
|
306
|
+
context.repository.commonDirIdentity,
|
|
307
|
+
candidateIdentity,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
assertNotSymlink(context.managedRoot, "managed worktree root");
|
|
312
|
+
assertNotSymlink(candidate, "managed worktree");
|
|
313
|
+
assertNotSymlink(candidatePath, "managed worktree");
|
|
314
|
+
return candidatePath;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function pathSeparator() {
|
|
318
|
+
return process.platform === "win32" ? "\\" : "/";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function validateDependencySource(repoDir, entry) {
|
|
322
|
+
if (!existsSync(entry.source)) return false;
|
|
323
|
+
assertNoSymlinkAncestors(repoDir, entry.source, "dependency source");
|
|
324
|
+
if (!lstatSync(entry.source).isDirectory()) {
|
|
325
|
+
throw new Error(`dependency source is not a directory: ${entry.name}`);
|
|
326
|
+
}
|
|
327
|
+
assertPhysicalDescendant(entry.source, repoDir, "dependency source");
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Remove only dependency links that this module previously planned and
|
|
333
|
+
* verified. Ordinary directories are never removed: if a link was replaced
|
|
334
|
+
* after creation, cleanup fails closed and leaves the worktree for inspection.
|
|
335
|
+
*/
|
|
336
|
+
export function removeManagedDependencyLinks(
|
|
337
|
+
repoDir,
|
|
338
|
+
worktreePath,
|
|
339
|
+
directories = [],
|
|
340
|
+
) {
|
|
341
|
+
const plan = planSymlinkDirectories(directories, {
|
|
342
|
+
repoDir,
|
|
343
|
+
worktreePath,
|
|
344
|
+
});
|
|
345
|
+
const removed = [];
|
|
346
|
+
for (const entry of plan) {
|
|
347
|
+
let stat;
|
|
348
|
+
try {
|
|
349
|
+
stat = lstatSync(entry.dest);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (error?.code === "ENOENT") continue;
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
assertNoSymlinkAncestors(
|
|
355
|
+
worktreePath,
|
|
356
|
+
dirname(entry.dest),
|
|
357
|
+
"dependency destination",
|
|
358
|
+
);
|
|
359
|
+
if (!stat.isSymbolicLink()) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`managed dependency link was replaced by a real path: ${entry.name}`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
if (
|
|
365
|
+
!validateDependencySource(repoDir, entry) ||
|
|
366
|
+
!sameFilesystemPath(realpathSync(entry.dest), realpathSync(entry.source))
|
|
367
|
+
) {
|
|
368
|
+
throw new Error(`managed dependency link target changed: ${entry.name}`);
|
|
369
|
+
}
|
|
370
|
+
if (process.platform === "win32") rmdirSync(entry.dest);
|
|
371
|
+
else unlinkSync(entry.dest);
|
|
372
|
+
removed.push(entry.name);
|
|
373
|
+
}
|
|
374
|
+
return removed;
|
|
375
|
+
}
|
|
376
|
+
|
|
27
377
|
export function createWorktree(repoDir, branchName, baseBranch, options = {}) {
|
|
28
378
|
assertSafeGitRef(branchName, "branch name");
|
|
29
379
|
if (baseBranch) assertSafeGitRef(baseBranch, "base branch");
|
|
@@ -31,83 +381,323 @@ export function createWorktree(repoDir, branchName, baseBranch, options = {}) {
|
|
|
31
381
|
throw new Error("Not a git repository");
|
|
32
382
|
}
|
|
33
383
|
|
|
34
|
-
const worktreePath =
|
|
384
|
+
const worktreePath = assertManagedWorktreePath(
|
|
35
385
|
repoDir,
|
|
36
|
-
|
|
37
|
-
branchName
|
|
386
|
+
managedWorktreePath(repoDir, branchName),
|
|
387
|
+
{ branchName },
|
|
38
388
|
);
|
|
39
389
|
|
|
40
390
|
if (existsSync(worktreePath)) {
|
|
41
391
|
throw new Error(`Worktree already exists: ${worktreePath}`);
|
|
42
392
|
}
|
|
43
393
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const result = { path: worktreePath, branch: branchName };
|
|
48
|
-
|
|
49
|
-
// Sparse checkout: only materialize the packages this task needs. For a
|
|
50
|
-
// large monorepo this cuts worktree creation time and disk. Cone mode wants
|
|
51
|
-
// directory paths — our sparsePaths are package dirs. Fail-closed on unsafe
|
|
52
|
-
// paths (normalizeSparsePaths drops them); empty/absent → full checkout,
|
|
53
|
-
// byte-identical to the no-sparse path.
|
|
394
|
+
// Validate all task-controlled configuration before creating a branch. A bad
|
|
395
|
+
// plan must not leave an untracked worktree behind.
|
|
54
396
|
const sparsePaths = normalizeSparsePaths(options.sparsePaths);
|
|
55
397
|
if (sparsePaths) {
|
|
56
|
-
for (const
|
|
57
|
-
|
|
58
|
-
|
|
398
|
+
for (const item of sparsePaths) {
|
|
399
|
+
assertSafeGitPath(item, "sparse path");
|
|
400
|
+
}
|
|
59
401
|
}
|
|
402
|
+
const symlinkPlan =
|
|
403
|
+
options.symlinkDirectories == null
|
|
404
|
+
? []
|
|
405
|
+
: planSymlinkDirectories(options.symlinkDirectories, {
|
|
406
|
+
repoDir,
|
|
407
|
+
worktreePath,
|
|
408
|
+
});
|
|
409
|
+
const availableSymlinkPlan = symlinkPlan.filter((entry) =>
|
|
410
|
+
validateDependencySource(repoDir, entry),
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const base = baseBranch || "HEAD";
|
|
414
|
+
gitExecArgs(
|
|
415
|
+
["worktree", "add", worktreePath, "-b", branchName, base],
|
|
416
|
+
repoDir,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const result = { path: worktreePath, branch: branchName };
|
|
420
|
+
let createdBranchOid = null;
|
|
60
421
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (options.symlinkDirectories != null) {
|
|
65
|
-
const plan = planSymlinkDirectories(options.symlinkDirectories, {
|
|
422
|
+
try {
|
|
423
|
+
createdBranchOid = gitExecArgs(
|
|
424
|
+
["rev-parse", "--verify", `refs/heads/${branchName}^{commit}`],
|
|
66
425
|
repoDir,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
426
|
+
)
|
|
427
|
+
.trim()
|
|
428
|
+
.toLowerCase();
|
|
429
|
+
assertManagedWorktreePath(repoDir, worktreePath, { branchName });
|
|
430
|
+
// Sparse checkout: only materialize the packages this task needs. For a
|
|
431
|
+
// large monorepo this cuts worktree creation time and disk. Cone mode wants
|
|
432
|
+
// directory paths — our sparsePaths are package dirs. Fail-closed on unsafe
|
|
433
|
+
// paths (normalizeSparsePaths drops them); empty/absent → full checkout,
|
|
434
|
+
// byte-identical to the no-sparse path.
|
|
435
|
+
if (sparsePaths) {
|
|
436
|
+
gitExecArgs(["sparse-checkout", "set", ...sparsePaths], worktreePath);
|
|
437
|
+
result.sparsePaths = sparsePaths;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Dependency reuse: link explicitly-approved directories (e.g. node_modules)
|
|
441
|
+
// from the main checkout into the worktree so the task does not reinstall.
|
|
442
|
+
// planSymlinkDirectories throws (fail-closed) on any junction/symlink escape.
|
|
443
|
+
if (availableSymlinkPlan.length > 0) {
|
|
444
|
+
const linked = [];
|
|
445
|
+
for (const { name, source, dest } of availableSymlinkPlan) {
|
|
446
|
+
assertNoSymlinkAncestors(
|
|
447
|
+
worktreePath,
|
|
448
|
+
dirname(dest),
|
|
449
|
+
"dependency destination",
|
|
450
|
+
);
|
|
451
|
+
assertNotSymlink(dest, "dependency destination");
|
|
452
|
+
if (existsSync(dest)) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`dependency destination already exists; refusing replacement: ${name}`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
458
|
+
// 'junction' on Windows needs no admin and works for directories;
|
|
459
|
+
// 'dir' elsewhere. Both resolve to an absolute source we validated.
|
|
460
|
+
symlinkSync(
|
|
461
|
+
source,
|
|
462
|
+
dest,
|
|
463
|
+
process.platform === "win32" ? "junction" : "dir",
|
|
464
|
+
);
|
|
465
|
+
// Register immediately after creation. If verification below fails,
|
|
466
|
+
// rollback must know a reparse point exists and must never send an
|
|
467
|
+
// unrecorded junction into force-removal.
|
|
468
|
+
linked.push(name);
|
|
469
|
+
result.symlinkedDirectories = [...linked];
|
|
470
|
+
if (
|
|
471
|
+
!lstatSync(dest).isSymbolicLink() ||
|
|
472
|
+
!sameFilesystemPath(realpathSync(dest), realpathSync(source))
|
|
473
|
+
) {
|
|
474
|
+
throw new Error(`dependency link verification failed: ${name}`);
|
|
475
|
+
}
|
|
476
|
+
assertManagedWorktreePath(repoDir, worktreePath, { branchName });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
try {
|
|
481
|
+
removeManagedDependencyLinks(
|
|
482
|
+
repoDir,
|
|
483
|
+
worktreePath,
|
|
484
|
+
result.symlinkedDirectories || [],
|
|
80
485
|
);
|
|
81
|
-
|
|
486
|
+
removeWorktree(repoDir, worktreePath, {
|
|
487
|
+
deleteBranch: true,
|
|
488
|
+
branchName,
|
|
489
|
+
...(createdBranchOid ? { expectedBranchOid: createdBranchOid } : {}),
|
|
490
|
+
force: true,
|
|
491
|
+
});
|
|
492
|
+
} catch (cleanupError) {
|
|
493
|
+
error.cleanupError =
|
|
494
|
+
cleanupError instanceof Error
|
|
495
|
+
? cleanupError.message
|
|
496
|
+
: String(cleanupError);
|
|
497
|
+
error.retainedWorktree = { path: worktreePath, branch: branchName };
|
|
82
498
|
}
|
|
83
|
-
|
|
499
|
+
throw error;
|
|
84
500
|
}
|
|
85
501
|
|
|
86
502
|
return result;
|
|
87
503
|
}
|
|
88
504
|
|
|
505
|
+
function findRegisteredWorktreeByPath(worktrees, worktreePath) {
|
|
506
|
+
return (
|
|
507
|
+
worktrees.find((worktree) =>
|
|
508
|
+
sameFilesystemPath(worktree.path, worktreePath),
|
|
509
|
+
) || null
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function validateWorktreeRemovalTarget(repoDir, worktreePath, branchName) {
|
|
514
|
+
const validatedPath = assertManagedWorktreePath(repoDir, worktreePath, {
|
|
515
|
+
branchName,
|
|
516
|
+
});
|
|
517
|
+
const registered = findRegisteredWorktreeByPath(
|
|
518
|
+
readRegisteredWorktrees(repoDir),
|
|
519
|
+
validatedPath,
|
|
520
|
+
);
|
|
521
|
+
if (existsSync(validatedPath) && !registered) {
|
|
522
|
+
throw new Error("refusing to remove an unregistered worktree path");
|
|
523
|
+
}
|
|
524
|
+
if (registered?.locked) {
|
|
525
|
+
const reason = registered.lockReason ? `: ${registered.lockReason}` : "";
|
|
526
|
+
throw new Error(`refusing to remove a locked worktree${reason}`);
|
|
527
|
+
}
|
|
528
|
+
if (registered && branchName && registered.branch !== branchName) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
"registered worktree branch does not match cleanup request",
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
return { validatedPath, registered };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function assertWorktreeCleanForRemoval(worktreePath) {
|
|
537
|
+
const status = gitExecArgs(
|
|
538
|
+
[
|
|
539
|
+
"status",
|
|
540
|
+
"--porcelain=v1",
|
|
541
|
+
"--untracked-files=all",
|
|
542
|
+
"--ignored=matching",
|
|
543
|
+
"--ignore-submodules=none",
|
|
544
|
+
],
|
|
545
|
+
worktreePath,
|
|
546
|
+
);
|
|
547
|
+
if (status.trim() !== "") {
|
|
548
|
+
throw new Error(
|
|
549
|
+
"refusing to remove a worktree with tracked, untracked, or ignored files",
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function readLocalBranchOid(repoDir, branch) {
|
|
555
|
+
assertSafeGitRef(branch, "branch name");
|
|
556
|
+
const ref = `refs/heads/${branch}`;
|
|
557
|
+
const output = gitExecArgs(
|
|
558
|
+
["for-each-ref", "--format=%(refname) %(objectname)", ref],
|
|
559
|
+
repoDir,
|
|
560
|
+
).trim();
|
|
561
|
+
if (output === "") return null;
|
|
562
|
+
const prefix = `${ref} `;
|
|
563
|
+
const exactMatches = output
|
|
564
|
+
.split(/\r?\n/)
|
|
565
|
+
.filter((line) => line.startsWith(prefix));
|
|
566
|
+
if (
|
|
567
|
+
exactMatches.length !== 1 ||
|
|
568
|
+
!/^[a-f0-9]{40,64}$/i.test(exactMatches[0].slice(prefix.length))
|
|
569
|
+
) {
|
|
570
|
+
if (exactMatches.length === 0) return null;
|
|
571
|
+
throw new Error(`unable to verify worktree branch: ${branch}`);
|
|
572
|
+
}
|
|
573
|
+
return exactMatches[0].slice(prefix.length).toLowerCase();
|
|
574
|
+
}
|
|
575
|
+
|
|
89
576
|
export function removeWorktree(repoDir, worktreePath, options = {}) {
|
|
90
577
|
const deleteBranch = options.deleteBranch !== false;
|
|
578
|
+
const force = options.force === true;
|
|
579
|
+
const expectedBranchOid =
|
|
580
|
+
options.expectedBranchOid == null
|
|
581
|
+
? null
|
|
582
|
+
: String(options.expectedBranchOid).toLowerCase();
|
|
583
|
+
if (
|
|
584
|
+
expectedBranchOid != null &&
|
|
585
|
+
!/^[a-f0-9]{40,64}$/i.test(expectedBranchOid)
|
|
586
|
+
) {
|
|
587
|
+
throw new TypeError("expected worktree branch OID is invalid");
|
|
588
|
+
}
|
|
589
|
+
const initialTarget = validateWorktreeRemovalTarget(
|
|
590
|
+
repoDir,
|
|
591
|
+
worktreePath,
|
|
592
|
+
options.branchName || null,
|
|
593
|
+
);
|
|
594
|
+
const validatedPath = initialTarget.validatedPath;
|
|
595
|
+
const registered = initialTarget.registered;
|
|
596
|
+
const initiallySettled = registered == null && !existsSync(validatedPath);
|
|
597
|
+
if (!force && existsSync(validatedPath)) {
|
|
598
|
+
assertWorktreeCleanForRemoval(validatedPath);
|
|
599
|
+
}
|
|
91
600
|
|
|
92
|
-
let branch =
|
|
601
|
+
let branch =
|
|
602
|
+
registered?.branch ||
|
|
603
|
+
(deleteBranch && expectedBranchOid && options.branchName
|
|
604
|
+
? options.branchName
|
|
605
|
+
: null);
|
|
93
606
|
if (deleteBranch) {
|
|
94
607
|
try {
|
|
95
|
-
branch =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
608
|
+
branch =
|
|
609
|
+
branch ||
|
|
610
|
+
gitExecArgs(
|
|
611
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
612
|
+
validatedPath,
|
|
613
|
+
).trim();
|
|
99
614
|
} catch (_e) {
|
|
100
615
|
// Can't determine branch, skip branch deletion.
|
|
101
616
|
}
|
|
102
617
|
}
|
|
618
|
+
let removalBranchOid = expectedBranchOid;
|
|
619
|
+
if (branch && (deleteBranch || removalBranchOid != null)) {
|
|
620
|
+
const currentBranchOid = readLocalBranchOid(repoDir, branch);
|
|
621
|
+
if (currentBranchOid == null) {
|
|
622
|
+
if (initiallySettled) return;
|
|
623
|
+
throw new Error(`Failed to verify worktree branch: ${branch}`);
|
|
624
|
+
}
|
|
625
|
+
removalBranchOid = removalBranchOid || currentBranchOid;
|
|
626
|
+
if (currentBranchOid !== removalBranchOid) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`refusing to remove worktree after branch moved: ${branch}`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
103
632
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (existsSync(
|
|
108
|
-
|
|
633
|
+
// Missing + unregistered is the idempotent recovery state after a crash
|
|
634
|
+
// between physical removal and snapshot settlement.
|
|
635
|
+
if (registered || existsSync(validatedPath)) {
|
|
636
|
+
if (!force && existsSync(validatedPath)) {
|
|
637
|
+
assertWorktreeCleanForRemoval(validatedPath);
|
|
638
|
+
}
|
|
639
|
+
if (branch && removalBranchOid) {
|
|
640
|
+
const currentBranchOid = readLocalBranchOid(repoDir, branch);
|
|
641
|
+
if (currentBranchOid == null) {
|
|
642
|
+
throw new Error(`Failed to verify worktree branch: ${branch}`);
|
|
643
|
+
}
|
|
644
|
+
if (currentBranchOid !== removalBranchOid) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
`refusing to remove worktree after branch moved: ${branch}`,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Re-read every path/registry/branch/lock authority as the final operation
|
|
652
|
+
// before deletion. In particular, persisted metadata is not deletion
|
|
653
|
+
// authority if it disappeared from Git's registry, became locked, or
|
|
654
|
+
// stopped identifying the same common repository.
|
|
655
|
+
const currentTarget = validateWorktreeRemovalTarget(
|
|
656
|
+
repoDir,
|
|
657
|
+
validatedPath,
|
|
658
|
+
options.branchName || registered?.branch || null,
|
|
659
|
+
);
|
|
660
|
+
const currentPath = currentTarget.validatedPath;
|
|
661
|
+
if (currentTarget.registered || existsSync(currentPath)) {
|
|
662
|
+
try {
|
|
663
|
+
if (process.platform === "win32") {
|
|
664
|
+
// Git for Windows follows directory junctions during
|
|
665
|
+
// `worktree remove --force`. Node 22's rmSync unlinks reparse points
|
|
666
|
+
// without traversing their targets, so remove the already-canonical,
|
|
667
|
+
// physically-verified checkout and then settle only stale, unlocked
|
|
668
|
+
// registry metadata. Exact-path verification below remains
|
|
669
|
+
// authoritative.
|
|
670
|
+
if (existsSync(currentPath)) {
|
|
671
|
+
rmSync(currentPath, {
|
|
672
|
+
recursive: true,
|
|
673
|
+
force: true,
|
|
674
|
+
maxRetries: 2,
|
|
675
|
+
retryDelay: 100,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
gitExecArgs(["worktree", "prune", "--expire", "now"], repoDir);
|
|
679
|
+
} else {
|
|
680
|
+
gitExecArgs(
|
|
681
|
+
["worktree", "remove", currentPath, ...(force ? ["--force"] : [])],
|
|
682
|
+
repoDir,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
} catch (error) {
|
|
686
|
+
throw new Error(`Failed to remove managed worktree: ${validatedPath}`, {
|
|
687
|
+
cause: error,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const remaining = findRegisteredWorktreeByPath(
|
|
692
|
+
readRegisteredWorktrees(repoDir),
|
|
693
|
+
currentPath,
|
|
694
|
+
);
|
|
695
|
+
if (remaining || existsSync(currentPath)) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`Failed to verify managed worktree removal: ${validatedPath}`,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
109
700
|
}
|
|
110
|
-
gitExec("worktree prune", repoDir);
|
|
111
701
|
}
|
|
112
702
|
|
|
113
703
|
if (
|
|
@@ -118,9 +708,24 @@ export function removeWorktree(repoDir, worktreePath, options = {}) {
|
|
|
118
708
|
!branch.startsWith("master")
|
|
119
709
|
) {
|
|
120
710
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
711
|
+
assertSafeGitRef(branch, "branch name");
|
|
712
|
+
const currentBranchOid = readLocalBranchOid(repoDir, branch);
|
|
713
|
+
if (currentBranchOid == null) return;
|
|
714
|
+
if (removalBranchOid && currentBranchOid !== removalBranchOid) {
|
|
715
|
+
throw new Error(`worktree branch moved during removal: ${branch}`);
|
|
716
|
+
}
|
|
717
|
+
gitExecArgs(["branch", "-D", branch], repoDir);
|
|
718
|
+
} catch (error) {
|
|
719
|
+
try {
|
|
720
|
+
if (readLocalBranchOid(repoDir, branch) == null) return;
|
|
721
|
+
} catch (probeError) {
|
|
722
|
+
throw new Error(`Failed to verify worktree branch: ${branch}`, {
|
|
723
|
+
cause: probeError,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
throw new Error(`Failed to delete worktree branch: ${branch}`, {
|
|
727
|
+
cause: error,
|
|
728
|
+
});
|
|
124
729
|
}
|
|
125
730
|
}
|
|
126
731
|
}
|
|
@@ -129,25 +734,7 @@ export function listWorktrees(repoDir) {
|
|
|
129
734
|
if (!isGitRepo(repoDir)) return [];
|
|
130
735
|
|
|
131
736
|
try {
|
|
132
|
-
|
|
133
|
-
const worktrees = [];
|
|
134
|
-
let current = {};
|
|
135
|
-
|
|
136
|
-
for (const line of output.split("\n")) {
|
|
137
|
-
if (line.startsWith("worktree ")) {
|
|
138
|
-
if (current.path) worktrees.push(current);
|
|
139
|
-
current = { path: line.slice(9) };
|
|
140
|
-
} else if (line.startsWith("HEAD ")) {
|
|
141
|
-
current.head = line.slice(5);
|
|
142
|
-
} else if (line.startsWith("branch ")) {
|
|
143
|
-
current.branch = line.slice(7).replace("refs/heads/", "");
|
|
144
|
-
} else if (line === "bare") {
|
|
145
|
-
current.bare = true;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (current.path) worktrees.push(current);
|
|
149
|
-
|
|
150
|
-
return worktrees;
|
|
737
|
+
return readRegisteredWorktrees(repoDir);
|
|
151
738
|
} catch (_e) {
|
|
152
739
|
return [];
|
|
153
740
|
}
|
|
@@ -157,7 +744,7 @@ export function pruneWorktrees(repoDir) {
|
|
|
157
744
|
if (!isGitRepo(repoDir)) return 0;
|
|
158
745
|
|
|
159
746
|
const before = listWorktrees(repoDir).length;
|
|
160
|
-
|
|
747
|
+
gitExecArgs(["worktree", "prune"], repoDir);
|
|
161
748
|
const after = listWorktrees(repoDir).length;
|
|
162
749
|
return before - after;
|
|
163
750
|
}
|
|
@@ -167,12 +754,8 @@ export async function isolateTask(repoDir, taskId, fn, options = {}) {
|
|
|
167
754
|
// options (large-monorepo): { sparsePaths, symlinkDirectories } forwarded to
|
|
168
755
|
// createWorktree so an isolated task only materializes the packages it needs
|
|
169
756
|
// and reuses approved dep dirs. Empty/absent → full checkout (byte-identical).
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
branchName,
|
|
173
|
-
undefined,
|
|
174
|
-
options || {},
|
|
175
|
-
);
|
|
757
|
+
const created = createWorktree(repoDir, branchName, undefined, options || {});
|
|
758
|
+
const worktreePath = created.path;
|
|
176
759
|
|
|
177
760
|
try {
|
|
178
761
|
const result = await fn(worktreePath);
|
|
@@ -187,8 +770,14 @@ export async function isolateTask(repoDir, taskId, fn, options = {}) {
|
|
|
187
770
|
} finally {
|
|
188
771
|
try {
|
|
189
772
|
const hasCommits = _hasBranchCommits(repoDir, branchName);
|
|
773
|
+
removeManagedDependencyLinks(
|
|
774
|
+
repoDir,
|
|
775
|
+
worktreePath,
|
|
776
|
+
created.symlinkedDirectories || [],
|
|
777
|
+
);
|
|
190
778
|
removeWorktree(repoDir, worktreePath, {
|
|
191
779
|
deleteBranch: !hasCommits,
|
|
780
|
+
force: false,
|
|
192
781
|
});
|
|
193
782
|
} catch (_e) {
|
|
194
783
|
// Best-effort cleanup.
|
|
@@ -290,7 +879,10 @@ export function cleanupAgentWorktrees(repoDir, options = {}) {
|
|
|
290
879
|
if (!decision.safeToRemove) continue; // keep work we cannot safely lose
|
|
291
880
|
}
|
|
292
881
|
try {
|
|
293
|
-
removeWorktree(repoDir, wt.path, {
|
|
882
|
+
removeWorktree(repoDir, wt.path, {
|
|
883
|
+
deleteBranch: true,
|
|
884
|
+
force,
|
|
885
|
+
});
|
|
294
886
|
cleaned++;
|
|
295
887
|
} catch (_e) {
|
|
296
888
|
// Skip if cleanup fails.
|