shipwright-cli 1.7.0 → 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/sw-init.sh +522 -0
- 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 -328
- package/scripts/cct-init.sh +0 -282
- package/scripts/cct-session.sh +0 -284
- package/scripts/cct-status.sh +0 -169
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright dashboard — Fleet Command Dashboard ║
|
|
4
|
+
# ║ Real-time WebSocket dashboard for fleet monitoring ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
|
+
|
|
9
|
+
VERSION="1.9.0"
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
|
|
12
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
13
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
14
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
15
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
16
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
17
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
18
|
+
RED='\033[38;2;248;113;113m' # error
|
|
19
|
+
DIM='\033[2m'
|
|
20
|
+
BOLD='\033[1m'
|
|
21
|
+
RESET='\033[0m'
|
|
22
|
+
|
|
23
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
24
|
+
# shellcheck source=lib/compat.sh
|
|
25
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
26
|
+
UNDERLINE='\033[4m'
|
|
27
|
+
|
|
28
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
29
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
30
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
31
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
32
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
33
|
+
|
|
34
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
35
|
+
now_epoch() { date +%s; }
|
|
36
|
+
|
|
37
|
+
# ─── Paths ──────────────────────────────────────────────────────────────────
|
|
38
|
+
TEAMS_DIR="${HOME}/.shipwright"
|
|
39
|
+
PID_FILE="${TEAMS_DIR}/dashboard.pid"
|
|
40
|
+
LOG_DIR="${TEAMS_DIR}/logs"
|
|
41
|
+
LOG_FILE="${LOG_DIR}/dashboard.log"
|
|
42
|
+
EVENTS_FILE="${TEAMS_DIR}/events.jsonl"
|
|
43
|
+
DEFAULT_PORT=8767
|
|
44
|
+
|
|
45
|
+
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
46
|
+
emit_event() {
|
|
47
|
+
local event_type="$1"
|
|
48
|
+
shift
|
|
49
|
+
local json_fields=""
|
|
50
|
+
for kv in "$@"; do
|
|
51
|
+
local key="${kv%%=*}"
|
|
52
|
+
local val="${kv#*=}"
|
|
53
|
+
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
54
|
+
json_fields="${json_fields},\"${key}\":${val}"
|
|
55
|
+
else
|
|
56
|
+
val="${val//\"/\\\"}"
|
|
57
|
+
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
mkdir -p "${TEAMS_DIR}"
|
|
61
|
+
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# ─── Header ────────────────────────────────────────────────────────────────
|
|
65
|
+
dashboard_header() {
|
|
66
|
+
echo ""
|
|
67
|
+
echo -e "${CYAN}${BOLD}╭─────────────────────────────────────────╮${RESET}"
|
|
68
|
+
echo -e "${CYAN}${BOLD}│${RESET} ${BOLD}⚓ Shipwright Fleet Command Dashboard${RESET} ${CYAN}${BOLD}│${RESET}"
|
|
69
|
+
echo -e "${CYAN}${BOLD}│${RESET} ${DIM}v${VERSION}${RESET} ${CYAN}${BOLD}│${RESET}"
|
|
70
|
+
echo -e "${CYAN}${BOLD}╰─────────────────────────────────────────╯${RESET}"
|
|
71
|
+
echo ""
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# ─── Help ───────────────────────────────────────────────────────────────────
|
|
75
|
+
show_help() {
|
|
76
|
+
dashboard_header
|
|
77
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
78
|
+
echo -e " ${CYAN}shipwright dashboard${RESET} [command] [options]"
|
|
79
|
+
echo ""
|
|
80
|
+
echo -e "${BOLD}COMMANDS${RESET}"
|
|
81
|
+
echo -e " ${CYAN}start${RESET} Start the dashboard server ${DIM}(background)${RESET}"
|
|
82
|
+
echo -e " ${CYAN}stop${RESET} Stop the dashboard server"
|
|
83
|
+
echo -e " ${CYAN}status${RESET} Show dashboard server status"
|
|
84
|
+
echo -e " ${CYAN}open${RESET} Open dashboard in browser"
|
|
85
|
+
echo -e " ${CYAN}help${RESET} Show this help message"
|
|
86
|
+
echo ""
|
|
87
|
+
echo -e "${BOLD}OPTIONS${RESET}"
|
|
88
|
+
echo -e " ${CYAN}--port${RESET} <N> Port to run on ${DIM}(default: ${DEFAULT_PORT})${RESET}"
|
|
89
|
+
echo -e " ${CYAN}--foreground${RESET} Run in foreground ${DIM}(don't daemonize)${RESET}"
|
|
90
|
+
echo ""
|
|
91
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
92
|
+
echo -e " ${DIM}shipwright dashboard${RESET} # Start in foreground"
|
|
93
|
+
echo -e " ${DIM}shipwright dashboard start${RESET} # Start in background"
|
|
94
|
+
echo -e " ${DIM}shipwright dashboard start --port 9000${RESET}"
|
|
95
|
+
echo -e " ${DIM}shipwright dashboard open${RESET} # Open in browser"
|
|
96
|
+
echo -e " ${DIM}shipwright dashboard stop${RESET} # Stop background server"
|
|
97
|
+
echo -e " ${DIM}shipwright dash status${RESET} # Check if running"
|
|
98
|
+
echo ""
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# ─── Prerequisite Check ────────────────────────────────────────────────────
|
|
102
|
+
check_bun() {
|
|
103
|
+
if ! command -v bun &>/dev/null; then
|
|
104
|
+
error "Bun is required but not installed"
|
|
105
|
+
info "Install Bun: ${UNDERLINE}https://bun.sh${RESET}"
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# ─── Find Server ───────────────────────────────────────────────────────────
|
|
111
|
+
find_server() {
|
|
112
|
+
# Look for dashboard/server.ts relative to the script's repo location
|
|
113
|
+
local repo_dir
|
|
114
|
+
repo_dir="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
115
|
+
local server_ts="${repo_dir}/dashboard/server.ts"
|
|
116
|
+
|
|
117
|
+
if [[ ! -f "$server_ts" ]]; then
|
|
118
|
+
# Also check in installed locations
|
|
119
|
+
for search_dir in \
|
|
120
|
+
"${HOME}/.local/share/shipwright/dashboard" \
|
|
121
|
+
"${HOME}/.shipwright/dashboard"; do
|
|
122
|
+
if [[ -f "${search_dir}/server.ts" ]]; then
|
|
123
|
+
server_ts="${search_dir}/server.ts"
|
|
124
|
+
break
|
|
125
|
+
fi
|
|
126
|
+
done
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
if [[ ! -f "$server_ts" ]]; then
|
|
130
|
+
error "Dashboard server not found at ${server_ts}"
|
|
131
|
+
info "Expected at: ${DIM}${repo_dir}/dashboard/server.ts${RESET}"
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
echo "$server_ts"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# ─── Is Running? ───────────────────────────────────────────────────────────
|
|
139
|
+
is_running() {
|
|
140
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
141
|
+
local pid
|
|
142
|
+
pid=$(cat "$PID_FILE" 2>/dev/null || true)
|
|
143
|
+
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
144
|
+
return 0
|
|
145
|
+
fi
|
|
146
|
+
fi
|
|
147
|
+
return 1
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get_pid() {
|
|
151
|
+
cat "$PID_FILE" 2>/dev/null || true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# ─── Start (Background) ───────────────────────────────────────────────────
|
|
155
|
+
dashboard_start_bg() {
|
|
156
|
+
local port="$1"
|
|
157
|
+
|
|
158
|
+
dashboard_header
|
|
159
|
+
|
|
160
|
+
if is_running; then
|
|
161
|
+
local pid
|
|
162
|
+
pid=$(get_pid)
|
|
163
|
+
error "Dashboard already running (PID: ${pid})"
|
|
164
|
+
info "Use ${CYAN}shipwright dashboard stop${RESET} to stop it first"
|
|
165
|
+
exit 1
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
check_bun
|
|
169
|
+
|
|
170
|
+
local server_ts
|
|
171
|
+
server_ts=$(find_server)
|
|
172
|
+
|
|
173
|
+
# Ensure directories exist
|
|
174
|
+
mkdir -p "$LOG_DIR" "$TEAMS_DIR"
|
|
175
|
+
|
|
176
|
+
info "Starting dashboard server on port ${CYAN}${port}${RESET}..."
|
|
177
|
+
|
|
178
|
+
# Start in background
|
|
179
|
+
nohup bun run "$server_ts" "$port" > "$LOG_FILE" 2>&1 &
|
|
180
|
+
local bg_pid=$!
|
|
181
|
+
|
|
182
|
+
# Write PID file
|
|
183
|
+
echo "$bg_pid" > "$PID_FILE"
|
|
184
|
+
|
|
185
|
+
# Wait briefly and verify process is alive
|
|
186
|
+
sleep 1
|
|
187
|
+
|
|
188
|
+
if kill -0 "$bg_pid" 2>/dev/null; then
|
|
189
|
+
success "Dashboard started (PID: ${bg_pid})"
|
|
190
|
+
echo ""
|
|
191
|
+
echo -e " ${BOLD}URL:${RESET} ${UNDERLINE}http://localhost:${port}${RESET}"
|
|
192
|
+
echo -e " ${BOLD}PID:${RESET} ${bg_pid}"
|
|
193
|
+
echo -e " ${BOLD}Log:${RESET} ${DIM}${LOG_FILE}${RESET}"
|
|
194
|
+
echo ""
|
|
195
|
+
info "Open in browser: ${DIM}shipwright dashboard open${RESET}"
|
|
196
|
+
info "Connect agents: ${DIM}shipwright connect start --url http://localhost:${port}${RESET}"
|
|
197
|
+
info "Stop server: ${DIM}shipwright dashboard stop${RESET}"
|
|
198
|
+
|
|
199
|
+
emit_event "dashboard.started" \
|
|
200
|
+
"pid=$bg_pid" \
|
|
201
|
+
"port=$port"
|
|
202
|
+
else
|
|
203
|
+
rm -f "$PID_FILE"
|
|
204
|
+
error "Dashboard failed to start"
|
|
205
|
+
info "Check logs: ${DIM}cat ${LOG_FILE}${RESET}"
|
|
206
|
+
exit 1
|
|
207
|
+
fi
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# ─── Start (Foreground) ───────────────────────────────────────────────────
|
|
211
|
+
dashboard_start_fg() {
|
|
212
|
+
local port="$1"
|
|
213
|
+
|
|
214
|
+
dashboard_header
|
|
215
|
+
|
|
216
|
+
if is_running; then
|
|
217
|
+
local pid
|
|
218
|
+
pid=$(get_pid)
|
|
219
|
+
error "Dashboard already running in background (PID: ${pid})"
|
|
220
|
+
info "Use ${CYAN}shipwright dashboard stop${RESET} to stop it first"
|
|
221
|
+
exit 1
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
check_bun
|
|
225
|
+
|
|
226
|
+
local server_ts
|
|
227
|
+
server_ts=$(find_server)
|
|
228
|
+
|
|
229
|
+
mkdir -p "$TEAMS_DIR"
|
|
230
|
+
|
|
231
|
+
info "Starting dashboard server on port ${CYAN}${port}${RESET} ${DIM}(foreground)${RESET}"
|
|
232
|
+
echo -e " ${BOLD}URL:${RESET} ${UNDERLINE}http://localhost:${port}${RESET}"
|
|
233
|
+
echo -e " ${BOLD}Connect:${RESET} ${DIM}shipwright connect start --url http://localhost:${port}${RESET}"
|
|
234
|
+
echo -e " ${DIM}Press Ctrl-C to stop${RESET}"
|
|
235
|
+
echo ""
|
|
236
|
+
|
|
237
|
+
emit_event "dashboard.started" \
|
|
238
|
+
"pid=$$" \
|
|
239
|
+
"port=$port" \
|
|
240
|
+
"mode=foreground"
|
|
241
|
+
|
|
242
|
+
# Run in foreground — exec replaces this process
|
|
243
|
+
exec bun run "$server_ts" "$port"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# ─── Stop ───────────────────────────────────────────────────────────────────
|
|
247
|
+
dashboard_stop() {
|
|
248
|
+
dashboard_header
|
|
249
|
+
|
|
250
|
+
if [[ ! -f "$PID_FILE" ]]; then
|
|
251
|
+
error "No dashboard PID file found"
|
|
252
|
+
info "Is the dashboard running?"
|
|
253
|
+
exit 1
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
local pid
|
|
257
|
+
pid=$(get_pid)
|
|
258
|
+
|
|
259
|
+
if [[ -z "$pid" ]]; then
|
|
260
|
+
error "Empty PID file"
|
|
261
|
+
rm -f "$PID_FILE"
|
|
262
|
+
exit 1
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
266
|
+
warn "Dashboard process (PID: ${pid}) is not running — cleaning up"
|
|
267
|
+
rm -f "$PID_FILE"
|
|
268
|
+
return 0
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
info "Stopping dashboard (PID: ${pid})..."
|
|
272
|
+
|
|
273
|
+
kill "$pid" 2>/dev/null || true
|
|
274
|
+
|
|
275
|
+
# Wait for graceful shutdown (up to 5s)
|
|
276
|
+
local wait_secs=0
|
|
277
|
+
while kill -0 "$pid" 2>/dev/null && [[ $wait_secs -lt 5 ]]; do
|
|
278
|
+
sleep 1
|
|
279
|
+
wait_secs=$((wait_secs + 1))
|
|
280
|
+
done
|
|
281
|
+
|
|
282
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
283
|
+
warn "Dashboard didn't stop gracefully — sending SIGKILL"
|
|
284
|
+
kill -9 "$pid" 2>/dev/null || true
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
rm -f "$PID_FILE"
|
|
288
|
+
|
|
289
|
+
success "Dashboard stopped"
|
|
290
|
+
|
|
291
|
+
emit_event "dashboard.stopped" \
|
|
292
|
+
"pid=$pid"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# ─── Status ─────────────────────────────────────────────────────────────────
|
|
296
|
+
dashboard_status() {
|
|
297
|
+
dashboard_header
|
|
298
|
+
|
|
299
|
+
if is_running; then
|
|
300
|
+
local pid
|
|
301
|
+
pid=$(get_pid)
|
|
302
|
+
echo -e " ${GREEN}●${RESET} ${BOLD}Running${RESET} ${DIM}(PID: ${pid})${RESET}"
|
|
303
|
+
|
|
304
|
+
# Try to get port from /proc or lsof
|
|
305
|
+
local port=""
|
|
306
|
+
if command -v lsof &>/dev/null; then
|
|
307
|
+
port=$(lsof -nP -iTCP -sTCP:LISTEN -a -p "$pid" 2>/dev/null | grep -oE ':\d+' | head -1 | tr -d ':' || true)
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
if [[ -n "$port" ]]; then
|
|
311
|
+
echo -e " ${BOLD}Port:${RESET} ${port}"
|
|
312
|
+
echo -e " ${BOLD}URL:${RESET} ${UNDERLINE}http://localhost:${port}${RESET}"
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
echo -e " ${BOLD}PID:${RESET} ${pid}"
|
|
316
|
+
|
|
317
|
+
# Uptime from PID file modification time
|
|
318
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
319
|
+
local pid_mtime
|
|
320
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
321
|
+
pid_mtime=$(stat -f %m "$PID_FILE" 2>/dev/null || echo "0")
|
|
322
|
+
else
|
|
323
|
+
pid_mtime=$(stat -c %Y "$PID_FILE" 2>/dev/null || echo "0")
|
|
324
|
+
fi
|
|
325
|
+
if [[ "$pid_mtime" -gt 0 ]]; then
|
|
326
|
+
local uptime_secs=$(( $(now_epoch) - pid_mtime ))
|
|
327
|
+
local uptime_str
|
|
328
|
+
if [[ "$uptime_secs" -ge 3600 ]]; then
|
|
329
|
+
uptime_str=$(printf "%dh %dm %ds" $((uptime_secs/3600)) $((uptime_secs%3600/60)) $((uptime_secs%60)))
|
|
330
|
+
elif [[ "$uptime_secs" -ge 60 ]]; then
|
|
331
|
+
uptime_str=$(printf "%dm %ds" $((uptime_secs/60)) $((uptime_secs%60)))
|
|
332
|
+
else
|
|
333
|
+
uptime_str=$(printf "%ds" "$uptime_secs")
|
|
334
|
+
fi
|
|
335
|
+
echo -e " ${BOLD}Uptime:${RESET} ${uptime_str}"
|
|
336
|
+
fi
|
|
337
|
+
fi
|
|
338
|
+
|
|
339
|
+
# Try to get health info from the server
|
|
340
|
+
if [[ -n "$port" ]]; then
|
|
341
|
+
local health
|
|
342
|
+
health=$(curl -s --max-time 2 "http://localhost:${port}/api/health" 2>/dev/null || true)
|
|
343
|
+
if [[ -n "$health" ]] && command -v jq &>/dev/null; then
|
|
344
|
+
local connections
|
|
345
|
+
connections=$(echo "$health" | jq -r '.connections // empty' 2>/dev/null || true)
|
|
346
|
+
if [[ -n "$connections" ]]; then
|
|
347
|
+
echo -e " ${BOLD}Clients:${RESET} ${connections} WebSocket connection(s)"
|
|
348
|
+
fi
|
|
349
|
+
fi
|
|
350
|
+
fi
|
|
351
|
+
|
|
352
|
+
echo -e " ${BOLD}Log:${RESET} ${DIM}${LOG_FILE}${RESET}"
|
|
353
|
+
else
|
|
354
|
+
echo -e " ${RED}●${RESET} ${BOLD}Stopped${RESET}"
|
|
355
|
+
|
|
356
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
357
|
+
warn "Stale PID file found — cleaning up"
|
|
358
|
+
rm -f "$PID_FILE"
|
|
359
|
+
fi
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
echo ""
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
# ─── Open ───────────────────────────────────────────────────────────────────
|
|
366
|
+
dashboard_open() {
|
|
367
|
+
local port="$1"
|
|
368
|
+
|
|
369
|
+
# Try to detect port from running server
|
|
370
|
+
if is_running; then
|
|
371
|
+
local pid
|
|
372
|
+
pid=$(get_pid)
|
|
373
|
+
if command -v lsof &>/dev/null; then
|
|
374
|
+
local detected_port
|
|
375
|
+
detected_port=$(lsof -nP -iTCP -sTCP:LISTEN -a -p "$pid" 2>/dev/null | grep -oE ':\d+' | head -1 | tr -d ':' || true)
|
|
376
|
+
if [[ -n "$detected_port" ]]; then
|
|
377
|
+
port="$detected_port"
|
|
378
|
+
fi
|
|
379
|
+
fi
|
|
380
|
+
else
|
|
381
|
+
warn "Dashboard doesn't appear to be running"
|
|
382
|
+
info "Start it first: ${DIM}shipwright dashboard start${RESET}"
|
|
383
|
+
exit 1
|
|
384
|
+
fi
|
|
385
|
+
|
|
386
|
+
local url="http://localhost:${port}"
|
|
387
|
+
info "Opening ${UNDERLINE}${url}${RESET}"
|
|
388
|
+
|
|
389
|
+
if open_url "$url" 2>/dev/null; then
|
|
390
|
+
: # opened via compat.sh
|
|
391
|
+
elif command -v powershell.exe &>/dev/null; then
|
|
392
|
+
powershell.exe -Command "Start-Process '$url'" 2>/dev/null
|
|
393
|
+
else
|
|
394
|
+
error "No browser opener found"
|
|
395
|
+
info "Open manually: ${UNDERLINE}${url}${RESET}"
|
|
396
|
+
exit 1
|
|
397
|
+
fi
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# ─── Parse Args ─────────────────────────────────────────────────────────────
|
|
401
|
+
SUBCOMMAND=""
|
|
402
|
+
PORT="$DEFAULT_PORT"
|
|
403
|
+
FOREGROUND=false
|
|
404
|
+
|
|
405
|
+
while [[ $# -gt 0 ]]; do
|
|
406
|
+
case "$1" in
|
|
407
|
+
start)
|
|
408
|
+
SUBCOMMAND="start"
|
|
409
|
+
shift
|
|
410
|
+
;;
|
|
411
|
+
stop)
|
|
412
|
+
SUBCOMMAND="stop"
|
|
413
|
+
shift
|
|
414
|
+
;;
|
|
415
|
+
status)
|
|
416
|
+
SUBCOMMAND="status"
|
|
417
|
+
shift
|
|
418
|
+
;;
|
|
419
|
+
open)
|
|
420
|
+
SUBCOMMAND="open"
|
|
421
|
+
shift
|
|
422
|
+
;;
|
|
423
|
+
help|--help|-h)
|
|
424
|
+
show_help
|
|
425
|
+
exit 0
|
|
426
|
+
;;
|
|
427
|
+
--port)
|
|
428
|
+
if [[ -z "${2:-}" ]]; then
|
|
429
|
+
error "--port requires a value"
|
|
430
|
+
exit 1
|
|
431
|
+
fi
|
|
432
|
+
PORT="$2"
|
|
433
|
+
shift 2
|
|
434
|
+
;;
|
|
435
|
+
--foreground|-f)
|
|
436
|
+
FOREGROUND=true
|
|
437
|
+
shift
|
|
438
|
+
;;
|
|
439
|
+
--version|-v)
|
|
440
|
+
echo "shipwright dashboard v${VERSION}"
|
|
441
|
+
exit 0
|
|
442
|
+
;;
|
|
443
|
+
*)
|
|
444
|
+
error "Unknown argument: ${1}"
|
|
445
|
+
echo ""
|
|
446
|
+
show_help
|
|
447
|
+
exit 1
|
|
448
|
+
;;
|
|
449
|
+
esac
|
|
450
|
+
done
|
|
451
|
+
|
|
452
|
+
# ─── Command Router ─────────────────────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
# No subcommand = foreground mode (like running `shipwright dashboard`)
|
|
455
|
+
if [[ -z "$SUBCOMMAND" ]]; then
|
|
456
|
+
dashboard_start_fg "$PORT"
|
|
457
|
+
exit 0
|
|
458
|
+
fi
|
|
459
|
+
|
|
460
|
+
case "$SUBCOMMAND" in
|
|
461
|
+
start)
|
|
462
|
+
if [[ "$FOREGROUND" == "true" ]]; then
|
|
463
|
+
dashboard_start_fg "$PORT"
|
|
464
|
+
else
|
|
465
|
+
dashboard_start_bg "$PORT"
|
|
466
|
+
fi
|
|
467
|
+
;;
|
|
468
|
+
stop)
|
|
469
|
+
dashboard_stop
|
|
470
|
+
;;
|
|
471
|
+
status)
|
|
472
|
+
dashboard_status
|
|
473
|
+
;;
|
|
474
|
+
open)
|
|
475
|
+
dashboard_open "$PORT"
|
|
476
|
+
;;
|
|
477
|
+
esac
|