claude-plugin-viban 1.3.12 → 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 +208 -311
- package/bin/viban +21 -1704
- 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,1654 +195,17 @@ 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 done_ids_tui=$(printf '%s' "$json_data" | jq '[.issues[]|select(.status=="done")|.id]')
|
|
605
|
-
local issues_data=$(printf '%s' "$json_data" | jq -r --arg s "$st" --argjson done "$done_ids_tui" "
|
|
606
|
-
.issues | map(select(.status==\$s)) | $sort_expr |
|
|
607
|
-
.[] | \"\\(.id)\t\\(.title)\t\\((.description // \"\") | gsub(\"[\\n\\t\\r]\"; \" \"))\t\\(.priority // \"P3\")\t\\(.type // \"\")\t\\(.external_id // \"\")\t\\(if ((.blocked_by // []) | length > 0 and any(. as \$b | \$done | index(\$b) == null)) then \"blocked\" else \"\" end)\"")
|
|
608
|
-
local count=0
|
|
609
|
-
# Count total issues (not capped) for overflow indicator
|
|
610
|
-
if [[ -n "$issues_data" ]]; then
|
|
611
|
-
local -a _count_arr=("${(f)issues_data}")
|
|
612
|
-
count=${#_count_arr[@]}
|
|
613
|
-
fi
|
|
614
|
-
|
|
615
|
-
# Header centered in column
|
|
616
|
-
local hdr_text="● $label"
|
|
617
|
-
local hdr_w=$((${#label} + 2))
|
|
618
|
-
local left_pad=$(( (col_w - hdr_w) / 2 ))
|
|
619
|
-
local right_pad=$((col_w - hdr_w - left_pad))
|
|
620
|
-
if (( is_col_selected )); then
|
|
621
|
-
printf "%${left_pad}s${A_BOLD}${A_SELECTED}%s${A_RESET}%${right_pad}s\n" "" "$hdr_text" ""
|
|
622
|
-
# Underline for selected column - use printf repeat pattern
|
|
623
|
-
local underline=$(printf '─%.0s' {1..$hdr_w})
|
|
624
|
-
printf "%${left_pad}s${A_SELECTED}%s${A_RESET}%${right_pad}s\n" "" "$underline" ""
|
|
625
|
-
else
|
|
626
|
-
printf "%${left_pad}s${color}%s${A_RESET}%${right_pad}s\n" "" "$hdr_text" ""
|
|
627
|
-
# Empty line for non-selected columns
|
|
628
|
-
printf "%${col_w}s\n" ""
|
|
629
|
-
fi
|
|
630
|
-
|
|
631
|
-
local lines_used=2
|
|
632
|
-
local card_inner=$((col_w - 4))
|
|
633
|
-
local border=$(gen_border $card_inner)
|
|
634
|
-
|
|
635
|
-
# --- Pass 1: Collect card data into arrays ---
|
|
636
|
-
local -a _ids _titles _descs _priorities _types _ext_ids _title_max_ws _title_pfxs
|
|
637
|
-
local _has_nonascii=0
|
|
638
|
-
local _desc_max_w=$((card_inner - 4))
|
|
639
|
-
local _spinner_w=0
|
|
640
|
-
[[ "$st" == "in_progress" ]] && _spinner_w=2
|
|
641
|
-
local _cc _bc _pfx _did
|
|
642
|
-
|
|
643
|
-
local -a _blocked_flags=()
|
|
644
|
-
while IFS=$'\t' read -r _id _title _desc _priority _type _ext_id _blocked; do
|
|
645
|
-
[[ -z "$_id" ]] && continue
|
|
646
|
-
(( ${#_ids} >= CACHED_MAX_TASKS )) && break
|
|
647
|
-
[[ -z "$_priority" || "$_priority" == "null" ]] && _priority="P3"
|
|
648
|
-
[[ -z "$_type" || "$_type" == "null" ]] && _type=""
|
|
649
|
-
[[ "$_desc" == "null" ]] && _desc=""
|
|
650
|
-
|
|
651
|
-
_ids+=("$_id"); _titles+=("$_title"); _descs+=("$_desc")
|
|
652
|
-
_priorities+=("$_priority"); _types+=("$_type"); _ext_ids+=("$_ext_id")
|
|
653
|
-
_blocked_flags+=("$_blocked")
|
|
654
|
-
|
|
655
|
-
# Per-card title width limit (use display ID length)
|
|
656
|
-
_did=$(display_id "$_id" "$_ext_id")
|
|
657
|
-
_title_max_ws+=($((card_inner - 4 - ${#_did} - _spinner_w)))
|
|
658
|
-
# Prefix for width calc (X as spinner placeholder - same width 1 as braille chars)
|
|
659
|
-
_pfx=" "
|
|
660
|
-
(( _spinner_w )) && _pfx=" X "
|
|
661
|
-
_title_pfxs+=("${_pfx}${_did} ")
|
|
662
|
-
|
|
663
|
-
# Check for non-ASCII
|
|
664
|
-
if (( ! _has_nonascii )); then
|
|
665
|
-
_cc=${#_title}
|
|
666
|
-
LC_ALL=C _bc=${#_title}; unset LC_ALL
|
|
667
|
-
(( _bc != _cc )) && _has_nonascii=1
|
|
668
|
-
if (( ! _has_nonascii && ${#_desc} > 0 )); then
|
|
669
|
-
_cc=${#_desc}; LC_ALL=C _bc=${#_desc}; unset LC_ALL
|
|
670
|
-
(( _bc != _cc )) && _has_nonascii=1
|
|
671
|
-
fi
|
|
672
|
-
fi
|
|
673
|
-
done <<< "$issues_data"
|
|
674
|
-
|
|
675
|
-
local _n=${#_ids}
|
|
676
|
-
|
|
677
|
-
# --- Pass 2: Batch compute truncation + widths (single Python call) ---
|
|
678
|
-
local -a _short_titles _title_cws _short_descs _desc_cws
|
|
679
|
-
|
|
680
|
-
if (( _n > 0 )); then
|
|
681
|
-
if (( _has_nonascii )); then
|
|
682
|
-
# Build batch input: 2 lines per card (title, desc)
|
|
683
|
-
# Format: max_w<TAB>prefix<TAB>string
|
|
684
|
-
local _batch_input=""
|
|
685
|
-
for (( _i=1; _i<=_n; _i++ )); do
|
|
686
|
-
_batch_input+="${_title_max_ws[$_i]}"$'\t'"${_title_pfxs[$_i]}"$'\t'"${_titles[$_i]}"$'\n'
|
|
687
|
-
_batch_input+="${_desc_max_w}"$'\t'" "$'\t'"${_descs[$_i]}"$'\n'
|
|
688
|
-
done
|
|
689
|
-
|
|
690
|
-
# Single Python call: truncate each string and compute content width
|
|
691
|
-
local _batch_output
|
|
692
|
-
_coproc_batch_trunc "$_batch_input"
|
|
693
|
-
_batch_output="$_COPROC_RESULT"
|
|
694
|
-
|
|
695
|
-
local _li=0
|
|
696
|
-
while IFS=$'\t' read -r _tr _cw; do
|
|
697
|
-
((_li++))
|
|
698
|
-
if (( _li % 2 == 1 )); then
|
|
699
|
-
_short_titles+=("$_tr"); _title_cws+=($_cw)
|
|
700
|
-
else
|
|
701
|
-
_short_descs+=("$_tr"); _desc_cws+=($_cw)
|
|
702
|
-
fi
|
|
703
|
-
done <<< "$_batch_output"
|
|
704
|
-
else
|
|
705
|
-
# All-ASCII fast path - no Python needed
|
|
706
|
-
local _t _mw _fc _d
|
|
707
|
-
for (( _i=1; _i<=_n; _i++ )); do
|
|
708
|
-
_t="${_titles[$_i]}" _mw=${_title_max_ws[$_i]}
|
|
709
|
-
(( ${#_t} > _mw )) && _t="${_t:0:$_mw}"
|
|
710
|
-
_short_titles+=("$_t")
|
|
711
|
-
_fc="${_title_pfxs[$_i]}${_t}"
|
|
712
|
-
_title_cws+=(${#_fc})
|
|
713
|
-
|
|
714
|
-
_d="${_descs[$_i]}"
|
|
715
|
-
(( ${#_d} > _desc_max_w )) && _d="${_d:0:$_desc_max_w}"
|
|
716
|
-
_short_descs+=("$_d")
|
|
717
|
-
_desc_cws+=($((2 + ${#_d})))
|
|
718
|
-
done
|
|
719
|
-
fi
|
|
720
|
-
fi
|
|
721
|
-
|
|
722
|
-
# --- Pass 3: Render cards ---
|
|
723
|
-
local shown=0
|
|
724
|
-
local id priority issue_type ext_id did
|
|
725
|
-
local spinner_prefix title_content title_pad
|
|
726
|
-
local desc_content desc_pad
|
|
727
|
-
local priority_tag priority_color type_tag type_color tags_w tags_pad
|
|
728
|
-
local border_color text_color desc_color
|
|
729
|
-
for (( _i=1; _i<=_n; _i++ )); do
|
|
730
|
-
id="${_ids[$_i]}"
|
|
731
|
-
priority="${_priorities[$_i]}"
|
|
732
|
-
issue_type="${_types[$_i]}"
|
|
733
|
-
ext_id="${_ext_ids[$_i]}"
|
|
734
|
-
did=$(display_id "$id" "$ext_id")
|
|
735
|
-
|
|
736
|
-
# Title line
|
|
737
|
-
spinner_prefix=""
|
|
738
|
-
[[ "$st" == "in_progress" ]] && spinner_prefix="${SPINNER_FRAMES[$((SPINNER_IDX % ${#SPINNER_FRAMES[@]} + 1))]} "
|
|
739
|
-
title_content=" ${spinner_prefix}${did} ${_short_titles[$_i]}"
|
|
740
|
-
title_pad=$((card_inner - ${_title_cws[$_i]}))
|
|
741
|
-
(( title_pad < 0 )) && title_pad=0
|
|
742
|
-
|
|
743
|
-
# Description line
|
|
744
|
-
desc_content=" ${_short_descs[$_i]}"
|
|
745
|
-
desc_pad=$((card_inner - ${_desc_cws[$_i]}))
|
|
746
|
-
(( desc_pad < 0 )) && desc_pad=0
|
|
747
|
-
|
|
748
|
-
# Priority, type, and blocked tags
|
|
749
|
-
priority_tag="[$priority]"
|
|
750
|
-
priority_color="${PRIORITY_COLOR[$priority]:-$A_DIM}"
|
|
751
|
-
type_tag="" type_color="" tags_w=0
|
|
752
|
-
local blocked_tag="" blocked_color=""
|
|
753
|
-
if [[ -n "$issue_type" ]]; then
|
|
754
|
-
type_tag="[${TYPE_LABEL[$issue_type]:-$issue_type}]"
|
|
755
|
-
type_color="${TYPE_COLOR[$issue_type]:-$A_DIM}"
|
|
756
|
-
tags_w=$((${#priority_tag} + 1 + ${#type_tag}))
|
|
757
|
-
else
|
|
758
|
-
tags_w=${#priority_tag}
|
|
759
|
-
fi
|
|
760
|
-
if [[ "${_blocked_flags[$_i]}" == "blocked" ]]; then
|
|
761
|
-
blocked_tag=" BLOCKED"
|
|
762
|
-
blocked_color="\033[38;2;255;69;58m"
|
|
763
|
-
tags_w=$((tags_w + 8))
|
|
764
|
-
fi
|
|
765
|
-
tags_pad=$((card_inner - tags_w - 2))
|
|
766
|
-
|
|
767
|
-
border_color="$A_DIM"
|
|
768
|
-
text_color="$A_FG"
|
|
769
|
-
desc_color="$A_DIM"
|
|
770
|
-
if (( is_col_selected && shown == card_sel )); then
|
|
771
|
-
border_color="${A_SELECTED}"
|
|
772
|
-
text_color="${A_BOLD}${A_ACCENT}"
|
|
773
|
-
desc_color="${A_ACCENT}"
|
|
774
|
-
fi
|
|
775
|
-
|
|
776
|
-
# 5-line card with priority+type tags on 4th line
|
|
777
|
-
printf " ${border_color}╭%s╮${A_RESET} \n" "$border"
|
|
778
|
-
printf " ${border_color}│${A_RESET}${text_color}%s${A_RESET}%${title_pad}s${border_color}│${A_RESET} \n" "$title_content" ""
|
|
779
|
-
printf " ${border_color}│${A_RESET}${desc_color}%s${A_RESET}%${desc_pad}s${border_color}│${A_RESET} \n" "$desc_content" ""
|
|
780
|
-
if [[ -n "$type_tag" ]]; then
|
|
781
|
-
printf " ${border_color}│${A_RESET} ${priority_color}%s${A_RESET} ${type_color}%s${A_RESET}${blocked_color}%s${A_RESET}%${tags_pad}s${border_color}│${A_RESET} \n" "$priority_tag" "$type_tag" "$blocked_tag" ""
|
|
782
|
-
else
|
|
783
|
-
printf " ${border_color}│${A_RESET} ${priority_color}%s${A_RESET}${blocked_color}%s${A_RESET}%${tags_pad}s${border_color}│${A_RESET} \n" "$priority_tag" "$blocked_tag" ""
|
|
784
|
-
fi
|
|
785
|
-
printf " ${border_color}╰%s╯${A_RESET} \n" "$border"
|
|
786
|
-
|
|
787
|
-
((shown++))
|
|
788
|
-
lines_used=$((lines_used + 5))
|
|
789
|
-
done
|
|
790
|
-
|
|
791
|
-
# Overflow indicator
|
|
792
|
-
if (( count > _n )); then
|
|
793
|
-
local more_text=" +$((count - _n)) more..."
|
|
794
|
-
printf "${A_DIM}%s${A_RESET}%$((col_w - ${#more_text}))s\n" "$more_text" ""
|
|
795
|
-
((lines_used++))
|
|
796
|
-
fi
|
|
797
|
-
|
|
798
|
-
if (( count == 0 )); then
|
|
799
|
-
local no_text=" No tasks"
|
|
800
|
-
local no_w=${#no_text}
|
|
801
|
-
printf "${A_DIM}%s${A_RESET}%$((col_w - no_w))s\n" "$no_text" ""
|
|
802
|
-
((lines_used++))
|
|
803
|
-
fi
|
|
804
|
-
|
|
805
|
-
while (( lines_used < max_h )); do
|
|
806
|
-
printf "%${col_w}s\n" ""
|
|
807
|
-
((lines_used++))
|
|
808
|
-
done
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
# ESC character for ANSI stripping (defined once at script level)
|
|
812
|
-
_ESC=$'\e'
|
|
813
|
-
|
|
814
|
-
# Pad line to exact width with spaces
|
|
815
|
-
# Optimized: use zsh parameter expansion to strip ANSI codes
|
|
816
|
-
pad_to_width() {
|
|
817
|
-
local line="$1"
|
|
818
|
-
local width="$2"
|
|
819
|
-
local precomputed_w="${3:-}"
|
|
820
|
-
# Strip ANSI codes: ESC [ followed by numbers/semicolons, ending with letter
|
|
821
|
-
local plain="${line//${_ESC}\[[0-9;]#[a-zA-Z]/}"
|
|
822
|
-
local display_w
|
|
823
|
-
if [[ -n "$precomputed_w" ]]; then
|
|
824
|
-
display_w=$precomputed_w
|
|
825
|
-
else
|
|
826
|
-
local char_count=${#plain} byte_count
|
|
827
|
-
LC_ALL=C byte_count=${#plain}
|
|
828
|
-
unset LC_ALL
|
|
829
|
-
if [[ $byte_count -eq $char_count ]]; then
|
|
830
|
-
display_w=$char_count
|
|
831
|
-
else
|
|
832
|
-
display_w=$(( char_count + (byte_count - char_count) / 2 ))
|
|
833
|
-
fi
|
|
834
|
-
fi
|
|
835
|
-
local pad=$((width - display_w))
|
|
836
|
-
printf '%s' "$line"
|
|
837
|
-
(( pad > 0 )) && printf "%${pad}s" ""
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
# Draw the board (optimized - arrays instead of temp files)
|
|
841
|
-
# $1: col_sel, $2: card_sel, $3: json_data
|
|
842
|
-
draw_board() {
|
|
843
|
-
local col_sel=${1:-0}
|
|
844
|
-
local card_sel=${2:--1}
|
|
845
|
-
local json_data="$3"
|
|
846
|
-
local col_w=$CACHED_COL_W
|
|
847
|
-
local max_h=$CACHED_MAX_H
|
|
848
|
-
|
|
849
|
-
local c1=-1 c2=-1 c3=-1
|
|
850
|
-
case $col_sel in
|
|
851
|
-
0) c1=$card_sel;;
|
|
852
|
-
1) c2=$card_sel;;
|
|
853
|
-
2) c3=$card_sel;;
|
|
854
|
-
esac
|
|
855
|
-
|
|
856
|
-
# Build columns to arrays
|
|
857
|
-
local -a col1 col2 col3
|
|
858
|
-
col1=("${(@f)$(build_column_lines "backlog" $((col_sel == 0)) $c1 $max_h $col_w "$json_data")}")
|
|
859
|
-
col2=("${(@f)$(build_column_lines "in_progress" $((col_sel == 1)) $c2 $max_h $col_w "$json_data")}")
|
|
860
|
-
col3=("${(@f)$(build_column_lines "review" $((col_sel == 2)) $c3 $max_h $col_w "$json_data")}")
|
|
861
|
-
|
|
862
|
-
# Batch compute display widths for all non-ASCII lines (single Python call)
|
|
863
|
-
# Build input: all lines from all 3 columns, ANSI-stripped
|
|
864
|
-
local -a all_plains
|
|
865
|
-
local -a all_widths
|
|
866
|
-
local _needs_python=0
|
|
867
|
-
local i _plain _cc _bc
|
|
868
|
-
for ((i=1; i<=max_h; i++)); do
|
|
869
|
-
for _col_line in "${col1[$i]}" "${col2[$i]}" "${col3[$i]}"; do
|
|
870
|
-
_plain="${_col_line//${_ESC}\[[0-9;]#[a-zA-Z]/}"
|
|
871
|
-
all_plains+=("$_plain")
|
|
872
|
-
_cc=${#_plain}
|
|
873
|
-
LC_ALL=C _bc=${#_plain}
|
|
874
|
-
unset LC_ALL
|
|
875
|
-
if [[ $_bc -eq $_cc ]]; then
|
|
876
|
-
all_widths+=($_cc)
|
|
877
|
-
else
|
|
878
|
-
all_widths+=(-1) # marker: needs Python
|
|
879
|
-
_needs_python=1
|
|
880
|
-
fi
|
|
881
|
-
done
|
|
882
|
-
done
|
|
883
|
-
|
|
884
|
-
if (( _needs_python )); then
|
|
885
|
-
# Single Python call to compute all non-ASCII widths
|
|
886
|
-
local _input="" _idx
|
|
887
|
-
for ((_idx=1; _idx<=${#all_plains[@]}; _idx++)); do
|
|
888
|
-
if [[ ${all_widths[$_idx]} -eq -1 ]]; then
|
|
889
|
-
_input+="${all_plains[$_idx]}"$'\n'
|
|
890
|
-
fi
|
|
891
|
-
done
|
|
892
|
-
local -a _py_results
|
|
893
|
-
_coproc_batch_width "$_input"
|
|
894
|
-
_py_results=("${(@f)_COPROC_RESULT}")
|
|
895
|
-
# Map Python results back to width array
|
|
896
|
-
local _pi=1
|
|
897
|
-
for ((_idx=1; _idx<=${#all_widths[@]}; _idx++)); do
|
|
898
|
-
if [[ ${all_widths[$_idx]} -eq -1 ]]; then
|
|
899
|
-
all_widths[$_idx]=${_py_results[$_pi]}
|
|
900
|
-
((_pi++))
|
|
901
|
-
fi
|
|
902
|
-
done
|
|
903
|
-
fi
|
|
904
|
-
|
|
905
|
-
# Merge line by line using precomputed widths
|
|
906
|
-
local _wi=1
|
|
907
|
-
for ((i=1; i<=max_h; i++)); do
|
|
908
|
-
pad_to_width "${col1[$i]}" $col_w "${all_widths[$_wi]}"
|
|
909
|
-
((_wi++))
|
|
910
|
-
printf "${A_DIM}│${A_RESET}"
|
|
911
|
-
pad_to_width "${col2[$i]}" $col_w "${all_widths[$_wi]}"
|
|
912
|
-
((_wi++))
|
|
913
|
-
printf "${A_DIM}│${A_RESET}"
|
|
914
|
-
pad_to_width "${col3[$i]}" $col_w "${all_widths[$_wi]}"
|
|
915
|
-
((_wi++))
|
|
916
|
-
printf '\033[K\n'
|
|
917
|
-
done
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
draw_footer() {
|
|
921
|
-
printf '\033[K\n'
|
|
922
|
-
print_center "←→ Column │ ↑↓ Card │ Shift+↑↓ Reorder │ Shift+←→ Move │ Enter Edit/PR │ ⌫ Del │ A Add │ Q Quit" "${A_DIM}"
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
read_key() {
|
|
926
|
-
local _rk_timeout="${1:-0.5}"
|
|
927
|
-
local key result=""
|
|
928
|
-
# Timeout for spinner animation refresh (default 0.5s)
|
|
929
|
-
read -sk1 -t "$_rk_timeout" key 2>/dev/null || { echo "timeout"; return; }
|
|
930
|
-
|
|
931
|
-
if [[ "$key" == $'\e' ]]; then
|
|
932
|
-
read -sk1 -t 0.1 c2 2>/dev/null
|
|
933
|
-
if [[ "$c2" == "[" ]]; then
|
|
934
|
-
read -sk1 -t 0.1 c3 2>/dev/null
|
|
935
|
-
case "$c3" in
|
|
936
|
-
D) result="left";; C) result="right";;
|
|
937
|
-
A) result="up";; B) result="down";;
|
|
938
|
-
"1")
|
|
939
|
-
# Handle Shift+arrow sequences: ESC[1;2X where X is A/B/C/D
|
|
940
|
-
read -sk1 -t 0.1 c4 2>/dev/null
|
|
941
|
-
if [[ "$c4" == ";" ]]; then
|
|
942
|
-
read -sk1 -t 0.1 c5 2>/dev/null
|
|
943
|
-
read -sk1 -t 0.1 c6 2>/dev/null
|
|
944
|
-
if [[ "$c5" == "2" ]]; then
|
|
945
|
-
case "$c6" in
|
|
946
|
-
A) result="shift_up";;
|
|
947
|
-
B) result="shift_down";;
|
|
948
|
-
C) result="shift_right";;
|
|
949
|
-
D) result="shift_left";;
|
|
950
|
-
esac
|
|
951
|
-
fi
|
|
952
|
-
fi
|
|
953
|
-
;;
|
|
954
|
-
esac
|
|
955
|
-
elif [[ "$c2" == "]" ]]; then
|
|
956
|
-
# Drain OSC sequence
|
|
957
|
-
while read -sk1 -t 0.01 _ 2>/dev/null; do :; done
|
|
958
|
-
fi
|
|
959
|
-
elif [[ "$key" == "" || "$key" == $'\n' ]]; then
|
|
960
|
-
result="enter"
|
|
961
|
-
elif [[ "$key" == $'\x7f' || "$key" == $'\b' ]]; then
|
|
962
|
-
result="backspace"
|
|
963
|
-
else
|
|
964
|
-
case "$key" in
|
|
965
|
-
q|Q) result="quit";;
|
|
966
|
-
a|A) result="add";;
|
|
967
|
-
esac
|
|
968
|
-
fi
|
|
969
|
-
|
|
970
|
-
echo "$result"
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
# Move card order up or down within a status column (fractional indexing)
|
|
974
|
-
# $1: status, $2: current card index, $3: direction (-1 for up, 1 for down)
|
|
975
|
-
# When manually moved, the card gets an order value to pin its position
|
|
976
|
-
move_card_order() {
|
|
977
|
-
local st="$1"
|
|
978
|
-
local cur_idx="$2"
|
|
979
|
-
local dir="$3"
|
|
980
|
-
local new_idx=$((cur_idx + dir))
|
|
981
|
-
|
|
982
|
-
# Get issues in current effective order (ordered cards first, then priority-sorted)
|
|
983
|
-
local issues=$(jq -r --arg s "$st" '
|
|
984
|
-
.issues | map(select(.status==$s)) | sort_by(
|
|
985
|
-
if .order != null then [0, .order]
|
|
986
|
-
else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id]
|
|
987
|
-
end
|
|
988
|
-
)
|
|
989
|
-
' "$VIBAN_JSON")
|
|
990
|
-
local cnt=$(printf '%s' "$issues" | jq 'length')
|
|
991
|
-
|
|
992
|
-
# Bounds check
|
|
993
|
-
(( new_idx < 0 || new_idx >= cnt )) && return 1
|
|
994
|
-
|
|
995
|
-
# Get ID of the card to move
|
|
996
|
-
local cur_id=$(printf '%s' "$issues" | jq -r ".[$cur_idx].id")
|
|
997
|
-
|
|
998
|
-
# Calculate effective order for a card (use actual order or virtual priority-based order)
|
|
999
|
-
get_eff_order() {
|
|
1000
|
-
local idx=$1
|
|
1001
|
-
local order=$(printf '%s' "$issues" | jq -r ".[$idx].order // \"null\"")
|
|
1002
|
-
if [[ "$order" != "null" ]]; then
|
|
1003
|
-
echo "$order"
|
|
1004
|
-
else
|
|
1005
|
-
local priority=$(printf '%s' "$issues" | jq -r ".[$idx].priority // \"P3\"")
|
|
1006
|
-
local id=$(printf '%s' "$issues" | jq -r ".[$idx].id")
|
|
1007
|
-
case "$priority" in
|
|
1008
|
-
P0) echo $((1000000 + id));;
|
|
1009
|
-
P1) echo $((2000000 + id));;
|
|
1010
|
-
P2) echo $((3000000 + id));;
|
|
1011
|
-
*) echo $((4000000 + id));;
|
|
1012
|
-
esac
|
|
1013
|
-
fi
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
# Calculate new order using fractional indexing
|
|
1017
|
-
# Place card between the target position and its neighbor
|
|
1018
|
-
local new_order
|
|
1019
|
-
if (( dir < 0 )); then
|
|
1020
|
-
# Moving up: place between target and the one above it
|
|
1021
|
-
local target_order=$(get_eff_order $new_idx)
|
|
1022
|
-
if (( new_idx == 0 )); then
|
|
1023
|
-
# Moving to top: use target_order - 1
|
|
1024
|
-
new_order=$(echo "$target_order - 1" | bc)
|
|
1025
|
-
else
|
|
1026
|
-
local above_order=$(get_eff_order $(($new_idx - 1)))
|
|
1027
|
-
new_order=$(echo "scale=6; ($above_order + $target_order) / 2" | bc)
|
|
1028
|
-
fi
|
|
1029
|
-
else
|
|
1030
|
-
# Moving down: place between target and the one below it
|
|
1031
|
-
local target_order=$(get_eff_order $new_idx)
|
|
1032
|
-
if (( new_idx == cnt - 1 )); then
|
|
1033
|
-
# Moving to bottom: use target_order + 1
|
|
1034
|
-
new_order=$(echo "$target_order + 1" | bc)
|
|
1035
|
-
else
|
|
1036
|
-
local below_order=$(get_eff_order $(($new_idx + 1)))
|
|
1037
|
-
new_order=$(echo "scale=6; ($target_order + $below_order) / 2" | bc)
|
|
1038
|
-
fi
|
|
1039
|
-
fi
|
|
1040
|
-
|
|
1041
|
-
# Update the card's order (this pins it to the new position)
|
|
1042
|
-
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1043
|
-
jq --argjson cur_id "$cur_id" --argjson new_order "$new_order" --arg now "$now" '
|
|
1044
|
-
(.issues[] | select(.id==$cur_id)) |= . + {order:$new_order,updated_at:$now}
|
|
1045
|
-
' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1046
|
-
|
|
1047
|
-
return 0
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
# Get issue ID by status and index (uses correct sort order per status)
|
|
1051
|
-
get_issue_id_by_index() {
|
|
1052
|
-
local st=$1 idx=$2
|
|
1053
|
-
local sort_expr=$(get_sort_expr "$st")
|
|
1054
|
-
jq -r --arg s "$st" --argjson i "$idx" ".issues | map(select(.status==\$s)) | $sort_expr | .[\$i].id // empty" "$VIBAN_JSON"
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
# Delete issue by ID (with worktree cleanup)
|
|
1058
|
-
delete_issue() {
|
|
1059
|
-
local id=$1
|
|
1060
|
-
local repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
1061
|
-
local wt_dir="$VIBAN_DATA_DIR/worktrees/$id"
|
|
1062
|
-
|
|
1063
|
-
local branch="issue-$id"
|
|
1064
|
-
local _ext_id=$(get_ext_id "$id")
|
|
1065
|
-
if [[ -n "$_ext_id" && "$_ext_id" != "null" ]]; then
|
|
1066
|
-
local _issue_num="${_ext_id##*:}"
|
|
1067
|
-
if git -C "$repo_root" rev-parse --verify "issue-${_issue_num}" &>/dev/null 2>&1; then
|
|
1068
|
-
branch="issue-${_issue_num}"
|
|
1069
|
-
fi
|
|
1070
|
-
fi
|
|
1071
|
-
|
|
1072
|
-
if [[ -d "$wt_dir" ]]; then
|
|
1073
|
-
git -C "$repo_root" worktree remove "$wt_dir" --force 2>/dev/null
|
|
1074
|
-
git -C "$repo_root" branch -D "$branch" 2>/dev/null
|
|
1075
|
-
fi
|
|
1076
|
-
jq --argjson id "$id" 'del(.issues[]|select((.id|tonumber)==$id))' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && \
|
|
1077
|
-
mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
level1_columns() {
|
|
1081
|
-
IN_TUI=true
|
|
1082
|
-
_start_coproc
|
|
1083
|
-
local col=0 card=0
|
|
1084
|
-
|
|
1085
|
-
# Auto-sync state (120 iterations × 0.5s timeout = ~60s interval)
|
|
1086
|
-
local _sync_counter=0
|
|
1087
|
-
local _SYNC_INTERVAL=120
|
|
1088
|
-
local _sync_pid=""
|
|
1089
|
-
|
|
1090
|
-
# Hide cursor and disable input echo
|
|
1091
|
-
stty -echo 2>/dev/null
|
|
1092
|
-
printf '\033[?25l\033[2J\033[H'
|
|
1093
|
-
|
|
1094
|
-
# Initial cache update
|
|
1095
|
-
update_term_cache
|
|
1096
|
-
|
|
1097
|
-
while true; do
|
|
1098
|
-
# Auto-sync: reap finished background sync
|
|
1099
|
-
if [[ -n "$_sync_pid" ]]; then
|
|
1100
|
-
if ! kill -0 "$_sync_pid" 2>/dev/null; then
|
|
1101
|
-
wait "$_sync_pid" 2>/dev/null
|
|
1102
|
-
_sync_pid=""
|
|
1103
|
-
fi
|
|
1104
|
-
fi
|
|
1105
|
-
|
|
1106
|
-
# Auto-sync: trigger when interval reached and sync configured
|
|
1107
|
-
((_sync_counter++)) || true
|
|
1108
|
-
if (( _sync_counter >= _SYNC_INTERVAL )) && [[ -z "$_sync_pid" && -f "$VIBAN_DATA_DIR/sync.json" ]]; then
|
|
1109
|
-
_sync_counter=0
|
|
1110
|
-
local _sync_provider
|
|
1111
|
-
_sync_provider=$(jq -r '.provider // ""' "$VIBAN_DATA_DIR/sync.json" 2>/dev/null)
|
|
1112
|
-
if [[ -n "$_sync_provider" && "$_sync_provider" != "null" ]]; then
|
|
1113
|
-
VIBAN_JSON="$VIBAN_JSON" VIBAN_DATA_DIR="$VIBAN_DATA_DIR" \
|
|
1114
|
-
VIBAN_PROVIDER="$_sync_provider" VIBAN_SCRIPT_DIR="$VIBAN_SCRIPT_DIR" \
|
|
1115
|
-
bash "$VIBAN_SCRIPT_DIR/scripts/sync.sh" --auto &
|
|
1116
|
-
_sync_pid=$!
|
|
1117
|
-
fi
|
|
1118
|
-
fi
|
|
1119
|
-
# Cache JSON data once per frame
|
|
1120
|
-
local json_data=$(cat "$VIBAN_JSON")
|
|
1121
|
-
|
|
1122
|
-
printf '\033[H\033[0m'
|
|
1123
|
-
draw_header
|
|
1124
|
-
draw_board $col $card "$json_data"
|
|
1125
|
-
draw_footer
|
|
1126
|
-
printf '\033[J'
|
|
1127
|
-
|
|
1128
|
-
# Advance spinner
|
|
1129
|
-
((SPINNER_IDX++))
|
|
1130
|
-
|
|
1131
|
-
local st="${VIBAN_STATUSES[$((col + 1))]}"
|
|
1132
|
-
# Use cached json_data for count
|
|
1133
|
-
local cnt=$(printf '%s' "$json_data" | jq -r --arg s "$st" '[.issues[]|select(.status==$s)]|length')
|
|
1134
|
-
|
|
1135
|
-
local key=$(read_key)
|
|
1136
|
-
case "$key" in
|
|
1137
|
-
left)
|
|
1138
|
-
local start_col=$col
|
|
1139
|
-
col=$(( (col - 1 + 3) % 3 ))
|
|
1140
|
-
# Skip empty columns (but stop if we return to start)
|
|
1141
|
-
while (( col != start_col )); do
|
|
1142
|
-
local next_st="${VIBAN_STATUSES[$((col + 1))]}"
|
|
1143
|
-
local next_cnt=$(printf '%s' "$json_data" | jq -r --arg s "$next_st" '[.issues[]|select(.status==$s)]|length')
|
|
1144
|
-
(( next_cnt > 0 )) && break
|
|
1145
|
-
col=$(( (col - 1 + 3) % 3 ))
|
|
1146
|
-
done
|
|
1147
|
-
card=0
|
|
1148
|
-
;;
|
|
1149
|
-
right)
|
|
1150
|
-
local start_col=$col
|
|
1151
|
-
col=$(( (col + 1) % 3 ))
|
|
1152
|
-
# Skip empty columns (but stop if we return to start)
|
|
1153
|
-
while (( col != start_col )); do
|
|
1154
|
-
local next_st="${VIBAN_STATUSES[$((col + 1))]}"
|
|
1155
|
-
local next_cnt=$(printf '%s' "$json_data" | jq -r --arg s "$next_st" '[.issues[]|select(.status==$s)]|length')
|
|
1156
|
-
(( next_cnt > 0 )) && break
|
|
1157
|
-
col=$(( (col + 1) % 3 ))
|
|
1158
|
-
done
|
|
1159
|
-
card=0
|
|
1160
|
-
;;
|
|
1161
|
-
up)
|
|
1162
|
-
(( cnt > 0 )) && card=$(( (card - 1 + cnt) % cnt ))
|
|
1163
|
-
;;
|
|
1164
|
-
down)
|
|
1165
|
-
(( cnt > 0 )) && card=$(( (card + 1) % cnt ))
|
|
1166
|
-
;;
|
|
1167
|
-
shift_up)
|
|
1168
|
-
if (( cnt > 0 && card > 0 )); 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
|
-
shift_down)
|
|
1177
|
-
if (( cnt > 0 && card < cnt - 1 )); then
|
|
1178
|
-
local card_id=$(get_issue_id_at_index "$st" "$card" "$json_data")
|
|
1179
|
-
if move_card_order "$st" $card 1; then
|
|
1180
|
-
local new_json=$(cat "$VIBAN_JSON")
|
|
1181
|
-
card=$(get_card_index_by_id "$st" "$card_id" "$new_json")
|
|
1182
|
-
fi
|
|
1183
|
-
fi
|
|
1184
|
-
;;
|
|
1185
|
-
enter)
|
|
1186
|
-
if (( cnt > 0 )); then
|
|
1187
|
-
local id=$(get_issue_id_at_index "$st" "$card" "$json_data")
|
|
1188
|
-
[[ -n "$id" ]] && {
|
|
1189
|
-
if [[ "$st" == "review" ]]; then
|
|
1190
|
-
# Open associated PR in browser
|
|
1191
|
-
local _branch="issue-${id}"
|
|
1192
|
-
local _ext_id=$(get_ext_id "$id")
|
|
1193
|
-
if [[ -n "$_ext_id" && "$_ext_id" != "null" ]]; then
|
|
1194
|
-
local _num="${_ext_id##*:}"
|
|
1195
|
-
gh pr view "$_num" --web 2>/dev/null || \
|
|
1196
|
-
gh pr list --head "$_branch" --web 2>/dev/null
|
|
1197
|
-
else
|
|
1198
|
-
gh pr list --head "$_branch" --web 2>/dev/null
|
|
1199
|
-
fi
|
|
1200
|
-
else
|
|
1201
|
-
printf '\033[?25h'
|
|
1202
|
-
stty echo 2>/dev/null
|
|
1203
|
-
edit_issue "$id"
|
|
1204
|
-
stty -echo 2>/dev/null
|
|
1205
|
-
printf '\033[?25l\033[2J\033[H'
|
|
1206
|
-
fi
|
|
1207
|
-
}
|
|
1208
|
-
fi
|
|
1209
|
-
;;
|
|
1210
|
-
shift_left)
|
|
1211
|
-
if (( cnt > 0 && col > 0 )); then
|
|
1212
|
-
move_card_status "$st" $card -1 && { col=$((col - 1)); card=0; }
|
|
1213
|
-
fi
|
|
1214
|
-
;;
|
|
1215
|
-
shift_right)
|
|
1216
|
-
if (( cnt > 0 && col < 2 )); then
|
|
1217
|
-
move_card_status "$st" $card 1 && { col=$((col + 1)); card=0; }
|
|
1218
|
-
fi
|
|
1219
|
-
;;
|
|
1220
|
-
add)
|
|
1221
|
-
printf '\033[?25h'
|
|
1222
|
-
stty echo 2>/dev/null
|
|
1223
|
-
add_issue
|
|
1224
|
-
stty -echo 2>/dev/null
|
|
1225
|
-
printf '\033[?25l\033[2J\033[H'
|
|
1226
|
-
;;
|
|
1227
|
-
backspace)
|
|
1228
|
-
if (( cnt > 0 )); then
|
|
1229
|
-
local id=$(get_issue_id_at_index "$st" "$card" "$json_data")
|
|
1230
|
-
[[ -n "$id" ]] && {
|
|
1231
|
-
# Move cursor to footer line and run gum there
|
|
1232
|
-
printf '\033[?25h'
|
|
1233
|
-
stty echo 2>/dev/null
|
|
1234
|
-
# Clear footer line and show confirm
|
|
1235
|
-
printf '\033[%d;1H\033[K' "$CACHED_TERM_H"
|
|
1236
|
-
if gum confirm "Delete $(display_id "$id" "$(get_ext_id "$id")")?" --affirmative "Yes" --negative "No" \
|
|
1237
|
-
--selected.foreground="#000000" --selected.background "${C[accent]}"; then
|
|
1238
|
-
delete_issue "$id"
|
|
1239
|
-
(( card > 0 )) && card=$((card - 1))
|
|
1240
|
-
fi
|
|
1241
|
-
stty -echo 2>/dev/null
|
|
1242
|
-
printf '\033[?25l'
|
|
1243
|
-
# Redraw footer only (cursor back to footer)
|
|
1244
|
-
printf '\033[%d;1H\033[K' "$((CACHED_TERM_H - 1))"
|
|
1245
|
-
draw_footer
|
|
1246
|
-
}
|
|
1247
|
-
fi
|
|
1248
|
-
;;
|
|
1249
|
-
quit)
|
|
1250
|
-
printf '\033[?25h\033[0m'
|
|
1251
|
-
stty echo 2>/dev/null
|
|
1252
|
-
clear
|
|
1253
|
-
exit 0
|
|
1254
|
-
;;
|
|
1255
|
-
esac
|
|
1256
|
-
done
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
# Edit issue in editor (title + description + priority + type)
|
|
1260
|
-
edit_issue() {
|
|
1261
|
-
local id=$1
|
|
1262
|
-
local issue=$(jq --argjson id "$id" '.issues[]|select((.id|tonumber)==$id)' "$VIBAN_JSON")
|
|
1263
|
-
[[ -z "$issue" ]] && return 1
|
|
1264
|
-
|
|
1265
|
-
local title=$(printf '%s' "$issue" | jq -r '.title')
|
|
1266
|
-
local desc=$(printf '%s' "$issue" | jq -r '.description // ""')
|
|
1267
|
-
local ist=$(printf '%s' "$issue" | jq -r '.status')
|
|
1268
|
-
local created=$(printf '%s' "$issue" | jq -r '.created_at')
|
|
1269
|
-
local priority=$(printf '%s' "$issue" | jq -r '.priority // "P3"')
|
|
1270
|
-
local issue_type=$(printf '%s' "$issue" | jq -r '.type // ""')
|
|
1271
|
-
local ext_id=$(printf '%s' "$issue" | jq -r '.external_id // ""')
|
|
1272
|
-
local did; did=$(display_id "$id" "$ext_id")
|
|
1273
|
-
|
|
1274
|
-
local tmpfile=$(mktemp)
|
|
1275
|
-
local editor="${EDITOR:-${VISUAL:-vim}}"
|
|
1276
|
-
|
|
1277
|
-
cat > "$tmpfile" <<TEMPLATE
|
|
1278
|
-
# ─────────────────────────────────────────────
|
|
1279
|
-
# VIBAN Issue $did
|
|
1280
|
-
# ─────────────────────────────────────────────
|
|
1281
|
-
# Status: ${STATUS_LABEL[$ist]}
|
|
1282
|
-
# Created: ${created:0:10}
|
|
1283
|
-
# ─────────────────────────────────────────────
|
|
1284
|
-
|
|
1285
|
-
# ▼ Priority (P0=CRITICAL, P1=HIGH, P2=MEDIUM, P3=LOW)
|
|
1286
|
-
$priority
|
|
1287
|
-
|
|
1288
|
-
# ▼ Type (bug, feat, chore, refactor) - leave empty for none
|
|
1289
|
-
$issue_type
|
|
1290
|
-
|
|
1291
|
-
# ▼ Title (single line)
|
|
1292
|
-
$title
|
|
1293
|
-
|
|
1294
|
-
# ▼ Description (multiple lines allowed)
|
|
1295
|
-
$desc
|
|
1296
|
-
TEMPLATE
|
|
1297
|
-
|
|
1298
|
-
$editor "$tmpfile"
|
|
1299
|
-
|
|
1300
|
-
# Parse: priority -> type -> title -> description
|
|
1301
|
-
local new_priority="" new_type="" new_title="" new_desc="" parse_stage=0
|
|
1302
|
-
while IFS= read -r line; do
|
|
1303
|
-
[[ "$line" =~ ^#.*$ ]] && continue
|
|
1304
|
-
case $parse_stage in
|
|
1305
|
-
0) # Looking for priority
|
|
1306
|
-
[[ -z "$line" ]] && continue
|
|
1307
|
-
# Validate priority format (P0-P3)
|
|
1308
|
-
if [[ "$line" =~ ^P[0-3]$ ]]; then
|
|
1309
|
-
new_priority="$line"
|
|
1310
|
-
else
|
|
1311
|
-
new_priority="P3" # Default if invalid
|
|
1312
|
-
fi
|
|
1313
|
-
parse_stage=1
|
|
1314
|
-
;;
|
|
1315
|
-
1) # Looking for type
|
|
1316
|
-
[[ -z "$line" ]] && continue # Skip empty lines
|
|
1317
|
-
# Validate type format (bug, feat, chore, refactor)
|
|
1318
|
-
if [[ "$line" =~ ^(bug|feat|chore|refactor)$ ]]; then
|
|
1319
|
-
new_type="$line"
|
|
1320
|
-
fi
|
|
1321
|
-
parse_stage=2 # Move to stage 2 on any non-empty line
|
|
1322
|
-
;;
|
|
1323
|
-
2) # Looking for title
|
|
1324
|
-
[[ -z "$line" ]] && continue
|
|
1325
|
-
new_title="$line"
|
|
1326
|
-
parse_stage=3
|
|
1327
|
-
;;
|
|
1328
|
-
3) # Collecting description
|
|
1329
|
-
# Skip empty lines right after title
|
|
1330
|
-
if [[ -z "$new_desc" && -z "$line" ]]; then
|
|
1331
|
-
continue
|
|
1332
|
-
fi
|
|
1333
|
-
new_desc+="$line"$'\n'
|
|
1334
|
-
;;
|
|
1335
|
-
esac
|
|
1336
|
-
done < "$tmpfile"
|
|
1337
|
-
|
|
1338
|
-
# Trim trailing newlines from description
|
|
1339
|
-
new_desc="${new_desc%$'\n'}"
|
|
1340
|
-
|
|
1341
|
-
rm -f "$tmpfile"
|
|
1342
|
-
|
|
1343
|
-
[[ -z "$new_title" ]] && return 1
|
|
1344
|
-
[[ -z "$new_priority" ]] && new_priority="P3"
|
|
1345
|
-
|
|
1346
|
-
# Update issue
|
|
1347
|
-
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1348
|
-
local tmpjson=$(mktemp)
|
|
1349
|
-
printf '%s' "$new_desc" > "$tmpjson"
|
|
1350
|
-
jq --argjson id "$id" --arg title "$new_title" --rawfile desc "$tmpjson" --arg priority "$new_priority" --arg issue_type "$new_type" --arg now "$now" \
|
|
1351
|
-
'(.issues[]|select((.id|tonumber)==$id)) |= . + {title:$title,description:$desc,priority:$priority,type:(if $issue_type == "" then null else $issue_type end),updated_at:$now}' \
|
|
1352
|
-
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1353
|
-
rm -f "$tmpjson"
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
# Move card to adjacent column (change status)
|
|
1357
|
-
move_card_status() {
|
|
1358
|
-
local st="$1"
|
|
1359
|
-
local card_idx="$2"
|
|
1360
|
-
local dir="$3" # -1 for left, 1 for right
|
|
1361
|
-
|
|
1362
|
-
local sort_expr=$(get_sort_expr "$st")
|
|
1363
|
-
local id=$(jq -r --arg s "$st" --argjson i "$card_idx" \
|
|
1364
|
-
".issues | map(select(.status==\$s)) | $sort_expr | .[\$i].id // empty" "$VIBAN_JSON")
|
|
1365
|
-
[[ -z "$id" ]] && return 1
|
|
1366
|
-
|
|
1367
|
-
# Find current status index and calculate new status
|
|
1368
|
-
local cur_idx=0
|
|
1369
|
-
for i in {1..3}; do
|
|
1370
|
-
[[ "${VIBAN_STATUSES[$i]}" == "$st" ]] && { cur_idx=$i; break; }
|
|
1371
|
-
done
|
|
1372
|
-
|
|
1373
|
-
local new_idx=$((cur_idx + dir))
|
|
1374
|
-
(( new_idx < 1 || new_idx > 3 )) && return 1
|
|
1375
|
-
|
|
1376
|
-
local new_st="${VIBAN_STATUSES[$new_idx]}"
|
|
1377
|
-
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1378
|
-
|
|
1379
|
-
jq --argjson id "$id" --arg new_st "$new_st" --arg now "$now" \
|
|
1380
|
-
'(.issues[]|select((.id|tonumber)==$id)) |= . + {status:$new_st,updated_at:$now}' \
|
|
1381
|
-
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
# CLI commands
|
|
1385
|
-
cmd_list() {
|
|
1386
|
-
init_json
|
|
1387
|
-
local _done_ids=$(jq '[.issues[]|select(.status=="done")|.id]' "$VIBAN_JSON")
|
|
1388
|
-
local filter_status=""
|
|
1389
|
-
[[ "$1" == "--status" && -n "$2" ]] && filter_status="$2"
|
|
1390
|
-
|
|
1391
|
-
echo ""
|
|
1392
|
-
if [[ -n "$filter_status" ]]; then
|
|
1393
|
-
local count=$(jq -r --arg s "$filter_status" '[.issues[]|select(.status==$s)]|length' "$VIBAN_JSON")
|
|
1394
|
-
echo "● $filter_status ($count)"
|
|
1395
|
-
jq -r --arg s "$filter_status" --argjson done "$_done_ids" '.issues|map(select(.status==$s))|sort_by(.updated_at)|reverse|.[]|" \(if .external_id then .external_id else "#\(.id)" end) [\(.priority // "P3")]\(if .type then " [\(.type | ascii_upcase)]" else "" end)\(if ((.blocked_by // []) | length > 0 and any(. as $b | $done | index($b) == null)) then " [BLOCKED]" else "" end) \(.title)"' "$VIBAN_JSON"
|
|
1396
|
-
echo ""
|
|
1397
|
-
else
|
|
1398
|
-
for st in $VIBAN_STATUSES; do
|
|
1399
|
-
gum style --foreground "${STATUS_COLOR[$st]}" --bold "● ${STATUS_LABEL[$st]} ($(count_issues_by_status "$st"))"
|
|
1400
|
-
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)"'
|
|
1401
|
-
echo ""
|
|
1402
|
-
done
|
|
1403
|
-
fi
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
cmd_history() {
|
|
1407
|
-
init_json
|
|
1408
|
-
local count=$(jq '[.issues[]|select(.status=="done")]|length' "$VIBAN_JSON")
|
|
1409
|
-
echo ""
|
|
1410
|
-
echo "● Done ($count)"
|
|
1411
|
-
jq -r '.issues|map(select(.status=="done"))|sort_by(.updated_at)|reverse|.[]|" \(if .external_id then .external_id else "#\(.id)" end) [\(.priority // "P3")]\(if .type then " [\(.type | ascii_upcase)]" else "" end) \(.title) (\(.updated_at | split("T")[0]))"' "$VIBAN_JSON"
|
|
1412
|
-
echo ""
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
cmd_priority() {
|
|
1416
|
-
init_json
|
|
1417
|
-
[[ -z "$1" ]] && { echo "Usage: viban priority <id> <P0|P1|P2|P3>"; exit 1; }
|
|
1418
|
-
local id="$1"
|
|
1419
|
-
local new_priority="${2:-}"
|
|
1420
|
-
|
|
1421
|
-
# Validate priority
|
|
1422
|
-
if [[ ! "$new_priority" =~ ^P[0-3]$ ]]; then
|
|
1423
|
-
echo "Error: Priority must be P0, P1, P2, or P3"
|
|
1424
|
-
exit 1
|
|
1425
|
-
fi
|
|
1426
|
-
|
|
1427
|
-
# Check if issue exists
|
|
1428
|
-
local exists=$(jq --argjson id "$id" '[.issues[]|select((.id|tonumber)==$id)]|length' "$VIBAN_JSON")
|
|
1429
|
-
[[ "$exists" == "0" ]] && { echo "Error: Issue #$id not found"; exit 1; }
|
|
1430
|
-
|
|
1431
|
-
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1432
|
-
jq --argjson id "$id" --arg priority "$new_priority" --arg now "$now" \
|
|
1433
|
-
'(.issues[]|select((.id|tonumber)==$id)) |= . + {priority:$priority,updated_at:$now}' \
|
|
1434
|
-
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1435
|
-
|
|
1436
|
-
echo "✓ $(display_id "$id" "$(get_ext_id "$id")") priority → $new_priority"
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
cmd_add() {
|
|
1440
|
-
init_json
|
|
1441
|
-
[[ -z "$1" ]] && { echo "Usage: viban add \"title\" [\"description\"] [priority] [type] [attachments...]"; exit 1; }
|
|
1442
|
-
|
|
1443
|
-
# Support both positional and named args (--title, --description, --priority, --type, --ext-id)
|
|
1444
|
-
local title="" desc="" priority="P3" issue_type="" ext_id="" parent_id=""
|
|
1445
|
-
local -a attachments=()
|
|
1446
|
-
local positional=()
|
|
1447
|
-
|
|
1448
|
-
while [[ $# -gt 0 ]]; do
|
|
1449
|
-
case "$1" in
|
|
1450
|
-
--title) title="$2"; shift 2 ;;
|
|
1451
|
-
--desc|--description) desc="$2"; shift 2 ;;
|
|
1452
|
-
--desc-file) [[ -f "$2" ]] && desc="$(cat "$2")"; shift 2 ;;
|
|
1453
|
-
--priority) priority="$2"; shift 2 ;;
|
|
1454
|
-
--type) issue_type="$2"; shift 2 ;;
|
|
1455
|
-
--ext-id|--external-id) ext_id="$2"; shift 2 ;;
|
|
1456
|
-
--parent) parent_id="$2"; shift 2 ;;
|
|
1457
|
-
--attach|--attachments) shift; while [[ $# -gt 0 && "$1" != --* ]]; do attachments+=("$1"); shift; done ;;
|
|
1458
|
-
--*) shift 2 2>/dev/null || shift ;; # skip unknown flags
|
|
1459
|
-
*) positional+=("$1"); shift ;;
|
|
1460
|
-
esac
|
|
1461
|
-
done
|
|
1462
|
-
|
|
1463
|
-
# Fall back to positional args if named args not used
|
|
1464
|
-
[[ -z "$title" ]] && title="${positional[1]:-}"
|
|
1465
|
-
[[ -z "$desc" ]] && desc="${positional[2]:-}"
|
|
1466
|
-
[[ "$priority" == "P3" && -n "${positional[3]:-}" ]] && priority="${positional[3]}"
|
|
1467
|
-
[[ -z "$issue_type" && -n "${positional[4]:-}" ]] && issue_type="${positional[4]}"
|
|
1468
|
-
if [[ ${#attachments[@]} -eq 0 && ${#positional[@]} -gt 4 ]]; then
|
|
1469
|
-
attachments=("${positional[@]:5}")
|
|
1470
|
-
fi
|
|
1471
|
-
|
|
1472
|
-
[[ -z "$title" ]] && { echo "Usage: viban add \"title\" [\"description\"] [priority] [type]"; exit 1; }
|
|
1473
|
-
|
|
1474
|
-
# Duplicate detection: warn on similar titles (word-level Jaccard >= 0.5)
|
|
1475
|
-
local duplicates=$(jq -r --arg title "$title" '
|
|
1476
|
-
def words: ascii_downcase | gsub("[^a-z0-9가-힣\\s]"; " ") | split(" ") | map(select(length > 1)) | unique;
|
|
1477
|
-
($title | words) as $new_words |
|
|
1478
|
-
if ($new_words | length) == 0 then empty else
|
|
1479
|
-
.issues[] | select(.status != "done") |
|
|
1480
|
-
(.title | words) as $existing_words |
|
|
1481
|
-
([$new_words[] | select(. as $w | $existing_words | index($w) != null)] | length) as $overlap |
|
|
1482
|
-
([$new_words[], $existing_words[]] | unique | length) as $union |
|
|
1483
|
-
select($union > 0 and ($overlap / $union) >= 0.5) |
|
|
1484
|
-
"\(.id)\t\(.title)"
|
|
1485
|
-
end
|
|
1486
|
-
' "$VIBAN_JSON")
|
|
1487
|
-
if [[ -n "$duplicates" ]]; then
|
|
1488
|
-
echo "⚠ Potential duplicate(s):"
|
|
1489
|
-
while IFS=$'\t' read -r dup_id dup_title; do
|
|
1490
|
-
echo " #$dup_id $dup_title"
|
|
1491
|
-
done <<< "$duplicates"
|
|
1492
|
-
fi
|
|
1493
|
-
|
|
1494
|
-
# Validate parent exists if specified
|
|
1495
|
-
if [[ -n "$parent_id" ]]; then
|
|
1496
|
-
local parent_exists=$(jq -r --argjson id "$parent_id" '.issues[]|select((.id|tonumber)==$id)|.id//empty' "$VIBAN_JSON")
|
|
1497
|
-
[[ -z "$parent_exists" ]] && { echo "Error: Parent issue #$parent_id not found"; exit 1; }
|
|
1498
|
-
fi
|
|
1499
|
-
|
|
1500
|
-
local id=$(get_next_id) now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1501
|
-
# Validate priority
|
|
1502
|
-
[[ ! "$priority" =~ ^P[0-3]$ ]] && priority="P3"
|
|
1503
|
-
# Validate type (bug, feat, chore, refactor)
|
|
1504
|
-
[[ -n "$issue_type" && ! "$issue_type" =~ ^(bug|feat|chore|refactor)$ ]] && issue_type=""
|
|
1505
|
-
# Build attachments JSON array
|
|
1506
|
-
local attachments_json="[]"
|
|
1507
|
-
if [[ ${#attachments[@]} -gt 0 ]]; then
|
|
1508
|
-
attachments_json=$(printf '%s\n' "${attachments[@]}" | jq -R . | jq -s .)
|
|
1509
|
-
fi
|
|
1510
|
-
# New cards don't have order - they follow priority-based sorting
|
|
1511
|
-
# Order is only assigned when manually moved
|
|
1512
|
-
local tmpjson=$(mktemp)
|
|
1513
|
-
printf '%s' "$desc" > "$tmpjson"
|
|
1514
|
-
jq --arg id "$id" --arg title "$title" --rawfile desc "$tmpjson" --arg priority "$priority" --arg issue_type "$issue_type" --arg ext_id "$ext_id" --arg parent "$parent_id" --argjson attachments "$attachments_json" --arg now "$now" '
|
|
1515
|
-
.next_id = ((.next_id // 0) + 1) |
|
|
1516
|
-
.issues += [{
|
|
1517
|
-
id:($id|tonumber),
|
|
1518
|
-
title:$title,
|
|
1519
|
-
description:$desc,
|
|
1520
|
-
status:"backlog",
|
|
1521
|
-
priority:$priority,
|
|
1522
|
-
type:(if $issue_type == "" then null else $issue_type end),
|
|
1523
|
-
external_id:(if $ext_id == "" then null else $ext_id end),
|
|
1524
|
-
parent_id:(if $parent == "" then null else ($parent|tonumber) end),
|
|
1525
|
-
attachments:$attachments,
|
|
1526
|
-
assigned_to:null,
|
|
1527
|
-
created_at:$now,
|
|
1528
|
-
updated_at:$now
|
|
1529
|
-
}]' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1530
|
-
rm -f "$tmpjson"
|
|
1531
|
-
|
|
1532
|
-
# Auto-create remote issue if sync is configured and no ext_id provided
|
|
1533
|
-
if [[ -z "$ext_id" && -f "$VIBAN_DATA_DIR/sync.json" ]]; then
|
|
1534
|
-
local provider
|
|
1535
|
-
provider=$(jq -r '.provider // ""' "$VIBAN_DATA_DIR/sync.json" 2>/dev/null)
|
|
1536
|
-
if [[ -n "$provider" && "$provider" != "null" ]]; then
|
|
1537
|
-
local created_ext_id
|
|
1538
|
-
created_ext_id=$(VIBAN_JSON="$VIBAN_JSON" VIBAN_DATA_DIR="$VIBAN_DATA_DIR" \
|
|
1539
|
-
VIBAN_PROVIDER="$provider" VIBAN_SCRIPT_DIR="$VIBAN_SCRIPT_DIR" \
|
|
1540
|
-
bash "$VIBAN_SCRIPT_DIR/scripts/sync_create.sh" "$id" 2>/dev/null) || true
|
|
1541
|
-
[[ -n "$created_ext_id" ]] && ext_id="$created_ext_id"
|
|
1542
|
-
fi
|
|
1543
|
-
fi
|
|
1544
|
-
|
|
1545
|
-
local type_info=""
|
|
1546
|
-
[[ -n "$issue_type" ]] && type_info=" [$issue_type]"
|
|
1547
|
-
local attach_info=""
|
|
1548
|
-
[[ ${#attachments[@]} -gt 0 ]] && attach_info=" +${#attachments[@]} files"
|
|
1549
|
-
local parent_info=""
|
|
1550
|
-
[[ -n "$parent_id" ]] && parent_info=" (child of #$parent_id)"
|
|
1551
|
-
echo "✓ $(display_id "$id" "$ext_id") added ($priority)$type_info$attach_info$parent_info"
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
cmd_assign() {
|
|
1555
|
-
init_json
|
|
1556
|
-
local session="${1:-$(echo $RANDOM | md5 | head -c 8)}"
|
|
1557
|
-
local done_ids=$(jq '[.issues[]|select(.status=="done")|.id]' "$VIBAN_JSON")
|
|
1558
|
-
local issue=$(jq -r --argjson done "$done_ids" '
|
|
1559
|
-
.issues|map(select(.status=="backlog"))|map(select(
|
|
1560
|
-
(.blocked_by // []) | length == 0 or all(. as $b | $done | index($b) != null)
|
|
1561
|
-
))|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")
|
|
1562
|
-
[[ "$issue" == "null" || -z "$issue" ]] && { echo "No backlog"; exit 1; }
|
|
1563
|
-
local id=$(printf '%s' "$issue" | jq -r '.id') now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1564
|
-
local ext_id=$(printf '%s' "$issue" | jq -r '.external_id // ""')
|
|
1565
|
-
|
|
1566
|
-
# Update status to in_progress (no worktree - use branch workflow)
|
|
1567
|
-
jq --argjson id "$id" --arg s "$session" --arg now "$now" \
|
|
1568
|
-
'(.issues[]|select((.id|tonumber)==$id)) |= . + {status:"in_progress",assigned_to:$s,updated_at:$now}' \
|
|
1569
|
-
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1570
|
-
|
|
1571
|
-
# Set iTerm2 session name to issue display ID
|
|
1572
|
-
local did; did=$(display_id "$id" "$ext_id")
|
|
1573
|
-
printf '\033]1;%s\007' "$did"
|
|
1574
|
-
|
|
1575
|
-
echo "✓ $did assigned"
|
|
1576
|
-
echo "$id"
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
cmd_review() {
|
|
1580
|
-
init_json
|
|
1581
|
-
local id="${1:-$(jq -r '.issues|map(select(.status=="in_progress"))|first|.id//empty' "$VIBAN_JSON")}"
|
|
1582
|
-
[[ -z "$id" ]] && { echo "None"; exit 1; }
|
|
1583
|
-
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1584
|
-
jq --argjson id "$id" --arg now "$now" \
|
|
1585
|
-
'(.issues[]|select((.id|tonumber)==$id)) |= . + {status:"review",assigned_to:null,updated_at:$now}' \
|
|
1586
|
-
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1587
|
-
|
|
1588
|
-
# Clear iTerm2 session name
|
|
1589
|
-
printf '\033]1;\007'
|
|
1590
|
-
|
|
1591
|
-
echo "✓ $(display_id "$id" "$(get_ext_id "$id")") → review"
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
cmd_done() {
|
|
1595
|
-
init_json
|
|
1596
|
-
[[ -z "$1" ]] && { echo "Usage: viban done <id> [--purge]"; exit 1; }
|
|
1597
|
-
local id="$1"
|
|
1598
|
-
local remove=false
|
|
1599
|
-
[[ "$2" == "--remove" || "$2" == "--purge" ]] && remove=true
|
|
1600
|
-
|
|
1601
|
-
# Cleanup worktree if exists
|
|
1602
|
-
local repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
1603
|
-
local wt_dir="$VIBAN_DATA_DIR/worktrees/$id"
|
|
1604
|
-
|
|
1605
|
-
local branch="issue-$id"
|
|
1606
|
-
local _ext_id=$(get_ext_id "$id")
|
|
1607
|
-
if [[ -n "$_ext_id" && "$_ext_id" != "null" ]]; then
|
|
1608
|
-
local _issue_num="${_ext_id##*:}"
|
|
1609
|
-
if git -C "$repo_root" rev-parse --verify "issue-${_issue_num}" &>/dev/null 2>&1; then
|
|
1610
|
-
branch="issue-${_issue_num}"
|
|
1611
|
-
fi
|
|
1612
|
-
fi
|
|
1613
|
-
|
|
1614
|
-
if [[ -d "$wt_dir" ]]; then
|
|
1615
|
-
git -C "$repo_root" worktree remove "$wt_dir" --force 2>/dev/null
|
|
1616
|
-
git -C "$repo_root" branch -D "$branch" 2>/dev/null
|
|
1617
|
-
echo "✓ worktree removed"
|
|
1618
|
-
fi
|
|
1619
|
-
|
|
1620
|
-
if $remove; then
|
|
1621
|
-
# Delete card (old behavior)
|
|
1622
|
-
jq --argjson id "$id" 'del(.issues[]|select((.id|tonumber)==$id))' \
|
|
1623
|
-
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1624
|
-
printf '\033]1;\007'
|
|
1625
|
-
echo "✓ $(display_id "$id" "$(get_ext_id "$id")") completed & removed"
|
|
1626
|
-
else
|
|
1627
|
-
# Move to done status (non-destructive default)
|
|
1628
|
-
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1629
|
-
jq --argjson id "$id" --arg now "$now" \
|
|
1630
|
-
'(.issues[]|select((.id|tonumber)==$id)) |= . + {status:"done",assigned_to:null,updated_at:$now}' \
|
|
1631
|
-
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1632
|
-
printf '\033]1;\007'
|
|
1633
|
-
echo "✓ $(display_id "$id" "$(get_ext_id "$id")") → done"
|
|
1634
|
-
fi
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
cmd_move() {
|
|
1638
|
-
init_json
|
|
1639
|
-
[[ -z "$1" || -z "$2" ]] && { echo "Usage: viban move <id> <status>"; exit 1; }
|
|
1640
|
-
local id="$1"
|
|
1641
|
-
local new_status="$2"
|
|
1642
|
-
|
|
1643
|
-
# Validate status
|
|
1644
|
-
local valid_statuses="backlog in_progress review done"
|
|
1645
|
-
if [[ ! " $valid_statuses " == *" $new_status "* ]]; then
|
|
1646
|
-
echo "Error: Invalid status '$new_status'. Valid: backlog, in_progress, review, done"
|
|
1647
|
-
exit 1
|
|
1648
|
-
fi
|
|
1649
|
-
|
|
1650
|
-
# Verify issue exists
|
|
1651
|
-
local cur_status=$(jq -r --argjson id "$id" '.issues[]|select((.id|tonumber)==$id)|.status//empty' "$VIBAN_JSON")
|
|
1652
|
-
[[ -z "$cur_status" ]] && { echo "Error: Issue #$id not found"; exit 1; }
|
|
1653
|
-
|
|
1654
|
-
if [[ "$cur_status" == "$new_status" ]]; then
|
|
1655
|
-
echo "Issue #$id is already in $new_status"
|
|
1656
|
-
return 0
|
|
1657
|
-
fi
|
|
1658
|
-
|
|
1659
|
-
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1660
|
-
jq --argjson id "$id" --arg s "$new_status" --arg now "$now" \
|
|
1661
|
-
'(.issues[]|select((.id|tonumber)==$id)) |= . + {status:$s,updated_at:$now}' \
|
|
1662
|
-
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1663
|
-
|
|
1664
|
-
echo "✓ $(display_id "$id" "$(get_ext_id "$id")") → $new_status"
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
cmd_comment() {
|
|
1668
|
-
init_json
|
|
1669
|
-
[[ -z "$1" || -z "$2" ]] && { echo "Usage: viban comment <id> \"message\""; exit 1; }
|
|
1670
|
-
local id="$1"
|
|
1671
|
-
shift
|
|
1672
|
-
local message="$*"
|
|
1673
|
-
|
|
1674
|
-
# Verify issue exists
|
|
1675
|
-
local exists=$(jq -r --argjson id "$id" '.issues[]|select((.id|tonumber)==$id)|.id//empty' "$VIBAN_JSON")
|
|
1676
|
-
[[ -z "$exists" ]] && { echo "Error: Issue #$id not found"; exit 1; }
|
|
1677
|
-
|
|
1678
|
-
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1679
|
-
jq --argjson id "$id" --arg msg "$message" --arg now "$now" \
|
|
1680
|
-
'(.issues[]|select((.id|tonumber)==$id)) |= . + {comments:((.comments // []) + [{text:$msg,created_at:$now}]),updated_at:$now}' \
|
|
1681
|
-
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1682
|
-
|
|
1683
|
-
local count=$(jq -r --argjson id "$id" '.issues[]|select((.id|tonumber)==$id)|.comments|length' "$VIBAN_JSON")
|
|
1684
|
-
echo "✓ comment #$count added to $(display_id "$id" "$(get_ext_id "$id")")"
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
cmd_get() {
|
|
1688
|
-
init_json
|
|
1689
|
-
local id="$1"
|
|
1690
|
-
# Output issue JSON
|
|
1691
|
-
jq --argjson id "$id" '.issues[]|select((.id|tonumber)==$id)' "$VIBAN_JSON"
|
|
1692
|
-
# Show sub-tasks if any
|
|
1693
|
-
local subtasks=$(jq -r --argjson id "$id" '[.issues[]|select(.parent_id==$id)]|length' "$VIBAN_JSON")
|
|
1694
|
-
if [[ "$subtasks" -gt 0 ]]; then
|
|
1695
|
-
local done_count=$(jq -r --argjson id "$id" '[.issues[]|select(.parent_id==$id and .status=="done")]|length' "$VIBAN_JSON")
|
|
1696
|
-
echo ""
|
|
1697
|
-
echo "Sub-tasks: $done_count/$subtasks done ($((done_count * 100 / subtasks))%)"
|
|
1698
|
-
jq -r --argjson id "$id" '.issues[]|select(.parent_id==$id)|" #\(.id) [\(.status)] \(.title)"' "$VIBAN_JSON"
|
|
1699
|
-
fi
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
cmd_attach() {
|
|
1703
|
-
init_json
|
|
1704
|
-
[[ -z "$1" || -z "$2" ]] && { echo "Usage: viban attach <id> <file1> [file2...]"; exit 1; }
|
|
1705
|
-
local id="$1"
|
|
1706
|
-
shift
|
|
1707
|
-
local files=("$@")
|
|
1708
|
-
|
|
1709
|
-
# Check if issue exists
|
|
1710
|
-
local exists=$(jq --argjson id "$id" '[.issues[]|select((.id|tonumber)==$id)]|length' "$VIBAN_JSON")
|
|
1711
|
-
[[ "$exists" == "0" ]] && { echo "Error: Issue #$id not found"; exit 1; }
|
|
1712
|
-
|
|
1713
|
-
# Build new attachments array
|
|
1714
|
-
local new_attachments=$(printf '%s\n' "${files[@]}" | jq -R . | jq -s .)
|
|
1715
|
-
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1716
|
-
|
|
1717
|
-
# Merge with existing attachments
|
|
1718
|
-
jq --argjson id "$id" --argjson new "$new_attachments" --arg now "$now" '
|
|
1719
|
-
(.issues[] | select((.id|tonumber)==$id)) |= . + {
|
|
1720
|
-
attachments: ((.attachments // []) + $new | unique),
|
|
1721
|
-
updated_at: $now
|
|
1722
|
-
}
|
|
1723
|
-
' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1724
|
-
|
|
1725
|
-
echo "✓ $(display_id "$id" "$(get_ext_id "$id")"): ${#files[@]} file(s) attached"
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
cmd_link() {
|
|
1729
|
-
init_json
|
|
1730
|
-
[[ -z "$1" || "$2" != "blocks" || -z "$3" ]] && { echo "Usage: viban link <id> blocks <id>"; exit 1; }
|
|
1731
|
-
local blocker_id="$1" blocked_id="$3"
|
|
1732
|
-
|
|
1733
|
-
# Verify both issues exist
|
|
1734
|
-
local b1=$(jq -r --argjson id "$blocker_id" '.issues[]|select((.id|tonumber)==$id)|.id//empty' "$VIBAN_JSON")
|
|
1735
|
-
local b2=$(jq -r --argjson id "$blocked_id" '.issues[]|select((.id|tonumber)==$id)|.id//empty' "$VIBAN_JSON")
|
|
1736
|
-
[[ -z "$b1" ]] && { echo "Error: Issue #$blocker_id not found"; exit 1; }
|
|
1737
|
-
[[ -z "$b2" ]] && { echo "Error: Issue #$blocked_id not found"; exit 1; }
|
|
1738
|
-
[[ "$blocker_id" == "$blocked_id" ]] && { echo "Error: Cannot block self"; exit 1; }
|
|
1739
|
-
|
|
1740
|
-
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1741
|
-
jq --argjson bid "$blocked_id" --argjson rid "$blocker_id" --arg now "$now" \
|
|
1742
|
-
'(.issues[]|select((.id|tonumber)==$bid)) |= . + {blocked_by:((.blocked_by // []) | if index($rid) then . else . + [$rid] end),updated_at:$now}' \
|
|
1743
|
-
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1744
|
-
|
|
1745
|
-
echo "✓ #$blocker_id blocks #$blocked_id"
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
cmd_unlink() {
|
|
1749
|
-
init_json
|
|
1750
|
-
[[ -z "$1" || "$2" != "blocks" || -z "$3" ]] && { echo "Usage: viban unlink <id> blocks <id>"; exit 1; }
|
|
1751
|
-
local blocker_id="$1" blocked_id="$3"
|
|
1752
|
-
|
|
1753
|
-
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1754
|
-
jq --argjson bid "$blocked_id" --argjson rid "$blocker_id" --arg now "$now" \
|
|
1755
|
-
'(.issues[]|select((.id|tonumber)==$bid)) |= . + {blocked_by:((.blocked_by // []) - [$rid]),updated_at:$now}' \
|
|
1756
|
-
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1757
|
-
|
|
1758
|
-
echo "✓ #$blocker_id no longer blocks #$blocked_id"
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
cmd_stats() {
|
|
1762
|
-
init_json
|
|
1763
|
-
local now_epoch=$(date +%s)
|
|
1764
|
-
local week_ago_epoch=$((now_epoch - 604800))
|
|
1765
|
-
|
|
1766
|
-
# Total by status
|
|
1767
|
-
echo ""
|
|
1768
|
-
echo "Board Summary"
|
|
1769
|
-
echo "─────────────"
|
|
1770
|
-
local backlog_n=$(jq '[.issues[]|select(.status=="backlog")]|length' "$VIBAN_JSON")
|
|
1771
|
-
local wip_n=$(jq '[.issues[]|select(.status=="in_progress")]|length' "$VIBAN_JSON")
|
|
1772
|
-
local review_n=$(jq '[.issues[]|select(.status=="review")]|length' "$VIBAN_JSON")
|
|
1773
|
-
local done_n=$(jq '[.issues[]|select(.status=="done")]|length' "$VIBAN_JSON")
|
|
1774
|
-
local total_n=$(jq '.issues|length' "$VIBAN_JSON")
|
|
1775
|
-
echo " Backlog: $backlog_n In Progress: $wip_n Review: $review_n Done: $done_n Total: $total_n"
|
|
1776
|
-
|
|
1777
|
-
# P0/P1 open count
|
|
1778
|
-
local p0_n=$(jq '[.issues[]|select(.status!="done" and .priority=="P0")]|length' "$VIBAN_JSON")
|
|
1779
|
-
local p1_n=$(jq '[.issues[]|select(.status!="done" and .priority=="P1")]|length' "$VIBAN_JSON")
|
|
1780
|
-
echo " Open P0: $p0_n Open P1: $p1_n"
|
|
1781
|
-
|
|
1782
|
-
# Issues added/closed this week
|
|
1783
|
-
echo ""
|
|
1784
|
-
echo "This Week (last 7 days)"
|
|
1785
|
-
echo "───────────────────────"
|
|
1786
|
-
local week_ago_iso=$(date -u -r $week_ago_epoch +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d "@$week_ago_epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null)
|
|
1787
|
-
local added_week=$(jq -r --arg since "$week_ago_iso" '[.issues[]|select(.created_at >= $since)]|length' "$VIBAN_JSON")
|
|
1788
|
-
local closed_week=$(jq -r --arg since "$week_ago_iso" '[.issues[]|select(.status=="done" and .updated_at >= $since)]|length' "$VIBAN_JSON")
|
|
1789
|
-
echo " Added: $added_week Completed: $closed_week"
|
|
1790
|
-
|
|
1791
|
-
# Average cycle time (created_at → updated_at for done issues)
|
|
1792
|
-
echo ""
|
|
1793
|
-
echo "Cycle Time"
|
|
1794
|
-
echo "──────────"
|
|
1795
|
-
local avg_hours=$(jq -r '
|
|
1796
|
-
[.issues[]|select(.status=="done")|
|
|
1797
|
-
((.updated_at|split("T")[0]|split("-")|map(tonumber)) as [$y2,$m2,$d2] |
|
|
1798
|
-
(.created_at|split("T")[0]|split("-")|map(tonumber)) as [$y1,$m1,$d1] |
|
|
1799
|
-
(($y2-$y1)*365 + ($m2-$m1)*30 + ($d2-$d1)) * 24)
|
|
1800
|
-
] | if length == 0 then null else (add / length | floor) end
|
|
1801
|
-
' "$VIBAN_JSON")
|
|
1802
|
-
if [[ "$avg_hours" == "null" || -z "$avg_hours" ]]; then
|
|
1803
|
-
echo " Average: no completed issues"
|
|
1804
|
-
elif [[ "$avg_hours" -lt 24 ]]; then
|
|
1805
|
-
echo " Average: <1 day"
|
|
1806
|
-
else
|
|
1807
|
-
echo " Average: $((avg_hours / 24)) days"
|
|
1808
|
-
fi
|
|
1809
|
-
|
|
1810
|
-
# Oldest open issue
|
|
1811
|
-
echo ""
|
|
1812
|
-
echo "Oldest Open Issue"
|
|
1813
|
-
echo "─────────────────"
|
|
1814
|
-
local oldest=$(jq -r '[.issues[]|select(.status!="done")]|sort_by(.created_at)|first|if . then "#\(.id) [\(.priority)] \(.title) (created \(.created_at|split("T")[0]))" else "none" end' "$VIBAN_JSON")
|
|
1815
|
-
echo " $oldest"
|
|
1816
|
-
echo ""
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
cmd_migrate() {
|
|
1820
|
-
init_json
|
|
1821
|
-
echo "Migrating issues..."
|
|
1822
|
-
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1823
|
-
|
|
1824
|
-
# Migration 1: extract [BUG], [FEATURE], [REFACTOR] from title to type field
|
|
1825
|
-
# Also strip [P0-P3] from title if present (already in priority field)
|
|
1826
|
-
echo " - Extracting type from titles..."
|
|
1827
|
-
jq --arg now "$now" '
|
|
1828
|
-
.issues = [.issues[] |
|
|
1829
|
-
# Extract type from title
|
|
1830
|
-
(if (.title | test("^\\[BUG\\]"; "i")) then "bug"
|
|
1831
|
-
elif (.title | test("^\\[FEATURE\\]"; "i")) then "feat"
|
|
1832
|
-
elif (.title | test("^\\[FEAT\\]"; "i")) then "feat"
|
|
1833
|
-
elif (.title | test("^\\[REFACTOR\\]"; "i")) then "refactor"
|
|
1834
|
-
elif (.title | test("^\\[CHORE\\]"; "i")) then "chore"
|
|
1835
|
-
else .type // null end) as $extracted_type |
|
|
1836
|
-
|
|
1837
|
-
# Clean title: remove [BUG], [FEATURE], [REFACTOR], [CHORE], [P0-P3] prefixes
|
|
1838
|
-
(.title |
|
|
1839
|
-
gsub("^\\[BUG\\]\\s*"; "") |
|
|
1840
|
-
gsub("^\\[FEATURE\\]\\s*"; "") |
|
|
1841
|
-
gsub("^\\[FEAT\\]\\s*"; "") |
|
|
1842
|
-
gsub("^\\[REFACTOR\\]\\s*"; "") |
|
|
1843
|
-
gsub("^\\[CHORE\\]\\s*"; "") |
|
|
1844
|
-
gsub("^\\[P[0-3]\\]\\s*"; "")
|
|
1845
|
-
) as $clean_title |
|
|
1846
|
-
|
|
1847
|
-
# Update issue
|
|
1848
|
-
. + {
|
|
1849
|
-
title: $clean_title,
|
|
1850
|
-
type: (if $extracted_type then $extracted_type else .type end),
|
|
1851
|
-
updated_at: $now
|
|
1852
|
-
}
|
|
1853
|
-
]
|
|
1854
|
-
' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1855
|
-
|
|
1856
|
-
# Migration 2: Remove order field from all issues
|
|
1857
|
-
# New behavior: order is only set when manually moved, otherwise follows priority
|
|
1858
|
-
echo " - Removing order field (reset to priority-based sorting)..."
|
|
1859
|
-
jq --arg now "$now" '
|
|
1860
|
-
.issues = [.issues[] | del(.order) | . + {updated_at: $now}]
|
|
1861
|
-
' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1862
|
-
|
|
1863
|
-
echo "✓ Migration complete"
|
|
1864
|
-
echo ""
|
|
1865
|
-
echo "Summary:"
|
|
1866
|
-
jq -r '
|
|
1867
|
-
[.issues[] | select(.type != null)] | group_by(.type) |
|
|
1868
|
-
.[] | " \(.[0].type): \(length) issues"
|
|
1869
|
-
' "$VIBAN_JSON"
|
|
1870
|
-
echo " (no type): $(jq '[.issues[] | select(.type == null)] | length' "$VIBAN_JSON") issues"
|
|
1871
|
-
echo ""
|
|
1872
|
-
echo "Issues by priority:"
|
|
1873
|
-
jq -r '
|
|
1874
|
-
[.issues[] | select(.status != "done")] |
|
|
1875
|
-
group_by(.priority // "P3") | sort_by(.[0].priority) |
|
|
1876
|
-
.[] | " \(.[0].priority // "P3"): \(length) issues"
|
|
1877
|
-
' "$VIBAN_JSON"
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
cmd_sync() {
|
|
1881
|
-
local provider="${VIBAN_SYNC_PROVIDER:-}"
|
|
1882
|
-
# Auto-detect provider from existing sync.json or default to github
|
|
1883
|
-
if [[ -z "$provider" && -f "$VIBAN_DATA_DIR/sync.json" ]]; then
|
|
1884
|
-
provider=$(jq -r '.provider // "github"' "$VIBAN_DATA_DIR/sync.json")
|
|
1885
|
-
fi
|
|
1886
|
-
provider="${provider:-github}"
|
|
1887
|
-
|
|
1888
|
-
local provider_script="$VIBAN_SCRIPT_DIR/scripts/providers/${provider}.sh"
|
|
1889
|
-
if [[ ! -f "$provider_script" ]]; then
|
|
1890
|
-
echo "Error: Unknown sync provider '$provider'"
|
|
1891
|
-
echo "Available: $(ls "$VIBAN_SCRIPT_DIR/scripts/providers/" 2>/dev/null | sed 's/\.sh$//' | tr '\n' ' ')"
|
|
1892
|
-
exit 1
|
|
1893
|
-
fi
|
|
1894
|
-
|
|
1895
|
-
VIBAN_JSON="$VIBAN_JSON" VIBAN_DATA_DIR="$VIBAN_DATA_DIR" \
|
|
1896
|
-
VIBAN_PROVIDER="$provider" VIBAN_SCRIPT_DIR="$VIBAN_SCRIPT_DIR" \
|
|
1897
|
-
bash "$VIBAN_SCRIPT_DIR/scripts/sync.sh" "$@"
|
|
1898
|
-
}
|
|
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"
|
|
1899
205
|
|
|
206
|
+
# ============================================================
|
|
207
|
+
# Main dispatch
|
|
208
|
+
# ============================================================
|
|
1900
209
|
main() {
|
|
1901
210
|
check_deps
|
|
1902
211
|
init_json
|
|
@@ -1907,15 +216,19 @@ main() {
|
|
|
1907
216
|
attach) shift; cmd_attach "$@";;
|
|
1908
217
|
assign) cmd_assign "$2";;
|
|
1909
218
|
review) cmd_review "$2";;
|
|
1910
|
-
done) cmd_done "
|
|
219
|
+
done) shift; cmd_done "$@";;
|
|
1911
220
|
move) cmd_move "$2" "$3";;
|
|
1912
221
|
get) cmd_get "$2";;
|
|
1913
222
|
comment) shift; cmd_comment "$@";;
|
|
1914
223
|
link) cmd_link "$2" "$3" "$4";;
|
|
1915
|
-
unlink) cmd_unlink "
|
|
224
|
+
unlink) shift; cmd_unlink "$@";;
|
|
1916
225
|
edit) [[ -z "$2" ]] && { echo "Usage: viban edit <id>"; exit 1; }; edit_issue "$2";;
|
|
1917
226
|
priority) cmd_priority "$2" "$3";;
|
|
1918
227
|
stats) cmd_stats;;
|
|
228
|
+
backup) cmd_backup;;
|
|
229
|
+
restore) shift; cmd_restore "$@";;
|
|
230
|
+
changelog) cmd_changelog "$2";;
|
|
231
|
+
export) cmd_export "$2";;
|
|
1919
232
|
migrate) cmd_migrate;;
|
|
1920
233
|
sync) shift; cmd_sync "$@";;
|
|
1921
234
|
--version|-v)
|
|
@@ -1940,7 +253,7 @@ main() {
|
|
|
1940
253
|
echo ""
|
|
1941
254
|
echo " viban TUI"
|
|
1942
255
|
echo " viban list Show board"
|
|
1943
|
-
echo " viban list --status <s>
|
|
256
|
+
echo " viban list [--status <s>] [--priority P0,P1] [--type bug] [--search text]"
|
|
1944
257
|
echo " viban history Show completed issues"
|
|
1945
258
|
echo " viban add \"title\" [\"desc\"] [P0-P3] [type] [--parent <id>] Add task"
|
|
1946
259
|
echo " viban attach <id> <file1> [file2...] Attach files to task"
|
|
@@ -1955,6 +268,10 @@ main() {
|
|
|
1955
268
|
echo " viban edit <id> Edit task in editor"
|
|
1956
269
|
echo " viban get <id> Get task details (JSON)"
|
|
1957
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"
|
|
1958
275
|
echo " viban migrate Migrate: extract type from title"
|
|
1959
276
|
echo " viban sync Sync with external issue tracker (GitHub, etc.)"
|
|
1960
277
|
echo " viban update Update to latest version (if available)"
|