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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +926 -0
  3. package/claude-code/CLAUDE.md.shipwright +125 -0
  4. package/claude-code/hooks/notify-idle.sh +35 -0
  5. package/claude-code/hooks/pre-compact-save.sh +57 -0
  6. package/claude-code/hooks/task-completed.sh +170 -0
  7. package/claude-code/hooks/teammate-idle.sh +68 -0
  8. package/claude-code/settings.json.template +184 -0
  9. package/completions/_shipwright +140 -0
  10. package/completions/shipwright.bash +89 -0
  11. package/completions/shipwright.fish +107 -0
  12. package/docs/KNOWN-ISSUES.md +199 -0
  13. package/docs/TIPS.md +331 -0
  14. package/docs/definition-of-done.example.md +16 -0
  15. package/docs/patterns/README.md +139 -0
  16. package/docs/patterns/audit-loop.md +149 -0
  17. package/docs/patterns/bug-hunt.md +183 -0
  18. package/docs/patterns/feature-implementation.md +159 -0
  19. package/docs/patterns/refactoring.md +183 -0
  20. package/docs/patterns/research-exploration.md +144 -0
  21. package/docs/patterns/test-generation.md +173 -0
  22. package/package.json +49 -0
  23. package/scripts/adapters/docker-deploy.sh +50 -0
  24. package/scripts/adapters/fly-deploy.sh +41 -0
  25. package/scripts/adapters/iterm2-adapter.sh +122 -0
  26. package/scripts/adapters/railway-deploy.sh +34 -0
  27. package/scripts/adapters/tmux-adapter.sh +87 -0
  28. package/scripts/adapters/vercel-deploy.sh +35 -0
  29. package/scripts/adapters/wezterm-adapter.sh +103 -0
  30. package/scripts/cct +242 -0
  31. package/scripts/cct-cleanup.sh +172 -0
  32. package/scripts/cct-cost.sh +590 -0
  33. package/scripts/cct-daemon.sh +3189 -0
  34. package/scripts/cct-doctor.sh +328 -0
  35. package/scripts/cct-fix.sh +478 -0
  36. package/scripts/cct-fleet.sh +904 -0
  37. package/scripts/cct-init.sh +282 -0
  38. package/scripts/cct-logs.sh +273 -0
  39. package/scripts/cct-loop.sh +1332 -0
  40. package/scripts/cct-memory.sh +1148 -0
  41. package/scripts/cct-pipeline.sh +3844 -0
  42. package/scripts/cct-prep.sh +1352 -0
  43. package/scripts/cct-ps.sh +168 -0
  44. package/scripts/cct-reaper.sh +390 -0
  45. package/scripts/cct-session.sh +284 -0
  46. package/scripts/cct-status.sh +169 -0
  47. package/scripts/cct-templates.sh +242 -0
  48. package/scripts/cct-upgrade.sh +422 -0
  49. package/scripts/cct-worktree.sh +405 -0
  50. package/scripts/postinstall.mjs +96 -0
  51. package/templates/pipelines/autonomous.json +71 -0
  52. package/templates/pipelines/cost-aware.json +95 -0
  53. package/templates/pipelines/deployed.json +79 -0
  54. package/templates/pipelines/enterprise.json +114 -0
  55. package/templates/pipelines/fast.json +63 -0
  56. package/templates/pipelines/full.json +104 -0
  57. package/templates/pipelines/hotfix.json +63 -0
  58. package/templates/pipelines/standard.json +91 -0
  59. package/tmux/claude-teams-overlay.conf +109 -0
  60. package/tmux/templates/architecture.json +19 -0
  61. package/tmux/templates/bug-fix.json +24 -0
  62. package/tmux/templates/code-review.json +24 -0
  63. package/tmux/templates/devops.json +19 -0
  64. package/tmux/templates/documentation.json +19 -0
  65. package/tmux/templates/exploration.json +19 -0
  66. package/tmux/templates/feature-dev.json +24 -0
  67. package/tmux/templates/full-stack.json +24 -0
  68. package/tmux/templates/migration.json +24 -0
  69. package/tmux/templates/refactor.json +19 -0
  70. package/tmux/templates/security-audit.json +24 -0
  71. package/tmux/templates/testing.json +24 -0
  72. package/tmux/tmux.conf +167 -0
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright init — One-command tmux setup + optional deploy configuration ║
4
+ # ║ ║
5
+ # ║ Installs tmux config, overlay, and templates. No interactive prompts, ║
6
+ # ║ no hooks, no Claude Code settings — just tmux config. ║
7
+ # ║ ║
8
+ # ║ --deploy Detect platform and generate deployed.json template ║
9
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
10
+ set -euo pipefail
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
14
+ ADAPTERS_DIR="$SCRIPT_DIR/adapters"
15
+
16
+ # ─── Colors ──────────────────────────────────────────────────────────────────
17
+ CYAN='\033[38;2;0;212;255m'
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
+ # ─── Flag parsing ───────────────────────────────────────────────────────────
31
+ DEPLOY_SETUP=false
32
+ DEPLOY_PLATFORM=""
33
+ SKIP_CLAUDE_MD=false
34
+
35
+ while [[ $# -gt 0 ]]; do
36
+ case "$1" in
37
+ --deploy)
38
+ DEPLOY_SETUP=true
39
+ shift
40
+ ;;
41
+ --platform)
42
+ DEPLOY_PLATFORM="${2:-}"
43
+ [[ -z "$DEPLOY_PLATFORM" ]] && { error "Missing value for --platform"; exit 1; }
44
+ shift 2
45
+ ;;
46
+ --no-claude-md)
47
+ SKIP_CLAUDE_MD=true
48
+ shift
49
+ ;;
50
+ --help|-h)
51
+ echo "Usage: shipwright init [--deploy] [--platform vercel|fly|railway|docker] [--no-claude-md]"
52
+ echo ""
53
+ echo "Options:"
54
+ echo " --deploy Detect deploy platform and generate deployed.json"
55
+ echo " --platform PLATFORM Skip detection, use specified platform"
56
+ echo " --no-claude-md Skip creating .claude/CLAUDE.md"
57
+ echo " --help, -h Show this help"
58
+ exit 0
59
+ ;;
60
+ *)
61
+ warn "Unknown option: $1"
62
+ shift
63
+ ;;
64
+ esac
65
+ done
66
+
67
+ echo ""
68
+ echo -e "${CYAN}${BOLD}shipwright init${RESET} — Quick tmux setup"
69
+ echo -e "${DIM}══════════════════════════════════════════${RESET}"
70
+ echo ""
71
+
72
+ # ─── tmux.conf ────────────────────────────────────────────────────────────────
73
+ if [[ -f "$HOME/.tmux.conf" ]]; then
74
+ cp "$HOME/.tmux.conf" "$HOME/.tmux.conf.bak"
75
+ warn "Backed up existing ~/.tmux.conf → ~/.tmux.conf.bak"
76
+ fi
77
+ cp "$REPO_DIR/tmux/tmux.conf" "$HOME/.tmux.conf"
78
+ success "Installed ~/.tmux.conf"
79
+
80
+ # ─── Overlay ──────────────────────────────────────────────────────────────────
81
+ mkdir -p "$HOME/.tmux"
82
+ cp "$REPO_DIR/tmux/claude-teams-overlay.conf" "$HOME/.tmux/claude-teams-overlay.conf"
83
+ success "Installed ~/.tmux/claude-teams-overlay.conf"
84
+
85
+ # ─── Templates ────────────────────────────────────────────────────────────────
86
+ mkdir -p "$HOME/.claude-teams/templates"
87
+ for tpl in "$REPO_DIR"/tmux/templates/*.json; do
88
+ [[ -f "$tpl" ]] || continue
89
+ cp "$tpl" "$HOME/.claude-teams/templates/$(basename "$tpl")"
90
+ done
91
+ success "Installed templates → ~/.claude-teams/templates/"
92
+
93
+ # ─── CLAUDE.md — Agent instructions ──────────────────────────────────────────
94
+ CLAUDE_MD_SRC="$REPO_DIR/claude-code/CLAUDE.md.shipwright"
95
+ CLAUDE_MD_DST=".claude/CLAUDE.md"
96
+
97
+ if [[ "$SKIP_CLAUDE_MD" == "false" && -f "$CLAUDE_MD_SRC" ]]; then
98
+ if [[ -f "$CLAUDE_MD_DST" ]]; then
99
+ # Check if it already contains Shipwright instructions
100
+ if grep -q "Shipwright" "$CLAUDE_MD_DST" 2>/dev/null; then
101
+ info "CLAUDE.md already contains Shipwright instructions — skipping"
102
+ else
103
+ # Append Shipwright section to existing CLAUDE.md
104
+ {
105
+ echo ""
106
+ echo "---"
107
+ echo ""
108
+ cat "$CLAUDE_MD_SRC"
109
+ } >> "$CLAUDE_MD_DST"
110
+ success "Appended Shipwright instructions to ${CLAUDE_MD_DST}"
111
+ fi
112
+ else
113
+ mkdir -p ".claude"
114
+ cp "$CLAUDE_MD_SRC" "$CLAUDE_MD_DST"
115
+ success "Created ${CLAUDE_MD_DST} with Shipwright agent instructions"
116
+ fi
117
+ fi
118
+
119
+ # ─── Reload tmux if inside a session ──────────────────────────────────────────
120
+ if [[ -n "${TMUX:-}" ]]; then
121
+ tmux source-file "$HOME/.tmux.conf" 2>/dev/null && \
122
+ success "Reloaded tmux config" || \
123
+ warn "Could not reload tmux config (reload manually with prefix + r)"
124
+ fi
125
+
126
+ # ─── Quick-start instructions ─────────────────────────────────────────────────
127
+ echo ""
128
+ echo -e "${BOLD}Done!${RESET} tmux is configured for Claude Code Teams."
129
+ echo ""
130
+ echo -e "${BOLD}Quick start:${RESET}"
131
+ if [[ -z "${TMUX:-}" ]]; then
132
+ echo -e " ${DIM}1.${RESET} tmux new -s dev"
133
+ echo -e " ${DIM}2.${RESET} shipwright session my-feature --template feature-dev"
134
+ else
135
+ echo -e " ${DIM}1.${RESET} shipwright session my-feature --template feature-dev"
136
+ fi
137
+ echo ""
138
+ echo -e "${BOLD}Layout keybindings:${RESET}"
139
+ echo -e " ${CYAN}prefix + M-1${RESET} main-horizontal (leader 65% left)"
140
+ echo -e " ${CYAN}prefix + M-2${RESET} main-vertical (leader 60% top)"
141
+ echo -e " ${CYAN}prefix + M-3${RESET} tiled (equal sizes)"
142
+ echo ""
143
+
144
+ # ─── Deploy setup (--deploy) ─────────────────────────────────────────────────
145
+ [[ "$DEPLOY_SETUP" == "false" ]] && exit 0
146
+
147
+ echo -e "${CYAN}${BOLD}Deploy Setup${RESET}"
148
+ echo -e "${DIM}══════════════════════════════════════════${RESET}"
149
+ echo ""
150
+
151
+ # Platform detection
152
+ detect_deploy_platform() {
153
+ local detected=""
154
+
155
+ for adapter_file in "$ADAPTERS_DIR"/*-deploy.sh; do
156
+ [[ -f "$adapter_file" ]] || continue
157
+ # Source the adapter in a subshell to get detection
158
+ if ( source "$adapter_file" && detect_platform ); then
159
+ local name
160
+ name=$(basename "$adapter_file" | sed 's/-deploy\.sh$//')
161
+ if [[ -n "$detected" ]]; then
162
+ detected="$detected $name"
163
+ else
164
+ detected="$name"
165
+ fi
166
+ fi
167
+ done
168
+
169
+ echo "$detected"
170
+ }
171
+
172
+ if [[ -n "$DEPLOY_PLATFORM" ]]; then
173
+ # User specified --platform, validate it
174
+ if [[ ! -f "$ADAPTERS_DIR/${DEPLOY_PLATFORM}-deploy.sh" ]]; then
175
+ error "Unknown platform: $DEPLOY_PLATFORM"
176
+ echo -e " Available: vercel, fly, railway, docker"
177
+ exit 1
178
+ fi
179
+ info "Using specified platform: ${BOLD}${DEPLOY_PLATFORM}${RESET}"
180
+ else
181
+ info "Detecting deploy platform..."
182
+ detected=$(detect_deploy_platform)
183
+
184
+ if [[ -z "$detected" ]]; then
185
+ warn "No platform detected in current directory"
186
+ echo ""
187
+ echo -e " Supported platforms:"
188
+ echo -e " ${CYAN}vercel${RESET} — vercel.json or .vercel/"
189
+ echo -e " ${CYAN}fly${RESET} — fly.toml"
190
+ echo -e " ${CYAN}railway${RESET} — railway.toml or .railway/"
191
+ echo -e " ${CYAN}docker${RESET} — Dockerfile or docker-compose.yml"
192
+ echo ""
193
+ echo -e " Specify manually: ${DIM}shipwright init --deploy --platform vercel${RESET}"
194
+ exit 1
195
+ fi
196
+
197
+ # If multiple platforms detected, use the first and warn
198
+ platform_count=$(echo "$detected" | wc -w | tr -d ' ')
199
+ DEPLOY_PLATFORM=$(echo "$detected" | awk '{print $1}')
200
+
201
+ if [[ "$platform_count" -gt 1 ]]; then
202
+ warn "Multiple platforms detected: ${BOLD}${detected}${RESET}"
203
+ info "Using: ${BOLD}${DEPLOY_PLATFORM}${RESET}"
204
+ echo -e " ${DIM}Override with: shipwright init --deploy --platform <name>${RESET}"
205
+ echo ""
206
+ else
207
+ success "Detected platform: ${BOLD}${DEPLOY_PLATFORM}${RESET}"
208
+ fi
209
+
210
+ # Confirm with user
211
+ read -rp "$(echo -e "${CYAN}${BOLD}▸${RESET} Configure deploy for ${BOLD}${DEPLOY_PLATFORM}${RESET}? [Y/n] ")" confirm
212
+ if [[ "${confirm,,}" == "n" ]]; then
213
+ info "Aborted. Use --platform to specify manually."
214
+ exit 0
215
+ fi
216
+ fi
217
+
218
+ # Source the adapter to get command values
219
+ ADAPTER_FILE="$ADAPTERS_DIR/${DEPLOY_PLATFORM}-deploy.sh"
220
+ source "$ADAPTER_FILE"
221
+
222
+ staging_cmd=$(get_staging_cmd)
223
+ production_cmd=$(get_production_cmd)
224
+ rollback_cmd=$(get_rollback_cmd)
225
+ health_url=$(get_health_url)
226
+ smoke_cmd=$(get_smoke_cmd)
227
+
228
+ # Generate deployed.json from template
229
+ TEMPLATE_SRC="$REPO_DIR/templates/pipelines/deployed.json"
230
+ TEMPLATE_DST=".claude/pipeline-templates/deployed.json"
231
+
232
+ if [[ ! -f "$TEMPLATE_SRC" ]]; then
233
+ error "Template not found: $TEMPLATE_SRC"
234
+ exit 1
235
+ fi
236
+
237
+ mkdir -p ".claude/pipeline-templates"
238
+
239
+ # Use jq to properly fill in the template values
240
+ jq --arg staging "$staging_cmd" \
241
+ --arg production "$production_cmd" \
242
+ --arg rollback "$rollback_cmd" \
243
+ --arg health "$health_url" \
244
+ --arg smoke "$smoke_cmd" \
245
+ --arg platform "$DEPLOY_PLATFORM" \
246
+ '
247
+ .name = "deployed-" + $platform |
248
+ .description = "Autonomous pipeline with " + $platform + " deploy — generated by shipwright init --deploy" |
249
+ (.stages[] | select(.id == "deploy") | .config) |= {
250
+ staging_cmd: $staging,
251
+ production_cmd: $production,
252
+ rollback_cmd: $rollback
253
+ } |
254
+ (.stages[] | select(.id == "validate") | .config) |= {
255
+ smoke_cmd: $smoke,
256
+ health_url: $health,
257
+ close_issue: true
258
+ } |
259
+ (.stages[] | select(.id == "monitor") | .config) |= (
260
+ .health_url = $health |
261
+ .rollback_cmd = $rollback
262
+ )
263
+ ' "$TEMPLATE_SRC" > "$TEMPLATE_DST"
264
+
265
+ success "Generated ${BOLD}${TEMPLATE_DST}${RESET}"
266
+
267
+ echo ""
268
+ echo -e "${BOLD}Deploy configured for ${DEPLOY_PLATFORM}!${RESET}"
269
+ echo ""
270
+ echo -e "${BOLD}Commands configured:${RESET}"
271
+ echo -e " ${DIM}staging:${RESET} $staging_cmd"
272
+ echo -e " ${DIM}production:${RESET} $production_cmd"
273
+ echo -e " ${DIM}rollback:${RESET} $rollback_cmd"
274
+ if [[ -n "$health_url" ]]; then
275
+ echo -e " ${DIM}health:${RESET} $health_url"
276
+ fi
277
+ echo ""
278
+ echo -e "${BOLD}Usage:${RESET}"
279
+ echo -e " ${DIM}shipwright pipeline start --issue 42 --template .claude/pipeline-templates/deployed.json${RESET}"
280
+ echo ""
281
+ echo -e "${DIM}Edit ${TEMPLATE_DST} to customize deploy commands, gates, or thresholds.${RESET}"
282
+ echo ""
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ cct-logs.sh — View and search agent pane logs ║
4
+ # ║ ║
5
+ # ║ Captures tmux pane scrollback and provides log browsing/search. ║
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
+ # ─── Helpers ─────────────────────────────────────────────────────────────────
21
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
22
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
23
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
24
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
25
+
26
+ LOGS_DIR="$HOME/.claude-teams/logs"
27
+
28
+ show_usage() {
29
+ echo -e "${CYAN}${BOLD}shipwright logs${RESET} — View agent pane logs"
30
+ echo ""
31
+ echo -e "${BOLD}USAGE${RESET}"
32
+ echo -e " ${CYAN}shipwright logs${RESET} List available log directories"
33
+ echo -e " ${CYAN}shipwright logs${RESET} <team> Show logs for a team (captures live)"
34
+ echo -e " ${CYAN}shipwright logs${RESET} <team> ${DIM}--pane <agent>${RESET} Show specific agent's log"
35
+ echo -e " ${CYAN}shipwright logs${RESET} <team> ${DIM}--follow${RESET} Tail logs in real-time"
36
+ echo -e " ${CYAN}shipwright logs${RESET} <team> ${DIM}--grep <pattern>${RESET} Search logs for a pattern"
37
+ echo -e " ${CYAN}shipwright logs${RESET} ${DIM}--capture${RESET} Capture all team pane scrollback now"
38
+ echo ""
39
+ echo -e "${BOLD}OPTIONS${RESET}"
40
+ echo -e " ${DIM}--pane <name>${RESET} Filter to a specific agent pane by title"
41
+ echo -e " ${DIM}--follow, -f${RESET} Tail the most recent log file"
42
+ echo -e " ${DIM}--grep <pat>${RESET} Search across log files with a pattern"
43
+ echo -e " ${DIM}--capture${RESET} Capture current scrollback from all team panes"
44
+ echo ""
45
+ }
46
+
47
+ # ─── Capture scrollback from all claude-* windows ────────────────────────────
48
+ capture_logs() {
49
+ local timestamp
50
+ timestamp="$(date +%Y%m%d-%H%M%S)"
51
+ local captured=0
52
+
53
+ while IFS='|' read -r session_window window_name pane_id pane_title; do
54
+ [[ -z "$window_name" ]] && continue
55
+ echo "$window_name" | grep -qi "claude" || continue
56
+
57
+ # Sanitize names for filesystem
58
+ local safe_window safe_title
59
+ safe_window="$(echo "$window_name" | tr '/' '-')"
60
+ safe_title="$(echo "${pane_title:-pane-$pane_id}" | tr '/' '-')"
61
+
62
+ local log_dir="${LOGS_DIR}/${safe_window}"
63
+ mkdir -p "$log_dir"
64
+
65
+ local log_file="${log_dir}/${safe_title}-${timestamp}.log"
66
+ tmux capture-pane -t "$pane_id" -pS - > "$log_file" 2>/dev/null || continue
67
+ captured=$((captured + 1))
68
+ done < <(tmux list-panes -a -F '#{session_name}:#{window_index}|#{window_name}|#{pane_id}|#{pane_title}' 2>/dev/null || true)
69
+
70
+ if [[ $captured -gt 0 ]]; then
71
+ success "Captured ${captured} pane(s) to ${LOGS_DIR}/"
72
+ else
73
+ warn "No Claude team panes found to capture"
74
+ fi
75
+ }
76
+
77
+ # ─── List available logs ────────────────────────────────────────────────────
78
+ list_logs() {
79
+ echo ""
80
+ echo -e "${CYAN}${BOLD} Agent Logs${RESET}"
81
+ echo -e "${DIM} ══════════════════════════════════════════${RESET}"
82
+ echo ""
83
+
84
+ if [[ ! -d "$LOGS_DIR" ]]; then
85
+ echo -e " ${DIM}No logs directory yet.${RESET}"
86
+ echo -e " ${DIM}Capture logs with: ${CYAN}shipwright logs --capture${RESET}"
87
+ echo ""
88
+ return
89
+ fi
90
+
91
+ local has_logs=false
92
+ while IFS= read -r team_dir; do
93
+ [[ -z "$team_dir" ]] && continue
94
+ has_logs=true
95
+ local team_name
96
+ team_name="$(basename "$team_dir")"
97
+ local file_count
98
+ file_count="$(find "$team_dir" -name '*.log' -type f 2>/dev/null | wc -l | tr -d ' ')"
99
+ local latest=""
100
+ latest="$(find "$team_dir" -name '*.log' -type f -print0 2>/dev/null | xargs -0 ls -t 2>/dev/null | head -1)"
101
+ local latest_time=""
102
+ if [[ -n "$latest" ]]; then
103
+ latest_time="$(stat -f '%Sm' -t '%Y-%m-%d %H:%M' "$latest" 2>/dev/null || stat --format='%y' "$latest" 2>/dev/null | cut -d. -f1)"
104
+ fi
105
+
106
+ echo -e " ${BLUE}●${RESET} ${BOLD}${team_name}${RESET} ${DIM}${file_count} logs${RESET}"
107
+ if [[ -n "$latest_time" ]]; then
108
+ echo -e " ${DIM}└─ latest: ${latest_time}${RESET}"
109
+ fi
110
+ done < <(find "$LOGS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
111
+
112
+ if ! $has_logs; then
113
+ echo -e " ${DIM}No log directories found.${RESET}"
114
+ echo -e " ${DIM}Capture logs with: ${CYAN}shipwright logs --capture${RESET}"
115
+ fi
116
+ echo ""
117
+ }
118
+
119
+ # ─── Show logs for a team ────────────────────────────────────────────────────
120
+ show_team_logs() {
121
+ local team="$1"
122
+ local pane_filter="${2:-}"
123
+ local grep_pattern="${3:-}"
124
+ local follow="${4:-false}"
125
+
126
+ # Try exact match first, then prefix match on claude-*
127
+ local team_dir="${LOGS_DIR}/${team}"
128
+ if [[ ! -d "$team_dir" ]]; then
129
+ team_dir="${LOGS_DIR}/claude-${team}"
130
+ fi
131
+
132
+ if [[ ! -d "$team_dir" ]]; then
133
+ # Capture live first if no logs exist
134
+ info "No saved logs for '${team}'. Capturing live scrollback..."
135
+ capture_logs
136
+
137
+ # Re-check
138
+ team_dir="${LOGS_DIR}/${team}"
139
+ [[ ! -d "$team_dir" ]] && team_dir="${LOGS_DIR}/claude-${team}"
140
+ if [[ ! -d "$team_dir" ]]; then
141
+ error "No team panes matching '${team}' found"
142
+ exit 1
143
+ fi
144
+ fi
145
+
146
+ local team_name
147
+ team_name="$(basename "$team_dir")"
148
+
149
+ echo ""
150
+ echo -e "${CYAN}${BOLD} Logs — ${team_name}${RESET}"
151
+ echo -e "${DIM} ══════════════════════════════════════════${RESET}"
152
+ echo ""
153
+
154
+ # Build file list, optionally filtered by pane
155
+ local log_files=()
156
+ while IFS= read -r f; do
157
+ [[ -z "$f" ]] && continue
158
+ if [[ -n "$pane_filter" ]]; then
159
+ local base
160
+ base="$(basename "$f")"
161
+ echo "$base" | grep -qi "$pane_filter" || continue
162
+ fi
163
+ log_files+=("$f")
164
+ done < <(find "$team_dir" -name '*.log' -type f -print0 2>/dev/null | xargs -0 ls -t 2>/dev/null)
165
+
166
+ if [[ ${#log_files[@]} -eq 0 ]]; then
167
+ if [[ -n "$pane_filter" ]]; then
168
+ warn "No logs matching pane '${pane_filter}' in ${team_name}"
169
+ else
170
+ warn "No log files in ${team_name}"
171
+ fi
172
+ return
173
+ fi
174
+
175
+ # --follow: tail the most recent file
176
+ if [[ "$follow" == "true" ]]; then
177
+ local latest="${log_files[0]}"
178
+ info "Tailing: $(basename "$latest")"
179
+ echo -e "${DIM} (Ctrl+C to stop)${RESET}"
180
+ echo ""
181
+ tail -f "$latest"
182
+ return
183
+ fi
184
+
185
+ # --grep: search across all files
186
+ if [[ -n "$grep_pattern" ]]; then
187
+ info "Searching for '${grep_pattern}' in ${#log_files[@]} log file(s)..."
188
+ echo ""
189
+ local found=false
190
+ for f in "${log_files[@]}"; do
191
+ local matches
192
+ matches="$(grep -n --color=always "$grep_pattern" "$f" 2>/dev/null || true)"
193
+ if [[ -n "$matches" ]]; then
194
+ found=true
195
+ echo -e " ${BLUE}──${RESET} ${BOLD}$(basename "$f")${RESET}"
196
+ echo "$matches" | while IFS= read -r line; do
197
+ echo -e " ${line}"
198
+ done
199
+ echo ""
200
+ fi
201
+ done
202
+ if ! $found; then
203
+ warn "No matches for '${grep_pattern}'"
204
+ fi
205
+ return
206
+ fi
207
+
208
+ # Default: list files then show the most recent
209
+ info "${#log_files[@]} log file(s):"
210
+ for f in "${log_files[@]}"; do
211
+ local size
212
+ size="$(wc -l < "$f" | tr -d ' ')"
213
+ echo -e " ${DIM}•${RESET} $(basename "$f") ${DIM}(${size} lines)${RESET}"
214
+ done
215
+
216
+ echo ""
217
+ local latest="${log_files[0]}"
218
+ info "Most recent: ${BOLD}$(basename "$latest")${RESET}"
219
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
220
+ cat "$latest"
221
+ }
222
+
223
+ # ─── Parse Arguments ─────────────────────────────────────────────────────────
224
+ TEAM=""
225
+ PANE=""
226
+ GREP_PATTERN=""
227
+ FOLLOW=false
228
+ DO_CAPTURE=false
229
+
230
+ while [[ $# -gt 0 ]]; do
231
+ case "$1" in
232
+ --help|-h)
233
+ show_usage
234
+ exit 0
235
+ ;;
236
+ --capture)
237
+ DO_CAPTURE=true
238
+ shift
239
+ ;;
240
+ --pane)
241
+ PANE="${2:-}"
242
+ [[ -z "$PANE" ]] && { error "--pane requires an agent name"; exit 1; }
243
+ shift 2
244
+ ;;
245
+ --follow|-f)
246
+ FOLLOW=true
247
+ shift
248
+ ;;
249
+ --grep)
250
+ GREP_PATTERN="${2:-}"
251
+ [[ -z "$GREP_PATTERN" ]] && { error "--grep requires a pattern"; exit 1; }
252
+ shift 2
253
+ ;;
254
+ -*)
255
+ error "Unknown option: $1"
256
+ show_usage
257
+ exit 1
258
+ ;;
259
+ *)
260
+ TEAM="$1"
261
+ shift
262
+ ;;
263
+ esac
264
+ done
265
+
266
+ # ─── Dispatch ────────────────────────────────────────────────────────────────
267
+ if $DO_CAPTURE; then
268
+ capture_logs
269
+ elif [[ -z "$TEAM" ]]; then
270
+ list_logs
271
+ else
272
+ show_team_logs "$TEAM" "$PANE" "$GREP_PATTERN" "$FOLLOW"
273
+ fi