claude-plugin-viban 1.3.11 → 1.3.13

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/bin/viban CHANGED
@@ -131,60 +131,6 @@ cleanup() {
131
131
  }
132
132
  trap cleanup INT TERM EXIT
133
133
 
134
- # Python coprocess for TUI rendering (eliminates per-frame spawn overhead)
135
- # Uses explicit file descriptors (fd 7/8) to avoid interfering with read -sk1
136
- _COPROC_PID=""
137
- _COPROC_RESULT=""
138
-
139
- _start_coproc() {
140
- local _in_fifo _out_fifo
141
- _in_fifo=$(mktemp -u /tmp/viban_cp_in.XXXXXX)
142
- _out_fifo=$(mktemp -u /tmp/viban_cp_out.XXXXXX)
143
- mkfifo "$_in_fifo" "$_out_fifo"
144
- python3 "$VIBAN_SCRIPT_DIR/scripts/tui_coprocess.py" < "$_in_fifo" > "$_out_fifo" &
145
- _COPROC_PID=$!
146
- exec 7>"$_in_fifo" 8<"$_out_fifo"
147
- rm -f "$_in_fifo" "$_out_fifo"
148
- }
149
-
150
- _stop_coproc() {
151
- if [[ -n "$_COPROC_PID" ]] && kill -0 "$_COPROC_PID" 2>/dev/null; then
152
- echo "QUIT" >&7 2>/dev/null
153
- exec 7>&- 2>/dev/null
154
- wait "$_COPROC_PID" 2>/dev/null
155
- else
156
- exec 7>&- 2>/dev/null
157
- fi
158
- exec 8<&- 2>/dev/null
159
- _COPROC_PID=""
160
- }
161
-
162
- _coproc_batch_trunc() {
163
- echo "BATCH_TRUNC" >&7
164
- echo "$1" >&7
165
- echo "END" >&7
166
- _COPROC_RESULT=""
167
- local line
168
- while read -r line <&8; do
169
- [[ "$line" == "END" ]] && break
170
- [[ -n "$_COPROC_RESULT" ]] && _COPROC_RESULT+=$'\n'
171
- _COPROC_RESULT+="$line"
172
- done
173
- }
174
-
175
- _coproc_batch_width() {
176
- echo "BATCH_WIDTH" >&7
177
- echo "$1" >&7
178
- echo "END" >&7
179
- _COPROC_RESULT=""
180
- local line
181
- while read -r line <&8; do
182
- [[ "$line" == "END" ]] && break
183
- [[ -n "$_COPROC_RESULT" ]] && _COPROC_RESULT+=$'\n'
184
- _COPROC_RESULT+="$line"
185
- done
186
- }
187
-
188
134
  # Prevent gum from querying terminal colors
189
135
  export CLICOLOR_FORCE=1
190
136
  export COLORTERM=truecolor
@@ -249,1450 +195,40 @@ case "$1" in
249
195
  *) init_viban_json ;;
250
196
  esac
251
197
 
