shipwright-cli 1.7.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/LICENSE +21 -0
- package/README.md +926 -0
- package/claude-code/CLAUDE.md.shipwright +125 -0
- package/claude-code/hooks/notify-idle.sh +35 -0
- package/claude-code/hooks/pre-compact-save.sh +57 -0
- package/claude-code/hooks/task-completed.sh +170 -0
- package/claude-code/hooks/teammate-idle.sh +68 -0
- package/claude-code/settings.json.template +184 -0
- package/completions/_shipwright +140 -0
- package/completions/shipwright.bash +89 -0
- package/completions/shipwright.fish +107 -0
- package/docs/KNOWN-ISSUES.md +199 -0
- package/docs/TIPS.md +331 -0
- package/docs/definition-of-done.example.md +16 -0
- package/docs/patterns/README.md +139 -0
- package/docs/patterns/audit-loop.md +149 -0
- package/docs/patterns/bug-hunt.md +183 -0
- package/docs/patterns/feature-implementation.md +159 -0
- package/docs/patterns/refactoring.md +183 -0
- package/docs/patterns/research-exploration.md +144 -0
- package/docs/patterns/test-generation.md +173 -0
- package/package.json +49 -0
- package/scripts/adapters/docker-deploy.sh +50 -0
- package/scripts/adapters/fly-deploy.sh +41 -0
- package/scripts/adapters/iterm2-adapter.sh +122 -0
- package/scripts/adapters/railway-deploy.sh +34 -0
- package/scripts/adapters/tmux-adapter.sh +87 -0
- package/scripts/adapters/vercel-deploy.sh +35 -0
- package/scripts/adapters/wezterm-adapter.sh +103 -0
- package/scripts/cct +242 -0
- package/scripts/cct-cleanup.sh +172 -0
- package/scripts/cct-cost.sh +590 -0
- package/scripts/cct-daemon.sh +3189 -0
- package/scripts/cct-doctor.sh +328 -0
- package/scripts/cct-fix.sh +478 -0
- package/scripts/cct-fleet.sh +904 -0
- package/scripts/cct-init.sh +282 -0
- package/scripts/cct-logs.sh +273 -0
- package/scripts/cct-loop.sh +1332 -0
- package/scripts/cct-memory.sh +1148 -0
- package/scripts/cct-pipeline.sh +3844 -0
- package/scripts/cct-prep.sh +1352 -0
- package/scripts/cct-ps.sh +168 -0
- package/scripts/cct-reaper.sh +390 -0
- package/scripts/cct-session.sh +284 -0
- package/scripts/cct-status.sh +169 -0
- package/scripts/cct-templates.sh +242 -0
- package/scripts/cct-upgrade.sh +422 -0
- package/scripts/cct-worktree.sh +405 -0
- package/scripts/postinstall.mjs +96 -0
- package/templates/pipelines/autonomous.json +71 -0
- package/templates/pipelines/cost-aware.json +95 -0
- package/templates/pipelines/deployed.json +79 -0
- package/templates/pipelines/enterprise.json +114 -0
- package/templates/pipelines/fast.json +63 -0
- package/templates/pipelines/full.json +104 -0
- package/templates/pipelines/hotfix.json +63 -0
- package/templates/pipelines/standard.json +91 -0
- package/tmux/claude-teams-overlay.conf +109 -0
- package/tmux/templates/architecture.json +19 -0
- package/tmux/templates/bug-fix.json +24 -0
- package/tmux/templates/code-review.json +24 -0
- package/tmux/templates/devops.json +19 -0
- package/tmux/templates/documentation.json +19 -0
- package/tmux/templates/exploration.json +19 -0
- package/tmux/templates/feature-dev.json +24 -0
- package/tmux/templates/full-stack.json +24 -0
- package/tmux/templates/migration.json +24 -0
- package/tmux/templates/refactor.json +19 -0
- package/tmux/templates/security-audit.json +24 -0
- package/tmux/templates/testing.json +24 -0
- package/tmux/tmux.conf +167 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ cct-session.sh — Launch a Claude Code team session in a new tmux window║
|
|
4
|
+
# ║ ║
|
|
5
|
+
# ║ Uses new-window (NOT split-window) to avoid the tmux send-keys race ║
|
|
6
|
+
# ║ condition that affects 4+ agents. See KNOWN-ISSUES.md for details. ║
|
|
7
|
+
# ║ ║
|
|
8
|
+
# ║ Supports --template to scaffold from a team template and --terminal ║
|
|
9
|
+
# ║ to select a terminal adapter (tmux, iterm2, wezterm). ║
|
|
10
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
|
|
15
|
+
# ─── Colors ──────────────────────────────────────────────────────────────────
|
|
16
|
+
CYAN='\033[38;2;0;212;255m'
|
|
17
|
+
PURPLE='\033[38;2;124;58;237m'
|
|
18
|
+
GREEN='\033[38;2;74;222;128m'
|
|
19
|
+
YELLOW='\033[38;2;250;204;21m'
|
|
20
|
+
RED='\033[38;2;248;113;113m'
|
|
21
|
+
DIM='\033[2m'
|
|
22
|
+
BOLD='\033[1m'
|
|
23
|
+
RESET='\033[0m'
|
|
24
|
+
|
|
25
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
26
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
27
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
28
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
29
|
+
|
|
30
|
+
# ─── Parse Arguments ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
TEAM_NAME=""
|
|
33
|
+
TEMPLATE_NAME=""
|
|
34
|
+
TERMINAL_ADAPTER=""
|
|
35
|
+
|
|
36
|
+
while [[ $# -gt 0 ]]; do
|
|
37
|
+
case "$1" in
|
|
38
|
+
--template|-t)
|
|
39
|
+
TEMPLATE_NAME="${2:-}"
|
|
40
|
+
[[ -z "$TEMPLATE_NAME" ]] && { error "Missing template name after --template"; exit 1; }
|
|
41
|
+
shift 2
|
|
42
|
+
;;
|
|
43
|
+
--terminal)
|
|
44
|
+
TERMINAL_ADAPTER="${2:-}"
|
|
45
|
+
[[ -z "$TERMINAL_ADAPTER" ]] && { error "Missing adapter name after --terminal"; exit 1; }
|
|
46
|
+
shift 2
|
|
47
|
+
;;
|
|
48
|
+
--help|-h)
|
|
49
|
+
echo -e "${CYAN}${BOLD}shipwright session${RESET} — Create a new team session"
|
|
50
|
+
echo ""
|
|
51
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
52
|
+
echo -e " shipwright session [name] [--template <name>] [--terminal <adapter>]"
|
|
53
|
+
echo ""
|
|
54
|
+
echo -e "${BOLD}OPTIONS${RESET}"
|
|
55
|
+
echo -e " ${CYAN}--template, -t${RESET} <name> Use a team template (see: shipwright templates list)"
|
|
56
|
+
echo -e " ${CYAN}--terminal${RESET} <adapter> Terminal adapter: tmux (default), iterm2, wezterm"
|
|
57
|
+
echo ""
|
|
58
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
59
|
+
echo -e " ${DIM}shipwright session refactor${RESET}"
|
|
60
|
+
echo -e " ${DIM}shipwright session my-feature --template feature-dev${RESET}"
|
|
61
|
+
echo -e " ${DIM}shipwright session my-feature --terminal iterm2${RESET}"
|
|
62
|
+
exit 0
|
|
63
|
+
;;
|
|
64
|
+
-*)
|
|
65
|
+
error "Unknown option: $1"
|
|
66
|
+
exit 1
|
|
67
|
+
;;
|
|
68
|
+
*)
|
|
69
|
+
# Positional: team name
|
|
70
|
+
[[ -z "$TEAM_NAME" ]] && TEAM_NAME="$1" || { error "Unexpected argument: $1"; exit 1; }
|
|
71
|
+
shift
|
|
72
|
+
;;
|
|
73
|
+
esac
|
|
74
|
+
done
|
|
75
|
+
|
|
76
|
+
TEAM_NAME="${TEAM_NAME:-team-$(date +%s)}"
|
|
77
|
+
WINDOW_NAME="claude-${TEAM_NAME}"
|
|
78
|
+
|
|
79
|
+
# ─── Template Loading ───────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
TEMPLATE_FILE=""
|
|
82
|
+
TEMPLATE_LAYOUT=""
|
|
83
|
+
TEMPLATE_LAYOUT_STYLE=""
|
|
84
|
+
TEMPLATE_MAIN_PANE_PERCENT=""
|
|
85
|
+
TEMPLATE_DESC=""
|
|
86
|
+
TEMPLATE_AGENTS=() # Populated as "name|role|focus" entries
|
|
87
|
+
|
|
88
|
+
if [[ -n "$TEMPLATE_NAME" ]]; then
|
|
89
|
+
# Search for template: user dir first, then repo dir
|
|
90
|
+
USER_TEMPLATES_DIR="${HOME}/.claude-teams/templates"
|
|
91
|
+
REPO_TEMPLATES_DIR="$(cd "$SCRIPT_DIR/../tmux/templates" 2>/dev/null && pwd)" || REPO_TEMPLATES_DIR=""
|
|
92
|
+
|
|
93
|
+
TEMPLATE_NAME="${TEMPLATE_NAME%.json}"
|
|
94
|
+
|
|
95
|
+
if [[ -f "$USER_TEMPLATES_DIR/${TEMPLATE_NAME}.json" ]]; then
|
|
96
|
+
TEMPLATE_FILE="$USER_TEMPLATES_DIR/${TEMPLATE_NAME}.json"
|
|
97
|
+
elif [[ -n "$REPO_TEMPLATES_DIR" && -f "$REPO_TEMPLATES_DIR/${TEMPLATE_NAME}.json" ]]; then
|
|
98
|
+
TEMPLATE_FILE="$REPO_TEMPLATES_DIR/${TEMPLATE_NAME}.json"
|
|
99
|
+
else
|
|
100
|
+
error "Template '${TEMPLATE_NAME}' not found."
|
|
101
|
+
echo -e " Run ${DIM}shipwright templates list${RESET} to see available templates."
|
|
102
|
+
exit 1
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
info "Loading template: ${PURPLE}${BOLD}${TEMPLATE_NAME}${RESET}"
|
|
106
|
+
|
|
107
|
+
# Parse template — single jq call extracts all fields + agents in one pass
|
|
108
|
+
if command -v jq &>/dev/null; then
|
|
109
|
+
# Single jq call: outputs metadata lines then agent lines
|
|
110
|
+
# Format: META<tab>field<tab>value for metadata, AGENT<tab>name|role|focus for agents
|
|
111
|
+
while IFS=$'\t' read -r tag key value; do
|
|
112
|
+
case "$tag" in
|
|
113
|
+
META)
|
|
114
|
+
case "$key" in
|
|
115
|
+
description) TEMPLATE_DESC="$value" ;;
|
|
116
|
+
layout) TEMPLATE_LAYOUT="$value" ;;
|
|
117
|
+
layout_style) TEMPLATE_LAYOUT_STYLE="$value" ;;
|
|
118
|
+
main_pane_percent) TEMPLATE_MAIN_PANE_PERCENT="$value" ;;
|
|
119
|
+
esac
|
|
120
|
+
;;
|
|
121
|
+
AGENT) [[ -n "$key" ]] && TEMPLATE_AGENTS+=("$key") ;;
|
|
122
|
+
esac
|
|
123
|
+
done < <(jq -r '
|
|
124
|
+
"META\tdescription\t\(.description // "")",
|
|
125
|
+
"META\tlayout\t\(.layout // "tiled")",
|
|
126
|
+
"META\tlayout_style\t\(.layout_style // "")",
|
|
127
|
+
"META\tmain_pane_percent\t\(.main_pane_percent // "")",
|
|
128
|
+
(.agents // [] | .[] | "AGENT\t\(.name)|\(.role // "")|\(.focus // "")\t")
|
|
129
|
+
' "$TEMPLATE_FILE")
|
|
130
|
+
else
|
|
131
|
+
error "jq is required for template parsing."
|
|
132
|
+
echo -e " ${DIM}brew install jq${RESET}"
|
|
133
|
+
exit 1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
echo -e " ${DIM}${TEMPLATE_DESC}${RESET}"
|
|
137
|
+
echo -e " ${DIM}Agents: ${#TEMPLATE_AGENTS[@]} Layout: ${TEMPLATE_LAYOUT}${RESET}"
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# ─── Resolve Terminal Adapter ───────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
# Auto-detect if not specified
|
|
143
|
+
if [[ -z "$TERMINAL_ADAPTER" ]]; then
|
|
144
|
+
TERMINAL_ADAPTER="tmux"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
ADAPTER_FILE="$SCRIPT_DIR/adapters/${TERMINAL_ADAPTER}-adapter.sh"
|
|
148
|
+
if [[ -f "$ADAPTER_FILE" ]]; then
|
|
149
|
+
# shellcheck source=/dev/null
|
|
150
|
+
source "$ADAPTER_FILE"
|
|
151
|
+
else
|
|
152
|
+
# Default to inline tmux behavior (backwards compatible)
|
|
153
|
+
if [[ "$TERMINAL_ADAPTER" != "tmux" ]]; then
|
|
154
|
+
error "Terminal adapter '${TERMINAL_ADAPTER}' not found."
|
|
155
|
+
echo -e " Available: tmux (default), iterm2, wezterm"
|
|
156
|
+
echo -e " Adapter dir: ${DIM}${SCRIPT_DIR}/adapters/${RESET}"
|
|
157
|
+
exit 1
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# ─── Create Session (tmux default path) ─────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
if [[ "$TERMINAL_ADAPTER" == "tmux" && ! -f "$ADAPTER_FILE" ]]; then
|
|
164
|
+
# Inline tmux path — original behavior (adapter not required for tmux)
|
|
165
|
+
|
|
166
|
+
# Check if a window with this name already exists
|
|
167
|
+
if tmux list-windows -F '#W' 2>/dev/null | grep -qx "$WINDOW_NAME"; then
|
|
168
|
+
warn "Window '${WINDOW_NAME}' already exists. Switching to it."
|
|
169
|
+
tmux select-window -t "$WINDOW_NAME"
|
|
170
|
+
exit 0
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
info "Creating team session: ${CYAN}${BOLD}${TEAM_NAME}${RESET}"
|
|
174
|
+
|
|
175
|
+
# Create a new window (not split-window — avoids race condition #23615)
|
|
176
|
+
tmux new-window -n "$WINDOW_NAME" -c "#{pane_current_path}"
|
|
177
|
+
|
|
178
|
+
# Force dark theme on the new pane (belt-and-suspenders with overlay hooks)
|
|
179
|
+
tmux select-pane -t "$WINDOW_NAME" -P 'bg=#1a1a2e,fg=#e4e4e7'
|
|
180
|
+
|
|
181
|
+
# Set the pane title so the overlay shows the team name
|
|
182
|
+
tmux send-keys -t "$WINDOW_NAME" "printf '\\033]2;${TEAM_NAME}-lead\\033\\\\'" Enter
|
|
183
|
+
|
|
184
|
+
sleep 0.2
|
|
185
|
+
tmux send-keys -t "$WINDOW_NAME" "clear" Enter
|
|
186
|
+
|
|
187
|
+
# ─── Template: Create Agent Panes ────────────────────────────────────────
|
|
188
|
+
if [[ ${#TEMPLATE_AGENTS[@]} -gt 0 ]]; then
|
|
189
|
+
info "Scaffolding ${#TEMPLATE_AGENTS[@]} agent panes..."
|
|
190
|
+
|
|
191
|
+
for agent_entry in "${TEMPLATE_AGENTS[@]}"; do
|
|
192
|
+
IFS='|' read -r aname arole afocus <<< "$agent_entry"
|
|
193
|
+
|
|
194
|
+
# Split the window to create a new pane
|
|
195
|
+
tmux split-window -t "$WINDOW_NAME" -c "#{pane_current_path}"
|
|
196
|
+
sleep 0.1
|
|
197
|
+
|
|
198
|
+
# Force dark theme on agent pane
|
|
199
|
+
tmux select-pane -t "$WINDOW_NAME" -P 'bg=#1a1a2e,fg=#e4e4e7'
|
|
200
|
+
|
|
201
|
+
# Set the pane title to the agent name
|
|
202
|
+
tmux send-keys -t "$WINDOW_NAME" "printf '\\033]2;${TEAM_NAME}-${aname}\\033\\\\'" Enter
|
|
203
|
+
sleep 0.1
|
|
204
|
+
tmux send-keys -t "$WINDOW_NAME" "clear" Enter
|
|
205
|
+
done
|
|
206
|
+
|
|
207
|
+
# Apply the layout from the template (layout_style takes precedence over layout)
|
|
208
|
+
if [[ -n "$TEMPLATE_LAYOUT_STYLE" ]]; then
|
|
209
|
+
tmux select-layout -t "$WINDOW_NAME" "$TEMPLATE_LAYOUT_STYLE" 2>/dev/null || \
|
|
210
|
+
tmux select-layout -t "$WINDOW_NAME" "${TEMPLATE_LAYOUT:-tiled}" 2>/dev/null || true
|
|
211
|
+
else
|
|
212
|
+
tmux select-layout -t "$WINDOW_NAME" "${TEMPLATE_LAYOUT:-tiled}" 2>/dev/null || true
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# Resize leader pane to desired percentage
|
|
216
|
+
if [[ -n "$TEMPLATE_MAIN_PANE_PERCENT" && -n "$TEMPLATE_LAYOUT_STYLE" ]]; then
|
|
217
|
+
case "$TEMPLATE_LAYOUT_STYLE" in
|
|
218
|
+
main-horizontal) tmux resize-pane -t "$WINDOW_NAME.0" -x "${TEMPLATE_MAIN_PANE_PERCENT}%" 2>/dev/null ;;
|
|
219
|
+
main-vertical) tmux resize-pane -t "$WINDOW_NAME.0" -y "${TEMPLATE_MAIN_PANE_PERCENT}%" 2>/dev/null ;;
|
|
220
|
+
esac
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# Select the first pane (leader)
|
|
224
|
+
tmux select-pane -t "$WINDOW_NAME.0"
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
else
|
|
228
|
+
# ─── Adapter-based session creation ──────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
if type -t spawn_agent &>/dev/null; then
|
|
231
|
+
info "Creating team session: ${CYAN}${BOLD}${TEAM_NAME}${RESET} ${DIM}(${TERMINAL_ADAPTER})${RESET}"
|
|
232
|
+
|
|
233
|
+
# Spawn leader
|
|
234
|
+
spawn_agent "${TEAM_NAME}-lead" "#{pane_current_path}" ""
|
|
235
|
+
|
|
236
|
+
# Spawn template agents if provided
|
|
237
|
+
if [[ ${#TEMPLATE_AGENTS[@]} -gt 0 ]]; then
|
|
238
|
+
info "Scaffolding ${#TEMPLATE_AGENTS[@]} agents..."
|
|
239
|
+
for agent_entry in "${TEMPLATE_AGENTS[@]}"; do
|
|
240
|
+
IFS='|' read -r aname arole afocus <<< "$agent_entry"
|
|
241
|
+
spawn_agent "${TEAM_NAME}-${aname}" "#{pane_current_path}" ""
|
|
242
|
+
done
|
|
243
|
+
fi
|
|
244
|
+
else
|
|
245
|
+
error "Adapter '${TERMINAL_ADAPTER}' loaded but spawn_agent() not found."
|
|
246
|
+
exit 1
|
|
247
|
+
fi
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
# ─── Summary ────────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
echo ""
|
|
253
|
+
success "Team session ${CYAN}${BOLD}${TEAM_NAME}${RESET} ready!"
|
|
254
|
+
|
|
255
|
+
if [[ ${#TEMPLATE_AGENTS[@]} -gt 0 ]]; then
|
|
256
|
+
echo ""
|
|
257
|
+
echo -e "${BOLD}Team from template ${PURPLE}${TEMPLATE_NAME}${RESET}${BOLD}:${RESET}"
|
|
258
|
+
echo -e " ${CYAN}${BOLD}lead${RESET} ${DIM}— Team coordinator${RESET}"
|
|
259
|
+
for agent_entry in "${TEMPLATE_AGENTS[@]}"; do
|
|
260
|
+
IFS='|' read -r aname arole afocus <<< "$agent_entry"
|
|
261
|
+
echo -e " ${PURPLE}${BOLD}${aname}${RESET} ${DIM}— ${arole}${RESET}"
|
|
262
|
+
done
|
|
263
|
+
echo ""
|
|
264
|
+
echo -e "${BOLD}Next steps:${RESET}"
|
|
265
|
+
echo -e " ${CYAN}1.${RESET} Switch to window ${DIM}${WINDOW_NAME}${RESET}"
|
|
266
|
+
echo -e " ${CYAN}2.${RESET} Start ${DIM}claude${RESET} in the lead pane (top-left)"
|
|
267
|
+
echo -e " ${CYAN}3.${RESET} Ask Claude to use the team — agents are ready in their panes"
|
|
268
|
+
else
|
|
269
|
+
echo ""
|
|
270
|
+
echo -e "${BOLD}Next steps:${RESET}"
|
|
271
|
+
echo -e " ${CYAN}1.${RESET} Switch to window ${DIM}${WINDOW_NAME}${RESET} ${DIM}(prefix + $(tmux list-windows -F '#I #W' | grep "$WINDOW_NAME" | cut -d' ' -f1))${RESET}"
|
|
272
|
+
echo -e " ${CYAN}2.${RESET} Start Claude Code:"
|
|
273
|
+
echo -e " ${DIM}claude${RESET}"
|
|
274
|
+
echo -e " ${CYAN}3.${RESET} Ask Claude to create a team:"
|
|
275
|
+
echo -e " ${DIM}\"Create a team with 2 agents to refactor the auth module\"${RESET}"
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
echo ""
|
|
279
|
+
echo -e "${PURPLE}${BOLD}Tip:${RESET} For file isolation between agents, use git worktrees:"
|
|
280
|
+
echo -e " ${DIM}git worktree add ../project-${TEAM_NAME} -b ${TEAM_NAME}${RESET}"
|
|
281
|
+
echo -e " Then launch Claude inside the worktree directory."
|
|
282
|
+
echo ""
|
|
283
|
+
echo -e "${DIM}Settings: ~/.claude/settings.json (see settings.json.template)${RESET}"
|
|
284
|
+
echo -e "${DIM}Keybinding: prefix + T re-runs this command${RESET}"
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ cct-status.sh — Dashboard showing Claude Code team status ║
|
|
4
|
+
# ║ ║
|
|
5
|
+
# ║ Shows running teams, agent windows, and task progress. ║
|
|
6
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
# ─── Colors ──────────────────────────────────────────────────────────────────
|
|
10
|
+
CYAN='\033[38;2;0;212;255m'
|
|
11
|
+
PURPLE='\033[38;2;124;58;237m'
|
|
12
|
+
BLUE='\033[38;2;0;102;255m'
|
|
13
|
+
GREEN='\033[38;2;74;222;128m'
|
|
14
|
+
YELLOW='\033[38;2;250;204;21m'
|
|
15
|
+
RED='\033[38;2;248;113;113m'
|
|
16
|
+
DIM='\033[2m'
|
|
17
|
+
BOLD='\033[1m'
|
|
18
|
+
RESET='\033[0m'
|
|
19
|
+
|
|
20
|
+
# ─── Header ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
echo ""
|
|
23
|
+
echo -e "${CYAN}${BOLD} Claude Code Teams — Status Dashboard${RESET}"
|
|
24
|
+
echo -e "${DIM} $(date '+%Y-%m-%d %H:%M:%S')${RESET}"
|
|
25
|
+
echo -e "${DIM} ══════════════════════════════════════════${RESET}"
|
|
26
|
+
echo ""
|
|
27
|
+
|
|
28
|
+
# ─── 1. Tmux Windows ────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
echo -e "${PURPLE}${BOLD} TMUX WINDOWS${RESET}"
|
|
31
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
32
|
+
|
|
33
|
+
# Get all windows, highlight Claude-related ones
|
|
34
|
+
HAS_CLAUDE_WINDOWS=false
|
|
35
|
+
while IFS= read -r line; do
|
|
36
|
+
session_window="$(echo "$line" | cut -d'|' -f1)"
|
|
37
|
+
window_name="$(echo "$line" | cut -d'|' -f2)"
|
|
38
|
+
pane_count="$(echo "$line" | cut -d'|' -f3)"
|
|
39
|
+
active="$(echo "$line" | cut -d'|' -f4)"
|
|
40
|
+
|
|
41
|
+
if echo "$window_name" | grep -qi "claude"; then
|
|
42
|
+
HAS_CLAUDE_WINDOWS=true
|
|
43
|
+
if [[ "$active" == "1" ]]; then
|
|
44
|
+
status_icon="${GREEN}●${RESET}"
|
|
45
|
+
status_label="${GREEN}active${RESET}"
|
|
46
|
+
else
|
|
47
|
+
status_icon="${YELLOW}●${RESET}"
|
|
48
|
+
status_label="${YELLOW}idle${RESET}"
|
|
49
|
+
fi
|
|
50
|
+
echo -e " ${status_icon} ${BOLD}${window_name}${RESET} ${DIM}${session_window}${RESET} panes:${pane_count} ${status_label}"
|
|
51
|
+
fi
|
|
52
|
+
done < <(tmux list-windows -a -F '#{session_name}:#{window_index}|#{window_name}|#{window_panes}|#{window_active}' 2>/dev/null || true)
|
|
53
|
+
|
|
54
|
+
if ! $HAS_CLAUDE_WINDOWS; then
|
|
55
|
+
echo -e " ${DIM}No Claude team windows found.${RESET}"
|
|
56
|
+
echo -e " ${DIM}Start one with: ${CYAN}shipwright session <name>${RESET}"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# ─── 2. Team Configurations ─────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
echo ""
|
|
62
|
+
echo -e "${PURPLE}${BOLD} TEAM CONFIGS${RESET} ${DIM}~/.claude/teams/${RESET}"
|
|
63
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
64
|
+
|
|
65
|
+
TEAMS_DIR="${HOME}/.claude/teams"
|
|
66
|
+
HAS_TEAMS=false
|
|
67
|
+
|
|
68
|
+
if [[ -d "$TEAMS_DIR" ]]; then
|
|
69
|
+
while IFS= read -r team_dir; do
|
|
70
|
+
[[ -z "$team_dir" ]] && continue
|
|
71
|
+
HAS_TEAMS=true
|
|
72
|
+
team_name="$(basename "$team_dir")"
|
|
73
|
+
|
|
74
|
+
# Try to read config.json for member info
|
|
75
|
+
config_file="${team_dir}/config.json"
|
|
76
|
+
if [[ -f "$config_file" ]]; then
|
|
77
|
+
# Count members from JSON (look for "name" keys in members array)
|
|
78
|
+
member_count=$(grep -c '"name"' "$config_file" 2>/dev/null || true)
|
|
79
|
+
member_count="${member_count:-0}"
|
|
80
|
+
# Extract member names
|
|
81
|
+
member_names=$(grep '"name"' "$config_file" 2>/dev/null | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | tr '\n' ', ' | sed 's/,$//' | sed 's/,/, /g')
|
|
82
|
+
|
|
83
|
+
echo -e " ${GREEN}●${RESET} ${BOLD}${team_name}${RESET} ${DIM}members:${member_count}${RESET}"
|
|
84
|
+
if [[ -n "$member_names" ]]; then
|
|
85
|
+
echo -e " ${DIM}└─ ${member_names}${RESET}"
|
|
86
|
+
fi
|
|
87
|
+
else
|
|
88
|
+
# Directory exists but no config — possibly orphaned
|
|
89
|
+
echo -e " ${RED}●${RESET} ${BOLD}${team_name}${RESET} ${DIM}(no config — possibly orphaned)${RESET}"
|
|
90
|
+
fi
|
|
91
|
+
done < <(find "$TEAMS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if ! $HAS_TEAMS; then
|
|
95
|
+
echo -e " ${DIM}No team configs found.${RESET}"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# ─── 3. Task Lists ──────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
echo ""
|
|
101
|
+
echo -e "${PURPLE}${BOLD} TASK LISTS${RESET} ${DIM}~/.claude/tasks/${RESET}"
|
|
102
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
103
|
+
|
|
104
|
+
TASKS_DIR="${HOME}/.claude/tasks"
|
|
105
|
+
HAS_TASKS=false
|
|
106
|
+
|
|
107
|
+
if [[ -d "$TASKS_DIR" ]]; then
|
|
108
|
+
while IFS= read -r task_dir; do
|
|
109
|
+
[[ -z "$task_dir" ]] && continue
|
|
110
|
+
HAS_TASKS=true
|
|
111
|
+
task_team="$(basename "$task_dir")"
|
|
112
|
+
|
|
113
|
+
# Count tasks by status
|
|
114
|
+
total=0
|
|
115
|
+
completed=0
|
|
116
|
+
in_progress=0
|
|
117
|
+
pending=0
|
|
118
|
+
|
|
119
|
+
while IFS= read -r task_file; do
|
|
120
|
+
[[ -z "$task_file" ]] && continue
|
|
121
|
+
total=$((total + 1))
|
|
122
|
+
status=$(grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' "$task_file" 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
|
|
123
|
+
case "$status" in
|
|
124
|
+
completed) completed=$((completed + 1)) ;;
|
|
125
|
+
in_progress) in_progress=$((in_progress + 1)) ;;
|
|
126
|
+
pending) pending=$((pending + 1)) ;;
|
|
127
|
+
esac
|
|
128
|
+
done < <(find "$task_dir" -type f -name '*.json' 2>/dev/null)
|
|
129
|
+
|
|
130
|
+
# Build progress bar
|
|
131
|
+
if [[ $total -gt 0 ]]; then
|
|
132
|
+
pct=$((completed * 100 / total))
|
|
133
|
+
bar_width=20
|
|
134
|
+
filled=$((pct * bar_width / 100))
|
|
135
|
+
empty=$((bar_width - filled))
|
|
136
|
+
bar="${GREEN}"
|
|
137
|
+
for ((i=0; i<filled; i++)); do bar+="█"; done
|
|
138
|
+
bar+="${DIM}"
|
|
139
|
+
for ((i=0; i<empty; i++)); do bar+="░"; done
|
|
140
|
+
bar+="${RESET}"
|
|
141
|
+
|
|
142
|
+
echo -e " ${BLUE}●${RESET} ${BOLD}${task_team}${RESET} ${bar} ${pct}% ${DIM}(${completed}/${total} done)${RESET}"
|
|
143
|
+
|
|
144
|
+
# Show breakdown if there are active tasks
|
|
145
|
+
details=""
|
|
146
|
+
[[ $in_progress -gt 0 ]] && details+="${GREEN}${in_progress} active${RESET} "
|
|
147
|
+
[[ $pending -gt 0 ]] && details+="${YELLOW}${pending} pending${RESET} "
|
|
148
|
+
[[ $completed -gt 0 ]] && details+="${DIM}${completed} done${RESET}"
|
|
149
|
+
[[ -n "$details" ]] && echo -e " ${DIM}└─${RESET} ${details}"
|
|
150
|
+
else
|
|
151
|
+
echo -e " ${DIM}●${RESET} ${BOLD}${task_team}${RESET} ${DIM}(no tasks)${RESET}"
|
|
152
|
+
fi
|
|
153
|
+
done < <(find "$TASKS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
if ! $HAS_TASKS; then
|
|
157
|
+
echo -e " ${DIM}No task lists found.${RESET}"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# ─── Footer ──────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
echo ""
|
|
163
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
164
|
+
if $HAS_CLAUDE_WINDOWS || $HAS_TEAMS || $HAS_TASKS; then
|
|
165
|
+
echo -e " ${DIM}Clean up:${RESET} ${CYAN}shipwright cleanup${RESET} ${DIM}|${RESET} ${DIM}New session:${RESET} ${CYAN}shipwright session <name>${RESET}"
|
|
166
|
+
else
|
|
167
|
+
echo -e " ${DIM}No active teams. Start one:${RESET} ${CYAN}shipwright session <name>${RESET}"
|
|
168
|
+
fi
|
|
169
|
+
echo ""
|