very-happy-cli 0.2.166 → 0.2.167

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.
Files changed (178) hide show
  1. package/dist/{AcpBackend-20VHOXwb.cjs → AcpBackend-D1IKJGz_.cjs} +3 -3
  2. package/dist/{AcpBackend-tRWk7yi7.mjs → AcpBackend-kFZlee5E.mjs} +3 -3
  3. package/dist/{BasePermissionHandler-COKc4pmt.cjs → BasePermissionHandler-DHxYAK5L.cjs} +1 -1
  4. package/dist/{BasePermissionHandler-CDCLIrZJ.mjs → BasePermissionHandler-DS0kRTNI.mjs} +1 -1
  5. package/dist/{BaseReasoningProcessor-BDVKvooR.mjs → BaseReasoningProcessor-BRtCCt7q.mjs} +1 -1
  6. package/dist/{BaseReasoningProcessor-DAGQdkNp.cjs → BaseReasoningProcessor-CsjVdt_H.cjs} +1 -1
  7. package/dist/{agentHome-2IbIjKCa.mjs → agentHome-Ct-VXFeG.mjs} +4 -4
  8. package/dist/{agentHome-KVWj6nCl.cjs → agentHome-DeAWzhSe.cjs} +4 -4
  9. package/dist/{api-DnNL5fLw.cjs → api-BZYLRDM9.cjs} +88 -10
  10. package/dist/{api-Cmz3XS4P.mjs → api-CReZazeT.mjs} +88 -10
  11. package/dist/{apiMachine-DeoQ8QbZ.cjs → apiMachine-BgMxGPsR.cjs} +50 -15
  12. package/dist/{apiMachine-B6ySCFe0.mjs → apiMachine-CWndf1Vn.mjs} +50 -15
  13. package/dist/{attachmentContent-DPWxHgX4.cjs → attachmentContent-HcJqFQW8.cjs} +2 -2
  14. package/dist/{attachmentContent-CTXapi-_.mjs → attachmentContent-j875mwU7.mjs} +2 -2
  15. package/dist/{attachmentLimits-BfgKV5iE.cjs → attachmentLimits-BEYEEBm8.cjs} +13 -1
  16. package/dist/{attachmentLimits-C1PCCM79.mjs → attachmentLimits-CxG3T18E.mjs} +13 -1
  17. package/dist/{auth-BMaBjRAj.mjs → auth-BIHuc7vY.mjs} +10 -10
  18. package/dist/{auth-pzWIFdOD.mjs → auth-CFS9Lfgv.mjs} +5 -5
  19. package/dist/{auth-D4-eQz85.cjs → auth-Myr2Q42x.cjs} +10 -10
  20. package/dist/{auth-ietHtvwu.cjs → auth-VzWpbo5P.cjs} +5 -5
  21. package/dist/{auto-DOWTQDPa.cjs → auto-Bf6Dr7_o.cjs} +14 -14
  22. package/dist/{auto-DL75Dx3I.mjs → auto-Df9-zmuN.mjs} +14 -14
  23. package/dist/{bootstrap-XE8A7Ut2.mjs → bootstrap-BSOxYpaP.mjs} +2 -2
  24. package/dist/{bootstrap-BpXIc2wn.cjs → bootstrap-Dj8WBiR2.cjs} +2 -2
  25. package/dist/{browser-DDFi1T8C.mjs → browser-BCFIHOvo.mjs} +1 -1
  26. package/dist/{browser-CcG8GLRV.cjs → browser-BoCV40yC.cjs} +1 -1
  27. package/dist/{claudeLocal-DXBXwIOG.cjs → claudeLocal-CGsq7Kul.cjs} +5 -5
  28. package/dist/{claudeLocal-DmM5Oyhv.mjs → claudeLocal-D5-eA9SC.mjs} +5 -5
  29. package/dist/{claudeLocalLauncher-Cs4eUC3P.mjs → claudeLocalLauncher-C9Cc438l.mjs} +10 -10
  30. package/dist/{claudeLocalLauncher-C41Y6Kp4.cjs → claudeLocalLauncher-Cf9QeYDT.cjs} +10 -10
  31. package/dist/{claudeSessionFork-Cs0I3eGI.cjs → claudeSessionFork-Byd8zD1U.cjs} +1 -1
  32. package/dist/{claudeSessionFork-BdLnc2Hv.mjs → claudeSessionFork-CMzAva8G.mjs} +1 -1
  33. package/dist/{client-JISJx2f-.cjs → client-BKnKRLSt.cjs} +3 -3
  34. package/dist/{client-GjTnsq6F.mjs → client-C7FYMer0.mjs} +2 -2
  35. package/dist/{client-DJ0kX_8M.cjs → client-D0T5-6Ds.cjs} +2 -2
  36. package/dist/{client-CbK33y_s.mjs → client-Dsfy_SAb.mjs} +3 -3
  37. package/dist/codex/happyMcpStdioBridge.cjs +14 -14
  38. package/dist/codex/happyMcpStdioBridge.mjs +14 -14
  39. package/dist/{codexAppServerClient-CldXEojt.mjs → codexAppServerClient-Dd_4TxHv.mjs} +4 -4
  40. package/dist/{codexAppServerClient-BTjn2ZCZ.cjs → codexAppServerClient-TiW9OBk4.cjs} +4 -4
  41. package/dist/{codexCommand-DjMU-Ijf.mjs → codexCommand-BZSr4MKI.mjs} +137 -56
  42. package/dist/{codexCommand-BpD0N1F7.cjs → codexCommand-Bxryqh90.cjs} +137 -56
  43. package/dist/{config-DjSJKatx.mjs → config-C_Pj7yxV.mjs} +3 -3
  44. package/dist/{config-DuLL0Ppe.cjs → config-DXPash-r.cjs} +3 -3
  45. package/dist/{configuration-B0cb3xA3.cjs → configuration-C3piBtxY.cjs} +1 -1
  46. package/dist/{configuration-D2v3puap.mjs → configuration-WspKd2C2.mjs} +2 -2
  47. package/dist/{connect-B7JWEzyg.cjs → connect-BN-IBElA.cjs} +13 -13
  48. package/dist/{connect-CnNNyM9I.mjs → connect-DGPZs5px.mjs} +13 -13
  49. package/dist/{context-DjfqLWzE.mjs → context-Cr6a3NTg.mjs} +1 -1
  50. package/dist/{context-BFUpYT39.cjs → context-Jrq4-eCZ.cjs} +1 -1
  51. package/dist/{controlClient-CmnHGcyl.cjs → controlClient-BI6Fw3fQ.cjs} +18 -3
  52. package/dist/{controlClient-BMUJfUeF.mjs → controlClient-DTheOlvC.mjs} +18 -4
  53. package/dist/{createSessionMetadata-DU0bCg2m.mjs → createSessionMetadata-BEZ4DIDA.mjs} +57 -5
  54. package/dist/{createSessionMetadata-DbfRBhRu.cjs → createSessionMetadata-zQSz3hgk.cjs} +56 -3
  55. package/dist/defaultPermissionMode-Cif_RHGb.mjs +191 -0
  56. package/dist/defaultPermissionMode-DNKNzk7l.cjs +197 -0
  57. package/dist/{deterministicJson-D-WPUrZb.mjs → deterministicJson-BMMifViN.mjs} +8 -8
  58. package/dist/{deterministicJson-Dp3TVyap.cjs → deterministicJson-C7O2ulKY.cjs} +8 -8
  59. package/dist/{doctor-CkJmdPbi.cjs → doctor-BykBaMTr.cjs} +6 -6
  60. package/dist/{doctor-BlXXyQ1L.mjs → doctor-D1KKyPzB.mjs} +6 -6
  61. package/dist/{downloadTarget-CiQqRIc-.cjs → downloadTarget-CWTb2-EM.cjs} +6 -6
  62. package/dist/{downloadTarget-DjSwMHun.mjs → downloadTarget-Cp48jrIP.mjs} +6 -6
  63. package/dist/{ensureDaemonRunning-CAQag1zE.mjs → ensureDaemonRunning-Ba5fYoxb.mjs} +5 -5
  64. package/dist/{ensureDaemonRunning-C_aIOhgG.cjs → ensureDaemonRunning-CCxX7MoP.cjs} +5 -5
  65. package/dist/{handleResumeCommand-Ci7wacT-.mjs → handleResumeCommand-BTUGlNjB.mjs} +5 -5
  66. package/dist/{handleResumeCommand-6KzBGDnl.cjs → handleResumeCommand-yPdDQHqz.cjs} +5 -5
  67. package/dist/{index-CaFCAlX8.cjs → index-BjhKqDa0.cjs} +3 -3
  68. package/dist/{index-HQsigW_J.mjs → index-C3_83bqS.mjs} +3 -3
  69. package/dist/{index-WGCT6SSV.mjs → index-CE2GgPbr.mjs} +34 -34
  70. package/dist/{index-DQplgo0W.mjs → index-D8m8Ripb.mjs} +4 -4
  71. package/dist/{index-DBmNwMX9.cjs → index-DifM62wX.cjs} +33 -33
  72. package/dist/{index-DEKIgZUo.cjs → index-PNMQPKhz.cjs} +4 -4
  73. package/dist/index.cjs +2 -2
  74. package/dist/index.mjs +2 -2
  75. package/dist/{install-oJUvsFu9.mjs → install-C0-QFn9s.mjs} +3 -3
  76. package/dist/{install-CejEiRGW.cjs → install-CqZVeze8.cjs} +3 -3
  77. package/dist/lib.cjs +11 -11
  78. package/dist/lib.d.cts +61 -0
  79. package/dist/lib.d.mts +61 -0
  80. package/dist/lib.mjs +11 -11
  81. package/dist/{lidState-BfximpjX.mjs → lidState-B2ZxdREe.mjs} +1 -1
  82. package/dist/{lidState-D8BmnJV1.cjs → lidState-Bc2K9H9l.cjs} +1 -1
  83. package/dist/{localHappyAgentAuth-Ct1aXwqd.mjs → localHappyAgentAuth-B5XPEMwk.mjs} +1 -1
  84. package/dist/{localHappyAgentAuth-d7B6S_o7.cjs → localHappyAgentAuth-T5GOvRha.cjs} +1 -1
  85. package/dist/{logger-JTcJU-uL.cjs → logger-B_MWPbZz.cjs} +2 -2
  86. package/dist/{logger-BpQcwNnt.mjs → logger-CbBv3q_b.mjs} +2 -2
  87. package/dist/{machineMetadata-BBg_Njw3.cjs → machineMetadata-BafS4JY8.cjs} +4 -3
  88. package/dist/{machineMetadata-CBdxABeI.mjs → machineMetadata-DHg238U4.mjs} +4 -3
  89. package/dist/{main-C16fOGgn.cjs → main-DNrA0qBy.cjs} +45 -45
  90. package/dist/{main-B5b1gA3K.mjs → main-DXAK0n7x.mjs} +45 -45
  91. package/dist/{manager-BQ4flvQZ.mjs → manager-BEsI4qt3.mjs} +9 -3
  92. package/dist/{manager-Cly8xVIR.cjs → manager-HrEj442o.cjs} +8 -2
  93. package/dist/{mcp-BnNiBBax.cjs → mcp-AvPOhDXy.cjs} +14 -14
  94. package/dist/{mcp-DGNWgVdv.mjs → mcp-BotAjCdD.mjs} +14 -14
  95. package/dist/mirrorProtocol-BdcN6QlN.cjs +1270 -0
  96. package/dist/mirrorProtocol-BwHm8odn.mjs +1247 -0
  97. package/dist/{package-C1XvUQFy.cjs → package-C9ZP9bpX.cjs} +7 -7
  98. package/dist/{package-BqwrPxL0.mjs → package-DU1DxhH9.mjs} +7 -7
  99. package/dist/{peerTools-DPvbcs8V.cjs → peerTools-Dkbu7gag.cjs} +4 -4
  100. package/dist/{peerTools-CeCe-3lk.mjs → peerTools-os2DVjA0.mjs} +4 -4
  101. package/dist/{persistence-2Xp_o6x6.cjs → persistence-BYGl0QYw.cjs} +147 -25
  102. package/dist/{persistence-B9LmhAE2.mjs → persistence-ON0zpHfb.mjs} +123 -12
  103. package/dist/{piRunnerArgs-7FaTyNOq.mjs → piRunnerArgs-CYsnT9sV.mjs} +1 -1
  104. package/dist/{piRunnerArgs-DI29dDNo.cjs → piRunnerArgs-DpbvstUX.cjs} +1 -1
  105. package/dist/{piRuntime-R8XVChxK.cjs → piRuntime-CDnILlCd.cjs} +1 -1
  106. package/dist/{piRuntime-0qEK5AqE.mjs → piRuntime-CmEid-wu.mjs} +1 -1
  107. package/dist/{piTerminal-zcQKc8Sp.mjs → piTerminal-0eNlXSlL.mjs} +16 -16
  108. package/dist/{piTerminal-Ccj0KNPr.cjs → piTerminal-B57TgMON.cjs} +16 -16
  109. package/dist/{rateLimitWindows-C2Jk3Ih7.cjs → rateLimitWindows-CusGIqlq.cjs} +1 -1
  110. package/dist/{rateLimitWindows-Dhfhbtqu.mjs → rateLimitWindows-vZ9Bo7aT.mjs} +1 -1
  111. package/dist/{registerCommonHandlers-DdfGsFbM.mjs → registerCommonHandlers-B0PVff2U.mjs} +2 -2
  112. package/dist/{registerCommonHandlers-Aha_KfM3.cjs → registerCommonHandlers-DVYB3iVn.cjs} +2 -2
  113. package/dist/{registerKillSessionHandler-DboQlGcH.mjs → registerKillSessionHandler-BJMHJHXe.mjs} +14 -9
  114. package/dist/{registerKillSessionHandler-CjitOpUA.cjs → registerKillSessionHandler-BwXGKtg_.cjs} +14 -9
  115. package/dist/{remoteSessionClient-B2DtWuNP.mjs → remoteSessionClient-BzPXSC21.mjs} +5 -5
  116. package/dist/{remoteSessionClient-BwI8rpQw.cjs → remoteSessionClient-PyUAIhwK.cjs} +5 -5
  117. package/dist/{reportProgressTool-DnAD8wOa.mjs → reportProgressTool-BvB13EwC.mjs} +1 -1
  118. package/dist/{reportProgressTool-B-Rcy0s3.cjs → reportProgressTool-D4KB_4cP.cjs} +1 -1
  119. package/dist/{run-Bh66Xs40.cjs → run-BC8jzq_h.cjs} +323 -37
  120. package/dist/{run-DPEwJpQR.mjs → run-O2M8LsWf.mjs} +322 -36
  121. package/dist/{runClaude-C-lJdAKD.cjs → runClaude-C7jPa8VP.cjs} +87 -25
  122. package/dist/{runClaude-DLr_nBzw.mjs → runClaude-DGkr5E7s.mjs} +87 -25
  123. package/dist/{runClaudeDeps-vWWLod7J.cjs → runClaudeDeps-5kzncqME.cjs} +100 -31
  124. package/dist/{runClaudeDeps-C7JV7nXG.mjs → runClaudeDeps-CX_TxS78.mjs} +105 -36
  125. package/dist/{runGemini-o-CUSayc.cjs → runGemini-BfYA4Uqc.cjs} +32 -32
  126. package/dist/{runGemini-tH6sLw-G.mjs → runGemini-CwmGBH3L.mjs} +32 -32
  127. package/dist/{runOpenClaw-C5DZvVhn.mjs → runOpenClaw-BYAPYFyL.mjs} +16 -16
  128. package/dist/{runOpenClaw-D24f35l0.cjs → runOpenClaw-C72jut7v.cjs} +16 -16
  129. package/dist/{sandbox-TJZJ6ngw.cjs → sandbox-Bqkp8IT7.cjs} +4 -4
  130. package/dist/{sandbox-BaL1aslA.mjs → sandbox-DlVnK1Vi.mjs} +4 -4
  131. package/dist/{selfCheck-Dugi0Nk_.cjs → selfCheck-B4lLZplk.cjs} +34 -34
  132. package/dist/selfCheck-DYTGBdIO.mjs +84 -0
  133. package/dist/{send-DDcJ3ayi.mjs → send-B5bWW9Tl.mjs} +8 -8
  134. package/dist/{send-CkWOSYmU.cjs → send-BCwS6n_s.cjs} +8 -8
  135. package/dist/{server-CUjRvQSd.mjs → server-CEJfRQfj.mjs} +4 -4
  136. package/dist/{server-7qFQ8J2u.cjs → server-CFWZehnC.cjs} +6 -6
  137. package/dist/{sessionLock-DqVyHFAQ.mjs → sessionLock-AwOYsKT7.mjs} +12 -4
  138. package/dist/{sessionLock-DfhNICyS.cjs → sessionLock-LBWNjpHf.cjs} +12 -3
  139. package/dist/{sessionModeFile-CIznKD7O.cjs → sessionModeFile-CyPuZRIU.cjs} +1 -1
  140. package/dist/{sessionModeFile-M4OpiyrF.mjs → sessionModeFile-fvFn1JVG.mjs} +1 -1
  141. package/dist/{sessionOps-BnaEaOKs.mjs → sessionOps-DZRH4-hV.mjs} +4 -4
  142. package/dist/{sessionOps-DOyy4gkY.cjs → sessionOps-DzbZI3Eq.cjs} +4 -4
  143. package/dist/{sessionScanner-CcuOn9uU.mjs → sessionScanner-Q_Ei2flB.mjs} +3 -3
  144. package/dist/{sessionScanner-Cu1yKlj4.cjs → sessionScanner-i-cgwFVw.cjs} +3 -3
  145. package/dist/{sessions-C9O91rL1.mjs → sessions-DAmjMAZN.mjs} +9 -9
  146. package/dist/{sessions-BQgk5Kw8.cjs → sessions-DYK5hD4T.cjs} +9 -9
  147. package/dist/{setupOfflineReconnection-Byy8aeKT.mjs → setupOfflineReconnection-BfDBxNdo.mjs} +2 -2
  148. package/dist/{setupOfflineReconnection-DFmQ8s6I.cjs → setupOfflineReconnection-C2LO3hSP.cjs} +2 -2
  149. package/dist/{spawn-EZ2yrtWd.mjs → spawn-BmjG5j5Y.mjs} +13 -13
  150. package/dist/{spawn-BS7LPmSm.cjs → spawn-GK_sXE8h.cjs} +13 -13
  151. package/dist/{spawnHappyCLI-BAa49m0q.mjs → spawnHappyCLI-B2h0P4b5.mjs} +1 -1
  152. package/dist/{spawnHappyCLI-DyaldIe5.cjs → spawnHappyCLI-J4iEhfyZ.cjs} +1 -1
  153. package/dist/{specialCommands-CCWtwIGZ.mjs → specialCommands-DisVxagO.mjs} +3 -3
  154. package/dist/{specialCommands-DouFaJ_0.cjs → specialCommands-DmJMKspE.cjs} +3 -3
  155. package/dist/{startFileWatcher-Dfs4whzr.mjs → startFileWatcher-CeK1k5gt.mjs} +1 -1
  156. package/dist/{startFileWatcher-DQbbypJv.cjs → startFileWatcher-xKo8AoYl.cjs} +1 -1
  157. package/dist/{sync-x_lI_3j_.cjs → sync-Bvc-vh6C.cjs} +1 -1
  158. package/dist/{sync-BLpH13Kb.mjs → sync-CCwYpJMS.mjs} +1 -1
  159. package/dist/{systemPrompt-BanyDihy.mjs → systemPrompt-BNIkP3tR.mjs} +1 -1
  160. package/dist/{systemPrompt-4erwILTB.cjs → systemPrompt-DwOddbuG.cjs} +1 -1
  161. package/dist/{teams-D6BOT3lR.mjs → teams-DRrLBcys.mjs} +6 -6
  162. package/dist/{teams-DzetlGWC.cjs → teams-GHMCRZOw.cjs} +6 -6
  163. package/dist/{time-T0YmeOhS.cjs → time-BXvtjhe9.cjs} +1 -1
  164. package/dist/{time-CfDKj6Bx.mjs → time-PG5Dfwdv.mjs} +1 -1
  165. package/dist/{titleGenerator-CoMxqKfP.cjs → titleGenerator-B07bZA0V.cjs} +2 -2
  166. package/dist/{titleGenerator-ChImQ4q-.mjs → titleGenerator-CIzfpfZW.mjs} +1 -1
  167. package/dist/{todo-CZZebKZg.cjs → todo-B0aeoNu3.cjs} +5 -5
  168. package/dist/{todo-SVtYxGON.mjs → todo-CfL7SBFc.mjs} +5 -5
  169. package/dist/{tools-3X2z3YDe.mjs → tools-D91mq5Ib.mjs} +1 -1
  170. package/dist/{tools-IJ0TAcJs.cjs → tools-DJIANHSh.cjs} +1 -1
  171. package/dist/{uninstall-CypD-kr1.mjs → uninstall-5xz3Xvhf.mjs} +3 -3
  172. package/dist/{uninstall-CqqCWj7w.cjs → uninstall-AtzOfdrj.cjs} +3 -3
  173. package/package.json +7 -7
  174. package/dist/defaultPermissionMode-BnxJPVHT.mjs +0 -4
  175. package/dist/defaultPermissionMode-CxmTXUQR.cjs +0 -7
  176. package/dist/mirrorProtocol-BEGk2--0.cjs +0 -266
  177. package/dist/mirrorProtocol-h5WHpdhh.mjs +0 -248
  178. package/dist/selfCheck-DPi4lqme.mjs +0 -84
