@alexcrondon/tx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexandre Rondon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # tx
2
+
3
+ Modular CLI for isolated dev environments using git worktrees. Manage dev servers, SSH tunnels, worktrees, and code editors from a single tool.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @alexcrondon/tx
9
+ ```
10
+
11
+ Or run without installing:
12
+
13
+ ```bash
14
+ npx @alexcrondon/tx
15
+ ```
16
+
17
+ ## Requirements
18
+
19
+ - POSIX shell (`sh`)
20
+ - Git (for worktrees)
21
+ - Optional: ngrok (for `tx tunnel`), tmux (for `tx code -t`)
22
+
23
+ ## Quick Start
24
+
25
+ ```bash
26
+ # Show status of servers, tunnel, db, worktrees
27
+ tx
28
+
29
+ # Create a worktree and launch your editor
30
+ tx code -b fix/my-bug
31
+
32
+ # Start dev server
33
+ tx serv start
34
+ ```
35
+
36
+ ## Commands
37
+
38
+ | Command | Default | Description |
39
+ |---------|---------|-------------|
40
+ | `tx` / `tx status` | — | Show status of servers, tunnel, db, worktrees |
41
+ | `tx config` | — | Manage config (user + project) |
42
+ | `tx serv` | list | Dev servers: list, start, stop, restart |
43
+ | `tx tunnel` | status | SSH tunnel (ngrok) |
44
+ | `tx db` | status | Background db process (port-forward, etc.) |
45
+ | `tx wt` | list | Git worktrees: add, remove, clean |
46
+ | `tx code` | start | Launch editor/agent in worktree |
47
+ | `tx nuke` | — | Stop everything, remove all worktrees |
48
+
49
+ Run `tx help` or `tx help <command>` for details.
50
+
51
+ ## Configuration
52
+
53
+ Config uses two files; the tool writes each key to the appropriate one:
54
+
55
+ | Scope | File | Keys |
56
+ |-------|------|------|
57
+ | User | `~/.txrc` | code, tunnel, db, auto_open, auto_tmux |
58
+ | Project | `.txrc` | port, start, url, branch, copy, worktrees_dir, install |
59
+
60
+ ```bash
61
+ tx config # Show current config (with scope per key)
62
+ tx config init # Interactive setup (writes to appropriate file)
63
+ tx config code cursor # Set user preference → ~/.txrc
64
+ tx config start "npm start" # Set project config → .txrc
65
+ tx config start --unset # Revert key to its default value
66
+ tx config reset # Delete project .txrc
67
+ tx config reset user # Delete ~/.txrc
68
+ ```
69
+
70
+ | Key | Scope | Default | Description |
71
+ |-----|-------|---------|-------------|
72
+ | port | project | 9001 | Starting port for servers |
73
+ | start | project | yarn start | Default server command |
74
+ | url | project | http://localhost:{PORT} | URL template |
75
+ | branch | project | (auto-detected) | Default branch for worktrees |
76
+ | copy | project | (empty) | Files to copy into new worktrees |
77
+ | worktrees_dir | project | .worktrees | Worktree directory |
78
+ | install | project | yarn install | Install command for new worktrees |
79
+ | code | user | claude | Command to run for `tx code` |
80
+ | tunnel | user | ngrok tcp 22 | Tunnel command |
81
+ | db | user | (empty) | Background db command |
82
+ | auto_open | user | false | Open browser after serv start |
83
+ | auto_tmux | user | false | Auto-launch tx code in tmux |
84
+
85
+ ## Examples
86
+
87
+ ```bash
88
+ # Status overview (default when no command)
89
+ tx
90
+
91
+ # Dev servers
92
+ tx serv # List running servers
93
+ tx serv start -o -p 3000 # Start on 3000, open browser
94
+ tx serv stop all # Stop all
95
+
96
+ # Worktrees
97
+ tx wt add -n hotfix # Create worktree "hotfix"
98
+ tx wt add -b fix/my-bug # Create worktree fix-my-bug on branch fix/my-bug
99
+
100
+ # Code editor (creates worktree by default)
101
+ tx code -b fix/my-bug # Worktree "fix-my-bug" on branch fix/my-bug
102
+ tx code -r # Run in repo root instead
103
+ tx code -t # Launch in tmux session
104
+ tx code attach hotfix # Reattach to tmux
105
+
106
+ # Tunnel
107
+ tx tunnel start -c # Start ngrok, caffeinate to prevent sleep
108
+
109
+ # Nuclear option
110
+ tx nuke # Stop all, remove worktrees (with confirmation)
111
+ ```
112
+
113
+ ## Shell completions
114
+
115
+ ```bash
116
+ # zsh
117
+ eval "$(tx completions)"
118
+ # or add to ~/.zshrc
119
+ ```
120
+
121
+ ## License
122
+
123
+ [MIT](LICENSE)
package/bin/tx ADDED
@@ -0,0 +1,47 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Resolve install directory (follow symlinks for npm global installs)
5
+ SELF="$0"
6
+ while [ -L "$SELF" ]; do
7
+ DIR=$(cd "$(dirname "$SELF")" && pwd)
8
+ SELF=$(readlink "$SELF")
9
+ case "$SELF" in
10
+ /*) ;;
11
+ *) SELF="$DIR/$SELF" ;;
12
+ esac
13
+ done
14
+ TX_ROOT="$(cd "$(dirname "$SELF")/.." && pwd)"
15
+
16
+ # Source shared utilities (loads defaults, sources .txrc)
17
+ . "$TX_ROOT/lib/common.sh"
18
+
19
+ # Parse command
20
+ COMMAND="${1:-status}"
21
+ shift 2>/dev/null || true
22
+
23
+ case "$COMMAND" in
24
+ --help|-h) COMMAND="help" ;;
25
+ esac
26
+
27
+ # Check for --help/-h flag on any command (e.g. tx serv --help)
28
+ for _arg in "$@"; do
29
+ case "$_arg" in
30
+ --help|-h)
31
+ . "$TX_ROOT/lib/help.sh"
32
+ cmd_help "$COMMAND"
33
+ exit 0
34
+ ;;
35
+ esac
36
+ done
37
+
38
+ # Dispatch to command module
39
+ COMMAND_FILE="$TX_ROOT/lib/${COMMAND}.sh"
40
+ if [ ! -f "$COMMAND_FILE" ]; then
41
+ echo "tx: unknown command '$COMMAND'"
42
+ echo "Run 'tx help' for usage."
43
+ exit 1
44
+ fi
45
+
46
+ . "$COMMAND_FILE"
47
+ cmd_"$COMMAND" "$@"
package/lib/code.sh ADDED
@@ -0,0 +1,216 @@
1
+ # lib/code.sh — tx code command
2
+
3
+ # Source dependencies (wt.sh already sources serv.sh)
4
+ . "$TX_ROOT/lib/wt.sh"
5
+
6
+ cmd_code() {
7
+ local flag_worktree=1
8
+ local flag_tunnel=0
9
+ [ "$TX_AUTO_TMUX" = "true" ] && flag_tunnel=1
10
+ local flag_attach=0
11
+ local flag_caffeinate=0
12
+ local flag_install=0
13
+ local name=""
14
+ local branch=""
15
+ local attach_name=""
16
+ local args=""
17
+
18
+ while [ $# -gt 0 ]; do
19
+ case "$1" in
20
+ --root|-r) flag_worktree=0; shift ;;
21
+ --tunnel|-t) flag_tunnel=1; shift ;;
22
+ --caffeinate|-c) flag_caffeinate=1; shift ;;
23
+ --install|-i) flag_install=1; shift ;;
24
+ --attach|-a) flag_attach=1; attach_name="${2:-}"; shift; shift 2>/dev/null || true ;;
25
+ --name=*|-n=*) name="${1#*=}"; shift ;;
26
+ --name|-n) name="$2"; shift 2 ;;
27
+ --branch=*|-b=*) branch="${1#*=}"; shift ;;
28
+ --branch|-b) branch="$2"; shift 2 ;;
29
+ attach) flag_attach=1; attach_name="${2:-}"; shift; shift 2>/dev/null || true ;;
30
+ start) shift ;;
31
+ *) [ -z "$name" ] && name="$1"; args="$args $1"; shift ;;
32
+ esac
33
+ done
34
+
35
+ if [ "$flag_attach" -eq 1 ] && [ -n "$args" ]; then
36
+ attach_name=$(echo "$args" | sed 's/^[[:space:]]*//' | cut -d' ' -f1)
37
+ fi
38
+
39
+ if [ "$flag_attach" -eq 1 ]; then
40
+ _code_attach "$attach_name"
41
+ return $?
42
+ fi
43
+
44
+ _code_start "$flag_worktree" "$flag_tunnel" "$flag_caffeinate" "$name" "$branch" "$flag_install"
45
+ }
46
+
47
+ _code_start() {
48
+ local flag_worktree="$1"
49
+ local flag_tunnel="$2"
50
+ local flag_caffeinate="$3"
51
+ local name="$4"
52
+ local branch="$5"
53
+ local flag_install="${6:-0}"
54
+ local work_dir="$PWD"
55
+
56
+ if [ "$flag_worktree" -eq 1 ]; then
57
+ local wt_args=""
58
+ [ -n "$name" ] && wt_args="$wt_args --name $name"
59
+ [ -n "$branch" ] && wt_args="$wt_args --branch $branch"
60
+ [ "$flag_install" -eq 1 ] && wt_args="$wt_args --install"
61
+
62
+ work_dir=$(_wt_add $wt_args | tail -1)
63
+ if [ $? -ne 0 ] || [ -z "$work_dir" ]; then
64
+ echo "Failed to create/open worktree."
65
+ return 1
66
+ fi
67
+ cd "$work_dir" || return 1
68
+ fi
69
+
70
+ local session_name
71
+ local detected_name
72
+ detected_name=$(tx_detect_worktree_name) || true
73
+ if [ -n "$name" ]; then
74
+ session_name="$name"
75
+ elif [ -n "$detected_name" ]; then
76
+ session_name="$detected_name"
77
+ else
78
+ session_name="tx"
79
+ fi
80
+
81
+ tx_ensure_serv_dir
82
+
83
+ # Find previous session to resume when inside a worktree
84
+ # Skip resume for auto-named worktrees (tx1, tx2, ...) since these are
85
+ # disposable — a recycled name shouldn't inherit an old session.
86
+ local resume_id=""
87
+ local auto_named=0
88
+ if [ "$flag_worktree" -eq 1 ] && [ -z "$name" ] && [ -z "$branch" ]; then
89
+ auto_named=1
90
+ fi
91
+ if [ -n "$detected_name" ] && [ "$auto_named" -eq 0 ]; then
92
+ local claude_project_dir
93
+ claude_project_dir="$HOME/.claude/projects/$(echo "$work_dir" | sed 's/[\/.]/-/g')"
94
+ # Only resume actual conversation sessions, not file-history-snapshot entries
95
+ local latest_session
96
+ latest_session=$(ls -t "${claude_project_dir}"/*.jsonl 2>/dev/null | while IFS= read -r f; do
97
+ grep -q '"sessionId"' "$f" 2>/dev/null && echo "$f" && break
98
+ done)
99
+ if [ -n "$latest_session" ]; then
100
+ resume_id=$(basename "$latest_session" .jsonl)
101
+ fi
102
+ fi
103
+
104
+ # Start caffeinate to prevent sleep
105
+ local caff_pid=""
106
+ if [ "$flag_caffeinate" -eq 1 ]; then
107
+ caffeinate -dims &
108
+ caff_pid=$!
109
+ fi
110
+
111
+ if [ "$flag_tunnel" -eq 1 ]; then
112
+ local tmux_name
113
+ tmux_name=$(tx_session_name "$session_name")
114
+ if tmux has-session -t "$tmux_name" 2>/dev/null; then
115
+ echo "Tmux session '$session_name' already exists. Attaching..."
116
+ tmux attach-session -t "$tmux_name"
117
+ else
118
+ echo "Launching $TX_CODE_CMD in tmux session '$session_name'..."
119
+ local tmux_cmd="$TX_CODE_CMD"
120
+ if [ -n "$resume_id" ]; then
121
+ tmux_cmd="$TX_CODE_CMD --resume $resume_id || $TX_CODE_CMD"
122
+ fi
123
+ echo " dir: $work_dir"
124
+ [ -n "$resume_id" ] && echo " resume: $resume_id" || true
125
+ tmux new-session -s "$tmux_name" -c "$work_dir" "$tmux_cmd"
126
+ fi
127
+ else
128
+ echo "Launching $TX_CODE_CMD..."
129
+ echo " dir: $work_dir"
130
+ [ -n "$resume_id" ] && echo " resume: $resume_id" || true
131
+ cd "$work_dir" || return 1
132
+ if [ -n "$resume_id" ]; then
133
+ $TX_CODE_CMD --resume "$resume_id" || $TX_CODE_CMD || true
134
+ else
135
+ $TX_CODE_CMD || true
136
+ fi
137
+ fi
138
+
139
+ # Cleanup after code command exits
140
+ _serv_stop_dir "$work_dir" 2>/dev/null || true
141
+ [ -n "$caff_pid" ] && kill "$caff_pid" 2>/dev/null || true
142
+
143
+ # Offer to remove worktree if we were in one
144
+ local wt_name=""
145
+ if [ "$flag_worktree" -eq 1 ]; then
146
+ wt_name=$(basename "$work_dir")
147
+ elif [ -n "$detected_name" ]; then
148
+ wt_name="$detected_name"
149
+ fi
150
+ if [ -n "$wt_name" ] && echo "$wt_name" | grep -q '^tx[0-9]*$'; then
151
+ printf "Remove worktree '%s'? [y/N] " "$wt_name"
152
+ read -r answer
153
+ case "$answer" in
154
+ y|Y|yes|YES)
155
+ cd "$(dirname "$work_dir")" 2>/dev/null && cd .. 2>/dev/null || true
156
+ _wt_remove --name "$wt_name"
157
+ ;;
158
+ esac
159
+ fi
160
+ }
161
+
162
+ _code_attach() {
163
+ local session_name="${1:-}"
164
+
165
+ # Direct attach if name provided
166
+ if [ -n "$session_name" ]; then
167
+ local tmux_name
168
+ tmux_name=$(tx_session_name "$session_name")
169
+ if ! tmux has-session -t "$tmux_name" 2>/dev/null; then
170
+ echo "No tx session '$session_name' found."
171
+ echo ""
172
+ _code_attach
173
+ return $?
174
+ fi
175
+ tmux attach-session -t "$tmux_name"
176
+ return $?
177
+ fi
178
+
179
+ # Interactive picker — list all tx sessions
180
+ local sessions
181
+ sessions=$(tx_list_sessions)
182
+ if [ -z "$sessions" ]; then
183
+ echo "No active tx sessions."
184
+ return 1
185
+ fi
186
+
187
+ echo "Active tx sessions:"
188
+ local i=1
189
+ echo "$sessions" | while IFS= read -r s; do
190
+ local display
191
+ display=$(tx_display_name "$s")
192
+ local dir
193
+ dir=$(tmux display-message -t "$s" -p '#{pane_current_path}' 2>/dev/null || echo "?")
194
+ # Shorten home prefix
195
+ dir=$(echo "$dir" | sed "s|^$HOME|~|")
196
+ printf " %d) %-20s %s\n" "$i" "$display" "$dir"
197
+ i=$((i + 1))
198
+ done
199
+
200
+ local count
201
+ count=$(echo "$sessions" | wc -l | tr -d ' ')
202
+
203
+ printf "Attach to (1-%s): " "$count"
204
+ read -r choice
205
+ case "$choice" in
206
+ ''|*[!0-9]*) echo "Cancelled."; return 1 ;;
207
+ esac
208
+ if [ "$choice" -lt 1 ] || [ "$choice" -gt "$count" ]; then
209
+ echo "Cancelled."
210
+ return 1
211
+ fi
212
+
213
+ local picked
214
+ picked=$(echo "$sessions" | sed -n "${choice}p")
215
+ tmux attach-session -t "$picked"
216
+ }
package/lib/common.sh ADDED
@@ -0,0 +1,266 @@
1
+ # lib/common.sh — shared utilities for tx
2
+ # Sourced by bin/tx on every invocation. Do not execute directly.
3
+
4
+ # --- Default Configuration ---
5
+ TX_PORT_START="${TX_PORT_START:-9001}"
6
+ TX_START_CMD="${TX_START_CMD:-yarn start}"
7
+ TX_URL_TEMPLATE="${TX_URL_TEMPLATE:-http://localhost:{PORT}}"
8
+ if [ -z "$TX_DEFAULT_BRANCH" ]; then
9
+ if git rev-parse --verify refs/heads/main >/dev/null 2>&1; then
10
+ TX_DEFAULT_BRANCH="main"
11
+ elif git rev-parse --verify refs/heads/master >/dev/null 2>&1; then
12
+ TX_DEFAULT_BRANCH="master"
13
+ else
14
+ TX_DEFAULT_BRANCH="main"
15
+ fi
16
+ fi
17
+ TX_COPY="${TX_COPY:-}"
18
+ TX_WORKTREES_DIR="${TX_WORKTREES_DIR:-.worktrees}"
19
+ TX_CODE_CMD="${TX_CODE_CMD:-claude}"
20
+ TX_TUNNEL_CMD="${TX_TUNNEL_CMD:-ngrok tcp 22}"
21
+ TX_DB_CMD="${TX_DB_CMD:-}"
22
+ TX_AUTO_OPEN="${TX_AUTO_OPEN:-false}"
23
+ TX_AUTO_TMUX="${TX_AUTO_TMUX:-false}"
24
+ TX_INSTALL_CMD="${TX_INSTALL_CMD:-yarn install}"
25
+
26
+ # Config scopes: user (~/.txrc) vs project (.txrc)
27
+ TX_CONFIG_USER_KEYS="code tunnel auto_open db auto_tmux"
28
+ TX_CONFIG_PROJECT_KEYS="port start url branch copy worktrees_dir install"
29
+ TX_CONFIG_KEYS="port start url branch copy worktrees_dir install code tunnel db auto_open auto_tmux"
30
+
31
+ # --- Config key-to-variable mapping ---
32
+ tx_config_var() {
33
+ case "$1" in
34
+ port) echo "TX_PORT_START" ;;
35
+ start) echo "TX_START_CMD" ;;
36
+ url) echo "TX_URL_TEMPLATE" ;;
37
+ branch) echo "TX_DEFAULT_BRANCH" ;;
38
+ copy) echo "TX_COPY" ;;
39
+ worktrees_dir) echo "TX_WORKTREES_DIR" ;;
40
+ code) echo "TX_CODE_CMD" ;;
41
+ tunnel) echo "TX_TUNNEL_CMD" ;;
42
+ db) echo "TX_DB_CMD" ;;
43
+ auto_open) echo "TX_AUTO_OPEN" ;;
44
+ install) echo "TX_INSTALL_CMD" ;;
45
+ auto_tmux) echo "TX_AUTO_TMUX" ;;
46
+ *) echo "" ;;
47
+ esac
48
+ }
49
+
50
+ # Return the hardcoded default value for a config key
51
+ tx_config_default() {
52
+ case "$1" in
53
+ port) echo "9001" ;;
54
+ start) echo "yarn start" ;;
55
+ url) echo "http://localhost:{PORT}" ;;
56
+ branch) echo "" ;;
57
+ copy) echo "" ;;
58
+ worktrees_dir) echo ".worktrees" ;;
59
+ code) echo "claude" ;;
60
+ tunnel) echo "ngrok tcp 22" ;;
61
+ db) echo "" ;;
62
+ auto_open) echo "false" ;;
63
+ install) echo "yarn install" ;;
64
+ auto_tmux) echo "false" ;;
65
+ esac
66
+ }
67
+
68
+ # Return "user" or "project" for a config key
69
+ tx_config_scope() {
70
+ case " $TX_CONFIG_USER_KEYS " in
71
+ *" $1 "*) echo "user" ;;
72
+ *) echo "project" ;;
73
+ esac
74
+ }
75
+
76
+ # Path to config file for a given scope
77
+ tx_config_file() {
78
+ case "$1" in
79
+ user) echo "${HOME}/.txrc" ;;
80
+ project) echo "$(_tx_project_root)/.txrc" ;;
81
+ *) echo "" ;;
82
+ esac
83
+ }
84
+
85
+ # --- Load config (user first, then project; each scope only from its file) ---
86
+ _tx_config_apply_file() {
87
+ local file="$1"
88
+ shift
89
+ [ ! -f "$file" ] && return
90
+ for key in "$@"; do
91
+ local var
92
+ var=$(tx_config_var "$key")
93
+ [ -z "$var" ] && continue
94
+ local line
95
+ line=$(grep "^${var}=" "$file" 2>/dev/null | head -1)
96
+ [ -n "$line" ] && eval "$line"
97
+ done
98
+ return 0
99
+ }
100
+
101
+ # Resolve main repo root (works from worktrees too).
102
+ # git --git-common-dir always points to the main .git directory.
103
+ _tx_project_root() {
104
+ local common_dir
105
+ common_dir=$(cd "$(git rev-parse --git-common-dir 2>/dev/null)" && pwd) || return 1
106
+ dirname "$common_dir"
107
+ }
108
+
109
+ _tx_config_apply_file "${HOME}/.txrc" $TX_CONFIG_USER_KEYS
110
+ _tx_config_apply_file "$(_tx_project_root)/.txrc" $TX_CONFIG_PROJECT_KEYS
111
+
112
+ # --- Shared Helpers ---
113
+
114
+ # Hash a directory path to a safe filename (MD5)
115
+ tx_hash_dir() {
116
+ echo -n "$1" | md5 -q 2>/dev/null || echo -n "$1" | md5sum | cut -d' ' -f1
117
+ }
118
+
119
+ # Ensure /tmp/tx-serv/ directory exists
120
+ tx_ensure_serv_dir() {
121
+ mkdir -p /tmp/tx-serv
122
+ }
123
+
124
+ # Find next available port starting from TX_PORT_START
125
+ tx_find_port() {
126
+ local port="${1:-$TX_PORT_START}"
127
+ while lsof -ti :"$port" > /dev/null 2>&1; do
128
+ port=$((port + 1))
129
+ done
130
+ echo "$port"
131
+ }
132
+
133
+ # Build URL from template and port
134
+ tx_build_url() {
135
+ local port="$1"
136
+ echo "$TX_URL_TEMPLATE" | sed "s/{PORT}/$port/g"
137
+ }
138
+
139
+ # Open URL in browser (macOS)
140
+ # Tries to open in a Chrome window on the same screen as the terminal.
141
+ # Falls back to regular `open` if Chrome isn't running or no window is found.
142
+ tx_open_browser() {
143
+ local url="$1"
144
+
145
+ osascript -l JavaScript -e '
146
+ ObjC.import("AppKit");
147
+ function run(argv) {
148
+ var url = argv[0];
149
+ var app = Application.currentApplication();
150
+ app.includeStandardAdditions = true;
151
+
152
+ // Get terminal window position (screen coords: origin top-left of primary)
153
+ var termX, termY;
154
+ try {
155
+ var se = Application("System Events");
156
+ var frontProc = se.processes.whose({ frontmost: true })[0];
157
+ var pos = frontProc.windows[0].position();
158
+ termX = pos[0];
159
+ termY = pos[1];
160
+ } catch (e) {
161
+ app.openLocation(url);
162
+ return;
163
+ }
164
+
165
+ // Check if Chrome is running with windows
166
+ var chrome;
167
+ try {
168
+ chrome = Application("Google Chrome");
169
+ if (!chrome.running() || chrome.windows.length === 0) {
170
+ app.openLocation(url);
171
+ return;
172
+ }
173
+ } catch (e) {
174
+ app.openLocation(url);
175
+ return;
176
+ }
177
+
178
+ // Get screen frames using NSScreen (Cocoa coords: origin bottom-left)
179
+ // and convert to screen coords (origin top-left) to match window positions
180
+ var screens = $.NSScreen.screens;
181
+ var primaryH = screens.objectAtIndex(0).frame.size.height;
182
+ var screenRects = [];
183
+ for (var i = 0; i < screens.count; i++) {
184
+ var f = screens.objectAtIndex(i).frame;
185
+ screenRects.push({
186
+ x: f.origin.x,
187
+ y: primaryH - f.origin.y - f.size.height,
188
+ w: f.size.width,
189
+ h: f.size.height
190
+ });
191
+ }
192
+
193
+ // Find which screen contains the terminal
194
+ var termScreen = screenRects[0];
195
+ for (var i = 0; i < screenRects.length; i++) {
196
+ var sr = screenRects[i];
197
+ if (termX >= sr.x && termX < sr.x + sr.w &&
198
+ termY >= sr.y && termY < sr.y + sr.h) {
199
+ termScreen = sr;
200
+ break;
201
+ }
202
+ }
203
+
204
+ // Find first Chrome window on the same screen as the terminal
205
+ var bestIdx = 0;
206
+ for (var i = 0; i < chrome.windows.length; i++) {
207
+ try {
208
+ var b = chrome.windows[i].bounds();
209
+ if (b.x >= termScreen.x && b.x < termScreen.x + termScreen.w &&
210
+ b.y >= termScreen.y && b.y < termScreen.y + termScreen.h) {
211
+ bestIdx = i;
212
+ break;
213
+ }
214
+ } catch (e) {}
215
+ }
216
+
217
+ // Open new tab in the matched window
218
+ chrome.windows[bestIdx].tabs.push(chrome.Tab({ url: url }));
219
+ chrome.activate();
220
+ }
221
+ ' "$url" 2>/dev/null || open "$url" 2>/dev/null
222
+ }
223
+
224
+ # Check if a process is alive by PID
225
+ tx_is_alive() {
226
+ kill -0 "$1" 2>/dev/null
227
+ }
228
+
229
+ # Detect if current directory is inside a tx worktree.
230
+ # Prints the worktree name if yes, empty string if no.
231
+ # Works by checking if any parent directory's name matches TX_WORKTREES_DIR.
232
+ tx_detect_worktree_name() {
233
+ local wt_basename
234
+ wt_basename=$(basename "$TX_WORKTREES_DIR")
235
+ local dir="$PWD"
236
+ while [ "$dir" != "/" ]; do
237
+ local parent
238
+ parent=$(dirname "$dir")
239
+ if [ "$(basename "$parent")" = "$wt_basename" ]; then
240
+ basename "$dir"
241
+ return 0
242
+ fi
243
+ dir="$parent"
244
+ done
245
+ echo ""
246
+ return 1
247
+ }
248
+
249
+ # --- Tmux session helpers ---
250
+ # All tx-created tmux sessions use a "tx-" prefix internally.
251
+ # These helpers translate between internal names and user-facing display names.
252
+
253
+ # Convert display name → internal tmux session name
254
+ tx_session_name() {
255
+ echo "tx-$1"
256
+ }
257
+
258
+ # Convert internal tmux session name → display name (strip tx- prefix)
259
+ tx_display_name() {
260
+ echo "$1" | sed 's/^tx-//'
261
+ }
262
+
263
+ # List all tx-managed tmux sessions (outputs internal session names, one per line)
264
+ tx_list_sessions() {
265
+ tmux list-sessions -F '#{session_name}' 2>/dev/null | grep '^tx-' || true
266
+ }