252
- # Colors - Sunset Orange Theme
253
- typeset -A C
254
- C=(
255
- fg "#FFE5D9"
256
- fg_dim "#B89685"
257
- backlog "#8B7B6B"
258
- progress "#FF6B35"
259
- review "#C44536"
260
- card_bg "#2D2416"
261
- card_bd "#5A4A3A"
262
- selected "#FF8C42"
263
- accent "#F7931E"
264
- )
265
-
266
- # 3 statuses only
267
- typeset -A STATUS_LABEL STATUS_COLOR
268
- STATUS_LABEL=(backlog "To-Do" in_progress "In Progress" review "Human Review")
269
- STATUS_COLOR=(backlog "${C[backlog]}" in_progress "${C[progress]}" review "${C[review]}")
270
-
271
- # Priority levels (P0=Critical, P3=Good to have)
272
- typeset -A PRIORITY_LABEL PRIORITY_COLOR
273
- PRIORITY_LABEL=(P0 "CRITICAL" P1 "HIGH" P2 "MEDIUM" P3 "LOW")
274
- PRIORITY_COLOR=(P0 "\033[38;2;255;69;58m" P1 "\033[38;2;255;159;10m" P2 "\033[38;2;255;214;10m" P3 "\033[38;2;142;142;147m")
275
-
276
- # Issue types (displayed as tags alongside priority)
277
- typeset -A TYPE_LABEL TYPE_COLOR
278
- TYPE_LABEL=(bug "BUG" feat "FEAT" chore "CHORE" refactor "REFAC")
279
- TYPE_COLOR=(bug "\033[38;2;255;69;58m" feat "\033[38;2;50;215;75m" chore "\033[38;2;142;142;147m" refactor "\033[38;2;90;200;250m")
280
-
281
- VIBAN_STATUSES=(backlog in_progress review)
282
-
283
- # Pre-generate horizontal borders (cache) - optimized with printf repeat
284
- typeset -A BORDER_CACHE
285
- gen_border() {
286
- local w=$1
287
- [[ -n "${BORDER_CACHE[$w]}" ]] && { echo "${BORDER_CACHE[$w]}"; return; }
288
- # Use printf with dynamic width - single call instead of loop
289
- local b=$(printf '─%.0s' {1..$w})
290
- BORDER_CACHE[$w]="$b"
291
- echo "$b"
292
- }
293
-
294
- # Cached terminal dimensions (with sensible defaults)
295
- CACHED_TERM_W=100
296
- CACHED_TERM_H=30
297
- CACHED_COL_W=32
298
- CACHED_MAX_H=22
299
- CACHED_MAX_TASKS=7
300
-
301
- # Spinner for in_progress cards (Braille dots - consistent 1-char width)
302
- SPINNER_FRAMES=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
303
- SPINNER_IDX=0
304
-
305
- update_term_cache() {
306
- if [[ -n "$COLUMNS" ]]; then
307
- CACHED_TERM_W=$COLUMNS
308
- elif command -v stty &>/dev/null; then
309
- CACHED_TERM_W=$(stty size 2>/dev/null | cut -d' ' -f2)
310
- else
311
- CACHED_TERM_W=$(tput cols 2>/dev/null || echo 100)
312
- fi
313
- if [[ -n "$LINES" ]]; then
314
- CACHED_TERM_H=$LINES
315
- elif command -v stty &>/dev/null; then
316
- CACHED_TERM_H=$(stty size 2>/dev/null | cut -d' ' -f1)
317
- else
318
- CACHED_TERM_H=$(tput lines 2>/dev/null || echo 30)
319
- fi
320
- CACHED_COL_W=$(( (CACHED_TERM_W - 2) / 3 ))
321
- local _header_extra=0
322
- $VIBAN_IS_GIT_REPO || _header_extra=1
323
- CACHED_MAX_H=$((CACHED_TERM_H - 8 - _header_extra))
324
- CACHED_MAX_TASKS=$((CACHED_MAX_H / 5))
325
- (( CACHED_MAX_TASKS < 2 )) && CACHED_MAX_TASKS=2
326
- (( CACHED_MAX_TASKS > 8 )) && CACHED_MAX_TASKS=8
327
- }
328
-
329
- check_deps() {
330
- command -v gum &>/dev/null || { echo "Error: gum required"; exit 1; }
331
- command -v jq &>/dev/null || { echo "Error: jq required"; exit 1; }
332
- }
333
-
334
- init_json() {
335
- if [[ ! -f "$VIBAN_JSON" ]]; then
336
- local max_wt_id=0
337
- if [[ -d "$VIBAN_DATA_DIR/worktrees" ]]; then
338
- local wt_id
339
- for d in "$VIBAN_DATA_DIR/worktrees/"*(/N); do
340
- wt_id="${d:t}"
341
- [[ "$wt_id" =~ ^[0-9]+$ ]] && (( wt_id > max_wt_id )) && max_wt_id=$wt_id
342
- done
343
- fi
344
- local next_id=$((max_wt_id + 1))
345
- echo "{\"version\":2,\"next_id\":$next_id,\"issues\":[]}" > "$VIBAN_JSON"
346
- elif [[ $(jq '.version // 1' "$VIBAN_JSON") -lt 2 ]]; then
347
- jq '{
348
- version: 2,
349
- next_id: (([.issues[].id] | max // 0) + 1),
350
- issues: .issues
351
- }' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
352
- fi
353
- }
354
-
355
- get_next_id() { jq -r '.next_id // (([.issues[].id] | max // 0) + 1)' "$VIBAN_JSON"; }
356
-
357
- # Display ID: show external_id if present, otherwise #id
358
- display_id() { local id="$1" ext_id="${2:-}"; [[ -n "$ext_id" && "$ext_id" != "null" ]] && echo "$ext_id" || echo "#$id"; }
359
-
360
- # Get external_id for an issue by internal id
361
- get_ext_id() { jq -r --argjson id "$1" '.issues[]|select((.id|tonumber)==$id)|.external_id//""' "$VIBAN_JSON"; }
362
-
363
- # Calculate effective order for sorting (priority-based virtual order for cards without order)
364
- # Used internally for fractional indexing calculations
365
- # Cards with order: use actual order
366
- # Cards without order: P0=1000000, P1=2000000, P2=3000000, P3=4000000 + id
367
- calc_effective_order() {
368
- local order="$1"
369
- local priority="${2:-P3}"
370
- local id="$3"
371
-
372
- if [[ -n "$order" && "$order" != "null" ]]; then
373
- echo "$order"
374
- else
375
- local base_order
376
- case "$priority" in
377
- P0) base_order=1000000;;
378
- P1) base_order=2000000;;
379
- P2) base_order=3000000;;
380
- *) base_order=4000000;;
381
- esac
382
- echo $((base_order + id))
383
- fi
384
- }
385
-
386
- add_issue() {
387
- local title=$(gum input --placeholder "Enter task title..." --width 50 \
388
- --prompt.foreground "${C[accent]}" --cursor.foreground "${C[selected]}")
389
- [[ -z "$title" ]] && return
390
-
391
- # Select type
392
- local issue_type=$(gum choose "bug (BUG)" "feat (FEATURE)" "chore (CHORE)" "refactor (REFACTOR)" \
393
- --header "Select type:" --cursor.foreground "${C[selected]}")
394
- issue_type="${issue_type%% *}" # Extract bug, feat, chore, or refactor
395
- [[ -z "$issue_type" ]] && issue_type="feat"
396
-
397
- # Select priority
398
- local priority=$(gum choose "P0 (CRITICAL)" "P1 (HIGH)" "P2 (MEDIUM)" "P3 (LOW)" \
399
- --header "Select priority:" --cursor.foreground "${C[selected]}")
400
- priority="${priority%% *}" # Extract P0, P1, P2, or P3
401
- [[ -z "$priority" ]] && priority="P3"
402
-
403
- local desc=""
404
- if gum confirm "Add description?" --affirmative "Yes (open editor)" --negative "No" \
405
- --selected.foreground="#000000" --selected.background "${C[accent]}"; then
406
- local tmpfile=$(mktemp)
407
- local editor="${EDITOR:-${VISUAL:-vim}}"
408
- local next_id=$(get_next_id)
409
- local today=$(date +"%Y-%m-%d")
410
- cat > "$tmpfile" <<TEMPLATE
411
- # ─────────────────────────────────────────────
412
- # VIBAN Issue #${next_id}
413
- # ─────────────────────────────────────────────
414
- # Title: $title
415
- # Priority: $priority
416
- # Created: $today
417
- # Status: backlog
418
- # ─────────────────────────────────────────────
419
-
420
- # ▼ Write description below (content below this line will be saved)
421
-
422
- TEMPLATE
423
- $editor "$tmpfile"
424
- desc=$(sed '/^#/d' "$tmpfile" | sed '/./,$!d')
425
- rm -f "$tmpfile"
426
- fi
427
-
428
- local id=$(get_next_id) now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
429
- # New cards don't have order - they follow priority-based sorting
430
- # Order is only assigned when manually moved
431
- local tmpjson=$(mktemp)
432
- printf '%s' "$desc" > "$tmpjson"
433
- jq --arg id "$id" --arg title "$title" --rawfile desc "$tmpjson" --arg priority "$priority" --arg issue_type "$issue_type" --arg now "$now" '
434
- .next_id = ((.next_id // 0) + 1) |
435
- .issues += [{
436
- id:($id|tonumber),
437
- title:$title,
438
- description:$desc,
439
- status:"backlog",
440
- priority:$priority,
441
- type:$issue_type,
442
- assigned_to:null,
443
- created_at:$now,
444
- updated_at:$now
445
- }]' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
446
- rm -f "$tmpjson"
447
- }
448
-
449
- # Sort: backlog/in_progress by effective order, review by updated_at desc
450
- # Effective order: if .order exists -> use it (manually positioned)
451
- # if .order is null -> priority-based virtual order (P0=1M, P1=2M, P2=3M, P3=4M) + id
452
- # This ensures: manually ordered cards stay fixed, others follow priority order
453
- get_issues_by_status() {
454
- local st="$1"
455
- if [[ "$st" == "review" ]]; then
456
- jq -r --arg s "$st" '.issues|map(select(.status==$s))|sort_by(.updated_at)|reverse' "$VIBAN_JSON"
457
- else
458
- jq -r --arg s "$st" '
459
- .issues | map(select(.status==$s)) | sort_by(
460
- if .order != null then [0, .order]
461
- else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id]
462
- end
463
- )
464
- ' "$VIBAN_JSON"
465
- fi
466
- }
467
- count_issues_by_status() { jq -r --arg s "$1" '[.issues[]|select(.status==$s)]|length' "$VIBAN_JSON"; }
468
-
469
- # Get jq sort expression for status (review uses updated_at, others use order/priority)
470
- get_sort_expr() {
471
- local st="$1"
472
- if [[ "$st" == "review" ]]; then
473
- echo 'sort_by(.updated_at) | reverse'
474
- else
475
- echo 'sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end)'
476
- fi
477
- }
478
-
479
- # Get issue ID by status and card index (uses correct sort order per status)
480
- get_issue_id_at_index() {
481
- local st="$1" idx="$2" json_data="$3"
482
- local sort_expr=$(get_sort_expr "$st")
483
- printf '%s' "$json_data" | jq -r --arg s "$st" --argjson i "$idx" \
484
- ".issues | map(select(.status==\$s)) | $sort_expr | .[\$i].id // empty"
485
- }
486
-
487
- # Find card index by ID after reorder (uses correct sort order per status)
488
- get_card_index_by_id() {
489
- local st="$1" card_id="$2" json_data="$3"
490
- local sort_expr=$(get_sort_expr "$st")
491
- printf '%s' "$json_data" | jq -r --arg s "$st" --argjson id "$card_id" \
492
- ".issues | map(select(.status==\$s)) | $sort_expr | to_entries | map(select(.value.id == \$id)) | .[0].key // 0"
493
- }
494
-
495
- get_term_width() {
496
- # Try multiple methods to get terminal width
497
- if [[ -n "$COLUMNS" ]]; then
498
- echo "$COLUMNS"
499
- elif command -v stty &>/dev/null; then
500
- stty size 2>/dev/null | cut -d' ' -f2
501
- else
502
- tput cols 2>/dev/null || echo 100
503
- fi
504
- }
505
- get_term_height() {
506
- if [[ -n "$LINES" ]]; then
507
- echo "$LINES"
508
- elif command -v stty &>/dev/null; then
509
- stty size 2>/dev/null | cut -d' ' -f1
510
- else
511
- tput lines 2>/dev/null || echo 30
512
- fi
513
- }
514
-
515
-
516
- # ANSI color codes - Orange Theme
517
- A_RESET="\033[0m"
518
- A_BOLD="\033[1m"
519
- A_DIM="\033[2m"
520
- A_FG="\033[38;2;255;229;217m" # Warm cream text
521
- A_GRAY="\033[38;2;139;123;107m" # Warm gray for backlog
522
- A_ORANGE="\033[38;2;255;107;53m" # Vibrant orange for in_progress
523
- A_DEEP_ORANGE="\033[38;2;196;69;54m" # Deep orange for review
524
- A_ACCENT="\033[38;2;247;147;30m" # Golden accent
525
- A_SELECTED="\033[38;2;255;140;66m" # Bright selection
526
-
527
- # Print centered text (uses cached width)
528
- print_center() {
529
- local text=$1 color=${2:-$A_FG}
530
- local w=$CACHED_TERM_W
531
- (( w == 0 )) && w=$(get_term_width)
532
- local len=${#text}
533
- local pad=$(( (w - len) / 2 ))
534
- printf "%${pad}s${color}%s${A_RESET}\033[K\n" "" "$text"
535
- }
536
-
537
- # Draw header with pure ANSI
538
- draw_header() {
539
- printf '\033[K\n'
540
- print_center "VIBAN" "${A_BOLD}${A_ACCENT}"
541
- local _ver repo_name subtitle="Vibe Kanban"
542
- _ver=$(grep '"version"' "$VIBAN_SCRIPT_DIR/package.json" 2>/dev/null | sed 's/.*: *"\([^"]*\)".*/\1/')
543
- repo_name=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null)
544
- [[ -n "$repo_name" ]] && subtitle="Vibe Kanban · $repo_name"
545
- if [[ -n "$_ver" ]]; then
546
- subtitle="$subtitle · v$_ver"
547
- # Check for update availability from cache
548
- if [[ -f "$_VIBAN_UPDATE_CACHE" ]]; then
549
- local cached_latest
550
- cached_latest=$(sed -n '2p' "$_VIBAN_UPDATE_CACHE" 2>/dev/null)
551
- if [[ -n "$cached_latest" && "$cached_latest" != "$_ver" ]]; then
552
- local -a cv lv
553
- cv=("${(@s/./)_ver}")
554
- lv=("${(@s/./)cached_latest}")
555
- local _is_newer=false _c _l
556
- for i in 1 2 3; do
557
- _c=${cv[$i]:-0}; _l=${lv[$i]:-0}
558
- _c=${_c%%[^0-9]*}; _l=${_l%%[^0-9]*}
559
- [[ -z "$_c" ]] && _c=0; [[ -z "$_l" ]] && _l=0
560
- if (( _l > _c )); then
561
- _is_newer=true
562
- break
563
- elif (( _l < _c )); then
564
- break
565
- fi
566
- done
567
- $_is_newer && subtitle="$subtitle → v$cached_latest"
568
- fi
569
- fi
570
- fi
571
- print_center "$subtitle" "${A_DIM}"
572
- if ! $VIBAN_IS_GIT_REPO; then
573
- print_center "⚠ Not a git repo · assign/PR unavailable" "${A_DIM}"
574
- fi
575
- printf '\033[K\n'
576
- }
577
-
578
- # Get status color code
579
- get_status_color() {
580
- case "$1" in
581
- backlog) echo "$A_GRAY";;
582
- in_progress) echo "$A_ORANGE";;
583
- review) echo "$A_DEEP_ORANGE";;
584
- esac
585
- }
586
-
587
- # Build column lines into array (optimized - single jq call, cached borders)
588
- # $1: status, $2: col_selected, $3: card_selected (-1 if none), $4: max_h, $5: col_w, $6: json_data
589
- build_column_lines() {
590
- local st="$1"
591
- local is_col_selected="${2:-0}"
592
- local card_sel="${3:--1}"
593
- local max_h="${4:-20}"
594
- local col_w="${5:-30}"
595
- local json_data="$6"
596
- local label="${STATUS_LABEL[$st]:-Unknown}"
597
- local color=$(get_status_color "$st")
598
-
599
- # Single jq call to get all issues for this status (include description, priority, type)
600
- # Replace newlines/tabs in description to prevent parsing issues
601
- # Sort: review by updated_at desc, others by effective order (ordered cards first, then priority)
602
- local sort_expr='sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end)'
603
- [[ "$st" == "review" ]] && sort_expr='sort_by(.updated_at) | reverse'
604
- local issues_data=$(printf '%s' "$json_data" | jq -r --arg s "$st" "
605
- .issues | map(select(.status==\$s)) | $sort_expr |
606
- .[] | \"\\(.id)\t\\(.title)\t\\((.description // \"\") | gsub(\"[\\n\\t\\r]\"; \" \"))\t\\(.priority // \"P3\")\t\\(.type // \"\")\t\\(.external_id // \"\")\"")
607
- local count=0
608
- # Count total issues (not capped) for overflow indicator
609
- if [[ -n "$issues_data" ]]; then
610
- local -a _count_arr=("${(f)issues_data}")
611
- count=${#_count_arr[@]}
612
- fi
613
-
614
- # Header centered in column
615
- local hdr_text="● $label"
616
- local hdr_w=$((${#label} + 2))
617
- local left_pad=$(( (col_w - hdr_w) / 2 ))
618
- local right_pad=$((col_w - hdr_w - left_pad))
619
- if (( is_col_selected )); then
620
- printf "%${left_pad}s${A_BOLD}${A_SELECTED}%s${A_RESET}%${right_pad}s\n" "" "$hdr_text" ""
621
- # Underline for selected column - use printf repeat pattern
622
- local underline=$(printf '─%.0s' {1..$hdr_w})
623
- printf "%${left_pad}s${A_SELECTED}%s${A_RESET}%${right_pad}s\n" "" "$underline" ""
624
- else
625
- printf "%${left_pad}s${color}%s${A_RESET}%${right_pad}s\n" "" "$hdr_text" ""
626
- # Empty line for non-selected columns
627
- printf "%${col_w}s\n" ""
628
- fi
629
-
630
- local lines_used=2
631
- local card_inner=$((col_w - 4))
632
- local border=$(gen_border $card_inner)
633
-
634
- # --- Pass 1: Collect card data into arrays ---
635
- local -a _ids _titles _descs _priorities _types _ext_ids _title_max_ws _title_pfxs
636
- local _has_nonascii=0
637
- local _desc_max_w=$((card_inner - 4))
638
- local _spinner_w=0
639
- [[ "$st" == "in_progress" ]] && _spinner_w=2
640
- local _cc _bc _pfx _did
641
-
642
- while IFS=$'\t' read -r _id _title _desc _priority _type _ext_id; do
643
- [[ -z "$_id" ]] && continue
644
- (( ${#_ids} >= CACHED_MAX_TASKS )) && break
645
- [[ -z "$_priority" || "$_priority" == "null" ]] && _priority="P3"
646
- [[ -z "$_type" || "$_type" == "null" ]] && _type=""
647
- [[ "$_desc" == "null" ]] && _desc=""
648
-
649
- _ids+=("$_id"); _titles+=("$_title"); _descs+=("$_desc")
650
- _priorities+=("$_priority"); _types+=("$_type"); _ext_ids+=("$_ext_id")
651
-
652
- # Per-card title width limit (use display ID length)
653
- _did=$(display_id "$_id" "$_ext_id")
654
- _title_max_ws+=($((card_inner - 4 - ${#_did} - _spinner_w)))
655
- # Prefix for width calc (X as spinner placeholder - same width 1 as braille chars)
656
- _pfx=" "
657
- (( _spinner_w )) && _pfx=" X "
658
- _title_pfxs+=("${_pfx}${_did} ")
659
-
660
- # Check for non-ASCII
661
- if (( ! _has_nonascii )); then
662
- _cc=${#_title}
663
- LC_ALL=C _bc=${#_title}; unset LC_ALL
664
- (( _bc != _cc )) && _has_nonascii=1
665
- if (( ! _has_nonascii && ${#_desc} > 0 )); then
666
- _cc=${#_desc}; LC_ALL=C _bc=${#_desc}; unset LC_ALL
667
- (( _bc != _cc )) && _has_nonascii=1
668
- fi
669
- fi
670
- done <<< "$issues_data"
671
-
672
- local _n=${#_ids}
673
-
674
- # --- Pass 2: Batch compute truncation + widths (single Python call) ---
675
- local -a _short_titles _title_cws _short_descs _desc_cws
676
-
677
- if (( _n > 0 )); then
678
- if (( _has_nonascii )); then
679
- # Build batch input: 2 lines per card (title, desc)
680
- # Format: max_w<TAB>prefix<TAB>string
681
- local _batch_input=""
682
- for (( _i=1; _i<=_n; _i++ )); do
683
- _batch_input+="${_title_max_ws[$_i]}"$'\t'"${_title_pfxs[$_i]}"$'\t'"${_titles[$_i]}"$'\n'
684
- _batch_input+="${_desc_max_w}"$'\t'" "$'\t'"${_descs[$_i]}"$'\n'
685
- done
686
-
687
- # Single Python call: truncate each string and compute content width
688
- local _batch_output
689
- _coproc_batch_trunc "$_batch_input"
690
- _batch_output="$_COPROC_RESULT"
691
-
692
- local _li=0
693
- while IFS=$'\t' read -r _tr _cw; do
694
- ((_li++))
695
- if (( _li % 2 == 1 )); then
696
- _short_titles+=("$_tr"); _title_cws+=($_cw)
697
- else
698
- _short_descs+=("$_tr"); _desc_cws+=($_cw)
699
- fi
700
- done <<< "$_batch_output"
701
- else
702
- # All-ASCII fast path - no Python needed
703
- local _t _mw _fc _d
704
- for (( _i=1; _i<=_n; _i++ )); do
705
- _t="${_titles[$_i]}" _mw=${_title_max_ws[$_i]}
706
- (( ${#_t} > _mw )) && _t="${_t:0:$_mw}"
707
- _short_titles+=("$_t")
708
- _fc="${_title_pfxs[$_i]}${_t}"
709
- _title_cws+=(${#_fc})
710
-
711
- _d="${_descs[$_i]}"
712
- (( ${#_d} > _desc_max_w )) && _d="${_d:0:$_desc_max_w}"
713
- _short_descs+=("$_d")
714
- _desc_cws+=($((2 + ${#_d})))
715
- done
716
- fi
717
- fi
718
-
719
- # --- Pass 3: Render cards ---
720
- local shown=0
721
- local id priority issue_type ext_id did
722
- local spinner_prefix title_content title_pad
723
- local desc_content desc_pad
724
- local priority_tag priority_color type_tag type_color tags_w tags_pad
725
- local border_color text_color desc_color
726
- for (( _i=1; _i<=_n; _i++ )); do
727
- id="${_ids[$_i]}"
728
- priority="${_priorities[$_i]}"
729
- issue_type="${_types[$_i]}"
730
- ext_id="${_ext_ids[$_i]}"
731
- did=$(display_id "$id" "$ext_id")
732
-
733
- # Title line
734
- spinner_prefix=""
735
- [[ "$st" == "in_progress" ]] && spinner_prefix="${SPINNER_FRAMES[$((SPINNER_IDX % ${#SPINNER_FRAMES[@]} + 1))]} "
736
- title_content=" ${spinner_prefix}${did} ${_short_titles[$_i]}"
737
- title_pad=$((card_inner - ${_title_cws[$_i]}))
738
- (( title_pad < 0 )) && title_pad=0
739
-
740
- # Description line
741
- desc_content=" ${_short_descs[$_i]}"
742
- desc_pad=$((card_inner - ${_desc_cws[$_i]}))
743
- (( desc_pad < 0 )) && desc_pad=0
744
-
745
- # Priority and type tags
746
- priority_tag="[$priority]"
747
- priority_color="${PRIORITY_COLOR[$priority]:-$A_DIM}"
748
- type_tag="" type_color="" tags_w=0
749
- if [[ -n "$issue_type" ]]; then
750
- type_tag="[${TYPE_LABEL[$issue_type]:-$issue_type}]"
751
- type_color="${TYPE_COLOR[$issue_type]:-$A_DIM}"
752
- tags_w=$((${#priority_tag} + 1 + ${#type_tag}))
753
- else
754
- tags_w=${#priority_tag}
755
- fi
756
- tags_pad=$((card_inner - tags_w - 2))
757
-
758
- border_color="$A_DIM"
759
- text_color="$A_FG"
760
- desc_color="$A_DIM"
761
- if (( is_col_selected && shown == card_sel )); then
762
- border_color="${A_SELECTED}"
763
- text_color="${A_BOLD}${A_ACCENT}"
764
- desc_color="${A_ACCENT}"
765
- fi
766
-
767
- # 5-line card with priority+type tags on 4th line
768
- printf " ${border_color}╭%s╮${A_RESET} \n" "$border"
769
- printf " ${border_color}│${A_RESET}${text_color}%s${A_RESET}%${title_pad}s${border_color}│${A_RESET} \n" "$title_content" ""
770
- printf " ${border_color}│${A_RESET}${desc_color}%s${A_RESET}%${desc_pad}s${border_color}│${A_RESET} \n" "$desc_content" ""
771
- if [[ -n "$type_tag" ]]; then
772
- printf " ${border_color}│${A_RESET} ${priority_color}%s${A_RESET} ${type_color}%s${A_RESET}%${tags_pad}s${border_color}│${A_RESET} \n" "$priority_tag" "$type_tag" ""
773
- else
774
- printf " ${border_color}│${A_RESET} ${priority_color}%s${A_RESET}%${tags_pad}s${border_color}│${A_RESET} \n" "$priority_tag" ""
775
- fi
776
- printf " ${border_color}╰%s╯${A_RESET} \n" "$border"
777
-
778
- ((shown++))
779
- lines_used=$((lines_used + 5))
780
- done
781
-
782
- # Overflow indicator
783
- if (( count > _n )); then
784
- local more_text=" +$((count - _n)) more..."
785
- printf "${A_DIM}%s${A_RESET}%$((col_w - ${#more_text}))s\n" "$more_text" ""
786
- ((lines_used++))
787
- fi
788
-
789
- if (( count == 0 )); then
790
- local no_text=" No tasks"
791
- local no_w=${#no_text}
792
- printf "${A_DIM}%s${A_RESET}%$((col_w - no_w))s\n" "$no_text" ""
793
- ((lines_used++))
794
- fi
795
-
796
- while (( lines_used < max_h )); do
797
- printf "%${col_w}s\n" ""
798
- ((lines_used++))
799
- done
800
- }
801
-
802
- # ESC character for ANSI stripping (defined once at script level)
803
- _ESC=$'\e'
804
-
805
- # Pad line to exact width with spaces
806
- # Optimized: use zsh parameter expansion to strip ANSI codes
807
- pad_to_width() {
808
- local line="$1"
809
- local width="$2"
810
- local precomputed_w="${3:-}"
811
- # Strip ANSI codes: ESC [ followed by numbers/semicolons, ending with letter
812
- local plain="${line//${_ESC}\[[0-9;]#[a-zA-Z]/}"
813
- local display_w
814
- if [[ -n "$precomputed_w" ]]; then
815
- display_w=$precomputed_w
816
- else
817
- local char_count=${#plain} byte_count
818
- LC_ALL=C byte_count=${#plain}
819
- unset LC_ALL
820
- if [[ $byte_count -eq $char_count ]]; then
821
- display_w=$char_count
822
- else
823
- display_w=$(( char_count + (byte_count - char_count) / 2 ))
824
- fi
825
- fi
826
- local pad=$((width - display_w))
827
- printf '%s' "$line"
828
- (( pad > 0 )) && printf "%${pad}s" ""
829
- }
830
-
831
- # Draw the board (optimized - arrays instead of temp files)
832
- # $1: col_sel, $2: card_sel, $3: json_data
833
- draw_board() {
834
- local col_sel=${1:-0}
835
- local card_sel=${2:--1}
836
- local json_data="$3"
837
- local col_w=$CACHED_COL_W
838
- local max_h=$CACHED_MAX_H
839
-
840
- local c1=-1 c2=-1 c3=-1
841
- case $col_sel in
842
- 0) c1=$card_sel;;
843
- 1) c2=$card_sel;;
844
- 2) c3=$card_sel;;
845
- esac
846
-
847
- # Build columns to arrays
848
- local -a col1 col2 col3
849
- col1=("${(@f)$(build_column_lines "backlog" $((col_sel == 0)) $c1 $max_h $col_w "$json_data")}")
850
- col2=("${(@f)$(build_column_lines "in_progress" $((col_sel == 1)) $c2 $max_h $col_w "$json_data")}")
851
- col3=("${(@f)$(build_column_lines "review" $((col_sel == 2)) $c3 $max_h $col_w "$json_data")}")
852
-
853
- # Batch compute display widths for all non-ASCII lines (single Python call)
854
- # Build input: all lines from all 3 columns, ANSI-stripped
855
- local -a all_plains
856
- local -a all_widths
857
- local _needs_python=0
858
- local i _plain _cc _bc
859
- for ((i=1; i<=max_h; i++)); do
860
- for _col_line in "${col1[$i]}" "${col2[$i]}" "${col3[$i]}"; do
861
- _plain="${_col_line//${_ESC}\[[0-9;]#[a-zA-Z]/}"
862
- all_plains+=("$_plain")
863
- _cc=${#_plain}
864
- LC_ALL=C _bc=${#_plain}
865
- unset LC_ALL
866
- if [[ $_bc -eq $_cc ]]; then
867
- all_widths+=($_cc)
868
- else
869
- all_widths+=(-1) # marker: needs Python
870
- _needs_python=1
871
- fi
872
- done
873
- done
874
-
875
- if (( _needs_python )); then
876
- # Single Python call to compute all non-ASCII widths
877
- local _input="" _idx
878
- for ((_idx=1; _idx<=${#all_plains[@]}; _idx++)); do
879
- if [[ ${all_widths[$_idx]} -eq -1 ]]; then
880
- _input+="${all_plains[$_idx]}"$'\n'
881
- fi
882
- done
883
- local -a _py_results
884
- _coproc_batch_width "$_input"
885
- _py_results=("${(@f)_COPROC_RESULT}")
886
- # Map Python results back to width array
887
- local _pi=1
888
- for ((_idx=1; _idx<=${#all_widths[@]}; _idx++)); do
889
- if [[ ${all_widths[$_idx]} -eq -1 ]]; then
890
- all_widths[$_idx]=${_py_results[$_pi]}
891
- ((_pi++))
892
- fi
893
- done
894
- fi
895
-
896
- # Merge line by line using precomputed widths
897
- local _wi=1
898
- for ((i=1; i<=max_h; i++)); do
899
- pad_to_width "${col1[$i]}" $col_w "${all_widths[$_wi]}"
900
- ((_wi++))
901
- printf "${A_DIM}│${A_RESET}"
902
- pad_to_width "${col2[$i]}" $col_w "${all_widths[$_wi]}"
903
- ((_wi++))
904
- printf "${A_DIM}│${A_RESET}"
905
- pad_to_width "${col3[$i]}" $col_w "${all_widths[$_wi]}"
906
- ((_wi++))
907
- printf '\033[K\n'
908
- done
909
- }
910
-
911
- draw_footer() {
912
- printf '\033[K\n'
913
- print_center "←→ Column │ ↑↓ Card │ Shift+↑↓ Reorder │ Shift+←→ Move │ Enter Edit/PR │ ⌫ Del │ A Add │ Q Quit" "${A_DIM}"
914
- }
915
-
916
- read_key() {
917
- local _rk_timeout="${1:-0.5}"
918
- local key result=""
919
- # Timeout for spinner animation refresh (default 0.5s)
920
- read -sk1 -t "$_rk_timeout" key 2>/dev/null || { echo "timeout"; return; }
921
-
922
- if [[ "$key" == $'\e' ]]; then
923
- read -sk1 -t 0.1 c2 2>/dev/null
924
- if [[ "$c2" == "[" ]]; then
925
- read -sk1 -t 0.1 c3 2>/dev/null
926
- case "$c3" in
927
- D) result="left";; C) result="right";;
928
- A) result="up";; B) result="down";;
929
- "1")
930
- # Handle Shift+arrow sequences: ESC[1;2X where X is A/B/C/D
931
- read -sk1 -t 0.1 c4 2>/dev/null
932
- if [[ "$c4" == ";" ]]; then
933
- read -sk1 -t 0.1 c5 2>/dev/null
934
- read -sk1 -t 0.1 c6 2>/dev/null
935
- if [[ "$c5" == "2" ]]; then
936
- case "$c6" in
937
- A) result="shift_up";;
938
- B) result="shift_down";;
939
- C) result="shift_right";;
940
- D) result="shift_left";;
941
- esac
942
- fi
943
- fi
944
- ;;
945
- esac
946
- elif [[ "$c2" == "]" ]]; then
947
- # Drain OSC sequence
948
- while read -sk1 -t 0.01 _ 2>/dev/null; do :; done
949
- fi
950
- elif [[ "$key" == "" || "$key" == $'\n' ]]; then
951
- result="enter"
952
- elif [[ "$key" == $'\x7f' || "$key" == $'\b' ]]; then
953
- result="backspace"
954
- else
955
- case "$key" in
956
- q|Q) result="quit";;
957
- a|A) result="add";;
958
- esac
959
- fi
960
-
961
- echo "$result"
962
- }
963
-
964
- # Move card order up or down within a status column (fractional indexing)
965
- # $1: status, $2: current card index, $3: direction (-1 for up, 1 for down)
966
- # When manually moved, the card gets an order value to pin its position
967
- move_card_order() {
968
- local st="$1"
969
- local cur_idx="$2"
970
- local dir="$3"
971
- local new_idx=$((cur_idx + dir))
972
-
973
- # Get issues in current effective order (ordered cards first, then priority-sorted)
974
- local issues=$(jq -r --arg s "$st" '
975
- .issues | map(select(.status==$s)) | sort_by(
976
- if .order != null then [0, .order]
977
- else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id]
978
- end
979
- )
980
- ' "$VIBAN_JSON")
981
- local cnt=$(printf '%s' "$issues" | jq 'length')
982
-
983
- # Bounds check
984
- (( new_idx < 0 || new_idx >= cnt )) && return 1
985
-
986
- # Get ID of the card to move
987
- local cur_id=$(printf '%s' "$issues" | jq -r ".[$cur_idx].id")
988
-
989
- # Calculate effective order for a card (use actual order or virtual priority-based order)
990
- get_eff_order() {
991
- local idx=$1
992
- local order=$(printf '%s' "$issues" | jq -r ".[$idx].order // \"null\"")
993
- if [[ "$order" != "null" ]]; then
994
- echo "$order"
995
- else
996
- local priority=$(printf '%s' "$issues" | jq -r ".[$idx].priority // \"P3\"")
997
- local id=$(printf '%s' "$issues" | jq -r ".[$idx].id")
998
- case "$priority" in
999
- P0) echo $((1000000 + id));;
1000
- P1) echo $((2000000 + id));;
1001
- P2) echo $((3000000 + id));;
1002
- *) echo $((4000000 + id));;
1003
- esac
1004
- fi
1005
- }
1006
-
1007
- # Calculate new order using fractional indexing
1008
- # Place card between the target position and its neighbor
1009
- local new_order
1010
- if (( dir < 0 )); then
1011
- # Moving up: place between target and the one above it
1012
- local target_order=$(get_eff_order $new_idx)
1013
- if (( new_idx == 0 )); then
1014
- # Moving to top: use target_order - 1
1015
- new_order=$(echo "$target_order - 1" | bc)
1016
- else
1017
- local above_order=$(get_eff_order $(($new_idx - 1)))
1018
- new_order=$(echo "scale=6; ($above_order + $target_order) / 2" | bc)
1019
- fi
1020
- else
1021
- # Moving down: place between target and the one below it
1022
- local target_order=$(get_eff_order $new_idx)
1023
- if (( new_idx == cnt - 1 )); then
1024
- # Moving to bottom: use target_order + 1
1025
- new_order=$(echo "$target_order + 1" | bc)
1026
- else
1027
- local below_order=$(get_eff_order $(($new_idx + 1)))
1028
- new_order=$(echo "scale=6; ($target_order + $below_order) / 2" | bc)
1029
- fi
1030
- fi
1031
-
1032
- # Update the card's order (this pins it to the new position)
1033
- local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1034
- jq --argjson cur_id "$cur_id" --argjson new_order "$new_order" --arg now "$now" '
1035
- (.issues[] | select(.id==$cur_id)) |= . + {order:$new_order,updated_at:$now}
1036
- ' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1037
-
1038
- return 0
1039
- }
1040
-
1041
- # Get issue ID by status and index (uses correct sort order per status)
1042
- get_issue_id_by_index() {
1043
- local st=$1 idx=$2
1044
- local sort_expr=$(get_sort_expr "$st")
1045
- jq -r --arg s "$st" --argjson i "$idx" ".issues | map(select(.status==\$s)) | $sort_expr | .[\$i].id // empty" "$VIBAN_JSON"
1046
- }
1047
-
1048
- # Delete issue by ID (with worktree cleanup)
1049
- delete_issue() {
1050
- local id=$1
1051
- local repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
1052
- local wt_dir="$VIBAN_DATA_DIR/worktrees/$id"
1053
-
1054
- local branch="issue-$id"
1055
- local _ext_id=$(get_ext_id "$id")
1056
- if [[ -n "$_ext_id" && "$_ext_id" != "null" ]]; then
1057
- local _issue_num="${_ext_id##*:}"
1058
- if git -C "$repo_root" rev-parse --verify "issue-${_issue_num}" &>/dev/null 2>&1; then
1059
- branch="issue-${_issue_num}"
1060
- fi
1061
- fi
1062
-
1063
- if [[ -d "$wt_dir" ]]; then
1064
- git -C "$repo_root" worktree remove "$wt_dir" --force 2>/dev/null
1065
- git -C "$repo_root" branch -D "$branch" 2>/dev/null
1066
- fi
1067
- jq --argjson id "$id" 'del(.issues[]|select((.id|tonumber)==$id))' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && \
1068
- mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1069
- }
1070
-
1071
- level1_columns() {
1072
- IN_TUI=true
1073
- _start_coproc
1074
- local col=0 card=0
1075
-
1076
- # Auto-sync state (120 iterations × 0.5s timeout = ~60s interval)
1077
- local _sync_counter=0
1078
- local _SYNC_INTERVAL=120
1079
- local _sync_pid=""
1080
-
1081
- # Hide cursor and disable input echo
1082
- stty -echo 2>/dev/null
1083
- printf '\033[?25l\033[2J\033[H'
1084
-
1085
- # Initial cache update
1086
- update_term_cache
1087
-
1088
- while true; do
1089
- # Auto-sync: reap finished background sync
1090
- if [[ -n "$_sync_pid" ]]; then
1091
- if ! kill -0 "$_sync_pid" 2>/dev/null; then
1092
- wait "$_sync_pid" 2>/dev/null
1093
- _sync_pid=""
1094
- fi
1095
- fi
1096
-
1097
- # Auto-sync: trigger when interval reached and sync configured
1098
- ((_sync_counter++)) || true
1099
- if (( _sync_counter >= _SYNC_INTERVAL )) && [[ -z "$_sync_pid" && -f "$VIBAN_DATA_DIR/sync.json" ]]; then
1100
- _sync_counter=0
1101
- local _sync_provider
1102
- _sync_provider=$(jq -r '.provider // ""' "$VIBAN_DATA_DIR/sync.json" 2>/dev/null)
1103
- if [[ -n "$_sync_provider" && "$_sync_provider" != "null" ]]; then
1104
- VIBAN_JSON="$VIBAN_JSON" VIBAN_DATA_DIR="$VIBAN_DATA_DIR" \
1105
- VIBAN_PROVIDER="$_sync_provider" VIBAN_SCRIPT_DIR="$VIBAN_SCRIPT_DIR" \
1106
- bash "$VIBAN_SCRIPT_DIR/scripts/sync.sh" --auto &
1107
- _sync_pid=$!
1108
- fi
1109
- fi
1110
- # Cache JSON data once per frame
1111
- local json_data=$(cat "$VIBAN_JSON")
1112
-
1113
- printf '\033[H\033[0m'
1114
- draw_header
1115
- draw_board $col $card "$json_data"
1116
- draw_footer
1117
- printf '\033[J'
1118
-
1119
- # Advance spinner
1120
- ((SPINNER_IDX++))
1121
-
1122
- local st="${VIBAN_STATUSES[$((col + 1))]}"
1123
- # Use cached json_data for count
1124
- local cnt=$(printf '%s' "$json_data" | jq -r --arg s "$st" '[.issues[]|select(.status==$s)]|length')
1125
-
1126
- local key=$(read_key)
1127
- case "$key" in
1128
- left)
1129
- local start_col=$col
1130
- col=$(( (col - 1 + 3) % 3 ))
1131
- # Skip empty columns (but stop if we return to start)
1132
- while (( col != start_col )); do
1133
- local next_st="${VIBAN_STATUSES[$((col + 1))]}"
1134
- local next_cnt=$(printf '%s' "$json_data" | jq -r --arg s "$next_st" '[.issues[]|select(.status==$s)]|length')
1135
- (( next_cnt > 0 )) && break
1136
- col=$(( (col - 1 + 3) % 3 ))
1137
- done
1138
- card=0
1139
- ;;
1140
- right)
1141
- local start_col=$col
1142
- col=$(( (col + 1) % 3 ))
1143
- # Skip empty columns (but stop if we return to start)
1144
- while (( col != start_col )); do
1145
- local next_st="${VIBAN_STATUSES[$((col + 1))]}"
1146
- local next_cnt=$(printf '%s' "$json_data" | jq -r --arg s "$next_st" '[.issues[]|select(.status==$s)]|length')
1147
- (( next_cnt > 0 )) && break
1148
- col=$(( (col + 1) % 3 ))
1149
- done
1150
- card=0
1151
- ;;
1152
- up)
1153
- (( cnt > 0 )) && card=$(( (card - 1 + cnt) % cnt ))
1154
- ;;
1155
- down)
1156
- (( cnt > 0 )) && card=$(( (card + 1) % cnt ))
1157
- ;;
1158
- shift_up)
1159
- if (( cnt > 0 && card > 0 )); then
1160
- local card_id=$(get_issue_id_at_index "$st" "$card" "$json_data")
1161
- if move_card_order "$st" $card -1; then
1162
- local new_json=$(cat "$VIBAN_JSON")
1163
- card=$(get_card_index_by_id "$st" "$card_id" "$new_json")
1164
- fi
1165
- fi
1166
- ;;
1167
- shift_down)
1168
- if (( cnt > 0 && card < cnt - 1 )); then
1169
- local card_id=$(get_issue_id_at_index "$st" "$card" "$json_data")
1170
- if move_card_order "$st" $card 1; then
1171
- local new_json=$(cat "$VIBAN_JSON")
1172
- card=$(get_card_index_by_id "$st" "$card_id" "$new_json")
1173
- fi
1174
- fi
1175
- ;;
1176
- enter)
1177
- if (( cnt > 0 )); then
1178
- local id=$(get_issue_id_at_index "$st" "$card" "$json_data")
1179
- [[ -n "$id" ]] && {
1180
- if [[ "$st" == "review" ]]; then
1181
- # Open associated PR in browser
1182
- local _branch="issue-${id}"
1183
- local _ext_id=$(get_ext_id "$id")
1184
- if [[ -n "$_ext_id" && "$_ext_id" != "null" ]]; then
1185
- local _num="${_ext_id##*:}"
1186
- gh pr view "$_num" --web 2>/dev/null || \
1187
- gh pr list --head "$_branch" --web 2>/dev/null
1188
- else
1189
- gh pr list --head "$_branch" --web 2>/dev/null
1190
- fi
1191
- else
1192
- printf '\033[?25h'
1193
- stty echo 2>/dev/null
1194
- edit_issue "$id"
1195
- stty -echo 2>/dev/null
1196
- printf '\033[?25l\033[2J\033[H'
1197
- fi
1198
- }
1199
- fi
1200
- ;;
1201
- shift_left)
1202
- if (( cnt > 0 && col > 0 )); then
1203
- move_card_status "$st" $card -1 && { col=$((col - 1)); card=0; }
1204
- fi
1205
- ;;
1206
- shift_right)
1207
- if (( cnt > 0 && col < 2 )); then
1208
- move_card_status "$st" $card 1 && { col=$((col + 1)); card=0; }
1209
- fi
1210
- ;;
1211
- add)
1212
- printf '\033[?25h'
1213
- stty echo 2>/dev/null
1214
- add_issue
1215
- stty -echo 2>/dev/null
1216
- printf '\033[?25l\033[2J\033[H'
1217
- ;;
1218
- backspace)
1219
- if (( cnt > 0 )); then
1220
- local id=$(get_issue_id_at_index "$st" "$card" "$json_data")
1221
- [[ -n "$id" ]] && {
1222
- # Move cursor to footer line and run gum there
1223
- printf '\033[?25h'
1224
- stty echo 2>/dev/null
1225
- # Clear footer line and show confirm
1226
- printf '\033[%d;1H\033[K' "$CACHED_TERM_H"
1227
- if gum confirm "Delete $(display_id "$id" "$(get_ext_id "$id")")?" --affirmative "Yes" --negative "No" \
1228
- --selected.foreground="#000000" --selected.background "${C[accent]}"; then
1229
- delete_issue "$id"
1230
- (( card > 0 )) && card=$((card - 1))
1231
- fi
1232
- stty -echo 2>/dev/null
1233
- printf '\033[?25l'
1234
- # Redraw footer only (cursor back to footer)
1235
- printf '\033[%d;1H\033[K' "$((CACHED_TERM_H - 1))"
1236
- draw_footer
1237
- }
1238
- fi
1239
- ;;
1240
- quit)
1241
- printf '\033[?25h\033[0m'
1242
- stty echo 2>/dev/null
1243
- clear
1244
- exit 0
1245
- ;;
1246
- esac
1247
- done
1248
- }
1249
-
1250
- # Edit issue in editor (title + description + priority + type)
1251
- edit_issue() {
1252
- local id=$1
1253
- local issue=$(jq --argjson id "$id" '.issues[]|select((.id|tonumber)==$id)' "$VIBAN_JSON")
1254
- [[ -z "$issue" ]] && return 1
1255
-
1256
- local title=$(printf '%s' "$issue" | jq -r '.title')
1257
- local desc=$(printf '%s' "$issue" | jq -r '.description // ""')
1258
- local ist=$(printf '%s' "$issue" | jq -r '.status')
1259
- local created=$(printf '%s' "$issue" | jq -r '.created_at')
1260
- local priority=$(printf '%s' "$issue" | jq -r '.priority // "P3"')
1261
- local issue_type=$(printf '%s' "$issue" | jq -r '.type // ""')
1262
- local ext_id=$(printf '%s' "$issue" | jq -r '.external_id // ""')
1263
- local did; did=$(display_id "$id" "$ext_id")
1264
-
1265
- local tmpfile=$(mktemp)
1266
- local editor="${EDITOR:-${VISUAL:-vim}}"
1267
-
1268
- cat > "$tmpfile" <<TEMPLATE
1269
- # ─────────────────────────────────────────────
1270
- # VIBAN Issue $did
1271
- # ─────────────────────────────────────────────
1272
- # Status: ${STATUS_LABEL[$ist]}
1273
- # Created: ${created:0:10}
1274
- # ─────────────────────────────────────────────
1275
-
1276
- # ▼ Priority (P0=CRITICAL, P1=HIGH, P2=MEDIUM, P3=LOW)
1277
- $priority
1278
-
1279
- # ▼ Type (bug, feat, chore, refactor) - leave empty for none
1280
- $issue_type
1281
-
1282
- # ▼ Title (single line)
1283
- $title
1284
-
1285
- # ▼ Description (multiple lines allowed)
1286
- $desc
1287
- TEMPLATE
1288
-
1289
- $editor "$tmpfile"
1290
-
1291
- # Parse: priority -> type -> title -> description
1292
- local new_priority="" new_type="" new_title="" new_desc="" parse_stage=0
1293
- while IFS= read -r line; do
1294
- [[ "$line" =~ ^#.*$ ]] && continue
1295
- case $parse_stage in
1296
- 0) # Looking for priority
1297
- [[ -z "$line" ]] && continue
1298
- # Validate priority format (P0-P3)
1299
- if [[ "$line" =~ ^P[0-3]$ ]]; then
1300
- new_priority="$line"
1301
- else
1302
- new_priority="P3" # Default if invalid
1303
- fi
1304
- parse_stage=1
1305
- ;;
1306
- 1) # Looking for type
1307
- [[ -z "$line" ]] && continue # Skip empty lines
1308
- # Validate type format (bug, feat, chore, refactor)
1309
- if [[ "$line" =~ ^(bug|feat|chore|refactor)$ ]]; then
1310
- new_type="$line"
1311
- fi
1312
- parse_stage=2 # Move to stage 2 on any non-empty line
1313
- ;;
1314
- 2) # Looking for title
1315
- [[ -z "$line" ]] && continue
1316
- new_title="$line"
1317
- parse_stage=3
1318
- ;;
1319
- 3) # Collecting description
1320
- # Skip empty lines right after title
1321
- if [[ -z "$new_desc" && -z "$line" ]]; then
1322
- continue
1323
- fi
1324
- new_desc+="$line"$'\n'
1325
- ;;
1326
- esac
1327
- done < "$tmpfile"
1328
-
1329
- # Trim trailing newlines from description
1330
- new_desc="${new_desc%$'\n'}"
1331
-
1332
- rm -f "$tmpfile"
1333
-
1334
- [[ -z "$new_title" ]] && return 1
1335
- [[ -z "$new_priority" ]] && new_priority="P3"
1336
-
1337
- # Update issue
1338
- local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1339
- local tmpjson=$(mktemp)
1340
- printf '%s' "$new_desc" > "$tmpjson"
1341
- jq --argjson id "$id" --arg title "$new_title" --rawfile desc "$tmpjson" --arg priority "$new_priority" --arg issue_type "$new_type" --arg now "$now" \
1342
- '(.issues[]|select((.id|tonumber)==$id)) |= . + {title:$title,description:$desc,priority:$priority,type:(if $issue_type == "" then null else $issue_type end),updated_at:$now}' \
1343
- "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1344
- rm -f "$tmpjson"
1345
- }
1346
-
1347
- # Move card to adjacent column (change status)
1348
- move_card_status() {
1349
- local st="$1"
1350
- local card_idx="$2"
1351
- local dir="$3" # -1 for left, 1 for right
1352
-
1353
- local sort_expr=$(get_sort_expr "$st")
1354
- local id=$(jq -r --arg s "$st" --argjson i "$card_idx" \
1355
- ".issues | map(select(.status==\$s)) | $sort_expr | .[\$i].id // empty" "$VIBAN_JSON")
1356
- [[ -z "$id" ]] && return 1
1357
-
1358
- # Find current status index and calculate new status
1359
- local cur_idx=0
1360
- for i in {1..3}; do
1361
- [[ "${VIBAN_STATUSES[$i]}" == "$st" ]] && { cur_idx=$i; break; }
1362
- done
1363
-
1364
- local new_idx=$((cur_idx + dir))
1365
- (( new_idx < 1 || new_idx > 3 )) && return 1
1366
-
1367
- local new_st="${VIBAN_STATUSES[$new_idx]}"
1368
- local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1369
-
1370
- jq --argjson id "$id" --arg new_st "$new_st" --arg now "$now" \
1371
- '(.issues[]|select((.id|tonumber)==$id)) |= . + {status:$new_st,updated_at:$now}' \
1372
- "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1373
- }
1374
-
1375
- # CLI commands
1376
- cmd_list() {
1377
- init_json
1378
- echo ""
1379
- for st in $VIBAN_STATUSES; do
1380
- gum style --foreground "${STATUS_COLOR[$st]}" --bold "● ${STATUS_LABEL[$st]} ($(count_issues_by_status "$st"))"
1381
- get_issues_by_status "$st" | jq -r '.[]|" \(if .external_id then .external_id else "#\(.id)" end) [\(.priority // "P3")]\(if .type then " [\(.type | ascii_upcase)]" else "" end) \(.title)"'
1382
- echo ""
1383
- done
1384
- }
1385
-
1386
- cmd_priority() {
1387
- init_json
1388
- [[ -z "$1" ]] && { echo "Usage: viban priority <id> <P0|P1|P2|P3>"; exit 1; }
1389
- local id="$1"
1390
- local new_priority="${2:-}"
1391
-
1392
- # Validate priority
1393
- if [[ ! "$new_priority" =~ ^P[0-3]$ ]]; then
1394
- echo "Error: Priority must be P0, P1, P2, or P3"
1395
- exit 1
1396
- fi
1397
-
1398
- # Check if issue exists
1399
- local exists=$(jq --argjson id "$id" '[.issues[]|select((.id|tonumber)==$id)]|length' "$VIBAN_JSON")
1400
- [[ "$exists" == "0" ]] && { echo "Error: Issue #$id not found"; exit 1; }
1401
-
1402
- local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1403
- jq --argjson id "$id" --arg priority "$new_priority" --arg now "$now" \
1404
- '(.issues[]|select((.id|tonumber)==$id)) |= . + {priority:$priority,updated_at:$now}' \
1405
- "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1406
-
1407
- echo "✓ $(display_id "$id" "$(get_ext_id "$id")") priority → $new_priority"
1408
- }
1409
-
1410
- cmd_add() {
1411
- init_json
1412
- [[ -z "$1" ]] && { echo "Usage: viban add \"title\" [\"description\"] [priority] [type] [attachments...]"; exit 1; }
1413
-
1414
- # Support both positional and named args (--title, --description, --priority, --type, --ext-id)
1415
- local title="" desc="" priority="P3" issue_type="" ext_id=""
1416
- local -a attachments=()
1417
- local positional=()
1418
-
1419
- while [[ $# -gt 0 ]]; do
1420
- case "$1" in
1421
- --title) title="$2"; shift 2 ;;
1422
- --desc|--description) desc="$2"; shift 2 ;;
1423
- --desc-file) [[ -f "$2" ]] && desc="$(cat "$2")"; shift 2 ;;
1424
- --priority) priority="$2"; shift 2 ;;
1425
- --type) issue_type="$2"; shift 2 ;;
1426
- --ext-id|--external-id) ext_id="$2"; shift 2 ;;
1427
- --attach|--attachments) shift; while [[ $# -gt 0 && "$1" != --* ]]; do attachments+=("$1"); shift; done ;;
1428
- --*) shift 2 2>/dev/null || shift ;; # skip unknown flags
1429
- *) positional+=("$1"); shift ;;
1430
- esac
1431
- done
1432
-
1433
- # Fall back to positional args if named args not used
1434
- [[ -z "$title" ]] && title="${positional[1]:-}"
1435
- [[ -z "$desc" ]] && desc="${positional[2]:-}"
1436
- [[ "$priority" == "P3" && -n "${positional[3]:-}" ]] && priority="${positional[3]}"
1437
- [[ -z "$issue_type" && -n "${positional[4]:-}" ]] && issue_type="${positional[4]}"
1438
- if [[ ${#attachments[@]} -eq 0 && ${#positional[@]} -gt 4 ]]; then
1439
- attachments=("${positional[@]:5}")
1440
- fi
1441
-
1442
- [[ -z "$title" ]] && { echo "Usage: viban add \"title\" [\"description\"] [priority] [type]"; exit 1; }
1443
-
1444
- local id=$(get_next_id) now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1445
- # Validate priority
1446
- [[ ! "$priority" =~ ^P[0-3]$ ]] && priority="P3"
1447
- # Validate type (bug, feat, chore, refactor)
1448
- [[ -n "$issue_type" && ! "$issue_type" =~ ^(bug|feat|chore|refactor)$ ]] && issue_type=""
1449
- # Build attachments JSON array
1450
- local attachments_json="[]"
1451
- if [[ ${#attachments[@]} -gt 0 ]]; then
1452
- attachments_json=$(printf '%s\n' "${attachments[@]}" | jq -R . | jq -s .)
1453
- fi
1454
- # New cards don't have order - they follow priority-based sorting
1455
- # Order is only assigned when manually moved
1456
- local tmpjson=$(mktemp)
1457
- printf '%s' "$desc" > "$tmpjson"
1458
- jq --arg id "$id" --arg title "$title" --rawfile desc "$tmpjson" --arg priority "$priority" --arg issue_type "$issue_type" --arg ext_id "$ext_id" --argjson attachments "$attachments_json" --arg now "$now" '
1459
- .next_id = ((.next_id // 0) + 1) |
1460
- .issues += [{
1461
- id:($id|tonumber),
1462
- title:$title,
1463
- description:$desc,
1464
- status:"backlog",
1465
- priority:$priority,
1466
- type:(if $issue_type == "" then null else $issue_type end),
1467
- external_id:(if $ext_id == "" then null else $ext_id end),
1468
- attachments:$attachments,
1469
- assigned_to:null,
1470
- created_at:$now,
1471
- updated_at:$now
1472
- }]' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1473
- rm -f "$tmpjson"
1474
-
1475
- # Auto-create remote issue if sync is configured and no ext_id provided
1476
- if [[ -z "$ext_id" && -f "$VIBAN_DATA_DIR/sync.json" ]]; then
1477
- local provider
1478
- provider=$(jq -r '.provider // ""' "$VIBAN_DATA_DIR/sync.json" 2>/dev/null)
1479
- if [[ -n "$provider" && "$provider" != "null" ]]; then
1480
- local created_ext_id
1481
- created_ext_id=$(VIBAN_JSON="$VIBAN_JSON" VIBAN_DATA_DIR="$VIBAN_DATA_DIR" \
1482
- VIBAN_PROVIDER="$provider" VIBAN_SCRIPT_DIR="$VIBAN_SCRIPT_DIR" \
1483
- bash "$VIBAN_SCRIPT_DIR/scripts/sync_create.sh" "$id" 2>/dev/null) || true
1484
- [[ -n "$created_ext_id" ]] && ext_id="$created_ext_id"
1485
- fi
1486
- fi
1487
-
1488
- local type_info=""
1489
- [[ -n "$issue_type" ]] && type_info=" [$issue_type]"
1490
- local attach_info=""
1491
- [[ ${#attachments[@]} -gt 0 ]] && attach_info=" +${#attachments[@]} files"
1492
- echo "✓ $(display_id "$id" "$ext_id") added ($priority)$type_info$attach_info"
1493
- }
1494
-
1495
- cmd_assign() {
1496
- init_json
1497
- local session="${1:-$(echo $RANDOM | md5 | head -c 8)}"
1498
- local issue=$(jq -r '.issues|map(select(.status=="backlog"))|sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end)|first' "$VIBAN_JSON")
1499
- [[ "$issue" == "null" || -z "$issue" ]] && { echo "No backlog"; exit 1; }
1500
- local id=$(printf '%s' "$issue" | jq -r '.id') now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1501
- local ext_id=$(printf '%s' "$issue" | jq -r '.external_id // ""')
1502
-
1503
- # Update status to in_progress (no worktree - use branch workflow)
1504
- jq --argjson id "$id" --arg s "$session" --arg now "$now" \
1505
- '(.issues[]|select((.id|tonumber)==$id)) |= . + {status:"in_progress",assigned_to:$s,updated_at:$now}' \
1506
- "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1507
-
1508
- # Set iTerm2 session name to issue display ID
1509
- local did; did=$(display_id "$id" "$ext_id")
1510
- printf '\033]1;%s\007' "$did"
1511
-
1512
- echo "✓ $did assigned"
1513
- echo "$id"
1514
- }
1515
-
1516
- cmd_review() {
1517
- init_json
1518
- local id="${1:-$(jq -r '.issues|map(select(.status=="in_progress"))|first|.id//empty' "$VIBAN_JSON")}"
1519
- [[ -z "$id" ]] && { echo "None"; exit 1; }
1520
- local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1521
- jq --argjson id "$id" --arg now "$now" \
1522
- '(.issues[]|select((.id|tonumber)==$id)) |= . + {status:"review",assigned_to:null,updated_at:$now}' \
1523
- "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1524
-
1525
- # Clear iTerm2 session name
1526
- printf '\033]1;\007'
1527
-
1528
- echo "✓ $(display_id "$id" "$(get_ext_id "$id")") → review"
1529
- }
1530
-
1531
- cmd_done() {
1532
- init_json
1533
- [[ -z "$1" ]] && { echo "Usage: viban done <id> [--remove]"; exit 1; }
1534
- local id="$1"
1535
- local remove=false
1536
- [[ "$2" == "--remove" ]] && remove=true
1537
-
1538
- # Cleanup worktree if exists
1539
- local repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
1540
- local wt_dir="$VIBAN_DATA_DIR/worktrees/$id"
1541
-
1542
- local branch="issue-$id"
1543
- local _ext_id=$(get_ext_id "$id")
1544
- if [[ -n "$_ext_id" && "$_ext_id" != "null" ]]; then
1545
- local _issue_num="${_ext_id##*:}"
1546
- if git -C "$repo_root" rev-parse --verify "issue-${_issue_num}" &>/dev/null 2>&1; then
1547
- branch="issue-${_issue_num}"
1548
- fi
1549
- fi
1550
-
1551
- if [[ -d "$wt_dir" ]]; then
1552
- git -C "$repo_root" worktree remove "$wt_dir" --force 2>/dev/null
1553
- git -C "$repo_root" branch -D "$branch" 2>/dev/null
1554
- echo "✓ worktree removed"
1555
- fi
1556
-
1557
- if $remove; then
1558
- # Delete card (old behavior)
1559
- jq --argjson id "$id" 'del(.issues[]|select((.id|tonumber)==$id))' \
1560
- "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1561
- printf '\033]1;\007'
1562
- echo "✓ $(display_id "$id" "$(get_ext_id "$id")") completed & removed"
1563
- else
1564
- # Move to done status (non-destructive default)
1565
- local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1566
- jq --argjson id "$id" --arg now "$now" \
1567
- '(.issues[]|select((.id|tonumber)==$id)) |= . + {status:"done",assigned_to:null,updated_at:$now}' \
1568
- "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1569
- printf '\033]1;\007'
1570
- echo "✓ $(display_id "$id" "$(get_ext_id "$id")") → done"
1571
- fi
1572
- }
1573
-
1574
- cmd_get() { init_json; jq --argjson id "$1" '.issues[]|select((.id|tonumber)==$id)' "$VIBAN_JSON"; }
1575
-
1576
- cmd_attach() {
1577
- init_json
1578
- [[ -z "$1" || -z "$2" ]] && { echo "Usage: viban attach <id> <file1> [file2...]"; exit 1; }
1579
- local id="$1"
1580
- shift
1581
- local files=("$@")
1582
-
1583
- # Check if issue exists
1584
- local exists=$(jq --argjson id "$id" '[.issues[]|select((.id|tonumber)==$id)]|length' "$VIBAN_JSON")
1585
- [[ "$exists" == "0" ]] && { echo "Error: Issue #$id not found"; exit 1; }
1586
-
1587
- # Build new attachments array
1588
- local new_attachments=$(printf '%s\n' "${files[@]}" | jq -R . | jq -s .)
1589
- local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1590
-
1591
- # Merge with existing attachments
1592
- jq --argjson id "$id" --argjson new "$new_attachments" --arg now "$now" '
1593
- (.issues[] | select((.id|tonumber)==$id)) |= . + {
1594
- attachments: ((.attachments // []) + $new | unique),
1595
- updated_at: $now
1596
- }
1597
- ' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1598
-
1599
- echo "✓ $(display_id "$id" "$(get_ext_id "$id")"): ${#files[@]} file(s) attached"
1600
- }
1601
-
1602
- cmd_migrate() {
1603
- init_json
1604
- echo "Migrating issues..."
1605
- local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1606
-
1607
- # Migration 1: extract [BUG], [FEATURE], [REFACTOR] from title to type field
1608
- # Also strip [P0-P3] from title if present (already in priority field)
1609
- echo " - Extracting type from titles..."
1610
- jq --arg now "$now" '
1611
- .issues = [.issues[] |
1612
- # Extract type from title
1613
- (if (.title | test("^\\[BUG\\]"; "i")) then "bug"
1614
- elif (.title | test("^\\[FEATURE\\]"; "i")) then "feat"
1615
- elif (.title | test("^\\[FEAT\\]"; "i")) then "feat"
1616
- elif (.title | test("^\\[REFACTOR\\]"; "i")) then "refactor"
1617
- elif (.title | test("^\\[CHORE\\]"; "i")) then "chore"
1618
- else .type // null end) as $extracted_type |
1619
-
1620
- # Clean title: remove [BUG], [FEATURE], [REFACTOR], [CHORE], [P0-P3] prefixes
1621
- (.title |
1622
- gsub("^\\[BUG\\]\\s*"; "") |
1623
- gsub("^\\[FEATURE\\]\\s*"; "") |
1624
- gsub("^\\[FEAT\\]\\s*"; "") |
1625
- gsub("^\\[REFACTOR\\]\\s*"; "") |
1626
- gsub("^\\[CHORE\\]\\s*"; "") |
1627
- gsub("^\\[P[0-3]\\]\\s*"; "")
1628
- ) as $clean_title |
1629
-
1630
- # Update issue
1631
- . + {
1632
- title: $clean_title,
1633
- type: (if $extracted_type then $extracted_type else .type end),
1634
- updated_at: $now
1635
- }
1636
- ]
1637
- ' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1638
-
1639
- # Migration 2: Remove order field from all issues
1640
- # New behavior: order is only set when manually moved, otherwise follows priority
1641
- echo " - Removing order field (reset to priority-based sorting)..."
1642
- jq --arg now "$now" '
1643
- .issues = [.issues[] | del(.order) | . + {updated_at: $now}]
1644
- ' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
1645
-
1646
- echo "✓ Migration complete"
1647
- echo ""
1648
- echo "Summary:"
1649
- jq -r '
1650
- [.issues[] | select(.type != null)] | group_by(.type) |
1651
- .[] | " \(.[0].type): \(length) issues"
1652
- ' "$VIBAN_JSON"
1653
- echo " (no type): $(jq '[.issues[] | select(.type == null)] | length' "$VIBAN_JSON") issues"
1654
- echo ""
1655
- echo "Issues by priority:"
1656
- jq -r '
1657
- [.issues[] | select(.status != "done")] |
1658
- group_by(.priority // "P3") | sort_by(.[0].priority) |
1659
- .[] | " \(.[0].priority // "P3"): \(length) issues"
1660
- ' "$VIBAN_JSON"
1661
- }
1662
-
1663
- cmd_sync() {
1664
- local provider="${VIBAN_SYNC_PROVIDER:-}"
1665
- # Auto-detect provider from existing sync.json or default to github
1666
- if [[ -z "$provider" && -f "$VIBAN_DATA_DIR/sync.json" ]]; then
1667
- provider=$(jq -r '.provider // "github"' "$VIBAN_DATA_DIR/sync.json")
1668
- fi
1669
- provider="${provider:-github}"
1670
-
1671
- local provider_script="$VIBAN_SCRIPT_DIR/scripts/providers/${provider}.sh"
1672
- if [[ ! -f "$provider_script" ]]; then
1673
- echo "Error: Unknown sync provider '$provider'"
1674
- echo "Available: $(ls "$VIBAN_SCRIPT_DIR/scripts/providers/" 2>/dev/null | sed 's/\.sh$//' | tr '\n' ' ')"
1675
- exit 1
1676
- fi
1677
-
1678
- VIBAN_JSON="$VIBAN_JSON" VIBAN_DATA_DIR="$VIBAN_DATA_DIR" \
1679
- VIBAN_PROVIDER="$provider" VIBAN_SCRIPT_DIR="$VIBAN_SCRIPT_DIR" \
1680
- bash "$VIBAN_SCRIPT_DIR/scripts/sync.sh" "$@"
1681
- }
198
+ # ============================================================
199
+ # Source library modules
200
+ # ============================================================
201
+ source "$VIBAN_SCRIPT_DIR/lib/config.zsh"
202
+ source "$VIBAN_SCRIPT_DIR/lib/helpers.zsh"
203
+ source "$VIBAN_SCRIPT_DIR/lib/tui.zsh"
204
+ source "$VIBAN_SCRIPT_DIR/lib/commands.zsh"
1682
205
 
206
+ # ============================================================
207
+ # Main dispatch
208
+ # ============================================================
1683
209
  main() {
1684
210
  check_deps
1685
211
  init_json
1686
212
  case "${1:-}" in
1687
- list) cmd_list;;
213
+ list) shift; cmd_list "$@";;
214
+ history) cmd_history;;
1688
215
  add) shift; cmd_add "$@";;
1689
216
  attach) shift; cmd_attach "$@";;
1690
217
  assign) cmd_assign "$2";;
1691
218
  review) cmd_review "$2";;
1692
- done) cmd_done "$2" "$3";;
219
+ done) shift; cmd_done "$@";;
220
+ move) cmd_move "$2" "$3";;
1693
221
  get) cmd_get "$2";;
222
+ comment) shift; cmd_comment "$@";;
223
+ link) cmd_link "$2" "$3" "$4";;
224
+ unlink) shift; cmd_unlink "$@";;
1694
225
  edit) [[ -z "$2" ]] && { echo "Usage: viban edit <id>"; exit 1; }; edit_issue "$2";;
1695
226
  priority) cmd_priority "$2" "$3";;
227
+ stats) cmd_stats;;
228
+ backup) cmd_backup;;
229
+ restore) shift; cmd_restore "$@";;
230
+ changelog) cmd_changelog "$2";;
231
+ export) cmd_export "$2";;
1696
232
  migrate) cmd_migrate;;
1697
233
  sync) shift; cmd_sync "$@";;
1698
234
  --version|-v)
