|
#!/bin/bash |
|
########################################################################### |
|
# Claude Code 並行開発 ステータスモニター |
|
# 使い方: watch -n 5 -c ./status.sh |
|
# |
|
# git / tmux の状態から自動検出し、STATUS.md も自動生成する。 |
|
# Claude インスタンスの自己申告に依存しない。 |
|
########################################################################### |
|
|
|
# ▼ あなたの環境に合わせて書き換える |
|
BASE_DIR="$HOME/myproj_base" |
|
TMUX_SESSION="myproj" |
|
PROJECT_PREFIX="project" # 作業ディレクトリ名のプレフィックス |
|
INSTANCE_COUNT=3 |
|
GH_REPO="YOUR_ORG/YOUR_REPO" # GitHub Issue タイトル取得用(不要なら空文字でOK) |
|
STATUS_FILE="$BASE_DIR/STATUS.md" |
|
|
|
# ▼ Vite/API ポート(並列起動用にずらす) |
|
get_ports() { |
|
local n="$1" |
|
case $n in |
|
1) echo "5203 30021" ;; |
|
2) echo "5204 31021" ;; |
|
3) echo "5205 32021" ;; |
|
*) echo "$((5202 + n)) $((30021 + (n - 1) * 1000))" ;; |
|
esac |
|
} |
|
|
|
# 色定義 |
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
CYAN='\033[0;36m' |
|
BOLD='\033[1m' |
|
DIM='\033[2m' |
|
NC='\033[0m' |
|
|
|
# --- ブランチ名から Issue 番号を推測 --- |
|
extract_issue_number() { |
|
local branch="$1" |
|
local num |
|
num=$(echo "$branch" | grep -oP '(?<![0-9])\d{1,4}(?![0-9])' | head -1) |
|
if [ -n "$num" ] && [ "$num" -gt 0 ] 2>/dev/null; then |
|
echo "$num" |
|
else |
|
echo "" |
|
fi |
|
} |
|
|
|
# --- GitHub Issue のタイトルを取得(5分キャッシュ) --- |
|
ISSUE_CACHE_DIR="/tmp/cc-status-issue-cache" |
|
mkdir -p "$ISSUE_CACHE_DIR" 2>/dev/null |
|
|
|
fetch_issue_title() { |
|
local num="$1" |
|
[ -z "$num" ] || [ -z "$GH_REPO" ] && return |
|
|
|
local cache_file="$ISSUE_CACHE_DIR/$num" |
|
if [ -f "$cache_file" ] && [ "$(find "$cache_file" -mmin -5 2>/dev/null)" ]; then |
|
cat "$cache_file" |
|
return |
|
fi |
|
|
|
local title |
|
title=$(gh issue view "$num" --repo "$GH_REPO" --json title --jq .title 2>/dev/null) |
|
if [ -n "$title" ]; then |
|
echo "$title" > "$cache_file" |
|
echo "$title" |
|
fi |
|
} |
|
|
|
# --- tmux ペインから Claude の状態を検出 --- |
|
# Claude Code のスピナー表示中に出る "↓ N tokens" を見て処理中を判定する。 |
|
# スピナー文字(✻ / ✶ / *)に依存しないため安定。 |
|
detect_claude_state() { |
|
local win_index="$1" |
|
[ -z "$win_index" ] && echo "?" && return |
|
|
|
local pane_text |
|
pane_text=$(tmux capture-pane -t "${TMUX_SESSION}:${win_index}" -p 2>/dev/null) |
|
|
|
if [ -z "$pane_text" ]; then |
|
echo "?" |
|
return |
|
fi |
|
|
|
local last_lines |
|
last_lines=$(echo "$pane_text" | tail -5) |
|
|
|
# 「↓ N tokens」が含まれる行 = スピナー表示中(処理中) |
|
if echo "$pane_text" | grep -qP '↓\s*[\d.]+k?\s*tokens'; then |
|
echo "作業中" |
|
elif echo "$last_lines" | grep -q "INSERT"; then |
|
if echo "$last_lines" | grep -q "plan mode"; then |
|
echo "plan待ち" |
|
else |
|
echo "入力待ち" |
|
fi |
|
elif echo "$last_lines" | grep -q "Try \"how does"; then |
|
echo "未指示" |
|
elif echo "$last_lines" | grep -q '^\$\|^❯'; then |
|
echo "shell" |
|
else |
|
echo "?" |
|
fi |
|
} |
|
|
|
# ======================================================================= |
|
# メイン処理 |
|
# ======================================================================= |
|
|
|
echo -e "${BOLD}並行開発${NC} ${DIM}$(date '+%H:%M:%S')${NC}" |
|
|
|
# ss 結果をキャッシュ(ループ内で毎回呼ばない) |
|
SS_CACHE=$(ss -tlnp 2>/dev/null) |
|
|
|
MD_ROWS="" |
|
|
|
for N in $(seq 1 "$INSTANCE_COUNT"); do |
|
DIR="$BASE_DIR/${PROJECT_PREFIX}_$N" |
|
[ ! -d "$DIR" ] && continue |
|
|
|
# git 情報 |
|
BRANCH=$(git -C "$DIR" branch --show-current 2>/dev/null) |
|
DIRTY=$(git -C "$DIR" status --porcelain 2>/dev/null | wc -l) |
|
|
|
# ポート |
|
read VITE API <<< "$(get_ports "$N")" |
|
|
|
# サービス起動確認 |
|
echo "$SS_CACHE" | grep -q ":$VITE " && V="${GREEN}V${NC}" || V="${DIM}-${NC}" |
|
echo "$SS_CACHE" | grep -q ":$VITE " && V_MD="V" || V_MD="-" |
|
echo "$SS_CACHE" | grep -q ":$API " && S="${GREEN}S${NC}" || S="${DIM}-${NC}" |
|
echo "$SS_CACHE" | grep -q ":$API " && S_MD="S" || S_MD="-" |
|
|
|
# Issue 番号(ブランチ名から自動検出) |
|
ISSUE_NUM=$(extract_issue_number "$BRANCH") |
|
ISSUE_TITLE="" |
|
[ -n "$ISSUE_NUM" ] && ISSUE_TITLE=$(fetch_issue_title "$ISSUE_NUM") |
|
|
|
# Claude 状態(ウィンドウインデックス N+1 で参照: manager=1, work-1=2, ...) |
|
WIN_IDX=$((N + 1)) |
|
CLAUDE=$(detect_claude_state "$WIN_IDX") |
|
|
|
# tmux ウィンドウタイトル更新 |
|
if tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then |
|
if [ -n "$ISSUE_NUM" ]; then |
|
tmux rename-window -t "$TMUX_SESSION:$WIN_IDX" "${N}-#${ISSUE_NUM}" 2>/dev/null |
|
else |
|
tmux rename-window -t "$TMUX_SESSION:$WIN_IDX" "${N}" 2>/dev/null |
|
fi |
|
fi |
|
|
|
# --- コンパクト表示(2行) --- |
|
ISSUE_DISPLAY="" |
|
if [ -n "$ISSUE_NUM" ]; then |
|
TITLE_SHORT=$(python3 -c "import sys; s=sys.argv[1]; print(s[:20]+'…' if len(s)>20 else s)" "$ISSUE_TITLE" 2>/dev/null || echo "$ISSUE_TITLE") |
|
ISSUE_DISPLAY="${BOLD}#${ISSUE_NUM}${NC} ${TITLE_SHORT}" |
|
fi |
|
|
|
if [ "$CLAUDE" = "作業中" ]; then |
|
ST="${GREEN}作業中${NC}" |
|
elif [ "$CLAUDE" = "入力待ち" ] || [ "$CLAUDE" = "plan待ち" ]; then |
|
ST="${YELLOW}${CLAUDE}${NC}" |
|
elif [ "$CLAUDE" = "未指示" ] || [ "$CLAUDE" = "shell" ]; then |
|
ST="${DIM}待機${NC}" |
|
else |
|
ST="${DIM}${CLAUDE}${NC}" |
|
fi |
|
|
|
echo -e "${CYAN}[${N}]${NC} ${ISSUE_DISPLAY:-${YELLOW}${BRANCH}${NC}} ${ST}" |
|
echo -e " ${DIM}${BRANCH}${NC} Δ${DIRTY} ${V}${S} ${DIM}claude:${NC}${CLAUDE}" |
|
|
|
# STATUS.md 行 |
|
ISSUE_MD="-" |
|
if [ -n "$ISSUE_NUM" ] && [ -n "$GH_REPO" ]; then |
|
ISSUE_MD="[#${ISSUE_NUM}](https://github.com/$GH_REPO/issues/$ISSUE_NUM) ${ISSUE_TITLE}" |
|
elif [ -n "$ISSUE_NUM" ]; then |
|
ISSUE_MD="#${ISSUE_NUM} ${ISSUE_TITLE}" |
|
fi |
|
MD_ROWS="${MD_ROWS}| ${N} | ${ISSUE_MD} | ${BRANCH} | ${CLAUDE} | ${V_MD}${S_MD} Δ${DIRTY} |\n" |
|
done |
|
|
|
# STATUS.md 自動生成 |
|
cat > "$STATUS_FILE" <<EOF |
|
| # | Issue | ブランチ | Claude | メモ | |
|
|---|-------|----------|--------|------| |
|
$(echo -e "$MD_ROWS" | sed '/^$/d') |
|
|
|
_$(date '+%H:%M:%S')_ |
|
EOF |