claude-plugin-viban 1.0.0
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 +21 -0
- package/LICENSE +21 -0
- package/README.md +361 -0
- package/bin/viban +1242 -0
- package/commands/assign.md +14 -0
- package/commands/todo.md +15 -0
- package/docs/CLAUDE.md +245 -0
- package/package.json +55 -0
- package/scripts/check-deps.sh +45 -0
- package/skills/assign/SKILL.md +203 -0
- package/skills/todo/SKILL.md +190 -0
package/bin/viban
ADDED
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
#!/bin/zsh
|
|
2
|
+
# viban - Vibe Kanban TUI (Notion-style)
|
|
3
|
+
# Requires: gum (brew install gum), jq
|
|
4
|
+
setopt EXTENDED_GLOB
|
|
5
|
+
|
|
6
|
+
# ============================================================
|
|
7
|
+
# Dependency Check (Early exit with helpful messages)
|
|
8
|
+
# ============================================================
|
|
9
|
+
check_dependencies() {
|
|
10
|
+
local missing=0
|
|
11
|
+
|
|
12
|
+
if ! command -v jq &>/dev/null; then
|
|
13
|
+
echo "❌ jq not found"
|
|
14
|
+
[[ "$OSTYPE" == "darwin"* ]] && echo " Install: brew install jq"
|
|
15
|
+
[[ "$OSTYPE" == "linux"* ]] && echo " Install: apt install jq"
|
|
16
|
+
missing=1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if ! command -v gum &>/dev/null; then
|
|
20
|
+
echo "❌ gum not found"
|
|
21
|
+
[[ "$OSTYPE" == "darwin"* ]] && echo " Install: brew install gum"
|
|
22
|
+
[[ "$OSTYPE" == "linux"* ]] && echo " Install: See https://github.com/charmbracelet/gum#installation"
|
|
23
|
+
missing=1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
if [[ $missing -eq 1 ]]; then
|
|
27
|
+
echo ""
|
|
28
|
+
echo "Please install missing dependencies and try again."
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Only check dependencies for interactive commands (not help)
|
|
34
|
+
[[ "$1" != "help" && "$1" != "--help" && "$1" != "-h" ]] && check_dependencies
|
|
35
|
+
|
|
36
|
+
cleanup() {
|
|
37
|
+
printf '\033[?25h\033[0m'
|
|
38
|
+
stty echo 2>/dev/null
|
|
39
|
+
clear
|
|
40
|
+
exit 0
|
|
41
|
+
}
|
|
42
|
+
trap cleanup INT TERM EXIT
|
|
43
|
+
|
|
44
|
+
# Prevent gum from querying terminal colors
|
|
45
|
+
export CLICOLOR_FORCE=1
|
|
46
|
+
export COLORTERM=truecolor
|
|
47
|
+
export TERM=${TERM:-xterm-256color}
|
|
48
|
+
|
|
49
|
+
# ============================================================
|
|
50
|
+
# Data Path Detection (with edge case handling)
|
|
51
|
+
# ============================================================
|
|
52
|
+
# Priority:
|
|
53
|
+
# 1. VIBAN_DATA_DIR env var (explicit override)
|
|
54
|
+
# 2. Git common dir (shared across worktrees)
|
|
55
|
+
# 3. Project root .viban/ directory (non-git projects)
|
|
56
|
+
|
|
57
|
+
VIBAN_DATA_DIR="${VIBAN_DATA_DIR:-}"
|
|
58
|
+
VIBAN_IS_GIT_REPO=false
|
|
59
|
+
|
|
60
|
+
if [[ -z "$VIBAN_DATA_DIR" ]]; then
|
|
61
|
+
# Try git common dir first (shared across worktrees)
|
|
62
|
+
if _git_common="$(git rev-parse --git-common-dir 2>/dev/null)"; then
|
|
63
|
+
VIBAN_IS_GIT_REPO=true
|
|
64
|
+
if [[ -d "$_git_common" ]]; then
|
|
65
|
+
VIBAN_DATA_DIR="$(cd "$_git_common" && pwd)"
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
# Fallback: project root .viban directory
|
|
69
|
+
if [[ -z "$VIBAN_DATA_DIR" ]]; then
|
|
70
|
+
VIBAN_DATA_DIR="${PWD}/.viban"
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
VIBAN_JSON="${VIBAN_DATA_DIR}/viban.json"
|
|
75
|
+
|
|
76
|
+
# Ensure data directory exists
|
|
77
|
+
mkdir -p "$VIBAN_DATA_DIR"
|
|
78
|
+
|
|
79
|
+
# ============================================================
|
|
80
|
+
# Initialize viban.json if not exists
|
|
81
|
+
# ============================================================
|
|
82
|
+
init_viban_json() {
|
|
83
|
+
if [[ ! -f "$VIBAN_JSON" ]]; then
|
|
84
|
+
cat > "$VIBAN_JSON" << 'EOF'
|
|
85
|
+
{
|
|
86
|
+
"version": 2,
|
|
87
|
+
"next_id": 1,
|
|
88
|
+
"issues": []
|
|
89
|
+
}
|
|
90
|
+
EOF
|
|
91
|
+
echo "✨ Initialized new viban board at: $VIBAN_JSON"
|
|
92
|
+
fi
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Auto-initialize for commands that need data (not help/init)
|
|
96
|
+
case "$1" in
|
|
97
|
+
help|--help|-h|init) ;;
|
|
98
|
+
*) init_viban_json ;;
|
|
99
|
+
esac
|
|
100
|
+
|
|
101
|
+
# Colors - Sunset Orange Theme
|
|
102
|
+
typeset -A C
|
|
103
|
+
C=(
|
|
104
|
+
fg "#FFE5D9"
|
|
105
|
+
fg_dim "#B89685"
|
|
106
|
+
backlog "#8B7B6B"
|
|
107
|
+
progress "#FF6B35"
|
|
108
|
+
review "#C44536"
|
|
109
|
+
card_bg "#2D2416"
|
|
110
|
+
card_bd "#5A4A3A"
|
|
111
|
+
selected "#FF8C42"
|
|
112
|
+
accent "#F7931E"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# 3 statuses only
|
|
116
|
+
typeset -A STATUS_LABEL STATUS_COLOR
|
|
117
|
+
STATUS_LABEL=(backlog "To-Do" in_progress "In Progress" review "Human Review")
|
|
118
|
+
STATUS_COLOR=(backlog "${C[backlog]}" in_progress "${C[progress]}" review "${C[review]}")
|
|
119
|
+
|
|
120
|
+
# Priority levels (P0=Critical, P3=Good to have)
|
|
121
|
+
typeset -A PRIORITY_LABEL PRIORITY_COLOR
|
|
122
|
+
PRIORITY_LABEL=(P0 "CRITICAL" P1 "HIGH" P2 "MEDIUM" P3 "LOW")
|
|
123
|
+
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")
|
|
124
|
+
|
|
125
|
+
# Issue types (displayed as tags alongside priority)
|
|
126
|
+
typeset -A TYPE_LABEL TYPE_COLOR
|
|
127
|
+
TYPE_LABEL=(bug "BUG" feat "FEAT" chore "CHORE" refactor "REFAC")
|
|
128
|
+
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")
|
|
129
|
+
|
|
130
|
+
VIBAN_STATUSES=(backlog in_progress review)
|
|
131
|
+
|
|
132
|
+
# Pre-generate horizontal borders (cache) - optimized with printf repeat
|
|
133
|
+
typeset -A BORDER_CACHE
|
|
134
|
+
gen_border() {
|
|
135
|
+
local w=$1
|
|
136
|
+
[[ -n "${BORDER_CACHE[$w]}" ]] && { echo "${BORDER_CACHE[$w]}"; return; }
|
|
137
|
+
# Use printf with dynamic width - single call instead of loop
|
|
138
|
+
local b=$(printf '─%.0s' {1..$w})
|
|
139
|
+
BORDER_CACHE[$w]="$b"
|
|
140
|
+
echo "$b"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Cached terminal dimensions (with sensible defaults)
|
|
144
|
+
CACHED_TERM_W=100
|
|
145
|
+
CACHED_TERM_H=30
|
|
146
|
+
CACHED_COL_W=32
|
|
147
|
+
CACHED_MAX_H=22
|
|
148
|
+
CACHED_MAX_TASKS=7
|
|
149
|
+
|
|
150
|
+
# Spinner for in_progress cards (ASCII to avoid width calculation issues)
|
|
151
|
+
SPINNER_FRAMES=('|' '/' '-' '\')
|
|
152
|
+
SPINNER_IDX=0
|
|
153
|
+
|
|
154
|
+
update_term_cache() {
|
|
155
|
+
if [[ -n "$COLUMNS" ]]; then
|
|
156
|
+
CACHED_TERM_W=$COLUMNS
|
|
157
|
+
elif command -v stty &>/dev/null; then
|
|
158
|
+
CACHED_TERM_W=$(stty size 2>/dev/null | cut -d' ' -f2)
|
|
159
|
+
else
|
|
160
|
+
CACHED_TERM_W=$(tput cols 2>/dev/null || echo 100)
|
|
161
|
+
fi
|
|
162
|
+
if [[ -n "$LINES" ]]; then
|
|
163
|
+
CACHED_TERM_H=$LINES
|
|
164
|
+
elif command -v stty &>/dev/null; then
|
|
165
|
+
CACHED_TERM_H=$(stty size 2>/dev/null | cut -d' ' -f1)
|
|
166
|
+
else
|
|
167
|
+
CACHED_TERM_H=$(tput lines 2>/dev/null || echo 30)
|
|
168
|
+
fi
|
|
169
|
+
CACHED_COL_W=$(( (CACHED_TERM_W - 2) / 3 ))
|
|
170
|
+
CACHED_MAX_H=$((CACHED_TERM_H - 8))
|
|
171
|
+
CACHED_MAX_TASKS=$((CACHED_MAX_H / 5))
|
|
172
|
+
(( CACHED_MAX_TASKS < 2 )) && CACHED_MAX_TASKS=2
|
|
173
|
+
(( CACHED_MAX_TASKS > 8 )) && CACHED_MAX_TASKS=8
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
check_deps() {
|
|
177
|
+
command -v gum &>/dev/null || { echo "Error: gum required"; exit 1; }
|
|
178
|
+
command -v jq &>/dev/null || { echo "Error: jq required"; exit 1; }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
init_json() {
|
|
182
|
+
if [[ ! -f "$VIBAN_JSON" ]]; then
|
|
183
|
+
local max_wt_id=0
|
|
184
|
+
if [[ -d "$VIBAN_DATA_DIR/worktrees" ]]; then
|
|
185
|
+
for d in "$VIBAN_DATA_DIR/worktrees/"*(/N); do
|
|
186
|
+
local wt_id="${d:t}"
|
|
187
|
+
[[ "$wt_id" =~ ^[0-9]+$ ]] && (( wt_id > max_wt_id )) && max_wt_id=$wt_id
|
|
188
|
+
done
|
|
189
|
+
fi
|
|
190
|
+
local next_id=$((max_wt_id + 1))
|
|
191
|
+
echo "{\"version\":2,\"next_id\":$next_id,\"issues\":[]}" > "$VIBAN_JSON"
|
|
192
|
+
elif [[ $(jq '.version // 1' "$VIBAN_JSON") -lt 2 ]]; then
|
|
193
|
+
jq '{
|
|
194
|
+
version: 2,
|
|
195
|
+
next_id: (([.issues[].id] | max // 0) + 1),
|
|
196
|
+
issues: .issues
|
|
197
|
+
}' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
198
|
+
fi
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
get_next_id() { jq -r '.next_id // (([.issues[].id] | max // 0) + 1)' "$VIBAN_JSON"; }
|
|
202
|
+
|
|
203
|
+
# Calculate effective order for sorting (priority-based virtual order for cards without order)
|
|
204
|
+
# Used internally for fractional indexing calculations
|
|
205
|
+
# Cards with order: use actual order
|
|
206
|
+
# Cards without order: P0=1000000, P1=2000000, P2=3000000, P3=4000000 + id
|
|
207
|
+
calc_effective_order() {
|
|
208
|
+
local order="$1"
|
|
209
|
+
local priority="${2:-P3}"
|
|
210
|
+
local id="$3"
|
|
211
|
+
|
|
212
|
+
if [[ -n "$order" && "$order" != "null" ]]; then
|
|
213
|
+
echo "$order"
|
|
214
|
+
else
|
|
215
|
+
local base_order
|
|
216
|
+
case "$priority" in
|
|
217
|
+
P0) base_order=1000000;;
|
|
218
|
+
P1) base_order=2000000;;
|
|
219
|
+
P2) base_order=3000000;;
|
|
220
|
+
*) base_order=4000000;;
|
|
221
|
+
esac
|
|
222
|
+
echo $((base_order + id))
|
|
223
|
+
fi
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
add_issue() {
|
|
227
|
+
local title=$(gum input --placeholder "Enter task title..." --width 50 \
|
|
228
|
+
--prompt.foreground "${C[accent]}" --cursor.foreground "${C[selected]}")
|
|
229
|
+
[[ -z "$title" ]] && return
|
|
230
|
+
|
|
231
|
+
# Select type
|
|
232
|
+
local issue_type=$(gum choose "bug (BUG)" "feat (FEATURE)" "chore (CHORE)" "refactor (REFACTOR)" \
|
|
233
|
+
--header "Select type:" --cursor.foreground "${C[selected]}")
|
|
234
|
+
issue_type="${issue_type%% *}" # Extract bug, feat, chore, or refactor
|
|
235
|
+
[[ -z "$issue_type" ]] && issue_type="feat"
|
|
236
|
+
|
|
237
|
+
# Select priority
|
|
238
|
+
local priority=$(gum choose "P0 (CRITICAL)" "P1 (HIGH)" "P2 (MEDIUM)" "P3 (LOW)" \
|
|
239
|
+
--header "Select priority:" --cursor.foreground "${C[selected]}")
|
|
240
|
+
priority="${priority%% *}" # Extract P0, P1, P2, or P3
|
|
241
|
+
[[ -z "$priority" ]] && priority="P3"
|
|
242
|
+
|
|
243
|
+
local desc=""
|
|
244
|
+
if gum confirm "Add description?" --affirmative "Yes (open editor)" --negative "No" \
|
|
245
|
+
--selected.foreground="#000000" --selected.background "${C[accent]}"; then
|
|
246
|
+
local tmpfile=$(mktemp)
|
|
247
|
+
local editor="${EDITOR:-${VISUAL:-vim}}"
|
|
248
|
+
local next_id=$(get_next_id)
|
|
249
|
+
local today=$(date +"%Y-%m-%d")
|
|
250
|
+
cat > "$tmpfile" <<TEMPLATE
|
|
251
|
+
# ─────────────────────────────────────────────
|
|
252
|
+
# VIBAN Issue #$next_id
|
|
253
|
+
# ─────────────────────────────────────────────
|
|
254
|
+
# Title: $title
|
|
255
|
+
# Priority: $priority
|
|
256
|
+
# Created: $today
|
|
257
|
+
# Status: backlog
|
|
258
|
+
# ─────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
# ▼ 아래에 설명을 작성하세요 (이 줄 아래부터 저장됩니다)
|
|
261
|
+
|
|
262
|
+
TEMPLATE
|
|
263
|
+
$editor "$tmpfile"
|
|
264
|
+
desc=$(sed '/^#/d' "$tmpfile" | sed '/./,$!d')
|
|
265
|
+
rm -f "$tmpfile"
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
local id=$(get_next_id) now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
269
|
+
# New cards don't have order - they follow priority-based sorting
|
|
270
|
+
# Order is only assigned when manually moved
|
|
271
|
+
local tmpjson=$(mktemp)
|
|
272
|
+
printf '%s' "$desc" > "$tmpjson"
|
|
273
|
+
jq --arg id "$id" --arg title "$title" --rawfile desc "$tmpjson" --arg priority "$priority" --arg issue_type "$issue_type" --arg now "$now" '
|
|
274
|
+
.next_id = ((.next_id // 0) + 1) |
|
|
275
|
+
.issues += [{
|
|
276
|
+
id:($id|tonumber),
|
|
277
|
+
title:$title,
|
|
278
|
+
description:$desc,
|
|
279
|
+
status:"backlog",
|
|
280
|
+
priority:$priority,
|
|
281
|
+
type:$issue_type,
|
|
282
|
+
assigned_to:null,
|
|
283
|
+
created_at:$now,
|
|
284
|
+
updated_at:$now
|
|
285
|
+
}]' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
286
|
+
rm -f "$tmpjson"
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# Sort: backlog/in_progress by effective order, review by updated_at desc
|
|
290
|
+
# Effective order: if .order exists -> use it (manually positioned)
|
|
291
|
+
# if .order is null -> priority-based virtual order (P0=1M, P1=2M, P2=3M, P3=4M) + id
|
|
292
|
+
# This ensures: manually ordered cards stay fixed, others follow priority order
|
|
293
|
+
get_issues_by_status() {
|
|
294
|
+
local st="$1"
|
|
295
|
+
if [[ "$st" == "review" ]]; then
|
|
296
|
+
jq -r --arg s "$st" '.issues|map(select(.status==$s))|sort_by(.updated_at)|reverse' "$VIBAN_JSON"
|
|
297
|
+
else
|
|
298
|
+
jq -r --arg s "$st" '
|
|
299
|
+
.issues | map(select(.status==$s)) | sort_by(
|
|
300
|
+
if .order != null then [0, .order]
|
|
301
|
+
else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id]
|
|
302
|
+
end
|
|
303
|
+
)
|
|
304
|
+
' "$VIBAN_JSON"
|
|
305
|
+
fi
|
|
306
|
+
}
|
|
307
|
+
count_issues_by_status() { jq -r --arg s "$1" '[.issues[]|select(.status==$s)]|length' "$VIBAN_JSON"; }
|
|
308
|
+
|
|
309
|
+
get_term_width() {
|
|
310
|
+
# Try multiple methods to get terminal width
|
|
311
|
+
if [[ -n "$COLUMNS" ]]; then
|
|
312
|
+
echo "$COLUMNS"
|
|
313
|
+
elif command -v stty &>/dev/null; then
|
|
314
|
+
stty size 2>/dev/null | cut -d' ' -f2
|
|
315
|
+
else
|
|
316
|
+
tput cols 2>/dev/null || echo 100
|
|
317
|
+
fi
|
|
318
|
+
}
|
|
319
|
+
get_term_height() {
|
|
320
|
+
if [[ -n "$LINES" ]]; then
|
|
321
|
+
echo "$LINES"
|
|
322
|
+
elif command -v stty &>/dev/null; then
|
|
323
|
+
stty size 2>/dev/null | cut -d' ' -f1
|
|
324
|
+
else
|
|
325
|
+
tput lines 2>/dev/null || echo 30
|
|
326
|
+
fi
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# Get display width of string (Korean = 2, ASCII = 1)
|
|
330
|
+
# Fully optimized: no subprocesses, pure zsh with LC_ALL=C trick
|
|
331
|
+
str_width() {
|
|
332
|
+
local str="$1"
|
|
333
|
+
local char_count=${#str}
|
|
334
|
+
# Get byte count in C locale (UTF-8): CJK chars use 3 bytes each
|
|
335
|
+
local byte_count
|
|
336
|
+
LC_ALL=C byte_count=${#str}
|
|
337
|
+
# Each CJK char adds 2 extra bytes (3 total - 1 for normal char = 2 extra)
|
|
338
|
+
# Display width = ASCII count + (extra bytes / 2 for CJK chars)
|
|
339
|
+
local multi_byte_chars=$(( (byte_count - char_count) / 2 ))
|
|
340
|
+
echo $(( char_count + multi_byte_chars ))
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# Truncate string to max display width (optimized)
|
|
344
|
+
# Uses str_width for width calculation to ensure correct byte counting
|
|
345
|
+
truncate_str() {
|
|
346
|
+
local str="$1" max=$2
|
|
347
|
+
local len=${#str}
|
|
348
|
+
local w=$(str_width "$str")
|
|
349
|
+
# If already fits, return as-is
|
|
350
|
+
(( w <= max )) && { echo "$str"; return; }
|
|
351
|
+
# Binary search for truncation point
|
|
352
|
+
local lo=0 hi=$len mid sub_str
|
|
353
|
+
while (( lo < hi )); do
|
|
354
|
+
mid=$(( (lo + hi + 1) / 2 ))
|
|
355
|
+
sub_str="${str:0:$mid}"
|
|
356
|
+
w=$(str_width "$sub_str")
|
|
357
|
+
if (( w <= max )); then
|
|
358
|
+
lo=$mid
|
|
359
|
+
else
|
|
360
|
+
hi=$((mid - 1))
|
|
361
|
+
fi
|
|
362
|
+
done
|
|
363
|
+
echo "${str:0:$lo}"
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# ANSI color codes - Orange Theme
|
|
367
|
+
A_RESET="\033[0m"
|
|
368
|
+
A_BOLD="\033[1m"
|
|
369
|
+
A_DIM="\033[2m"
|
|
370
|
+
A_FG="\033[38;2;255;229;217m" # Warm cream text
|
|
371
|
+
A_GRAY="\033[38;2;139;123;107m" # Warm gray for backlog
|
|
372
|
+
A_ORANGE="\033[38;2;255;107;53m" # Vibrant orange for in_progress
|
|
373
|
+
A_DEEP_ORANGE="\033[38;2;196;69;54m" # Deep orange for review
|
|
374
|
+
A_ACCENT="\033[38;2;247;147;30m" # Golden accent
|
|
375
|
+
A_SELECTED="\033[38;2;255;140;66m" # Bright selection
|
|
376
|
+
|
|
377
|
+
# Print centered text (uses cached width)
|
|
378
|
+
print_center() {
|
|
379
|
+
local text=$1 color=${2:-$A_FG}
|
|
380
|
+
local w=$CACHED_TERM_W
|
|
381
|
+
(( w == 0 )) && w=$(get_term_width)
|
|
382
|
+
local len=${#text}
|
|
383
|
+
local pad=$(( (w - len) / 2 ))
|
|
384
|
+
printf "%${pad}s${color}%s${A_RESET}\033[K\n" "" "$text"
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
# Draw header with pure ANSI
|
|
388
|
+
draw_header() {
|
|
389
|
+
printf '\033[K\n'
|
|
390
|
+
print_center "VIBAN" "${A_BOLD}${A_ACCENT}"
|
|
391
|
+
print_center "Vibe Kanban" "${A_DIM}"
|
|
392
|
+
printf '\033[K\n'
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# Get status color code
|
|
396
|
+
get_status_color() {
|
|
397
|
+
case "$1" in
|
|
398
|
+
backlog) echo "$A_GRAY";;
|
|
399
|
+
in_progress) echo "$A_ORANGE";;
|
|
400
|
+
review) echo "$A_DEEP_ORANGE";;
|
|
401
|
+
esac
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# Build column lines into array (optimized - single jq call, cached borders)
|
|
405
|
+
# $1: status, $2: col_selected, $3: card_selected (-1 if none), $4: max_h, $5: col_w, $6: json_data
|
|
406
|
+
build_column_lines() {
|
|
407
|
+
local st="$1"
|
|
408
|
+
local is_col_selected="${2:-0}"
|
|
409
|
+
local card_sel="${3:--1}"
|
|
410
|
+
local max_h="${4:-20}"
|
|
411
|
+
local col_w="${5:-30}"
|
|
412
|
+
local json_data="$6"
|
|
413
|
+
local label="${STATUS_LABEL[$st]:-Unknown}"
|
|
414
|
+
local color=$(get_status_color "$st")
|
|
415
|
+
|
|
416
|
+
# Single jq call to get all issues for this status (include description, priority, type)
|
|
417
|
+
# Replace newlines/tabs in description to prevent parsing issues
|
|
418
|
+
# Sort: review by updated_at desc, others by effective order (ordered cards first, then priority)
|
|
419
|
+
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)'
|
|
420
|
+
[[ "$st" == "review" ]] && sort_expr='sort_by(.updated_at) | reverse'
|
|
421
|
+
local issues_data=$(printf '%s' "$json_data" | jq -r --arg s "$st" "
|
|
422
|
+
.issues | map(select(.status==\$s)) | $sort_expr |
|
|
423
|
+
.[] | \"\\(.id)\t\\(.title)\t\\((.description // \"\") | gsub(\"[\\n\\t\\r]\"; \" \"))\t\\(.priority // \"P3\")\t\\(.type // \"\")\"")
|
|
424
|
+
local count=0
|
|
425
|
+
# Count lines without subprocess using zsh array splitting
|
|
426
|
+
[[ -n "$issues_data" ]] && count=${#${(f)issues_data}}
|
|
427
|
+
|
|
428
|
+
# Header centered in column
|
|
429
|
+
local hdr_text="● $label"
|
|
430
|
+
local hdr_w=$((${#label} + 2))
|
|
431
|
+
local left_pad=$(( (col_w - hdr_w) / 2 ))
|
|
432
|
+
local right_pad=$((col_w - hdr_w - left_pad))
|
|
433
|
+
if (( is_col_selected )); then
|
|
434
|
+
printf "%${left_pad}s${A_BOLD}${A_SELECTED}%s${A_RESET}%${right_pad}s\n" "" "$hdr_text" ""
|
|
435
|
+
# Underline for selected column - use printf repeat pattern
|
|
436
|
+
local underline=$(printf '─%.0s' {1..$hdr_w})
|
|
437
|
+
printf "%${left_pad}s${A_SELECTED}%s${A_RESET}%${right_pad}s\n" "" "$underline" ""
|
|
438
|
+
else
|
|
439
|
+
printf "%${left_pad}s${color}%s${A_RESET}%${right_pad}s\n" "" "$hdr_text" ""
|
|
440
|
+
# Empty line for non-selected columns
|
|
441
|
+
printf "%${col_w}s\n" ""
|
|
442
|
+
fi
|
|
443
|
+
|
|
444
|
+
local lines_used=2
|
|
445
|
+
local card_inner=$((col_w - 4))
|
|
446
|
+
local border=$(gen_border $card_inner)
|
|
447
|
+
|
|
448
|
+
# Task cards (5 lines: top border, title, desc/priority, empty, bottom border)
|
|
449
|
+
local shown=0
|
|
450
|
+
while IFS=$'\t' read -r id title desc priority issue_type; do
|
|
451
|
+
[[ -z "$id" ]] && continue
|
|
452
|
+
(( shown >= CACHED_MAX_TASKS )) && {
|
|
453
|
+
local more_text=" +$((count - shown)) more..."
|
|
454
|
+
local more_w=${#more_text}
|
|
455
|
+
printf "${A_DIM}%s${A_RESET}%$((col_w - more_w))s\n" "$more_text" ""
|
|
456
|
+
((lines_used++))
|
|
457
|
+
break
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
# Default priority and type if not set
|
|
461
|
+
[[ -z "$priority" || "$priority" == "null" ]] && priority="P3"
|
|
462
|
+
[[ -z "$issue_type" || "$issue_type" == "null" ]] && issue_type=""
|
|
463
|
+
|
|
464
|
+
# Title line (with spinner for in_progress)
|
|
465
|
+
local spinner_prefix=""
|
|
466
|
+
local spinner_w=0
|
|
467
|
+
if [[ "$st" == "in_progress" ]]; then
|
|
468
|
+
spinner_prefix="${SPINNER_FRAMES[$((SPINNER_IDX % ${#SPINNER_FRAMES[@]} + 1))]} "
|
|
469
|
+
spinner_w=2 # char(1) + space(1)
|
|
470
|
+
fi
|
|
471
|
+
local title_w=$((card_inner - 7 - spinner_w))
|
|
472
|
+
local short=$(truncate_str "$title" $title_w)
|
|
473
|
+
local title_content=" ${spinner_prefix}#$id $short"
|
|
474
|
+
local title_content_w=$(str_width "$title_content")
|
|
475
|
+
local title_pad=$((card_inner - title_content_w))
|
|
476
|
+
(( title_pad < 0 )) && title_pad=0
|
|
477
|
+
|
|
478
|
+
# Description line (dimmed, truncated)
|
|
479
|
+
local desc_w=$((card_inner - 4))
|
|
480
|
+
local desc_short=""
|
|
481
|
+
if [[ -n "$desc" && "$desc" != "null" ]]; then
|
|
482
|
+
desc_short=$(truncate_str "$desc" $desc_w)
|
|
483
|
+
fi
|
|
484
|
+
local desc_content=" $desc_short"
|
|
485
|
+
local desc_content_w=$(str_width "$desc_content")
|
|
486
|
+
local desc_pad=$((card_inner - desc_content_w))
|
|
487
|
+
(( desc_pad < 0 )) && desc_pad=0
|
|
488
|
+
|
|
489
|
+
# Priority and type tags on same line (e.g., [P0] [BUG])
|
|
490
|
+
local priority_tag="[$priority]"
|
|
491
|
+
local priority_color="${PRIORITY_COLOR[$priority]:-$A_DIM}"
|
|
492
|
+
local type_tag=""
|
|
493
|
+
local type_color=""
|
|
494
|
+
local tags_content=""
|
|
495
|
+
local tags_w=0
|
|
496
|
+
if [[ -n "$issue_type" ]]; then
|
|
497
|
+
type_tag="[${TYPE_LABEL[$issue_type]:-$issue_type}]"
|
|
498
|
+
type_color="${TYPE_COLOR[$issue_type]:-$A_DIM}"
|
|
499
|
+
# Calculate total width: " [P1] [BUG]"
|
|
500
|
+
tags_w=$((${#priority_tag} + 1 + ${#type_tag}))
|
|
501
|
+
else
|
|
502
|
+
tags_w=${#priority_tag}
|
|
503
|
+
fi
|
|
504
|
+
local tags_pad=$((card_inner - tags_w - 2)) # -2 for leading spaces
|
|
505
|
+
|
|
506
|
+
local border_color="$A_DIM"
|
|
507
|
+
local text_color="$A_FG"
|
|
508
|
+
local desc_color="$A_DIM"
|
|
509
|
+
if (( is_col_selected && shown == card_sel )); then
|
|
510
|
+
border_color="${A_SELECTED}"
|
|
511
|
+
text_color="${A_BOLD}${A_ACCENT}"
|
|
512
|
+
desc_color="${A_ACCENT}"
|
|
513
|
+
fi
|
|
514
|
+
|
|
515
|
+
# 5-line card with priority+type tags on 4th line
|
|
516
|
+
printf " ${border_color}╭%s╮${A_RESET} \n" "$border"
|
|
517
|
+
printf " ${border_color}│${A_RESET}${text_color}%s${A_RESET}%${title_pad}s${border_color}│${A_RESET} \n" "$title_content" ""
|
|
518
|
+
printf " ${border_color}│${A_RESET}${desc_color}%s${A_RESET}%${desc_pad}s${border_color}│${A_RESET} \n" "$desc_content" ""
|
|
519
|
+
if [[ -n "$type_tag" ]]; then
|
|
520
|
+
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" ""
|
|
521
|
+
else
|
|
522
|
+
printf " ${border_color}│${A_RESET} ${priority_color}%s${A_RESET}%${tags_pad}s${border_color}│${A_RESET} \n" "$priority_tag" ""
|
|
523
|
+
fi
|
|
524
|
+
printf " ${border_color}╰%s╯${A_RESET} \n" "$border"
|
|
525
|
+
|
|
526
|
+
((shown++))
|
|
527
|
+
lines_used=$((lines_used + 5))
|
|
528
|
+
done <<< "$issues_data"
|
|
529
|
+
|
|
530
|
+
if (( count == 0 )); then
|
|
531
|
+
local no_text=" No tasks"
|
|
532
|
+
local no_w=${#no_text}
|
|
533
|
+
printf "${A_DIM}%s${A_RESET}%$((col_w - no_w))s\n" "$no_text" ""
|
|
534
|
+
((lines_used++))
|
|
535
|
+
fi
|
|
536
|
+
|
|
537
|
+
while (( lines_used < max_h )); do
|
|
538
|
+
printf "%${col_w}s\n" ""
|
|
539
|
+
((lines_used++))
|
|
540
|
+
done
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
# ESC character for ANSI stripping (defined once at script level)
|
|
544
|
+
_ESC=$'\e'
|
|
545
|
+
|
|
546
|
+
# Pad line to exact width with spaces
|
|
547
|
+
# Optimized: use zsh parameter expansion to strip ANSI codes
|
|
548
|
+
pad_to_width() {
|
|
549
|
+
local line="$1"
|
|
550
|
+
local width="$2"
|
|
551
|
+
# Strip ANSI codes: ESC [ followed by numbers/semicolons, ending with letter
|
|
552
|
+
local plain="${line//${_ESC}\[[0-9;]#[a-zA-Z]/}"
|
|
553
|
+
# Calculate display width: char count + (bytes - chars) / 2 for CJK chars
|
|
554
|
+
local char_count=${#plain} byte_count
|
|
555
|
+
LC_ALL=C byte_count=${#plain}
|
|
556
|
+
unset LC_ALL # Restore default locale to prevent affecting subsequent calls
|
|
557
|
+
local display_w=$(( char_count + (byte_count - char_count) / 2 ))
|
|
558
|
+
local pad=$((width - display_w))
|
|
559
|
+
printf '%s' "$line"
|
|
560
|
+
(( pad > 0 )) && printf "%${pad}s" ""
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
# Draw the board (optimized - arrays instead of temp files)
|
|
564
|
+
# $1: col_sel, $2: card_sel, $3: json_data
|
|
565
|
+
draw_board() {
|
|
566
|
+
local col_sel=${1:-0}
|
|
567
|
+
local card_sel=${2:--1}
|
|
568
|
+
local json_data="$3"
|
|
569
|
+
local col_w=$CACHED_COL_W
|
|
570
|
+
local max_h=$CACHED_MAX_H
|
|
571
|
+
|
|
572
|
+
local c1=-1 c2=-1 c3=-1
|
|
573
|
+
case $col_sel in
|
|
574
|
+
0) c1=$card_sel;;
|
|
575
|
+
1) c2=$card_sel;;
|
|
576
|
+
2) c3=$card_sel;;
|
|
577
|
+
esac
|
|
578
|
+
|
|
579
|
+
# Build columns to arrays
|
|
580
|
+
local -a col1 col2 col3
|
|
581
|
+
col1=("${(@f)$(build_column_lines "backlog" $((col_sel == 0)) $c1 $max_h $col_w "$json_data")}")
|
|
582
|
+
col2=("${(@f)$(build_column_lines "in_progress" $((col_sel == 1)) $c2 $max_h $col_w "$json_data")}")
|
|
583
|
+
col3=("${(@f)$(build_column_lines "review" $((col_sel == 2)) $c3 $max_h $col_w "$json_data")}")
|
|
584
|
+
|
|
585
|
+
# Merge line by line
|
|
586
|
+
local i
|
|
587
|
+
for ((i=1; i<=max_h; i++)); do
|
|
588
|
+
pad_to_width "${col1[$i]}" $col_w
|
|
589
|
+
printf "${A_DIM}│${A_RESET}"
|
|
590
|
+
pad_to_width "${col2[$i]}" $col_w
|
|
591
|
+
printf "${A_DIM}│${A_RESET}"
|
|
592
|
+
pad_to_width "${col3[$i]}" $col_w
|
|
593
|
+
printf '\033[K\n'
|
|
594
|
+
done
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
draw_footer() {
|
|
598
|
+
printf '\033[K\n'
|
|
599
|
+
print_center "←→ Column │ ↑↓ Card │ Shift+↑↓ Reorder │ Shift+←→ Move │ Enter Edit │ ⌫ Del │ A Add │ Q Quit" "${A_DIM}"
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
read_key() {
|
|
603
|
+
local key result=""
|
|
604
|
+
# Timeout for spinner animation refresh (0.5s to prevent key drops)
|
|
605
|
+
read -sk1 -t 0.5 key 2>/dev/null || { echo "timeout"; return; }
|
|
606
|
+
|
|
607
|
+
if [[ "$key" == $'\e' ]]; then
|
|
608
|
+
read -sk1 -t 0.1 c2 2>/dev/null
|
|
609
|
+
if [[ "$c2" == "[" ]]; then
|
|
610
|
+
read -sk1 -t 0.1 c3 2>/dev/null
|
|
611
|
+
case "$c3" in
|
|
612
|
+
D) result="left";; C) result="right";;
|
|
613
|
+
A) result="up";; B) result="down";;
|
|
614
|
+
"1")
|
|
615
|
+
# Handle Shift+arrow sequences: ESC[1;2X where X is A/B/C/D
|
|
616
|
+
read -sk1 -t 0.1 c4 2>/dev/null
|
|
617
|
+
if [[ "$c4" == ";" ]]; then
|
|
618
|
+
read -sk1 -t 0.1 c5 2>/dev/null
|
|
619
|
+
read -sk1 -t 0.1 c6 2>/dev/null
|
|
620
|
+
if [[ "$c5" == "2" ]]; then
|
|
621
|
+
case "$c6" in
|
|
622
|
+
A) result="shift_up";;
|
|
623
|
+
B) result="shift_down";;
|
|
624
|
+
C) result="shift_right";;
|
|
625
|
+
D) result="shift_left";;
|
|
626
|
+
esac
|
|
627
|
+
fi
|
|
628
|
+
fi
|
|
629
|
+
;;
|
|
630
|
+
esac
|
|
631
|
+
elif [[ "$c2" == "]" ]]; then
|
|
632
|
+
# Drain OSC sequence
|
|
633
|
+
while read -sk1 -t 0.01 _ 2>/dev/null; do :; done
|
|
634
|
+
fi
|
|
635
|
+
elif [[ "$key" == "" || "$key" == $'\n' ]]; then
|
|
636
|
+
result="enter"
|
|
637
|
+
elif [[ "$key" == $'\x7f' || "$key" == $'\b' ]]; then
|
|
638
|
+
result="backspace"
|
|
639
|
+
else
|
|
640
|
+
case "$key" in
|
|
641
|
+
q|Q) result="quit";;
|
|
642
|
+
a|A) result="add";;
|
|
643
|
+
esac
|
|
644
|
+
fi
|
|
645
|
+
|
|
646
|
+
echo "$result"
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
# Move card order up or down within a status column (fractional indexing)
|
|
650
|
+
# $1: status, $2: current card index, $3: direction (-1 for up, 1 for down)
|
|
651
|
+
# When manually moved, the card gets an order value to pin its position
|
|
652
|
+
move_card_order() {
|
|
653
|
+
local st="$1"
|
|
654
|
+
local cur_idx="$2"
|
|
655
|
+
local dir="$3"
|
|
656
|
+
local new_idx=$((cur_idx + dir))
|
|
657
|
+
|
|
658
|
+
# Get issues in current effective order (ordered cards first, then priority-sorted)
|
|
659
|
+
local issues=$(jq -r --arg s "$st" '
|
|
660
|
+
.issues | map(select(.status==$s)) | sort_by(
|
|
661
|
+
if .order != null then [0, .order]
|
|
662
|
+
else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id]
|
|
663
|
+
end
|
|
664
|
+
)
|
|
665
|
+
' "$VIBAN_JSON")
|
|
666
|
+
local cnt=$(printf '%s' "$issues" | jq 'length')
|
|
667
|
+
|
|
668
|
+
# Bounds check
|
|
669
|
+
(( new_idx < 0 || new_idx >= cnt )) && return 1
|
|
670
|
+
|
|
671
|
+
# Get ID of the card to move
|
|
672
|
+
local cur_id=$(printf '%s' "$issues" | jq -r ".[$cur_idx].id")
|
|
673
|
+
|
|
674
|
+
# Calculate effective order for a card (use actual order or virtual priority-based order)
|
|
675
|
+
get_eff_order() {
|
|
676
|
+
local idx=$1
|
|
677
|
+
local order=$(printf '%s' "$issues" | jq -r ".[$idx].order // \"null\"")
|
|
678
|
+
if [[ "$order" != "null" ]]; then
|
|
679
|
+
echo "$order"
|
|
680
|
+
else
|
|
681
|
+
local priority=$(printf '%s' "$issues" | jq -r ".[$idx].priority // \"P3\"")
|
|
682
|
+
local id=$(printf '%s' "$issues" | jq -r ".[$idx].id")
|
|
683
|
+
case "$priority" in
|
|
684
|
+
P0) echo $((1000000 + id));;
|
|
685
|
+
P1) echo $((2000000 + id));;
|
|
686
|
+
P2) echo $((3000000 + id));;
|
|
687
|
+
*) echo $((4000000 + id));;
|
|
688
|
+
esac
|
|
689
|
+
fi
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
# Calculate new order using fractional indexing
|
|
693
|
+
# Place card between the target position and its neighbor
|
|
694
|
+
local new_order
|
|
695
|
+
if (( dir < 0 )); then
|
|
696
|
+
# Moving up: place between target and the one above it
|
|
697
|
+
local target_order=$(get_eff_order $new_idx)
|
|
698
|
+
if (( new_idx == 0 )); then
|
|
699
|
+
# Moving to top: use target_order - 1
|
|
700
|
+
new_order=$(echo "$target_order - 1" | bc)
|
|
701
|
+
else
|
|
702
|
+
local above_order=$(get_eff_order $(($new_idx - 1)))
|
|
703
|
+
new_order=$(echo "scale=6; ($above_order + $target_order) / 2" | bc)
|
|
704
|
+
fi
|
|
705
|
+
else
|
|
706
|
+
# Moving down: place between target and the one below it
|
|
707
|
+
local target_order=$(get_eff_order $new_idx)
|
|
708
|
+
if (( new_idx == cnt - 1 )); then
|
|
709
|
+
# Moving to bottom: use target_order + 1
|
|
710
|
+
new_order=$(echo "$target_order + 1" | bc)
|
|
711
|
+
else
|
|
712
|
+
local below_order=$(get_eff_order $(($new_idx + 1)))
|
|
713
|
+
new_order=$(echo "scale=6; ($target_order + $below_order) / 2" | bc)
|
|
714
|
+
fi
|
|
715
|
+
fi
|
|
716
|
+
|
|
717
|
+
# Update the card's order (this pins it to the new position)
|
|
718
|
+
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
719
|
+
jq --argjson cur_id "$cur_id" --argjson new_order "$new_order" --arg now "$now" '
|
|
720
|
+
(.issues[] | select(.id==$cur_id)) |= . + {order:$new_order,updated_at:$now}
|
|
721
|
+
' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
722
|
+
|
|
723
|
+
return 0
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
# Get issue ID by status and index
|
|
727
|
+
get_issue_id_by_index() {
|
|
728
|
+
local st=$1 idx=$2
|
|
729
|
+
jq -r --arg s "$st" --argjson i "$idx" '.issues|map(select(.status==$s))|sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end)|.[$i].id // empty' "$VIBAN_JSON"
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
# Delete issue by ID (with worktree cleanup)
|
|
733
|
+
delete_issue() {
|
|
734
|
+
local id=$1
|
|
735
|
+
local repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
736
|
+
local wt_dir="$VIBAN_DATA_DIR/worktrees/$id"
|
|
737
|
+
local branch="viban-$id"
|
|
738
|
+
if [[ -d "$wt_dir" ]]; then
|
|
739
|
+
git -C "$repo_root" worktree remove "$wt_dir" --force 2>/dev/null
|
|
740
|
+
git -C "$repo_root" branch -D "$branch" 2>/dev/null
|
|
741
|
+
fi
|
|
742
|
+
jq --argjson id "$id" 'del(.issues[]|select((.id|tonumber)==$id))' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && \
|
|
743
|
+
mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
level1_columns() {
|
|
747
|
+
local col=0 card=0
|
|
748
|
+
|
|
749
|
+
# Hide cursor and disable input echo
|
|
750
|
+
stty -echo 2>/dev/null
|
|
751
|
+
printf '\033[?25l\033[2J\033[H'
|
|
752
|
+
|
|
753
|
+
# Initial cache update
|
|
754
|
+
update_term_cache
|
|
755
|
+
|
|
756
|
+
while true; do
|
|
757
|
+
# Cache JSON data once per frame
|
|
758
|
+
local json_data=$(cat "$VIBAN_JSON")
|
|
759
|
+
|
|
760
|
+
printf '\033[H\033[0m'
|
|
761
|
+
draw_header
|
|
762
|
+
draw_board $col $card "$json_data"
|
|
763
|
+
draw_footer
|
|
764
|
+
printf '\033[J'
|
|
765
|
+
|
|
766
|
+
# Advance spinner
|
|
767
|
+
((SPINNER_IDX++))
|
|
768
|
+
|
|
769
|
+
local st="${VIBAN_STATUSES[$((col + 1))]}"
|
|
770
|
+
# Use cached json_data for count
|
|
771
|
+
local cnt=$(printf '%s' "$json_data" | jq -r --arg s "$st" '[.issues[]|select(.status==$s)]|length')
|
|
772
|
+
|
|
773
|
+
local key=$(read_key)
|
|
774
|
+
case "$key" in
|
|
775
|
+
left)
|
|
776
|
+
local start_col=$col
|
|
777
|
+
col=$(( (col - 1 + 3) % 3 ))
|
|
778
|
+
# Skip empty columns (but stop if we return to start)
|
|
779
|
+
while (( col != start_col )); do
|
|
780
|
+
local next_st="${VIBAN_STATUSES[$((col + 1))]}"
|
|
781
|
+
local next_cnt=$(printf '%s' "$json_data" | jq -r --arg s "$next_st" '[.issues[]|select(.status==$s)]|length')
|
|
782
|
+
(( next_cnt > 0 )) && break
|
|
783
|
+
col=$(( (col - 1 + 3) % 3 ))
|
|
784
|
+
done
|
|
785
|
+
card=0
|
|
786
|
+
;;
|
|
787
|
+
right)
|
|
788
|
+
local start_col=$col
|
|
789
|
+
col=$(( (col + 1) % 3 ))
|
|
790
|
+
# Skip empty columns (but stop if we return to start)
|
|
791
|
+
while (( col != start_col )); do
|
|
792
|
+
local next_st="${VIBAN_STATUSES[$((col + 1))]}"
|
|
793
|
+
local next_cnt=$(printf '%s' "$json_data" | jq -r --arg s "$next_st" '[.issues[]|select(.status==$s)]|length')
|
|
794
|
+
(( next_cnt > 0 )) && break
|
|
795
|
+
col=$(( (col + 1) % 3 ))
|
|
796
|
+
done
|
|
797
|
+
card=0
|
|
798
|
+
;;
|
|
799
|
+
up)
|
|
800
|
+
(( cnt > 0 )) && card=$(( (card - 1 + cnt) % cnt ))
|
|
801
|
+
;;
|
|
802
|
+
down)
|
|
803
|
+
(( cnt > 0 )) && card=$(( (card + 1) % cnt ))
|
|
804
|
+
;;
|
|
805
|
+
shift_up)
|
|
806
|
+
if (( cnt > 0 && card > 0 )); then
|
|
807
|
+
# Get card ID before move
|
|
808
|
+
local card_id=$(printf '%s' "$json_data" | jq -r --arg s "$st" --argjson i "$card" \
|
|
809
|
+
'.issues|map(select(.status==$s))|sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end)|.[$i].id // empty')
|
|
810
|
+
if move_card_order "$st" $card -1; then
|
|
811
|
+
# Find new index by ID after move (order changes sort position)
|
|
812
|
+
local new_json=$(cat "$VIBAN_JSON")
|
|
813
|
+
card=$(printf '%s' "$new_json" | jq -r --arg s "$st" --argjson id "$card_id" '
|
|
814
|
+
.issues | map(select(.status==$s)) | sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end) |
|
|
815
|
+
to_entries | map(select(.value.id == $id)) | .[0].key // 0
|
|
816
|
+
')
|
|
817
|
+
fi
|
|
818
|
+
fi
|
|
819
|
+
;;
|
|
820
|
+
shift_down)
|
|
821
|
+
if (( cnt > 0 && card < cnt - 1 )); then
|
|
822
|
+
# Get card ID before move
|
|
823
|
+
local card_id=$(printf '%s' "$json_data" | jq -r --arg s "$st" --argjson i "$card" \
|
|
824
|
+
'.issues|map(select(.status==$s))|sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end)|.[$i].id // empty')
|
|
825
|
+
if move_card_order "$st" $card 1; then
|
|
826
|
+
# Find new index by ID after move (order changes sort position)
|
|
827
|
+
local new_json=$(cat "$VIBAN_JSON")
|
|
828
|
+
card=$(printf '%s' "$new_json" | jq -r --arg s "$st" --argjson id "$card_id" '
|
|
829
|
+
.issues | map(select(.status==$s)) | sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end) |
|
|
830
|
+
to_entries | map(select(.value.id == $id)) | .[0].key // 0
|
|
831
|
+
')
|
|
832
|
+
fi
|
|
833
|
+
fi
|
|
834
|
+
;;
|
|
835
|
+
enter)
|
|
836
|
+
if (( cnt > 0 )); then
|
|
837
|
+
local id=$(printf '%s' "$json_data" | jq -r --arg s "$st" --argjson i "$card" \
|
|
838
|
+
'.issues|map(select(.status==$s))|sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end)|.[$i].id // empty')
|
|
839
|
+
[[ -n "$id" ]] && {
|
|
840
|
+
printf '\033[?25h'
|
|
841
|
+
stty echo 2>/dev/null
|
|
842
|
+
edit_issue "$id"
|
|
843
|
+
stty -echo 2>/dev/null
|
|
844
|
+
printf '\033[?25l\033[2J\033[H'
|
|
845
|
+
}
|
|
846
|
+
fi
|
|
847
|
+
;;
|
|
848
|
+
shift_left)
|
|
849
|
+
if (( cnt > 0 && col > 0 )); then
|
|
850
|
+
move_card_status "$st" $card -1 && { col=$((col - 1)); card=0; }
|
|
851
|
+
fi
|
|
852
|
+
;;
|
|
853
|
+
shift_right)
|
|
854
|
+
if (( cnt > 0 && col < 2 )); then
|
|
855
|
+
move_card_status "$st" $card 1 && { col=$((col + 1)); card=0; }
|
|
856
|
+
fi
|
|
857
|
+
;;
|
|
858
|
+
add)
|
|
859
|
+
printf '\033[?25h'
|
|
860
|
+
stty echo 2>/dev/null
|
|
861
|
+
add_issue
|
|
862
|
+
stty -echo 2>/dev/null
|
|
863
|
+
printf '\033[?25l\033[2J\033[H'
|
|
864
|
+
;;
|
|
865
|
+
backspace)
|
|
866
|
+
if (( cnt > 0 )); then
|
|
867
|
+
local id=$(printf '%s' "$json_data" | jq -r --arg s "$st" --argjson i "$card" \
|
|
868
|
+
'.issues|map(select(.status==$s))|sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end)|.[$i].id // empty')
|
|
869
|
+
[[ -n "$id" ]] && {
|
|
870
|
+
# Move cursor to footer line and run gum there
|
|
871
|
+
printf '\033[?25h'
|
|
872
|
+
stty echo 2>/dev/null
|
|
873
|
+
# Clear footer line and show confirm
|
|
874
|
+
printf '\033[%d;1H\033[K' "$CACHED_TERM_H"
|
|
875
|
+
if gum confirm "Delete #$id?" --affirmative "Yes" --negative "No" \
|
|
876
|
+
--selected.foreground="#000000" --selected.background "${C[accent]}"; then
|
|
877
|
+
delete_issue "$id"
|
|
878
|
+
(( card > 0 )) && card=$((card - 1))
|
|
879
|
+
fi
|
|
880
|
+
stty -echo 2>/dev/null
|
|
881
|
+
printf '\033[?25l'
|
|
882
|
+
# Redraw footer only (cursor back to footer)
|
|
883
|
+
printf '\033[%d;1H\033[K' "$((CACHED_TERM_H - 1))"
|
|
884
|
+
draw_footer
|
|
885
|
+
}
|
|
886
|
+
fi
|
|
887
|
+
;;
|
|
888
|
+
quit)
|
|
889
|
+
printf '\033[?25h\033[0m'
|
|
890
|
+
stty echo 2>/dev/null
|
|
891
|
+
clear
|
|
892
|
+
exit 0
|
|
893
|
+
;;
|
|
894
|
+
esac
|
|
895
|
+
done
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
# Edit issue in editor (title + description + priority + type)
|
|
899
|
+
edit_issue() {
|
|
900
|
+
local id=$1
|
|
901
|
+
local issue=$(jq --argjson id "$id" '.issues[]|select((.id|tonumber)==$id)' "$VIBAN_JSON")
|
|
902
|
+
[[ -z "$issue" ]] && return 1
|
|
903
|
+
|
|
904
|
+
local title=$(printf '%s' "$issue" | jq -r '.title')
|
|
905
|
+
local desc=$(printf '%s' "$issue" | jq -r '.description // ""')
|
|
906
|
+
local ist=$(printf '%s' "$issue" | jq -r '.status')
|
|
907
|
+
local created=$(printf '%s' "$issue" | jq -r '.created_at')
|
|
908
|
+
local priority=$(printf '%s' "$issue" | jq -r '.priority // "P3"')
|
|
909
|
+
local issue_type=$(printf '%s' "$issue" | jq -r '.type // ""')
|
|
910
|
+
|
|
911
|
+
local tmpfile=$(mktemp)
|
|
912
|
+
local editor="${EDITOR:-${VISUAL:-vim}}"
|
|
913
|
+
|
|
914
|
+
cat > "$tmpfile" <<TEMPLATE
|
|
915
|
+
# ─────────────────────────────────────────────
|
|
916
|
+
# VIBAN Issue #$id
|
|
917
|
+
# ─────────────────────────────────────────────
|
|
918
|
+
# Status: ${STATUS_LABEL[$ist]}
|
|
919
|
+
# Created: ${created:0:10}
|
|
920
|
+
# ─────────────────────────────────────────────
|
|
921
|
+
|
|
922
|
+
# ▼ Priority (P0=CRITICAL, P1=HIGH, P2=MEDIUM, P3=LOW)
|
|
923
|
+
$priority
|
|
924
|
+
|
|
925
|
+
# ▼ Type (bug, feat, chore, refactor) - leave empty for none
|
|
926
|
+
$issue_type
|
|
927
|
+
|
|
928
|
+
# ▼ Title (한 줄)
|
|
929
|
+
$title
|
|
930
|
+
|
|
931
|
+
# ▼ Description (여러 줄 가능)
|
|
932
|
+
$desc
|
|
933
|
+
TEMPLATE
|
|
934
|
+
|
|
935
|
+
$editor "$tmpfile"
|
|
936
|
+
|
|
937
|
+
# Parse: priority -> type -> title -> description
|
|
938
|
+
local new_priority="" new_type="" new_title="" new_desc="" parse_stage=0
|
|
939
|
+
while IFS= read -r line; do
|
|
940
|
+
[[ "$line" =~ ^#.*$ ]] && continue
|
|
941
|
+
case $parse_stage in
|
|
942
|
+
0) # Looking for priority
|
|
943
|
+
[[ -z "$line" ]] && continue
|
|
944
|
+
# Validate priority format (P0-P3)
|
|
945
|
+
if [[ "$line" =~ ^P[0-3]$ ]]; then
|
|
946
|
+
new_priority="$line"
|
|
947
|
+
else
|
|
948
|
+
new_priority="P3" # Default if invalid
|
|
949
|
+
fi
|
|
950
|
+
parse_stage=1
|
|
951
|
+
;;
|
|
952
|
+
1) # Looking for type
|
|
953
|
+
[[ -z "$line" ]] && continue # 빈 줄은 무시하고 계속 대기
|
|
954
|
+
# Validate type format (bug, feat, chore, refactor)
|
|
955
|
+
if [[ "$line" =~ ^(bug|feat|chore|refactor)$ ]]; then
|
|
956
|
+
new_type="$line"
|
|
957
|
+
fi
|
|
958
|
+
parse_stage=2 # type이든 아니든 비-빈 줄을 만났으면 stage 2로
|
|
959
|
+
;;
|
|
960
|
+
2) # Looking for title
|
|
961
|
+
[[ -z "$line" ]] && continue
|
|
962
|
+
new_title="$line"
|
|
963
|
+
parse_stage=3
|
|
964
|
+
;;
|
|
965
|
+
3) # Collecting description
|
|
966
|
+
# Skip empty lines right after title
|
|
967
|
+
if [[ -z "$new_desc" && -z "$line" ]]; then
|
|
968
|
+
continue
|
|
969
|
+
fi
|
|
970
|
+
new_desc+="$line"$'\n'
|
|
971
|
+
;;
|
|
972
|
+
esac
|
|
973
|
+
done < "$tmpfile"
|
|
974
|
+
|
|
975
|
+
# Trim trailing newlines from description
|
|
976
|
+
new_desc="${new_desc%$'\n'}"
|
|
977
|
+
|
|
978
|
+
rm -f "$tmpfile"
|
|
979
|
+
|
|
980
|
+
[[ -z "$new_title" ]] && return 1
|
|
981
|
+
[[ -z "$new_priority" ]] && new_priority="P3"
|
|
982
|
+
|
|
983
|
+
# Update issue
|
|
984
|
+
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
985
|
+
local tmpjson=$(mktemp)
|
|
986
|
+
printf '%s' "$new_desc" > "$tmpjson"
|
|
987
|
+
jq --argjson id "$id" --arg title "$new_title" --rawfile desc "$tmpjson" --arg priority "$new_priority" --arg issue_type "$new_type" --arg now "$now" \
|
|
988
|
+
'(.issues[]|select((.id|tonumber)==$id)) |= . + {title:$title,description:$desc,priority:$priority,type:(if $issue_type == "" then null else $issue_type end),updated_at:$now}' \
|
|
989
|
+
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
990
|
+
rm -f "$tmpjson"
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
# Move card to adjacent column (change status)
|
|
994
|
+
move_card_status() {
|
|
995
|
+
local st="$1"
|
|
996
|
+
local card_idx="$2"
|
|
997
|
+
local dir="$3" # -1 for left, 1 for right
|
|
998
|
+
|
|
999
|
+
local id=$(jq -r --arg s "$st" --argjson i "$card_idx" \
|
|
1000
|
+
'.issues|map(select(.status==$s))|sort_by(if .order != null then [0, .order] else [1, ({"P0":0,"P1":1,"P2":2,"P3":3}[.priority // "P3"] // 3), .id] end)|.[$i].id // empty' "$VIBAN_JSON")
|
|
1001
|
+
[[ -z "$id" ]] && return 1
|
|
1002
|
+
|
|
1003
|
+
# Find current status index and calculate new status
|
|
1004
|
+
local cur_idx=0
|
|
1005
|
+
for i in {1..3}; do
|
|
1006
|
+
[[ "${VIBAN_STATUSES[$i]}" == "$st" ]] && { cur_idx=$i; break; }
|
|
1007
|
+
done
|
|
1008
|
+
|
|
1009
|
+
local new_idx=$((cur_idx + dir))
|
|
1010
|
+
(( new_idx < 1 || new_idx > 3 )) && return 1
|
|
1011
|
+
|
|
1012
|
+
local new_st="${VIBAN_STATUSES[$new_idx]}"
|
|
1013
|
+
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1014
|
+
|
|
1015
|
+
jq --argjson id "$id" --arg new_st "$new_st" --arg now "$now" \
|
|
1016
|
+
'(.issues[]|select((.id|tonumber)==$id)) |= . + {status:$new_st,updated_at:$now}' \
|
|
1017
|
+
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
# CLI commands
|
|
1021
|
+
cmd_list() {
|
|
1022
|
+
init_json
|
|
1023
|
+
echo ""
|
|
1024
|
+
for st in $VIBAN_STATUSES; do
|
|
1025
|
+
gum style --foreground "${STATUS_COLOR[$st]}" --bold "● ${STATUS_LABEL[$st]} ($(count_issues_by_status "$st"))"
|
|
1026
|
+
get_issues_by_status "$st" | jq -r '.[]|" #\(.id) [\(.priority // "P3")]\(if .type then " [\(.type | ascii_upcase)]" else "" end) \(.title)"'
|
|
1027
|
+
echo ""
|
|
1028
|
+
done
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
cmd_priority() {
|
|
1032
|
+
init_json
|
|
1033
|
+
[[ -z "$1" ]] && { echo "Usage: viban priority <id> <P0|P1|P2|P3>"; exit 1; }
|
|
1034
|
+
local id="$1"
|
|
1035
|
+
local new_priority="${2:-}"
|
|
1036
|
+
|
|
1037
|
+
# Validate priority
|
|
1038
|
+
if [[ ! "$new_priority" =~ ^P[0-3]$ ]]; then
|
|
1039
|
+
echo "Error: Priority must be P0, P1, P2, or P3"
|
|
1040
|
+
exit 1
|
|
1041
|
+
fi
|
|
1042
|
+
|
|
1043
|
+
# Check if issue exists
|
|
1044
|
+
local exists=$(jq --argjson id "$id" '[.issues[]|select((.id|tonumber)==$id)]|length' "$VIBAN_JSON")
|
|
1045
|
+
[[ "$exists" == "0" ]] && { echo "Error: Issue #$id not found"; exit 1; }
|
|
1046
|
+
|
|
1047
|
+
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1048
|
+
jq --argjson id "$id" --arg priority "$new_priority" --arg now "$now" \
|
|
1049
|
+
'(.issues[]|select((.id|tonumber)==$id)) |= . + {priority:$priority,updated_at:$now}' \
|
|
1050
|
+
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1051
|
+
|
|
1052
|
+
echo "✓ #$id priority → $new_priority"
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
cmd_add() {
|
|
1056
|
+
init_json
|
|
1057
|
+
[[ -z "$1" ]] && { echo "Usage: viban add \"title\" [\"description\"] [priority] [type]"; exit 1; }
|
|
1058
|
+
local id=$(get_next_id) now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1059
|
+
local desc="${2:-}"
|
|
1060
|
+
local priority="${3:-P3}"
|
|
1061
|
+
local issue_type="${4:-}"
|
|
1062
|
+
# Validate priority
|
|
1063
|
+
[[ ! "$priority" =~ ^P[0-3]$ ]] && priority="P3"
|
|
1064
|
+
# Validate type (bug, feat, chore, refactor)
|
|
1065
|
+
[[ -n "$issue_type" && ! "$issue_type" =~ ^(bug|feat|chore|refactor)$ ]] && issue_type=""
|
|
1066
|
+
# New cards don't have order - they follow priority-based sorting
|
|
1067
|
+
# Order is only assigned when manually moved
|
|
1068
|
+
local tmpjson=$(mktemp)
|
|
1069
|
+
printf '%s' "$desc" > "$tmpjson"
|
|
1070
|
+
jq --arg id "$id" --arg title "$1" --rawfile desc "$tmpjson" --arg priority "$priority" --arg issue_type "$issue_type" --arg now "$now" '
|
|
1071
|
+
.next_id = ((.next_id // 0) + 1) |
|
|
1072
|
+
.issues += [{
|
|
1073
|
+
id:($id|tonumber),
|
|
1074
|
+
title:$title,
|
|
1075
|
+
description:$desc,
|
|
1076
|
+
status:"backlog",
|
|
1077
|
+
priority:$priority,
|
|
1078
|
+
type:(if $issue_type == "" then null else $issue_type end),
|
|
1079
|
+
assigned_to:null,
|
|
1080
|
+
created_at:$now,
|
|
1081
|
+
updated_at:$now
|
|
1082
|
+
}]' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1083
|
+
rm -f "$tmpjson"
|
|
1084
|
+
local type_info=""
|
|
1085
|
+
[[ -n "$issue_type" ]] && type_info=" [$issue_type]"
|
|
1086
|
+
echo "✓ #$id added ($priority)$type_info"
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
cmd_assign() {
|
|
1090
|
+
init_json
|
|
1091
|
+
local session="${1:-$(echo $RANDOM | md5 | head -c 8)}"
|
|
1092
|
+
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")
|
|
1093
|
+
[[ "$issue" == "null" || -z "$issue" ]] && { echo "No backlog"; exit 1; }
|
|
1094
|
+
local id=$(printf '%s' "$issue" | jq -r '.id') now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1095
|
+
|
|
1096
|
+
# Update status to in_progress (no worktree - use branch workflow)
|
|
1097
|
+
jq --argjson id "$id" --arg s "$session" --arg now "$now" \
|
|
1098
|
+
'(.issues[]|select((.id|tonumber)==$id)) |= . + {status:"in_progress",assigned_to:$s,updated_at:$now}' \
|
|
1099
|
+
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1100
|
+
|
|
1101
|
+
# Set iTerm2 session name to issue number
|
|
1102
|
+
printf '\033]1;#%s\007' "$id"
|
|
1103
|
+
|
|
1104
|
+
echo "✓ #$id assigned"
|
|
1105
|
+
echo "$id"
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
cmd_review() {
|
|
1109
|
+
init_json
|
|
1110
|
+
local id="${1:-$(jq -r '.issues|map(select(.status=="in_progress"))|first|.id//empty' "$VIBAN_JSON")}"
|
|
1111
|
+
[[ -z "$id" ]] && { echo "None"; exit 1; }
|
|
1112
|
+
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1113
|
+
jq --argjson id "$id" --arg now "$now" \
|
|
1114
|
+
'(.issues[]|select((.id|tonumber)==$id)) |= . + {status:"review",assigned_to:null,updated_at:$now}' \
|
|
1115
|
+
"$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1116
|
+
|
|
1117
|
+
# Clear iTerm2 session name
|
|
1118
|
+
printf '\033]1;\007'
|
|
1119
|
+
|
|
1120
|
+
echo "✓ #$id → review"
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
cmd_done() {
|
|
1124
|
+
init_json
|
|
1125
|
+
[[ -z "$1" ]] && { echo "Usage: viban done <id>"; exit 1; }
|
|
1126
|
+
# Cleanup worktree if exists
|
|
1127
|
+
local repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
1128
|
+
local wt_dir="$VIBAN_DATA_DIR/worktrees/$1"
|
|
1129
|
+
local branch="viban-$1"
|
|
1130
|
+
if [[ -d "$wt_dir" ]]; then
|
|
1131
|
+
git -C "$repo_root" worktree remove "$wt_dir" --force 2>/dev/null
|
|
1132
|
+
git -C "$repo_root" branch -D "$branch" 2>/dev/null
|
|
1133
|
+
echo "✓ worktree removed"
|
|
1134
|
+
fi
|
|
1135
|
+
# Remove task (handle both string and number ID)
|
|
1136
|
+
jq --argjson id "$1" 'del(.issues[]|select((.id|tonumber)==$id))' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && \
|
|
1137
|
+
mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1138
|
+
|
|
1139
|
+
# Clear iTerm2 session name
|
|
1140
|
+
printf '\033]1;\007'
|
|
1141
|
+
|
|
1142
|
+
echo "✓ #$1 completed & removed"
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
cmd_get() { init_json; jq --argjson id "$1" '.issues[]|select((.id|tonumber)==$id)' "$VIBAN_JSON"; }
|
|
1146
|
+
|
|
1147
|
+
cmd_migrate() {
|
|
1148
|
+
init_json
|
|
1149
|
+
echo "Migrating issues..."
|
|
1150
|
+
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1151
|
+
|
|
1152
|
+
# Migration 1: extract [BUG], [FEATURE], [REFACTOR] from title to type field
|
|
1153
|
+
# Also strip [P0-P3] from title if present (already in priority field)
|
|
1154
|
+
echo " - Extracting type from titles..."
|
|
1155
|
+
jq --arg now "$now" '
|
|
1156
|
+
.issues = [.issues[] |
|
|
1157
|
+
# Extract type from title
|
|
1158
|
+
(if (.title | test("^\\[BUG\\]"; "i")) then "bug"
|
|
1159
|
+
elif (.title | test("^\\[FEATURE\\]"; "i")) then "feat"
|
|
1160
|
+
elif (.title | test("^\\[FEAT\\]"; "i")) then "feat"
|
|
1161
|
+
elif (.title | test("^\\[REFACTOR\\]"; "i")) then "refactor"
|
|
1162
|
+
elif (.title | test("^\\[CHORE\\]"; "i")) then "chore"
|
|
1163
|
+
else .type // null end) as $extracted_type |
|
|
1164
|
+
|
|
1165
|
+
# Clean title: remove [BUG], [FEATURE], [REFACTOR], [CHORE], [P0-P3] prefixes
|
|
1166
|
+
(.title |
|
|
1167
|
+
gsub("^\\[BUG\\]\\s*"; "") |
|
|
1168
|
+
gsub("^\\[FEATURE\\]\\s*"; "") |
|
|
1169
|
+
gsub("^\\[FEAT\\]\\s*"; "") |
|
|
1170
|
+
gsub("^\\[REFACTOR\\]\\s*"; "") |
|
|
1171
|
+
gsub("^\\[CHORE\\]\\s*"; "") |
|
|
1172
|
+
gsub("^\\[P[0-3]\\]\\s*"; "")
|
|
1173
|
+
) as $clean_title |
|
|
1174
|
+
|
|
1175
|
+
# Update issue
|
|
1176
|
+
. + {
|
|
1177
|
+
title: $clean_title,
|
|
1178
|
+
type: (if $extracted_type then $extracted_type else .type end),
|
|
1179
|
+
updated_at: $now
|
|
1180
|
+
}
|
|
1181
|
+
]
|
|
1182
|
+
' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1183
|
+
|
|
1184
|
+
# Migration 2: Remove order field from all issues
|
|
1185
|
+
# New behavior: order is only set when manually moved, otherwise follows priority
|
|
1186
|
+
echo " - Removing order field (reset to priority-based sorting)..."
|
|
1187
|
+
jq --arg now "$now" '
|
|
1188
|
+
.issues = [.issues[] | del(.order) | . + {updated_at: $now}]
|
|
1189
|
+
' "$VIBAN_JSON" > "$VIBAN_JSON.tmp" && mv "$VIBAN_JSON.tmp" "$VIBAN_JSON"
|
|
1190
|
+
|
|
1191
|
+
echo "✓ Migration complete"
|
|
1192
|
+
echo ""
|
|
1193
|
+
echo "Summary:"
|
|
1194
|
+
jq -r '
|
|
1195
|
+
[.issues[] | select(.type != null)] | group_by(.type) |
|
|
1196
|
+
.[] | " \(.[0].type): \(length) issues"
|
|
1197
|
+
' "$VIBAN_JSON"
|
|
1198
|
+
echo " (no type): $(jq '[.issues[] | select(.type == null)] | length' "$VIBAN_JSON") issues"
|
|
1199
|
+
echo ""
|
|
1200
|
+
echo "Issues by priority:"
|
|
1201
|
+
jq -r '
|
|
1202
|
+
[.issues[] | select(.status != "done")] |
|
|
1203
|
+
group_by(.priority // "P3") | sort_by(.[0].priority) |
|
|
1204
|
+
.[] | " \(.[0].priority // "P3"): \(length) issues"
|
|
1205
|
+
' "$VIBAN_JSON"
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
main() {
|
|
1209
|
+
check_deps
|
|
1210
|
+
init_json
|
|
1211
|
+
case "${1:-}" in
|
|
1212
|
+
list) cmd_list;;
|
|
1213
|
+
add) cmd_add "$2" "$3" "$4" "$5";;
|
|
1214
|
+
assign) cmd_assign "$2";;
|
|
1215
|
+
review) cmd_review "$2";;
|
|
1216
|
+
done) cmd_done "$2";;
|
|
1217
|
+
get) cmd_get "$2";;
|
|
1218
|
+
edit) [[ -z "$2" ]] && { echo "Usage: viban edit <id>"; exit 1; }; edit_issue "$2";;
|
|
1219
|
+
priority) cmd_priority "$2" "$3";;
|
|
1220
|
+
migrate) cmd_migrate;;
|
|
1221
|
+
help|--help|-h)
|
|
1222
|
+
echo "viban - Vibe Kanban"
|
|
1223
|
+
echo ""
|
|
1224
|
+
echo " viban TUI"
|
|
1225
|
+
echo " viban list Show board"
|
|
1226
|
+
echo " viban add \"title\" [\"desc\"] [P0-P3] [type] Add task"
|
|
1227
|
+
echo " viban priority <id> <P0-P3> Set priority"
|
|
1228
|
+
echo " viban assign Assign first backlog (by priority)"
|
|
1229
|
+
echo " viban review → Human Review"
|
|
1230
|
+
echo " viban done <id> Complete & remove"
|
|
1231
|
+
echo " viban edit <id> Edit task in editor"
|
|
1232
|
+
echo " viban migrate Migrate: extract type from title"
|
|
1233
|
+
echo ""
|
|
1234
|
+
echo " Priority: P0=CRITICAL, P1=HIGH, P2=MEDIUM, P3=LOW"
|
|
1235
|
+
echo " Type: bug, feat, chore, refactor"
|
|
1236
|
+
;;
|
|
1237
|
+
"") level1_columns;;
|
|
1238
|
+
*) echo "Unknown: $1"; exit 1;;
|
|
1239
|
+
esac
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
main "$@"
|