@kortix/sandbox 0.4.1

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 (246) hide show
  1. package/config/customize.sh +143 -0
  2. package/config/kortix-env-setup.sh +25 -0
  3. package/kortix-master/package.json +22 -0
  4. package/kortix-master/src/config.ts +22 -0
  5. package/kortix-master/src/index.ts +44 -0
  6. package/kortix-master/src/routes/env.ts +65 -0
  7. package/kortix-master/src/routes/proxy.ts +108 -0
  8. package/kortix-master/src/routes/update.ts +185 -0
  9. package/kortix-master/src/services/proxy.ts +43 -0
  10. package/kortix-master/src/services/secret-store.ts +156 -0
  11. package/kortix-master/tsconfig.json +14 -0
  12. package/opencode/agents/kortix-browser.md +142 -0
  13. package/opencode/agents/kortix-build.md +62 -0
  14. package/opencode/agents/kortix-explore.md +66 -0
  15. package/opencode/agents/kortix-image-gen.md +33 -0
  16. package/opencode/agents/kortix-main.md +450 -0
  17. package/opencode/agents/kortix-plan.md +100 -0
  18. package/opencode/agents/kortix-research.md +84 -0
  19. package/opencode/agents/kortix-sheets.md +61 -0
  20. package/opencode/agents/kortix-slides.md +64 -0
  21. package/opencode/agents/kortix-web-dev.md +572 -0
  22. package/opencode/commands/email.md +36 -0
  23. package/opencode/commands/init.md +43 -0
  24. package/opencode/commands/journal.md +44 -0
  25. package/opencode/commands/memory-init.md +81 -0
  26. package/opencode/commands/memory-search.md +50 -0
  27. package/opencode/commands/memory-status.md +56 -0
  28. package/opencode/commands/research.md +36 -0
  29. package/opencode/commands/search.md +38 -0
  30. package/opencode/commands/slides.md +32 -0
  31. package/opencode/commands/spreadsheet.md +30 -0
  32. package/opencode/memory.json +37 -0
  33. package/opencode/ocx.jsonc +10 -0
  34. package/opencode/opencode.jsonc +103 -0
  35. package/opencode/package.json +25 -0
  36. package/opencode/patches/apply.sh +19 -0
  37. package/opencode/patches/opencode-pty-spawn.txt +49 -0
  38. package/opencode/plugin/background-agents.ts.disabled +483 -0
  39. package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
  40. package/opencode/plugin/kdco-primitives/index.ts +26 -0
  41. package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
  42. package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
  43. package/opencode/plugin/kdco-primitives/shell.ts +138 -0
  44. package/opencode/plugin/kdco-primitives/temp.ts +36 -0
  45. package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
  46. package/opencode/plugin/kdco-primitives/types.ts +13 -0
  47. package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
  48. package/opencode/plugin/memory.ts +306 -0
  49. package/opencode/plugin/worktree/state.ts +412 -0
  50. package/opencode/plugin/worktree/terminal.ts +1002 -0
  51. package/opencode/plugin/worktree.ts +861 -0
  52. package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
  53. package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
  54. package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
  55. package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
  56. package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
  57. package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
  58. package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
  59. package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
  60. package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
  61. package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
  62. package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
  63. package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
  64. package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
  65. package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
  66. package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
  67. package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
  68. package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
  69. package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
  70. package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
  71. package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
  72. package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
  73. package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
  74. package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
  75. package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
  76. package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
  77. package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
  78. package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
  79. package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
  80. package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
  81. package/opencode/skills/KORTIX-email/SKILL.md +145 -0
  82. package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
  83. package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
  84. package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
  85. package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
  86. package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
  87. package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
  88. package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
  89. package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
  90. package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
  91. package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
  92. package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
  93. package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
  94. package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
  95. package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
  96. package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
  97. package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
  98. package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
  99. package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
  100. package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
  101. package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
  102. package/opencode/skills/KORTIX-pdf/forms.md +36 -0
  103. package/opencode/skills/KORTIX-pdf/reference.md +105 -0
  104. package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
  105. package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
  106. package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
  107. package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
  108. package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
  109. package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
  110. package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
  111. package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
  112. package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
  113. package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
  114. package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
  115. package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
  116. package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
  117. package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
  118. package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
  119. package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
  120. package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
  121. package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
  122. package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
  123. package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
  124. package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
  125. package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
  126. package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
  127. package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
  128. package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
  129. package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
  130. package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
  131. package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
  132. package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
  133. package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
  134. package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
  135. package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
  136. package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
  137. package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
  138. package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
  139. package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
  140. package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
  141. package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
  142. package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
  143. package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
  144. package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
  145. package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
  146. package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
  147. package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
  148. package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
  149. package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
  150. package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
  151. package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
  152. package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
  153. package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
  154. package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
  155. package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
  156. package/opencode/skills/KORTIX-session-search/Untitled +1 -0
  157. package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
  158. package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
  159. package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
  160. package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
  161. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
  162. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
  163. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
  164. package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
  165. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  166. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  167. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  168. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  169. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  170. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  171. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  172. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  173. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  174. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  175. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  176. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  177. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  178. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  179. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  180. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  181. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  182. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  183. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  184. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  185. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  186. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  187. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  188. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  189. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  190. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  191. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  192. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  193. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  194. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  195. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  196. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
  197. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  198. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  199. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  200. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  201. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  202. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  203. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  204. package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
  205. package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
  206. package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
  207. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
  208. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
  209. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
  210. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
  211. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
  212. package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
  213. package/opencode/tools/image-gen.ts +342 -0
  214. package/opencode/tools/image-search.ts +190 -0
  215. package/opencode/tools/memory-get.ts +168 -0
  216. package/opencode/tools/memory-search.ts +247 -0
  217. package/opencode/tools/presentation-gen.ts +723 -0
  218. package/opencode/tools/scrape-webpage.ts +115 -0
  219. package/opencode/tools/scripts/.python-version +1 -0
  220. package/opencode/tools/scripts/convert_pdf.py +184 -0
  221. package/opencode/tools/scripts/convert_pptx.py +562 -0
  222. package/opencode/tools/scripts/pyproject.toml +11 -0
  223. package/opencode/tools/scripts/uv.lock +287 -0
  224. package/opencode/tools/scripts/validate_slide.py +74 -0
  225. package/opencode/tools/show-user.ts +217 -0
  226. package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
  227. package/opencode/tools/tests/image-gen.test.ts +215 -0
  228. package/opencode/tools/tests/image-search.test.ts +125 -0
  229. package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
  230. package/opencode/tools/tests/presentation-gen.test.ts +389 -0
  231. package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
  232. package/opencode/tools/tests/show-user.test.ts +241 -0
  233. package/opencode/tools/tests/video-gen.test.ts +110 -0
  234. package/opencode/tools/tests/web-search.test.ts +106 -0
  235. package/opencode/tools/video-gen.ts +200 -0
  236. package/opencode/tools/web-search.ts +153 -0
  237. package/opencode/tsconfig.json +29 -0
  238. package/package.json +36 -0
  239. package/patch-agent-browser.js +100 -0
  240. package/postinstall.sh +88 -0
  241. package/services/KORTIX-presentation-viewer/run +37 -0
  242. package/services/agent-browser-viewer/run +48 -0
  243. package/services/kortix-master/run +16 -0
  244. package/services/lss-sync/run +22 -0
  245. package/services/opencode-serve/run +25 -0
  246. package/services/opencode-web/run +21 -0
