@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.
@@ -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