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.
Files changed (42) hide show
  1. package/README.md +151 -0
  2. package/VERSION +1 -0
  3. package/bin/ghost-tab +360 -0
  4. package/bin/npx-ghost-tab.js +141 -0
  5. package/ghostty/config +5 -0
  6. package/lib/ai-select-tui.sh +66 -0
  7. package/lib/ai-tools.sh +19 -0
  8. package/lib/config-tui.sh +95 -0
  9. package/lib/ghostty-config.sh +26 -0
  10. package/lib/input.sh +39 -0
  11. package/lib/install.sh +224 -0
  12. package/lib/loading.sh +190 -0
  13. package/lib/menu-tui.sh +189 -0
  14. package/lib/notification-setup.sh +210 -0
  15. package/lib/process.sh +13 -0
  16. package/lib/project-actions-tui.sh +29 -0
  17. package/lib/project-actions.sh +9 -0
  18. package/lib/projects.sh +18 -0
  19. package/lib/settings-json.sh +178 -0
  20. package/lib/settings-menu-tui.sh +32 -0
  21. package/lib/setup.sh +16 -0
  22. package/lib/statusline-setup.sh +60 -0
  23. package/lib/statusline.sh +31 -0
  24. package/lib/tab-title-watcher.sh +118 -0
  25. package/lib/terminal-select-tui.sh +119 -0
  26. package/lib/terminals/adapter.sh +19 -0
  27. package/lib/terminals/ghostty.sh +58 -0
  28. package/lib/terminals/iterm2.sh +51 -0
  29. package/lib/terminals/kitty.sh +40 -0
  30. package/lib/terminals/registry.sh +37 -0
  31. package/lib/terminals/wezterm.sh +50 -0
  32. package/lib/tmux-session.sh +49 -0
  33. package/lib/tui.sh +80 -0
  34. package/lib/update.sh +52 -0
  35. package/package.json +42 -0
  36. package/templates/ccstatusline-settings.json +29 -0
  37. package/templates/statusline-command.sh +30 -0
  38. package/templates/statusline-wrapper.sh +40 -0
  39. package/terminals/ghostty/config +5 -0
  40. package/terminals/kitty/config +1 -0
  41. package/terminals/wezterm/config.lua +4 -0
  42. 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
+ ![ghost-tab screenshot](docs/screenshot.png)
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();
package/ghostty/config ADDED
@@ -0,0 +1,5 @@
1
+ keybind = cmd+shift+left=previous_tab
2
+ keybind = cmd+shift+right=next_tab
3
+ keybind = cmd+t=new_tab
4
+ macos-option-as-alt = left
5
+ command = ~/.config/ghost-tab/wrapper.sh