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.
- package/README.md +114 -36
- package/completions/_shipwright +212 -32
- package/completions/shipwright.bash +97 -25
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/package.json +4 -2
- package/scripts/sw +208 -1
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +637 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +432 -130
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +540 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +59 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +471 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +617 -0
- package/scripts/sw-init.sh +88 -1
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +363 -28
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +64 -3
- package/scripts/sw-memory.sh +1 -1
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +689 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +687 -24
- package/scripts/sw-pm.sh +693 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +573 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +658 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +583 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +515 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +603 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
|
@@ -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
|