shipwright-cli 1.10.0 → 2.0.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 (108) hide show
  1. package/README.md +114 -36
  2. package/completions/_shipwright +212 -32
  3. package/completions/shipwright.bash +97 -25
  4. package/docs/strategy/01-market-research.md +619 -0
  5. package/docs/strategy/02-mission-and-brand.md +587 -0
  6. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  7. package/docs/strategy/QUICK-START.txt +289 -0
  8. package/docs/strategy/README.md +172 -0
  9. package/package.json +4 -2
  10. package/scripts/sw +208 -1
  11. package/scripts/sw-activity.sh +500 -0
  12. package/scripts/sw-adaptive.sh +925 -0
  13. package/scripts/sw-adversarial.sh +1 -1
  14. package/scripts/sw-architecture-enforcer.sh +1 -1
  15. package/scripts/sw-auth.sh +613 -0
  16. package/scripts/sw-autonomous.sh +664 -0
  17. package/scripts/sw-changelog.sh +704 -0
  18. package/scripts/sw-checkpoint.sh +1 -1
  19. package/scripts/sw-ci.sh +602 -0
  20. package/scripts/sw-cleanup.sh +1 -1
  21. package/scripts/sw-code-review.sh +637 -0
  22. package/scripts/sw-connect.sh +1 -1
  23. package/scripts/sw-context.sh +605 -0
  24. package/scripts/sw-cost.sh +1 -1
  25. package/scripts/sw-daemon.sh +432 -130
  26. package/scripts/sw-dashboard.sh +1 -1
  27. package/scripts/sw-db.sh +540 -0
  28. package/scripts/sw-decompose.sh +539 -0
  29. package/scripts/sw-deps.sh +551 -0
  30. package/scripts/sw-developer-simulation.sh +1 -1
  31. package/scripts/sw-discovery.sh +412 -0
  32. package/scripts/sw-docs-agent.sh +539 -0
  33. package/scripts/sw-docs.sh +1 -1
  34. package/scripts/sw-doctor.sh +59 -1
  35. package/scripts/sw-dora.sh +615 -0
  36. package/scripts/sw-durable.sh +710 -0
  37. package/scripts/sw-e2e-orchestrator.sh +535 -0
  38. package/scripts/sw-eventbus.sh +393 -0
  39. package/scripts/sw-feedback.sh +471 -0
  40. package/scripts/sw-fix.sh +1 -1
  41. package/scripts/sw-fleet-discover.sh +567 -0
  42. package/scripts/sw-fleet-viz.sh +404 -0
  43. package/scripts/sw-fleet.sh +8 -1
  44. package/scripts/sw-github-app.sh +596 -0
  45. package/scripts/sw-github-checks.sh +1 -1
  46. package/scripts/sw-github-deploy.sh +1 -1
  47. package/scripts/sw-github-graphql.sh +1 -1
  48. package/scripts/sw-guild.sh +569 -0
  49. package/scripts/sw-heartbeat.sh +1 -1
  50. package/scripts/sw-hygiene.sh +559 -0
  51. package/scripts/sw-incident.sh +617 -0
  52. package/scripts/sw-init.sh +88 -1
  53. package/scripts/sw-instrument.sh +699 -0
  54. package/scripts/sw-intelligence.sh +1 -1
  55. package/scripts/sw-jira.sh +1 -1
  56. package/scripts/sw-launchd.sh +363 -28
  57. package/scripts/sw-linear.sh +1 -1
  58. package/scripts/sw-logs.sh +1 -1
  59. package/scripts/sw-loop.sh +64 -3
  60. package/scripts/sw-memory.sh +1 -1
  61. package/scripts/sw-mission-control.sh +487 -0
  62. package/scripts/sw-model-router.sh +545 -0
  63. package/scripts/sw-otel.sh +596 -0
  64. package/scripts/sw-oversight.sh +689 -0
  65. package/scripts/sw-pipeline-composer.sh +1 -1
  66. package/scripts/sw-pipeline-vitals.sh +1 -1
  67. package/scripts/sw-pipeline.sh +687 -24
  68. package/scripts/sw-pm.sh +693 -0
  69. package/scripts/sw-pr-lifecycle.sh +522 -0
  70. package/scripts/sw-predictive.sh +1 -1
  71. package/scripts/sw-prep.sh +1 -1
  72. package/scripts/sw-ps.sh +1 -1
  73. package/scripts/sw-public-dashboard.sh +798 -0
  74. package/scripts/sw-quality.sh +595 -0
  75. package/scripts/sw-reaper.sh +1 -1
  76. package/scripts/sw-recruit.sh +573 -0
  77. package/scripts/sw-regression.sh +642 -0
  78. package/scripts/sw-release-manager.sh +736 -0
  79. package/scripts/sw-release.sh +706 -0
  80. package/scripts/sw-remote.sh +1 -1
  81. package/scripts/sw-replay.sh +520 -0
  82. package/scripts/sw-retro.sh +691 -0
  83. package/scripts/sw-scale.sh +444 -0
  84. package/scripts/sw-security-audit.sh +505 -0
  85. package/scripts/sw-self-optimize.sh +1 -1
  86. package/scripts/sw-session.sh +1 -1
  87. package/scripts/sw-setup.sh +1 -1
  88. package/scripts/sw-standup.sh +712 -0
  89. package/scripts/sw-status.sh +1 -1
  90. package/scripts/sw-strategic.sh +658 -0
  91. package/scripts/sw-stream.sh +450 -0
  92. package/scripts/sw-swarm.sh +583 -0
  93. package/scripts/sw-team-stages.sh +511 -0
  94. package/scripts/sw-templates.sh +1 -1
  95. package/scripts/sw-testgen.sh +515 -0
  96. package/scripts/sw-tmux-pipeline.sh +554 -0
  97. package/scripts/sw-tmux.sh +1 -1
  98. package/scripts/sw-trace.sh +485 -0
  99. package/scripts/sw-tracker-github.sh +188 -0
  100. package/scripts/sw-tracker-jira.sh +172 -0
  101. package/scripts/sw-tracker-linear.sh +251 -0
  102. package/scripts/sw-tracker.sh +117 -2
  103. package/scripts/sw-triage.sh +603 -0
  104. package/scripts/sw-upgrade.sh +1 -1
  105. package/scripts/sw-ux.sh +677 -0
  106. package/scripts/sw-webhook.sh +627 -0
  107. package/scripts/sw-widgets.sh +530 -0
  108. package/scripts/sw-worktree.sh +1 -1
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="1.10.0"
9
+ VERSION="2.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="1.10.0"
9
+ VERSION="2.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -0,0 +1,613 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright auth — GitHub OAuth Authentication ║
4
+ # ║ Device flow · Token management · Session validation · Multi-user ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
+
9
+ VERSION="2.0.0"
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
+
13
+ # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
14
+ CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
15
+ PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
16
+ BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
17
+ GREEN='\033[38;2;74;222;128m' # success
18
+ YELLOW='\033[38;2;250;204;21m' # warning
19
+ RED='\033[38;2;248;113;113m' # error
20
+ DIM='\033[2m'
21
+ BOLD='\033[1m'
22
+ RESET='\033[0m'
23
+
24
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
25
+ # shellcheck source=lib/compat.sh
26
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
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
+ # ─── Structured Event Log ──────────────────────────────────────────────────
38
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
39
+
40
+ emit_event() {
41
+ local event_type="$1"
42
+ shift
43
+ local json_fields=""
44
+ for kv in "$@"; do
45
+ local key="${kv%%=*}"
46
+ local val="${kv#*=}"
47
+ if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
48
+ json_fields="${json_fields},\"${key}\":${val}"
49
+ else
50
+ local escaped_val
51
+ escaped_val=$(printf '%s' "$val" | jq -Rs '.' 2>/dev/null || printf '"%s"' "${val//\"/\\\"}")
52
+ json_fields="${json_fields},\"${key}\":${escaped_val}"
53
+ fi
54
+ done
55
+ mkdir -p "${HOME}/.shipwright"
56
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
57
+ }
58
+
59
+ # ─── Auth Storage ───────────────────────────────────────────────────────────
60
+ AUTH_FILE="${HOME}/.shipwright/auth.json"
61
+ DEVICE_FLOW_ENDPOINT="https://github.com/login/device"
62
+ API_ENDPOINT="https://api.github.com"
63
+ OAUTH_CLIENT_ID="${GITHUB_OAUTH_CLIENT_ID:-Iv1.d3f6a7e8c9b2a1d4}" # Shipwright app ID
64
+ OAUTH_TIMEOUT=900 # 15 minutes
65
+
66
+ # Ensure auth storage directory exists
67
+ ensure_auth_dir() {
68
+ mkdir -p "${HOME}/.shipwright"
69
+ if [[ ! -f "$AUTH_FILE" ]]; then
70
+ echo '{"users":[],"active_user":null}' > "$AUTH_FILE"
71
+ chmod 600 "$AUTH_FILE"
72
+ fi
73
+ }
74
+
75
+ # ─── Device Flow (GitHub OAuth) ──────────────────────────────────────────────
76
+ # Implements GitHub OAuth device flow without requiring a web server.
77
+ # Returns device_code, user_code, interval, expires_in
78
+ initiate_device_flow() {
79
+ local response
80
+ response=$(curl -s -X POST \
81
+ -H "Accept: application/json" \
82
+ "${API_ENDPOINT}/login/device/code" \
83
+ -d "client_id=${OAUTH_CLIENT_ID}&scope=read:user%20user:email" 2>/dev/null) || {
84
+ error "Failed to contact GitHub OAuth endpoint"
85
+ return 1
86
+ }
87
+
88
+ # Check for errors in response
89
+ if echo "$response" | jq -e '.error' >/dev/null 2>&1; then
90
+ local err
91
+ err=$(echo "$response" | jq -r '.error_description // .error')
92
+ error "GitHub OAuth error: $err"
93
+ return 1
94
+ fi
95
+
96
+ # Extract device code, user code, interval
97
+ local device_code user_code interval expires_in
98
+ device_code=$(echo "$response" | jq -r '.device_code')
99
+ user_code=$(echo "$response" | jq -r '.user_code')
100
+ interval=$(echo "$response" | jq -r '.interval // 5')
101
+ expires_in=$(echo "$response" | jq -r '.expires_in // 900')
102
+
103
+ # Output as key=value pairs for easy sourcing
104
+ echo "DEVICE_CODE=${device_code}"
105
+ echo "USER_CODE=${user_code}"
106
+ echo "INTERVAL=${interval}"
107
+ echo "EXPIRES_IN=${expires_in}"
108
+ }
109
+
110
+ # Poll for token after user authorizes at github.com/login/device
111
+ poll_for_token() {
112
+ local device_code="$1"
113
+ local interval="$2"
114
+ local expires_in="$3"
115
+ local start_time
116
+ start_time=$(now_epoch)
117
+
118
+ while true; do
119
+ local elapsed
120
+ elapsed=$(($(now_epoch) - start_time))
121
+
122
+ if [[ $elapsed -gt $expires_in ]]; then
123
+ error "Device code expired. Authorization timeout."
124
+ return 1
125
+ fi
126
+
127
+ local response
128
+ response=$(curl -s -X POST \
129
+ -H "Accept: application/json" \
130
+ "${API_ENDPOINT}/login/oauth/access_token" \
131
+ -d "client_id=${OAUTH_CLIENT_ID}&device_code=${device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code" 2>/dev/null) || {
132
+ warn "Failed to reach GitHub. Retrying..."
133
+ sleep "$interval"
134
+ continue
135
+ }
136
+
137
+ # Check if authorization pending
138
+ if echo "$response" | jq -e '.error' >/dev/null 2>&1; then
139
+ local error_code
140
+ error_code=$(echo "$response" | jq -r '.error')
141
+
142
+ if [[ "$error_code" == "authorization_pending" ]]; then
143
+ # User hasn't authorized yet, wait and retry
144
+ sleep "$interval"
145
+ continue
146
+ elif [[ "$error_code" == "expired_token" ]]; then
147
+ error "Device code expired. Please try again."
148
+ return 1
149
+ else
150
+ # Other error (bad request, etc.)
151
+ local err_desc
152
+ err_desc=$(echo "$response" | jq -r '.error_description // .error')
153
+ error "GitHub OAuth error: $err_desc"
154
+ return 1
155
+ fi
156
+ fi
157
+
158
+ # Success! Extract token
159
+ local access_token
160
+ access_token=$(echo "$response" | jq -r '.access_token')
161
+ if [[ -z "$access_token" ]] || [[ "$access_token" == "null" ]]; then
162
+ error "No access token in response"
163
+ return 1
164
+ fi
165
+
166
+ echo "$access_token"
167
+ return 0
168
+ done
169
+ }
170
+
171
+ # Fetch user info from GitHub
172
+ fetch_user_info() {
173
+ local token="$1"
174
+ local response
175
+
176
+ response=$(curl -s -H "Authorization: Bearer ${token}" \
177
+ -H "Accept: application/vnd.github.v3+json" \
178
+ "${API_ENDPOINT}/user" 2>/dev/null) || {
179
+ error "Failed to fetch user info"
180
+ return 1
181
+ }
182
+
183
+ if echo "$response" | jq -e '.message' >/dev/null 2>&1; then
184
+ local err
185
+ err=$(echo "$response" | jq -r '.message')
186
+ error "GitHub API error: $err"
187
+ return 1
188
+ fi
189
+
190
+ echo "$response"
191
+ }
192
+
193
+ # Validate token by hitting /user endpoint
194
+ validate_token() {
195
+ local token="$1"
196
+
197
+ if ! curl -s -f \
198
+ -H "Authorization: Bearer ${token}" \
199
+ -H "Accept: application/vnd.github.v3+json" \
200
+ "${API_ENDPOINT}/user" >/dev/null 2>&1; then
201
+ return 1
202
+ fi
203
+ return 0
204
+ }
205
+
206
+ # Revoke a GitHub token
207
+ revoke_token() {
208
+ local token="$1"
209
+
210
+ # GitHub revoke endpoint requires basic auth with client_id and client_secret
211
+ # For now, we just remove it locally (tokens expire naturally)
212
+ return 0
213
+ }
214
+
215
+ # ─── Token Management ────────────────────────────────────────────────────────
216
+ # Store user token in auth.json
217
+ store_user() {
218
+ local login="$1"
219
+ local token="$2"
220
+ local user_json="$3"
221
+
222
+ ensure_auth_dir
223
+
224
+ local temp_file
225
+ temp_file=$(mktemp)
226
+ trap "rm -f '$temp_file'" RETURN
227
+
228
+ local updated
229
+ updated=$(jq --arg login "$login" \
230
+ --arg token "$token" \
231
+ --argjson user "$user_json" \
232
+ '.users |= map(select(.login != $login)) | .users += [{login: $login, token: $token, user: $user, stored_at: now | todate}] | .active_user = $login' \
233
+ "$AUTH_FILE")
234
+
235
+ echo "$updated" | jq '.' > "$temp_file" 2>/dev/null || {
236
+ error "Failed to update auth file"
237
+ return 1
238
+ }
239
+
240
+ mv "$temp_file" "$AUTH_FILE"
241
+ chmod 600 "$AUTH_FILE"
242
+ success "User $login authenticated and stored"
243
+ }
244
+
245
+ # Get active user
246
+ get_active_user() {
247
+ ensure_auth_dir
248
+ jq -r '.active_user // empty' "$AUTH_FILE" 2>/dev/null || echo ""
249
+ }
250
+
251
+ # Get all users
252
+ list_users() {
253
+ ensure_auth_dir
254
+ jq -r '.users[] | .login' "$AUTH_FILE" 2>/dev/null || true
255
+ }
256
+
257
+ # Switch active user
258
+ switch_user() {
259
+ local login="$1"
260
+ ensure_auth_dir
261
+
262
+ # Verify user exists
263
+ if ! jq -e ".users[] | select(.login == \"${login}\")" "$AUTH_FILE" >/dev/null 2>&1; then
264
+ error "User not found: $login"
265
+ return 1
266
+ fi
267
+
268
+ local temp_file
269
+ temp_file=$(mktemp)
270
+ trap "rm -f '$temp_file'" RETURN
271
+
272
+ jq --arg login "$login" '.active_user = $login' "$AUTH_FILE" > "$temp_file"
273
+ mv "$temp_file" "$AUTH_FILE"
274
+ chmod 600 "$AUTH_FILE"
275
+ success "Switched to user: $login"
276
+ }
277
+
278
+ # Remove user
279
+ remove_user() {
280
+ local login="$1"
281
+ ensure_auth_dir
282
+
283
+ local temp_file
284
+ temp_file=$(mktemp)
285
+ trap "rm -f '$temp_file'" RETURN
286
+
287
+ jq --arg login "$login" \
288
+ '.users |= map(select(.login != $login)) |
289
+ if .active_user == $login then .active_user = null else . end' \
290
+ "$AUTH_FILE" > "$temp_file"
291
+ mv "$temp_file" "$AUTH_FILE"
292
+ chmod 600 "$AUTH_FILE"
293
+ success "User removed: $login"
294
+ }
295
+
296
+ # Get token for user (or active user)
297
+ get_token() {
298
+ local login="${1:-}"
299
+ ensure_auth_dir
300
+
301
+ if [[ -z "$login" ]]; then
302
+ login=$(get_active_user)
303
+ if [[ -z "$login" ]]; then
304
+ error "No user logged in"
305
+ return 1
306
+ fi
307
+ fi
308
+
309
+ local token
310
+ token=$(jq -r ".users[] | select(.login == \"${login}\") | .token" "$AUTH_FILE" 2>/dev/null)
311
+
312
+ if [[ -z "$token" ]] || [[ "$token" == "null" ]]; then
313
+ error "No token found for user: $login"
314
+ return 1
315
+ fi
316
+
317
+ echo "$token"
318
+ }
319
+
320
+ # Get user info (login, name, avatar_url, email)
321
+ get_user_info() {
322
+ local login="${1:-}"
323
+ ensure_auth_dir
324
+
325
+ if [[ -z "$login" ]]; then
326
+ login=$(get_active_user)
327
+ if [[ -z "$login" ]]; then
328
+ error "No user logged in"
329
+ return 1
330
+ fi
331
+ fi
332
+
333
+ jq -r ".users[] | select(.login == \"${login}\") | .user" "$AUTH_FILE" 2>/dev/null || {
334
+ error "User info not found"
335
+ return 1
336
+ }
337
+ }
338
+
339
+ # ─── Middleware Helpers ──────────────────────────────────────────────────────
340
+ # Output auth header for use in other tools
341
+ output_auth_header() {
342
+ local login="${1:-}"
343
+ local token
344
+
345
+ token=$(get_token "$login") || return 1
346
+ echo "Authorization: Bearer ${token}"
347
+ }
348
+
349
+ # Output user info in dashboard-friendly format
350
+ output_user_json() {
351
+ local login="${1:-}"
352
+ local user_info
353
+
354
+ user_info=$(get_user_info "$login") || return 1
355
+ echo "$user_info"
356
+ }
357
+
358
+ # ─── Command Handlers ────────────────────────────────────────────────────────
359
+ cmd_login() {
360
+ info "Starting GitHub OAuth device flow..."
361
+
362
+ # Initiate device flow
363
+ local device_flow_vars
364
+ device_flow_vars=$(initiate_device_flow) || return 1
365
+
366
+ # Source the variables
367
+ eval "$device_flow_vars"
368
+
369
+ info "Visit: ${CYAN}${DEVICE_FLOW_ENDPOINT}${RESET}"
370
+ info "Enter code: ${BOLD}${USER_CODE}${RESET}"
371
+ echo ""
372
+ warn "Waiting for authorization (expires in ${EXPIRES_IN}s)..."
373
+
374
+ # Poll for token
375
+ local access_token
376
+ access_token=$(poll_for_token "$DEVICE_CODE" "$INTERVAL" "$EXPIRES_IN") || return 1
377
+
378
+ info "Authorization successful! Fetching user info..."
379
+
380
+ # Fetch user info
381
+ local user_info
382
+ user_info=$(fetch_user_info "$access_token") || return 1
383
+
384
+ local login
385
+ login=$(echo "$user_info" | jq -r '.login')
386
+
387
+ # Store user
388
+ store_user "$login" "$access_token" "$user_info"
389
+ emit_event "auth_login" "user=${login}"
390
+ success "Logged in as ${CYAN}${login}${RESET}"
391
+ }
392
+
393
+ cmd_logout() {
394
+ local login="${1:-}"
395
+ ensure_auth_dir
396
+
397
+ if [[ -z "$login" ]]; then
398
+ login=$(get_active_user)
399
+ if [[ -z "$login" ]]; then
400
+ error "No user logged in"
401
+ return 1
402
+ fi
403
+ fi
404
+
405
+ # Revoke token
406
+ local token
407
+ token=$(get_token "$login") || return 1
408
+ revoke_token "$token"
409
+
410
+ # Remove user from storage
411
+ remove_user "$login"
412
+ emit_event "auth_logout" "user=${login}"
413
+ success "Logged out and token revoked"
414
+ }
415
+
416
+ cmd_status() {
417
+ ensure_auth_dir
418
+
419
+ local active
420
+ active=$(get_active_user)
421
+
422
+ if [[ -z "$active" ]]; then
423
+ warn "Not logged in"
424
+ return 1
425
+ fi
426
+
427
+ local user_info
428
+ user_info=$(get_user_info "$active") || return 1
429
+
430
+ local login name avatar_url email
431
+ login=$(echo "$user_info" | jq -r '.login')
432
+ name=$(echo "$user_info" | jq -r '.name // "N/A"')
433
+ avatar_url=$(echo "$user_info" | jq -r '.avatar_url // "N/A"')
434
+ email=$(echo "$user_info" | jq -r '.email // "N/A"')
435
+
436
+ info "Authenticated as:"
437
+ echo -e " ${CYAN}Login${RESET}: ${login}"
438
+ echo -e " ${CYAN}Name${RESET}: ${name}"
439
+ echo -e " ${CYAN}Email${RESET}: ${email}"
440
+ echo -e " ${CYAN}Avatar${RESET}: ${avatar_url}"
441
+
442
+ # Check token validity
443
+ local token
444
+ token=$(get_token "$active")
445
+ if validate_token "$token"; then
446
+ success "Token is valid"
447
+ else
448
+ warn "Token is invalid or expired"
449
+ fi
450
+ }
451
+
452
+ cmd_token() {
453
+ local login="${1:-}"
454
+ get_token "$login"
455
+ }
456
+
457
+ cmd_user() {
458
+ local login="${1:-}"
459
+ local format="${2:-json}"
460
+
461
+ local user_info
462
+ user_info=$(get_user_info "$login") || return 1
463
+
464
+ if [[ "$format" == "json" ]]; then
465
+ echo "$user_info" | jq '.'
466
+ else
467
+ # Simple text format
468
+ echo "$user_info" | jq -r '
469
+ "Login: \(.login)\n" +
470
+ "Name: \(.name // "N/A")\n" +
471
+ "Email: \(.email // "N/A")\n" +
472
+ "Avatar: \(.avatar_url // "N/A")\n" +
473
+ "Company: \(.company // "N/A")\n" +
474
+ "Location: \(.location // "N/A")\n" +
475
+ "Bio: \(.bio // "N/A")"
476
+ '
477
+ fi
478
+ }
479
+
480
+ cmd_refresh() {
481
+ local login="${1:-}"
482
+ ensure_auth_dir
483
+
484
+ if [[ -z "$login" ]]; then
485
+ login=$(get_active_user)
486
+ if [[ -z "$login" ]]; then
487
+ error "No user logged in"
488
+ return 1
489
+ fi
490
+ fi
491
+
492
+ info "Validating token for ${login}..."
493
+ local token
494
+ token=$(get_token "$login") || return 1
495
+
496
+ if ! validate_token "$token"; then
497
+ error "Token invalid or expired. Please login again."
498
+ remove_user "$login"
499
+ return 1
500
+ fi
501
+
502
+ # Refresh user info
503
+ local user_info
504
+ user_info=$(fetch_user_info "$token") || return 1
505
+
506
+ # Re-store with updated info
507
+ store_user "$login" "$token" "$user_info"
508
+ success "Token and user info refreshed"
509
+ }
510
+
511
+ cmd_users() {
512
+ ensure_auth_dir
513
+
514
+ local users
515
+ users=$(list_users)
516
+
517
+ if [[ -z "$users" ]]; then
518
+ warn "No users authenticated"
519
+ return 1
520
+ fi
521
+
522
+ local active
523
+ active=$(get_active_user)
524
+
525
+ info "Authenticated users:"
526
+ while IFS= read -r user; do
527
+ if [[ "$user" == "$active" ]]; then
528
+ echo -e " ${GREEN}✓${RESET} ${user} ${DIM}(active)${RESET}"
529
+ else
530
+ echo -e " ${CYAN}•${RESET} ${user}"
531
+ fi
532
+ done <<< "$users"
533
+ }
534
+
535
+ cmd_switch() {
536
+ local login="$1"
537
+ if [[ -z "$login" ]]; then
538
+ error "Usage: shipwright auth switch <login>"
539
+ return 1
540
+ fi
541
+ switch_user "$login"
542
+ }
543
+
544
+ cmd_help() {
545
+ cat << 'EOF'
546
+ Usage: shipwright auth <command> [options]
547
+
548
+ Commands:
549
+ login Start GitHub OAuth device flow
550
+ logout [user] Revoke token and remove user (or active user)
551
+ status Show current auth status
552
+ token [user] Output current access token (for piping)
553
+ user [user] [fmt] Show authenticated user profile (json or text)
554
+ refresh [user] Force token validation and refresh
555
+ users List all authenticated users
556
+ switch <user> Switch active user
557
+ help Show this help message
558
+
559
+ Examples:
560
+ shipwright auth login # Start OAuth flow
561
+ shipwright auth status # Show logged-in user
562
+ shipwright auth token | xargs -I {} curl -H "Authorization: Bearer {}" https://api.github.com/user
563
+ shipwright auth users # List all users
564
+ shipwright auth switch alice # Switch to alice
565
+ shipwright auth logout # Logout active user
566
+
567
+ EOF
568
+ }
569
+
570
+ # ─── Main ───────────────────────────────────────────────────────────────────
571
+ main() {
572
+ local cmd="${1:-help}"
573
+ shift 2>/dev/null || true
574
+
575
+ case "$cmd" in
576
+ login)
577
+ cmd_login "$@"
578
+ ;;
579
+ logout)
580
+ cmd_logout "$@"
581
+ ;;
582
+ status)
583
+ cmd_status "$@"
584
+ ;;
585
+ token)
586
+ cmd_token "$@"
587
+ ;;
588
+ user)
589
+ cmd_user "$@"
590
+ ;;
591
+ refresh)
592
+ cmd_refresh "$@"
593
+ ;;
594
+ users)
595
+ cmd_users "$@"
596
+ ;;
597
+ switch)
598
+ cmd_switch "$@"
599
+ ;;
600
+ help|--help|-h)
601
+ cmd_help
602
+ ;;
603
+ *)
604
+ error "Unknown command: $cmd"
605
+ cmd_help >&2
606
+ exit 1
607
+ ;;
608
+ esac
609
+ }
610
+
611
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
612
+ main "$@"
613
+ fi