@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 +21 -0
- package/README.md +123 -0
- package/bin/tx +47 -0
- package/lib/code.sh +216 -0
- package/lib/common.sh +266 -0
- package/lib/completions.sh +59 -0
- package/lib/config.sh +174 -0
- package/lib/db.sh +91 -0
- package/lib/help.sh +192 -0
- package/lib/nuke.sh +58 -0
- package/lib/serv.sh +349 -0
- package/lib/status.sh +172 -0
- package/lib/tunnel.sh +144 -0
- package/lib/wt.sh +281 -0
- package/package.json +13 -0
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
|
+
}
|