@humanu/orchestra 0.5.2 → 0.5.3
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/install.js +151 -102
- package/package.json +5 -4
- package/resources/api/git.sh +359 -0
- package/resources/api/tmux.sh +1266 -0
- package/resources/prebuilt/macos-arm64/orchestra +0 -0
- package/resources/prebuilt/macos-intel/orchestra +0 -0
- package/resources/scripts/commands.sh +227 -0
- package/resources/scripts/gw-bridge.sh +1184 -0
- package/resources/scripts/gw.sh +148 -0
- package/resources/scripts/gwr.sh +171 -0
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
###############################################################################
|
|
4
|
+
# gw-bridge.sh – API bridge between Rust TUI and bash APIs
|
|
5
|
+
# ---------------------------------------------------------------------------
|
|
6
|
+
# This script sources the existing git.sh and tmux.sh APIs and exposes them
|
|
7
|
+
# via a JSON interface for the Rust TUI application to consume.
|
|
8
|
+
###############################################################################
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
# Source the API modules
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
source "$SCRIPT_DIR/api/git.sh"
|
|
15
|
+
source "$SCRIPT_DIR/api/tmux.sh"
|
|
16
|
+
|
|
17
|
+
# Define utilities we need
|
|
18
|
+
err() { printf '❌ %s\n' "$*" >&2; }
|
|
19
|
+
info() { printf '%s\n' "$*"; }
|
|
20
|
+
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
|
21
|
+
|
|
22
|
+
# Check if jq is available for JSON processing
|
|
23
|
+
if ! have_cmd jq; then
|
|
24
|
+
err "jq is required for JSON processing but not installed"
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Helper function to output JSON safely
|
|
29
|
+
json_output() {
|
|
30
|
+
if [[ $# -eq 0 ]]; then
|
|
31
|
+
echo "null"
|
|
32
|
+
else
|
|
33
|
+
printf '%s\n' "$@" | jq -R -s 'split("\n")[:-1]'
|
|
34
|
+
fi
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Extract a concise summary line from git output for error dialogs
|
|
38
|
+
git_error_summary() {
|
|
39
|
+
local text="${1-}"
|
|
40
|
+
local trimmed=""
|
|
41
|
+
local candidate=""
|
|
42
|
+
local fallback=""
|
|
43
|
+
while IFS= read -r line; do
|
|
44
|
+
line="${line%$'\r'}" # drop trailing carriage returns
|
|
45
|
+
trimmed="${line#${line%%[![:space:]]*}}"
|
|
46
|
+
if [[ -z "${trimmed//[[:space:]]/}" ]]; then
|
|
47
|
+
continue
|
|
48
|
+
fi
|
|
49
|
+
local is_hint=0
|
|
50
|
+
if [[ $trimmed == hint:* || $trimmed == Hint:* ]]; then
|
|
51
|
+
is_hint=1
|
|
52
|
+
fi
|
|
53
|
+
if [[ $is_hint -eq 0 ]]; then
|
|
54
|
+
fallback="$trimmed"
|
|
55
|
+
elif [[ -z "$fallback" ]]; then
|
|
56
|
+
fallback="$trimmed"
|
|
57
|
+
fi
|
|
58
|
+
if [[ $is_hint -eq 0 ]]; then
|
|
59
|
+
case "$trimmed" in
|
|
60
|
+
fatal:*|Fatal:*|error:*|Error:*|CONFLICT*|Conflict*|\
|
|
61
|
+
*merge\ failed*|*Merge\ failed*|\
|
|
62
|
+
*could\ not\ apply*)
|
|
63
|
+
candidate="$trimmed"
|
|
64
|
+
;;
|
|
65
|
+
esac
|
|
66
|
+
fi
|
|
67
|
+
done <<<"$text"
|
|
68
|
+
if [[ -n "$candidate" ]]; then
|
|
69
|
+
printf '%s' "$candidate"
|
|
70
|
+
else
|
|
71
|
+
printf '%s' "$fallback"
|
|
72
|
+
fi
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Helper function to output structured worktree data
|
|
76
|
+
json_worktrees() {
|
|
77
|
+
git_list_worktrees | jq -R -s '
|
|
78
|
+
split("\n")[:-1] |
|
|
79
|
+
map(split("\t")) |
|
|
80
|
+
map({
|
|
81
|
+
path: .[0],
|
|
82
|
+
branch: .[1],
|
|
83
|
+
sha: .[2]
|
|
84
|
+
})'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Helper function to load Anthropic API key from config or .env file
|
|
88
|
+
load_anthropic_api_key() {
|
|
89
|
+
# First try to load from ~/.orchestra/config.json
|
|
90
|
+
local config_file="$HOME/.orchestra/config.json"
|
|
91
|
+
if [[ -f "$config_file" ]] && have_cmd jq; then
|
|
92
|
+
local api_key
|
|
93
|
+
api_key="$(jq -r '.anthropic_api_key // empty' "$config_file" 2>/dev/null)"
|
|
94
|
+
if [[ -n "$api_key" ]] && [[ "$api_key" != "null" ]]; then
|
|
95
|
+
export ANTHROPIC_API_KEY="$api_key"
|
|
96
|
+
return 0
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# Fallback to .env file in current directory
|
|
101
|
+
if [[ -f ".env" ]]; then
|
|
102
|
+
source .env
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Fallback to .env file in repo root
|
|
106
|
+
if [[ -z "${ANTHROPIC_API_KEY-}" ]]; then
|
|
107
|
+
local repo_root
|
|
108
|
+
repo_root="$(git_repo_root 2>/dev/null)"
|
|
109
|
+
if [[ -n "$repo_root" ]] && [[ -f "$repo_root/.env" ]]; then
|
|
110
|
+
source "$repo_root/.env"
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Helper function to output error
|
|
116
|
+
json_error() {
|
|
117
|
+
local message="$1"
|
|
118
|
+
jq -n --arg msg "$message" '{"error": $msg}'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Main command dispatcher
|
|
122
|
+
case "${1:-}" in
|
|
123
|
+
"list-worktrees")
|
|
124
|
+
if git_require_repo_root >/dev/null 2>&1; then
|
|
125
|
+
json_worktrees
|
|
126
|
+
else
|
|
127
|
+
json_error "Not a git repository"
|
|
128
|
+
fi
|
|
129
|
+
;;
|
|
130
|
+
|
|
131
|
+
"list-sessions")
|
|
132
|
+
if [[ -z "${2:-}" ]]; then
|
|
133
|
+
json_error "Slug parameter required"
|
|
134
|
+
exit 1
|
|
135
|
+
fi
|
|
136
|
+
slug="$2"
|
|
137
|
+
# Optional third parameter for worktree path
|
|
138
|
+
worktree_path="${3:-}"
|
|
139
|
+
|
|
140
|
+
if tmux_available; then
|
|
141
|
+
if [[ -n "$worktree_path" ]]; then
|
|
142
|
+
sessions_output="$(tmux_list_sessions_for_slug "$slug" "$worktree_path")"
|
|
143
|
+
else
|
|
144
|
+
sessions_output="$(tmux_list_sessions_for_slug "$slug")"
|
|
145
|
+
fi
|
|
146
|
+
if [[ -n "$sessions_output" ]]; then
|
|
147
|
+
printf '%s\n' "$sessions_output" | jq -R -s 'split("\n")[:-1]'
|
|
148
|
+
else
|
|
149
|
+
echo "null"
|
|
150
|
+
fi
|
|
151
|
+
else
|
|
152
|
+
echo "null"
|
|
153
|
+
fi
|
|
154
|
+
;;
|
|
155
|
+
|
|
156
|
+
"create-session")
|
|
157
|
+
if [[ -z "${2:-}" ]]; then
|
|
158
|
+
json_error "Slug required"
|
|
159
|
+
exit 1
|
|
160
|
+
fi
|
|
161
|
+
slug="$2"
|
|
162
|
+
name="${3:-}"
|
|
163
|
+
path="${4:-$(pwd)}"
|
|
164
|
+
|
|
165
|
+
if tmux_available; then
|
|
166
|
+
session_name="$(tmux_ensure_session "$slug" "$name" "$path")"
|
|
167
|
+
echo "\"$session_name\""
|
|
168
|
+
else
|
|
169
|
+
json_error "tmux not available"
|
|
170
|
+
fi
|
|
171
|
+
;;
|
|
172
|
+
|
|
173
|
+
"create-session-exact")
|
|
174
|
+
if [[ -z "${2:-}" ]]; then
|
|
175
|
+
json_error "Session name required"
|
|
176
|
+
exit 1
|
|
177
|
+
fi
|
|
178
|
+
session_name="$2"
|
|
179
|
+
path="${3:-$(pwd)}"
|
|
180
|
+
if tmux_available; then
|
|
181
|
+
created="$(tmux_create_session "$session_name" "$path")" || { json_error "Failed to create exact session"; exit 1; }
|
|
182
|
+
echo "\"$created\""
|
|
183
|
+
else
|
|
184
|
+
json_error "tmux not available"
|
|
185
|
+
fi
|
|
186
|
+
;;
|
|
187
|
+
|
|
188
|
+
"kill-session")
|
|
189
|
+
if [[ -z "${2:-}" ]]; then
|
|
190
|
+
json_error "Session name required"
|
|
191
|
+
exit 1
|
|
192
|
+
fi
|
|
193
|
+
session_name="$2"
|
|
194
|
+
|
|
195
|
+
if tmux_available; then
|
|
196
|
+
if tmux_kill_session "$session_name"; then
|
|
197
|
+
echo "true"
|
|
198
|
+
else
|
|
199
|
+
json_error "Failed to kill session"
|
|
200
|
+
fi
|
|
201
|
+
else
|
|
202
|
+
json_error "tmux not available"
|
|
203
|
+
fi
|
|
204
|
+
;;
|
|
205
|
+
|
|
206
|
+
"attach-session")
|
|
207
|
+
if [[ -z "${2:-}" ]]; then
|
|
208
|
+
json_error "Session name required"
|
|
209
|
+
exit 1
|
|
210
|
+
fi
|
|
211
|
+
session_name="$2"
|
|
212
|
+
|
|
213
|
+
if tmux_available; then
|
|
214
|
+
tmux_attach_session "$session_name"
|
|
215
|
+
echo "true"
|
|
216
|
+
else
|
|
217
|
+
json_error "tmux not available"
|
|
218
|
+
fi
|
|
219
|
+
;;
|
|
220
|
+
|
|
221
|
+
"tmux-send-keys")
|
|
222
|
+
if [[ -z "${2:-}" ]]; then
|
|
223
|
+
json_error "Session name required"
|
|
224
|
+
exit 1
|
|
225
|
+
fi
|
|
226
|
+
session_name="$2"; shift 2 || true
|
|
227
|
+
if tmux_available; then
|
|
228
|
+
tmux_send_keys "$session_name" "$*" || { json_error "Failed to send keys"; exit 1; }
|
|
229
|
+
echo '{"ok":true}'
|
|
230
|
+
else
|
|
231
|
+
json_error "tmux not available"
|
|
232
|
+
fi
|
|
233
|
+
;;
|
|
234
|
+
|
|
235
|
+
"session-metadata")
|
|
236
|
+
if [[ -z "${2:-}" ]]; then
|
|
237
|
+
json_error "Session name required"
|
|
238
|
+
exit 1
|
|
239
|
+
fi
|
|
240
|
+
session_name="$2"
|
|
241
|
+
|
|
242
|
+
if tmux_available; then
|
|
243
|
+
# Get tmux metadata for context detection
|
|
244
|
+
window_name=$(tmux display-message -t "$session_name" -p '#{window_name}' 2>/dev/null || echo "")
|
|
245
|
+
pane_title=$(tmux display-message -t "$session_name" -p '#{pane_title}' 2>/dev/null || echo "")
|
|
246
|
+
pane_cmd=$(tmux display-message -t "$session_name" -p '#{pane_current_command}' 2>/dev/null || echo "")
|
|
247
|
+
|
|
248
|
+
# Return JSON with metadata
|
|
249
|
+
jq -n --arg window "$window_name" --arg title "$pane_title" --arg cmd "$pane_cmd" \
|
|
250
|
+
'{window_name: $window, pane_title: $title, current_command: $cmd}'
|
|
251
|
+
else
|
|
252
|
+
json_error "tmux not available"
|
|
253
|
+
fi
|
|
254
|
+
;;
|
|
255
|
+
|
|
256
|
+
"session-preview")
|
|
257
|
+
if [[ -z "${2:-}" ]]; then
|
|
258
|
+
json_error "Session name required"
|
|
259
|
+
exit 1
|
|
260
|
+
fi
|
|
261
|
+
session_name="$2"
|
|
262
|
+
|
|
263
|
+
if tmux_available; then
|
|
264
|
+
preview="$(tmux_session_preview "$session_name")"
|
|
265
|
+
printf '%s\n' "$preview" | jq -R -s .
|
|
266
|
+
else
|
|
267
|
+
echo "\"tmux not available\""
|
|
268
|
+
fi
|
|
269
|
+
;;
|
|
270
|
+
|
|
271
|
+
"ai-generate-name-from-base64")
|
|
272
|
+
if [[ -z "${2:-}" ]]; then
|
|
273
|
+
json_error "Base64 content required"
|
|
274
|
+
exit 1
|
|
275
|
+
fi
|
|
276
|
+
content_b64="$2"
|
|
277
|
+
load_anthropic_api_key
|
|
278
|
+
if [[ -z "${ANTHROPIC_API_KEY-}" ]]; then
|
|
279
|
+
echo "\"missing_api_key\""
|
|
280
|
+
exit 0
|
|
281
|
+
fi
|
|
282
|
+
# Build request via Python to ensure proper JSON encoding
|
|
283
|
+
request_body=$(python3 - "$content_b64" <<'PY'
|
|
284
|
+
import sys, json, base64
|
|
285
|
+
b64 = sys.argv[1]
|
|
286
|
+
try:
|
|
287
|
+
content = base64.b64decode(b64.encode('utf-8')).decode('utf-8','ignore')
|
|
288
|
+
except Exception:
|
|
289
|
+
content = ''
|
|
290
|
+
prompt = f"""Analyze this terminal session and create a descriptive name based ONLY on the console output and commands executed.
|
|
291
|
+
|
|
292
|
+
Terminal output:
|
|
293
|
+
{content}
|
|
294
|
+
|
|
295
|
+
IMPORTANT: Look carefully at the terminal output for:
|
|
296
|
+
1. Commands that were executed (lines starting with $ or > or commands after prompts)
|
|
297
|
+
2. Application output patterns (errors, build messages, server logs)
|
|
298
|
+
3. Tool-specific output (git commands, npm/yarn, docker, ssh, etc.)
|
|
299
|
+
4. AI coding assistant interactions (Claude Code, OpenCode, Cursor, etc.)
|
|
300
|
+
5. Git-specific patterns and outputs
|
|
301
|
+
|
|
302
|
+
CRITICAL GIT DETECTION: Check for ANY of these Git indicators in the last outputs:
|
|
303
|
+
- Git commands: git status, git diff, git add, git commit, git push, git pull, git checkout, git branch, git merge, git rebase, git log, git stash, etc.
|
|
304
|
+
- Git output patterns: \"On branch\", \"Your branch is\", \"Changes not staged\", \"Changes to be committed\", \"Untracked files\", \"modified:\", \"deleted:\", \"new file:\", \"renamed:\"
|
|
305
|
+
- Git diff output: Lines starting with +, -, @@, diff --git
|
|
306
|
+
- Git merge/rebase messages: \"CONFLICT\", \"Merge branch\", \"Rebase\", \"Cherry-pick\"
|
|
307
|
+
- Git status indicators: \"nothing to commit\", \"working tree clean\", \"ahead of\", \"behind\"
|
|
308
|
+
- Git error messages: \"fatal:\", \"error:\", \"warning:\" (when preceded by git commands)
|
|
309
|
+
|
|
310
|
+
Use these prefixes based on detected application/context (PRIORITIZE git_ if Git patterns found):
|
|
311
|
+
- git_ : ANY Git operations or Git output patterns detected (HIGHEST PRIORITY if found)
|
|
312
|
+
- opencode_ : If you see Claude Code, OpenCode, Cursor, AI assistant interactions, or AI-powered coding
|
|
313
|
+
- running_ : Dev servers (npm run dev, yarn dev, python manage.py runserver, rails server, etc.)
|
|
314
|
+
- build_ : Build processes (npm run build, cargo build, make, webpack, etc.)
|
|
315
|
+
- test_ : Testing (npm test, pytest, jest, cargo test, etc.)
|
|
316
|
+
- docker_ : Docker commands, container operations
|
|
317
|
+
- ssh_ : SSH connections, remote operations
|
|
318
|
+
- db_ : Database operations (psql, mysql, mongo, migrations)
|
|
319
|
+
- debug_ : Debugging sessions, error investigation
|
|
320
|
+
- deploy_ : Deployment operations
|
|
321
|
+
- Otherwise, no prefix for general development
|
|
322
|
+
|
|
323
|
+
If Git patterns are detected, ALWAYS use git_ prefix, even if other activities are present.
|
|
324
|
+
|
|
325
|
+
Describe what the user was actually doing based on the console output, not just the current state.
|
|
326
|
+
|
|
327
|
+
The name should be max 100 chars total, use underscores only (no colons or special chars).
|
|
328
|
+
|
|
329
|
+
Respond with ONLY the session name, nothing else."""
|
|
330
|
+
|
|
331
|
+
req = {
|
|
332
|
+
"model": "claude-3-5-haiku-latest",
|
|
333
|
+
"max_tokens": 100,
|
|
334
|
+
"messages": [{"role":"user","content": prompt}]
|
|
335
|
+
}
|
|
336
|
+
print(json.dumps(req))
|
|
337
|
+
PY
|
|
338
|
+
)
|
|
339
|
+
response=$(curl -s -X POST https://api.anthropic.com/v1/messages \
|
|
340
|
+
--max-time 20 \
|
|
341
|
+
-H "Content-Type: application/json" \
|
|
342
|
+
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
|
343
|
+
-H "anthropic-version: 2023-06-01" \
|
|
344
|
+
-d "$request_body" 2>/dev/null)
|
|
345
|
+
# Extract text
|
|
346
|
+
new_name=$(echo "$response" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('content',[{}])[0].get('text',''))" 2>/dev/null)
|
|
347
|
+
# Clean and truncate
|
|
348
|
+
new_name=$(echo "$new_name" | tr ' ' '_' | sed 's/[^a-zA-Z0-9_-]//g' | cut -c1-100)
|
|
349
|
+
if [[ -z "$new_name" ]]; then
|
|
350
|
+
echo "\"ai_generation_failed\""
|
|
351
|
+
else
|
|
352
|
+
printf '"%s"\n' "$new_name"
|
|
353
|
+
fi
|
|
354
|
+
;;
|
|
355
|
+
|
|
356
|
+
"rename-session")
|
|
357
|
+
if [[ -z "${2:-}" ]]; then
|
|
358
|
+
json_error "Session name required"
|
|
359
|
+
exit 1
|
|
360
|
+
fi
|
|
361
|
+
original_session_name="$2"
|
|
362
|
+
|
|
363
|
+
# Load API key from config file or fallback to .env
|
|
364
|
+
load_anthropic_api_key
|
|
365
|
+
|
|
366
|
+
if [[ -z "${ANTHROPIC_API_KEY-}" ]]; then
|
|
367
|
+
echo "\"missing_api_key\""
|
|
368
|
+
exit 0
|
|
369
|
+
fi
|
|
370
|
+
|
|
371
|
+
# Check if tmux is available and session exists
|
|
372
|
+
if ! tmux_available; then
|
|
373
|
+
echo "\"tmux_not_available\""
|
|
374
|
+
exit 0
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
if ! tmux_session_exists "$original_session_name"; then
|
|
378
|
+
echo "\"session_not_found\""
|
|
379
|
+
exit 0
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
# Generate AI name using the CURRENT session (no renaming to temp name)
|
|
383
|
+
ai_name="$(tmux_generate_ai_session_name "$original_session_name" 2>/dev/null)"
|
|
384
|
+
|
|
385
|
+
if [[ -n "$ai_name" ]] && [[ "$ai_name" != *"error"* ]] && [[ "$ai_name" != *"Failed"* ]]; then
|
|
386
|
+
# Clean the AI name for session naming
|
|
387
|
+
ai_name="$(echo "$ai_name" | tr ' ' '_' | sed 's/[^a-zA-Z0-9_-]//g' | cut -c1-100)"
|
|
388
|
+
|
|
389
|
+
# Remove any prefix from AI name to avoid duplication
|
|
390
|
+
ai_name="$(echo "$ai_name" | sed -E 's/^(opencode|running|ssh|docker|k8s|git|db)_//')"
|
|
391
|
+
|
|
392
|
+
# Remove any leading underscores
|
|
393
|
+
ai_name="$(echo "$ai_name" | sed 's/^_*//')"
|
|
394
|
+
|
|
395
|
+
# Perform the rename preserving canonical prefix and delimiter
|
|
396
|
+
if tmux_rename_session "$original_session_name" "$ai_name" >/dev/null; then
|
|
397
|
+
echo "\"success\""
|
|
398
|
+
else
|
|
399
|
+
echo "\"rename_failed\""
|
|
400
|
+
fi
|
|
401
|
+
else
|
|
402
|
+
# AI generation failed, keep original name
|
|
403
|
+
echo "\"ai_generation_failed\""
|
|
404
|
+
fi
|
|
405
|
+
;;
|
|
406
|
+
|
|
407
|
+
"manual-rename-session")
|
|
408
|
+
if [[ -z "${2:-}" ]] || [[ -z "${3:-}" ]]; then
|
|
409
|
+
json_error "Old and new session names required"
|
|
410
|
+
exit 1
|
|
411
|
+
fi
|
|
412
|
+
old_name="$2"
|
|
413
|
+
new_display_name="$3"
|
|
414
|
+
|
|
415
|
+
if ! tmux_available; then
|
|
416
|
+
echo "\"tmux_not_available\""
|
|
417
|
+
exit 0
|
|
418
|
+
fi
|
|
419
|
+
|
|
420
|
+
# Check if session exists
|
|
421
|
+
if ! tmux has-session -t "$old_name" 2>/dev/null; then
|
|
422
|
+
echo "\"session_not_found\""
|
|
423
|
+
exit 0
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
# Validate new display name (alphanumeric and underscore only)
|
|
427
|
+
if [[ ! "$new_display_name" =~ ^[a-zA-Z0-9_]+$ ]]; then
|
|
428
|
+
echo "\"invalid_name\""
|
|
429
|
+
exit 0
|
|
430
|
+
fi
|
|
431
|
+
|
|
432
|
+
# Perform rename via tmux API to preserve delimiter and canonical prefix
|
|
433
|
+
if tmux_rename_session "$old_name" "$new_display_name" >/dev/null; then
|
|
434
|
+
echo "\"success\""
|
|
435
|
+
else
|
|
436
|
+
echo "\"rename_failed\""
|
|
437
|
+
fi
|
|
438
|
+
;;
|
|
439
|
+
|
|
440
|
+
"check-branch")
|
|
441
|
+
if [[ -z "${2:-}" ]]; then
|
|
442
|
+
json_error "Branch name required"
|
|
443
|
+
exit 1
|
|
444
|
+
fi
|
|
445
|
+
branch_name="$2"
|
|
446
|
+
|
|
447
|
+
if git_require_repo_root >/dev/null 2>&1; then
|
|
448
|
+
local_exists="false"
|
|
449
|
+
if git_branch_exists "$branch_name"; then
|
|
450
|
+
local_exists="true"
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
remote_exists="false"
|
|
454
|
+
remote_error=""
|
|
455
|
+
remote_name="origin"
|
|
456
|
+
root="$(git_repo_root)"
|
|
457
|
+
|
|
458
|
+
if (cd "$root" && git remote get-url "$remote_name" >/dev/null 2>&1); then
|
|
459
|
+
remote_output="$(cd "$root" && git ls-remote --heads "$remote_name" "$branch_name" 2>&1)"
|
|
460
|
+
remote_status=$?
|
|
461
|
+
if [[ $remote_status -eq 0 ]]; then
|
|
462
|
+
if [[ -n "$remote_output" ]]; then
|
|
463
|
+
remote_exists="true"
|
|
464
|
+
fi
|
|
465
|
+
else
|
|
466
|
+
remote_error="$remote_output"
|
|
467
|
+
fi
|
|
468
|
+
else
|
|
469
|
+
remote_error="Remote '$remote_name' not configured"
|
|
470
|
+
fi
|
|
471
|
+
|
|
472
|
+
if [[ -n "$remote_error" ]]; then
|
|
473
|
+
remote_error="$(printf '%s' "$remote_error" | tr '\n' ' ')"
|
|
474
|
+
fi
|
|
475
|
+
|
|
476
|
+
jq -n \
|
|
477
|
+
--argjson local "$local_exists" \
|
|
478
|
+
--argjson remote "$remote_exists" \
|
|
479
|
+
--arg remote_error "$remote_error" \
|
|
480
|
+
'{local_exists:$local, remote_exists:$remote} + (if ($remote_error | length) > 0 then {remote_error:$remote_error} else {} end)'
|
|
481
|
+
else
|
|
482
|
+
json_error "Not a git repository"
|
|
483
|
+
fi
|
|
484
|
+
;;
|
|
485
|
+
|
|
486
|
+
"create-worktree")
|
|
487
|
+
if [[ -z "${2:-}" ]]; then
|
|
488
|
+
json_error "Branch name required"
|
|
489
|
+
exit 1
|
|
490
|
+
fi
|
|
491
|
+
branch_name="$2"
|
|
492
|
+
|
|
493
|
+
if git_require_repo_root >/dev/null 2>&1; then
|
|
494
|
+
worktree_path="$(git_create_branch_and_worktree "$branch_name")"
|
|
495
|
+
echo "\"$worktree_path\""
|
|
496
|
+
else
|
|
497
|
+
json_error "Not a git repository"
|
|
498
|
+
fi
|
|
499
|
+
;;
|
|
500
|
+
|
|
501
|
+
"create-worktree-from-existing")
|
|
502
|
+
if [[ -z "${2:-}" ]]; then
|
|
503
|
+
json_error "Branch name required"
|
|
504
|
+
exit 1
|
|
505
|
+
fi
|
|
506
|
+
branch_name="$2"
|
|
507
|
+
|
|
508
|
+
if git_require_repo_root >/dev/null 2>&1; then
|
|
509
|
+
if ! git_branch_exists "$branch_name"; then
|
|
510
|
+
json_error "Branch does not exist: $branch_name"
|
|
511
|
+
exit 1
|
|
512
|
+
fi
|
|
513
|
+
|
|
514
|
+
if git_worktree_exists_for_branch "$branch_name"; then
|
|
515
|
+
json_error "Worktree already exists for branch: $branch_name"
|
|
516
|
+
exit 1
|
|
517
|
+
fi
|
|
518
|
+
|
|
519
|
+
if worktree_path="$(git_create_worktree_for_existing_branch "$branch_name")"; then
|
|
520
|
+
echo "\"$worktree_path\""
|
|
521
|
+
else
|
|
522
|
+
json_error "Failed to create worktree for branch: $branch_name"
|
|
523
|
+
fi
|
|
524
|
+
else
|
|
525
|
+
json_error "Not a git repository"
|
|
526
|
+
fi
|
|
527
|
+
;;
|
|
528
|
+
|
|
529
|
+
"create-worktree-from-remote")
|
|
530
|
+
if [[ -z "${2:-}" ]]; then
|
|
531
|
+
json_error "Branch name required"
|
|
532
|
+
exit 1
|
|
533
|
+
fi
|
|
534
|
+
branch_name="$2"
|
|
535
|
+
remote_name="${3:-origin}"
|
|
536
|
+
|
|
537
|
+
if git_require_repo_root >/dev/null 2>&1; then
|
|
538
|
+
if git_worktree_exists_for_branch "$branch_name"; then
|
|
539
|
+
json_error "Worktree already exists for branch: $branch_name"
|
|
540
|
+
exit 1
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
if worktree_path="$(git_create_worktree_from_remote_branch "$branch_name" "$remote_name")"; then
|
|
544
|
+
echo "\"$worktree_path\""
|
|
545
|
+
else
|
|
546
|
+
json_error "Failed to create worktree from remote branch: $remote_name/$branch_name"
|
|
547
|
+
fi
|
|
548
|
+
else
|
|
549
|
+
json_error "Not a git repository"
|
|
550
|
+
fi
|
|
551
|
+
;;
|
|
552
|
+
|
|
553
|
+
"delete-worktree")
|
|
554
|
+
if [[ -z "${2:-}" ]]; then
|
|
555
|
+
json_error "Branch name required"
|
|
556
|
+
exit 1
|
|
557
|
+
fi
|
|
558
|
+
branch_name="$2"
|
|
559
|
+
|
|
560
|
+
if git_require_repo_root >/dev/null 2>&1; then
|
|
561
|
+
worktree_path="$(git_branch_to_worktree_path "$branch_name")"
|
|
562
|
+
|
|
563
|
+
if [[ -n "$worktree_path" ]]; then
|
|
564
|
+
git_remove_worktree "$worktree_path"
|
|
565
|
+
fi
|
|
566
|
+
|
|
567
|
+
if git_branch_exists "$branch_name"; then
|
|
568
|
+
git_delete_branch "$branch_name"
|
|
569
|
+
fi
|
|
570
|
+
|
|
571
|
+
echo "true"
|
|
572
|
+
else
|
|
573
|
+
json_error "Not a git repository"
|
|
574
|
+
fi
|
|
575
|
+
;;
|
|
576
|
+
|
|
577
|
+
"delete-worktree-only")
|
|
578
|
+
if [[ -z "${2:-}" ]]; then
|
|
579
|
+
json_error "Branch name required"
|
|
580
|
+
exit 1
|
|
581
|
+
fi
|
|
582
|
+
branch_name="$2"
|
|
583
|
+
|
|
584
|
+
if git_require_repo_root >/dev/null 2>&1; then
|
|
585
|
+
worktree_path="$(git_branch_to_worktree_path "$branch_name")"
|
|
586
|
+
|
|
587
|
+
if [[ -n "$worktree_path" ]]; then
|
|
588
|
+
git_remove_worktree "$worktree_path"
|
|
589
|
+
echo "true"
|
|
590
|
+
else
|
|
591
|
+
json_error "No worktree found for branch: $branch_name"
|
|
592
|
+
fi
|
|
593
|
+
else
|
|
594
|
+
json_error "Not a git repository"
|
|
595
|
+
fi
|
|
596
|
+
;;
|
|
597
|
+
|
|
598
|
+
"delete-branch-only")
|
|
599
|
+
if [[ -z "${2:-}" ]]; then
|
|
600
|
+
json_error "Branch name required"
|
|
601
|
+
exit 1
|
|
602
|
+
fi
|
|
603
|
+
branch_name="$2"
|
|
604
|
+
|
|
605
|
+
if git_require_repo_root >/dev/null 2>&1; then
|
|
606
|
+
if git_branch_exists "$branch_name"; then
|
|
607
|
+
git_delete_branch "$branch_name"
|
|
608
|
+
echo "true"
|
|
609
|
+
else
|
|
610
|
+
json_error "Branch does not exist: $branch_name"
|
|
611
|
+
fi
|
|
612
|
+
else
|
|
613
|
+
json_error "Not a git repository"
|
|
614
|
+
fi
|
|
615
|
+
;;
|
|
616
|
+
|
|
617
|
+
"switch-worktree")
|
|
618
|
+
if [[ -z "${2:-}" ]]; then
|
|
619
|
+
json_error "Path required"
|
|
620
|
+
exit 1
|
|
621
|
+
fi
|
|
622
|
+
path="$2"
|
|
623
|
+
echo "\"cd \\\"$path\\\"\""
|
|
624
|
+
;;
|
|
625
|
+
|
|
626
|
+
"repo-info")
|
|
627
|
+
if git_require_repo_root >/dev/null 2>&1; then
|
|
628
|
+
root="$(git_repo_root)"
|
|
629
|
+
current_path="$(git_current_path)"
|
|
630
|
+
current_branch="$(git_current_branch)"
|
|
631
|
+
current_commit="$(git_current_commit_short)"
|
|
632
|
+
|
|
633
|
+
jq -n \
|
|
634
|
+
--arg root "$root" \
|
|
635
|
+
--arg current_path "$current_path" \
|
|
636
|
+
--arg current_branch "$current_branch" \
|
|
637
|
+
--arg current_commit "$current_commit" \
|
|
638
|
+
'{
|
|
639
|
+
root: $root,
|
|
640
|
+
current_path: $current_path,
|
|
641
|
+
current_branch: $current_branch,
|
|
642
|
+
current_commit: $current_commit
|
|
643
|
+
}'
|
|
644
|
+
else
|
|
645
|
+
json_error "Not a git repository"
|
|
646
|
+
fi
|
|
647
|
+
;;
|
|
648
|
+
|
|
649
|
+
"git-status")
|
|
650
|
+
if git_require_repo_root >/dev/null 2>&1; then
|
|
651
|
+
status_output="$(git status --porcelain 2>/dev/null || echo "")"
|
|
652
|
+
echo "\"$status_output\""
|
|
653
|
+
else
|
|
654
|
+
json_error "Not a git repository"
|
|
655
|
+
fi
|
|
656
|
+
;;
|
|
657
|
+
|
|
658
|
+
"enhanced-git-status")
|
|
659
|
+
# Optional directory parameter - if provided, cd to that directory first
|
|
660
|
+
target_dir="${2:-}"
|
|
661
|
+
if [[ -n "$target_dir" ]]; then
|
|
662
|
+
if [[ ! -d "$target_dir" ]]; then
|
|
663
|
+
json_error "Directory does not exist: $target_dir"
|
|
664
|
+
exit 1
|
|
665
|
+
fi
|
|
666
|
+
cd "$target_dir" || {
|
|
667
|
+
json_error "Cannot change to directory: $target_dir"
|
|
668
|
+
exit 1
|
|
669
|
+
}
|
|
670
|
+
fi
|
|
671
|
+
|
|
672
|
+
if git_require_repo_root >/dev/null 2>&1; then
|
|
673
|
+
# Get porcelain status
|
|
674
|
+
status_lines="$(git status --porcelain=v1 2>/dev/null)"
|
|
675
|
+
|
|
676
|
+
# Get branch info
|
|
677
|
+
branch="$(git_current_branch)"
|
|
678
|
+
|
|
679
|
+
# Get ahead/behind info (handle cases where upstream doesn't exist)
|
|
680
|
+
ahead_behind="$(git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null || echo "0 0")"
|
|
681
|
+
ahead="$(echo "$ahead_behind" | cut -f1)"
|
|
682
|
+
behind="$(echo "$ahead_behind" | cut -f2)"
|
|
683
|
+
|
|
684
|
+
# Process each file and get diff stats
|
|
685
|
+
files_json="$(echo "$status_lines" | while IFS= read -r line; do
|
|
686
|
+
if [[ -n "$line" ]]; then
|
|
687
|
+
index_status="${line:0:1}"
|
|
688
|
+
workdir_status="${line:1:1}"
|
|
689
|
+
filepath="${line:3}"
|
|
690
|
+
|
|
691
|
+
# Get diff stats for the file
|
|
692
|
+
added=0
|
|
693
|
+
deleted=0
|
|
694
|
+
|
|
695
|
+
# Check for staged changes first
|
|
696
|
+
if [[ "$index_status" != " " && "$index_status" != "?" ]]; then
|
|
697
|
+
# Staged changes - use --cached
|
|
698
|
+
stats="$(git diff --cached --numstat "$filepath" 2>/dev/null | head -1)"
|
|
699
|
+
if [[ -n "$stats" && "$stats" != "- -"* ]]; then
|
|
700
|
+
added="$(echo "$stats" | cut -f1)"
|
|
701
|
+
deleted="$(echo "$stats" | cut -f2)"
|
|
702
|
+
# Handle binary files (shows -)
|
|
703
|
+
[[ "$added" == "-" ]] && added=0
|
|
704
|
+
[[ "$deleted" == "-" ]] && deleted=0
|
|
705
|
+
fi
|
|
706
|
+
fi
|
|
707
|
+
|
|
708
|
+
# Check for working directory changes if no staged stats or if workdir is also modified
|
|
709
|
+
if [[ "$workdir_status" != " " && "$workdir_status" != "?" ]] && [[ $added -eq 0 && $deleted -eq 0 ]]; then
|
|
710
|
+
# Working directory changes
|
|
711
|
+
stats="$(git diff --numstat "$filepath" 2>/dev/null | head -1)"
|
|
712
|
+
if [[ -n "$stats" && "$stats" != "- -"* ]]; then
|
|
713
|
+
added="$(echo "$stats" | cut -f1)"
|
|
714
|
+
deleted="$(echo "$stats" | cut -f2)"
|
|
715
|
+
# Handle binary files (shows -)
|
|
716
|
+
[[ "$added" == "-" ]] && added=0
|
|
717
|
+
[[ "$deleted" == "-" ]] && deleted=0
|
|
718
|
+
fi
|
|
719
|
+
fi
|
|
720
|
+
|
|
721
|
+
# For untracked files, try to count lines
|
|
722
|
+
if [[ "$index_status" == "?" && "$workdir_status" == "?" ]]; then
|
|
723
|
+
if [[ -f "$filepath" ]]; then
|
|
724
|
+
# Count lines in new file
|
|
725
|
+
line_count="$(wc -l < "$filepath" 2>/dev/null || echo "0")"
|
|
726
|
+
added="$line_count"
|
|
727
|
+
deleted=0
|
|
728
|
+
fi
|
|
729
|
+
fi
|
|
730
|
+
|
|
731
|
+
# Output JSON for this file
|
|
732
|
+
jq -n --arg path "$filepath" \
|
|
733
|
+
--arg idx "$index_status" \
|
|
734
|
+
--arg wd "$workdir_status" \
|
|
735
|
+
--argjson add "$added" \
|
|
736
|
+
--argjson del "$deleted" \
|
|
737
|
+
'{path: $path, index_status: $idx, workdir_status: $wd, lines_added: $add, lines_deleted: $del}'
|
|
738
|
+
fi
|
|
739
|
+
done | jq -s .)"
|
|
740
|
+
|
|
741
|
+
# Handle empty files array
|
|
742
|
+
if [[ -z "$files_json" || "$files_json" == "null" ]]; then
|
|
743
|
+
files_json="[]"
|
|
744
|
+
fi
|
|
745
|
+
|
|
746
|
+
# Combine into final JSON
|
|
747
|
+
jq -n --arg branch "$branch" \
|
|
748
|
+
--argjson ahead "$ahead" \
|
|
749
|
+
--argjson behind "$behind" \
|
|
750
|
+
--argjson files "$files_json" \
|
|
751
|
+
'{branch: $branch, ahead: $ahead, behind: $behind, files: $files}'
|
|
752
|
+
else
|
|
753
|
+
json_error "Not a git repository"
|
|
754
|
+
fi
|
|
755
|
+
;;
|
|
756
|
+
|
|
757
|
+
"primary-branch")
|
|
758
|
+
if pr=$(git_primary_branch); then
|
|
759
|
+
jq -n --arg primary "$pr" '{ok:true, primary:$primary}'
|
|
760
|
+
else
|
|
761
|
+
jq -n '{ok:false, error_type:"no_primary_detected", message:"Could not detect primary branch"}'
|
|
762
|
+
fi
|
|
763
|
+
;;
|
|
764
|
+
|
|
765
|
+
"merge-from-primary")
|
|
766
|
+
# Parse args: --worktree <path> --branch <name>
|
|
767
|
+
shift || true
|
|
768
|
+
wt=""; br=""
|
|
769
|
+
while [[ $# -gt 0 ]]; do
|
|
770
|
+
case "$1" in
|
|
771
|
+
--worktree) wt="$2"; shift 2;;
|
|
772
|
+
--branch) br="$2"; shift 2;;
|
|
773
|
+
*) shift;;
|
|
774
|
+
esac
|
|
775
|
+
done
|
|
776
|
+
if [[ -z "$wt" || -z "$br" ]]; then
|
|
777
|
+
json_error "--worktree and --branch required"; exit 1
|
|
778
|
+
fi
|
|
779
|
+
if ! git_require_repo_root >/dev/null 2>&1; then
|
|
780
|
+
json_error "Not a git repository"; exit 1
|
|
781
|
+
fi
|
|
782
|
+
pr="$(git_primary_branch || true)"
|
|
783
|
+
if [[ -z "$pr" ]]; then
|
|
784
|
+
jq -n --arg op "merge-from-primary" '{ok:false, operation:$op, error_type:"no_primary_detected", message:"Could not detect primary branch"}'
|
|
785
|
+
exit 0
|
|
786
|
+
fi
|
|
787
|
+
LOGS=""
|
|
788
|
+
append() { LOGS+="$1"$'\n'; }
|
|
789
|
+
# Fetch in target worktree
|
|
790
|
+
append "$ git -C $wt fetch origin --prune"
|
|
791
|
+
append "$(git_fetch_prune "$wt")"
|
|
792
|
+
# If primary worktree exists, update it fast-forward-only
|
|
793
|
+
pr_path="$(git_branch_to_worktree_path "$pr" || true)"
|
|
794
|
+
if [[ -n "$pr_path" ]]; then
|
|
795
|
+
if ! git_is_worktree_clean "$pr_path"; then
|
|
796
|
+
logs_json="$(printf '%s\n' "$LOGS" | jq -R -s 'split("\n")[:-1]')"
|
|
797
|
+
jq -n --arg op "merge-from-primary" --arg primary "$pr" --arg target_branch "$br" --arg target_path "$wt" \
|
|
798
|
+
--arg message "Primary worktree is dirty" --arg error_type "dirty_primary" --argjson logs "$logs_json" \
|
|
799
|
+
'{ok:false, operation:$op, error_type:$error_type, message:$message, primary:$primary, target_branch:$target_branch, target_path:$target_path, logs:$logs}'
|
|
800
|
+
exit 0
|
|
801
|
+
fi
|
|
802
|
+
append "$ git -C $pr_path pull --ff-only"
|
|
803
|
+
set +e
|
|
804
|
+
pull_out="$(git_pull_ff_only "$pr_path")"
|
|
805
|
+
sts=$?
|
|
806
|
+
set -e
|
|
807
|
+
append "$pull_out"
|
|
808
|
+
if [[ $sts -ne 0 ]]; then
|
|
809
|
+
logs_json="$(printf '%s\n' "$LOGS" | jq -R -s 'split("\n")[:-1]')"
|
|
810
|
+
jq -n --arg op "merge-from-primary" --arg primary "$pr" --arg target_branch "$br" --arg target_path "$wt" \
|
|
811
|
+
--arg message "Primary cannot fast-forward" --arg error_type "ff_only_failed" --argjson logs "$logs_json" \
|
|
812
|
+
'{ok:false, operation:$op, error_type:$error_type, message:$message, primary:$primary, target_branch:$target_branch, target_path:$target_path, logs:$logs}'
|
|
813
|
+
exit 0
|
|
814
|
+
fi
|
|
815
|
+
fi
|
|
816
|
+
# Choose ref to merge
|
|
817
|
+
if git -C "$wt" rev-parse --verify "origin/$pr" >/dev/null 2>&1; then
|
|
818
|
+
ref="origin/$pr"
|
|
819
|
+
else
|
|
820
|
+
ref="$pr"
|
|
821
|
+
fi
|
|
822
|
+
append "$ git -C $wt merge $ref"
|
|
823
|
+
set +e
|
|
824
|
+
out="$(git_merge_into "$wt" "$ref")"
|
|
825
|
+
rc=$?
|
|
826
|
+
set -e
|
|
827
|
+
append "$out"
|
|
828
|
+
summary="$(git_error_summary "$out")"
|
|
829
|
+
if [[ -z "$summary" ]]; then
|
|
830
|
+
summary="Merge conflict or error"
|
|
831
|
+
fi
|
|
832
|
+
logs_json="$(printf '%s\n' "$LOGS" | jq -R -s 'split("\n")[:-1]')"
|
|
833
|
+
if [[ $rc -eq 0 ]]; then
|
|
834
|
+
result="merged"
|
|
835
|
+
if echo "$out" | grep -qi 'already up[- ]to[- ]date'; then result="up_to_date"; fi
|
|
836
|
+
jq -n --arg op "merge-from-primary" --arg primary "$pr" --arg target_branch "$br" --arg target_path "$wt" --arg result "$result" --argjson logs "$logs_json" \
|
|
837
|
+
'{ok:true, operation:$op, primary:$primary, target_branch:$target_branch, target_path:$target_path, result:$result, logs:$logs}'
|
|
838
|
+
else
|
|
839
|
+
jq -n --arg op "merge-from-primary" --arg primary "$pr" --arg target_branch "$br" --arg target_path "$wt" \
|
|
840
|
+
--arg message "$summary" --arg error_type "merge_conflict" --argjson logs "$logs_json" \
|
|
841
|
+
'{ok:false, operation:$op, error_type:$error_type, message:$message, primary:$primary, target_branch:$target_branch, target_path:$target_path, logs:$logs}'
|
|
842
|
+
fi
|
|
843
|
+
;;
|
|
844
|
+
|
|
845
|
+
"rebase-from-primary")
|
|
846
|
+
shift || true
|
|
847
|
+
wt=""; br=""
|
|
848
|
+
while [[ $# -gt 0 ]]; do
|
|
849
|
+
case "$1" in
|
|
850
|
+
--worktree) wt="$2"; shift 2;;
|
|
851
|
+
--branch) br="$2"; shift 2;;
|
|
852
|
+
*) shift;;
|
|
853
|
+
esac
|
|
854
|
+
done
|
|
855
|
+
if [[ -z "$wt" || -z "$br" ]]; then
|
|
856
|
+
json_error "--worktree and --branch required"; exit 1
|
|
857
|
+
fi
|
|
858
|
+
if ! git_require_repo_root >/dev/null 2>&1; then
|
|
859
|
+
json_error "Not a git repository"; exit 1
|
|
860
|
+
fi
|
|
861
|
+
pr="$(git_primary_branch || true)"
|
|
862
|
+
if [[ -z "$pr" ]]; then
|
|
863
|
+
jq -n --arg op "rebase-from-primary" '{ok:false, operation:$op, error_type:"no_primary_detected", message:"Could not detect primary branch"}'
|
|
864
|
+
exit 0
|
|
865
|
+
fi
|
|
866
|
+
LOGS=""; append() { LOGS+="$1"$'\n'; }
|
|
867
|
+
append "$ git -C $wt fetch origin --prune"
|
|
868
|
+
append "$(git_fetch_prune "$wt")"
|
|
869
|
+
if git -C "$wt" rev-parse --verify "origin/$pr" >/dev/null 2>&1; then
|
|
870
|
+
upstream="origin/$pr"
|
|
871
|
+
else
|
|
872
|
+
upstream="$pr"
|
|
873
|
+
fi
|
|
874
|
+
append "$ git -C $wt rebase $upstream"
|
|
875
|
+
set +e
|
|
876
|
+
out="$(git_rebase_onto "$wt" "$upstream")"
|
|
877
|
+
rc=$?
|
|
878
|
+
set -e
|
|
879
|
+
append "$out"
|
|
880
|
+
summary="$(git_error_summary "$out")"
|
|
881
|
+
if [[ -z "$summary" ]]; then
|
|
882
|
+
summary="Rebase conflict or error"
|
|
883
|
+
fi
|
|
884
|
+
logs_json="$(printf '%s\n' "$LOGS" | jq -R -s 'split("\n")[:-1]')"
|
|
885
|
+
if [[ $rc -eq 0 ]]; then
|
|
886
|
+
result="rebased"
|
|
887
|
+
if echo "$out" | grep -qi 'up[- ]to[- ]date'; then result="up_to_date"; fi
|
|
888
|
+
jq -n --arg op "rebase-from-primary" --arg primary "$pr" --arg target_branch "$br" --arg target_path "$wt" --arg result "$result" --argjson logs "$logs_json" \
|
|
889
|
+
'{ok:true, operation:$op, primary:$primary, target_branch:$target_branch, target_path:$target_path, result:$result, logs:$logs}'
|
|
890
|
+
else
|
|
891
|
+
jq -n --arg op "rebase-from-primary" --arg primary "$pr" --arg target_branch "$br" --arg target_path "$wt" \
|
|
892
|
+
--arg message "$summary" --arg error_type "rebase_conflict" --argjson logs "$logs_json" \
|
|
893
|
+
'{ok:false, operation:$op, error_type:$error_type, message:$message, primary:$primary, target_branch:$target_branch, target_path:$target_path, logs:$logs}'
|
|
894
|
+
fi
|
|
895
|
+
;;
|
|
896
|
+
|
|
897
|
+
"merge-into-primary")
|
|
898
|
+
shift || true
|
|
899
|
+
wt=""; br=""; msg=""
|
|
900
|
+
while [[ $# -gt 0 ]]; do
|
|
901
|
+
case "$1" in
|
|
902
|
+
--worktree) wt="$2"; shift 2;;
|
|
903
|
+
--branch) br="$2"; shift 2;;
|
|
904
|
+
--message) msg="$2"; shift 2;;
|
|
905
|
+
*) shift;;
|
|
906
|
+
esac
|
|
907
|
+
done
|
|
908
|
+
if [[ -z "$wt" || -z "$br" ]]; then
|
|
909
|
+
json_error "--worktree and --branch required"; exit 1
|
|
910
|
+
fi
|
|
911
|
+
if ! git_require_repo_root >/dev/null 2>&1; then
|
|
912
|
+
json_error "Not a git repository"; exit 1
|
|
913
|
+
fi
|
|
914
|
+
pr="$(git_primary_branch || true)"
|
|
915
|
+
if [[ -z "$pr" ]]; then
|
|
916
|
+
jq -n --arg op "merge-into-primary" '{ok:false, operation:$op, error_type:"no_primary_detected", message:"Could not detect primary branch"}'
|
|
917
|
+
exit 0
|
|
918
|
+
fi
|
|
919
|
+
LOGS=""; append() { LOGS+="$1"$'\n'; }
|
|
920
|
+
pr_path="$(git_ensure_primary_worktree || true)"
|
|
921
|
+
if [[ -z "$pr_path" ]]; then
|
|
922
|
+
jq -n --arg op "merge-into-primary" '{ok:false, operation:$op, error_type:"ensure_primary_failed", message:"Failed to ensure primary worktree"}'
|
|
923
|
+
exit 0
|
|
924
|
+
fi
|
|
925
|
+
if ! git_is_worktree_clean "$pr_path"; then
|
|
926
|
+
jq -n --arg op "merge-into-primary" --arg primary "$pr" --arg message "Primary worktree is dirty" '{ok:false, operation:$op, error_type:"dirty_primary", message:$message, primary:$primary}'
|
|
927
|
+
exit 0
|
|
928
|
+
fi
|
|
929
|
+
append "$ git -C $pr_path fetch origin --prune"
|
|
930
|
+
append "$(git_fetch_prune "$pr_path")"
|
|
931
|
+
append "$ git -C $pr_path pull --ff-only"
|
|
932
|
+
set +e
|
|
933
|
+
pull_out="$(git_pull_ff_only "$pr_path")"
|
|
934
|
+
pull_rc=$?
|
|
935
|
+
set -e
|
|
936
|
+
append "$pull_out"
|
|
937
|
+
if [[ $pull_rc -ne 0 ]]; then
|
|
938
|
+
logs_json="$(printf '%s\n' "$LOGS" | jq -R -s 'split("\n")[:-1]')"
|
|
939
|
+
jq -n --arg op "merge-into-primary" --arg primary "$pr" --arg target_branch "$br" --arg target_path "$wt" \
|
|
940
|
+
--arg message "Primary cannot fast-forward" --arg error_type "ff_only_failed" --argjson logs "$logs_json" \
|
|
941
|
+
'{ok:false, operation:$op, error_type:$error_type, message:$message, primary:$primary, target_branch:$target_branch, target_path:$target_path, logs:$logs}'
|
|
942
|
+
exit 0
|
|
943
|
+
fi
|
|
944
|
+
if [[ -n "$msg" ]]; then
|
|
945
|
+
append "$ git -C $pr_path merge --no-ff -m '$msg' $br"
|
|
946
|
+
set +e
|
|
947
|
+
out="$(git -C "$pr_path" merge --no-ff -m "$msg" "$br" 2>&1)"
|
|
948
|
+
rc=$?
|
|
949
|
+
set -e
|
|
950
|
+
else
|
|
951
|
+
append "$ git -C $pr_path merge --no-ff $br"
|
|
952
|
+
set +e
|
|
953
|
+
out="$(git -C "$pr_path" merge --no-ff "$br" 2>&1)"
|
|
954
|
+
rc=$?
|
|
955
|
+
set -e
|
|
956
|
+
fi
|
|
957
|
+
append "$out"
|
|
958
|
+
summary="$(git_error_summary "$out")"
|
|
959
|
+
if [[ -z "$summary" ]]; then
|
|
960
|
+
summary="Merge conflict or error"
|
|
961
|
+
fi
|
|
962
|
+
logs_json="$(printf '%s\n' "$LOGS" | jq -R -s 'split("\n")[:-1]')"
|
|
963
|
+
if [[ $rc -eq 0 ]]; then
|
|
964
|
+
# Detect no-op merge (already up to date)
|
|
965
|
+
result="merged"
|
|
966
|
+
if echo "$out" | grep -qi 'already up[ -]to[ -]date'; then
|
|
967
|
+
result="up_to_date"
|
|
968
|
+
fi
|
|
969
|
+
jq -n --arg op "merge-into-primary" --arg primary "$pr" --arg target_branch "$br" --arg primary_path "$pr_path" --arg result "$result" --argjson logs "$logs_json" \
|
|
970
|
+
'{ok:true, operation:$op, primary:$primary, target_branch:$target_branch, primary_path:$primary_path, result:$result, logs:$logs}'
|
|
971
|
+
else
|
|
972
|
+
jq -n --arg op "merge-into-primary" --arg primary "$pr" --arg target_branch "$br" --arg primary_path "$pr_path" \
|
|
973
|
+
--arg message "$summary" --arg error_type "merge_conflict" --argjson logs "$logs_json" \
|
|
974
|
+
'{ok:false, operation:$op, error_type:$error_type, message:$message, primary:$primary, target_branch:$target_branch, primary_path:$primary_path, logs:$logs}'
|
|
975
|
+
fi
|
|
976
|
+
;;
|
|
977
|
+
|
|
978
|
+
"squash-into-primary")
|
|
979
|
+
shift || true
|
|
980
|
+
wt=""; br=""; msg=""
|
|
981
|
+
while [[ $# -gt 0 ]]; do
|
|
982
|
+
case "$1" in
|
|
983
|
+
--worktree) wt="$2"; shift 2;;
|
|
984
|
+
--branch) br="$2"; shift 2;;
|
|
985
|
+
--message) msg="$2"; shift 2;;
|
|
986
|
+
*) shift;;
|
|
987
|
+
esac
|
|
988
|
+
done
|
|
989
|
+
if [[ -z "$wt" || -z "$br" ]]; then
|
|
990
|
+
json_error "--worktree and --branch required"; exit 1
|
|
991
|
+
fi
|
|
992
|
+
if ! git_require_repo_root >/dev/null 2>&1; then
|
|
993
|
+
json_error "Not a git repository"; exit 1
|
|
994
|
+
fi
|
|
995
|
+
pr="$(git_primary_branch || true)"
|
|
996
|
+
if [[ -z "$pr" ]]; then
|
|
997
|
+
jq -n --arg op "squash-into-primary" '{ok:false, operation:$op, error_type:"no_primary_detected", message:"Could not detect primary branch"}'
|
|
998
|
+
exit 0
|
|
999
|
+
fi
|
|
1000
|
+
LOGS=""; append() { LOGS+="$1"$'\n'; }
|
|
1001
|
+
pr_path="$(git_ensure_primary_worktree || true)"
|
|
1002
|
+
if [[ -z "$pr_path" ]]; then
|
|
1003
|
+
jq -n --arg op "squash-into-primary" '{ok:false, operation:$op, error_type:"ensure_primary_failed", message:"Failed to ensure primary worktree"}'
|
|
1004
|
+
exit 0
|
|
1005
|
+
fi
|
|
1006
|
+
if ! git_is_worktree_clean "$pr_path"; then
|
|
1007
|
+
jq -n --arg op "squash-into-primary" --arg primary "$pr" --arg message "Primary worktree is dirty" '{ok:false, operation:$op, error_type:"dirty_primary", message:$message, primary:$primary}'
|
|
1008
|
+
exit 0
|
|
1009
|
+
fi
|
|
1010
|
+
append "$ git -C $pr_path fetch origin --prune"
|
|
1011
|
+
append "$(git_fetch_prune "$pr_path")"
|
|
1012
|
+
append "$ git -C $pr_path pull --ff-only"
|
|
1013
|
+
set +e
|
|
1014
|
+
pull_out="$(git_pull_ff_only "$pr_path")"
|
|
1015
|
+
pull_rc=$?
|
|
1016
|
+
set -e
|
|
1017
|
+
append "$pull_out"
|
|
1018
|
+
if [[ $pull_rc -ne 0 ]]; then
|
|
1019
|
+
logs_json="$(printf '%s\n' "$LOGS" | jq -R -s 'split("\n")[:-1]')"
|
|
1020
|
+
jq -n --arg op "squash-into-primary" --arg primary "$pr" --arg target_branch "$br" --arg primary_path "$pr_path" \
|
|
1021
|
+
--arg message "Primary cannot fast-forward" --arg error_type "ff_only_failed" --argjson logs "$logs_json" \
|
|
1022
|
+
'{ok:false, operation:$op, error_type:$error_type, message:$message, primary:$primary, target_branch:$target_branch, primary_path:$primary_path, logs:$logs}'
|
|
1023
|
+
exit 0
|
|
1024
|
+
fi
|
|
1025
|
+
append "$ git -C $pr_path merge --squash $br"
|
|
1026
|
+
set +e
|
|
1027
|
+
out="$(git -C "$pr_path" merge --squash "$br" 2>&1)"
|
|
1028
|
+
rc=$?
|
|
1029
|
+
set -e
|
|
1030
|
+
append "$out"
|
|
1031
|
+
if [[ $rc -ne 0 ]]; then
|
|
1032
|
+
summary="$(git_error_summary "$out")"
|
|
1033
|
+
if [[ -z "$summary" ]]; then
|
|
1034
|
+
summary="Squash merge conflict or error"
|
|
1035
|
+
fi
|
|
1036
|
+
logs_json="$(printf '%s\n' "$LOGS" | jq -R -s 'split("\n")[:-1]')"
|
|
1037
|
+
jq -n --arg op "squash-into-primary" --arg primary "$pr" --arg target_branch "$br" --arg primary_path "$pr_path" \
|
|
1038
|
+
--arg message "$summary" --arg error_type "merge_conflict" --argjson logs "$logs_json" \
|
|
1039
|
+
'{ok:false, operation:$op, error_type:$error_type, message:$message, primary:$primary, target_branch:$target_branch, primary_path:$primary_path, logs:$logs}'
|
|
1040
|
+
exit 0
|
|
1041
|
+
fi
|
|
1042
|
+
# If there are staged changes, commit them
|
|
1043
|
+
if git -C "$pr_path" diff --cached --quiet; then
|
|
1044
|
+
# Nothing to commit
|
|
1045
|
+
logs_json="$(printf '%s\n' "$LOGS" | jq -R -s 'split("\n")[:-1]')"
|
|
1046
|
+
jq -n --arg op "squash-into-primary" --arg primary "$pr" --arg target_branch "$br" --arg primary_path "$pr_path" --arg result "up_to_date" --argjson logs "$logs_json" \
|
|
1047
|
+
'{ok:true, operation:$op, primary:$primary, target_branch:$target_branch, primary_path:$primary_path, result:$result, logs:$logs}'
|
|
1048
|
+
else
|
|
1049
|
+
if [[ -n "$msg" ]]; then
|
|
1050
|
+
append "$ git -C $pr_path commit -m '$msg'"
|
|
1051
|
+
set +e
|
|
1052
|
+
com_out="$(git -C "$pr_path" commit -m "$msg" 2>&1)"
|
|
1053
|
+
crc=$?
|
|
1054
|
+
set -e
|
|
1055
|
+
else
|
|
1056
|
+
append "$ git -C $pr_path commit -m 'Squash merge ${br} into ${pr}'"
|
|
1057
|
+
set +e
|
|
1058
|
+
com_out="$(git -C "$pr_path" commit -m "Squash merge ${br} into ${pr}" 2>&1)"
|
|
1059
|
+
crc=$?
|
|
1060
|
+
set -e
|
|
1061
|
+
fi
|
|
1062
|
+
append "$com_out"
|
|
1063
|
+
logs_json="$(printf '%s\n' "$LOGS" | jq -R -s 'split("\n")[:-1]')"
|
|
1064
|
+
if [[ $crc -eq 0 ]]; then
|
|
1065
|
+
jq -n --arg op "squash-into-primary" --arg primary "$pr" --arg target_branch "$br" --arg primary_path "$pr_path" --arg result "squashed" --argjson logs "$logs_json" \
|
|
1066
|
+
'{ok:true, operation:$op, primary:$primary, target_branch:$target_branch, primary_path:$primary_path, result:$result, logs:$logs}'
|
|
1067
|
+
else
|
|
1068
|
+
jq -n --arg op "squash-into-primary" --arg primary "$pr" --arg target_branch "$br" --arg primary_path "$pr_path" \
|
|
1069
|
+
--arg message "Commit failed after squash" --arg error_type "commit_failed" --argjson logs "$logs_json" \
|
|
1070
|
+
'{ok:false, operation:$op, error_type:$error_type, message:$message, primary:$primary, target_branch:$target_branch, primary_path:$primary_path, logs:$logs}'
|
|
1071
|
+
fi
|
|
1072
|
+
fi
|
|
1073
|
+
;;
|
|
1074
|
+
|
|
1075
|
+
"tmux-available")
|
|
1076
|
+
if tmux_available; then
|
|
1077
|
+
echo "true"
|
|
1078
|
+
else
|
|
1079
|
+
echo "false"
|
|
1080
|
+
fi
|
|
1081
|
+
;;
|
|
1082
|
+
|
|
1083
|
+
"copy-env-files")
|
|
1084
|
+
if [[ -z "${2:-}" ]] || [[ -z "${3:-}" ]]; then
|
|
1085
|
+
json_error "Source and target paths required"
|
|
1086
|
+
exit 1
|
|
1087
|
+
fi
|
|
1088
|
+
source_path="$2"
|
|
1089
|
+
target_path="$3"
|
|
1090
|
+
|
|
1091
|
+
# Copy .env files using the same logic as commands.sh
|
|
1092
|
+
copy_success=0
|
|
1093
|
+
|
|
1094
|
+
# Copy root .env file
|
|
1095
|
+
if [[ -f "$source_path/.env" ]]; then
|
|
1096
|
+
cp "$source_path/.env" "$target_path/.env" 2>/dev/null && copy_success=1
|
|
1097
|
+
fi
|
|
1098
|
+
|
|
1099
|
+
# Copy nodejs .env files
|
|
1100
|
+
if [[ -d "$source_path/nodejs" ]]; then
|
|
1101
|
+
mkdir -p "$target_path/nodejs"
|
|
1102
|
+
for env_file in "$source_path/nodejs"/.env*; do
|
|
1103
|
+
[[ -f "$env_file" ]] || continue
|
|
1104
|
+
cp "$env_file" "$target_path/nodejs/$(basename "$env_file")" 2>/dev/null && copy_success=1
|
|
1105
|
+
done
|
|
1106
|
+
fi
|
|
1107
|
+
|
|
1108
|
+
# Copy nextjs .env files
|
|
1109
|
+
if [[ -d "$source_path/nextjs" ]]; then
|
|
1110
|
+
mkdir -p "$target_path/nextjs"
|
|
1111
|
+
for env_file in "$source_path/nextjs"/.env*; do
|
|
1112
|
+
[[ -f "$env_file" ]] || continue
|
|
1113
|
+
cp "$env_file" "$target_path/nextjs/$(basename "$env_file")" 2>/dev/null && copy_success=1
|
|
1114
|
+
done
|
|
1115
|
+
fi
|
|
1116
|
+
|
|
1117
|
+
# Copy ingest_py .env files
|
|
1118
|
+
if [[ -d "$source_path/ingest_py" ]]; then
|
|
1119
|
+
mkdir -p "$target_path/ingest_py"
|
|
1120
|
+
for env_file in "$source_path/ingest_py"/.env*; do
|
|
1121
|
+
[[ -f "$env_file" ]] || continue
|
|
1122
|
+
cp "$env_file" "$target_path/ingest_py/$(basename "$env_file")" 2>/dev/null && copy_success=1
|
|
1123
|
+
done
|
|
1124
|
+
fi
|
|
1125
|
+
|
|
1126
|
+
if [[ $copy_success -eq 1 ]]; then
|
|
1127
|
+
echo "true"
|
|
1128
|
+
else
|
|
1129
|
+
echo "false"
|
|
1130
|
+
fi
|
|
1131
|
+
;;
|
|
1132
|
+
|
|
1133
|
+
"help"|"-h"|"--help")
|
|
1134
|
+
cat << 'EOF'
|
|
1135
|
+
Usage: gw-bridge.sh <command> [args]
|
|
1136
|
+
|
|
1137
|
+
API bridge for gw-tui Rust application.
|
|
1138
|
+
|
|
1139
|
+
Commands:
|
|
1140
|
+
list-worktrees List all worktrees as JSON
|
|
1141
|
+
list-sessions <slug> List tmux sessions for worktree slug
|
|
1142
|
+
create-session <slug> [name] [path] Create new tmux session
|
|
1143
|
+
kill-session <session> Kill tmux session
|
|
1144
|
+
attach-session <session> Attach to tmux session
|
|
1145
|
+
session-metadata <session> Get session metadata (window, pane, command)
|
|
1146
|
+
session-preview <session> Get session preview text
|
|
1147
|
+
tmux-send-keys <session> <cmd> Send command to session and press Enter
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
check-branch <branch> Check local/remote branch existence
|
|
1151
|
+
create-worktree <branch> Create new worktree and branch
|
|
1152
|
+
create-worktree-from-existing <branch>
|
|
1153
|
+
Create worktree from existing local branch
|
|
1154
|
+
create-worktree-from-remote <branch> [remote]
|
|
1155
|
+
Create worktree from remote branch
|
|
1156
|
+
delete-worktree <branch> Delete worktree and branch
|
|
1157
|
+
delete-worktree-only <branch> Delete worktree only (keep branch)
|
|
1158
|
+
delete-branch-only <branch> Delete branch only (keep worktree)
|
|
1159
|
+
switch-worktree <path> Generate cd command for path
|
|
1160
|
+
copy-env-files <source> <target> Copy .env files between directories
|
|
1161
|
+
repo-info Get repository information
|
|
1162
|
+
git-status Get git status --porcelain
|
|
1163
|
+
enhanced-git-status [dir] Get enhanced git status with file stats and line counts
|
|
1164
|
+
primary-branch Detect primary branch name
|
|
1165
|
+
merge-from-primary --worktree <path> --branch <name>
|
|
1166
|
+
Merge primary into selected branch
|
|
1167
|
+
rebase-from-primary --worktree <path> --branch <name>
|
|
1168
|
+
Rebase selected branch onto primary
|
|
1169
|
+
merge-into-primary --worktree <path> --branch <name> [--message <msg>]
|
|
1170
|
+
Merge selected branch into primary (merge commit)
|
|
1171
|
+
squash-into-primary --worktree <path> --branch <name> [--message <msg>]
|
|
1172
|
+
Squash selected branch into primary (single commit)
|
|
1173
|
+
tmux-available Check if tmux is available
|
|
1174
|
+
help Show this help
|
|
1175
|
+
|
|
1176
|
+
All commands return JSON output or JSON errors.
|
|
1177
|
+
EOF
|
|
1178
|
+
;;
|
|
1179
|
+
|
|
1180
|
+
*)
|
|
1181
|
+
json_error "Unknown command: ${1:-}. Use 'help' for usage."
|
|
1182
|
+
exit 1
|
|
1183
|
+
;;
|
|
1184
|
+
esac
|