@@ -0,0 +1,1002 @@
1
+ /**
2
+ * Terminal Module for Worktree Plugin
3
+ *
4
+ * Provides mutex-protected tmux operations and cross-platform terminal spawning.
5
+ * Serializes tmux commands to prevent socket races since tmux server is single-threaded.
6
+ *
7
+ * This module is extracted from worktree.ts to provide a focused, testable
8
+ * interface for terminal operations with proper concurrency control.
9
+ */
10
+
11
+ import * as fs from "node:fs/promises"
12
+ import * as os from "node:os"
13
+ import * as path from "node:path"
14
+ import { z } from "zod"
15
+ import type { OpencodeClient } from "../kdco-primitives"
16
+ import {
17
+ escapeAppleScript,
18
+ escapeBash,
19
+ escapeBatch,
20
+ getTempDir,
21
+ isInsideTmux,
22
+ logWarn,
23
+ Mutex,
24
+ } from "../kdco-primitives"
25
+
26
+ // =============================================================================
27
+ // TEMP SCRIPT HELPER
28
+ // =============================================================================
29
+
30
+ /**
31
+ * Execute a function with a temporary script file that is guaranteed to be cleaned up.
32
+ * Uses try-finally pattern to ensure cleanup even on errors.
33
+ *
34
+ * @param scriptContent - Content to write to the temp script
35
+ * @param fn - Function to execute with the script path
36
+ * @param extension - File extension for the script (default: ".sh")
37
+ * @returns Result of the function execution
38
+ */
39
+ export async function withTempScript<T>(
40
+ scriptContent: string,
41
+ fn: (scriptPath: string) => Promise<T>,
42
+ extension: string = ".sh",
43
+ client?: OpencodeClient,
44
+ ): Promise<T> {
45
+ const scriptPath = path.join(
46
+ getTempDir(),
47
+ `worktree-${Date.now()}-${Math.random().toString(36).slice(2)}${extension}`,
48
+ )
49
+ await Bun.write(scriptPath, scriptContent)
50
+ await fs.chmod(scriptPath, 0o755)
51
+
52
+ try {
53
+ return await fn(scriptPath)
54
+ } finally {
55
+ try {
56
+ if (await Bun.file(scriptPath).exists()) {
57
+ await fs.rm(scriptPath)
58
+ }
59
+ } catch (cleanupError) {
60
+ // Log but don't throw - cleanup is best-effort
61
+ logWarn(client, "worktree", `Failed to cleanup temp script: ${scriptPath}: ${cleanupError}`)
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Wrap a bash script with trap-based self-cleanup.
68
+ * The script deletes itself on ANY exit (success, error, or signal).
69
+ * This eliminates race conditions with detached processes.
70
+ */
71
+ function wrapWithSelfCleanup(script: string): string {
72
+ return `#!/bin/bash
73
+ trap 'rm -f "$0"' EXIT INT TERM
74
+ ${script}`
75
+ }
76
+
77
+ /**
78
+ * Wrap a batch script with self-cleanup.
79
+ * Uses goto trick to delete itself after execution.
80
+ */
81
+ function wrapBatchWithSelfCleanup(script: string): string {
82
+ return `@echo off
83
+ ${script}
84
+ (goto) 2>nul & del "%~f0"`
85
+ }
86
+
87
+ // =============================================================================
88
+ // TYPES
89
+ // =============================================================================
90
+
91
+ /** Terminal type for the current platform */
92
+ export type TerminalType = "tmux" | "macos" | "windows" | "linux-desktop"
93
+
94
+ /** Result of a terminal operation */
95
+ export interface TerminalResult {
96
+ success: boolean
97
+ error?: string
98
+ }
99
+
100
+ // Singleton mutex for all tmux operations in this process
101
+ const tmuxMutex = new Mutex()
102
+
103
+ /** Stabilization delay after spawning tmux windows (ms) */
104
+ const STABILIZATION_DELAY_MS = 150
105
+
106
+ // =============================================================================
107
+ // ENVIRONMENT DETECTION SCHEMAS
108
+ // =============================================================================
109
+
110
+ /** Validates WSL environment detection */
111
+ const wslEnvSchema = z.object({
112
+ WSL_DISTRO_NAME: z.string().optional(),
113
+ WSLENV: z.string().optional(),
114
+ })
115
+
116
+ /** Validates Linux terminal environment detection */
117
+ const linuxTerminalEnvSchema = z.object({
118
+ KITTY_WINDOW_ID: z.string().optional(),
119
+ WEZTERM_PANE: z.string().optional(),
120
+ ALACRITTY_WINDOW_ID: z.string().optional(),
121
+ GHOSTTY_RESOURCES_DIR: z.string().optional(),
122
+ TERM_PROGRAM: z.string().optional(),
123
+ GNOME_TERMINAL_SERVICE: z.string().optional(),
124
+ KONSOLE_VERSION: z.string().optional(),
125
+ })
126
+
127
+ /** Environment variables for macOS terminal detection */
128
+ const macTerminalEnvSchema = z.object({
129
+ TERM_PROGRAM: z.string().optional(),
130
+ GHOSTTY_RESOURCES_DIR: z.string().optional(),
131
+ ITERM_SESSION_ID: z.string().optional(),
132
+ KITTY_WINDOW_ID: z.string().optional(),
133
+ ALACRITTY_WINDOW_ID: z.string().optional(),
134
+ __CFBundleIdentifier: z.string().optional(),
135
+ })
136
+
137
+ type LinuxTerminal =
138
+ | "kitty"
139
+ | "wezterm"
140
+ | "alacritty"
141
+ | "ghostty"
142
+ | "foot"
143
+ | "gnome-terminal"
144
+ | "konsole"
145
+ | "xfce4-terminal"
146
+ | "xdg-terminal-exec"
147
+ | "x-terminal-emulator"
148
+ | "xterm"
149
+
150
+ type MacTerminal = "ghostty" | "iterm" | "warp" | "kitty" | "alacritty" | "terminal"
151
+
152
+ // =============================================================================
153
+ // PLATFORM DETECTION
154
+ // =============================================================================
155
+
156
+ /**
157
+ * Check if running inside WSL (Windows Subsystem for Linux).
158
+ * Checks environment variables and os.release() for Microsoft string.
159
+ */
160
+ function isInsideWSL(): boolean {
161
+ const parsed = wslEnvSchema.safeParse(process.env)
162
+ if (parsed.success && (parsed.data.WSL_DISTRO_NAME || parsed.data.WSLENV)) {
163
+ return true
164
+ }
165
+
166
+ // Fallback: check os.release() for Microsoft string
167
+ try {
168
+ return os.release().toLowerCase().includes("microsoft")
169
+ } catch {
170
+ return false
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Detect the best terminal type for the current platform.
176
+ * Priority: tmux > WSL > platform-specific
177
+ *
178
+ * @returns The detected terminal type
179
+ */
180
+ export function detectTerminalType(): TerminalType {
181
+ // tmux takes priority - user may be inside tmux on any platform
182
+ if (isInsideTmux()) {
183
+ return "tmux"
184
+ }
185
+
186
+ // WSL check (Linux inside Windows) - before platform detection
187
+ if (process.platform === "linux" && isInsideWSL()) {
188
+ return "windows" // Use Windows Terminal via interop
189
+ }
190
+
191
+ // Platform-specific
192
+ switch (process.platform) {
193
+ case "darwin":
194
+ return "macos"
195
+ case "win32":
196
+ return "windows"
197
+ case "linux":
198
+ return "linux-desktop"
199
+ default:
200
+ return "linux-desktop"
201
+ }
202
+ }
203
+
204
+ // =============================================================================
205
+ // TMUX OPERATIONS (MUTEX-PROTECTED)
206
+ // =============================================================================
207
+
208
+ /**
209
+ * Open a new tmux window with mutex protection.
210
+ * Includes stabilization delay after spawning to prevent races.
211
+ *
212
+ * SECURITY NOTE: Branch names and paths are passed via array-based spawn
213
+ * (Bun.spawnSync with array arguments), NOT shell string interpolation.
214
+ * This prevents command injection even if values contain special characters.
215
+ * The tmux `-n` flag treats its argument as a literal window name string.
216
+ *
217
+ * @param options - Window configuration
218
+ * @param options.sessionName - Optional tmux session name (uses current session if not specified)
219
+ * @param options.windowName - Name for the new window
220
+ * @param options.cwd - Working directory for the window
221
+ * @param options.command - Optional command to execute in the window
222
+ * @returns Success status and optional error message
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * const result = await openTmuxWindow({
227
+ * windowName: "feature-branch",
228
+ * cwd: "/path/to/worktree",
229
+ * command: "opencode --session abc123",
230
+ * })
231
+ * if (!result.success) {
232
+ * console.error(result.error)
233
+ * }
234
+ * ```
235
+ */
236
+ export async function openTmuxWindow(options: {
237
+ sessionName?: string
238
+ windowName: string
239
+ cwd: string
240
+ command?: string
241
+ }): Promise<TerminalResult> {
242
+ const { sessionName, windowName, cwd, command } = options
243
+
244
+ return tmuxMutex.runExclusive(async () => {
245
+ try {
246
+ // Build tmux new-window command
247
+ const tmuxArgs = ["new-window", "-n", windowName, "-c", cwd, "-P", "-F", "#{pane_id}"]
248
+
249
+ // Add session target if specified
250
+ if (sessionName) {
251
+ tmuxArgs.splice(1, 0, "-t", sessionName)
252
+ }
253
+
254
+ // If there's a command to run, create script first and pass it to new-window
255
+ if (command) {
256
+ const scriptPath = path.join(getTempDir(), `worktree-${Bun.randomUUIDv7()}.sh`)
257
+ const escapedCwd = escapeBash(cwd)
258
+ const escapedCommand = escapeBash(command)
259
+ const scriptContent = wrapWithSelfCleanup(
260
+ `cd "${escapedCwd}" || exit 1
261
+ ${escapedCommand}
262
+ exec $SHELL`,
263
+ )
264
+ await Bun.write(scriptPath, scriptContent)
265
+ Bun.spawnSync(["chmod", "+x", scriptPath])
266
+
267
+ // Add script execution to tmux args
268
+ tmuxArgs.push("--", "bash", scriptPath)
269
+ }
270
+
271
+ const createResult = Bun.spawnSync(["tmux", ...tmuxArgs])
272
+
273
+ if (createResult.exitCode !== 0) {
274
+ return {
275
+ success: false,
276
+ error: `Failed to create tmux window: ${createResult.stderr.toString()}`,
277
+ }
278
+ }
279
+
280
+ // Stabilization delay to let tmux server process the window
281
+ await Bun.sleep(STABILIZATION_DELAY_MS)
282
+
283
+ return { success: true }
284
+ } catch (error) {
285
+ return {
286
+ success: false,
287
+ error: error instanceof Error ? error.message : String(error),
288
+ }
289
+ }
290
+ })
291
+ }
292
+
293
+ // =============================================================================
294
+ // MACOS TERMINAL
295
+ // =============================================================================
296
+
297
+ /**
298
+ * Detect the current macOS terminal from environment variables.
299
+ * Prioritizes terminal-specific env vars over TERM_PROGRAM for reliability.
300
+ */
301
+ function detectCurrentMacTerminal(): MacTerminal {
302
+ const env = macTerminalEnvSchema.parse(process.env)
303
+
304
+ // Check specific env vars first (most reliable)
305
+ if (env.GHOSTTY_RESOURCES_DIR) return "ghostty"
306
+ if (env.ITERM_SESSION_ID) return "iterm"
307
+ if (env.KITTY_WINDOW_ID) return "kitty"
308
+ if (env.ALACRITTY_WINDOW_ID) return "alacritty"
309
+ if (env.__CFBundleIdentifier === "dev.warp.Warp-Stable") return "warp"
310
+
311
+ // Fallback to TERM_PROGRAM
312
+ const termProgram = env.TERM_PROGRAM?.toLowerCase()
313
+ switch (termProgram) {
314
+ case "ghostty":
315
+ return "ghostty"
316
+ case "iterm.app":
317
+ return "iterm"
318
+ case "warpterm":
319
+ return "warp"
320
+ case "apple_terminal":
321
+ return "terminal"
322
+ }
323
+
324
+ // Default to Terminal.app
325
+ return "terminal"
326
+ }
327
+
328
+ /**
329
+ * Open terminal on macOS (Terminal.app, iTerm, Ghostty, etc.)
330
+ * Detects current terminal and uses appropriate method.
331
+ *
332
+ * @param cwd - Working directory for the terminal
333
+ * @param command - Optional command to execute
334
+ * @returns Success status and optional error message
335
+ */
336
+ export async function openMacOSTerminal(cwd: string, command?: string): Promise<TerminalResult> {
337
+ // Guard: validate cwd
338
+ if (!cwd) {
339
+ return { success: false, error: "Working directory is required" }
340
+ }
341
+
342
+ const escapedCwd = escapeBash(cwd)
343
+ const escapedCommand = command ? escapeBash(command) : ""
344
+ const scriptContent = wrapWithSelfCleanup(
345
+ command
346
+ ? `cd "${escapedCwd}" && ${escapedCommand}\nexec bash`
347
+ : `cd "${escapedCwd}"\nexec bash`,
348
+ )
349
+
350
+ const terminal = detectCurrentMacTerminal()
351
+
352
+ // Track script path for detached spawns to clean up on error
353
+ let detachedScriptPath: string | null = null
354
+
355
+ // Handle terminals based on whether they use detached spawns
356
+ try {
357
+ switch (terminal) {
358
+ // Ghostty uses inline command to avoid permission dialog - no temp script needed
359
+ case "ghostty": {
360
+ try {
361
+ const proc = Bun.spawn(
362
+ [
363
+ "open",
364
+ "-na",
365
+ "Ghostty.app",
366
+ "--args",
367
+ `--working-directory=${cwd}`,
368
+ "-e",
369
+ "bash",
370
+ "-c",
371
+ command ? `cd "${escapedCwd}" && ${escapedCommand}` : `cd "${escapedCwd}"`,
372
+ ],
373
+ {
374
+ detached: true,
375
+ stdio: ["ignore", "ignore", "ignore"],
376
+ },
377
+ )
378
+ proc.unref()
379
+ return { success: true }
380
+ } catch (error) {
381
+ return {
382
+ success: false,
383
+ error: error instanceof Error ? error.message : String(error),
384
+ }
385
+ }
386
+ }
387
+
388
+ // Detached terminals: write script directly - it self-deletes via trap
389
+ // DO NOT use withTempScript for these - the finally block would delete
390
+ // the script before the detached process reads it
391
+ case "kitty": {
392
+ // Try kitty @ remote control first (synchronous, can use withTempScript)
393
+ const remoteResult = await withTempScript(scriptContent, async (scriptPath) => {
394
+ const result = Bun.spawnSync([
395
+ "kitty",
396
+ "@",
397
+ "launch",
398
+ "--type",
399
+ "tab",
400
+ "--cwd",
401
+ cwd,
402
+ "--",
403
+ "bash",
404
+ scriptPath,
405
+ ])
406
+ return result.exitCode === 0
407
+ })
408
+ if (remoteResult) {
409
+ return { success: true }
410
+ }
411
+
412
+ // Fallback: new window (detached) - write script directly
413
+ detachedScriptPath = path.join(
414
+ getTempDir(),
415
+ `worktree-${Date.now()}-${Math.random().toString(36).slice(2)}.sh`,
416
+ )
417
+ await Bun.write(detachedScriptPath, scriptContent)
418
+ await fs.chmod(detachedScriptPath, 0o755)
419
+
420
+ const kittyProc = Bun.spawn(
421
+ ["kitty", "--directory", cwd, "-e", "bash", detachedScriptPath],
422
+ {
423
+ detached: true,
424
+ stdio: ["ignore", "ignore", "ignore"],
425
+ },
426
+ )
427
+ kittyProc.unref()
428
+ detachedScriptPath = null // Clear on success - script will self-clean
429
+ return { success: true }
430
+ }
431
+
432
+ case "alacritty": {
433
+ // Detached spawn - write script directly
434
+ detachedScriptPath = path.join(
435
+ getTempDir(),
436
+ `worktree-${Date.now()}-${Math.random().toString(36).slice(2)}.sh`,
437
+ )
438
+ await Bun.write(detachedScriptPath, scriptContent)
439
+ await fs.chmod(detachedScriptPath, 0o755)
440
+
441
+ const alacrittyProc = Bun.spawn(
442
+ ["alacritty", "--working-directory", cwd, "-e", "bash", detachedScriptPath],
443
+ {
444
+ detached: true,
445
+ stdio: ["ignore", "ignore", "ignore"],
446
+ },
447
+ )
448
+ alacrittyProc.unref()
449
+ detachedScriptPath = null // Clear on success - script will self-clean
450
+ return { success: true }
451
+ }
452
+
453
+ case "warp": {
454
+ // Detached spawn - write script directly
455
+ detachedScriptPath = path.join(
456
+ getTempDir(),
457
+ `worktree-${Date.now()}-${Math.random().toString(36).slice(2)}.sh`,
458
+ )
459
+ await Bun.write(detachedScriptPath, scriptContent)
460
+ await fs.chmod(detachedScriptPath, 0o755)
461
+
462
+ const warpProc = Bun.spawn(["open", "-b", "dev.warp.Warp-Stable", detachedScriptPath], {
463
+ detached: true,
464
+ stdio: ["ignore", "ignore", "ignore"],
465
+ })
466
+ warpProc.unref()
467
+ detachedScriptPath = null // Clear on success - script will self-clean
468
+ return { success: true }
469
+ }
470
+
471
+ // iTerm uses AppleScript `write text` which returns before execution completes.
472
+ // Script must self-delete via trap — withTempScript would race.
473
+ case "iterm": {
474
+ detachedScriptPath = path.join(
475
+ getTempDir(),
476
+ `worktree-${Date.now()}-${Math.random().toString(36).slice(2)}.sh`,
477
+ )
478
+ await Bun.write(detachedScriptPath, scriptContent)
479
+ await fs.chmod(detachedScriptPath, 0o755)
480
+
481
+ const escapedPath = escapeAppleScript(detachedScriptPath)
482
+ const appleScript = `
483
+ tell application "iTerm"
484
+ if not (exists window 1) then
485
+ reopen
486
+ else
487
+ tell current window
488
+ create tab with default profile
489
+ end tell
490
+ end if
491
+ activate
492
+ tell first session of current tab of current window
493
+ write text "${escapedPath}"
494
+ end tell
495
+ end tell
496
+ `
497
+ const result = Bun.spawnSync(["osascript", "-e", appleScript])
498
+ if (result.exitCode !== 0) {
499
+ return {
500
+ success: false,
501
+ error: `iTerm AppleScript failed: ${result.stderr.toString()}`,
502
+ }
503
+ }
504
+ detachedScriptPath = null
505
+ return { success: true }
506
+ }
507
+
508
+ default: {
509
+ // Terminal.app - waits for completion, safe to use withTempScript
510
+ return await withTempScript(scriptContent, async (scriptPath) => {
511
+ const proc = Bun.spawn(["open", "-a", "Terminal", scriptPath], {
512
+ stdio: ["ignore", "ignore", "pipe"],
513
+ })
514
+ const exitCode = await proc.exited
515
+ if (exitCode !== 0) {
516
+ const stderr = await new Response(proc.stderr).text()
517
+ return { success: false, error: `Failed to open Terminal: ${stderr}` }
518
+ }
519
+ return { success: true }
520
+ })
521
+ }
522
+ }
523
+ } catch (error) {
524
+ // Clean up orphaned script on error (matches Linux/Windows behavior)
525
+ if (detachedScriptPath) {
526
+ try {
527
+ await fs.rm(detachedScriptPath)
528
+ } catch {
529
+ // Best-effort cleanup
530
+ }
531
+ }
532
+ return {
533
+ success: false,
534
+ error: `Failed to open terminal: ${error instanceof Error ? error.message : String(error)}`,
535
+ }
536
+ }
537
+ }
538
+
539
+ // =============================================================================
540
+ // LINUX TERMINAL
541
+ // =============================================================================
542
+
543
+ /**
544
+ * Detect the current Linux terminal from environment variables.
545
+ * Returns null if no terminal can be detected (use fallback chain).
546
+ */
547
+ function detectCurrentLinuxTerminal(): LinuxTerminal | null {
548
+ const env = linuxTerminalEnvSchema.parse(process.env)
549
+
550
+ // Check specific env vars first (most reliable)
551
+ if (env.KITTY_WINDOW_ID) return "kitty"
552
+ if (env.WEZTERM_PANE) return "wezterm"
553
+ if (env.ALACRITTY_WINDOW_ID) return "alacritty"
554
+ if (env.GHOSTTY_RESOURCES_DIR) return "ghostty"
555
+ if (env.GNOME_TERMINAL_SERVICE) return "gnome-terminal"
556
+ if (env.KONSOLE_VERSION) return "konsole"
557
+
558
+ // TERM_PROGRAM fallback
559
+ const termProgram = env.TERM_PROGRAM?.toLowerCase()
560
+ if (termProgram === "foot") return "foot"
561
+
562
+ return null
563
+ }
564
+
565
+ /**
566
+ * Open terminal on Linux with desktop environment detection.
567
+ * Priority: current terminal > xdg-terminal-exec > x-terminal-emulator > modern > DE > xterm
568
+ *
569
+ * NOTE: All Linux terminal spawns are detached, so we write the script directly
570
+ * instead of using withTempScript. The script self-deletes via trap.
571
+ *
572
+ * @param cwd - Working directory for the terminal
573
+ * @param command - Optional command to execute
574
+ * @returns Success status and optional error message
575
+ */
576
+ export async function openLinuxTerminal(cwd: string, command?: string): Promise<TerminalResult> {
577
+ // Guard: validate cwd
578
+ if (!cwd) {
579
+ return { success: false, error: "Working directory is required" }
580
+ }
581
+
582
+ const escapedCwd = escapeBash(cwd)
583
+ const escapedCommand = command ? escapeBash(command) : ""
584
+ const scriptContent = wrapWithSelfCleanup(
585
+ command
586
+ ? `cd "${escapedCwd}" && ${escapedCommand}\nexec bash`
587
+ : `cd "${escapedCwd}"\nexec bash`,
588
+ )
589
+
590
+ // Write script directly - it self-deletes via trap
591
+ // DO NOT use withTempScript - all Linux spawns are detached
592
+ const scriptPath = path.join(
593
+ getTempDir(),
594
+ `worktree-${Date.now()}-${Math.random().toString(36).slice(2)}.sh`,
595
+ )
596
+ await Bun.write(scriptPath, scriptContent)
597
+ await fs.chmod(scriptPath, 0o755)
598
+
599
+ try {
600
+ // Helper to try a terminal (all detached spawns)
601
+ const tryTerminal = async (
602
+ name: string,
603
+ args: string[],
604
+ ): Promise<{ tried: boolean; success: boolean }> => {
605
+ const check = Bun.spawnSync(["which", name])
606
+ if (check.exitCode !== 0) {
607
+ return { tried: false, success: false }
608
+ }
609
+
610
+ try {
611
+ const proc = Bun.spawn(args, {
612
+ detached: true,
613
+ stdio: ["ignore", "ignore", "ignore"],
614
+ })
615
+ proc.unref()
616
+ return { tried: true, success: true }
617
+ } catch {
618
+ return { tried: true, success: false }
619
+ }
620
+ }
621
+
622
+ // 1. Check current terminal via env detection
623
+ const currentTerminal = detectCurrentLinuxTerminal()
624
+ if (currentTerminal) {
625
+ let result: { tried: boolean; success: boolean }
626
+
627
+ switch (currentTerminal) {
628
+ case "kitty": {
629
+ // Try remote control first (synchronous, script still needed after)
630
+ const kittyRemote = Bun.spawnSync([
631
+ "kitty",
632
+ "@",
633
+ "launch",
634
+ "--type",
635
+ "tab",
636
+ "--cwd",
637
+ cwd,
638
+ "--",
639
+ "bash",
640
+ scriptPath,
641
+ ])
642
+ if (kittyRemote.exitCode === 0) {
643
+ return { success: true }
644
+ }
645
+ result = await tryTerminal("kitty", [
646
+ "kitty",
647
+ "--directory",
648
+ cwd,
649
+ "-e",
650
+ "bash",
651
+ scriptPath,
652
+ ])
653
+ break
654
+ }
655
+ case "wezterm":
656
+ result = await tryTerminal("wezterm", [
657
+ "wezterm",
658
+ "cli",
659
+ "spawn",
660
+ "--cwd",
661
+ cwd,
662
+ "--",
663
+ "bash",
664
+ scriptPath,
665
+ ])
666
+ break
667
+ case "alacritty":
668
+ result = await tryTerminal("alacritty", [
669
+ "alacritty",
670
+ "--working-directory",
671
+ cwd,
672
+ "-e",
673
+ "bash",
674
+ scriptPath,
675
+ ])
676
+ break
677
+ case "ghostty":
678
+ result = await tryTerminal("ghostty", ["ghostty", "-e", "bash", scriptPath])
679
+ break
680
+ case "foot":
681
+ result = await tryTerminal("foot", [
682
+ "foot",
683
+ "--working-directory",
684
+ cwd,
685
+ "bash",
686
+ scriptPath,
687
+ ])
688
+ break
689
+ case "gnome-terminal":
690
+ result = await tryTerminal("gnome-terminal", [
691
+ "gnome-terminal",
692
+ "--working-directory",
693
+ cwd,
694
+ "--",
695
+ "bash",
696
+ scriptPath,
697
+ ])
698
+ break
699
+ case "konsole":
700
+ result = await tryTerminal("konsole", [
701
+ "konsole",
702
+ "--workdir",
703
+ cwd,
704
+ "-e",
705
+ "bash",
706
+ scriptPath,
707
+ ])
708
+ break
709
+ default:
710
+ result = { tried: false, success: false }
711
+ }
712
+
713
+ if (result.success) {
714
+ return { success: true }
715
+ }
716
+ }
717
+
718
+ // 2. xdg-terminal-exec (modern XDG standard)
719
+ const xdgResult = await tryTerminal("xdg-terminal-exec", [
720
+ "xdg-terminal-exec",
721
+ "--",
722
+ "bash",
723
+ scriptPath,
724
+ ])
725
+ if (xdgResult.success) return { success: true }
726
+
727
+ // 3. x-terminal-emulator (Debian/Ubuntu)
728
+ const xteResult = await tryTerminal("x-terminal-emulator", [
729
+ "x-terminal-emulator",
730
+ "-e",
731
+ "bash",
732
+ scriptPath,
733
+ ])
734
+ if (xteResult.success) return { success: true }
735
+
736
+ // 4. Modern terminals fallback
737
+ const modernTerminals: Array<{ name: string; args: string[] }> = [
738
+ { name: "kitty", args: ["kitty", "--directory", cwd, "-e", "bash", scriptPath] },
739
+ {
740
+ name: "alacritty",
741
+ args: ["alacritty", "--working-directory", cwd, "-e", "bash", scriptPath],
742
+ },
743
+ {
744
+ name: "wezterm",
745
+ args: ["wezterm", "cli", "spawn", "--cwd", cwd, "--", "bash", scriptPath],
746
+ },
747
+ { name: "ghostty", args: ["ghostty", "-e", "bash", scriptPath] },
748
+ { name: "foot", args: ["foot", "--working-directory", cwd, "bash", scriptPath] },
749
+ ]
750
+
751
+ for (const { name, args } of modernTerminals) {
752
+ const result = await tryTerminal(name, args)
753
+ if (result.success) return { success: true }
754
+ }
755
+
756
+ // 5. DE terminals fallback
757
+ const deTerminals: Array<{ name: string; args: string[] }> = [
758
+ {
759
+ name: "gnome-terminal",
760
+ args: ["gnome-terminal", "--working-directory", cwd, "--", "bash", scriptPath],
761
+ },
762
+ { name: "konsole", args: ["konsole", "--workdir", cwd, "-e", "bash", scriptPath] },
763
+ {
764
+ name: "xfce4-terminal",
765
+ args: ["xfce4-terminal", "--working-directory", cwd, "-x", "bash", scriptPath],
766
+ },
767
+ ]
768
+
769
+ for (const { name, args } of deTerminals) {
770
+ const result = await tryTerminal(name, args)
771
+ if (result.success) return { success: true }
772
+ }
773
+
774
+ // 6. Last resort: xterm
775
+ const xtermResult = await tryTerminal("xterm", ["xterm", "-e", "bash", scriptPath])
776
+ if (xtermResult.success) return { success: true }
777
+
778
+ // No terminal found - clean up the orphaned script
779
+ try {
780
+ await fs.rm(scriptPath)
781
+ } catch {
782
+ // Best-effort cleanup
783
+ }
784
+ return { success: false, error: "No terminal emulator found" }
785
+ } catch (error) {
786
+ return {
787
+ success: false,
788
+ error: `Failed to spawn terminal: ${error instanceof Error ? error.message : String(error)}`,
789
+ }
790
+ }
791
+ }
792
+
793
+ // =============================================================================
794
+ // WINDOWS TERMINAL
795
+ // =============================================================================
796
+
797
+ /**
798
+ * Open terminal on Windows (Windows Terminal or cmd).
799
+ * Tries Windows Terminal (wt.exe) first, falls back to cmd.exe.
800
+ *
801
+ * NOTE: All Windows terminal spawns are detached, so we write the script directly
802
+ * instead of using withTempScript. The script self-deletes via goto trick.
803
+ *
804
+ * @param cwd - Working directory for the terminal
805
+ * @param command - Optional command to execute
806
+ * @returns Success status and optional error message
807
+ */
808
+ export async function openWindowsTerminal(cwd: string, command?: string): Promise<TerminalResult> {
809
+ // Guard: validate cwd
810
+ if (!cwd) {
811
+ return { success: false, error: "Working directory is required" }
812
+ }
813
+
814
+ const escapedCwd = escapeBatch(cwd)
815
+ const escapedCommand = command ? escapeBatch(command) : ""
816
+ const scriptContent = wrapBatchWithSelfCleanup(
817
+ command
818
+ ? `cd /d "${escapedCwd}"\r\n${escapedCommand}\r\ncmd /k`
819
+ : `cd /d "${escapedCwd}"\r\ncmd /k`,
820
+ )
821
+
822
+ // Write script directly - it self-deletes via goto trick
823
+ // DO NOT use withTempScript - all Windows spawns are detached
824
+ const scriptPath = path.join(
825
+ getTempDir(),
826
+ `worktree-${Date.now()}-${Math.random().toString(36).slice(2)}.bat`,
827
+ )
828
+ await Bun.write(scriptPath, scriptContent)
829
+ await fs.chmod(scriptPath, 0o755)
830
+
831
+ try {
832
+ // Check for Windows Terminal
833
+ const wtCheck = Bun.spawnSync(["where", "wt"], {
834
+ stdout: "pipe",
835
+ stderr: "pipe",
836
+ })
837
+
838
+ if (wtCheck.exitCode === 0) {
839
+ try {
840
+ const proc = Bun.spawn(["wt.exe", "-d", cwd, "cmd", "/k", scriptPath], {
841
+ detached: true,
842
+ stdio: ["ignore", "ignore", "ignore"],
843
+ })
844
+ proc.unref()
845
+ return { success: true }
846
+ } catch {
847
+ // Fall through to cmd.exe
848
+ }
849
+ }
850
+
851
+ // Fallback: cmd.exe
852
+ try {
853
+ const proc = Bun.spawn(["cmd", "/c", "start", "", scriptPath], {
854
+ detached: true,
855
+ stdio: ["ignore", "ignore", "ignore"],
856
+ })
857
+ proc.unref()
858
+ return { success: true }
859
+ } catch (error) {
860
+ // Failed to spawn - clean up orphaned script
861
+ try {
862
+ await fs.rm(scriptPath)
863
+ } catch {
864
+ // Best-effort cleanup
865
+ }
866
+ return {
867
+ success: false,
868
+ error: error instanceof Error ? error.message : String(error),
869
+ }
870
+ }
871
+ } catch (error) {
872
+ return {
873
+ success: false,
874
+ error: `Failed to spawn terminal: ${error instanceof Error ? error.message : String(error)}`,
875
+ }
876
+ }
877
+ }
878
+
879
+ // =============================================================================
880
+ // WSL TERMINAL
881
+ // =============================================================================
882
+
883
+ /**
884
+ * Open terminal in WSL via Windows Terminal interop.
885
+ * Falls back to bash in current terminal if wt.exe not available.
886
+ *
887
+ * NOTE: All WSL terminal spawns are detached, so we write the script directly
888
+ * instead of using withTempScript. The script self-deletes via trap.
889
+ */
890
+ export async function openWSLTerminal(cwd: string, command?: string): Promise<TerminalResult> {
891
+ // Guard: validate cwd
892
+ if (!cwd) {
893
+ return { success: false, error: "Working directory is required" }
894
+ }
895
+
896
+ const escapedCwd = escapeBash(cwd)
897
+ const escapedCommand = command ? escapeBash(command) : ""
898
+ const scriptContent = wrapWithSelfCleanup(
899
+ command
900
+ ? `cd "${escapedCwd}" && ${escapedCommand}\nexec bash`
901
+ : `cd "${escapedCwd}"\nexec bash`,
902
+ )
903
+
904
+ // Write script directly - it self-deletes via trap
905
+ // DO NOT use withTempScript - all WSL spawns are detached
906
+ const scriptPath = path.join(
907
+ getTempDir(),
908
+ `worktree-${Date.now()}-${Math.random().toString(36).slice(2)}.sh`,
909
+ )
910
+ await Bun.write(scriptPath, scriptContent)
911
+ await fs.chmod(scriptPath, 0o755)
912
+
913
+ try {
914
+ // Try wt.exe first (Windows Terminal via PATH interop)
915
+ const wtResult = Bun.spawnSync(["which", "wt.exe"])
916
+ if (wtResult.exitCode === 0) {
917
+ try {
918
+ const proc = Bun.spawn(["wt.exe", "-d", cwd, "bash", scriptPath], {
919
+ detached: true,
920
+ stdio: ["ignore", "ignore", "ignore"],
921
+ })
922
+ proc.unref()
923
+ return { success: true }
924
+ } catch {
925
+ // Fall through to bash
926
+ }
927
+ }
928
+
929
+ // Fallback: open in current terminal (new bash process)
930
+ try {
931
+ const proc = Bun.spawn(["bash", scriptPath], {
932
+ cwd,
933
+ detached: true,
934
+ stdio: ["ignore", "ignore", "ignore"],
935
+ })
936
+ proc.unref()
937
+ return { success: true }
938
+ } catch (error) {
939
+ // Failed to spawn - clean up orphaned script
940
+ try {
941
+ await fs.rm(scriptPath)
942
+ } catch {
943
+ // Best-effort cleanup
944
+ }
945
+ return {
946
+ success: false,
947
+ error: error instanceof Error ? error.message : String(error),
948
+ }
949
+ }
950
+ } catch (error) {
951
+ return {
952
+ success: false,
953
+ error: `Failed to spawn terminal: ${error instanceof Error ? error.message : String(error)}`,
954
+ }
955
+ }
956
+ }
957
+
958
+ // =============================================================================
959
+ // UNIFIED TERMINAL OPENING
960
+ // =============================================================================
961
+
962
+ /**
963
+ * Open a terminal window on the current platform.
964
+ * Automatically detects the best terminal type and method.
965
+ *
966
+ * @param cwd - Working directory for the terminal
967
+ * @param command - Optional command to execute
968
+ * @param windowName - Optional window name (used for tmux)
969
+ * @returns Success status and optional error message
970
+ */
971
+ export async function openTerminal(
972
+ cwd: string,
973
+ command?: string,
974
+ windowName?: string,
975
+ ): Promise<TerminalResult> {
976
+ const terminalType = detectTerminalType()
977
+
978
+ switch (terminalType) {
979
+ case "tmux":
980
+ return openTmuxWindow({
981
+ windowName: windowName || "worktree",
982
+ cwd,
983
+ command,
984
+ })
985
+
986
+ case "macos":
987
+ return openMacOSTerminal(cwd, command)
988
+
989
+ case "windows":
990
+ // Check if we're in WSL
991
+ if (process.platform === "linux" && isInsideWSL()) {
992
+ return openWSLTerminal(cwd, command)
993
+ }
994
+ return openWindowsTerminal(cwd, command)
995
+
996
+ case "linux-desktop":
997
+ return openLinuxTerminal(cwd, command)
998
+
999
+ default:
1000
+ return { success: false, error: `Unsupported terminal type: ${terminalType}` }
1001
+ }
1002
+ }