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,619 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ sw-connect.sh — Sync local state to team dashboard ║
|
|
4
|
+
# ║ ║
|
|
5
|
+
# ║ Background heartbeat process that streams developer status, daemon ║
|
|
6
|
+
# ║ state, and events to a remote or local Shipwright dashboard. ║
|
|
7
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
10
|
+
|
|
11
|
+
VERSION="1.9.0"
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
|
|
14
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
15
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
16
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
17
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
18
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
19
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
20
|
+
RED='\033[38;2;248;113;113m' # error
|
|
21
|
+
DIM='\033[2m'
|
|
22
|
+
BOLD='\033[1m'
|
|
23
|
+
RESET='\033[0m'
|
|
24
|
+
|
|
25
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
26
|
+
# shellcheck source=lib/compat.sh
|
|
27
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
28
|
+
|
|
29
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
30
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
31
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
32
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
33
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
34
|
+
|
|
35
|
+
# ─── Constants ──────────────────────────────────────────────────────────────
|
|
36
|
+
SHIPWRIGHT_DIR="$HOME/.shipwright"
|
|
37
|
+
PID_FILE="$SHIPWRIGHT_DIR/connect.pid"
|
|
38
|
+
TEAM_CONFIG="$SHIPWRIGHT_DIR/team-config.json"
|
|
39
|
+
DAEMON_PID_FILE="$SHIPWRIGHT_DIR/daemon.pid"
|
|
40
|
+
DAEMON_STATE_FILE="$SHIPWRIGHT_DIR/daemon-state.json"
|
|
41
|
+
EVENTS_FILE="$SHIPWRIGHT_DIR/events.jsonl"
|
|
42
|
+
CONNECT_LOG="$SHIPWRIGHT_DIR/connect.log"
|
|
43
|
+
DEFAULT_URL="http://localhost:8767"
|
|
44
|
+
HEARTBEAT_INTERVAL=10
|
|
45
|
+
|
|
46
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
47
|
+
|
|
48
|
+
ensure_dir() {
|
|
49
|
+
mkdir -p "$SHIPWRIGHT_DIR"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ─── Resolve identity ──────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
resolve_developer_id() {
|
|
55
|
+
if [[ -n "${DEVELOPER_ID:-}" ]]; then
|
|
56
|
+
echo "$DEVELOPER_ID"
|
|
57
|
+
return
|
|
58
|
+
fi
|
|
59
|
+
local git_name
|
|
60
|
+
git_name="$(git config user.name 2>/dev/null || true)"
|
|
61
|
+
if [[ -n "$git_name" ]]; then
|
|
62
|
+
echo "$git_name"
|
|
63
|
+
return
|
|
64
|
+
fi
|
|
65
|
+
echo "${USER:-unknown}"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
resolve_machine_name() {
|
|
69
|
+
if [[ -n "${MACHINE_NAME:-}" ]]; then
|
|
70
|
+
echo "$MACHINE_NAME"
|
|
71
|
+
return
|
|
72
|
+
fi
|
|
73
|
+
hostname -s 2>/dev/null || echo "unknown"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resolve_dashboard_url() {
|
|
77
|
+
local url_flag="${1:-}"
|
|
78
|
+
|
|
79
|
+
# 1. --url flag
|
|
80
|
+
if [[ -n "$url_flag" ]]; then
|
|
81
|
+
echo "$url_flag"
|
|
82
|
+
return
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# 2. Environment variable
|
|
86
|
+
if [[ -n "${DASHBOARD_URL:-}" ]]; then
|
|
87
|
+
echo "$DASHBOARD_URL"
|
|
88
|
+
return
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# 3. team-config.json
|
|
92
|
+
if [[ -f "$TEAM_CONFIG" ]]; then
|
|
93
|
+
local cfg_url
|
|
94
|
+
cfg_url="$(jq -r '.dashboard_url // empty' "$TEAM_CONFIG" 2>/dev/null || true)"
|
|
95
|
+
if [[ -n "$cfg_url" ]]; then
|
|
96
|
+
echo "$cfg_url"
|
|
97
|
+
return
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# 4. Default
|
|
102
|
+
echo "$DEFAULT_URL"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# ─── Daemon state helpers ──────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
check_daemon_running() {
|
|
108
|
+
if [[ -f "$DAEMON_PID_FILE" ]]; then
|
|
109
|
+
local pid
|
|
110
|
+
pid="$(cat "$DAEMON_PID_FILE" 2>/dev/null || true)"
|
|
111
|
+
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
112
|
+
echo "$pid"
|
|
113
|
+
return
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
echo ""
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get_active_jobs() {
|
|
120
|
+
if [[ -f "$DAEMON_STATE_FILE" ]]; then
|
|
121
|
+
jq -c '[.active_jobs // [] | .[] | {issue: .issue, title: (.title // ""), stage: (.stage // "")}]' "$DAEMON_STATE_FILE" 2>/dev/null || echo "[]"
|
|
122
|
+
else
|
|
123
|
+
echo "[]"
|
|
124
|
+
fi
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get_queued_issues() {
|
|
128
|
+
if [[ -f "$DAEMON_STATE_FILE" ]]; then
|
|
129
|
+
jq -c '[.queued // [] | .[]]' "$DAEMON_STATE_FILE" 2>/dev/null || echo "[]"
|
|
130
|
+
else
|
|
131
|
+
echo "[]"
|
|
132
|
+
fi
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# ─── Events delta ──────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
get_new_events() {
|
|
138
|
+
local last_ts="$1"
|
|
139
|
+
|
|
140
|
+
if [[ ! -f "$EVENTS_FILE" ]]; then
|
|
141
|
+
echo "[]"
|
|
142
|
+
return
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
if [[ -z "$last_ts" || "$last_ts" == "null" ]]; then
|
|
146
|
+
# First sync — send last 20 events
|
|
147
|
+
tail -n 20 "$EVENTS_FILE" 2>/dev/null | jq -s '.' 2>/dev/null || echo "[]"
|
|
148
|
+
else
|
|
149
|
+
# Send events newer than last_ts (events use "ts" field)
|
|
150
|
+
jq -c --arg ts "$last_ts" 'select(.ts > $ts)' "$EVENTS_FILE" 2>/dev/null | jq -s '.' 2>/dev/null || echo "[]"
|
|
151
|
+
fi
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get_latest_event_ts() {
|
|
155
|
+
local events_json="$1"
|
|
156
|
+
echo "$events_json" | jq -r 'if length > 0 then .[-1].ts // "" else "" end' 2>/dev/null || echo ""
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# ─── Heartbeat loop ────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
run_heartbeat_loop() {
|
|
162
|
+
local dashboard_url="$1"
|
|
163
|
+
local developer_id="$2"
|
|
164
|
+
local machine_name="$3"
|
|
165
|
+
local full_hostname
|
|
166
|
+
full_hostname="$(hostname 2>/dev/null || echo "unknown")"
|
|
167
|
+
local platform
|
|
168
|
+
platform="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
169
|
+
|
|
170
|
+
local last_event_ts=""
|
|
171
|
+
local backoff=5
|
|
172
|
+
local max_backoff=30
|
|
173
|
+
local consecutive_failures=0
|
|
174
|
+
|
|
175
|
+
# Load invite token from team config if present (for auth)
|
|
176
|
+
local invite_token=""
|
|
177
|
+
if [[ -f "$TEAM_CONFIG" ]]; then
|
|
178
|
+
invite_token="$(jq -r '.invite_token // ""' "$TEAM_CONFIG" 2>/dev/null || true)"
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# Trap for graceful shutdown
|
|
182
|
+
trap 'send_disconnect "$dashboard_url" "$developer_id" "$machine_name"; exit 0' SIGTERM SIGINT
|
|
183
|
+
|
|
184
|
+
info "Connect heartbeat started (PID $$)"
|
|
185
|
+
info "Dashboard: ${dashboard_url}"
|
|
186
|
+
info "Developer: ${developer_id} @ ${machine_name}"
|
|
187
|
+
|
|
188
|
+
while true; do
|
|
189
|
+
# Collect state
|
|
190
|
+
local daemon_pid
|
|
191
|
+
daemon_pid="$(check_daemon_running)"
|
|
192
|
+
local daemon_running="false"
|
|
193
|
+
local daemon_pid_json="null"
|
|
194
|
+
if [[ -n "$daemon_pid" ]]; then
|
|
195
|
+
daemon_running="true"
|
|
196
|
+
daemon_pid_json="$daemon_pid"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
local active_jobs
|
|
200
|
+
active_jobs="$(get_active_jobs)"
|
|
201
|
+
local queued
|
|
202
|
+
queued="$(get_queued_issues)"
|
|
203
|
+
local events
|
|
204
|
+
events="$(get_new_events "$last_event_ts")"
|
|
205
|
+
|
|
206
|
+
# Build JSON payload with jq
|
|
207
|
+
local payload
|
|
208
|
+
payload="$(jq -n \
|
|
209
|
+
--arg developer_id "$developer_id" \
|
|
210
|
+
--arg machine_name "$machine_name" \
|
|
211
|
+
--arg hostname "$full_hostname" \
|
|
212
|
+
--arg platform "$platform" \
|
|
213
|
+
--argjson daemon_running "$daemon_running" \
|
|
214
|
+
--argjson daemon_pid "$daemon_pid_json" \
|
|
215
|
+
--argjson active_jobs "$active_jobs" \
|
|
216
|
+
--argjson queued "$queued" \
|
|
217
|
+
--argjson events "$events" \
|
|
218
|
+
--arg ts "$(now_iso)" \
|
|
219
|
+
--arg invite_token "$invite_token" \
|
|
220
|
+
'{
|
|
221
|
+
developer_id: $developer_id,
|
|
222
|
+
machine_name: $machine_name,
|
|
223
|
+
hostname: $hostname,
|
|
224
|
+
platform: $platform,
|
|
225
|
+
daemon_running: $daemon_running,
|
|
226
|
+
daemon_pid: $daemon_pid,
|
|
227
|
+
active_jobs: $active_jobs,
|
|
228
|
+
queued: $queued,
|
|
229
|
+
events: $events,
|
|
230
|
+
ts: $ts
|
|
231
|
+
} + (if $invite_token != "" then {invite_token: $invite_token} else {} end)')"
|
|
232
|
+
|
|
233
|
+
# Send heartbeat
|
|
234
|
+
local http_code
|
|
235
|
+
http_code="$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 \
|
|
236
|
+
-X POST \
|
|
237
|
+
-H "Content-Type: application/json" \
|
|
238
|
+
-d "$payload" \
|
|
239
|
+
"${dashboard_url}/api/connect/heartbeat" 2>/dev/null || echo "000")"
|
|
240
|
+
|
|
241
|
+
if [[ "$http_code" == "200" || "$http_code" == "201" || "$http_code" == "204" ]]; then
|
|
242
|
+
# Success — update event bookmark and reset backoff
|
|
243
|
+
local new_ts
|
|
244
|
+
new_ts="$(get_latest_event_ts "$events")"
|
|
245
|
+
if [[ -n "$new_ts" ]]; then
|
|
246
|
+
last_event_ts="$new_ts"
|
|
247
|
+
fi
|
|
248
|
+
backoff=5
|
|
249
|
+
consecutive_failures=0
|
|
250
|
+
else
|
|
251
|
+
consecutive_failures=$((consecutive_failures + 1))
|
|
252
|
+
if [[ "$consecutive_failures" -le 3 ]]; then
|
|
253
|
+
echo "$(now_iso) WARN: Dashboard unreachable (HTTP $http_code), retrying in ${backoff}s" >> "$CONNECT_LOG"
|
|
254
|
+
fi
|
|
255
|
+
sleep "$backoff"
|
|
256
|
+
# Exponential backoff: 5 → 10 → 20 → 30 (capped)
|
|
257
|
+
backoff=$((backoff * 2))
|
|
258
|
+
if [[ "$backoff" -gt "$max_backoff" ]]; then
|
|
259
|
+
backoff="$max_backoff"
|
|
260
|
+
fi
|
|
261
|
+
continue
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
sleep "$HEARTBEAT_INTERVAL"
|
|
265
|
+
done
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
send_disconnect() {
|
|
269
|
+
local dashboard_url="$1"
|
|
270
|
+
local developer_id="$2"
|
|
271
|
+
local machine_name="$3"
|
|
272
|
+
|
|
273
|
+
local payload
|
|
274
|
+
payload="$(jq -n \
|
|
275
|
+
--arg developer_id "$developer_id" \
|
|
276
|
+
--arg machine_name "$machine_name" \
|
|
277
|
+
'{developer_id: $developer_id, machine_name: $machine_name}')"
|
|
278
|
+
|
|
279
|
+
curl -s -o /dev/null --max-time 5 \
|
|
280
|
+
-X POST \
|
|
281
|
+
-H "Content-Type: application/json" \
|
|
282
|
+
-d "$payload" \
|
|
283
|
+
"${dashboard_url}/api/connect/disconnect" 2>/dev/null || true
|
|
284
|
+
|
|
285
|
+
info "Disconnected from dashboard"
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# ─── Start ──────────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
cmd_start() {
|
|
291
|
+
local url_flag=""
|
|
292
|
+
|
|
293
|
+
while [[ $# -gt 0 ]]; do
|
|
294
|
+
case "$1" in
|
|
295
|
+
--url)
|
|
296
|
+
url_flag="${2:-}"
|
|
297
|
+
shift 2
|
|
298
|
+
;;
|
|
299
|
+
--url=*)
|
|
300
|
+
url_flag="${1#--url=}"
|
|
301
|
+
shift
|
|
302
|
+
;;
|
|
303
|
+
--help|-h)
|
|
304
|
+
show_help
|
|
305
|
+
return 0
|
|
306
|
+
;;
|
|
307
|
+
*)
|
|
308
|
+
warn "Unknown option: $1"
|
|
309
|
+
shift
|
|
310
|
+
;;
|
|
311
|
+
esac
|
|
312
|
+
done
|
|
313
|
+
|
|
314
|
+
ensure_dir
|
|
315
|
+
|
|
316
|
+
# Check if already running
|
|
317
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
318
|
+
local existing_pid
|
|
319
|
+
existing_pid="$(cat "$PID_FILE" 2>/dev/null || true)"
|
|
320
|
+
if [[ -n "$existing_pid" ]] && kill -0 "$existing_pid" 2>/dev/null; then
|
|
321
|
+
error "Connect already running (PID ${existing_pid})"
|
|
322
|
+
echo -e " ${DIM}Stop it first: shipwright connect stop${RESET}"
|
|
323
|
+
return 1
|
|
324
|
+
fi
|
|
325
|
+
# Stale PID file — clean up
|
|
326
|
+
rm -f "$PID_FILE"
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
local dashboard_url
|
|
330
|
+
dashboard_url="$(resolve_dashboard_url "$url_flag")"
|
|
331
|
+
local developer_id
|
|
332
|
+
developer_id="$(resolve_developer_id)"
|
|
333
|
+
local machine_name
|
|
334
|
+
machine_name="$(resolve_machine_name)"
|
|
335
|
+
|
|
336
|
+
info "Starting connect to ${BOLD}${dashboard_url}${RESET}"
|
|
337
|
+
info "Developer: ${BOLD}${developer_id}${RESET} @ ${BOLD}${machine_name}${RESET}"
|
|
338
|
+
|
|
339
|
+
# Fork heartbeat loop to background
|
|
340
|
+
run_heartbeat_loop "$dashboard_url" "$developer_id" "$machine_name" >> "$CONNECT_LOG" 2>&1 &
|
|
341
|
+
local bg_pid=$!
|
|
342
|
+
|
|
343
|
+
# Write PID file atomically
|
|
344
|
+
local tmp_pid
|
|
345
|
+
tmp_pid="$(mktemp "$SHIPWRIGHT_DIR/.connect-pid.XXXXXX")"
|
|
346
|
+
echo "$bg_pid" > "$tmp_pid"
|
|
347
|
+
mv "$tmp_pid" "$PID_FILE"
|
|
348
|
+
|
|
349
|
+
success "Connect started (PID ${bg_pid})"
|
|
350
|
+
echo -e " ${DIM}Logs: ${CONNECT_LOG}${RESET}"
|
|
351
|
+
echo -e " ${DIM}Stop: shipwright connect stop${RESET}"
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# ─── Stop ───────────────────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
cmd_stop() {
|
|
357
|
+
if [[ ! -f "$PID_FILE" ]]; then
|
|
358
|
+
warn "Connect is not running (no PID file)"
|
|
359
|
+
return 0
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
local pid
|
|
363
|
+
pid="$(cat "$PID_FILE" 2>/dev/null || true)"
|
|
364
|
+
|
|
365
|
+
if [[ -z "$pid" ]]; then
|
|
366
|
+
warn "Empty PID file — cleaning up"
|
|
367
|
+
rm -f "$PID_FILE"
|
|
368
|
+
return 0
|
|
369
|
+
fi
|
|
370
|
+
|
|
371
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
372
|
+
kill "$pid" 2>/dev/null || true
|
|
373
|
+
# Wait briefly for graceful shutdown
|
|
374
|
+
local waited=0
|
|
375
|
+
while kill -0 "$pid" 2>/dev/null && [[ "$waited" -lt 5 ]]; do
|
|
376
|
+
sleep 1
|
|
377
|
+
waited=$((waited + 1))
|
|
378
|
+
done
|
|
379
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
380
|
+
kill -9 "$pid" 2>/dev/null || true
|
|
381
|
+
fi
|
|
382
|
+
success "Connect stopped (PID ${pid})"
|
|
383
|
+
else
|
|
384
|
+
warn "Process ${pid} not running — cleaning up stale PID file"
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
rm -f "$PID_FILE"
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# ─── Status ─────────────────────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
cmd_status() {
|
|
393
|
+
echo ""
|
|
394
|
+
echo -e "${CYAN}${BOLD} Shipwright Connect${RESET} ${DIM}v${VERSION}${RESET}"
|
|
395
|
+
echo -e "${DIM} ══════════════════════════════════════════${RESET}"
|
|
396
|
+
echo ""
|
|
397
|
+
|
|
398
|
+
local running=false
|
|
399
|
+
local pid=""
|
|
400
|
+
|
|
401
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
402
|
+
pid="$(cat "$PID_FILE" 2>/dev/null || true)"
|
|
403
|
+
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
404
|
+
running=true
|
|
405
|
+
fi
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
if [[ "$running" == "true" ]]; then
|
|
409
|
+
echo -e " Status: ${GREEN}${BOLD}connected${RESET} (PID ${pid})"
|
|
410
|
+
else
|
|
411
|
+
echo -e " Status: ${RED}${BOLD}disconnected${RESET}"
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
# Show dashboard URL
|
|
415
|
+
local dashboard_url
|
|
416
|
+
dashboard_url="$(resolve_dashboard_url "")"
|
|
417
|
+
echo -e " Dashboard: ${CYAN}${dashboard_url}${RESET}"
|
|
418
|
+
|
|
419
|
+
# Show identity
|
|
420
|
+
local developer_id
|
|
421
|
+
developer_id="$(resolve_developer_id)"
|
|
422
|
+
local machine_name
|
|
423
|
+
machine_name="$(resolve_machine_name)"
|
|
424
|
+
echo -e " Developer: ${BOLD}${developer_id}${RESET}"
|
|
425
|
+
echo -e " Machine: ${BOLD}${machine_name}${RESET}"
|
|
426
|
+
|
|
427
|
+
# Show uptime if running
|
|
428
|
+
if [[ "$running" == "true" && -n "$pid" ]]; then
|
|
429
|
+
local start_time
|
|
430
|
+
# macOS: ps -o lstart= gives human-readable start time
|
|
431
|
+
start_time="$(ps -o lstart= -p "$pid" 2>/dev/null || true)"
|
|
432
|
+
if [[ -n "$start_time" ]]; then
|
|
433
|
+
echo -e " Started: ${DIM}${start_time}${RESET}"
|
|
434
|
+
fi
|
|
435
|
+
fi
|
|
436
|
+
|
|
437
|
+
# Show team config if exists
|
|
438
|
+
if [[ -f "$TEAM_CONFIG" ]]; then
|
|
439
|
+
local team_name
|
|
440
|
+
team_name="$(jq -r '.team_name // "—"' "$TEAM_CONFIG" 2>/dev/null || echo "—")"
|
|
441
|
+
echo -e " Team: ${BOLD}${team_name}${RESET}"
|
|
442
|
+
fi
|
|
443
|
+
|
|
444
|
+
echo ""
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# ─── Join ───────────────────────────────────────────────────────────────────
|
|
448
|
+
|
|
449
|
+
cmd_join() {
|
|
450
|
+
local token=""
|
|
451
|
+
local url_flag=""
|
|
452
|
+
|
|
453
|
+
# Parse args: supports both positional and flag styles
|
|
454
|
+
# shipwright connect join <token>
|
|
455
|
+
# shipwright connect join --url <url> --token <token>
|
|
456
|
+
while [[ $# -gt 0 ]]; do
|
|
457
|
+
case "$1" in
|
|
458
|
+
--token) token="${2:-}"; shift 2 ;;
|
|
459
|
+
--token=*) token="${1#--token=}"; shift ;;
|
|
460
|
+
--url) url_flag="${2:-}"; shift 2 ;;
|
|
461
|
+
--url=*) url_flag="${1#--url=}"; shift ;;
|
|
462
|
+
--help|-h) show_help; return 0 ;;
|
|
463
|
+
-*) warn "Unknown flag: $1"; shift ;;
|
|
464
|
+
*)
|
|
465
|
+
# Positional: if no token yet, treat as token
|
|
466
|
+
if [[ -z "$token" ]]; then
|
|
467
|
+
token="$1"
|
|
468
|
+
fi
|
|
469
|
+
shift
|
|
470
|
+
;;
|
|
471
|
+
esac
|
|
472
|
+
done
|
|
473
|
+
|
|
474
|
+
if [[ -z "$token" ]]; then
|
|
475
|
+
error "Usage: shipwright connect join <token>"
|
|
476
|
+
echo -e " ${DIM}Or: shipwright connect join --url <dashboard-url> --token <token>${RESET}"
|
|
477
|
+
echo -e " ${DIM}Get a token from the dashboard: Settings → Team → Invite${RESET}"
|
|
478
|
+
return 1
|
|
479
|
+
fi
|
|
480
|
+
|
|
481
|
+
ensure_dir
|
|
482
|
+
|
|
483
|
+
# Determine dashboard URL to verify against
|
|
484
|
+
local verify_url
|
|
485
|
+
verify_url="${url_flag:-$(resolve_dashboard_url "")}"
|
|
486
|
+
|
|
487
|
+
info "Verifying invite token against ${BOLD}${verify_url}${RESET}..."
|
|
488
|
+
|
|
489
|
+
# Try invite token verification endpoint first
|
|
490
|
+
local response
|
|
491
|
+
response="$(curl -s --max-time 10 "${verify_url}/api/team/invite/${token}" 2>/dev/null || true)"
|
|
492
|
+
|
|
493
|
+
if [[ -z "$response" ]]; then
|
|
494
|
+
error "Could not reach dashboard at ${verify_url}"
|
|
495
|
+
echo -e " ${DIM}Make sure the dashboard is running: shipwright dashboard start${RESET}"
|
|
496
|
+
return 1
|
|
497
|
+
fi
|
|
498
|
+
|
|
499
|
+
# Check if token is valid
|
|
500
|
+
local valid
|
|
501
|
+
valid="$(echo "$response" | jq -r '.valid // false' 2>/dev/null || echo "false")"
|
|
502
|
+
|
|
503
|
+
if [[ "$valid" != "true" ]]; then
|
|
504
|
+
local err_msg
|
|
505
|
+
err_msg="$(echo "$response" | jq -r '.error // "Unknown error"' 2>/dev/null || echo "Unknown error")"
|
|
506
|
+
error "Invalid invite token: ${err_msg}"
|
|
507
|
+
return 1
|
|
508
|
+
fi
|
|
509
|
+
|
|
510
|
+
# Parse response
|
|
511
|
+
local join_url join_team
|
|
512
|
+
join_url="$(echo "$response" | jq -r '.dashboard_url // empty' 2>/dev/null || true)"
|
|
513
|
+
join_team="$(echo "$response" | jq -r '.team_name // empty' 2>/dev/null || true)"
|
|
514
|
+
|
|
515
|
+
if [[ -z "$join_url" ]]; then
|
|
516
|
+
# Fallback: use the URL we verified against
|
|
517
|
+
join_url="$verify_url"
|
|
518
|
+
fi
|
|
519
|
+
|
|
520
|
+
local developer_id
|
|
521
|
+
developer_id="$(resolve_developer_id)"
|
|
522
|
+
local machine_name
|
|
523
|
+
machine_name="$(resolve_machine_name)"
|
|
524
|
+
|
|
525
|
+
# Save team config atomically
|
|
526
|
+
local tmp_config
|
|
527
|
+
tmp_config="$(mktemp "$SHIPWRIGHT_DIR/.team-config.XXXXXX")"
|
|
528
|
+
|
|
529
|
+
jq -n \
|
|
530
|
+
--arg dashboard_url "$join_url" \
|
|
531
|
+
--arg team_name "${join_team:-}" \
|
|
532
|
+
--arg developer_id "$developer_id" \
|
|
533
|
+
--arg machine_name "$machine_name" \
|
|
534
|
+
--arg invite_token "$token" \
|
|
535
|
+
--argjson auto_connect true \
|
|
536
|
+
'{
|
|
537
|
+
dashboard_url: $dashboard_url,
|
|
538
|
+
team_name: $team_name,
|
|
539
|
+
developer_id: $developer_id,
|
|
540
|
+
machine_name: $machine_name,
|
|
541
|
+
invite_token: $invite_token,
|
|
542
|
+
auto_connect: $auto_connect
|
|
543
|
+
}' > "$tmp_config"
|
|
544
|
+
|
|
545
|
+
mv "$tmp_config" "$TEAM_CONFIG"
|
|
546
|
+
|
|
547
|
+
success "Joined team ${BOLD}${join_team:-unknown}${RESET}"
|
|
548
|
+
echo -e " ${DIM}Dashboard: ${join_url}${RESET}"
|
|
549
|
+
echo -e " ${DIM}Config: ${TEAM_CONFIG}${RESET}"
|
|
550
|
+
|
|
551
|
+
# Auto-start connection
|
|
552
|
+
info "Starting connection..."
|
|
553
|
+
cmd_start --url "$join_url"
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
# ─── Help ───────────────────────────────────────────────────────────────────
|
|
557
|
+
|
|
558
|
+
show_help() {
|
|
559
|
+
echo ""
|
|
560
|
+
echo -e "${CYAN}${BOLD} Shipwright Connect${RESET} ${DIM}v${VERSION}${RESET}"
|
|
561
|
+
echo -e "${DIM} ══════════════════════════════════════════${RESET}"
|
|
562
|
+
echo ""
|
|
563
|
+
echo -e " ${BOLD}USAGE${RESET}"
|
|
564
|
+
echo -e " shipwright connect <command> [options]"
|
|
565
|
+
echo ""
|
|
566
|
+
echo -e " ${BOLD}COMMANDS${RESET}"
|
|
567
|
+
echo -e " ${CYAN}start${RESET} [--url <url>] Start syncing local state to dashboard"
|
|
568
|
+
echo -e " ${CYAN}stop${RESET} Stop the connect process"
|
|
569
|
+
echo -e " ${CYAN}status${RESET} Show connection status"
|
|
570
|
+
echo -e " ${CYAN}join${RESET} <token> Join a team using an invite token"
|
|
571
|
+
echo ""
|
|
572
|
+
echo -e " ${BOLD}START OPTIONS${RESET}"
|
|
573
|
+
echo -e " --url <url> Dashboard URL (default: from team-config or localhost:8767)"
|
|
574
|
+
echo ""
|
|
575
|
+
echo -e " ${BOLD}IDENTITY${RESET} ${DIM}(auto-detected, override with env vars)${RESET}"
|
|
576
|
+
echo -e " DEVELOPER_ID Developer name (default: git user.name or \$USER)"
|
|
577
|
+
echo -e " MACHINE_NAME Machine name (default: hostname -s)"
|
|
578
|
+
echo -e " DASHBOARD_URL Dashboard URL (default: http://localhost:8767)"
|
|
579
|
+
echo ""
|
|
580
|
+
echo -e " ${BOLD}EXAMPLES${RESET}"
|
|
581
|
+
echo -e " ${DIM}# Start syncing to local dashboard${RESET}"
|
|
582
|
+
echo -e " shipwright connect start"
|
|
583
|
+
echo ""
|
|
584
|
+
echo -e " ${DIM}# Connect to a remote dashboard${RESET}"
|
|
585
|
+
echo -e " shipwright connect start --url http://team-server:8767"
|
|
586
|
+
echo ""
|
|
587
|
+
echo -e " ${DIM}# Join a team with an invite token${RESET}"
|
|
588
|
+
echo -e " shipwright connect join abc123"
|
|
589
|
+
echo ""
|
|
590
|
+
echo -e " ${DIM}# Check connection status${RESET}"
|
|
591
|
+
echo -e " shipwright connect status"
|
|
592
|
+
echo ""
|
|
593
|
+
echo -e " ${DIM}# Stop syncing${RESET}"
|
|
594
|
+
echo -e " shipwright connect stop"
|
|
595
|
+
echo ""
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
# ─── Command Router ────────────────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
main() {
|
|
601
|
+
local cmd="${1:-help}"
|
|
602
|
+
shift 2>/dev/null || true
|
|
603
|
+
|
|
604
|
+
case "$cmd" in
|
|
605
|
+
start) cmd_start "$@" ;;
|
|
606
|
+
stop) cmd_stop ;;
|
|
607
|
+
status) cmd_status ;;
|
|
608
|
+
join) cmd_join "$@" ;;
|
|
609
|
+
help|--help|-h) show_help ;;
|
|
610
|
+
*)
|
|
611
|
+
error "Unknown command: ${cmd}"
|
|
612
|
+
echo ""
|
|
613
|
+
show_help
|
|
614
|
+
exit 1
|
|
615
|
+
;;
|
|
616
|
+
esac
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
main "$@"
|