ghost-tab 2.6.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/README.md +151 -0
- package/VERSION +1 -0
- package/bin/ghost-tab +360 -0
- package/bin/npx-ghost-tab.js +141 -0
- package/ghostty/config +5 -0
- package/lib/ai-select-tui.sh +66 -0
- package/lib/ai-tools.sh +19 -0
- package/lib/config-tui.sh +95 -0
- package/lib/ghostty-config.sh +26 -0
- package/lib/input.sh +39 -0
- package/lib/install.sh +224 -0
- package/lib/loading.sh +190 -0
- package/lib/menu-tui.sh +189 -0
- package/lib/notification-setup.sh +210 -0
- package/lib/process.sh +13 -0
- package/lib/project-actions-tui.sh +29 -0
- package/lib/project-actions.sh +9 -0
- package/lib/projects.sh +18 -0
- package/lib/settings-json.sh +178 -0
- package/lib/settings-menu-tui.sh +32 -0
- package/lib/setup.sh +16 -0
- package/lib/statusline-setup.sh +60 -0
- package/lib/statusline.sh +31 -0
- package/lib/tab-title-watcher.sh +118 -0
- package/lib/terminal-select-tui.sh +119 -0
- package/lib/terminals/adapter.sh +19 -0
- package/lib/terminals/ghostty.sh +58 -0
- package/lib/terminals/iterm2.sh +51 -0
- package/lib/terminals/kitty.sh +40 -0
- package/lib/terminals/registry.sh +37 -0
- package/lib/terminals/wezterm.sh +50 -0
- package/lib/tmux-session.sh +49 -0
- package/lib/tui.sh +80 -0
- package/lib/update.sh +52 -0
- package/package.json +42 -0
- package/templates/ccstatusline-settings.json +29 -0
- package/templates/statusline-command.sh +30 -0
- package/templates/statusline-wrapper.sh +40 -0
- package/terminals/ghostty/config +5 -0
- package/terminals/kitty/config +1 -0
- package/terminals/wezterm/config.lua +4 -0
- package/wrapper.sh +222 -0
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Ghost Tab
|
|
2
|
+
|
|
3
|
+
A **`Ghostty`** + **`tmux`** wrapper that launches a four-pane dev session with **`Claude Code`**, **`lazygit`**, **`broot`**, and a spare terminal. Automatically cleans up all processes when the window is closed — no zombie **`Claude Code`** processes.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
git clone https://github.com/JackUait/ghost-tab.git && cd ghost-tab && ./bin/ghost-tab
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That's it — only requirement is **`macOS`**. Everything (**`Ghostty`**, **`tmux`**, **`lazygit`**, **`broot`**, **`Claude Code`**) is installed automatically.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
**Step 1.** Open a new **`Ghostty`** window (`Cmd+N`)
|
|
22
|
+
|
|
23
|
+
**Step 2.** Use the interactive project selector:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
⬡ Ghost Tab
|
|
27
|
+
──────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
1❯ my-app
|
|
30
|
+
~/Projects/my-app
|
|
31
|
+
2 another-project
|
|
32
|
+
~/Projects/another-project
|
|
33
|
+
──────────────────────────────────────
|
|
34
|
+
A Add new project
|
|
35
|
+
D Delete a project
|
|
36
|
+
O Open once
|
|
37
|
+
P Plain terminal
|
|
38
|
+
──────────────────────────────────────
|
|
39
|
+
↑↓ navigate ⏎ select
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **Arrow keys** or **mouse click** to navigate
|
|
43
|
+
- **Number keys** (1-9) to jump directly to a project
|
|
44
|
+
- **Letter keys** — **A** add, **D** delete, **O** open once, **P** plain terminal
|
|
45
|
+
- **Enter** to select
|
|
46
|
+
- **Path autocomplete** when adding projects (with Tab completion)
|
|
47
|
+
- **Plain terminal** opens a bare shell with no tmux overhead
|
|
48
|
+
|
|
49
|
+
**Step 3.** The four-pane **`tmux`** session launches automatically with **`Claude Code`** already focused — start typing your prompt right away.
|
|
50
|
+
|
|
51
|
+
> [!TIP]
|
|
52
|
+
> You can also open a specific project directly from the terminal:
|
|
53
|
+
> ```sh
|
|
54
|
+
> ~/.config/ghostty/claude-wrapper.sh /path/to/project
|
|
55
|
+
> ```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Hotkeys
|
|
60
|
+
|
|
61
|
+
| Shortcut | Action |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `Cmd+T` | New tab |
|
|
64
|
+
| `Cmd+Shift+Left` | Previous tab |
|
|
65
|
+
| `Cmd+Shift+Right` | Next tab |
|
|
66
|
+
| `Left Option` | Acts as `Alt` instead of typing special characters |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## What `ghost-tab` Does
|
|
71
|
+
|
|
72
|
+
1. Downloads **`tmux`**, **`lazygit`**, **`broot`**, and **`jq`** natively (no package manager required)
|
|
73
|
+
2. Installs **`Claude Code`** via native installer (auto-updates)
|
|
74
|
+
3. Prompts to install **`Ghostty`** from ghostty.org if not already installed
|
|
75
|
+
4. Sets up the **`Ghostty`** config (with merge option if you have an existing one)
|
|
76
|
+
5. Walks you through adding your **project directories**
|
|
77
|
+
6. Installs **`Node.js`** LTS (if needed) and sets up **Claude Code status line** showing git info and context usage
|
|
78
|
+
7. Auto-updates via **`git pull`** in the background — notifies on next launch
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Status Line
|
|
83
|
+
|
|
84
|
+
The `ghost-tab` command configures a custom **Claude Code** status line based on [Matt Pocock's guide](https://www.aihero.dev/creating-the-perfect-claude-code-status-line):
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
my-project | main | S: 0 | U: 2 | A: 1 | 23.5%
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- **Repository name** — current project
|
|
91
|
+
- **Branch** — current git branch
|
|
92
|
+
- **S** — staged files count
|
|
93
|
+
- **U** — unstaged files count
|
|
94
|
+
- **A** — untracked (added) files count
|
|
95
|
+
- **Context %** — how much of Claude's context window is used
|
|
96
|
+
|
|
97
|
+
> [!TIP]
|
|
98
|
+
> Monitor context usage to know when to start a new conversation. Lower is better.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Process Cleanup
|
|
103
|
+
|
|
104
|
+
> [!CAUTION]
|
|
105
|
+
> When you close the **`Ghostty`** window, **all processes are force-terminated** — make sure your work is saved.
|
|
106
|
+
|
|
107
|
+
The wrapper automatically:
|
|
108
|
+
|
|
109
|
+
1. **Recursively kills** the full process tree of every **`tmux`** pane (including deeply nested subprocesses spawned by **`Claude Code`**, **`lazygit`**, etc.)
|
|
110
|
+
2. **Force-kills** (`SIGKILL`) any processes that ignored the initial `SIGTERM` after a brief grace period
|
|
111
|
+
3. **Destroys** the **`tmux`** session
|
|
112
|
+
4. **Self-destructs** the session via `destroy-unattached` if the **`tmux`** client disconnects without triggering cleanup
|
|
113
|
+
|
|
114
|
+
This prevents zombie **`Claude Code`** processes from accumulating.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Architecture
|
|
119
|
+
|
|
120
|
+
Ghost Tab uses a **hybrid architecture**:
|
|
121
|
+
|
|
122
|
+
**Layer 1: Go TUI Binary (`ghost-tab-tui`)**
|
|
123
|
+
- Interactive terminal UI components built with Bubbletea
|
|
124
|
+
- Project selector, AI tool selector, settings menu, input forms
|
|
125
|
+
- Outputs structured JSON for bash consumption
|
|
126
|
+
- Binary: `~/.local/bin/ghost-tab-tui`
|
|
127
|
+
|
|
128
|
+
**Layer 2: Bash Orchestration (`ghost-tab`)**
|
|
129
|
+
- Entry point and session orchestration
|
|
130
|
+
- Process management, config file operations
|
|
131
|
+
- Calls ghost-tab-tui for interactive parts
|
|
132
|
+
- Parses JSON responses with jq
|
|
133
|
+
- Script: `~/.local/bin/ghost-tab`
|
|
134
|
+
|
|
135
|
+
**Dependencies:**
|
|
136
|
+
- Go 1.21+ (for building)
|
|
137
|
+
- jq (for JSON parsing)
|
|
138
|
+
- tmux (session management)
|
|
139
|
+
- Ghostty (terminal emulator)
|
|
140
|
+
|
|
141
|
+
**Communication:**
|
|
142
|
+
```bash
|
|
143
|
+
# Bash calls Go with subcommand
|
|
144
|
+
result=$(ghost-tab-tui select-project --projects-file ~/.config/ghost-tab/projects)
|
|
145
|
+
|
|
146
|
+
# Go returns JSON
|
|
147
|
+
{"name": "my-project", "path": "/home/user/code/my-project", "selected": true}
|
|
148
|
+
|
|
149
|
+
# Bash parses with jq
|
|
150
|
+
project_name=$(echo "$result" | jq -r '.name')
|
|
151
|
+
```
|
package/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.6.0
|
package/bin/ghost-tab
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Wrap everything in a function so `curl | bash` reads the entire
|
|
5
|
+
# script into memory before executing. Without this, bash reads
|
|
6
|
+
# line-by-line from the pipe and `read` consumes script lines as input.
|
|
7
|
+
main() {
|
|
8
|
+
|
|
9
|
+
# Ensure ~/.local/bin is in PATH (where ghost-tab-tui gets installed)
|
|
10
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
11
|
+
|
|
12
|
+
# Determine where supporting files live
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
14
|
+
SHARE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
15
|
+
|
|
16
|
+
source "$SHARE_DIR/lib/tui.sh"
|
|
17
|
+
source "$SHARE_DIR/lib/install.sh"
|
|
18
|
+
source "$SHARE_DIR/lib/terminal-select-tui.sh"
|
|
19
|
+
source "$SHARE_DIR/lib/terminals/registry.sh"
|
|
20
|
+
source "$SHARE_DIR/lib/terminals/adapter.sh"
|
|
21
|
+
source "$SHARE_DIR/lib/settings-json.sh"
|
|
22
|
+
source "$SHARE_DIR/lib/ai-select-tui.sh"
|
|
23
|
+
source "$SHARE_DIR/lib/statusline-setup.sh"
|
|
24
|
+
source "$SHARE_DIR/lib/project-actions.sh"
|
|
25
|
+
source "$SHARE_DIR/lib/project-actions-tui.sh"
|
|
26
|
+
source "$SHARE_DIR/lib/update.sh"
|
|
27
|
+
|
|
28
|
+
# ---------- Terminal-only setup ----------
|
|
29
|
+
run_terminal_setup() {
|
|
30
|
+
local share_dir="$1"
|
|
31
|
+
|
|
32
|
+
# Ensure TUI binary is up to date (--terminal path skips main flow checks)
|
|
33
|
+
header "Checking TUI binary..."
|
|
34
|
+
if ! ensure_ghost_tab_tui "$share_dir"; then
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
header "Selecting terminal..."
|
|
39
|
+
echo ""
|
|
40
|
+
echo -e " Ghost Tab supports multiple terminal emulators."
|
|
41
|
+
echo -e " Select your preferred terminal:"
|
|
42
|
+
echo ""
|
|
43
|
+
|
|
44
|
+
if select_terminal_interactive; then
|
|
45
|
+
if [[ -z "$_selected_terminal" ]]; then
|
|
46
|
+
error "Internal error: TUI did not set terminal"
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
local selected_terminal="$_selected_terminal"
|
|
50
|
+
|
|
51
|
+
# Save preference
|
|
52
|
+
local pref_dir="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab"
|
|
53
|
+
mkdir -p "$pref_dir"
|
|
54
|
+
save_terminal_preference "$selected_terminal" "$pref_dir/terminal"
|
|
55
|
+
|
|
56
|
+
echo ""
|
|
57
|
+
success "Selected terminal: $(get_terminal_display_name "$selected_terminal")"
|
|
58
|
+
|
|
59
|
+
# Install terminal if needed
|
|
60
|
+
header "Checking $(get_terminal_display_name "$selected_terminal")..."
|
|
61
|
+
load_terminal_adapter "$selected_terminal"
|
|
62
|
+
if ! terminal_install; then
|
|
63
|
+
error "Terminal installation failed. Install manually and re-run: ghost-tab --terminal"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Ensure wrapper symlink exists
|
|
68
|
+
header "Setting up wrapper script..."
|
|
69
|
+
local wrapper_dir="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab"
|
|
70
|
+
mkdir -p "$wrapper_dir"
|
|
71
|
+
ln -sf "$share_dir/wrapper.sh" "$wrapper_dir/wrapper.sh"
|
|
72
|
+
success "Linked wrapper.sh in $wrapper_dir/"
|
|
73
|
+
|
|
74
|
+
if [ -d "$share_dir/lib" ]; then
|
|
75
|
+
[ -d "$wrapper_dir/lib" ] && [ ! -L "$wrapper_dir/lib" ] && rm -rf "${wrapper_dir:?}/lib"
|
|
76
|
+
ln -sfn "$share_dir/lib" "$wrapper_dir/lib"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Configure terminal
|
|
80
|
+
header "Setting up $(get_terminal_display_name "$selected_terminal") config..."
|
|
81
|
+
local terminal_config
|
|
82
|
+
terminal_config="$(terminal_get_config_path)"
|
|
83
|
+
local wrapper_path
|
|
84
|
+
wrapper_path="$(terminal_get_wrapper_path)"
|
|
85
|
+
|
|
86
|
+
if [ -f "$terminal_config" ]; then
|
|
87
|
+
warn "Existing config found at $terminal_config"
|
|
88
|
+
echo ""
|
|
89
|
+
echo -e " ${_BOLD}1)${_NC} Merge — add the wrapper command to your existing config"
|
|
90
|
+
echo -e " ${_BOLD}2)${_NC} Skip — don't modify the config (manual setup required)"
|
|
91
|
+
echo ""
|
|
92
|
+
read -rn1 -p "$(echo -e "${_BLUE}Choose (1/2):${_NC} ")" config_choice </dev/tty
|
|
93
|
+
echo ""
|
|
94
|
+
|
|
95
|
+
case "$config_choice" in
|
|
96
|
+
1) terminal_setup_config "$terminal_config" "$wrapper_path" ;;
|
|
97
|
+
*) info "Skipped config modification. Add the wrapper manually." ;;
|
|
98
|
+
esac
|
|
99
|
+
else
|
|
100
|
+
mkdir -p "$(dirname "$terminal_config")"
|
|
101
|
+
terminal_setup_config "$terminal_config" "$wrapper_path"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
echo ""
|
|
105
|
+
success "Terminal configured: $(get_terminal_display_name "$selected_terminal")"
|
|
106
|
+
info "Open a new $(get_terminal_display_name "$selected_terminal") window to start coding."
|
|
107
|
+
else
|
|
108
|
+
warn "Terminal setup did not complete. Run 'ghost-tab --terminal' to try again."
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# ---------- Argument parsing ----------
|
|
114
|
+
case "${1:-}" in
|
|
115
|
+
--terminal)
|
|
116
|
+
run_terminal_setup "$SHARE_DIR"
|
|
117
|
+
exit 0
|
|
118
|
+
;;
|
|
119
|
+
--*)
|
|
120
|
+
error "Unknown flag: $1"
|
|
121
|
+
echo "Usage: ghost-tab [--terminal]"
|
|
122
|
+
exit 1
|
|
123
|
+
;;
|
|
124
|
+
esac
|
|
125
|
+
|
|
126
|
+
# ---------- OS check ----------
|
|
127
|
+
header "Checking platform..."
|
|
128
|
+
if [ "$(uname)" != "Darwin" ]; then
|
|
129
|
+
error "This setup script only supports macOS."
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
132
|
+
success "macOS detected"
|
|
133
|
+
notify_if_updated
|
|
134
|
+
|
|
135
|
+
# ---------- Dependencies ----------
|
|
136
|
+
header "Installing dependencies..."
|
|
137
|
+
ensure_base_requirements
|
|
138
|
+
|
|
139
|
+
# ---------- TUI Binary ----------
|
|
140
|
+
header "Checking TUI binary..."
|
|
141
|
+
if ! ensure_ghost_tab_tui "$SHARE_DIR"; then
|
|
142
|
+
exit 1
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# ---------- AI Coding Tools ----------
|
|
146
|
+
header "Setting up AI coding tools..."
|
|
147
|
+
echo ""
|
|
148
|
+
echo -e " Ghost Tab supports multiple AI coding assistants."
|
|
149
|
+
echo -e " Select your preferred AI tool:"
|
|
150
|
+
echo ""
|
|
151
|
+
|
|
152
|
+
# Use TUI to select AI tool
|
|
153
|
+
if select_ai_tool_interactive; then
|
|
154
|
+
# Variable set by select_ai_tool_interactive: _selected_ai_tool
|
|
155
|
+
if [[ -z "$_selected_ai_tool" ]]; then
|
|
156
|
+
error "Internal error: TUI did not set AI tool"
|
|
157
|
+
exit 1
|
|
158
|
+
fi
|
|
159
|
+
SELECTED_AI_TOOL="$_selected_ai_tool"
|
|
160
|
+
|
|
161
|
+
# Save preference
|
|
162
|
+
AI_TOOL_PREF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab"
|
|
163
|
+
mkdir -p "$AI_TOOL_PREF_DIR"
|
|
164
|
+
echo "$SELECTED_AI_TOOL" > "$AI_TOOL_PREF_DIR/ai-tool"
|
|
165
|
+
|
|
166
|
+
echo ""
|
|
167
|
+
success "Selected AI tool: $SELECTED_AI_TOOL"
|
|
168
|
+
|
|
169
|
+
# Install the selected tool
|
|
170
|
+
case "$SELECTED_AI_TOOL" in
|
|
171
|
+
claude)
|
|
172
|
+
ensure_command "claude" "curl -fsSL https://claude.ai/install.sh | bash" \
|
|
173
|
+
"Run 'claude' to authenticate before launching Ghost Tab." "Claude Code"
|
|
174
|
+
;;
|
|
175
|
+
codex)
|
|
176
|
+
ensure_command "codex" "brew install --cask codex" "" "Codex CLI"
|
|
177
|
+
;;
|
|
178
|
+
copilot)
|
|
179
|
+
ensure_command "copilot" "brew install copilot-cli" "" "Copilot CLI"
|
|
180
|
+
;;
|
|
181
|
+
opencode)
|
|
182
|
+
ensure_command "opencode" "brew install anomalyco/tap/opencode" "" "OpenCode"
|
|
183
|
+
;;
|
|
184
|
+
esac
|
|
185
|
+
else
|
|
186
|
+
info "AI tool selection cancelled"
|
|
187
|
+
exit 1
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
# ---------- Terminal Selection ----------
|
|
191
|
+
header "Selecting terminal..."
|
|
192
|
+
echo ""
|
|
193
|
+
echo -e " Ghost Tab supports multiple terminal emulators."
|
|
194
|
+
echo -e " Select your preferred terminal:"
|
|
195
|
+
echo ""
|
|
196
|
+
|
|
197
|
+
if select_terminal_interactive; then
|
|
198
|
+
if [[ -z "$_selected_terminal" ]]; then
|
|
199
|
+
error "Internal error: TUI did not set terminal"
|
|
200
|
+
exit 1
|
|
201
|
+
fi
|
|
202
|
+
SELECTED_TERMINAL="$_selected_terminal"
|
|
203
|
+
|
|
204
|
+
# Save preference
|
|
205
|
+
TERMINAL_PREF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab"
|
|
206
|
+
mkdir -p "$TERMINAL_PREF_DIR"
|
|
207
|
+
save_terminal_preference "$SELECTED_TERMINAL" "$TERMINAL_PREF_DIR/terminal"
|
|
208
|
+
|
|
209
|
+
echo ""
|
|
210
|
+
success "Selected terminal: $(get_terminal_display_name "$SELECTED_TERMINAL")"
|
|
211
|
+
else
|
|
212
|
+
warn "Terminal setup did not complete. Run 'ghost-tab --terminal' to try again."
|
|
213
|
+
exit 1
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
# ---------- Terminal Installation ----------
|
|
217
|
+
header "Checking $(get_terminal_display_name "$SELECTED_TERMINAL")..."
|
|
218
|
+
load_terminal_adapter "$SELECTED_TERMINAL"
|
|
219
|
+
if ! terminal_install; then
|
|
220
|
+
error "Terminal installation failed. Install manually and re-run: ghost-tab --terminal"
|
|
221
|
+
exit 1
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
# Verify supporting files exist
|
|
225
|
+
if [ ! -f "$SHARE_DIR/wrapper.sh" ] || [ ! -d "$SHARE_DIR/templates" ]; then
|
|
226
|
+
error "Supporting files not found in $SHARE_DIR. Re-clone the repo and try again."
|
|
227
|
+
exit 1
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
# ---------- Wrapper Script ----------
|
|
231
|
+
header "Setting up wrapper script..."
|
|
232
|
+
WRAPPER_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab"
|
|
233
|
+
mkdir -p "$WRAPPER_DIR"
|
|
234
|
+
ln -sf "$SHARE_DIR/wrapper.sh" "$WRAPPER_DIR/wrapper.sh"
|
|
235
|
+
success "Linked wrapper.sh in $WRAPPER_DIR/"
|
|
236
|
+
|
|
237
|
+
# Symlink shared libraries (remove old copies if present)
|
|
238
|
+
if [ -d "$SHARE_DIR/lib" ]; then
|
|
239
|
+
[ -d "$WRAPPER_DIR/lib" ] && [ ! -L "$WRAPPER_DIR/lib" ] && rm -rf "${WRAPPER_DIR:?}/lib"
|
|
240
|
+
ln -sfn "$SHARE_DIR/lib" "$WRAPPER_DIR/lib"
|
|
241
|
+
success "Linked shared libraries to $WRAPPER_DIR/lib/"
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# ---------- Ghost Tab CLI Command ----------
|
|
245
|
+
header "Setting up ghost-tab command..."
|
|
246
|
+
ln -sf "$SHARE_DIR/bin/ghost-tab-config" "$HOME/.local/bin/ghost-tab"
|
|
247
|
+
success "Created ghost-tab command at ~/.local/bin/ghost-tab"
|
|
248
|
+
|
|
249
|
+
# ---------- Terminal Config ----------
|
|
250
|
+
header "Setting up $(get_terminal_display_name "$SELECTED_TERMINAL") config..."
|
|
251
|
+
TERMINAL_CONFIG="$(terminal_get_config_path)"
|
|
252
|
+
WRAPPER_PATH="$(terminal_get_wrapper_path)"
|
|
253
|
+
|
|
254
|
+
if [ -f "$TERMINAL_CONFIG" ]; then
|
|
255
|
+
warn "Existing config found at $TERMINAL_CONFIG"
|
|
256
|
+
echo ""
|
|
257
|
+
echo -e " ${_BOLD}1)${_NC} Merge — add the wrapper command to your existing config"
|
|
258
|
+
echo -e " ${_BOLD}2)${_NC} Skip — don't modify the config (manual setup required)"
|
|
259
|
+
echo ""
|
|
260
|
+
read -rn1 -p "$(echo -e "${_BLUE}Choose (1/2):${_NC} ")" config_choice </dev/tty
|
|
261
|
+
echo ""
|
|
262
|
+
|
|
263
|
+
case "$config_choice" in
|
|
264
|
+
1) terminal_setup_config "$TERMINAL_CONFIG" "$WRAPPER_PATH" ;;
|
|
265
|
+
*) info "Skipped config modification. Add the wrapper manually." ;;
|
|
266
|
+
esac
|
|
267
|
+
else
|
|
268
|
+
mkdir -p "$(dirname "$TERMINAL_CONFIG")"
|
|
269
|
+
terminal_setup_config "$TERMINAL_CONFIG" "$WRAPPER_PATH"
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
# Migrate from old config location
|
|
273
|
+
OLD_PROJECTS_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/vibecode-editor"
|
|
274
|
+
NEW_PROJECTS_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab"
|
|
275
|
+
if [ -d "$OLD_PROJECTS_DIR" ] && [ ! -d "$NEW_PROJECTS_DIR" ]; then
|
|
276
|
+
mv "$OLD_PROJECTS_DIR" "$NEW_PROJECTS_DIR"
|
|
277
|
+
info "Migrated config from vibecode-editor to ghost-tab"
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# ---------- Projects ----------
|
|
281
|
+
header "Setting up projects..."
|
|
282
|
+
PROJECTS_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab"
|
|
283
|
+
PROJECTS_FILE="$PROJECTS_DIR/projects"
|
|
284
|
+
mkdir -p "$PROJECTS_DIR"
|
|
285
|
+
|
|
286
|
+
echo ""
|
|
287
|
+
read -rn1 -p "$(echo -e "${_BLUE}Add a project? (y/n):${_NC} ")" add_project </dev/tty
|
|
288
|
+
echo ""
|
|
289
|
+
|
|
290
|
+
while [[ "$add_project" =~ ^[yY]$ ]]; do
|
|
291
|
+
if add_project_interactive; then
|
|
292
|
+
# shellcheck disable=SC2154 # _add_project_name and _add_project_path are set by add_project_interactive
|
|
293
|
+
add_project_to_file "$_add_project_name" "$_add_project_path" "$PROJECTS_FILE"
|
|
294
|
+
success "Added project: $_add_project_name"
|
|
295
|
+
else
|
|
296
|
+
info "Cancelled"
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
echo ""
|
|
300
|
+
read -rn1 -p "$(echo -e "${_BLUE}Add another? (y/n):${_NC} ")" add_project </dev/tty
|
|
301
|
+
echo ""
|
|
302
|
+
done
|
|
303
|
+
|
|
304
|
+
if [ -f "$PROJECTS_FILE" ] && [ -s "$PROJECTS_FILE" ]; then
|
|
305
|
+
success "Projects saved to $PROJECTS_FILE"
|
|
306
|
+
else
|
|
307
|
+
info "No projects added. Add them later to $PROJECTS_FILE"
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
# ---------- Claude Code Status Line ----------
|
|
311
|
+
if [ "$SELECTED_AI_TOOL" = "claude" ]; then
|
|
312
|
+
header "Setting up Claude Code status line..."
|
|
313
|
+
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
|
|
314
|
+
setup_statusline "$SHARE_DIR" "$CLAUDE_SETTINGS" "$HOME"
|
|
315
|
+
else
|
|
316
|
+
header "Skipping Claude Code status line..."
|
|
317
|
+
info "Status line features are only available with Claude Code"
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
# Codex CLI status line (always configure if Codex is selected)
|
|
321
|
+
if [ "$SELECTED_AI_TOOL" = "codex" ]; then
|
|
322
|
+
header "Setting up Codex CLI status line..."
|
|
323
|
+
mkdir -p ~/.codex
|
|
324
|
+
# Only add status_line if not already present
|
|
325
|
+
if [ ! -f ~/.codex/config.toml ] || ! grep -q 'status_line' ~/.codex/config.toml 2>/dev/null; then
|
|
326
|
+
if ! grep -q '^\[tui\]' ~/.codex/config.toml 2>/dev/null; then
|
|
327
|
+
printf '\n[tui]\n' >> ~/.codex/config.toml
|
|
328
|
+
fi
|
|
329
|
+
printf 'status_line = ["model-with-reasoning", "git-branch", "context-remaining", "used-tokens"]\n' >> ~/.codex/config.toml
|
|
330
|
+
success "Codex CLI status line configured (model, branch, context, tokens)"
|
|
331
|
+
else
|
|
332
|
+
success "Codex CLI status line already configured"
|
|
333
|
+
fi
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
# ---------- Summary ----------
|
|
337
|
+
header "Setup complete!"
|
|
338
|
+
echo ""
|
|
339
|
+
success "Wrapper script: ~/.config/ghost-tab/wrapper.sh (symlink)"
|
|
340
|
+
_terminal_pref="$(cat "${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab/terminal" 2>/dev/null)"
|
|
341
|
+
success "Terminal: $(get_terminal_display_name "$_terminal_pref")"
|
|
342
|
+
_ai_default="$(cat "${XDG_CONFIG_HOME:-$HOME/.config}/ghost-tab/ai-tool" 2>/dev/null)"
|
|
343
|
+
success "AI tool: $(echo "$_ai_default" | sed 's/claude/Claude Code/;s/codex/Codex CLI/;s/copilot/Copilot CLI/;s/opencode/OpenCode/')"
|
|
344
|
+
success "Projects file: $PROJECTS_FILE"
|
|
345
|
+
success "Config command: ghost-tab (in ~/.local/bin/)"
|
|
346
|
+
if [ -f ~/.claude/statusline-wrapper.sh ]; then
|
|
347
|
+
success "Status line: ~/.claude/statusline-wrapper.sh"
|
|
348
|
+
fi
|
|
349
|
+
if [ -f ~/.codex/config.toml ]; then
|
|
350
|
+
success "Codex config: ~/.codex/config.toml"
|
|
351
|
+
fi
|
|
352
|
+
if [ -f "${XDG_CONFIG_HOME:-$HOME/.config}/opencode/plugins/ghost-tab.ts" ]; then
|
|
353
|
+
success "OpenCode plugin: ~/.config/opencode/plugins/ghost-tab.ts"
|
|
354
|
+
fi
|
|
355
|
+
echo ""
|
|
356
|
+
info "Run 'ghost-tab' to manage configuration, or open a new $(get_terminal_display_name "${SELECTED_TERMINAL:-$_terminal_pref}") window to start coding."
|
|
357
|
+
|
|
358
|
+
} # end main
|
|
359
|
+
|
|
360
|
+
main "$@"
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
const REPO = 'JackUait/ghost-tab';
|
|
10
|
+
|
|
11
|
+
// Allow overrides for testing
|
|
12
|
+
const home = process.env.HOME || os.homedir();
|
|
13
|
+
const installDir = process.env.GHOST_TAB_INSTALL_DIR
|
|
14
|
+
|| path.join(home, '.local', 'share', 'ghost-tab');
|
|
15
|
+
const tuiBinDir = path.join(home, '.local', 'bin');
|
|
16
|
+
const tuiBinPath = path.join(tuiBinDir, 'ghost-tab-tui');
|
|
17
|
+
|
|
18
|
+
// Package root (where npm extracted us)
|
|
19
|
+
const pkgRoot = path.resolve(__dirname, '..');
|
|
20
|
+
|
|
21
|
+
function main() {
|
|
22
|
+
// Platform check
|
|
23
|
+
const platform = process.env.GHOST_TAB_MOCK_PLATFORM || process.platform;
|
|
24
|
+
if (platform !== 'darwin') {
|
|
25
|
+
process.stderr.write(`Error: ghost-tab only supports macOS (detected: ${platform})\n`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const version = fs.readFileSync(path.join(pkgRoot, 'VERSION'), 'utf8').trim();
|
|
30
|
+
|
|
31
|
+
// Check if already installed at correct version
|
|
32
|
+
const versionMarker = path.join(installDir, '.version');
|
|
33
|
+
let installedVersion = '';
|
|
34
|
+
try {
|
|
35
|
+
installedVersion = fs.readFileSync(versionMarker, 'utf8').trim();
|
|
36
|
+
} catch (_) {
|
|
37
|
+
// Not installed yet
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (installedVersion === version) {
|
|
41
|
+
process.stdout.write(`ghost-tab ${version} already up to date\n`);
|
|
42
|
+
} else {
|
|
43
|
+
// Copy bash distribution to install dir
|
|
44
|
+
process.stdout.write(`Installing ghost-tab ${version} to ${installDir}...\n`);
|
|
45
|
+
copyDistribution(pkgRoot, installDir);
|
|
46
|
+
fs.writeFileSync(versionMarker, version + '\n');
|
|
47
|
+
process.stdout.write(`Installed ghost-tab ${version}\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Download TUI binary if needed
|
|
51
|
+
if (!process.env.GHOST_TAB_SKIP_TUI_DOWNLOAD) {
|
|
52
|
+
ensureTuiBinary(version);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Exec the bash installer
|
|
56
|
+
if (!process.env.GHOST_TAB_SKIP_EXEC) {
|
|
57
|
+
const installer = path.join(installDir, 'bin', 'ghost-tab');
|
|
58
|
+
const args = process.argv.slice(2);
|
|
59
|
+
try {
|
|
60
|
+
execFileSync('bash', [installer, ...args], { stdio: 'inherit' });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
process.exit(err.status || 1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Recursively copy the bash distribution files.
|
|
68
|
+
function copyDistribution(src, dest) {
|
|
69
|
+
const entries = [
|
|
70
|
+
'bin/ghost-tab',
|
|
71
|
+
'lib',
|
|
72
|
+
'templates',
|
|
73
|
+
'ghostty',
|
|
74
|
+
'terminals',
|
|
75
|
+
'wrapper.sh',
|
|
76
|
+
'VERSION',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const srcPath = path.join(src, entry);
|
|
81
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
82
|
+
const destPath = path.join(dest, entry);
|
|
83
|
+
copyRecursive(srcPath, destPath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function copyRecursive(src, dest) {
|
|
88
|
+
const stat = fs.statSync(src);
|
|
89
|
+
if (stat.isDirectory()) {
|
|
90
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
91
|
+
for (const child of fs.readdirSync(src)) {
|
|
92
|
+
copyRecursive(path.join(src, child), path.join(dest, child));
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
96
|
+
fs.copyFileSync(src, dest);
|
|
97
|
+
// Preserve executable bit
|
|
98
|
+
if (stat.mode & 0o111) {
|
|
99
|
+
fs.chmodSync(dest, stat.mode);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Download the TUI binary from GitHub Releases if missing or wrong version.
|
|
105
|
+
function ensureTuiBinary(version) {
|
|
106
|
+
// Check if existing binary matches version
|
|
107
|
+
try {
|
|
108
|
+
const out = execFileSync(tuiBinPath, ['--version'], { encoding: 'utf8' });
|
|
109
|
+
const installed = out.replace(/.*version\s*/, '').trim();
|
|
110
|
+
if (installed === version) {
|
|
111
|
+
process.stdout.write(`ghost-tab-tui ${version} already up to date\n`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
process.stdout.write(`Updating ghost-tab-tui (${installed} -> ${version})...\n`);
|
|
115
|
+
} catch (_) {
|
|
116
|
+
process.stdout.write(`Downloading ghost-tab-tui ${version}...\n`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const arch = process.arch === 'x64' ? 'amd64' : process.arch;
|
|
120
|
+
const url = `https://github.com/${REPO}/releases/download/v${version}/ghost-tab-tui-darwin-${arch}`;
|
|
121
|
+
|
|
122
|
+
fs.mkdirSync(tuiBinDir, { recursive: true });
|
|
123
|
+
downloadFile(url, tuiBinPath);
|
|
124
|
+
fs.chmodSync(tuiBinPath, 0o755);
|
|
125
|
+
process.stdout.write(`ghost-tab-tui ${version} installed\n`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Synchronous HTTPS download with redirect following.
|
|
129
|
+
function downloadFile(url, dest) {
|
|
130
|
+
try {
|
|
131
|
+
execFileSync('curl', ['-fsSL', '-o', dest, url], {
|
|
132
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
133
|
+
});
|
|
134
|
+
} catch (_) {
|
|
135
|
+
process.stderr.write(`Failed to download ${url}\n`);
|
|
136
|
+
process.stderr.write('Check your network connection and that this version has been released.\n');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
main();
|