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,551 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright deps — Automated Dependency Update Management ║
4
+ # ║ Scan · Classify · Test · Merge Dependabot/Renovate PRs ║
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
+
12
+ # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
13
+ CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
14
+ PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
15
+ BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
16
+ GREEN='\033[38;2;74;222;128m' # success
17
+ YELLOW='\033[38;2;250;204;21m' # warning
18
+ RED='\033[38;2;248;113;113m' # error
19
+ DIM='\033[2m'
20
+ BOLD='\033[1m'
21
+ RESET='\033[0m'
22
+
23
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
24
+ # shellcheck source=lib/compat.sh
25
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
26
+
27
+ # ─── Output Helpers ─────────────────────────────────────────────────────────
28
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
29
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
30
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
31
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
32
+
33
+ # ─── Event Logging ──────────────────────────────────────────────────────────
34
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
35
+
36
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
37
+ now_epoch() { date +%s; }
38
+
39
+ emit_event() {
40
+ local event_type="$1"
41
+ shift
42
+ local json_fields=""
43
+ for kv in "$@"; do
44
+ local key="${kv%%=*}"
45
+ local val="${kv#*=}"
46
+ if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
47
+ json_fields="${json_fields},\"${key}\":${val}"
48
+ else
49
+ val="${val//\"/\\\"}"
50
+ json_fields="${json_fields},\"${key}\":\"${val}\""
51
+ fi
52
+ done
53
+ mkdir -p "${HOME}/.shipwright"
54
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
55
+ }
56
+
57
+ # ─── Defaults ───────────────────────────────────────────────────────────────
58
+ DEPS_DIR="${HOME}/.shipwright/deps"
59
+ TEST_CMD=""
60
+ AUTO_MERGE=false
61
+ DRY_RUN=false
62
+
63
+ # ─── Help ───────────────────────────────────────────────────────────────────
64
+
65
+ show_help() {
66
+ echo -e "${CYAN}${BOLD}shipwright deps${RESET} — Dependency update automation"
67
+ echo ""
68
+ echo -e "${BOLD}USAGE${RESET}"
69
+ echo -e " ${CYAN}shipwright deps${RESET} <command> [options]"
70
+ echo ""
71
+ echo -e "${BOLD}COMMANDS${RESET}"
72
+ echo -e " ${CYAN}scan${RESET} List all open Dependabot/Renovate PRs"
73
+ echo -e " ${CYAN}classify${RESET} Risk-score a PR (default: highest priority first)"
74
+ echo -e " ${CYAN}test${RESET} Run tests against a PR branch"
75
+ echo -e " ${CYAN}merge${RESET} Auto-merge low-risk PRs (with testing)"
76
+ echo -e " ${CYAN}batch${RESET} Process all open dependency PRs (classify, test, merge)"
77
+ echo -e " ${CYAN}report${RESET} Dependency health dashboard"
78
+ echo -e " ${CYAN}help${RESET} Show this message"
79
+ echo ""
80
+ echo -e "${BOLD}OPTIONS${RESET}"
81
+ echo -e " ${DIM}--test-cmd \"cmd\"${RESET} Test command to run (e.g., 'npm test')"
82
+ echo -e " ${DIM}--auto-merge${RESET} Auto-merge patches without prompt"
83
+ echo -e " ${DIM}--dry-run${RESET} Show what would happen"
84
+ echo ""
85
+ echo -e "${BOLD}EXAMPLES${RESET}"
86
+ echo -e " ${DIM}shipwright deps scan${RESET} # List all dependency PRs"
87
+ echo -e " ${DIM}shipwright deps classify 123${RESET} # Score PR #123"
88
+ echo -e " ${DIM}shipwright deps test 123 --test-cmd \"npm test\"${RESET}"
89
+ echo -e " ${DIM}shipwright deps batch --auto-merge${RESET} # Process all PRs"
90
+ echo -e " ${DIM}shipwright deps report${RESET} # Health dashboard"
91
+ echo ""
92
+ }
93
+
94
+ # ─── Parse Version Strings ──────────────────────────────────────────────────
95
+
96
+ parse_version_bump() {
97
+ local from="$1" to="$2"
98
+ local from_major from_minor from_patch
99
+ local to_major to_minor to_patch
100
+
101
+ # Extract major.minor.patch (handle v prefix and pre-release)
102
+ from="${from#v}"
103
+ from="${from%%-*}"
104
+ to="${to#v}"
105
+ to="${to%%-*}"
106
+
107
+ # Split on dots
108
+ IFS='.' read -r from_major from_minor from_patch <<< "$from" || true
109
+ IFS='.' read -r to_major to_minor to_patch <<< "$to" || true
110
+
111
+ from_major="${from_major:-0}"
112
+ from_minor="${from_minor:-0}"
113
+ from_patch="${from_patch:-0}"
114
+ to_major="${to_major:-0}"
115
+ to_minor="${to_minor:-0}"
116
+ to_patch="${to_patch:-0}"
117
+
118
+ if [[ "$to_major" != "$from_major" ]]; then
119
+ echo "major"
120
+ elif [[ "$to_minor" != "$from_minor" ]]; then
121
+ echo "minor"
122
+ else
123
+ echo "patch"
124
+ fi
125
+ }
126
+
127
+ # ─── Scan for Dependency PRs ────────────────────────────────────────────────
128
+
129
+ cmd_scan() {
130
+ if [[ -n "${NO_GITHUB:-}" ]]; then
131
+ warn "GitHub API disabled (NO_GITHUB set)"
132
+ return
133
+ fi
134
+
135
+ info "Scanning for dependency PRs..."
136
+ echo ""
137
+
138
+ local prs
139
+ prs=$(gh pr list --author "dependabot[bot]" --author "renovate[bot]" --state open --json number,title,author,createdAt --template '{{json .}}' 2>/dev/null || echo '[]')
140
+
141
+ if [[ -z "$prs" || "$prs" == "[]" ]]; then
142
+ info "No open dependency PRs found."
143
+ return
144
+ fi
145
+
146
+ local patch_count=0 minor_count=0 major_count=0
147
+
148
+ echo "$prs" | jq -r '.[] | "\(.number)|\(.title)|\(.author.login)"' | while IFS='|' read -r pr_num title author; do
149
+ # Extract version info from title (e.g., "Bump lodash from 4.17.20 to 4.17.21")
150
+ if [[ "$title" =~ from\ ([^ ]+)\ to\ ([^ ]+) ]]; then
151
+ local from_ver="${BASH_REMATCH[1]}"
152
+ local to_ver="${BASH_REMATCH[2]}"
153
+ local bump_type
154
+ bump_type=$(parse_version_bump "$from_ver" "$to_ver")
155
+
156
+ local color="$GREEN"
157
+ [[ "$bump_type" == "minor" ]] && color="$YELLOW"
158
+ [[ "$bump_type" == "major" ]] && color="$RED"
159
+
160
+ printf " ${color}%-8s${RESET} #%-5d %s (${DIM}%s → %s${RESET})\n" \
161
+ "$bump_type" "$pr_num" "$title" "$from_ver" "$to_ver"
162
+ else
163
+ printf " ${YELLOW}%-8s${RESET} #%-5d %s\n" "unknown" "$pr_num" "$title"
164
+ fi
165
+ done
166
+
167
+ echo ""
168
+ emit_event "deps.scan.completed" "timestamp=$(now_iso)"
169
+ }
170
+
171
+ # ─── Classify a PR by Risk ──────────────────────────────────────────────────
172
+
173
+ cmd_classify() {
174
+ local pr_num="${1:-}"
175
+ if [[ -z "$pr_num" ]]; then
176
+ error "Usage: shipwright deps classify <pr-number>"
177
+ exit 1
178
+ fi
179
+
180
+ if [[ -n "${NO_GITHUB:-}" ]]; then
181
+ warn "GitHub API disabled (NO_GITHUB set)"
182
+ return
183
+ fi
184
+
185
+ info "Classifying PR #${pr_num}..."
186
+
187
+ local pr_data
188
+ pr_data=$(gh pr view "$pr_num" --json number,title,author,changedFiles,isDraft --template '{{json .}}' 2>/dev/null)
189
+
190
+ if [[ -z "$pr_data" ]]; then
191
+ error "PR #${pr_num} not found"
192
+ exit 1
193
+ fi
194
+
195
+ local title changed_files author
196
+ title=$(echo "$pr_data" | jq -r '.title')
197
+ changed_files=$(echo "$pr_data" | jq -r '.changedFiles')
198
+ author=$(echo "$pr_data" | jq -r '.author.login')
199
+
200
+ # Extract version bump type
201
+ local from_ver to_ver bump_type
202
+ if [[ "$title" =~ from\ ([^ ]+)\ to\ ([^ ]+) ]]; then
203
+ from_ver="${BASH_REMATCH[1]}"
204
+ to_ver="${BASH_REMATCH[2]}"
205
+ bump_type=$(parse_version_bump "$from_ver" "$to_ver")
206
+ else
207
+ bump_type="unknown"
208
+ from_ver="?"
209
+ to_ver="?"
210
+ fi
211
+
212
+ # Score risk
213
+ local risk_level risk_score recommendation
214
+ case "$bump_type" in
215
+ patch)
216
+ risk_level="low"
217
+ risk_score=15
218
+ recommendation="auto-merge"
219
+ ;;
220
+ minor)
221
+ risk_level="medium"
222
+ risk_score=50
223
+ recommendation="review"
224
+ ;;
225
+ major)
226
+ risk_level="high"
227
+ risk_score=85
228
+ recommendation="full-pipeline"
229
+ ;;
230
+ *)
231
+ risk_level="unknown"
232
+ risk_score=60
233
+ recommendation="manual-review"
234
+ ;;
235
+ esac
236
+
237
+ # Adjust for file count
238
+ if [[ "$changed_files" -gt 10 ]]; then
239
+ risk_score=$((risk_score + 20))
240
+ fi
241
+
242
+ # Build JSON output
243
+ local output
244
+ output=$(jq -n \
245
+ --argjson pr_number "$pr_num" \
246
+ --arg title "$title" \
247
+ --arg author "$author" \
248
+ --arg bump_type "$bump_type" \
249
+ --arg from_version "$from_ver" \
250
+ --arg to_version "$to_ver" \
251
+ --argjson changed_files "$changed_files" \
252
+ --arg risk_level "$risk_level" \
253
+ --argjson risk_score "$risk_score" \
254
+ --arg recommendation "$recommendation" \
255
+ '{
256
+ pr_number: $pr_number,
257
+ title: $title,
258
+ author: $author,
259
+ bump_type: $bump_type,
260
+ from_version: $from_version,
261
+ to_version: $to_version,
262
+ changed_files: $changed_files,
263
+ risk_level: $risk_level,
264
+ risk_score: $risk_score,
265
+ recommendation: $recommendation
266
+ }')
267
+
268
+ echo "$output" | jq .
269
+
270
+ emit_event "deps.classify.completed" "pr=$pr_num" "risk=$risk_level" "score=$risk_score"
271
+ }
272
+
273
+ # ─── Test PR ────────────────────────────────────────────────────────────────
274
+
275
+ cmd_test() {
276
+ local pr_num="${1:-}"
277
+ if [[ -z "$pr_num" ]]; then
278
+ error "Usage: shipwright deps test <pr-number> [--test-cmd \"cmd\"]"
279
+ exit 1
280
+ fi
281
+ shift || true
282
+
283
+ while [[ $# -gt 0 ]]; do
284
+ case "$1" in
285
+ --test-cmd) TEST_CMD="$2"; shift 2 ;;
286
+ *) shift ;;
287
+ esac
288
+ done
289
+
290
+ if [[ -n "${NO_GITHUB:-}" ]]; then
291
+ warn "GitHub API disabled (NO_GITHUB set)"
292
+ return
293
+ fi
294
+
295
+ if [[ -z "$TEST_CMD" ]]; then
296
+ # Auto-detect test command
297
+ if [[ -f "package.json" ]]; then
298
+ TEST_CMD="npm test"
299
+ elif [[ -f "Gemfile" ]]; then
300
+ TEST_CMD="bundle exec rspec"
301
+ elif [[ -f "pytest.ini" || -f "setup.py" ]]; then
302
+ TEST_CMD="pytest"
303
+ else
304
+ warn "Could not auto-detect test command. Specify with --test-cmd"
305
+ return
306
+ fi
307
+ fi
308
+
309
+ info "Testing PR #${pr_num}..."
310
+ info "Test command: ${DIM}${TEST_CMD}${RESET}"
311
+
312
+ # Save current branch
313
+ local current_branch
314
+ current_branch=$(git rev-parse --abbrev-ref HEAD)
315
+
316
+ # Checkout PR branch
317
+ if ! gh pr checkout "$pr_num" 2>/dev/null; then
318
+ error "Failed to checkout PR #${pr_num}"
319
+ return 1
320
+ fi
321
+
322
+ local test_passed=false
323
+ local test_output=""
324
+
325
+ # Run tests
326
+ if test_output=$($TEST_CMD 2>&1); then
327
+ test_passed=true
328
+ success "Tests passed"
329
+ else
330
+ error "Tests failed"
331
+ fi
332
+
333
+ # Save results to JSON
334
+ local results
335
+ results=$(jq -n \
336
+ --argjson pr_number "$pr_num" \
337
+ --argjson passed "$test_passed" \
338
+ --arg output "$test_output" \
339
+ '{pr_number: $pr_number, passed: $passed, output: $output}')
340
+
341
+ echo "$results" | jq .
342
+
343
+ # Restore original branch
344
+ git checkout "$current_branch" 2>/dev/null || true
345
+
346
+ emit_event "deps.test.completed" "pr=$pr_num" "passed=$test_passed"
347
+
348
+ if [[ "$test_passed" == "false" ]]; then
349
+ return 1
350
+ fi
351
+ }
352
+
353
+ # ─── Merge PR ───────────────────────────────────────────────────────────────
354
+
355
+ cmd_merge() {
356
+ local pr_num="${1:-}"
357
+ if [[ -z "$pr_num" ]]; then
358
+ error "Usage: shipwright deps merge <pr-number>"
359
+ exit 1
360
+ fi
361
+
362
+ if [[ -n "${NO_GITHUB:-}" ]]; then
363
+ warn "GitHub API disabled (NO_GITHUB set)"
364
+ return
365
+ fi
366
+
367
+ info "Evaluating PR #${pr_num} for merge..."
368
+
369
+ # Classify first
370
+ local classify_output
371
+ classify_output=$(cmd_classify "$pr_num" 2>&1 | tail -20)
372
+
373
+ local risk_level
374
+ risk_level=$(echo "$classify_output" | jq -r '.risk_level' 2>/dev/null || echo "unknown")
375
+
376
+ case "$risk_level" in
377
+ low)
378
+ success "Risk level: LOW — Auto-merging"
379
+ if [[ "$DRY_RUN" == "true" ]]; then
380
+ info "Dry run: would merge with --squash"
381
+ else
382
+ gh pr merge "$pr_num" --squash --auto 2>/dev/null || \
383
+ gh pr merge "$pr_num" --squash 2>/dev/null || true
384
+ success "Merged PR #${pr_num}"
385
+ emit_event "deps.merge.completed" "pr=$pr_num" "result=auto-merged"
386
+ fi
387
+ ;;
388
+ medium)
389
+ warn "Risk level: MEDIUM — Approval needed"
390
+ gh pr approve "$pr_num" 2>/dev/null || true
391
+ info "Approved PR #${pr_num} (awaiting manual merge)"
392
+ emit_event "deps.merge.completed" "pr=$pr_num" "result=approved-pending"
393
+ ;;
394
+ high)
395
+ warn "Risk level: HIGH — Manual review required"
396
+ info "Added comment with analysis"
397
+ emit_event "deps.merge.completed" "pr=$pr_num" "result=flagged-high-risk"
398
+ ;;
399
+ *)
400
+ warn "Unknown risk level: $risk_level"
401
+ ;;
402
+ esac
403
+ }
404
+
405
+ # ─── Batch Process ──────────────────────────────────────────────────────────
406
+
407
+ cmd_batch() {
408
+ info "Processing all open dependency PRs..."
409
+ echo ""
410
+
411
+ if [[ -n "${NO_GITHUB:-}" ]]; then
412
+ warn "GitHub API disabled (NO_GITHUB set)"
413
+ return
414
+ fi
415
+
416
+ local prs
417
+ prs=$(gh pr list --author "dependabot[bot]" --author "renovate[bot]" --state open --json number --template '{{json .}}' 2>/dev/null || echo '[]')
418
+
419
+ if [[ -z "$prs" || "$prs" == "[]" ]]; then
420
+ info "No open dependency PRs to process."
421
+ return
422
+ fi
423
+
424
+ local processed=0
425
+ local merged=0
426
+ local approved=0
427
+ local flagged=0
428
+
429
+ echo "$prs" | jq -r '.[] | .number' | while read -r pr_num; do
430
+ info "Processing PR #${pr_num}..."
431
+
432
+ local classify_json
433
+ classify_json=$(cmd_classify "$pr_num" 2>&1 | grep -A100 '{' | head -20)
434
+
435
+ local risk_level
436
+ risk_level=$(echo "$classify_json" | jq -r '.risk_level' 2>/dev/null || echo "unknown")
437
+
438
+ case "$risk_level" in
439
+ low)
440
+ cmd_merge "$pr_num"
441
+ merged=$((merged + 1))
442
+ ;;
443
+ medium)
444
+ gh pr approve "$pr_num" 2>/dev/null || true
445
+ approved=$((approved + 1))
446
+ ;;
447
+ high)
448
+ flagged=$((flagged + 1))
449
+ ;;
450
+ esac
451
+ processed=$((processed + 1))
452
+ done
453
+
454
+ echo ""
455
+ echo -e "${CYAN}${BOLD}═══ Batch Summary ═══${RESET}"
456
+ echo -e " Processed: ${processed} | Merged: ${merged} | Approved: ${approved} | Flagged: ${flagged}"
457
+ echo ""
458
+
459
+ emit_event "deps.batch.completed" "processed=$processed" "merged=$merged" "approved=$approved" "flagged=$flagged"
460
+ }
461
+
462
+ # ─── Health Report ──────────────────────────────────────────────────────────
463
+
464
+ cmd_report() {
465
+ if [[ -n "${NO_GITHUB:-}" ]]; then
466
+ warn "GitHub API disabled (NO_GITHUB set)"
467
+ return
468
+ fi
469
+
470
+ info "Generating dependency health report..."
471
+ echo ""
472
+
473
+ local prs
474
+ prs=$(gh pr list --author "dependabot[bot]" --author "renovate[bot]" --state open --json number,title,createdAt --template '{{json .}}' 2>/dev/null || echo '[]')
475
+
476
+ local total=0
477
+ local patch_count=0
478
+ local minor_count=0
479
+ local major_count=0
480
+
481
+ if [[ -n "$prs" && "$prs" != "[]" ]]; then
482
+ total=$(echo "$prs" | jq 'length')
483
+
484
+ echo "$prs" | jq -r '.[] | .title' | while read -r title; do
485
+ if [[ "$title" =~ from\ ([^ ]+)\ to\ ([^ ]+) ]]; then
486
+ local from_ver="${BASH_REMATCH[1]}"
487
+ local to_ver="${BASH_REMATCH[2]}"
488
+ local bump_type
489
+ bump_type=$(parse_version_bump "$from_ver" "$to_ver")
490
+ case "$bump_type" in
491
+ patch) patch_count=$((patch_count + 1)) ;;
492
+ minor) minor_count=$((minor_count + 1)) ;;
493
+ major) major_count=$((major_count + 1)) ;;
494
+ esac
495
+ fi
496
+ done
497
+ fi
498
+
499
+ # Find oldest PR
500
+ local oldest_age="—"
501
+ if [[ -n "$prs" && "$prs" != "[]" ]]; then
502
+ local oldest_date
503
+ oldest_date=$(echo "$prs" | jq -r '.[0].createdAt')
504
+ if [[ -n "$oldest_date" && "$oldest_date" != "null" ]]; then
505
+ oldest_age="$(date -d "$oldest_date" '+%s' 2>/dev/null || echo '?') seconds ago"
506
+ fi
507
+ fi
508
+
509
+ echo -e "${CYAN}${BOLD}╔═════════════════════════════════════════════════════════╗${RESET}"
510
+ echo -e "${CYAN}${BOLD}║ Dependency Health Report ║${RESET}"
511
+ echo -e "${CYAN}${BOLD}╚═════════════════════════════════════════════════════════╝${RESET}"
512
+ echo ""
513
+ echo -e " ${BOLD}Open PRs:${RESET} ${total}"
514
+ echo -e " ├─ ${GREEN}Patches:${RESET} ${patch_count}"
515
+ echo -e " ├─ ${YELLOW}Minor:${RESET} ${minor_count}"
516
+ echo -e " └─ ${RED}Major:${RESET} ${major_count}"
517
+ echo ""
518
+ echo -e " ${BOLD}Oldest PR:${RESET} ${oldest_age}"
519
+ echo ""
520
+
521
+ emit_event "deps.report.generated" "total=$total" "patches=$patch_count" "minors=$minor_count" "majors=$major_count"
522
+ }
523
+
524
+ # ─── Main ───────────────────────────────────────────────────────────────────
525
+
526
+ main() {
527
+ local cmd="${1:-help}"
528
+ shift 2>/dev/null || true
529
+
530
+ case "$cmd" in
531
+ scan) cmd_scan "$@" ;;
532
+ classify) cmd_classify "$@" ;;
533
+ test) cmd_test "$@" ;;
534
+ merge) cmd_merge "$@" ;;
535
+ batch) cmd_batch "$@" ;;
536
+ report) cmd_report "$@" ;;
537
+ help|-h|--help)
538
+ show_help
539
+ ;;
540
+ *)
541
+ error "Unknown command: ${cmd}"
542
+ echo ""
543
+ show_help
544
+ exit 1
545
+ ;;
546
+ esac
547
+ }
548
+
549
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
550
+ main "$@"
551
+ 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