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/.claude-plugin/plugin.json +1 -1
- package/README.md +209 -310
- package/assets/viban.png +0 -0
- package/bin/viban +35 -1488
- package/commands/assign.md +3 -5
- package/package.json +1 -1
- package/scripts/providers/github.sh +8 -0
- package/scripts/sync.sh +118 -21
- package/scripts/sync_create.sh +1 -0
- package/skills/assign/SKILL.md +7 -0
- package/skills/parallel-assign/SKILL.md +7 -0
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
|
-
#
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 "
|
|
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
|
|
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
|
|
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)"
|