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
@@ -0,0 +1,596 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright github-app — GitHub App Management & Webhook Receiver ║
4
+ # ║ JWT generation · Installation tokens · Webhook validation · Events ║
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
+ val="${val//\"/\\\"}"
51
+ json_fields="${json_fields},\"${key}\":\"${val}\""
52
+ fi
53
+ done
54
+ mkdir -p "${HOME}/.shipwright"
55
+ echo "{\"ts\":\"$(now_iso)\",\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
56
+ }
57
+
58
+ # ─── Config File Locations ────────────────────────────────────────────────
59
+ CONFIG_DIR="${HOME}/.shipwright"
60
+ CONFIG_FILE="${CONFIG_DIR}/github-app.json"
61
+ TOKENS_FILE="${CONFIG_DIR}/github-app-tokens.json"
62
+ WEBHOOK_LOG="${CONFIG_DIR}/webhook-events.jsonl"
63
+
64
+ # ─── Ensure config directory exists ───────────────────────────────────────
65
+ _ensure_config_dir() {
66
+ mkdir -p "$CONFIG_DIR"
67
+ }
68
+
69
+ # ═══════════════════════════════════════════════════════════════════════════════
70
+ # APP CONFIG FUNCTIONS
71
+ # ═══════════════════════════════════════════════════════════════════════════════
72
+
73
+ # ─── Setup: Interactive configuration ──────────────────────────────────────
74
+ cmd_setup() {
75
+ _ensure_config_dir
76
+
77
+ if [[ -f "$CONFIG_FILE" ]]; then
78
+ warn "GitHub App config already exists at ${CONFIG_FILE}"
79
+ read -p "Overwrite? (y/n) " -r
80
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
81
+ info "Skipped setup"
82
+ return 0
83
+ fi
84
+ fi
85
+
86
+ echo ""
87
+ info "GitHub App Configuration"
88
+ echo ""
89
+
90
+ read -p "App ID: " app_id
91
+ read -p "Private key file path: " key_path
92
+
93
+ if [[ ! -f "$key_path" ]]; then
94
+ error "Private key file not found: $key_path"
95
+ return 1
96
+ fi
97
+
98
+ read -p "Installation ID: " installation_id
99
+ read -p "Webhook secret (optional, press Enter to skip): " webhook_secret
100
+
101
+ # Create config atomically
102
+ local tmp_config
103
+ tmp_config=$(mktemp)
104
+ jq -n \
105
+ --arg app_id "$app_id" \
106
+ --arg key_path "$key_path" \
107
+ --arg install_id "$installation_id" \
108
+ --arg webhook_secret "$webhook_secret" \
109
+ '{
110
+ app_id: ($app_id | tonumber),
111
+ private_key_path: $key_path,
112
+ installation_id: ($install_id | tonumber),
113
+ webhook_secret: $webhook_secret,
114
+ created_at: "'$(now_iso)'"
115
+ }' > "$tmp_config"
116
+
117
+ mv "$tmp_config" "$CONFIG_FILE"
118
+ chmod 600 "$CONFIG_FILE"
119
+
120
+ success "GitHub App config saved to ${CONFIG_FILE}"
121
+ emit_event "github_app.setup" "app_id=$app_id" "install_id=$installation_id"
122
+ }
123
+
124
+ # ─── Load config from file ────────────────────────────────────────────────
125
+ _load_config() {
126
+ if [[ ! -f "$CONFIG_FILE" ]]; then
127
+ error "GitHub App config not found. Run 'shipwright github-app setup' first."
128
+ return 1
129
+ fi
130
+ cat "$CONFIG_FILE"
131
+ }
132
+
133
+ # ─── Get config value ────────────────────────────────────────────────────
134
+ _get_config_value() {
135
+ local key="$1"
136
+ _load_config | jq -r ".$key // empty" 2>/dev/null || true
137
+ }
138
+
139
+ # ═══════════════════════════════════════════════════════════════════════════════
140
+ # JWT & TOKEN FUNCTIONS
141
+ # ═══════════════════════════════════════════════════════════════════════════════
142
+
143
+ # ─── Generate JWT from private key ────────────────────────────────────────
144
+ _generate_jwt() {
145
+ local app_id="$1"
146
+ local key_path="$2"
147
+
148
+ if [[ ! -f "$key_path" ]]; then
149
+ error "Private key not found: $key_path"
150
+ return 1
151
+ fi
152
+
153
+ # JWT header (alg: RS256, typ: JWT)
154
+ local header
155
+ header=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64 | tr -d '=' | tr '+/' '-_')
156
+
157
+ # JWT payload (iss: app_id, iat: now, exp: now + 10 min)
158
+ local now
159
+ now=$(date +%s)
160
+ local exp=$((now + 600))
161
+
162
+ local payload
163
+ payload=$(echo -n '{"iss":'$app_id',"iat":'$now',"exp":'$exp'}' | base64 | tr -d '=' | tr '+/' '-_')
164
+
165
+ # Sign with private key
166
+ local signature_input="${header}.${payload}"
167
+ local signature
168
+ signature=$(echo -n "$signature_input" | openssl dgst -sha256 -sign "$key_path" | base64 | tr -d '=' | tr '+/' '-_')
169
+
170
+ echo "${signature_input}.${signature}"
171
+ }
172
+
173
+ # ─── Exchange JWT for installation token ─────────────────────────────────
174
+ _get_installation_token() {
175
+ local jwt="$1"
176
+ local installation_id="$2"
177
+
178
+ if [[ "${NO_GITHUB:-}" == "true" || "${NO_GITHUB:-}" == "1" ]]; then
179
+ echo ""
180
+ return 0
181
+ fi
182
+
183
+ local response
184
+ response=$(curl -s -H "Authorization: Bearer $jwt" \
185
+ -H "Accept: application/vnd.github+json" \
186
+ "https://api.github.com/app/installations/${installation_id}/access_tokens" \
187
+ -d '{}' -X POST 2>/dev/null) || true
188
+
189
+ if echo "$response" | jq -e '.token' >/dev/null 2>&1; then
190
+ echo "$response" | jq -r '.token'
191
+ else
192
+ error "Failed to get installation token"
193
+ echo "$response" | jq -r '.message // "Unknown error"' >&2
194
+ return 1
195
+ fi
196
+ }
197
+
198
+ # ─── Cache token with expiry ──────────────────────────────────────────────
199
+ _cache_token() {
200
+ local installation_id="$1"
201
+ local token="$2"
202
+ local expires_at="$3"
203
+
204
+ _ensure_config_dir
205
+
206
+ local tmp_tokens
207
+ tmp_tokens=$(mktemp)
208
+
209
+ if [[ -f "$TOKENS_FILE" ]]; then
210
+ jq ".tokens += [{\"installation_id\":$installation_id,\"token\":\"$token\",\"expires_at\":\"$expires_at\"}]" \
211
+ "$TOKENS_FILE" > "$tmp_tokens"
212
+ else
213
+ jq -n ".tokens = [{\"installation_id\":$installation_id,\"token\":\"$token\",\"expires_at\":\"$expires_at\"}]" \
214
+ > "$tmp_tokens"
215
+ fi
216
+
217
+ mv "$tmp_tokens" "$TOKENS_FILE"
218
+ chmod 600 "$TOKENS_FILE"
219
+ }
220
+
221
+ # ─── Get cached token if still valid ──────────────────────────────────────
222
+ _get_cached_token() {
223
+ local installation_id="$1"
224
+
225
+ if [[ ! -f "$TOKENS_FILE" ]]; then
226
+ echo ""
227
+ return 1
228
+ fi
229
+
230
+ local now
231
+ now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
232
+
233
+ local token
234
+ token=$(jq -r ".tokens[] | select(.installation_id==$installation_id and .expires_at > \"$now\") | .token" \
235
+ "$TOKENS_FILE" 2>/dev/null | head -1 || true)
236
+
237
+ if [[ -n "$token" ]]; then
238
+ echo "$token"
239
+ return 0
240
+ fi
241
+
242
+ echo ""
243
+ return 1
244
+ }
245
+
246
+ # ─── Get installation token (cached or fresh) ─────────────────────────────
247
+ cmd_token() {
248
+ local app_id
249
+ app_id=$(_get_config_value "app_id") || {
250
+ error "GitHub App not configured. Run 'shipwright github-app setup' first."
251
+ return 1
252
+ }
253
+
254
+ local key_path
255
+ key_path=$(_get_config_value "private_key_path") || {
256
+ error "Missing private_key_path in config"
257
+ return 1
258
+ }
259
+
260
+ local installation_id
261
+ installation_id=$(_get_config_value "installation_id") || {
262
+ error "Missing installation_id in config"
263
+ return 1
264
+ }
265
+
266
+ # Try cached token first
267
+ local cached_token
268
+ cached_token=$(_get_cached_token "$installation_id" 2>/dev/null) || true
269
+ if [[ -n "$cached_token" ]]; then
270
+ echo "$cached_token"
271
+ return 0
272
+ fi
273
+
274
+ # Generate JWT and exchange for token
275
+ info "Generating JWT and requesting installation token..."
276
+ local jwt
277
+ jwt=$(_generate_jwt "$app_id" "$key_path") || return 1
278
+
279
+ local token
280
+ token=$(_get_installation_token "$jwt" "$installation_id") || return 1
281
+
282
+ # Cache token (valid for 1 hour)
283
+ local expires_at
284
+ expires_at=$(date -u -d "+1 hour" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+1H +"%Y-%m-%dT%H:%M:%SZ")
285
+ _cache_token "$installation_id" "$token" "$expires_at"
286
+
287
+ success "Got installation token (cached for 1 hour)"
288
+ emit_event "github_app.token_acquired" "installation_id=$installation_id"
289
+ echo "$token"
290
+ }
291
+
292
+ # ═══════════════════════════════════════════════════════════════════════════════
293
+ # WEBHOOK FUNCTIONS
294
+ # ═══════════════════════════════════════════════════════════════════════════════
295
+
296
+ # ─── Verify webhook signature (HMAC-SHA256) ───────────────────────────────
297
+ cmd_verify() {
298
+ local webhook_secret
299
+ webhook_secret=$(_get_config_value "webhook_secret") || true
300
+
301
+ if [[ -z "$webhook_secret" ]]; then
302
+ error "Webhook secret not configured"
303
+ return 1
304
+ fi
305
+
306
+ # Read payload from stdin
307
+ local payload
308
+ payload=$(cat)
309
+
310
+ # Get signature from header (passed as argument or env var)
311
+ local signature="${1:-${X_HUB_SIGNATURE_256:-}}"
312
+
313
+ if [[ -z "$signature" ]]; then
314
+ error "No signature provided. Pass as argument or X_HUB_SIGNATURE_256 env var."
315
+ return 1
316
+ fi
317
+
318
+ # Compute expected signature
319
+ local expected_sig
320
+ expected_sig=$(echo -n "$payload" | openssl dgst -sha256 -mac HMAC -macopt "key:${webhook_secret}" | sed 's/^.* /sha256=/')
321
+
322
+ if [[ "$expected_sig" == "$signature" ]]; then
323
+ success "Webhook signature verified"
324
+ echo "$payload"
325
+ return 0
326
+ else
327
+ error "Webhook signature verification failed"
328
+ error "Expected: $expected_sig"
329
+ error "Got: $signature"
330
+ return 1
331
+ fi
332
+ }
333
+
334
+ # ─── Log webhook event ────────────────────────────────────────────────────
335
+ _log_webhook_event() {
336
+ local event_type="$1"
337
+ local payload="$2"
338
+
339
+ _ensure_config_dir
340
+
341
+ local event
342
+ event=$(jq -n \
343
+ --arg ts "$(now_iso)" \
344
+ --arg type "$event_type" \
345
+ --argjson payload "$payload" \
346
+ '{timestamp: $ts, event_type: $type, payload: $payload}')
347
+
348
+ echo "$event" >> "$WEBHOOK_LOG"
349
+ }
350
+
351
+ # ─── Handle GitHub webhook events ─────────────────────────────────────────
352
+ _handle_webhook_event() {
353
+ local event_type="$1"
354
+ local payload="$2"
355
+
356
+ case "$event_type" in
357
+ issues)
358
+ local action
359
+ action=$(echo "$payload" | jq -r '.action // empty')
360
+ if [[ "$action" == "labeled" ]]; then
361
+ local label
362
+ label=$(echo "$payload" | jq -r '.label.name // empty')
363
+ info "Issue labeled with: $label"
364
+ emit_event "webhook.issue_labeled" "label=$label"
365
+ fi
366
+ ;;
367
+ pull_request)
368
+ local action
369
+ action=$(echo "$payload" | jq -r '.action // empty')
370
+ if [[ "$action" == "opened" ]]; then
371
+ info "Pull request opened"
372
+ emit_event "webhook.pr_opened"
373
+ elif [[ "$action" == "review_requested" ]]; then
374
+ info "Review requested on PR"
375
+ emit_event "webhook.pr_review_requested"
376
+ fi
377
+ ;;
378
+ check_suite)
379
+ local action
380
+ action=$(echo "$payload" | jq -r '.action // empty')
381
+ if [[ "$action" == "requested" ]]; then
382
+ info "Check suite requested"
383
+ emit_event "webhook.check_suite_requested"
384
+ fi
385
+ ;;
386
+ push)
387
+ local ref
388
+ ref=$(echo "$payload" | jq -r '.ref // empty')
389
+ info "Push to: $ref"
390
+ emit_event "webhook.push" "ref=$ref"
391
+ ;;
392
+ *)
393
+ info "Webhook event: $event_type"
394
+ ;;
395
+ esac
396
+
397
+ _log_webhook_event "$event_type" "$payload"
398
+ }
399
+
400
+ # ═══════════════════════════════════════════════════════════════════════════════
401
+ # APP MANIFEST & STATUS FUNCTIONS
402
+ # ═══════════════════════════════════════════════════════════════════════════════
403
+
404
+ # ─── Generate GitHub App manifest for easy setup ────────────────────────
405
+ cmd_manifest() {
406
+ local app_name="${1:-shipwright-app}"
407
+ local webhook_url="${2:-https://webhook.example.com}"
408
+
409
+ local manifest
410
+ manifest=$(jq -n \
411
+ --arg name "$app_name" \
412
+ --arg webhook_url "$webhook_url" \
413
+ '{
414
+ name: $name,
415
+ url: "https://github.com/sethdford/shipwright",
416
+ hook_attributes: {
417
+ url: $webhook_url
418
+ },
419
+ redirect_url: "https://github.com/apps/'$app_name'/installations/new",
420
+ description: "Autonomous pipeline delivery with Shipwright",
421
+ public: true,
422
+ default_events: [
423
+ "issues",
424
+ "pull_request",
425
+ "pull_request_review",
426
+ "pull_request_review_comment",
427
+ "check_suite",
428
+ "check_run",
429
+ "push"
430
+ ],
431
+ default_permissions: {
432
+ contents: "write",
433
+ checks: "write",
434
+ pull_requests: "write",
435
+ issues: "write",
436
+ deployments: "write"
437
+ }
438
+ }')
439
+
440
+ echo "$manifest" | jq .
441
+ success "Manifest generated. Visit: https://github.com/settings/apps/new to create your app."
442
+ }
443
+
444
+ # ─── Show app status and config ─────────────────────────────────────────
445
+ cmd_status() {
446
+ if [[ ! -f "$CONFIG_FILE" ]]; then
447
+ warn "GitHub App not configured"
448
+ echo ""
449
+ echo "Run '${CYAN}shipwright github-app setup${RESET}' to configure"
450
+ return 0
451
+ fi
452
+
453
+ info "GitHub App Status"
454
+ echo ""
455
+
456
+ local config
457
+ config=$(cat "$CONFIG_FILE")
458
+
459
+ local app_id
460
+ app_id=$(echo "$config" | jq -r '.app_id')
461
+ local install_id
462
+ install_id=$(echo "$config" | jq -r '.installation_id')
463
+ local webhook_secret
464
+ webhook_secret=$(echo "$config" | jq -r '.webhook_secret // empty')
465
+
466
+ echo -e "${BOLD}Configuration:${RESET}"
467
+ echo " App ID: $app_id"
468
+ echo " Installation ID: $install_id"
469
+ echo " Webhook Secret: ${webhook_secret:-${DIM}(none)${RESET}}"
470
+ echo ""
471
+
472
+ # Show recent webhook events
473
+ if [[ -f "$WEBHOOK_LOG" ]]; then
474
+ local count
475
+ count=$(wc -l < "$WEBHOOK_LOG" 2>/dev/null || echo 0)
476
+ if [[ "$count" -gt 0 ]]; then
477
+ echo -e "${BOLD}Recent Webhook Events (last 10):${RESET}"
478
+ tail -10 "$WEBHOOK_LOG" | jq '{timestamp, event_type}' -c
479
+ echo ""
480
+ fi
481
+ fi
482
+
483
+ # Show cached tokens
484
+ if [[ -f "$TOKENS_FILE" ]]; then
485
+ echo -e "${BOLD}Cached Tokens:${RESET}"
486
+ jq '.tokens[] | {installation_id, expires_at}' "$TOKENS_FILE" 2>/dev/null || echo " (none)"
487
+ echo ""
488
+ fi
489
+
490
+ success "Status retrieved"
491
+ }
492
+
493
+ # ─── List recent webhook events ────────────────────────────────────────
494
+ cmd_events() {
495
+ local limit="${1:-20}"
496
+
497
+ if [[ ! -f "$WEBHOOK_LOG" ]]; then
498
+ warn "No webhook events logged yet"
499
+ return 0
500
+ fi
501
+
502
+ info "Recent Webhook Events"
503
+ echo ""
504
+
505
+ tail -"$limit" "$WEBHOOK_LOG" | jq '{timestamp, event_type, payload: (.payload | keys)}' -c
506
+ }
507
+
508
+ # ═══════════════════════════════════════════════════════════════════════════════
509
+ # HELP & MAIN
510
+ # ═══════════════════════════════════════════════════════════════════════════════
511
+
512
+ show_help() {
513
+ cat <<EOF
514
+ ${CYAN}${BOLD}shipwright github-app${RESET} — GitHub App Management & Webhook Receiver
515
+
516
+ ${BOLD}USAGE${RESET}
517
+ shipwright github-app <command> [options]
518
+
519
+ ${BOLD}COMMANDS${RESET}
520
+ ${CYAN}setup${RESET} Interactive configuration (app ID, private key, installation ID)
521
+ ${CYAN}token${RESET} Get/refresh installation access token (with caching)
522
+ ${CYAN}manifest${RESET} Generate GitHub App manifest JSON for setup at github.com/settings/apps/new
523
+ ${CYAN}verify${RESET} Verify webhook signature (read payload from stdin)
524
+ ${CYAN}events${RESET} [limit] List recent webhook events (default: 20)
525
+ ${CYAN}status${RESET} Show current app config, installation status, cached tokens
526
+ ${CYAN}help${RESET} Show this help message
527
+
528
+ ${BOLD}EXAMPLES${RESET}
529
+ ${DIM}# Initial setup${RESET}
530
+ shipwright github-app setup
531
+
532
+ ${DIM}# Get installation token${RESET}
533
+ shipwright github-app token
534
+
535
+ ${DIM}# Generate manifest for app creation${RESET}
536
+ shipwright github-app manifest "my-app" "https://my-webhook.com"
537
+
538
+ ${DIM}# Verify webhook signature${RESET}
539
+ cat webhook-payload.json | shipwright github-app verify "sha256=..."
540
+
541
+ ${DIM}# Check app status${RESET}
542
+ shipwright github-app status
543
+
544
+ ${DIM}# View recent webhook events${RESET}
545
+ shipwright github-app events 50
546
+
547
+ ${BOLD}CONFIG LOCATION${RESET}
548
+ ${DIM}${CONFIG_FILE}${RESET}
549
+
550
+ ${BOLD}WEBHOOK LOG${RESET}
551
+ ${DIM}${WEBHOOK_LOG}${RESET}
552
+
553
+ ${BOLD}TOKEN CACHE${RESET}
554
+ ${DIM}${TOKENS_FILE}${RESET}
555
+
556
+ EOF
557
+ }
558
+
559
+ main() {
560
+ local cmd="${1:-help}"
561
+ shift 2>/dev/null || true
562
+
563
+ case "$cmd" in
564
+ setup)
565
+ cmd_setup "$@"
566
+ ;;
567
+ token)
568
+ cmd_token "$@"
569
+ ;;
570
+ manifest)
571
+ cmd_manifest "$@"
572
+ ;;
573
+ verify)
574
+ cmd_verify "$@"
575
+ ;;
576
+ events)
577
+ cmd_events "$@"
578
+ ;;
579
+ status)
580
+ cmd_status "$@"
581
+ ;;
582
+ help|--help|-h)
583
+ show_help
584
+ ;;
585
+ *)
586
+ error "Unknown command: ${cmd}"
587
+ echo ""
588
+ show_help
589
+ exit 1
590
+ ;;
591
+ esac
592
+ }
593
+
594
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
595
+ main "$@"
596
+ fi
@@ -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
 
@@ -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