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.
Files changed (105) hide show
  1. package/.claude/agents/code-reviewer.md +90 -0
  2. package/.claude/agents/devops-engineer.md +142 -0
  3. package/.claude/agents/pipeline-agent.md +80 -0
  4. package/.claude/agents/shell-script-specialist.md +150 -0
  5. package/.claude/agents/test-specialist.md +196 -0
  6. package/.claude/hooks/post-tool-use.sh +38 -0
  7. package/.claude/hooks/pre-tool-use.sh +25 -0
  8. package/.claude/hooks/session-started.sh +37 -0
  9. package/README.md +212 -814
  10. package/claude-code/CLAUDE.md.shipwright +54 -0
  11. package/claude-code/hooks/notify-idle.sh +2 -2
  12. package/claude-code/hooks/session-start.sh +24 -0
  13. package/claude-code/hooks/task-completed.sh +6 -2
  14. package/claude-code/settings.json.template +12 -0
  15. package/dashboard/public/app.js +4422 -0
  16. package/dashboard/public/index.html +816 -0
  17. package/dashboard/public/styles.css +4755 -0
  18. package/dashboard/server.ts +4315 -0
  19. package/docs/KNOWN-ISSUES.md +18 -10
  20. package/docs/TIPS.md +38 -26
  21. package/docs/patterns/README.md +33 -23
  22. package/package.json +9 -5
  23. package/scripts/adapters/iterm2-adapter.sh +1 -1
  24. package/scripts/adapters/tmux-adapter.sh +52 -23
  25. package/scripts/adapters/wezterm-adapter.sh +26 -14
  26. package/scripts/lib/compat.sh +200 -0
  27. package/scripts/lib/helpers.sh +72 -0
  28. package/scripts/postinstall.mjs +72 -13
  29. package/scripts/{cct → sw} +109 -21
  30. package/scripts/sw-adversarial.sh +274 -0
  31. package/scripts/sw-architecture-enforcer.sh +330 -0
  32. package/scripts/sw-checkpoint.sh +390 -0
  33. package/scripts/{cct-cleanup.sh → sw-cleanup.sh} +3 -1
  34. package/scripts/sw-connect.sh +619 -0
  35. package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
  36. package/scripts/{cct-daemon.sh → sw-daemon.sh} +2217 -204
  37. package/scripts/sw-dashboard.sh +477 -0
  38. package/scripts/sw-developer-simulation.sh +252 -0
  39. package/scripts/sw-docs.sh +635 -0
  40. package/scripts/sw-doctor.sh +907 -0
  41. package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
  42. package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
  43. package/scripts/sw-github-checks.sh +521 -0
  44. package/scripts/sw-github-deploy.sh +533 -0
  45. package/scripts/sw-github-graphql.sh +972 -0
  46. package/scripts/sw-heartbeat.sh +293 -0
  47. package/scripts/{cct-init.sh → sw-init.sh} +144 -11
  48. package/scripts/sw-intelligence.sh +1196 -0
  49. package/scripts/sw-jira.sh +643 -0
  50. package/scripts/sw-launchd.sh +364 -0
  51. package/scripts/sw-linear.sh +648 -0
  52. package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
  53. package/scripts/{cct-loop.sh → sw-loop.sh} +534 -44
  54. package/scripts/{cct-memory.sh → sw-memory.sh} +321 -38
  55. package/scripts/sw-patrol-meta.sh +417 -0
  56. package/scripts/sw-pipeline-composer.sh +455 -0
  57. package/scripts/{cct-pipeline.sh → sw-pipeline.sh} +2319 -178
  58. package/scripts/sw-predictive.sh +820 -0
  59. package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
  60. package/scripts/{cct-ps.sh → sw-ps.sh} +6 -4
  61. package/scripts/{cct-reaper.sh → sw-reaper.sh} +6 -4
  62. package/scripts/sw-remote.sh +687 -0
  63. package/scripts/sw-self-optimize.sh +947 -0
  64. package/scripts/sw-session.sh +519 -0
  65. package/scripts/sw-setup.sh +234 -0
  66. package/scripts/sw-status.sh +605 -0
  67. package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
  68. package/scripts/sw-tmux.sh +591 -0
  69. package/scripts/sw-tracker-jira.sh +277 -0
  70. package/scripts/sw-tracker-linear.sh +292 -0
  71. package/scripts/sw-tracker.sh +409 -0
  72. package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
  73. package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
  74. package/templates/pipelines/autonomous.json +27 -5
  75. package/templates/pipelines/full.json +12 -0
  76. package/templates/pipelines/standard.json +12 -0
  77. package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
  78. package/tmux/templates/accessibility.json +34 -0
  79. package/tmux/templates/api-design.json +35 -0
  80. package/tmux/templates/architecture.json +1 -0
  81. package/tmux/templates/bug-fix.json +9 -0
  82. package/tmux/templates/code-review.json +1 -0
  83. package/tmux/templates/compliance.json +36 -0
  84. package/tmux/templates/data-pipeline.json +36 -0
  85. package/tmux/templates/debt-paydown.json +34 -0
  86. package/tmux/templates/devops.json +1 -0
  87. package/tmux/templates/documentation.json +1 -0
  88. package/tmux/templates/exploration.json +1 -0
  89. package/tmux/templates/feature-dev.json +1 -0
  90. package/tmux/templates/full-stack.json +8 -0
  91. package/tmux/templates/i18n.json +34 -0
  92. package/tmux/templates/incident-response.json +36 -0
  93. package/tmux/templates/migration.json +1 -0
  94. package/tmux/templates/observability.json +35 -0
  95. package/tmux/templates/onboarding.json +33 -0
  96. package/tmux/templates/performance.json +35 -0
  97. package/tmux/templates/refactor.json +1 -0
  98. package/tmux/templates/release.json +35 -0
  99. package/tmux/templates/security-audit.json +8 -0
  100. package/tmux/templates/spike.json +34 -0
  101. package/tmux/templates/testing.json +1 -0
  102. package/tmux/tmux.conf +98 -9
  103. package/scripts/cct-doctor.sh +0 -414
  104. package/scripts/cct-session.sh +0 -284
  105. 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