shipwright-cli 1.7.1 → 1.9.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/agents/code-reviewer.md +90 -0
- package/.claude/agents/devops-engineer.md +142 -0
- package/.claude/agents/pipeline-agent.md +80 -0
- package/.claude/agents/shell-script-specialist.md +150 -0
- package/.claude/agents/test-specialist.md +196 -0
- package/.claude/hooks/post-tool-use.sh +38 -0
- package/.claude/hooks/pre-tool-use.sh +25 -0
- package/.claude/hooks/session-started.sh +37 -0
- package/README.md +212 -814
- package/claude-code/CLAUDE.md.shipwright +54 -0
- package/claude-code/hooks/notify-idle.sh +2 -2
- package/claude-code/hooks/session-start.sh +24 -0
- package/claude-code/hooks/task-completed.sh +6 -2
- package/claude-code/settings.json.template +12 -0
- package/dashboard/public/app.js +4422 -0
- package/dashboard/public/index.html +816 -0
- package/dashboard/public/styles.css +4755 -0
- package/dashboard/server.ts +4315 -0
- package/docs/KNOWN-ISSUES.md +18 -10
- package/docs/TIPS.md +38 -26
- package/docs/patterns/README.md +33 -23
- package/package.json +9 -5
- package/scripts/adapters/iterm2-adapter.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +52 -23
- package/scripts/adapters/wezterm-adapter.sh +26 -14
- package/scripts/lib/compat.sh +200 -0
- package/scripts/lib/helpers.sh +72 -0
- package/scripts/postinstall.mjs +72 -13
- package/scripts/{cct → sw} +109 -21
- package/scripts/sw-adversarial.sh +274 -0
- package/scripts/sw-architecture-enforcer.sh +330 -0
- package/scripts/sw-checkpoint.sh +390 -0
- package/scripts/{cct-cleanup.sh → sw-cleanup.sh} +3 -1
- package/scripts/sw-connect.sh +619 -0
- package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
- package/scripts/{cct-daemon.sh → sw-daemon.sh} +2217 -204
- package/scripts/sw-dashboard.sh +477 -0
- package/scripts/sw-developer-simulation.sh +252 -0
- package/scripts/sw-docs.sh +635 -0
- package/scripts/sw-doctor.sh +907 -0
- package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
- package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
- package/scripts/sw-github-checks.sh +521 -0
- package/scripts/sw-github-deploy.sh +533 -0
- package/scripts/sw-github-graphql.sh +972 -0
- package/scripts/sw-heartbeat.sh +293 -0
- package/scripts/{cct-init.sh → sw-init.sh} +144 -11
- package/scripts/sw-intelligence.sh +1196 -0
- package/scripts/sw-jira.sh +643 -0
- package/scripts/sw-launchd.sh +364 -0
- package/scripts/sw-linear.sh +648 -0
- package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
- package/scripts/{cct-loop.sh → sw-loop.sh} +534 -44
- package/scripts/{cct-memory.sh → sw-memory.sh} +321 -38
- package/scripts/sw-patrol-meta.sh +417 -0
- package/scripts/sw-pipeline-composer.sh +455 -0
- package/scripts/{cct-pipeline.sh → sw-pipeline.sh} +2319 -178
- package/scripts/sw-predictive.sh +820 -0
- package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
- package/scripts/{cct-ps.sh → sw-ps.sh} +6 -4
- package/scripts/{cct-reaper.sh → sw-reaper.sh} +6 -4
- package/scripts/sw-remote.sh +687 -0
- package/scripts/sw-self-optimize.sh +947 -0
- package/scripts/sw-session.sh +519 -0
- package/scripts/sw-setup.sh +234 -0
- package/scripts/sw-status.sh +605 -0
- package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
- package/scripts/sw-tmux.sh +591 -0
- package/scripts/sw-tracker-jira.sh +277 -0
- package/scripts/sw-tracker-linear.sh +292 -0
- package/scripts/sw-tracker.sh +409 -0
- package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
- package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
- package/templates/pipelines/autonomous.json +27 -5
- package/templates/pipelines/full.json +12 -0
- package/templates/pipelines/standard.json +12 -0
- package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
- package/tmux/templates/accessibility.json +34 -0
- package/tmux/templates/api-design.json +35 -0
- package/tmux/templates/architecture.json +1 -0
- package/tmux/templates/bug-fix.json +9 -0
- package/tmux/templates/code-review.json +1 -0
- package/tmux/templates/compliance.json +36 -0
- package/tmux/templates/data-pipeline.json +36 -0
- package/tmux/templates/debt-paydown.json +34 -0
- package/tmux/templates/devops.json +1 -0
- package/tmux/templates/documentation.json +1 -0
- package/tmux/templates/exploration.json +1 -0
- package/tmux/templates/feature-dev.json +1 -0
- package/tmux/templates/full-stack.json +8 -0
- package/tmux/templates/i18n.json +34 -0
- package/tmux/templates/incident-response.json +36 -0
- package/tmux/templates/migration.json +1 -0
- package/tmux/templates/observability.json +35 -0
- package/tmux/templates/onboarding.json +33 -0
- package/tmux/templates/performance.json +35 -0
- package/tmux/templates/refactor.json +1 -0
- package/tmux/templates/release.json +35 -0
- package/tmux/templates/security-audit.json +8 -0
- package/tmux/templates/spike.json +34 -0
- package/tmux/templates/testing.json +1 -0
- package/tmux/tmux.conf +98 -9
- package/scripts/cct-doctor.sh +0 -414
- package/scripts/cct-session.sh +0 -284
- package/scripts/cct-status.sh +0 -169
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ sw-doctor.sh — Validate Shipwright setup ║
|
|
4
|
+
# ║ ║
|
|
5
|
+
# ║ Checks prerequisites, installed files, PATH, and common issues. ║
|
|
6
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
7
|
+
VERSION="1.9.0"
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
10
|
+
|
|
11
|
+
# ─── Colors ──────────────────────────────────────────────────────────────────
|
|
12
|
+
CYAN='\033[38;2;0;212;255m'
|
|
13
|
+
PURPLE='\033[38;2;124;58;237m'
|
|
14
|
+
BLUE='\033[38;2;0;102;255m'
|
|
15
|
+
GREEN='\033[38;2;74;222;128m'
|
|
16
|
+
YELLOW='\033[38;2;250;204;21m'
|
|
17
|
+
RED='\033[38;2;248;113;113m'
|
|
18
|
+
DIM='\033[2m'
|
|
19
|
+
BOLD='\033[1m'
|
|
20
|
+
RESET='\033[0m'
|
|
21
|
+
|
|
22
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
23
|
+
_COMPAT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/compat.sh"
|
|
24
|
+
# shellcheck source=lib/compat.sh
|
|
25
|
+
[[ -f "$_COMPAT" ]] && source "$_COMPAT"
|
|
26
|
+
|
|
27
|
+
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
28
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
29
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
30
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
31
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*"; }
|
|
32
|
+
|
|
33
|
+
PASS=0
|
|
34
|
+
WARN=0
|
|
35
|
+
FAIL=0
|
|
36
|
+
|
|
37
|
+
check_pass() { success "$*"; PASS=$((PASS + 1)); }
|
|
38
|
+
check_warn() { warn "$*"; WARN=$((WARN + 1)); }
|
|
39
|
+
check_fail() { error "$*"; FAIL=$((FAIL + 1)); }
|
|
40
|
+
|
|
41
|
+
# ─── Header ─────────────────────────────────────────────────────────────────
|
|
42
|
+
echo ""
|
|
43
|
+
echo -e "${CYAN}${BOLD} Shipwright — Doctor${RESET}"
|
|
44
|
+
echo -e "${DIM} $(date '+%Y-%m-%d %H:%M:%S')${RESET}"
|
|
45
|
+
echo -e "${DIM} ══════════════════════════════════════════${RESET}"
|
|
46
|
+
echo ""
|
|
47
|
+
|
|
48
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
49
|
+
# 1. Prerequisites
|
|
50
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
51
|
+
echo -e "${PURPLE}${BOLD} PREREQUISITES${RESET}"
|
|
52
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
53
|
+
|
|
54
|
+
# tmux
|
|
55
|
+
if command -v tmux &>/dev/null; then
|
|
56
|
+
TMUX_VERSION="$(tmux -V | grep -oE '[0-9]+\.[0-9a-z]+')"
|
|
57
|
+
TMUX_MAJOR="$(echo "$TMUX_VERSION" | cut -d. -f1)"
|
|
58
|
+
TMUX_MINOR="$(echo "$TMUX_VERSION" | cut -d. -f2 | tr -dc '0-9')"
|
|
59
|
+
if [[ "$TMUX_MAJOR" -ge 3 && "$TMUX_MINOR" -ge 3 ]] || [[ "$TMUX_MAJOR" -ge 4 ]]; then
|
|
60
|
+
check_pass "tmux ${TMUX_VERSION} (all features: passthrough, popups, extended-keys)"
|
|
61
|
+
elif [[ "$TMUX_MAJOR" -ge 3 && "$TMUX_MINOR" -ge 2 ]]; then
|
|
62
|
+
check_warn "tmux ${TMUX_VERSION} — 3.3+ recommended for allow-passthrough"
|
|
63
|
+
else
|
|
64
|
+
check_warn "tmux ${TMUX_VERSION} — 3.2+ required, 3.3+ recommended"
|
|
65
|
+
fi
|
|
66
|
+
else
|
|
67
|
+
check_fail "tmux not installed"
|
|
68
|
+
echo -e " ${DIM}brew install tmux (macOS)${RESET}"
|
|
69
|
+
echo -e " ${DIM}sudo apt install tmux (Ubuntu/Debian)${RESET}"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# TPM (Tmux Plugin Manager)
|
|
73
|
+
if [[ -d "$HOME/.tmux/plugins/tpm" ]]; then
|
|
74
|
+
check_pass "TPM installed"
|
|
75
|
+
else
|
|
76
|
+
check_warn "TPM not installed — run: shipwright tmux install"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# jq
|
|
80
|
+
if command -v jq &>/dev/null; then
|
|
81
|
+
check_pass "jq $(jq --version 2>&1 | tr -d 'jq-')"
|
|
82
|
+
else
|
|
83
|
+
check_fail "jq not installed — required for template parsing"
|
|
84
|
+
echo -e " ${DIM}brew install jq${RESET} (macOS)"
|
|
85
|
+
echo -e " ${DIM}sudo apt install jq${RESET} (Ubuntu/Debian)"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Claude Code CLI
|
|
89
|
+
if command -v claude &>/dev/null; then
|
|
90
|
+
check_pass "Claude Code CLI found"
|
|
91
|
+
else
|
|
92
|
+
check_fail "Claude Code CLI not found"
|
|
93
|
+
echo -e " ${DIM}npm install -g @anthropic-ai/claude-code${RESET}"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Node.js
|
|
97
|
+
if command -v node &>/dev/null; then
|
|
98
|
+
NODE_VERSION="$(node -v | tr -d 'v')"
|
|
99
|
+
NODE_MAJOR="$(echo "$NODE_VERSION" | cut -d. -f1)"
|
|
100
|
+
if [[ "$NODE_MAJOR" -ge 20 ]]; then
|
|
101
|
+
check_pass "Node.js ${NODE_VERSION}"
|
|
102
|
+
else
|
|
103
|
+
check_warn "Node.js ${NODE_VERSION} — 20+ recommended"
|
|
104
|
+
fi
|
|
105
|
+
else
|
|
106
|
+
check_fail "Node.js not found"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Git
|
|
110
|
+
if command -v git &>/dev/null; then
|
|
111
|
+
check_pass "git $(git --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')"
|
|
112
|
+
else
|
|
113
|
+
check_fail "git not found"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Bash version
|
|
117
|
+
BASH_MAJOR="${BASH_VERSINFO[0]:-0}"
|
|
118
|
+
BASH_MINOR="${BASH_VERSINFO[1]:-0}"
|
|
119
|
+
if [[ "$BASH_MAJOR" -ge 5 ]]; then
|
|
120
|
+
check_pass "bash ${BASH_VERSION}"
|
|
121
|
+
elif [[ "$BASH_MAJOR" -ge 4 ]]; then
|
|
122
|
+
check_pass "bash ${BASH_VERSION}"
|
|
123
|
+
else
|
|
124
|
+
check_warn "bash ${BASH_VERSION} — 4.0+ required for associative arrays"
|
|
125
|
+
echo -e " ${DIM}brew install bash (macOS ships 3.2)${RESET}"
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
129
|
+
# 2. Installed Files
|
|
130
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
131
|
+
echo ""
|
|
132
|
+
echo -e "${PURPLE}${BOLD} INSTALLED FILES${RESET}"
|
|
133
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
134
|
+
|
|
135
|
+
# tmux overlay
|
|
136
|
+
if [[ -f "$HOME/.tmux/shipwright-overlay.conf" ]]; then
|
|
137
|
+
check_pass "Overlay: ~/.tmux/shipwright-overlay.conf"
|
|
138
|
+
else
|
|
139
|
+
check_fail "Overlay not found: ~/.tmux/shipwright-overlay.conf"
|
|
140
|
+
echo -e " ${DIM}Re-run install.sh to install it${RESET}"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# Overlay sourced in tmux.conf
|
|
144
|
+
if [[ -f "$HOME/.tmux.conf" ]]; then
|
|
145
|
+
if grep -q "shipwright-overlay" "$HOME/.tmux.conf" 2>/dev/null; then
|
|
146
|
+
check_pass "Overlay sourced in ~/.tmux.conf"
|
|
147
|
+
else
|
|
148
|
+
check_warn "Overlay not sourced in ~/.tmux.conf"
|
|
149
|
+
echo -e " ${DIM}Add: source-file -q ~/.tmux/shipwright-overlay.conf${RESET}"
|
|
150
|
+
fi
|
|
151
|
+
else
|
|
152
|
+
check_warn "No ~/.tmux.conf found"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# Claude settings
|
|
156
|
+
if [[ -f "$HOME/.claude/settings.json" ]]; then
|
|
157
|
+
check_pass "Settings: ~/.claude/settings.json"
|
|
158
|
+
else
|
|
159
|
+
check_warn "No ~/.claude/settings.json"
|
|
160
|
+
echo -e " ${DIM}Copy from settings.json.template${RESET}"
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# Hooks directory
|
|
164
|
+
HOOKS_DIR="$HOME/.claude/hooks"
|
|
165
|
+
if [[ -d "$HOOKS_DIR" ]]; then
|
|
166
|
+
hook_count=0
|
|
167
|
+
non_exec=0
|
|
168
|
+
while IFS= read -r hook; do
|
|
169
|
+
[[ -z "$hook" ]] && continue
|
|
170
|
+
hook_count=$((hook_count + 1))
|
|
171
|
+
if [[ ! -x "$hook" ]]; then
|
|
172
|
+
non_exec=$((non_exec + 1))
|
|
173
|
+
fi
|
|
174
|
+
done < <(find "$HOOKS_DIR" -maxdepth 1 -name '*.sh' -type f 2>/dev/null)
|
|
175
|
+
|
|
176
|
+
if [[ $hook_count -gt 0 && $non_exec -eq 0 ]]; then
|
|
177
|
+
check_pass "Hooks: ${hook_count} scripts, all executable"
|
|
178
|
+
elif [[ $hook_count -gt 0 && $non_exec -gt 0 ]]; then
|
|
179
|
+
check_warn "Hooks: ${non_exec}/${hook_count} scripts not executable"
|
|
180
|
+
echo -e " ${DIM}chmod +x ~/.claude/hooks/*.sh${RESET}"
|
|
181
|
+
else
|
|
182
|
+
check_warn "Hooks dir exists but no .sh scripts found"
|
|
183
|
+
fi
|
|
184
|
+
else
|
|
185
|
+
check_warn "No hooks directory at ~/.claude/hooks/"
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
# Hook wiring validation — check hooks are configured in settings.json
|
|
189
|
+
if [[ -d "$HOOKS_DIR" && -f "$HOME/.claude/settings.json" ]] && jq -e '.' "$HOME/.claude/settings.json" &>/dev/null; then
|
|
190
|
+
wired=0 unwired=0 hook_total_check=0
|
|
191
|
+
# Colon-separated pairs: filename:EventName (Bash 3.2 compatible)
|
|
192
|
+
for pair in \
|
|
193
|
+
"teammate-idle.sh:TeammateIdle" \
|
|
194
|
+
"task-completed.sh:TaskCompleted" \
|
|
195
|
+
"notify-idle.sh:Notification" \
|
|
196
|
+
"pre-compact-save.sh:PreCompact" \
|
|
197
|
+
"session-start.sh:SessionStart"; do
|
|
198
|
+
hfile="" hevent=""
|
|
199
|
+
IFS=':' read -r hfile hevent <<< "$pair"
|
|
200
|
+
# Only check hooks that are actually installed
|
|
201
|
+
[[ -f "$HOOKS_DIR/$hfile" ]] || continue
|
|
202
|
+
hook_total_check=$((hook_total_check + 1))
|
|
203
|
+
if jq -e ".hooks.${hevent}" "$HOME/.claude/settings.json" &>/dev/null; then
|
|
204
|
+
wired=$((wired + 1))
|
|
205
|
+
else
|
|
206
|
+
unwired=$((unwired + 1))
|
|
207
|
+
check_warn "Hook ${hfile} not wired to ${hevent} event in settings.json"
|
|
208
|
+
fi
|
|
209
|
+
done
|
|
210
|
+
if [[ $hook_total_check -gt 0 && $unwired -eq 0 ]]; then
|
|
211
|
+
check_pass "Hooks wired in settings.json: ${wired}/${hook_total_check}"
|
|
212
|
+
elif [[ $unwired -gt 0 ]]; then
|
|
213
|
+
echo -e " ${DIM}Run: shipwright init to wire hooks${RESET}"
|
|
214
|
+
fi
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
218
|
+
# 3. Agent Teams
|
|
219
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
220
|
+
echo ""
|
|
221
|
+
echo -e "${PURPLE}${BOLD} AGENT TEAMS${RESET}"
|
|
222
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
223
|
+
|
|
224
|
+
# Agent teams env var in settings.json
|
|
225
|
+
SETTINGS_FILE="$HOME/.claude/settings.json"
|
|
226
|
+
if [[ -f "$SETTINGS_FILE" ]]; then
|
|
227
|
+
if grep -q 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS' "$SETTINGS_FILE" 2>/dev/null; then
|
|
228
|
+
check_pass "Agent teams enabled in settings.json"
|
|
229
|
+
else
|
|
230
|
+
check_fail "Agent teams NOT enabled in settings.json"
|
|
231
|
+
echo -e " ${DIM}Run: shipwright init${RESET}"
|
|
232
|
+
echo -e " ${DIM}Or add to ~/.claude/settings.json:${RESET}"
|
|
233
|
+
echo -e " ${DIM}\"env\": { \"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS\": \"1\" }${RESET}"
|
|
234
|
+
fi
|
|
235
|
+
else
|
|
236
|
+
check_fail "No ~/.claude/settings.json — agent teams not configured"
|
|
237
|
+
echo -e " ${DIM}Run: shipwright init${RESET}"
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# CLAUDE.md with Shipwright instructions
|
|
241
|
+
GLOBAL_CLAUDE_MD="$HOME/.claude/CLAUDE.md"
|
|
242
|
+
if [[ -f "$GLOBAL_CLAUDE_MD" ]]; then
|
|
243
|
+
if grep -q "Shipwright" "$GLOBAL_CLAUDE_MD" 2>/dev/null; then
|
|
244
|
+
check_pass "CLAUDE.md contains Shipwright instructions"
|
|
245
|
+
else
|
|
246
|
+
check_warn "CLAUDE.md exists but missing Shipwright instructions"
|
|
247
|
+
echo -e " ${DIM}Run: shipwright init${RESET}"
|
|
248
|
+
fi
|
|
249
|
+
else
|
|
250
|
+
check_warn "No ~/.claude/CLAUDE.md — agents won't know Shipwright commands"
|
|
251
|
+
echo -e " ${DIM}Run: shipwright init${RESET}"
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
# Team templates
|
|
255
|
+
TEMPLATES_DIR="$HOME/.shipwright/templates"
|
|
256
|
+
if [[ -d "$TEMPLATES_DIR" ]]; then
|
|
257
|
+
tpl_count=0
|
|
258
|
+
while IFS= read -r f; do
|
|
259
|
+
[[ -n "$f" ]] && tpl_count=$((tpl_count + 1))
|
|
260
|
+
done < <(find "$TEMPLATES_DIR" -maxdepth 1 -name '*.json' -type f 2>/dev/null)
|
|
261
|
+
if [[ $tpl_count -gt 0 ]]; then
|
|
262
|
+
check_pass "Team templates: ${tpl_count} installed"
|
|
263
|
+
else
|
|
264
|
+
check_warn "Template dir exists but no .json files found"
|
|
265
|
+
fi
|
|
266
|
+
else
|
|
267
|
+
check_warn "No team templates at ~/.shipwright/templates/"
|
|
268
|
+
echo -e " ${DIM}Run: shipwright init${RESET}"
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
# Pipeline templates
|
|
272
|
+
PIPELINES_DIR="$HOME/.shipwright/pipelines"
|
|
273
|
+
if [[ -d "$PIPELINES_DIR" ]]; then
|
|
274
|
+
pip_count=0
|
|
275
|
+
while IFS= read -r f; do
|
|
276
|
+
[[ -n "$f" ]] && pip_count=$((pip_count + 1))
|
|
277
|
+
done < <(find "$PIPELINES_DIR" -maxdepth 1 -name '*.json' -type f 2>/dev/null)
|
|
278
|
+
if [[ $pip_count -gt 0 ]]; then
|
|
279
|
+
check_pass "Pipeline templates: ${pip_count} installed"
|
|
280
|
+
else
|
|
281
|
+
check_warn "Pipeline dir exists but no .json files found"
|
|
282
|
+
fi
|
|
283
|
+
else
|
|
284
|
+
check_warn "No pipeline templates at ~/.shipwright/pipelines/"
|
|
285
|
+
echo -e " ${DIM}Run: shipwright init${RESET}"
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
# GitHub CLI
|
|
289
|
+
if command -v gh &>/dev/null; then
|
|
290
|
+
if gh auth status &>/dev/null; then
|
|
291
|
+
GH_USER="$(gh api user -q .login 2>/dev/null || echo "authenticated")"
|
|
292
|
+
check_pass "GitHub CLI: ${GH_USER}"
|
|
293
|
+
else
|
|
294
|
+
check_warn "GitHub CLI installed but not authenticated"
|
|
295
|
+
echo -e " ${DIM}gh auth login${RESET}"
|
|
296
|
+
fi
|
|
297
|
+
else
|
|
298
|
+
check_warn "GitHub CLI (gh) not installed — daemon/pipeline need it for PRs and issues"
|
|
299
|
+
echo -e " ${DIM}brew install gh${RESET} (macOS)"
|
|
300
|
+
echo -e " ${DIM}sudo apt install gh${RESET} (Ubuntu/Debian)"
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
304
|
+
# 4. PATH & CLI
|
|
305
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
306
|
+
echo ""
|
|
307
|
+
echo -e "${PURPLE}${BOLD} PATH & CLI${RESET}"
|
|
308
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
309
|
+
|
|
310
|
+
BIN_DIR="$HOME/.local/bin"
|
|
311
|
+
|
|
312
|
+
if echo "$PATH" | tr ':' '\n' | grep -q "$BIN_DIR"; then
|
|
313
|
+
check_pass "${BIN_DIR} is in PATH"
|
|
314
|
+
else
|
|
315
|
+
check_warn "${BIN_DIR} is NOT in PATH"
|
|
316
|
+
echo -e " ${DIM}Add to ~/.zshrc or ~/.bashrc:${RESET}"
|
|
317
|
+
echo -e " ${DIM}export PATH=\"\$HOME/.local/bin:\$PATH\"${RESET}"
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
# Check sw subcommands are installed alongside the router
|
|
321
|
+
if command -v sw &>/dev/null; then
|
|
322
|
+
SW_DIR="$(dirname "$(command -v sw)")"
|
|
323
|
+
check_pass "shipwright router found at ${SW_DIR}/sw"
|
|
324
|
+
|
|
325
|
+
missing_subs=()
|
|
326
|
+
for sub in sw-session.sh sw-status.sh sw-cleanup.sh; do
|
|
327
|
+
if [[ ! -x "${SW_DIR}/${sub}" ]]; then
|
|
328
|
+
missing_subs+=("$sub")
|
|
329
|
+
fi
|
|
330
|
+
done
|
|
331
|
+
|
|
332
|
+
if [[ ${#missing_subs[@]} -eq 0 ]]; then
|
|
333
|
+
check_pass "All core subcommands installed"
|
|
334
|
+
else
|
|
335
|
+
check_warn "Missing subcommands: ${missing_subs[*]}"
|
|
336
|
+
echo -e " ${DIM}Re-run install.sh or shipwright upgrade --apply${RESET}"
|
|
337
|
+
fi
|
|
338
|
+
else
|
|
339
|
+
check_fail "shipwright command not found in PATH"
|
|
340
|
+
echo -e " ${DIM}Re-run install.sh to install the CLI${RESET}"
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
344
|
+
# 5. Pane Display
|
|
345
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
346
|
+
echo ""
|
|
347
|
+
echo -e "${PURPLE}${BOLD} PANE DISPLAY${RESET}"
|
|
348
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
349
|
+
|
|
350
|
+
# Check overlay file exists
|
|
351
|
+
if [[ -f "$HOME/.tmux/shipwright-overlay.conf" ]]; then
|
|
352
|
+
# Check for set-hook color enforcement
|
|
353
|
+
if grep -q "set-hook.*after-split-window" "$HOME/.tmux/shipwright-overlay.conf" 2>/dev/null; then
|
|
354
|
+
check_pass "Overlay has color hooks (set-hook)"
|
|
355
|
+
else
|
|
356
|
+
check_warn "Overlay missing color hooks — new panes may flash white"
|
|
357
|
+
echo -e " ${DIM}Run: shipwright upgrade --apply or shipwright init${RESET}"
|
|
358
|
+
fi
|
|
359
|
+
else
|
|
360
|
+
check_fail "Overlay not found — pane display features unavailable"
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
# Check if set-hook commands are active in tmux
|
|
364
|
+
if [[ -n "${TMUX:-}" ]]; then
|
|
365
|
+
if tmux show-hooks -g 2>/dev/null | grep -q "after-split-window"; then
|
|
366
|
+
check_pass "set-hook commands active in tmux"
|
|
367
|
+
else
|
|
368
|
+
check_warn "set-hook commands not active — reload config: prefix + r"
|
|
369
|
+
fi
|
|
370
|
+
|
|
371
|
+
# Check default-terminal
|
|
372
|
+
TMUX_TERM="$(tmux show-option -gv default-terminal 2>/dev/null || echo "unknown")"
|
|
373
|
+
if [[ "$TMUX_TERM" == *"256color"* ]]; then
|
|
374
|
+
check_pass "default-terminal: $TMUX_TERM"
|
|
375
|
+
else
|
|
376
|
+
check_warn "default-terminal: $TMUX_TERM — 256color variant recommended"
|
|
377
|
+
echo -e " ${DIM}set -g default-terminal 'tmux-256color'${RESET}"
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
# Check pane border includes cyan accent
|
|
381
|
+
BORDER_FMT="$(tmux show-option -gv pane-border-format 2>/dev/null || echo "")"
|
|
382
|
+
if echo "$BORDER_FMT" | grep -q "#00d4ff"; then
|
|
383
|
+
check_pass "Pane border format includes cyan accent"
|
|
384
|
+
else
|
|
385
|
+
check_warn "Pane border format missing cyan accent — overlay may not be loaded"
|
|
386
|
+
fi
|
|
387
|
+
# Check Claude Code compatibility settings
|
|
388
|
+
PASSTHROUGH="$(tmux show-option -gv allow-passthrough 2>/dev/null || echo "off")"
|
|
389
|
+
if [[ "$PASSTHROUGH" == "on" ]]; then
|
|
390
|
+
check_pass "allow-passthrough: on (DEC 2026 synchronized output)"
|
|
391
|
+
else
|
|
392
|
+
check_warn "allow-passthrough: ${PASSTHROUGH} — Claude Code may flicker"
|
|
393
|
+
echo -e " ${DIM}Fix: shipwright tmux fix${RESET}"
|
|
394
|
+
fi
|
|
395
|
+
|
|
396
|
+
EXTKEYS="$(tmux show-option -gv extended-keys 2>/dev/null || echo "off")"
|
|
397
|
+
if [[ "$EXTKEYS" == "on" ]]; then
|
|
398
|
+
check_pass "extended-keys: on"
|
|
399
|
+
else
|
|
400
|
+
check_warn "extended-keys: ${EXTKEYS} — some TUI key combos may not work"
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
HIST_LIMIT="$(tmux show-option -gv history-limit 2>/dev/null || echo "2000")"
|
|
404
|
+
if [[ "$HIST_LIMIT" -ge 100000 ]]; then
|
|
405
|
+
check_pass "history-limit: ${HIST_LIMIT}"
|
|
406
|
+
else
|
|
407
|
+
check_warn "history-limit: ${HIST_LIMIT} — 250000+ recommended for Claude Code"
|
|
408
|
+
echo -e " ${DIM}Fix: shipwright tmux fix${RESET}"
|
|
409
|
+
fi
|
|
410
|
+
else
|
|
411
|
+
info "Not in tmux session — skipping runtime display checks"
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
415
|
+
# 6. Orphaned Sessions
|
|
416
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
417
|
+
echo ""
|
|
418
|
+
echo -e "${PURPLE}${BOLD} ORPHAN CHECK${RESET}"
|
|
419
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
420
|
+
|
|
421
|
+
orphaned_teams=0
|
|
422
|
+
TEAMS_DIR="$HOME/.claude/teams"
|
|
423
|
+
if [[ -d "$TEAMS_DIR" ]]; then
|
|
424
|
+
while IFS= read -r team_dir; do
|
|
425
|
+
[[ -z "$team_dir" ]] && continue
|
|
426
|
+
team_name="$(basename "$team_dir")"
|
|
427
|
+
config_file="${team_dir}/config.json"
|
|
428
|
+
if [[ ! -f "$config_file" ]]; then
|
|
429
|
+
orphaned_teams=$((orphaned_teams + 1))
|
|
430
|
+
check_warn "Orphaned team dir: ${team_name} (no config.json)"
|
|
431
|
+
fi
|
|
432
|
+
done < <(find "$TEAMS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
|
|
433
|
+
fi
|
|
434
|
+
|
|
435
|
+
if [[ $orphaned_teams -eq 0 ]]; then
|
|
436
|
+
check_pass "No orphaned team sessions"
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
440
|
+
# 7. Environment & Resources
|
|
441
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
442
|
+
echo ""
|
|
443
|
+
echo -e "${PURPLE}${BOLD} ENVIRONMENT${RESET}"
|
|
444
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
445
|
+
|
|
446
|
+
# Expected directories
|
|
447
|
+
EXPECTED_DIRS=(
|
|
448
|
+
"$HOME/.claude"
|
|
449
|
+
"$HOME/.claude/hooks"
|
|
450
|
+
"$HOME/.shipwright"
|
|
451
|
+
"$HOME/.shipwright"
|
|
452
|
+
)
|
|
453
|
+
missing_dirs=0
|
|
454
|
+
for dir in "${EXPECTED_DIRS[@]}"; do
|
|
455
|
+
if [[ -d "$dir" ]]; then
|
|
456
|
+
check_pass "Directory: ${dir/#$HOME/\~}"
|
|
457
|
+
else
|
|
458
|
+
check_warn "Missing directory: ${dir/#$HOME/\~}"
|
|
459
|
+
echo -e " ${DIM}mkdir -p \"$dir\"${RESET}"
|
|
460
|
+
missing_dirs=$((missing_dirs + 1))
|
|
461
|
+
fi
|
|
462
|
+
done
|
|
463
|
+
|
|
464
|
+
# JSON validation for templates
|
|
465
|
+
if command -v jq &>/dev/null; then
|
|
466
|
+
json_errors=0
|
|
467
|
+
json_total=0
|
|
468
|
+
for tpl_dir in "$HOME/.shipwright/templates" "$HOME/.shipwright/pipelines"; do
|
|
469
|
+
if [[ -d "$tpl_dir" ]]; then
|
|
470
|
+
while IFS= read -r json_file; do
|
|
471
|
+
[[ -z "$json_file" ]] && continue
|
|
472
|
+
json_total=$((json_total + 1))
|
|
473
|
+
if ! jq -e . "$json_file" &>/dev/null; then
|
|
474
|
+
check_fail "Invalid JSON: ${json_file/#$HOME/\~}"
|
|
475
|
+
json_errors=$((json_errors + 1))
|
|
476
|
+
fi
|
|
477
|
+
done < <(find "$tpl_dir" -maxdepth 1 -name '*.json' -type f 2>/dev/null)
|
|
478
|
+
fi
|
|
479
|
+
done
|
|
480
|
+
if [[ $json_total -gt 0 && $json_errors -eq 0 ]]; then
|
|
481
|
+
check_pass "Template JSON: ${json_total} files valid"
|
|
482
|
+
elif [[ $json_total -eq 0 ]]; then
|
|
483
|
+
check_warn "No template JSON files found to validate"
|
|
484
|
+
fi
|
|
485
|
+
fi
|
|
486
|
+
|
|
487
|
+
# Terminal 256-color support
|
|
488
|
+
TERM_VAR="${TERM:-}"
|
|
489
|
+
if [[ "$TERM_VAR" == *"256color"* || "$TERM_VAR" == "xterm-kitty" || "$TERM_VAR" == "tmux-256color" ]]; then
|
|
490
|
+
check_pass "TERM=$TERM_VAR (256 colors)"
|
|
491
|
+
elif [[ -z "$TERM_VAR" ]]; then
|
|
492
|
+
check_warn "TERM not set — colors may not display correctly"
|
|
493
|
+
else
|
|
494
|
+
check_warn "TERM=$TERM_VAR — 256color variant recommended for full theme support"
|
|
495
|
+
echo -e " ${DIM}export TERM=xterm-256color${RESET}"
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
# Disk space check (warn if < 1GB free)
|
|
499
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
500
|
+
FREE_GB="$(df -g "$HOME" 2>/dev/null | awk 'NR==2{print $4}')" || FREE_GB=""
|
|
501
|
+
else
|
|
502
|
+
# Linux: df -BG gives output in GB
|
|
503
|
+
FREE_GB="$(df -BG "$HOME" 2>/dev/null | awk 'NR==2{print $4}' | tr -d 'G')" || FREE_GB=""
|
|
504
|
+
fi
|
|
505
|
+
if [[ -n "$FREE_GB" && "$FREE_GB" =~ ^[0-9]+$ ]]; then
|
|
506
|
+
if [[ "$FREE_GB" -ge 5 ]]; then
|
|
507
|
+
check_pass "Disk space: ${FREE_GB}GB free"
|
|
508
|
+
elif [[ "$FREE_GB" -ge 1 ]]; then
|
|
509
|
+
check_warn "Disk space: ${FREE_GB}GB free — getting low"
|
|
510
|
+
else
|
|
511
|
+
check_fail "Disk space: ${FREE_GB}GB free — less than 1GB available"
|
|
512
|
+
echo -e " ${DIM}Free up disk space to avoid pipeline failures${RESET}"
|
|
513
|
+
fi
|
|
514
|
+
fi
|
|
515
|
+
|
|
516
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
517
|
+
# 8. Terminal Compatibility
|
|
518
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
519
|
+
echo ""
|
|
520
|
+
echo -e "${PURPLE}${BOLD} TERMINAL${RESET}"
|
|
521
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
522
|
+
|
|
523
|
+
TERM_PROGRAM="${TERM_PROGRAM:-unknown}"
|
|
524
|
+
|
|
525
|
+
case "$TERM_PROGRAM" in
|
|
526
|
+
iTerm.app|iTerm2)
|
|
527
|
+
check_pass "iTerm2 — full support (true color, SGR mouse, focus events)"
|
|
528
|
+
# Verify mouse reporting is actually enabled in iTerm2 profile
|
|
529
|
+
ITERM_MOUSE="$(defaults read com.googlecode.iterm2 "New Bookmarks" 2>/dev/null | grep '"Mouse Reporting"' | head -1 | grep -oE '[0-9]+' || echo "unknown")"
|
|
530
|
+
if [[ "$ITERM_MOUSE" == "0" ]]; then
|
|
531
|
+
check_fail "iTerm2 mouse reporting is DISABLED — tmux cannot receive mouse clicks"
|
|
532
|
+
echo -e " ${DIM}Fix: iTerm2 → Preferences → Profiles → Terminal → enable 'Report mouse clicks & drags'${RESET}"
|
|
533
|
+
echo -e " ${DIM}Or run: ${CYAN}/usr/libexec/PlistBuddy -c \"Set ':New Bookmarks:0:Mouse Reporting' 1\" ~/Library/Preferences/com.googlecode.iterm2.plist${RESET}"
|
|
534
|
+
elif [[ "$ITERM_MOUSE" == "1" ]]; then
|
|
535
|
+
check_pass "iTerm2 mouse reporting: enabled"
|
|
536
|
+
fi
|
|
537
|
+
;;
|
|
538
|
+
Apple_Terminal)
|
|
539
|
+
check_warn "Terminal.app — limited support"
|
|
540
|
+
echo -e " ${DIM}No true color (256 colors only), no SGR extended mouse.${RESET}"
|
|
541
|
+
echo -e " ${DIM}Mouse clicking works, but wide terminals (>223 cols) may mistrack.${RESET}"
|
|
542
|
+
echo -e " ${DIM}Recommended: use iTerm2 or WezTerm for best experience.${RESET}"
|
|
543
|
+
;;
|
|
544
|
+
WezTerm)
|
|
545
|
+
check_pass "WezTerm — full support (true color, SGR mouse, focus events)"
|
|
546
|
+
;;
|
|
547
|
+
tmux)
|
|
548
|
+
# Detect parent terminal when nested inside tmux
|
|
549
|
+
PARENT_TERM="${LC_TERMINAL:-unknown}"
|
|
550
|
+
check_pass "Running inside tmux — parent terminal: ${PARENT_TERM}"
|
|
551
|
+
# Check iTerm2 mouse reporting even when nested inside tmux
|
|
552
|
+
if [[ "$PARENT_TERM" == *iTerm* ]]; then
|
|
553
|
+
ITERM_MOUSE="$(defaults read com.googlecode.iterm2 "New Bookmarks" 2>/dev/null | grep '"Mouse Reporting"' | head -1 | grep -oE '[0-9]+' || echo "unknown")"
|
|
554
|
+
if [[ "$ITERM_MOUSE" == "0" ]]; then
|
|
555
|
+
check_fail "iTerm2 mouse reporting is DISABLED — tmux cannot receive mouse clicks"
|
|
556
|
+
echo -e " ${DIM}Fix: iTerm2 → Preferences → Profiles → Terminal → enable 'Report mouse clicks & drags'${RESET}"
|
|
557
|
+
echo -e " ${DIM}Or run: ${CYAN}shipwright init${RESET} (auto-fixes this)${RESET}"
|
|
558
|
+
elif [[ "$ITERM_MOUSE" == "1" ]]; then
|
|
559
|
+
check_pass "iTerm2 mouse reporting: enabled"
|
|
560
|
+
fi
|
|
561
|
+
fi
|
|
562
|
+
;;
|
|
563
|
+
vscode)
|
|
564
|
+
check_warn "VS Code integrated terminal"
|
|
565
|
+
echo -e " ${DIM}Some pane border features may not render correctly.${RESET}"
|
|
566
|
+
echo -e " ${DIM}Consider running tmux in an external terminal.${RESET}"
|
|
567
|
+
;;
|
|
568
|
+
Ghostty)
|
|
569
|
+
check_pass "Ghostty — full support (true color, SGR mouse)"
|
|
570
|
+
;;
|
|
571
|
+
Alacritty)
|
|
572
|
+
check_pass "Alacritty — full support (true color, SGR mouse)"
|
|
573
|
+
;;
|
|
574
|
+
kitty)
|
|
575
|
+
check_pass "kitty — full support (true color, extended keyboard)"
|
|
576
|
+
;;
|
|
577
|
+
*)
|
|
578
|
+
info "Terminal: ${TERM_PROGRAM}"
|
|
579
|
+
;;
|
|
580
|
+
esac
|
|
581
|
+
|
|
582
|
+
# Check mouse window clicking (tmux 3.4+ changed the default)
|
|
583
|
+
if command -v tmux &>/dev/null && [[ -n "${TMUX:-}" ]]; then
|
|
584
|
+
MOUSE_BIND="$(tmux list-keys 2>/dev/null | grep 'MouseDown1Status' | head -1 || true)"
|
|
585
|
+
if echo "$MOUSE_BIND" | grep -q 'select-window'; then
|
|
586
|
+
check_pass "Mouse window click: select-window (correct)"
|
|
587
|
+
elif echo "$MOUSE_BIND" | grep -q 'switch-client'; then
|
|
588
|
+
check_fail "Mouse window click: switch-client (broken — clicking windows won't work)"
|
|
589
|
+
echo -e " ${DIM}Fix: add to tmux.conf: bind -T root MouseDown1Status select-window -t =${RESET}"
|
|
590
|
+
echo -e " ${DIM}Or run: shipwright init${RESET}"
|
|
591
|
+
fi
|
|
592
|
+
fi
|
|
593
|
+
|
|
594
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
595
|
+
# 9. Issue Tracker
|
|
596
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
597
|
+
echo ""
|
|
598
|
+
echo -e "${PURPLE}${BOLD} ISSUE TRACKER${RESET}"
|
|
599
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
600
|
+
|
|
601
|
+
TRACKER_CONFIG="${HOME}/.shipwright/tracker-config.json"
|
|
602
|
+
if [[ -f "$TRACKER_CONFIG" ]]; then
|
|
603
|
+
TRACKER_PROVIDER=$(jq -r '.provider // "none"' "$TRACKER_CONFIG" 2>/dev/null || echo "none")
|
|
604
|
+
if [[ "$TRACKER_PROVIDER" != "none" && -n "$TRACKER_PROVIDER" ]]; then
|
|
605
|
+
check_pass "Tracker provider: ${TRACKER_PROVIDER}"
|
|
606
|
+
# Validate provider-specific config
|
|
607
|
+
case "$TRACKER_PROVIDER" in
|
|
608
|
+
linear)
|
|
609
|
+
LINEAR_KEY=$(jq -r '.linear.api_key // empty' "$TRACKER_CONFIG" 2>/dev/null || true)
|
|
610
|
+
if [[ -n "$LINEAR_KEY" ]]; then
|
|
611
|
+
check_pass "Linear API key: configured"
|
|
612
|
+
else
|
|
613
|
+
check_warn "Linear API key: not set — set via shipwright tracker init or LINEAR_API_KEY env var"
|
|
614
|
+
fi
|
|
615
|
+
;;
|
|
616
|
+
jira)
|
|
617
|
+
JIRA_URL=$(jq -r '.jira.base_url // empty' "$TRACKER_CONFIG" 2>/dev/null || true)
|
|
618
|
+
JIRA_TOKEN=$(jq -r '.jira.api_token // empty' "$TRACKER_CONFIG" 2>/dev/null || true)
|
|
619
|
+
if [[ -n "$JIRA_URL" && -n "$JIRA_TOKEN" ]]; then
|
|
620
|
+
check_pass "Jira: configured (${JIRA_URL})"
|
|
621
|
+
else
|
|
622
|
+
check_warn "Jira: incomplete config — run shipwright jira init"
|
|
623
|
+
fi
|
|
624
|
+
;;
|
|
625
|
+
esac
|
|
626
|
+
else
|
|
627
|
+
info " No tracker configured ${DIM}(optional — run shipwright tracker init)${RESET}"
|
|
628
|
+
fi
|
|
629
|
+
else
|
|
630
|
+
info " No tracker configured ${DIM}(optional — run shipwright tracker init)${RESET}"
|
|
631
|
+
fi
|
|
632
|
+
|
|
633
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
634
|
+
# 10. Agent Heartbeats & Checkpoints
|
|
635
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
636
|
+
echo ""
|
|
637
|
+
echo -e "${PURPLE}${BOLD} HEARTBEATS & CHECKPOINTS${RESET}"
|
|
638
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
639
|
+
|
|
640
|
+
HEARTBEAT_DIR="$HOME/.shipwright/heartbeats"
|
|
641
|
+
if [[ -d "$HEARTBEAT_DIR" ]]; then
|
|
642
|
+
check_pass "Heartbeat directory: ${HEARTBEAT_DIR/#$HOME/\~}"
|
|
643
|
+
# Check permissions
|
|
644
|
+
if [[ -w "$HEARTBEAT_DIR" ]]; then
|
|
645
|
+
check_pass "Heartbeat directory: writable"
|
|
646
|
+
else
|
|
647
|
+
check_fail "Heartbeat directory: not writable"
|
|
648
|
+
fi
|
|
649
|
+
|
|
650
|
+
# Count active/stale heartbeats
|
|
651
|
+
hb_active=0
|
|
652
|
+
hb_stale=0
|
|
653
|
+
for hb_file in "${HEARTBEAT_DIR}"/*.json; do
|
|
654
|
+
[[ -f "$hb_file" ]] || continue
|
|
655
|
+
hb_updated=$(jq -r '.updated_at // ""' "$hb_file" 2>/dev/null || true)
|
|
656
|
+
if [[ -n "$hb_updated" && "$hb_updated" != "null" ]]; then
|
|
657
|
+
hb_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$hb_updated" +%s 2>/dev/null || echo 0)
|
|
658
|
+
if [[ "$hb_epoch" -gt 0 ]]; then
|
|
659
|
+
now_e=$(date +%s)
|
|
660
|
+
hb_age=$((now_e - hb_epoch))
|
|
661
|
+
if [[ "$hb_age" -ge 120 ]]; then
|
|
662
|
+
hb_stale=$((hb_stale + 1))
|
|
663
|
+
else
|
|
664
|
+
hb_active=$((hb_active + 1))
|
|
665
|
+
fi
|
|
666
|
+
fi
|
|
667
|
+
fi
|
|
668
|
+
done
|
|
669
|
+
if [[ $hb_active -gt 0 ]]; then
|
|
670
|
+
check_pass "Active heartbeats: ${hb_active}"
|
|
671
|
+
fi
|
|
672
|
+
if [[ $hb_stale -gt 0 ]]; then
|
|
673
|
+
check_warn "Stale heartbeats: ${hb_stale} (>120s old)"
|
|
674
|
+
echo -e " ${DIM}Clean up with: shipwright heartbeat clear <job-id>${RESET}"
|
|
675
|
+
fi
|
|
676
|
+
else
|
|
677
|
+
info " No heartbeat directory ${DIM}(created automatically when agents run)${RESET}"
|
|
678
|
+
fi
|
|
679
|
+
|
|
680
|
+
# Checkpoint directory
|
|
681
|
+
CHECKPOINT_DIR=".claude/pipeline-artifacts/checkpoints"
|
|
682
|
+
if [[ -d "$CHECKPOINT_DIR" ]]; then
|
|
683
|
+
cp_count=0
|
|
684
|
+
for cp_file in "${CHECKPOINT_DIR}"/*-checkpoint.json; do
|
|
685
|
+
[[ -f "$cp_file" ]] || continue
|
|
686
|
+
cp_count=$((cp_count + 1))
|
|
687
|
+
done
|
|
688
|
+
if [[ $cp_count -gt 0 ]]; then
|
|
689
|
+
check_pass "Checkpoints: ${cp_count} saved"
|
|
690
|
+
else
|
|
691
|
+
check_pass "Checkpoint directory exists (no checkpoints saved)"
|
|
692
|
+
fi
|
|
693
|
+
else
|
|
694
|
+
info " No checkpoint directory ${DIM}(created on first checkpoint save)${RESET}"
|
|
695
|
+
fi
|
|
696
|
+
|
|
697
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
698
|
+
# 11. Remote Machines
|
|
699
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
700
|
+
echo ""
|
|
701
|
+
echo -e "${PURPLE}${BOLD} REMOTE MACHINES${RESET}"
|
|
702
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
703
|
+
|
|
704
|
+
MACHINES_FILE="$HOME/.shipwright/machines.json"
|
|
705
|
+
if [[ -f "$MACHINES_FILE" ]]; then
|
|
706
|
+
machine_count=$(jq '.machines | length' "$MACHINES_FILE" 2>/dev/null || echo 0)
|
|
707
|
+
if [[ "$machine_count" -gt 0 ]]; then
|
|
708
|
+
check_pass "Registered machines: ${machine_count}"
|
|
709
|
+
# Check SSH connectivity (quick check, 5s timeout per machine)
|
|
710
|
+
if command -v ssh &>/dev/null; then
|
|
711
|
+
while IFS= read -r machine; do
|
|
712
|
+
[[ -z "$machine" ]] && continue
|
|
713
|
+
m_name=$(echo "$machine" | jq -r '.name // ""')
|
|
714
|
+
m_host=$(echo "$machine" | jq -r '.host // ""')
|
|
715
|
+
m_user=$(echo "$machine" | jq -r '.user // ""')
|
|
716
|
+
m_port=$(echo "$machine" | jq -r '.port // 22')
|
|
717
|
+
|
|
718
|
+
if [[ -n "$m_host" ]]; then
|
|
719
|
+
ssh_target="${m_user:+${m_user}@}${m_host}"
|
|
720
|
+
if ssh -o ConnectTimeout=5 -o BatchMode=yes -p "$m_port" "$ssh_target" true 2>/dev/null; then
|
|
721
|
+
check_pass "SSH: ${m_name} (${ssh_target}) reachable"
|
|
722
|
+
else
|
|
723
|
+
check_warn "SSH: ${m_name} (${ssh_target}) unreachable"
|
|
724
|
+
echo -e " ${DIM}Check SSH key and connectivity: ssh -p ${m_port} ${ssh_target}${RESET}"
|
|
725
|
+
fi
|
|
726
|
+
fi
|
|
727
|
+
done < <(jq -c '.machines[]' "$MACHINES_FILE" 2>/dev/null)
|
|
728
|
+
fi
|
|
729
|
+
else
|
|
730
|
+
info " No machines registered ${DIM}(add with: shipwright remote add)${RESET}"
|
|
731
|
+
fi
|
|
732
|
+
else
|
|
733
|
+
info " No remote machines ${DIM}(optional — run shipwright remote add)${RESET}"
|
|
734
|
+
fi
|
|
735
|
+
|
|
736
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
737
|
+
# 12. Team Connectivity
|
|
738
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
739
|
+
echo ""
|
|
740
|
+
echo -e "${PURPLE}${BOLD} TEAM CONNECTIVITY${RESET}"
|
|
741
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
742
|
+
|
|
743
|
+
# Check connect process
|
|
744
|
+
CONNECT_PID_FILE="$HOME/.shipwright/connect.pid"
|
|
745
|
+
if [[ -f "$CONNECT_PID_FILE" ]]; then
|
|
746
|
+
CONNECT_PID=$(cat "$CONNECT_PID_FILE" 2>/dev/null || echo "")
|
|
747
|
+
if [[ -n "$CONNECT_PID" ]]; then
|
|
748
|
+
if kill -0 "$CONNECT_PID" 2>/dev/null; then
|
|
749
|
+
check_pass "Connect process: running (PID ${CONNECT_PID})"
|
|
750
|
+
else
|
|
751
|
+
check_warn "Connect process: PID file exists but process not running"
|
|
752
|
+
echo -e " ${DIM}Clean up with: rm ${CONNECT_PID_FILE}${RESET}"
|
|
753
|
+
fi
|
|
754
|
+
else
|
|
755
|
+
check_warn "Connect PID file exists but is empty"
|
|
756
|
+
fi
|
|
757
|
+
else
|
|
758
|
+
info " Team connect not configured ${DIM}(optional)${RESET}"
|
|
759
|
+
fi
|
|
760
|
+
|
|
761
|
+
# Check team config
|
|
762
|
+
TEAM_CONFIG="$HOME/.shipwright/team-config.json"
|
|
763
|
+
if [[ -f "$TEAM_CONFIG" ]]; then
|
|
764
|
+
if jq -e . "$TEAM_CONFIG" &>/dev/null; then
|
|
765
|
+
check_pass "Team config: valid JSON"
|
|
766
|
+
|
|
767
|
+
# Check dashboard_url field
|
|
768
|
+
DASHBOARD_URL=$(jq -r '.dashboard_url // empty' "$TEAM_CONFIG" 2>/dev/null || true)
|
|
769
|
+
if [[ -n "$DASHBOARD_URL" ]]; then
|
|
770
|
+
check_pass "Dashboard URL: configured"
|
|
771
|
+
|
|
772
|
+
# Try to reach dashboard with 3s timeout
|
|
773
|
+
if command -v curl &>/dev/null; then
|
|
774
|
+
if curl -s -m 3 "${DASHBOARD_URL}/api/health" &>/dev/null; then
|
|
775
|
+
check_pass "Dashboard reachable: ${DASHBOARD_URL}"
|
|
776
|
+
else
|
|
777
|
+
check_warn "Dashboard unreachable: ${DASHBOARD_URL}"
|
|
778
|
+
echo -e " ${DIM}Check if dashboard service is running or URL is correct${RESET}"
|
|
779
|
+
fi
|
|
780
|
+
else
|
|
781
|
+
info " curl not found — skipping dashboard health check"
|
|
782
|
+
fi
|
|
783
|
+
else
|
|
784
|
+
check_warn "Team config: missing dashboard_url field"
|
|
785
|
+
fi
|
|
786
|
+
else
|
|
787
|
+
check_fail "Team config: invalid JSON"
|
|
788
|
+
echo -e " ${DIM}Fix JSON syntax in ${TEAM_CONFIG}${RESET}"
|
|
789
|
+
fi
|
|
790
|
+
else
|
|
791
|
+
info " Team config not found ${DIM}(optional — run shipwright init)${RESET}"
|
|
792
|
+
fi
|
|
793
|
+
|
|
794
|
+
# Check developer registry
|
|
795
|
+
DEVELOPER_REGISTRY="$HOME/.shipwright/developer-registry.json"
|
|
796
|
+
if [[ -f "$DEVELOPER_REGISTRY" ]]; then
|
|
797
|
+
if jq -e . "$DEVELOPER_REGISTRY" &>/dev/null; then
|
|
798
|
+
check_pass "Developer registry: exists and valid"
|
|
799
|
+
else
|
|
800
|
+
check_fail "Developer registry: invalid JSON"
|
|
801
|
+
echo -e " ${DIM}Fix JSON syntax in ${DEVELOPER_REGISTRY}${RESET}"
|
|
802
|
+
fi
|
|
803
|
+
else
|
|
804
|
+
info " Developer registry not found ${DIM}(optional)${RESET}"
|
|
805
|
+
fi
|
|
806
|
+
|
|
807
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
808
|
+
# 13. GitHub Integration
|
|
809
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
810
|
+
echo ""
|
|
811
|
+
echo -e "${PURPLE}${BOLD} GITHUB INTEGRATION${RESET}"
|
|
812
|
+
echo -e "${DIM} ──────────────────────────────────────────${RESET}"
|
|
813
|
+
|
|
814
|
+
if command -v gh &>/dev/null; then
|
|
815
|
+
if gh auth status &>/dev/null 2>&1; then
|
|
816
|
+
check_pass "gh CLI authenticated"
|
|
817
|
+
|
|
818
|
+
# Check required scopes
|
|
819
|
+
gh_scopes=""
|
|
820
|
+
gh_scopes=$(gh auth status 2>&1 | grep -i "token scopes" || echo "")
|
|
821
|
+
if [[ -n "$gh_scopes" ]]; then
|
|
822
|
+
if echo "$gh_scopes" | grep -qi "repo"; then
|
|
823
|
+
check_pass "Token has 'repo' scope"
|
|
824
|
+
else
|
|
825
|
+
check_warn "'repo' scope not detected — some features may not work"
|
|
826
|
+
echo -e " ${DIM}Fix: gh auth refresh -s repo${RESET}"
|
|
827
|
+
fi
|
|
828
|
+
if echo "$gh_scopes" | grep -qi "read:org"; then
|
|
829
|
+
check_pass "Token has 'read:org' scope"
|
|
830
|
+
else
|
|
831
|
+
check_warn "'read:org' scope not detected — org data may be unavailable"
|
|
832
|
+
echo -e " ${DIM}Fix: gh auth refresh -s read:org${RESET}"
|
|
833
|
+
fi
|
|
834
|
+
fi
|
|
835
|
+
|
|
836
|
+
# Check GraphQL endpoint
|
|
837
|
+
if gh api graphql -f query='{viewer{login}}' &>/dev/null 2>&1; then
|
|
838
|
+
check_pass "GraphQL API accessible"
|
|
839
|
+
else
|
|
840
|
+
check_warn "GraphQL API not accessible — intelligence enrichment will use fallbacks"
|
|
841
|
+
fi
|
|
842
|
+
|
|
843
|
+
# Check code scanning API
|
|
844
|
+
dr_repo_owner=""
|
|
845
|
+
dr_repo_name=""
|
|
846
|
+
dr_repo_owner=$(git remote get-url origin 2>/dev/null | sed -E 's#.*[:/]([^/]+)/[^/]+(\.git)?$#\1#' || echo "")
|
|
847
|
+
dr_repo_name=$(git remote get-url origin 2>/dev/null | sed -E 's#.*/([^/]+)(\.git)?$#\1#' || echo "")
|
|
848
|
+
if [[ -n "$dr_repo_owner" && -n "$dr_repo_name" ]]; then
|
|
849
|
+
if gh api "repos/$dr_repo_owner/$dr_repo_name/code-scanning/alerts?per_page=1" &>/dev/null 2>&1; then
|
|
850
|
+
check_pass "Code scanning API accessible"
|
|
851
|
+
else
|
|
852
|
+
info " Code scanning API not available ${DIM}(may need GitHub Advanced Security)${RESET}"
|
|
853
|
+
fi
|
|
854
|
+
fi
|
|
855
|
+
|
|
856
|
+
# Check CODEOWNERS file
|
|
857
|
+
if [[ -f "CODEOWNERS" || -f ".github/CODEOWNERS" || -f "docs/CODEOWNERS" ]]; then
|
|
858
|
+
check_pass "CODEOWNERS file found"
|
|
859
|
+
else
|
|
860
|
+
info " No CODEOWNERS file ${DIM}(reviewer selection will use contributor data)${RESET}"
|
|
861
|
+
fi
|
|
862
|
+
|
|
863
|
+
# Check GitHub modules installed
|
|
864
|
+
_DOCTOR_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
865
|
+
if [[ -f "$_DOCTOR_SCRIPT_DIR/sw-github-graphql.sh" ]]; then
|
|
866
|
+
check_pass "GitHub GraphQL module installed"
|
|
867
|
+
else
|
|
868
|
+
info " GitHub GraphQL module not found ${DIM}(scripts/sw-github-graphql.sh)${RESET}"
|
|
869
|
+
fi
|
|
870
|
+
if [[ -f "$_DOCTOR_SCRIPT_DIR/sw-github-checks.sh" ]]; then
|
|
871
|
+
check_pass "GitHub Checks module installed"
|
|
872
|
+
else
|
|
873
|
+
info " GitHub Checks module not found ${DIM}(scripts/sw-github-checks.sh)${RESET}"
|
|
874
|
+
fi
|
|
875
|
+
if [[ -f "$_DOCTOR_SCRIPT_DIR/sw-github-deploy.sh" ]]; then
|
|
876
|
+
check_pass "GitHub Deploy module installed"
|
|
877
|
+
else
|
|
878
|
+
info " GitHub Deploy module not found ${DIM}(scripts/sw-github-deploy.sh)${RESET}"
|
|
879
|
+
fi
|
|
880
|
+
else
|
|
881
|
+
check_warn "gh CLI not authenticated — run: ${DIM}gh auth login${RESET}"
|
|
882
|
+
fi
|
|
883
|
+
else
|
|
884
|
+
check_warn "gh CLI not installed — GitHub integration disabled"
|
|
885
|
+
echo -e " ${DIM}Install: brew install gh (macOS) or see https://cli.github.com${RESET}"
|
|
886
|
+
fi
|
|
887
|
+
|
|
888
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
889
|
+
# Summary
|
|
890
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
891
|
+
echo ""
|
|
892
|
+
echo -e "${DIM} ══════════════════════════════════════════${RESET}"
|
|
893
|
+
echo ""
|
|
894
|
+
|
|
895
|
+
TOTAL=$((PASS + WARN + FAIL))
|
|
896
|
+
|
|
897
|
+
echo -e " ${GREEN}${BOLD}${PASS}${RESET} passed ${YELLOW}${BOLD}${WARN}${RESET} warnings ${RED}${BOLD}${FAIL}${RESET} failed ${DIM}(${TOTAL} checks)${RESET}"
|
|
898
|
+
echo ""
|
|
899
|
+
|
|
900
|
+
if [[ $FAIL -gt 0 ]]; then
|
|
901
|
+
error "Some checks failed. Fix the issues above and re-run ${CYAN}shipwright doctor${RESET}"
|
|
902
|
+
elif [[ $WARN -gt 0 ]]; then
|
|
903
|
+
warn "Setup mostly OK, but there are warnings above"
|
|
904
|
+
else
|
|
905
|
+
success "Everything looks good!"
|
|
906
|
+
fi
|
|
907
|
+
echo ""
|