@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.
- package/config/customize.sh +143 -0
- package/config/kortix-env-setup.sh +25 -0
- package/kortix-master/package.json +22 -0
- package/kortix-master/src/config.ts +22 -0
- package/kortix-master/src/index.ts +44 -0
- package/kortix-master/src/routes/env.ts +65 -0
- package/kortix-master/src/routes/proxy.ts +108 -0
- package/kortix-master/src/routes/update.ts +185 -0
- package/kortix-master/src/services/proxy.ts +43 -0
- package/kortix-master/src/services/secret-store.ts +156 -0
- package/kortix-master/tsconfig.json +14 -0
- package/opencode/agents/kortix-browser.md +142 -0
- package/opencode/agents/kortix-build.md +62 -0
- package/opencode/agents/kortix-explore.md +66 -0
- package/opencode/agents/kortix-image-gen.md +33 -0
- package/opencode/agents/kortix-main.md +450 -0
- package/opencode/agents/kortix-plan.md +100 -0
- package/opencode/agents/kortix-research.md +84 -0
- package/opencode/agents/kortix-sheets.md +61 -0
- package/opencode/agents/kortix-slides.md +64 -0
- package/opencode/agents/kortix-web-dev.md +572 -0
- package/opencode/commands/email.md +36 -0
- package/opencode/commands/init.md +43 -0
- package/opencode/commands/journal.md +44 -0
- package/opencode/commands/memory-init.md +81 -0
- package/opencode/commands/memory-search.md +50 -0
- package/opencode/commands/memory-status.md +56 -0
- package/opencode/commands/research.md +36 -0
- package/opencode/commands/search.md +38 -0
- package/opencode/commands/slides.md +32 -0
- package/opencode/commands/spreadsheet.md +30 -0
- package/opencode/memory.json +37 -0
- package/opencode/ocx.jsonc +10 -0
- package/opencode/opencode.jsonc +103 -0
- package/opencode/package.json +25 -0
- package/opencode/patches/apply.sh +19 -0
- package/opencode/patches/opencode-pty-spawn.txt +49 -0
- package/opencode/plugin/background-agents.ts.disabled +483 -0
- package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
- package/opencode/plugin/kdco-primitives/index.ts +26 -0
- package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
- package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
- package/opencode/plugin/kdco-primitives/shell.ts +138 -0
- package/opencode/plugin/kdco-primitives/temp.ts +36 -0
- package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
- package/opencode/plugin/kdco-primitives/types.ts +13 -0
- package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
- package/opencode/plugin/memory.ts +306 -0
- package/opencode/plugin/worktree/state.ts +412 -0
- package/opencode/plugin/worktree/terminal.ts +1002 -0
- package/opencode/plugin/worktree.ts +861 -0
- package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
- package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
- package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
- package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
- package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
- package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
- package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
- package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
- package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
- package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
- package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
- package/opencode/skills/KORTIX-email/SKILL.md +145 -0
- package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
- package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
- package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
- package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
- package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
- package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
- package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
- package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
- package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
- package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
- package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
- package/opencode/skills/KORTIX-pdf/forms.md +36 -0
- package/opencode/skills/KORTIX-pdf/reference.md +105 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
- package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
- package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
- package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
- package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
- package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
- package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
- package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
- package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
- package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
- package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
- package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
- package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
- package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
- package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
- package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
- package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
- package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
- package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
- package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
- package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
- package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
- package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
- package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
- package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
- package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
- package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
- package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
- package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
- package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
- package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
- package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
- package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
- package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
- package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
- package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
- package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
- package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
- package/opencode/skills/KORTIX-session-search/Untitled +1 -0
- package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
- package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
- package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
- package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
- package/opencode/tools/image-gen.ts +342 -0
- package/opencode/tools/image-search.ts +190 -0
- package/opencode/tools/memory-get.ts +168 -0
- package/opencode/tools/memory-search.ts +247 -0
- package/opencode/tools/presentation-gen.ts +723 -0
- package/opencode/tools/scrape-webpage.ts +115 -0
- package/opencode/tools/scripts/.python-version +1 -0
- package/opencode/tools/scripts/convert_pdf.py +184 -0
- package/opencode/tools/scripts/convert_pptx.py +562 -0
- package/opencode/tools/scripts/pyproject.toml +11 -0
- package/opencode/tools/scripts/uv.lock +287 -0
- package/opencode/tools/scripts/validate_slide.py +74 -0
- package/opencode/tools/show-user.ts +217 -0
- package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
- package/opencode/tools/tests/image-gen.test.ts +215 -0
- package/opencode/tools/tests/image-search.test.ts +125 -0
- package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
- package/opencode/tools/tests/presentation-gen.test.ts +389 -0
- package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
- package/opencode/tools/tests/show-user.test.ts +241 -0
- package/opencode/tools/tests/video-gen.test.ts +110 -0
- package/opencode/tools/tests/web-search.test.ts +106 -0
- package/opencode/tools/video-gen.ts +200 -0
- package/opencode/tools/web-search.ts +153 -0
- package/opencode/tsconfig.json +29 -0
- package/package.json +36 -0
- package/patch-agent-browser.js +100 -0
- package/postinstall.sh +88 -0
- package/services/KORTIX-presentation-viewer/run +37 -0
- package/services/agent-browser-viewer/run +48 -0
- package/services/kortix-master/run +16 -0
- package/services/lss-sync/run +22 -0
- package/services/opencode-serve/run +25 -0
- 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
|
+
}
|