@@ -0,0 +1,1270 @@
1
+ 'use strict';
2
+
3
+ var node_child_process = require('node:child_process');
4
+ var node_crypto = require('node:crypto');
5
+ var node_fs = require('node:fs');
6
+ var path = require('node:path');
7
+ var promises = require('node:fs/promises');
8
+ var configuration = require('./configuration-C3piBtxY.cjs');
9
+ var index = require('./index-C9hI8a10.cjs');
10
+ var _package = require('./package-C9ZP9bpX.cjs');
11
+
12
+ function summarizeSpawnSessionForLog(value) {
13
+ const input = value && typeof value === "object" ? value : {};
14
+ const environmentVariables = input.environmentVariables && typeof input.environmentVariables === "object" ? input.environmentVariables : {};
15
+ return {
16
+ ...typeof input.agent === "string" ? { agent: input.agent } : {},
17
+ ...typeof input.variant === "string" ? { variant: input.variant } : {},
18
+ ...typeof input.sessionId === "string" ? { sessionId: input.sessionId } : {},
19
+ hasDirectory: typeof input.directory === "string" && input.directory.length > 0,
20
+ hasToken: typeof input.token === "string" && input.token.length > 0,
21
+ environmentVariableNames: Object.keys(environmentVariables).sort(),
22
+ resumesClaude: typeof input.resumeClaudeSessionId === "string" && input.resumeClaudeSessionId.length > 0,
23
+ resumesCodex: typeof input.resumeCodexThreadId === "string" && input.resumeCodexThreadId.length > 0,
24
+ importsClaude: typeof input.importedFromClaudeSessionId === "string" && input.importedFromClaudeSessionId.length > 0,
25
+ forceNew: input.forceNew === true,
26
+ ...typeof input.permissionMode === "string" ? { permissionMode: input.permissionMode } : {}
27
+ };
28
+ }
29
+
30
+ const WORKTREE_CAPABILITY_VERSION = 1;
31
+ const obj = (value) => value && typeof value === "object" ? value : {};
32
+ function registerWorktreeHandlers(rpc, service) {
33
+ rpc.registerHandler("worktree-info", async (params) => service.info(obj(params).path));
34
+ rpc.registerHandler("worktree-create", async (params) => {
35
+ const p = obj(params);
36
+ return service.create({ path: p.path, branch: p.branch, baseRef: p.baseRef, location: p.location });
37
+ });
38
+ rpc.registerHandler("worktree-setup-status", async (params) => service.status(obj(params).path));
39
+ rpc.registerHandler("worktree-setup-approve", async (params) => service.approve(obj(params).path, obj(params).hash));
40
+ rpc.registerHandler("worktree-setup-decline", async (params) => service.decline(obj(params).path));
41
+ rpc.registerHandler("worktree-setup-log", async (params) => service.log(obj(params).path, obj(params).offset));
42
+ rpc.registerHandler("worktree-inspect", async (params) => service.inspect(obj(params).path));
43
+ rpc.registerHandler("worktree-remove", async (params) => service.remove(obj(params).path, obj(params).discardChanges, obj(params).expectedFingerprint));
44
+ }
45
+
46
+ const CONFIG_RELATIVE_PATH = ".veryhappy/worktree.json";
47
+ const CONFIG_MAX_BYTES = 64 * 1024;
48
+ const SCRIPT_MAX_BYTES = 32 * 1024;
49
+ const MAX_COPY_PATTERNS = 64;
50
+ const MAX_LINK_ENTRIES = 16;
51
+ const DEFAULT_SETUP_TIMEOUT_MINUTES = 10;
52
+ const DEFAULT_WEB_COPY = [".env*"];
53
+ const EMPTY_CONFIG = { copy: [], link: [], setup: null, setupWindows: null, setupTimeoutMinutes: DEFAULT_SETUP_TIMEOUT_MINUTES };
54
+ function isSafeRelativePattern(pattern) {
55
+ if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > 512) return false;
56
+ if (pattern.includes("\0") || pattern.includes("\\")) return false;
57
+ if (pattern.startsWith("/") || /^[a-zA-Z]:/.test(pattern) || pattern.startsWith("~")) return false;
58
+ const segments = pattern.split("/");
59
+ return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
60
+ }
61
+ function globSegmentToRegExp(segment) {
62
+ let source = "^";
63
+ for (const ch of segment) {
64
+ if (ch === "*") source += "[^/]*";
65
+ else if (ch === "?") source += "[^/]";
66
+ else source += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
67
+ }
68
+ return new RegExp(`${source}$`);
69
+ }
70
+ function hasGlob(segment) {
71
+ return segment.includes("*") || segment.includes("?");
72
+ }
73
+ function clampTimeout(value) {
74
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_SETUP_TIMEOUT_MINUTES;
75
+ return Math.min(60, Math.max(1, Math.round(value)));
76
+ }
77
+ function readScript(value, field, errors) {
78
+ if (value === void 0 || value === null) return null;
79
+ if (typeof value !== "string") {
80
+ errors.push(`${field} must be a string`);
81
+ return null;
82
+ }
83
+ if (Buffer.byteLength(value, "utf8") > SCRIPT_MAX_BYTES) {
84
+ errors.push(`${field} exceeds ${SCRIPT_MAX_BYTES} bytes`);
85
+ return null;
86
+ }
87
+ return value.trim() ? value : null;
88
+ }
89
+ function readPatterns(value, field, max, errors) {
90
+ if (value === void 0) return [];
91
+ if (!Array.isArray(value)) {
92
+ errors.push(`${field} must be an array`);
93
+ return [];
94
+ }
95
+ const out = [];
96
+ for (const entry of value.slice(0, max)) {
97
+ if (typeof entry === "string" && isSafeRelativePattern(entry)) out.push(entry);
98
+ else errors.push(`${field}: rejected ${JSON.stringify(entry).slice(0, 80)}`);
99
+ }
100
+ if (value.length > max) errors.push(`${field}: only the first ${max} entries are used`);
101
+ return out;
102
+ }
103
+ function parseWorktreeConfig(text, origin) {
104
+ if (text === null) {
105
+ return origin === "web" ? { source: "default", config: { ...EMPTY_CONFIG, copy: [...DEFAULT_WEB_COPY] }, errors: [] } : { source: "none", config: { ...EMPTY_CONFIG }, errors: [] };
106
+ }
107
+ let raw;
108
+ try {
109
+ raw = JSON.parse(text);
110
+ } catch {
111
+ return { source: "invalid", config: { ...EMPTY_CONFIG }, errors: [`${CONFIG_RELATIVE_PATH} is not valid JSON`] };
112
+ }
113
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
114
+ return { source: "invalid", config: { ...EMPTY_CONFIG }, errors: [`${CONFIG_RELATIVE_PATH} must be a JSON object`] };
115
+ }
116
+ const obj = raw;
117
+ const errors = [];
118
+ const config = {
119
+ copy: readPatterns(obj.copy, "copy", MAX_COPY_PATTERNS, errors),
120
+ link: readPatterns(obj.link, "link", MAX_LINK_ENTRIES, errors).filter((entry) => {
121
+ if (hasGlob(entry)) {
122
+ errors.push(`link: globs are not allowed (${entry})`);
123
+ return false;
124
+ }
125
+ return true;
126
+ }),
127
+ setup: readScript(obj.setup, "setup", errors),
128
+ setupWindows: readScript(obj.setupWindows, "setupWindows", errors),
129
+ setupTimeoutMinutes: clampTimeout(obj.setupTimeoutMinutes)
130
+ };
131
+ return { source: "file", config, errors };
132
+ }
133
+ function isShebangScript(script) {
134
+ return script.trimStart().startsWith("#!");
135
+ }
136
+ function selectSetupScript(config, platform) {
137
+ if (platform === "win32") {
138
+ const script2 = config.setupWindows ?? config.setup;
139
+ if (!script2) return { kind: "none" };
140
+ if (isShebangScript(script2)) {
141
+ return { kind: "unsupported", script: script2, message: 'A "#!" setup script needs a POSIX shell; native Windows runs setup through cmd.exe. Add "setupWindows" with cmd syntax.' };
142
+ }
143
+ return { kind: "script", script: script2, runner: "cmd" };
144
+ }
145
+ const script = config.setup;
146
+ if (!script) return { kind: "none" };
147
+ return { kind: "script", script, runner: isShebangScript(script) ? "shebang" : "posix-sh" };
148
+ }
149
+ function setupScriptHash(runner, script) {
150
+ return node_crypto.createHash("sha256").update(`${runner}\0${script}`, "utf8").digest("hex");
151
+ }
152
+ function describeRunner(runner) {
153
+ if (runner === "cmd") return "cmd.exe /d /c setup.cmd";
154
+ if (runner === "shebang") return "./setup (its own #! interpreter)";
155
+ return "/bin/sh -e setup.sh";
156
+ }
157
+
158
+ const COPY_MAX_FILE_BYTES = 10 * 1024 * 1024;
159
+ const COPY_MAX_TOTAL_BYTES = 50 * 1024 * 1024;
160
+ const COPY_MAX_FILES = 200;
161
+ const GLOB_MAX_MATCHES = 1e3;
162
+ function isInside(parent, child) {
163
+ const rel = path.relative(parent, child);
164
+ return rel === "" || !!rel && !rel.startsWith("..") && !path.isAbsolute(rel) && rel.split(path.sep)[0] !== "..";
165
+ }
166
+ async function expandPattern(root, pattern) {
167
+ if (!isSafeRelativePattern(pattern)) return [];
168
+ let current = [""];
169
+ for (const segment of pattern.split("/")) {
170
+ const next = [];
171
+ for (const base of current) {
172
+ if (!hasGlob(segment)) {
173
+ next.push(base ? `${base}/${segment}` : segment);
174
+ continue;
175
+ }
176
+ const regex = globSegmentToRegExp(segment);
177
+ let names = [];
178
+ try {
179
+ names = await promises.readdir(path.join(root, ...base.split("/").filter(Boolean)));
180
+ } catch {
181
+ continue;
182
+ }
183
+ for (const name of names.sort()) {
184
+ if (name.startsWith(".") && !segment.startsWith(".")) continue;
185
+ if (name === ".git") continue;
186
+ if (regex.test(name)) next.push(base ? `${base}/${name}` : name);
187
+ if (next.length >= GLOB_MAX_MATCHES) break;
188
+ }
189
+ }
190
+ current = next;
191
+ if (current.length === 0) break;
192
+ }
193
+ return current.filter(Boolean);
194
+ }
195
+ async function realOrNull(path) {
196
+ try {
197
+ return await promises.realpath(path);
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+ async function realExistingAncestor(path$1) {
203
+ let current = path$1;
204
+ for (let i = 0; i < 64; i++) {
205
+ const real = await realOrNull(current);
206
+ if (real) return real;
207
+ const parent = path.dirname(current);
208
+ if (parent === current) return null;
209
+ current = parent;
210
+ }
211
+ return null;
212
+ }
213
+ async function copyIntoWorktree(sourceRoot, worktree, patterns) {
214
+ const copied = [];
215
+ const skipped = [];
216
+ const realSource = await promises.realpath(sourceRoot);
217
+ const realWorktree = await promises.realpath(worktree);
218
+ let total = 0;
219
+ const seen = /* @__PURE__ */ new Set();
220
+ for (const pattern of patterns) {
221
+ if (!isSafeRelativePattern(pattern)) {
222
+ skipped.push({ path: pattern, reason: "unsafe-pattern" });
223
+ continue;
224
+ }
225
+ for (const rel of await expandPattern(realSource, pattern)) {
226
+ if (seen.has(rel)) continue;
227
+ seen.add(rel);
228
+ if (rel.split("/").includes(".git")) {
229
+ skipped.push({ path: rel, reason: "git-internal" });
230
+ continue;
231
+ }
232
+ const source = path.join(realSource, ...rel.split("/"));
233
+ let info;
234
+ try {
235
+ info = await promises.lstat(source);
236
+ } catch {
237
+ continue;
238
+ }
239
+ const resolved = await realOrNull(source);
240
+ if (!resolved || !isInside(realSource, resolved)) {
241
+ skipped.push({ path: rel, reason: "outside-repo" });
242
+ continue;
243
+ }
244
+ const target = info.isSymbolicLink() ? await promises.stat(resolved).catch(() => null) : info;
245
+ if (!target || !target.isFile()) {
246
+ skipped.push({ path: rel, reason: "not-a-file" });
247
+ continue;
248
+ }
249
+ if (target.size > COPY_MAX_FILE_BYTES) {
250
+ skipped.push({ path: rel, reason: "too-large" });
251
+ continue;
252
+ }
253
+ if (copied.length >= COPY_MAX_FILES) {
254
+ skipped.push({ path: rel, reason: "file-limit" });
255
+ continue;
256
+ }
257
+ if (total + target.size > COPY_MAX_TOTAL_BYTES) {
258
+ skipped.push({ path: rel, reason: "total-limit" });
259
+ continue;
260
+ }
261
+ const destination = path.join(realWorktree, ...rel.split("/"));
262
+ if (await promises.lstat(destination).then(() => true, () => false)) {
263
+ skipped.push({ path: rel, reason: "exists" });
264
+ continue;
265
+ }
266
+ const parentReal = await realExistingAncestor(path.dirname(destination));
267
+ if (!parentReal || !isInside(realWorktree, parentReal)) {
268
+ skipped.push({ path: rel, reason: "outside-worktree" });
269
+ continue;
270
+ }
271
+ try {
272
+ await promises.mkdir(path.dirname(destination), { recursive: true });
273
+ const madeReal = await promises.realpath(path.dirname(destination));
274
+ if (!isInside(realWorktree, madeReal)) {
275
+ skipped.push({ path: rel, reason: "outside-worktree" });
276
+ continue;
277
+ }
278
+ await promises.copyFile(resolved, destination, node_fs.constants.COPYFILE_EXCL);
279
+ copied.push(rel);
280
+ total += target.size;
281
+ } catch (error) {
282
+ skipped.push({ path: rel, reason: `copy-failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 200) });
283
+ }
284
+ }
285
+ }
286
+ return { copied, skipped };
287
+ }
288
+ function linkTypeCandidates(platform) {
289
+ return platform === "win32" ? ["junction", "dir"] : ["dir"];
290
+ }
291
+ async function linkIntoWorktree(sourceRoot, worktree, entries, platform = process.platform) {
292
+ const linked = [];
293
+ const skipped = [];
294
+ const realSource = await promises.realpath(sourceRoot);
295
+ const realWorktree = await promises.realpath(worktree);
296
+ for (const rel of entries) {
297
+ if (!isSafeRelativePattern(rel) || hasGlob(rel)) {
298
+ skipped.push({ path: rel, reason: "unsafe-pattern" });
299
+ continue;
300
+ }
301
+ if (rel.split("/").includes(".git")) {
302
+ skipped.push({ path: rel, reason: "git-internal" });
303
+ continue;
304
+ }
305
+ try {
306
+ const source = path.join(realSource, ...rel.split("/"));
307
+ let info;
308
+ try {
309
+ info = await promises.lstat(source);
310
+ } catch {
311
+ skipped.push({ path: rel, reason: "missing" });
312
+ continue;
313
+ }
314
+ if (info.isSymbolicLink() || !info.isDirectory()) {
315
+ skipped.push({ path: rel, reason: "not-a-directory" });
316
+ continue;
317
+ }
318
+ const resolved = await realOrNull(source);
319
+ if (!resolved || !isInside(realSource, resolved) || resolved === realSource) {
320
+ skipped.push({ path: rel, reason: "outside-repo" });
321
+ continue;
322
+ }
323
+ const destination = path.join(realWorktree, ...rel.split("/"));
324
+ if (await promises.lstat(destination).then(() => true, () => false)) {
325
+ skipped.push({ path: rel, reason: "exists" });
326
+ continue;
327
+ }
328
+ const parentReal = await realExistingAncestor(path.dirname(destination));
329
+ if (!parentReal || !isInside(realWorktree, parentReal)) {
330
+ skipped.push({ path: rel, reason: "outside-worktree" });
331
+ continue;
332
+ }
333
+ await promises.mkdir(path.dirname(destination), { recursive: true });
334
+ const madeReal = await promises.realpath(path.dirname(destination));
335
+ if (!isInside(realWorktree, madeReal)) {
336
+ skipped.push({ path: rel, reason: "outside-worktree" });
337
+ continue;
338
+ }
339
+ let done = false;
340
+ let lastError = "";
341
+ for (const type of linkTypeCandidates(platform)) {
342
+ try {
343
+ await promises.symlink(resolved, destination, type);
344
+ done = true;
345
+ break;
346
+ } catch (error) {
347
+ lastError = error instanceof Error ? error.message : String(error);
348
+ }
349
+ }
350
+ if (done) linked.push(rel);
351
+ else skipped.push({ path: rel, reason: `link-failed: ${lastError}`.slice(0, 200) });
352
+ } catch (error) {
353
+ skipped.push({ path: rel, reason: `link-failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 200) });
354
+ }
355
+ }
356
+ return { linked, skipped };
357
+ }
358
+
359
+ const SETUP_KILL_GRACE_MS = 1e4;
360
+ const SETUP_LOG_MAX_BYTES = 20 * 1024 * 1024;
361
+ const LOG_CHECK_MS = 500;
362
+ function buildWindowsRunnerScript(script) {
363
+ const header = "@echo off\r\nsetlocal EnableExtensions DisableDelayedExpansion\r\n";
364
+ let out = header;
365
+ for (const raw of script.split("\n")) {
366
+ const line = raw.replace(/\r$/, "").trim();
367
+ if (!line) {
368
+ out += "\r\n";
369
+ continue;
370
+ }
371
+ out += `call ${line}\r
372
+ if errorlevel 1 exit /b %errorlevel%\r
373
+ `;
374
+ }
375
+ return out;
376
+ }
377
+ function sanitizeSetupEnv(base, paths) {
378
+ const env = {};
379
+ for (const [key, value] of Object.entries(base)) {
380
+ if (value === void 0) continue;
381
+ if (/^(HAPPY|VH)_/i.test(key)) continue;
382
+ env[key] = value;
383
+ }
384
+ env.VERY_HAPPY_WORKTREE_PATH = paths.worktree;
385
+ env.VERY_HAPPY_ROOT_PATH = paths.root;
386
+ return env;
387
+ }
388
+ function prepareRunner(runDir, runner, script) {
389
+ if (runner === "cmd") {
390
+ const scriptPath2 = path.join(runDir, "setup.cmd");
391
+ node_fs.writeFileSync(scriptPath2, buildWindowsRunnerScript(script), { mode: 384 });
392
+ return { command: process.env.ComSpec || "cmd.exe", args: ["/d", "/c", scriptPath2], scriptPath: scriptPath2 };
393
+ }
394
+ const scriptPath = path.join(runDir, runner === "shebang" ? "setup" : "setup.sh");
395
+ node_fs.writeFileSync(scriptPath, script, { mode: 448 });
396
+ if (runner === "shebang") {
397
+ node_fs.chmodSync(scriptPath, 448);
398
+ return { command: scriptPath, args: [], scriptPath };
399
+ }
400
+ return { command: "/bin/sh", args: ["-e", scriptPath], scriptPath };
401
+ }
402
+ function killSetupTree(child, signal, platform = process.platform) {
403
+ if (!child.pid || child.exitCode !== null || child.signalCode !== null) return;
404
+ try {
405
+ if (platform === "win32") {
406
+ node_child_process.spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }).on("error", () => void 0);
407
+ } else {
408
+ process.kill(-child.pid, signal);
409
+ }
410
+ } catch {
411
+ try {
412
+ child.kill(signal);
413
+ } catch {
414
+ }
415
+ }
416
+ }
417
+ function startSetupRun(opts) {
418
+ const maxLogBytes = opts.maxLogBytes ?? SETUP_LOG_MAX_BYTES;
419
+ const logFd = node_fs.openSync(opts.logPath, "a", 384);
420
+ let child;
421
+ let interrupted = false;
422
+ let timedOut = false;
423
+ let truncated = false;
424
+ let settle = () => void 0;
425
+ const done = new Promise((resolve) => {
426
+ settle = resolve;
427
+ });
428
+ let settled = false;
429
+ const finish = (outcome) => {
430
+ if (settled) return;
431
+ settled = true;
432
+ clearTimeout(timer);
433
+ clearInterval(sizeWatch);
434
+ try {
435
+ node_fs.closeSync(logFd);
436
+ } catch {
437
+ }
438
+ settle(outcome);
439
+ };
440
+ const terminate = () => {
441
+ killSetupTree(child, "SIGTERM");
442
+ const hard = setTimeout(() => killSetupTree(child, "SIGKILL"), SETUP_KILL_GRACE_MS);
443
+ hard.unref();
444
+ child.once("exit", () => clearTimeout(hard));
445
+ };
446
+ const timer = setTimeout(() => {
447
+ timedOut = true;
448
+ terminate();
449
+ }, opts.timeoutMs);
450
+ timer.unref();
451
+ const sizeWatch = setInterval(() => {
452
+ if (truncated) return;
453
+ let size = 0;
454
+ try {
455
+ size = node_fs.fstatSync(logFd).size;
456
+ } catch {
457
+ return;
458
+ }
459
+ if (size > maxLogBytes) {
460
+ truncated = true;
461
+ terminate();
462
+ }
463
+ }, LOG_CHECK_MS);
464
+ sizeWatch.unref();
465
+ try {
466
+ child = (opts.spawnImpl ?? node_child_process.spawn)(opts.launch.command, opts.launch.args, {
467
+ cwd: opts.cwd,
468
+ env: opts.env,
469
+ shell: false,
470
+ windowsHide: true,
471
+ detached: process.platform !== "win32",
472
+ stdio: ["ignore", logFd, logFd]
473
+ });
474
+ } catch (error) {
475
+ finish({ state: "failed", exitCode: null, message: error instanceof Error ? error.message : String(error) });
476
+ return { pid: void 0, done, stop: () => void 0 };
477
+ }
478
+ child.on("error", (error) => finish({ state: "failed", exitCode: null, message: error.message }));
479
+ child.on("exit", (code, signal) => {
480
+ if (process.platform !== "win32" && child.pid) {
481
+ try {
482
+ process.kill(-child.pid, "SIGKILL");
483
+ } catch {
484
+ }
485
+ }
486
+ if (truncated) finish({ state: "failed", exitCode: code, truncated: true, message: `Output exceeded ${Math.round(maxLogBytes / 1024 / 1024)} MB; stopped` });
487
+ else if (timedOut) finish({ state: "timed-out", exitCode: code, message: `Timed out after ${Math.round(opts.timeoutMs / 6e4)} min` });
488
+ else if (interrupted) finish({ state: "interrupted", exitCode: code, message: "Stopped" });
489
+ else if (code === 0) finish({ state: "succeeded", exitCode: 0 });
490
+ else finish({ state: "failed", exitCode: code, ...signal ? { message: `Killed by ${signal}` } : {} });
491
+ });
492
+ return {
493
+ pid: child.pid,
494
+ done,
495
+ stop: () => {
496
+ if (settled) return;
497
+ interrupted = true;
498
+ terminate();
499
+ }
500
+ };
501
+ }
502
+
503
+ const MAX_WORKTREES = 500;
504
+ const MAX_RUN_DIRS = 100;
505
+ class WorktreeStore {
506
+ dir;
507
+ constructor(home) {
508
+ this.dir = path.join(home, "worktree-setup");
509
+ }
510
+ file(name) {
511
+ return path.join(this.dir, name);
512
+ }
513
+ read(name, fallback) {
514
+ const path = this.file(name);
515
+ if (!node_fs.existsSync(path)) return fallback;
516
+ try {
517
+ return JSON.parse(node_fs.readFileSync(path, "utf8"));
518
+ } catch {
519
+ return fallback;
520
+ }
521
+ }
522
+ write(name, value) {
523
+ configuration.ensurePrivateDirectorySync(this.dir);
524
+ const path = this.file(name);
525
+ const tmp = `${path}.${process.pid}.tmp`;
526
+ configuration.writePrivateFileSync(tmp, JSON.stringify(value, null, 2));
527
+ node_fs.renameSync(tmp, path);
528
+ }
529
+ runDir(runId) {
530
+ const dir = path.join(this.dir, "runs", runId);
531
+ configuration.ensurePrivateDirectorySync(dir);
532
+ return dir;
533
+ }
534
+ logPath(runId) {
535
+ return path.join(this.dir, "runs", runId, "output.log");
536
+ }
537
+ /** Keep the newest MAX_RUN_DIRS runs (run ids start with a base36 timestamp, so names sort by age). */
538
+ pruneRuns(keep = /* @__PURE__ */ new Set(), max = MAX_RUN_DIRS) {
539
+ const root = path.join(this.dir, "runs");
540
+ let names;
541
+ try {
542
+ names = node_fs.readdirSync(root).sort();
543
+ } catch {
544
+ return 0;
545
+ }
546
+ let removed = 0;
547
+ for (const name of names.slice(0, Math.max(0, names.length - max))) {
548
+ if (keep.has(name)) continue;
549
+ try {
550
+ node_fs.rmSync(path.join(root, name), { recursive: true, force: true });
551
+ removed++;
552
+ } catch {
553
+ }
554
+ }
555
+ return removed;
556
+ }
557
+ // ── approvals ──
558
+ approvals() {
559
+ const data = this.read("approvals.json", {});
560
+ return Array.isArray(data.approvals) ? data.approvals : [];
561
+ }
562
+ isApproved(machineId, repoRoot, hash) {
563
+ return this.approvals().some((a) => a.machineId === machineId && a.repoRoot === repoRoot && a.hash === hash);
564
+ }
565
+ /** True when this repo has SOME approval but not for `hash` (the script changed). */
566
+ hasOtherApproval(machineId, repoRoot, hash) {
567
+ return this.approvals().some((a) => a.machineId === machineId && a.repoRoot === repoRoot && a.hash !== hash);
568
+ }
569
+ approve(entry) {
570
+ const rest = this.approvals().filter((a) => !(a.machineId === entry.machineId && a.repoRoot === entry.repoRoot && a.hash === entry.hash));
571
+ const next = [...rest, entry].sort((a, b) => a.approvedAt - b.approvedAt).slice(-500);
572
+ this.write("approvals.json", { version: 1, approvals: next });
573
+ }
574
+ // ── worktree registry ──
575
+ worktrees() {
576
+ const data = this.read("worktrees.json", {});
577
+ return data.worktrees && typeof data.worktrees === "object" ? data.worktrees : {};
578
+ }
579
+ get(worktree) {
580
+ return this.worktrees()[worktree];
581
+ }
582
+ put(worktree, record) {
583
+ const all = { ...this.worktrees(), [worktree]: record };
584
+ const keys = Object.keys(all).sort((a, b) => all[a].createdAt - all[b].createdAt);
585
+ for (const key of keys.slice(0, Math.max(0, keys.length - MAX_WORKTREES))) delete all[key];
586
+ this.write("worktrees.json", { version: 1, worktrees: all });
587
+ }
588
+ updateSetup(worktree, patch) {
589
+ const record = this.get(worktree);
590
+ if (!record) return void 0;
591
+ const next = { ...record, setup: { ...record.setup, ...patch } };
592
+ this.put(worktree, next);
593
+ return next;
594
+ }
595
+ remove(worktree) {
596
+ const all = this.worktrees();
597
+ if (!(worktree in all)) return;
598
+ delete all[worktree];
599
+ this.write("worktrees.json", { version: 1, worktrees: all });
600
+ }
601
+ /**
602
+ * Daemon start: any `running` record belongs to a previous daemon process
603
+ * (this one has not started anything yet) — it was cut off. Never re-run.
604
+ */
605
+ markStaleRunsInterrupted(now = Date.now()) {
606
+ const all = this.worktrees();
607
+ const cut = [];
608
+ for (const [worktree, record] of Object.entries(all)) {
609
+ if (record.setup.state === "running") {
610
+ cut.push({ worktree, pid: record.setup.pid, runId: record.setup.runId });
611
+ record.setup = { ...record.setup, state: "interrupted", endedAt: now, message: "Daemon stopped during setup" };
612
+ }
613
+ }
614
+ if (cut.length) this.write("worktrees.json", { version: 1, worktrees: all });
615
+ return cut;
616
+ }
617
+ }
618
+
619
+ const GIT_CREATE_TIMEOUT_MS = 25e3;
620
+ const LOG_CHUNK_BYTES = 64 * 1024;
621
+ const MAX_BRANCHES = 200;
622
+ const MAX_DIRTY_LISTED = 50;
623
+ class WorktreeError extends Error {
624
+ constructor(code, message) {
625
+ super(message ? `${code}: ${message}` : code);
626
+ this.code = code;
627
+ }
628
+ }
629
+ function runGit(cwd, args, timeoutMs = 15e3) {
630
+ return new Promise((resolvePromise) => {
631
+ node_child_process.execFile("git", ["-C", cwd, ...args], { timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024, windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }, (error, stdout, stderr) => {
632
+ const code = error ? typeof error.code === "number" ? error.code : 1 : 0;
633
+ resolvePromise({ code, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") || (error && code !== 0 ? error.message : "") });
634
+ });
635
+ });
636
+ }
637
+ function defaultWorktreeLocation(repoRoot, branch) {
638
+ const slug = branch.replace(/[\\/]+/g, "-").replace(/[^A-Za-z0-9._-]/g, "-").replace(/^-+|-+$/g, "") || "worktree";
639
+ return path.join(path.dirname(repoRoot), `${path.basename(repoRoot)}.worktrees`, slug);
640
+ }
641
+ function parsePorcelainZ(output) {
642
+ const parts = output.split("\0");
643
+ const paths = [];
644
+ for (let i = 0; i < parts.length; i++) {
645
+ const entry = parts[i];
646
+ if (entry.length < 4) continue;
647
+ const status = entry.slice(0, 2);
648
+ paths.push(entry.slice(3).replace(/\/$/, ""));
649
+ if (status[0] === "R" || status[0] === "C") i++;
650
+ }
651
+ return paths;
652
+ }
653
+ function userDirtyPaths(dirty, managed) {
654
+ const own = /* @__PURE__ */ new Set([...managed.copied, ...managed.linked]);
655
+ return dirty.filter((path) => !own.has(path));
656
+ }
657
+ function real(path$1) {
658
+ try {
659
+ return node_fs.realpathSync(path$1);
660
+ } catch {
661
+ return path.resolve(path$1);
662
+ }
663
+ }
664
+ function requireAbsolute(path$1, field = "path") {
665
+ if (typeof path$1 !== "string" || !path$1 || !path.isAbsolute(path$1) || path$1.includes("\0")) throw new WorktreeError("invalid-path", `${field} must be an absolute path`);
666
+ return path$1;
667
+ }
668
+ class WorktreeService {
669
+ constructor(opts) {
670
+ this.opts = opts;
671
+ this.store = new WorktreeStore(opts.home);
672
+ this.platform = opts.platform ?? process.platform;
673
+ this.debug = opts.log ?? (() => void 0);
674
+ try {
675
+ this.killOrphanedRuns(this.store.markStaleRunsInterrupted());
676
+ } catch (error) {
677
+ this.debug("[worktree] could not reconcile stale setup runs", error);
678
+ }
679
+ }
680
+ store;
681
+ runs = /* @__PURE__ */ new Map();
682
+ platform;
683
+ debug;
684
+ /**
685
+ * A daemon that died hard (SIGKILL/crash) left its setup process groups
686
+ * running. Kill a group ONLY when its leader's command line still names
687
+ * that run's own script path (a reused pid belongs to something else).
688
+ * A group whose leader already exited cannot be verified and is left alone.
689
+ */
690
+ killOrphanedRuns(cut) {
691
+ if (this.platform === "win32") return;
692
+ const probe = this.opts.processCommand ?? defaultProcessCommand;
693
+ for (const run of cut) {
694
+ if (!run.pid || !run.runId) continue;
695
+ const command = probe(run.pid);
696
+ if (!command || !command.includes(path.join(this.store.dir, "runs", run.runId))) continue;
697
+ try {
698
+ process.kill(-run.pid, "SIGKILL");
699
+ this.debug(`[worktree] killed orphaned setup group ${run.pid}`);
700
+ } catch (error) {
701
+ this.debug("[worktree] orphan kill failed", error);
702
+ }
703
+ }
704
+ }
705
+ async toplevel(path) {
706
+ const result = await runGit(path, ["rev-parse", "--show-toplevel"]);
707
+ if (result.code !== 0) throw new WorktreeError("not-a-git-repo", path);
708
+ return real(result.stdout.trim());
709
+ }
710
+ /** Read `.veryhappy/worktree.json` from the source checkout (never followed outside it). */
711
+ readConfig(root, origin) {
712
+ const path$1 = path.join(root, ...CONFIG_RELATIVE_PATH.split("/"));
713
+ let info;
714
+ try {
715
+ info = node_fs.lstatSync(path$1);
716
+ } catch {
717
+ return parseWorktreeConfig(null, origin);
718
+ }
719
+ try {
720
+ const resolved = node_fs.realpathSync(path$1);
721
+ if (!isInside(real(root), resolved)) return { ...parseWorktreeConfig("{}", origin), source: "invalid", errors: [`${CONFIG_RELATIVE_PATH} points outside the repository`] };
722
+ const size = info.isSymbolicLink() ? node_fs.statSync(resolved).size : info.size;
723
+ if (size > CONFIG_MAX_BYTES) return { ...parseWorktreeConfig("{}", origin), source: "invalid", errors: [`${CONFIG_RELATIVE_PATH} exceeds ${CONFIG_MAX_BYTES} bytes`] };
724
+ return parseWorktreeConfig(node_fs.readFileSync(resolved, "utf8"), origin);
725
+ } catch (error) {
726
+ return { ...parseWorktreeConfig("{}", origin), source: "invalid", errors: [`${CONFIG_RELATIVE_PATH}: ${error instanceof Error ? error.message : String(error)}`] };
727
+ }
728
+ }
729
+ setupFor(root, origin) {
730
+ const resolved = this.readConfig(root, origin);
731
+ const selection = selectSetupScript(resolved.config, this.platform);
732
+ const hash = selection.kind === "script" ? setupScriptHash(selection.runner, selection.script) : null;
733
+ return { resolved, selection, hash };
734
+ }
735
+ async info(path$1) {
736
+ const root = await this.toplevel(requireAbsolute(path$1));
737
+ const [head, refs] = await Promise.all([
738
+ runGit(root, ["symbolic-ref", "--short", "-q", "HEAD"]),
739
+ runGit(root, ["for-each-ref", "--sort=-committerdate", `--count=${MAX_BRANCHES}`, "--format=%(refname:short)", "refs/heads"])
740
+ ]);
741
+ const { resolved, selection, hash } = this.setupFor(root, "web");
742
+ return {
743
+ repoRoot: root,
744
+ currentBranch: head.code === 0 ? head.stdout.trim() || null : null,
745
+ branches: refs.stdout.split("\n").map((line) => line.trim()).filter(Boolean),
746
+ defaultParent: path.join(path.dirname(root), `${path.basename(root)}.worktrees`),
747
+ config: {
748
+ source: resolved.source,
749
+ copy: resolved.config.copy,
750
+ link: resolved.config.link,
751
+ hasSetup: selection.kind !== "none",
752
+ setupApproved: hash ? this.store.isApproved(this.opts.machineId, root, hash) : false,
753
+ ...resolved.errors.length ? { error: resolved.errors.join("; ") } : {}
754
+ }
755
+ };
756
+ }
757
+ async create(params) {
758
+ const root = await this.toplevel(requireAbsolute(params.path));
759
+ const branch = typeof params.branch === "string" ? params.branch.trim() : "";
760
+ if (!branch || branch.length > 200 || branch.startsWith("-")) throw new WorktreeError("invalid-branch");
761
+ const refCheck = await runGit(root, ["check-ref-format", "--branch", branch]);
762
+ if (refCheck.code !== 0 || refCheck.stdout.trim() !== branch) throw new WorktreeError("invalid-branch", branch);
763
+ const baseRef = typeof params.baseRef === "string" && params.baseRef.trim() ? params.baseRef.trim() : "HEAD";
764
+ if (baseRef.startsWith("-")) throw new WorktreeError("invalid-base", baseRef);
765
+ const location = typeof params.location === "string" && params.location.trim() ? requireAbsolute(params.location.trim(), "location") : defaultWorktreeLocation(root, branch);
766
+ const existing = node_fs.existsSync(location) ? this.store.get(real(location)) : void 0;
767
+ if (existing && existing.origin === "web" && existing.repoRoot === root && existing.branch === branch) {
768
+ return { worktreePath: real(location), branch, baseRef: existing.baseRef ?? baseRef, init: summarize(existing), reused: true };
769
+ }
770
+ if ((await runGit(root, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`])).code === 0) throw new WorktreeError("branch-exists", branch);
771
+ const base = await runGit(root, ["rev-parse", "--verify", "--quiet", `${baseRef}^{commit}`]);
772
+ if (base.code !== 0) throw new WorktreeError("invalid-base", baseRef);
773
+ if (node_fs.existsSync(location)) throw new WorktreeError("location-exists", location);
774
+ let ancestor = path.dirname(location);
775
+ while (!node_fs.existsSync(ancestor) && path.dirname(ancestor) !== ancestor) ancestor = path.dirname(ancestor);
776
+ if (isInside(root, real(ancestor)) || isInside(root, path.resolve(location))) throw new WorktreeError("location-inside-repo", location);
777
+ node_fs.mkdirSync(path.dirname(location), { recursive: true });
778
+ const added = await runGit(root, ["worktree", "add", "-b", branch, location, base.stdout.trim()], GIT_CREATE_TIMEOUT_MS);
779
+ if (added.code !== 0) {
780
+ await this.cleanupFailedAdd(root, location, branch);
781
+ throw new WorktreeError("git-worktree-add-failed", added.stderr.trim().slice(0, 500));
782
+ }
783
+ const worktreePath = real(location);
784
+ const record = await this.initialize({ source: root, worktree: worktreePath, branch, baseRef, origin: "web" });
785
+ return { worktreePath, branch, baseRef, init: summarize(record) };
786
+ }
787
+ /** Remove a half-made worktree + the branch this call created (it did not exist before). */
788
+ async cleanupFailedAdd(root, location, branch) {
789
+ if (node_fs.existsSync(location)) await runGit(root, ["worktree", "remove", "--force", location]);
790
+ await runGit(root, ["worktree", "prune"]);
791
+ if ((await runGit(root, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`])).code === 0) {
792
+ await runGit(root, ["branch", "-D", branch]);
793
+ }
794
+ }
795
+ /**
796
+ * Copy → link → setup decision. Returns after materialization; an approved
797
+ * script keeps running in the background. Never throws for config problems.
798
+ */
799
+ async initialize(input) {
800
+ const source = real(input.source);
801
+ const worktree = real(input.worktree);
802
+ const record = {
803
+ repoRoot: source,
804
+ branch: input.branch,
805
+ baseRef: input.baseRef,
806
+ origin: input.origin,
807
+ createdAt: Date.now(),
808
+ copied: [],
809
+ linked: [],
810
+ skipped: [],
811
+ setup: { state: "none" }
812
+ };
813
+ this.store.put(worktree, record);
814
+ const { resolved, selection, hash } = this.setupFor(source, input.origin);
815
+ const errors = [];
816
+ const step = async (label, run, fallback) => {
817
+ try {
818
+ return await run();
819
+ } catch (error) {
820
+ errors.push(`${label}: ${error instanceof Error ? error.message : String(error)}`.slice(0, 300));
821
+ return fallback;
822
+ }
823
+ };
824
+ const copy = resolved.config.copy.length ? await step("copy", () => copyIntoWorktree(source, worktree, resolved.config.copy), { copied: [], skipped: [] }) : { copied: [], skipped: [] };
825
+ const link = resolved.config.link.length ? await step("link", () => linkIntoWorktree(source, worktree, resolved.config.link, this.platform), { linked: [], skipped: [] }) : { linked: [], skipped: [] };
826
+ Object.assign(record, {
827
+ copied: copy.copied,
828
+ linked: link.linked,
829
+ skipped: [...copy.skipped, ...link.skipped],
830
+ ...resolved.errors.length ? { configError: resolved.errors.join("; ") } : {},
831
+ ...errors.length ? { initError: errors.join("; ") } : {},
832
+ setup: selection.kind === "none" ? { state: "none" } : selection.kind === "unsupported" ? { state: "unsupported", message: selection.message } : { state: "needs-approval", hash, runner: selection.runner }
833
+ });
834
+ this.store.put(worktree, record);
835
+ if (selection.kind === "script" && hash && this.store.isApproved(this.opts.machineId, source, hash)) {
836
+ try {
837
+ return this.startRun(worktree, record, selection.script, selection.runner, hash, resolved.config.setupTimeoutMinutes);
838
+ } catch (error) {
839
+ return this.store.updateSetup(worktree, { state: "failed", message: error instanceof Error ? error.message : String(error) }) ?? record;
840
+ }
841
+ }
842
+ return record;
843
+ }
844
+ startRun(worktree, record, script, runner, hash, timeoutMinutes) {
845
+ const runId = `${Date.now().toString(36)}-${node_crypto.randomUUID().slice(0, 8)}`;
846
+ const active = new Set(Object.values(this.store.worktrees()).filter((r) => r.setup.state === "running" && r.setup.runId).map((r) => r.setup.runId));
847
+ try {
848
+ this.store.pruneRuns(active);
849
+ } catch (error) {
850
+ this.debug("[worktree] run pruning failed", error);
851
+ }
852
+ const runDir = this.store.runDir(runId);
853
+ const launch = prepareRunner(runDir, runner, script);
854
+ const handle = startSetupRun({
855
+ launch,
856
+ cwd: worktree,
857
+ env: sanitizeSetupEnv(this.opts.env ?? process.env, { worktree, root: record.repoRoot }),
858
+ logPath: this.store.logPath(runId),
859
+ timeoutMs: timeoutMinutes * 6e4
860
+ });
861
+ this.runs.set(worktree, handle);
862
+ const next = this.store.updateSetup(worktree, { state: "running", hash, runner, runId, pid: handle.pid, startedAt: Date.now(), endedAt: void 0, exitCode: void 0, message: void 0 });
863
+ void handle.done.then((outcome) => {
864
+ this.runs.delete(worktree);
865
+ this.store.updateSetup(worktree, { state: outcome.state, exitCode: outcome.exitCode, endedAt: Date.now(), message: outcome.message, ...outcome.truncated ? { truncated: true } : {} });
866
+ }).catch((error) => this.debug("[worktree] setup outcome not recorded", error));
867
+ return next;
868
+ }
869
+ status(path$1) {
870
+ const worktree = real(requireAbsolute(path$1));
871
+ const record = this.store.get(worktree);
872
+ if (!record) return { tracked: false };
873
+ const base = { tracked: true, worktreePath: worktree, origin: record.origin, branch: record.branch, repoRoot: record.repoRoot, copied: record.copied, linked: record.linked, skipped: record.skipped, ...record.configError ? { configError: record.configError } : {}, ...record.initError ? { initError: record.initError } : {}, setup: record.setup };
874
+ if (record.setup.state !== "needs-approval" && record.setup.state !== "declined") return base;
875
+ const { resolved, selection, hash } = this.setupFor(record.repoRoot, record.origin);
876
+ if (selection.kind !== "script" || !hash) return base;
877
+ return {
878
+ ...base,
879
+ pending: {
880
+ script: selection.script,
881
+ hash,
882
+ runner: selection.runner,
883
+ runnerLabel: describeRunner(selection.runner),
884
+ cwd: worktree,
885
+ repoRoot: record.repoRoot,
886
+ configPath: path.join(record.repoRoot, ...CONFIG_RELATIVE_PATH.split("/")),
887
+ timeoutMinutes: resolved.config.setupTimeoutMinutes,
888
+ changedSinceApproval: this.store.hasOtherApproval(this.opts.machineId, record.repoRoot, hash)
889
+ }
890
+ };
891
+ }
892
+ approve(path, hash) {
893
+ const worktree = real(requireAbsolute(path));
894
+ const record = this.store.get(worktree);
895
+ if (!record) throw new WorktreeError("not-tracked");
896
+ if (record.setup.state !== "needs-approval" && record.setup.state !== "declined") throw new WorktreeError("not-awaiting-approval", record.setup.state);
897
+ const { resolved, selection, hash: current2 } = this.setupFor(record.repoRoot, record.origin);
898
+ if (selection.kind !== "script" || !current2) throw new WorktreeError("no-setup-script");
899
+ if (typeof hash !== "string" || hash !== current2) throw new WorktreeError("script-changed");
900
+ this.store.approve({ machineId: this.opts.machineId, repoRoot: record.repoRoot, hash: current2, runner: selection.runner, approvedAt: Date.now() });
901
+ this.startRun(worktree, record, selection.script, selection.runner, current2, resolved.config.setupTimeoutMinutes);
902
+ return this.status(worktree);
903
+ }
904
+ decline(path) {
905
+ const worktree = real(requireAbsolute(path));
906
+ const record = this.store.get(worktree);
907
+ if (!record) throw new WorktreeError("not-tracked");
908
+ if (record.setup.state === "needs-approval") this.store.updateSetup(worktree, { state: "declined", endedAt: Date.now() });
909
+ return this.status(worktree);
910
+ }
911
+ log(path, offset) {
912
+ const worktree = real(requireAbsolute(path));
913
+ const record = this.store.get(worktree);
914
+ const runId = record?.setup.runId;
915
+ if (!record || !runId) return { text: "", offset: 0, size: 0, done: true };
916
+ const logPath = this.store.logPath(runId);
917
+ if (!node_fs.existsSync(logPath)) return { text: "", offset: 0, size: 0, done: record.setup.state !== "running" };
918
+ const size = node_fs.statSync(logPath).size;
919
+ const start = typeof offset === "number" && offset >= 0 && offset <= size ? Math.floor(offset) : 0;
920
+ const length = Math.min(LOG_CHUNK_BYTES, size - start);
921
+ const buffer = Buffer.alloc(length);
922
+ if (length > 0) {
923
+ const fd = node_fs.openSync(logPath, "r");
924
+ try {
925
+ node_fs.readSync(fd, buffer, 0, length, start);
926
+ } finally {
927
+ node_fs.closeSync(fd);
928
+ }
929
+ }
930
+ return { text: buffer.toString("utf8"), offset: start + length, size, done: record.setup.state !== "running" && start + length >= size };
931
+ }
932
+ async inspect(path$1) {
933
+ const worktree = real(requireAbsolute(path$1));
934
+ const record = this.store.get(worktree);
935
+ if (!record) return { removable: false, reason: "not-tracked", dirty: [], dirtyCount: 0, branch: null };
936
+ if (record.origin !== "web") return { removable: false, reason: `owned-by-${record.origin}`, dirty: [], dirtyCount: 0, branch: record.branch };
937
+ const gitFile = path.join(worktree, ".git");
938
+ if (!node_fs.existsSync(gitFile) || !node_fs.lstatSync(gitFile).isFile()) return { removable: false, reason: "not-a-linked-worktree", dirty: [], dirtyCount: 0, branch: record.branch };
939
+ const status = await runGit(worktree, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
940
+ if (status.code !== 0) return { removable: false, reason: "git-status-failed", dirty: [], dirtyCount: 0, branch: record.branch };
941
+ const dirty = userDirtyPaths(parsePorcelainZ(status.stdout), record);
942
+ const head = await runGit(worktree, ["symbolic-ref", "--short", "-q", "HEAD"]);
943
+ return { removable: true, dirty: dirty.slice(0, MAX_DIRTY_LISTED), dirtyCount: dirty.length, dirtyFingerprint: dirtyFingerprint(worktree, dirty), branch: head.stdout.trim() || record.branch };
944
+ }
945
+ /**
946
+ * `expectedFingerprint` is what the user confirmed (inspect's
947
+ * `dirtyFingerprint`). If the changes moved since — new files, or an edit
948
+ * to a listed one — refuse with `changes-changed` and make them re-confirm.
949
+ * Residual window: between this final check and `git worktree remove`
950
+ * (milliseconds) an edit can still be lost; spec §5 accepts it.
951
+ */
952
+ async remove(path$1, discardChanges, expectedFingerprint) {
953
+ const worktree = real(requireAbsolute(path$1));
954
+ const inspection = await this.inspect(worktree);
955
+ if (!inspection.removable) throw new WorktreeError("not-removable", inspection.reason);
956
+ if (inspection.dirtyCount > 0 && discardChanges !== true) throw new WorktreeError("dirty", String(inspection.dirtyCount));
957
+ if (typeof expectedFingerprint === "string" && "dirtyFingerprint" in inspection && expectedFingerprint !== inspection.dirtyFingerprint) {
958
+ throw new WorktreeError("changes-changed", String(inspection.dirtyCount));
959
+ }
960
+ const record = this.store.get(worktree);
961
+ this.runs.get(worktree)?.stop();
962
+ for (const rel of record.linked) {
963
+ const target = path.join(worktree, ...rel.split("/"));
964
+ try {
965
+ const info = node_fs.lstatSync(target);
966
+ if (info.isSymbolicLink()) node_fs.unlinkSync(target);
967
+ else if (this.platform === "win32") node_fs.rmdirSync(target);
968
+ } catch {
969
+ }
970
+ }
971
+ const removed = await runGit(record.repoRoot, ["worktree", "remove", "--force", worktree], GIT_CREATE_TIMEOUT_MS);
972
+ if (removed.code !== 0) throw new WorktreeError("git-worktree-remove-failed", removed.stderr.trim().slice(0, 500));
973
+ this.store.remove(worktree);
974
+ return { removed: true, branch: inspection.branch };
975
+ }
976
+ /** Daemon shutdown: kill every running setup tree (recorded as interrupted). */
977
+ stopAll() {
978
+ for (const handle of this.runs.values()) handle.stop();
979
+ }
980
+ }
981
+ function dirtyFingerprint(worktree, dirty) {
982
+ const hash = node_crypto.createHash("sha256");
983
+ for (const rel of [...dirty].sort()) {
984
+ let meta = "missing";
985
+ try {
986
+ const info = node_fs.lstatSync(path.join(worktree, ...rel.split("/")));
987
+ meta = `${info.size}:${info.mtimeMs}`;
988
+ } catch {
989
+ }
990
+ hash.update(`${rel}\0${meta}
991
+ `);
992
+ }
993
+ return hash.digest("hex").slice(0, 32);
994
+ }
995
+ function defaultProcessCommand(pid) {
996
+ try {
997
+ return node_child_process.execFileSync("ps", ["-o", "command=", "-p", String(pid)], { encoding: "utf8", timeout: 2e3 }).trim() || null;
998
+ } catch {
999
+ return null;
1000
+ }
1001
+ }
1002
+ function summarize(record) {
1003
+ return { copied: record.copied, linked: record.linked, skipped: record.skipped, ...record.configError ? { configError: record.configError } : {}, ...record.initError ? { initError: record.initError } : {}, setup: { state: record.setup.state, ...record.setup.message ? { message: record.setup.message } : {} } };
1004
+ }
1005
+ let current = null;
1006
+ function configureWorktreeService(opts) {
1007
+ if (!current || current.store.dir !== new WorktreeStore(opts.home).dir) current = new WorktreeService(opts);
1008
+ return current;
1009
+ }
1010
+ function currentWorktreeService() {
1011
+ return current;
1012
+ }
1013
+ async function initializeOwnedWorktree(input) {
1014
+ const service = currentWorktreeService();
1015
+ if (!service) return;
1016
+ try {
1017
+ const source = await service.toplevel(input.source);
1018
+ await service.initialize({ ...input, source, baseRef: null });
1019
+ } catch (error) {
1020
+ service.debug(`[worktree] ${input.origin} worktree initialization failed`, error);
1021
+ }
1022
+ }
1023
+
1024
+ function sanitizeGeometry(cols, rows) {
1025
+ const ok = (v) => typeof v === "number" && Number.isInteger(v) && v >= 2 && v <= 1e4;
1026
+ return ok(cols) && ok(rows) ? { cols, rows } : void 0;
1027
+ }
1028
+ function sanitizeTagList(value) {
1029
+ if (!Array.isArray(value)) return void 0;
1030
+ const out = [];
1031
+ for (const tag of value) {
1032
+ if (typeof tag !== "string") continue;
1033
+ const clean = tag.trim();
1034
+ if (clean && !out.includes(clean)) out.push(clean);
1035
+ if (out.length >= 64) break;
1036
+ }
1037
+ return out;
1038
+ }
1039
+ const LIVE_SNAPSHOT_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
1040
+ const LIVE_SNAPSHOT_MAX = 100;
1041
+ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE_SNAPSHOT_MAX) {
1042
+ const out = /* @__PURE__ */ new Map();
1043
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
1044
+ const rows = [];
1045
+ for (const [id, value] of Object.entries(raw)) {
1046
+ if (!id || !value || typeof value !== "object") continue;
1047
+ const { title, cwd, seenAt, tags, manual, attachTmux, cols: rawCols, rows: rawRows } = value;
1048
+ if (typeof seenAt !== "number" || !Number.isFinite(seenAt)) continue;
1049
+ if (now - seenAt >= ttlMs) continue;
1050
+ const tagList = sanitizeTagList(tags);
1051
+ rows.push([id, {
1052
+ title: typeof title === "string" && title.trim() ? title : void 0,
1053
+ cwd: typeof cwd === "string" && cwd ? cwd : void 0,
1054
+ ...tagList !== void 0 ? { tags: tagList } : {},
1055
+ ...manual === true ? { manual: true } : {},
1056
+ ...typeof attachTmux === "string" && attachTmux ? { attachTmux } : {},
1057
+ ...sanitizeGeometry(rawCols, rawRows) ?? {},
1058
+ seenAt
1059
+ }]);
1060
+ }
1061
+ rows.sort((a, b) => b[1].seenAt - a[1].seenAt);
1062
+ for (const [id, info] of rows.slice(0, max)) out.set(id, info);
1063
+ return out;
1064
+ }
1065
+ function serializeLiveSnapshot(map, max = LIVE_SNAPSHOT_MAX) {
1066
+ const rows = [...map.entries()].sort((a, b) => b[1].seenAt - a[1].seenAt).slice(0, max);
1067
+ const out = {};
1068
+ for (const [id, info] of rows) out[id] = info;
1069
+ return out;
1070
+ }
1071
+ function liveSnapshotChanged(prev, next) {
1072
+ if (prev.size !== next.size) return true;
1073
+ for (const [id, info] of next) {
1074
+ const before = prev.get(id);
1075
+ if (!before) return true;
1076
+ if (before.title !== info.title || before.cwd !== info.cwd) return true;
1077
+ if (!!before.manual !== !!info.manual) return true;
1078
+ if ((before.attachTmux ?? "") !== (info.attachTmux ?? "")) return true;
1079
+ if ((before.tags ?? []).join("\0") !== (info.tags ?? []).join("\0")) return true;
1080
+ if (before.cols !== info.cols || before.rows !== info.rows) return true;
1081
+ }
1082
+ return false;
1083
+ }
1084
+ const CLAUDE_SESSION_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
1085
+ function isClaudeSessionId(value) {
1086
+ return typeof value === "string" && CLAUDE_SESSION_ID_RE.test(value);
1087
+ }
1088
+ function pickMirrorForTerminal(sessions, terminalId) {
1089
+ if (!terminalId) return null;
1090
+ let best = null;
1091
+ for (const [id, session] of Object.entries(sessions || {})) {
1092
+ const meta = session?.metadata;
1093
+ if (!meta || meta.terminalId !== terminalId) continue;
1094
+ const savedAt = typeof session.savedAt === "number" ? session.savedAt : 0;
1095
+ if (best && savedAt <= best.savedAt) continue;
1096
+ best = {
1097
+ id,
1098
+ savedAt,
1099
+ claude: isClaudeSessionId(meta.claudeSessionId) ? meta.claudeSessionId : void 0
1100
+ };
1101
+ }
1102
+ return best ? { sessionId: best.id, claudeSessionId: best.claude } : null;
1103
+ }
1104
+
1105
+ const AGENT_CHECK_INTERVAL = 6 * 60 * 60 * 1e3;
1106
+ function stableVersion(value) {
1107
+ const match = value.trim().match(/(?:^|\s|v)(\d+\.\d+\.\d+)([-+][\w.-]+)?(?=\s|$)/);
1108
+ return match && !match[2] ? match[1] : null;
1109
+ }
1110
+ function newerVersion(current, latest) {
1111
+ const a = current.split(".").map(Number), b = latest.split(".").map(Number);
1112
+ for (let i = 0; i < 3; i++) {
1113
+ if (b[i] !== a[i]) return b[i] > a[i];
1114
+ }
1115
+ return false;
1116
+ }
1117
+ const runVersionCommand = (command, args, shell) => new Promise((resolve, reject) => {
1118
+ node_child_process.execFile(
1119
+ command,
1120
+ args,
1121
+ { timeout: 5e3, maxBuffer: 32 * 1024, windowsHide: true, shell },
1122
+ (error, stdout) => error ? reject(error) : resolve(stdout)
1123
+ );
1124
+ });
1125
+ async function probeAgentVersion(command, platform = process.platform, run = runVersionCommand) {
1126
+ if (platform === "win32") {
1127
+ try {
1128
+ await run("where.exe", [command], false);
1129
+ } catch (error) {
1130
+ return { version: null, missing: error.code === 1 };
1131
+ }
1132
+ }
1133
+ try {
1134
+ return { version: stableVersion(await run(command, ["--version"], platform === "win32")), missing: false };
1135
+ } catch (error) {
1136
+ return { version: null, missing: error.code === "ENOENT" };
1137
+ }
1138
+ }
1139
+ async function registryVersion(name) {
1140
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, { signal: AbortSignal.timeout(5e3) });
1141
+ if (!response.ok) throw new Error("Registry unavailable");
1142
+ const data = await response.json();
1143
+ return typeof data.version === "string" ? stableVersion(data.version) : null;
1144
+ }
1145
+ async function detectAgentVersions(deps = { probe: probeAgentVersion, registryVersion }, now = Date.now()) {
1146
+ const agents = await Promise.all(index.AGENT_VERSION_SOURCES.map(async (source) => {
1147
+ const local = source.command ? await deps.probe(source.command).catch(() => ({ version: null, missing: false })) : { version: stableVersion(_package.packageJson.dependencies["@anthropic-ai/claude-agent-sdk"]), missing: false };
1148
+ if (local.missing) return { id: source.id, installed: null, latest: null, status: "not-installed" };
1149
+ const latest = await deps.registryVersion(source.package).catch(() => null);
1150
+ const installed = local.version;
1151
+ const status = !installed || !latest ? "unknown" : newerVersion(installed, latest) ? "update-available" : "current";
1152
+ return { id: source.id, installed, latest, status };
1153
+ }));
1154
+ return { checkedAt: now, daemonPid: process.pid, agents };
1155
+ }
1156
+ function startAgentVersionChecks(publish, onError, detect = detectAgentVersions) {
1157
+ let stopped = false, pending = false;
1158
+ async function refresh() {
1159
+ if (stopped || pending) return;
1160
+ pending = true;
1161
+ try {
1162
+ const snapshot = await detect();
1163
+ if (!stopped) await publish(snapshot);
1164
+ } catch (error) {
1165
+ onError(error);
1166
+ } finally {
1167
+ pending = false;
1168
+ }
1169
+ }
1170
+ void refresh();
1171
+ const timer = setInterval(() => void refresh(), AGENT_CHECK_INTERVAL);
1172
+ timer.unref();
1173
+ return () => {
1174
+ stopped = true;
1175
+ clearInterval(timer);
1176
+ };
1177
+ }
1178
+
1179
+ function parseTerminalHookPayload(body) {
1180
+ if (!body || typeof body !== "object") return null;
1181
+ const raw = body;
1182
+ const eventName = raw.hook_event_name;
1183
+ if (eventName !== "SessionStart" && eventName !== "SessionEnd") return null;
1184
+ const claudeSessionId = typeof raw.session_id === "string" && raw.session_id.length > 0 ? raw.session_id : typeof raw.sessionId === "string" && raw.sessionId.length > 0 ? raw.sessionId : null;
1185
+ if (!claudeSessionId) return null;
1186
+ const terminalId = typeof raw.terminalId === "string" && raw.terminalId.length > 0 ? raw.terminalId : null;
1187
+ if (!terminalId) return null;
1188
+ return {
1189
+ event: eventName,
1190
+ claudeSessionId,
1191
+ terminalId,
1192
+ transcriptPath: typeof raw.transcript_path === "string" && raw.transcript_path.length > 0 ? raw.transcript_path : void 0,
1193
+ cwd: typeof raw.cwd === "string" && raw.cwd.length > 0 ? raw.cwd : void 0,
1194
+ source: typeof raw.source === "string" && raw.source.length > 0 ? raw.source : void 0
1195
+ };
1196
+ }
1197
+ const FRESH_CONVERSATION_SOURCES = /* @__PURE__ */ new Set(["startup", "clear"]);
1198
+ function decideMirrorBinding(event, binding) {
1199
+ if (event.event === "SessionEnd") {
1200
+ if (!binding) return { action: "ignore", reason: "no binding for terminal" };
1201
+ if (binding.claudeSessionId !== event.claudeSessionId) {
1202
+ return { action: "ignore", reason: "stale SessionEnd for a superseded claude session" };
1203
+ }
1204
+ if (binding.status === "ended") return { action: "ignore", reason: "already ended" };
1205
+ return { action: "end" };
1206
+ }
1207
+ if (!binding) return { action: "create" };
1208
+ if (binding.status === "active" && binding.claudeSessionId === event.claudeSessionId) {
1209
+ return { action: "ignore", reason: "duplicate SessionStart for the bound session" };
1210
+ }
1211
+ if (FRESH_CONVERSATION_SOURCES.has(event.source ?? "startup")) {
1212
+ return { action: "create", replaces: binding };
1213
+ }
1214
+ return { action: "continue" };
1215
+ }
1216
+ function mirrorLineKey(message) {
1217
+ if (message.type === "summary") return `summary-${message.leafUuid}`;
1218
+ if (message.type === "user" || message.type === "assistant" || message.type === "system") {
1219
+ return message.uuid;
1220
+ }
1221
+ return null;
1222
+ }
1223
+ function mirrorLocalId(lineKey, envelopeIndex) {
1224
+ return `mirror:${lineKey}:${envelopeIndex}`;
1225
+ }
1226
+ function extractCompleteLines(buf) {
1227
+ const lastNl = buf.lastIndexOf(10);
1228
+ if (lastNl === -1) return { lines: [], consumedBytes: 0 };
1229
+ const text = buf.subarray(0, lastNl).toString("utf-8");
1230
+ const lines = text.split("\n").filter((l) => l.trim().length > 0);
1231
+ return { lines, consumedBytes: lastNl + 1 };
1232
+ }
1233
+ const MIRROR_SEND_MAX_CHARS = 16384;
1234
+ function parseMirrorSendParams(params, isValidTerminalId) {
1235
+ if (!params || typeof params !== "object") return { error: "invalid params" };
1236
+ const { terminalId, text, submit } = params;
1237
+ if (!isValidTerminalId(terminalId)) return { error: "invalid terminalId" };
1238
+ if (typeof text !== "string" || text.length === 0) return { error: "text is required" };
1239
+ if (text.length > MIRROR_SEND_MAX_CHARS) return { error: `text too long (max ${MIRROR_SEND_MAX_CHARS} chars)` };
1240
+ return { terminalId, text, submit: submit !== false };
1241
+ }
1242
+ const MIRROR_BACKFILL_LINES_DEFAULT = 500;
1243
+ function decideBackfill(messages, maxLines) {
1244
+ const cap = Math.max(0, Math.floor(maxLines));
1245
+ if (messages.length <= cap) return { replay: messages, truncated: false };
1246
+ return { replay: messages.slice(messages.length - cap), truncated: true };
1247
+ }
1248
+
1249
+ exports.MIRROR_BACKFILL_LINES_DEFAULT = MIRROR_BACKFILL_LINES_DEFAULT;
1250
+ exports.WORKTREE_CAPABILITY_VERSION = WORKTREE_CAPABILITY_VERSION;
1251
+ exports.configureWorktreeService = configureWorktreeService;
1252
+ exports.currentWorktreeService = currentWorktreeService;
1253
+ exports.decideBackfill = decideBackfill;
1254
+ exports.decideMirrorBinding = decideMirrorBinding;
1255
+ exports.extractCompleteLines = extractCompleteLines;
1256
+ exports.initializeOwnedWorktree = initializeOwnedWorktree;
1257
+ exports.isClaudeSessionId = isClaudeSessionId;
1258
+ exports.liveSnapshotChanged = liveSnapshotChanged;
1259
+ exports.mirrorLineKey = mirrorLineKey;
1260
+ exports.mirrorLocalId = mirrorLocalId;
1261
+ exports.parseMirrorSendParams = parseMirrorSendParams;
1262
+ exports.parseTerminalHookPayload = parseTerminalHookPayload;
1263
+ exports.pickMirrorForTerminal = pickMirrorForTerminal;
1264
+ exports.registerWorktreeHandlers = registerWorktreeHandlers;
1265
+ exports.sanitizeLiveSnapshot = sanitizeLiveSnapshot;
1266
+ exports.sanitizeTagList = sanitizeTagList;
1267
+ exports.serializeLiveSnapshot = serializeLiveSnapshot;
1268
+ exports.stableVersion = stableVersion;
1269
+ exports.startAgentVersionChecks = startAgentVersionChecks;
1270
+ exports.summarizeSpawnSessionForLog = summarizeSpawnSessionForLog;