@@ -1717,14 +253,25 @@ main() {
1717
253
  echo ""
1718
254
  echo " viban TUI"
1719
255
  echo " viban list Show board"
1720
- echo " viban add \"title\" [\"desc\"] [P0-P3] [type] [files...] Add task"
256
+ echo " viban list [--status <s>] [--priority P0,P1] [--type bug] [--search text]"
257
+ echo " viban history Show completed issues"
258
+ echo " viban add \"title\" [\"desc\"] [P0-P3] [type] [--parent <id>] Add task"
1721
259
  echo " viban attach <id> <file1> [file2...] Attach files to task"
1722
260
  echo " viban priority <id> <P0-P3> Set priority"
1723
261
  echo " viban assign Assign first backlog (by priority)"
1724
262
  echo " viban review → Human Review"
1725
- echo " viban done <id> [--remove] Complete (--remove to delete)"
263
+ echo " viban move <id> <status> Move to status (backlog,in_progress,review,done)"
264
+ echo " viban done <id> [--purge] Complete (--purge to permanently delete)"
265
+ echo " viban comment <id> \"msg\" Add comment to task"
266
+ echo " viban link <id> blocks <id> Add dependency"
267
+ echo " viban unlink <id> blocks <id> Remove dependency"
1726
268
  echo " viban edit <id> Edit task in editor"
1727
269
  echo " viban get <id> Get task details (JSON)"
270
+ echo " viban stats Show throughput metrics and statistics"
271
+ echo " viban backup Snapshot viban.json to backups/"
272
+ echo " viban restore [f] List or restore a backup"
273
+ echo " viban changelog [range] Generate changelog from commits"
274
+ echo " viban export [md|html] Export board as markdown or HTML"
1728
275
  echo " viban migrate Migrate: extract type from title"
1729
276
  echo " viban sync Sync with external issue tracker (GitHub, etc.)"
1730
277
  echo " viban update Update to latest version (if available)"