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,704 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright changelog — Automated Release Notes & Migration Guides ║
4
+ # ║ Parse commits, categorize changes, generate markdown and stakeholder ║
5
+ # ║ announcements with version recommendations and migration instructions ║
6
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
7
+ set -euo pipefail
8
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
9
+
10
+ VERSION="2.0.0"
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
13
+
14
+ # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
15
+ CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
16
+ PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
17
+ BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
18
+ GREEN='\033[38;2;74;222;128m' # success
19
+ YELLOW='\033[38;2;250;204;21m' # warning
20
+ RED='\033[38;2;248;113;113m' # error
21
+ DIM='\033[2m'
22
+ BOLD='\033[1m'
23
+ RESET='\033[0m'
24
+
25
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
26
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
27
+
28
+ # ─── 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
+ emit_event() {
35
+ local event_type="$1"; shift
36
+ local events_file="${HOME}/.shipwright/events.jsonl"
37
+ mkdir -p "$(dirname "$events_file")"
38
+ local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
39
+ while [[ $# -gt 0 ]]; do
40
+ local key="${1%%=*}" val="${1#*=}"
41
+ payload="${payload},\"${key}\":\"${val}\""
42
+ shift
43
+ done
44
+ payload="${payload}}"
45
+ echo "$payload" >> "$events_file"
46
+ }
47
+
48
+ # ─── Commit Parsing ──────────────────────────────────────────────────────────
49
+
50
+ # Extract conventional commit type (feat, fix, perf, security, etc.)
51
+ get_commit_type() {
52
+ local msg="$1"
53
+ if [[ "$msg" =~ ^(feat|fix|perf|chore|docs|style|refactor|test|ci|security|breaking) ]]; then
54
+ echo "${BASH_REMATCH[1]}"
55
+ elif [[ "$msg" =~ ^BREAKING ]]; then
56
+ echo "breaking"
57
+ else
58
+ echo "other"
59
+ fi
60
+ }
61
+
62
+ # Extract scope if present (e.g., "feat(auth):" → "auth")
63
+ get_commit_scope() {
64
+ local msg="$1"
65
+ # Use simpler pattern: extract text between parens after type
66
+ echo "$msg" | grep -oE '^[a-z]+\([^)]+\)' | sed 's/^[a-z]*(\(.*\))/\1/' || true
67
+ }
68
+
69
+ # Clean commit message (remove type prefix and scope)
70
+ clean_commit_msg() {
71
+ local msg="$1"
72
+ msg="${msg#feat: }"
73
+ msg="${msg#fix: }"
74
+ msg="${msg#perf: }"
75
+ msg="${msg#chore: }"
76
+ msg="${msg#docs: }"
77
+ msg="${msg#style: }"
78
+ msg="${msg#refactor: }"
79
+ msg="${msg#test: }"
80
+ msg="${msg#ci: }"
81
+ msg="${msg#security: }"
82
+ msg="${msg#BREAKING: }"
83
+ msg="${msg#BREAKING CHANGE: }"
84
+ # Remove scope if present
85
+ msg="${msg#*()*: }"
86
+ msg=$(echo "$msg" | sed 's/^[a-z]*(\([^)]*\)): //')
87
+ echo "$msg"
88
+ }
89
+
90
+ # Get commits since last release (tag) or from start
91
+ get_commits() {
92
+ local from_ref="${1:-}"
93
+ local to_ref="${2:-HEAD}"
94
+
95
+ if [[ -z "$from_ref" ]]; then
96
+ # Find last tag
97
+ from_ref=$(git -C "$REPO_DIR" describe --tags --abbrev=0 2>/dev/null || echo "$(git -C "$REPO_DIR" rev-list --max-parents=0 HEAD)")
98
+ fi
99
+
100
+ if [[ "$from_ref" == "HEAD" ]]; then
101
+ return
102
+ fi
103
+
104
+ git -C "$REPO_DIR" log "${from_ref}..${to_ref}" --pretty=format:"%H|%an|%ae|%ai|%s|%b" 2>/dev/null || true
105
+ }
106
+
107
+ # Parse commits into categorized structure
108
+ parse_commits() {
109
+ local from_ref="${1:-}"
110
+ local to_ref="${2:-HEAD}"
111
+ local commits_log
112
+ commits_log=$(get_commits "$from_ref" "$to_ref")
113
+
114
+ local features fixes perf_changes security_changes breaking_changes docs_changes chores
115
+ features=""
116
+ fixes=""
117
+ perf_changes=""
118
+ security_changes=""
119
+ breaking_changes=""
120
+ docs_changes=""
121
+ chores=""
122
+ local contributors=""
123
+ local pr_links=""
124
+
125
+ while IFS='|' read -r hash author email date subject body; do
126
+ [[ -z "$hash" ]] && continue
127
+
128
+ local type scope msg
129
+ type=$(get_commit_type "$subject")
130
+ scope=$(get_commit_scope "$subject")
131
+ msg=$(clean_commit_msg "$subject")
132
+
133
+ # Extract PR number if in body
134
+ local pr_num=""
135
+ if [[ "$body" =~ ([Pp][Rr]\s*#?([0-9]+)|#([0-9]+)) ]]; then
136
+ pr_num="${BASH_REMATCH[2]:-${BASH_REMATCH[3]}}"
137
+ fi
138
+
139
+ # Build entry
140
+ local entry="$msg"
141
+ [[ -n "$scope" ]] && entry="**${scope}**: $entry"
142
+ [[ -n "$pr_num" ]] && entry="$entry ([\#$pr_num](https://github.com/sethdford/shipwright/pull/$pr_num))"
143
+
144
+ # Categorize
145
+ case "$type" in
146
+ breaking)
147
+ breaking_changes="${breaking_changes}${entry}
148
+ "
149
+ ;;
150
+ feat)
151
+ features="${features}${entry}
152
+ "
153
+ ;;
154
+ fix)
155
+ fixes="${fixes}${entry}
156
+ "
157
+ ;;
158
+ perf)
159
+ perf_changes="${perf_changes}${entry}
160
+ "
161
+ ;;
162
+ security)
163
+ security_changes="${security_changes}${entry}
164
+ "
165
+ ;;
166
+ docs)
167
+ docs_changes="${docs_changes}${entry}
168
+ "
169
+ ;;
170
+ chore|style|refactor|test|ci)
171
+ chores="${chores}${entry}
172
+ "
173
+ ;;
174
+ *)
175
+ # Treat as feature if breaking indicator present
176
+ if echo "$subject" | grep -qi "breaking"; then
177
+ breaking_changes="${breaking_changes}${msg}
178
+ "
179
+ else
180
+ features="${features}${entry}
181
+ "
182
+ fi
183
+ ;;
184
+ esac
185
+
186
+ # Track contributors
187
+ contributors="${contributors}${author} (${email})
188
+ "
189
+ done <<< "$commits_log"
190
+
191
+ # Output as JSON
192
+ jq -n \
193
+ --arg features "$features" \
194
+ --arg fixes "$fixes" \
195
+ --arg perf "$perf_changes" \
196
+ --arg security "$security_changes" \
197
+ --arg breaking "$breaking_changes" \
198
+ --arg docs "$docs_changes" \
199
+ --arg chores "$chores" \
200
+ --arg contributors "$contributors" \
201
+ '{
202
+ features: ($features | split("\n") | map(select(length > 0))),
203
+ fixes: ($fixes | split("\n") | map(select(length > 0))),
204
+ perf: ($perf | split("\n") | map(select(length > 0))),
205
+ security: ($security | split("\n") | map(select(length > 0))),
206
+ breaking: ($breaking | split("\n") | map(select(length > 0))),
207
+ docs: ($docs | split("\n") | map(select(length > 0))),
208
+ chores: ($chores | split("\n") | map(select(length > 0))),
209
+ contributors: ($contributors | split("\n") | map(select(length > 0)) | unique)
210
+ }'
211
+ }
212
+
213
+ # ─── Version Recommendation ──────────────────────────────────────────────────
214
+
215
+ recommend_version() {
216
+ local changes_json="$1"
217
+ local current_version="${2:-0.1.0}"
218
+
219
+ # Parse current version
220
+ local major minor patch
221
+ IFS='.' read -r major minor patch <<< "$current_version"
222
+ patch="${patch%%[^0-9]*}" # Strip any pre-release/build metadata
223
+
224
+ # Check for breaking changes
225
+ local breaking_count
226
+ breaking_count=$(echo "$changes_json" | jq '.breaking | length')
227
+
228
+ # Check for features
229
+ local features_count
230
+ features_count=$(echo "$changes_json" | jq '.features | length')
231
+
232
+ # Check for fixes
233
+ local fixes_count
234
+ fixes_count=$(echo "$changes_json" | jq '.fixes | length')
235
+
236
+ if [[ "$breaking_count" -gt 0 ]]; then
237
+ echo "$((major + 1)).0.0"
238
+ elif [[ "$features_count" -gt 0 ]]; then
239
+ echo "${major}.$((minor + 1)).0"
240
+ elif [[ "$fixes_count" -gt 0 ]]; then
241
+ echo "${major}.${minor}.$((patch + 1))"
242
+ else
243
+ echo "${major}.${minor}.${patch}"
244
+ fi
245
+ }
246
+
247
+ # ─── Release Notes Generation ────────────────────────────────────────────────
248
+
249
+ generate_markdown() {
250
+ local changes_json="$1"
251
+ local version="${2:-Unreleased}"
252
+ local date="${3:-$(date -u +%Y-%m-%d)}"
253
+
254
+ local output=""
255
+ output+="## [${version}] — ${date}
256
+ "
257
+ output+="
258
+ "
259
+
260
+ # Breaking Changes
261
+ local breaking_count
262
+ breaking_count=$(echo "$changes_json" | jq '.breaking | length')
263
+ if [[ "$breaking_count" -gt 0 ]]; then
264
+ output+="### ⚠️ Breaking Changes
265
+ "
266
+ output+="
267
+ "
268
+ echo "$changes_json" | jq -r '.breaking[]' | while read -r change; do
269
+ output+="- ${change}
270
+ "
271
+ done
272
+ output+="
273
+ "
274
+ fi
275
+
276
+ # Features
277
+ local features_count
278
+ features_count=$(echo "$changes_json" | jq '.features | length')
279
+ if [[ "$features_count" -gt 0 ]]; then
280
+ output+="### ✨ Features
281
+ "
282
+ output+="
283
+ "
284
+ echo "$changes_json" | jq -r '.features[]' | while read -r change; do
285
+ output+="- ${change}
286
+ "
287
+ done
288
+ output+="
289
+ "
290
+ fi
291
+
292
+ # Security
293
+ local security_count
294
+ security_count=$(echo "$changes_json" | jq '.security | length')
295
+ if [[ "$security_count" -gt 0 ]]; then
296
+ output+="### 🔒 Security
297
+ "
298
+ output+="
299
+ "
300
+ echo "$changes_json" | jq -r '.security[]' | while read -r change; do
301
+ output+="- ${change}
302
+ "
303
+ done
304
+ output+="
305
+ "
306
+ fi
307
+
308
+ # Performance
309
+ local perf_count
310
+ perf_count=$(echo "$changes_json" | jq '.perf | length')
311
+ if [[ "$perf_count" -gt 0 ]]; then
312
+ output+="### 🚀 Performance
313
+ "
314
+ output+="
315
+ "
316
+ echo "$changes_json" | jq -r '.perf[]' | while read -r change; do
317
+ output+="- ${change}
318
+ "
319
+ done
320
+ output+="
321
+ "
322
+ fi
323
+
324
+ # Bug Fixes
325
+ local fixes_count
326
+ fixes_count=$(echo "$changes_json" | jq '.fixes | length')
327
+ if [[ "$fixes_count" -gt 0 ]]; then
328
+ output+="### 🐛 Bug Fixes
329
+ "
330
+ output+="
331
+ "
332
+ echo "$changes_json" | jq -r '.fixes[]' | while read -r change; do
333
+ output+="- ${change}
334
+ "
335
+ done
336
+ output+="
337
+ "
338
+ fi
339
+
340
+ # Documentation
341
+ local docs_count
342
+ docs_count=$(echo "$changes_json" | jq '.docs | length')
343
+ if [[ "$docs_count" -gt 0 ]]; then
344
+ output+="### 📚 Documentation
345
+ "
346
+ output+="
347
+ "
348
+ echo "$changes_json" | jq -r '.docs[]' | while read -r change; do
349
+ output+="- ${change}
350
+ "
351
+ done
352
+ output+="
353
+ "
354
+ fi
355
+
356
+ # Contributors
357
+ local contributors_count
358
+ contributors_count=$(echo "$changes_json" | jq '.contributors | length')
359
+ if [[ "$contributors_count" -gt 0 ]]; then
360
+ output+="### 👥 Contributors
361
+ "
362
+ output+="
363
+ "
364
+ echo "$changes_json" | jq -r '.contributors[]' | while read -r contrib; do
365
+ [[ -n "$contrib" ]] && output+="- ${contrib}
366
+ "
367
+ done
368
+ output+="
369
+ "
370
+ fi
371
+
372
+ echo -e "$output"
373
+ }
374
+
375
+ # ─── Migration Guide Generation ──────────────────────────────────────────────
376
+
377
+ generate_migration_guide() {
378
+ local changes_json="$1"
379
+ local version="${2:-Unreleased}"
380
+
381
+ local breaking_count
382
+ breaking_count=$(echo "$changes_json" | jq '.breaking | length')
383
+
384
+ if [[ "$breaking_count" -eq 0 ]]; then
385
+ echo "No breaking changes in this release."
386
+ return 0
387
+ fi
388
+
389
+ local output=""
390
+ output+="# Migration Guide — Version ${version}
391
+ "
392
+ output+="
393
+ "
394
+ output+="This release includes breaking changes. Follow this guide to update your code.
395
+ "
396
+ output+="
397
+ "
398
+
399
+ local idx=1
400
+ echo "$changes_json" | jq -r '.breaking[]' | while read -r change; do
401
+ output+="## Change ${idx}: ${change}
402
+ "
403
+ output+="
404
+ "
405
+ output+="### Before
406
+ "
407
+ output+="
408
+ "
409
+ output+="\`\`\`bash
410
+ # Previous approach
411
+ old_command --flag value
412
+ \`\`\`
413
+ "
414
+ output+="
415
+ "
416
+ output+="### After
417
+ "
418
+ output+="
419
+ "
420
+ output+="\`\`\`bash
421
+ # New approach
422
+ new_command --new-flag value
423
+ \`\`\`
424
+ "
425
+ output+="
426
+ "
427
+ idx=$((idx + 1))
428
+ done
429
+
430
+ echo -e "$output"
431
+ }
432
+
433
+ # ─── Stakeholder Announcement ────────────────────────────────────────────────
434
+
435
+ generate_announcement() {
436
+ local changes_json="$1"
437
+ local version="${2:-Unreleased}"
438
+
439
+ local features_count
440
+ features_count=$(echo "$changes_json" | jq '.features | length')
441
+ local fixes_count
442
+ fixes_count=$(echo "$changes_json" | jq '.fixes | length')
443
+ local breaking_count
444
+ breaking_count=$(echo "$changes_json" | jq '.breaking | length')
445
+
446
+ local output=""
447
+ output+="# 🎉 Release: Version ${version}
448
+ "
449
+ output+="
450
+ "
451
+ output+="We're excited to announce the release of Shipwright ${version}, packed with improvements to make your CI/CD pipeline even more powerful.
452
+ "
453
+ output+="
454
+ "
455
+ output+="## What's New
456
+ "
457
+ output+="
458
+ "
459
+
460
+ if [[ "$features_count" -gt 0 ]]; then
461
+ output+="**${features_count} new features** that streamline your workflow and improve productivity:
462
+ "
463
+ output+="
464
+ "
465
+ echo "$changes_json" | jq -r '.features[]' | head -3 | while read -r feature; do
466
+ output+="- ${feature}
467
+ "
468
+ done
469
+ output+="
470
+ "
471
+ fi
472
+
473
+ if [[ "$fixes_count" -gt 0 ]]; then
474
+ output+="**${fixes_count} bug fixes** that make Shipwright more reliable:
475
+ "
476
+ output+="
477
+ "
478
+ echo "$changes_json" | jq -r '.fixes[]' | head -3 | while read -r fix; do
479
+ output+="- ${fix}
480
+ "
481
+ done
482
+ output+="
483
+ "
484
+ fi
485
+
486
+ if [[ "$breaking_count" -gt 0 ]]; then
487
+ output+="⚠️ **Note:** This release includes breaking changes. Please review the migration guide before upgrading.
488
+ "
489
+ output+="
490
+ "
491
+ fi
492
+
493
+ output+="## Get Started
494
+ "
495
+ output+="
496
+ "
497
+ output+="Upgrade to version ${version} to take advantage of these improvements. See our documentation for detailed information.
498
+ "
499
+ output+="
500
+ "
501
+
502
+ echo -e "$output"
503
+ }
504
+
505
+ # ─── Subcommands ────────────────────────────────────────────────────────────
506
+
507
+ cmd_generate() {
508
+ local from_ref="${1:-}"
509
+ local to_ref="HEAD"
510
+
511
+ if [[ "$from_ref" == "--from" ]] && [[ -n "${2:-}" ]]; then
512
+ from_ref="$2"
513
+ if [[ "${3:-}" == "--to" ]] && [[ -n "${4:-}" ]]; then
514
+ to_ref="$4"
515
+ fi
516
+ fi
517
+
518
+ info "Parsing commits from ${from_ref:-last release} to ${to_ref}..."
519
+ local changes_json
520
+ changes_json=$(parse_commits "$from_ref" "$to_ref")
521
+
522
+ local current_version
523
+ current_version=$(git -C "$REPO_DIR" describe --tags --abbrev=0 2>/dev/null || echo "0.1.0")
524
+ current_version="${current_version#v}"
525
+
526
+ local next_version
527
+ next_version=$(recommend_version "$changes_json" "$current_version")
528
+
529
+ info "Recommended version: ${CYAN}${next_version}${RESET}"
530
+
531
+ local release_notes
532
+ release_notes=$(generate_markdown "$changes_json" "v${next_version}")
533
+
534
+ local output_file="${REPO_DIR}/CHANGELOG-${next_version}.md"
535
+ echo "$release_notes" > "$output_file"
536
+ success "Release notes generated: ${output_file}"
537
+
538
+ emit_event "changelog.generate" "version=$next_version" "commits=$(echo "$changes_json" | jq '.features | length')"
539
+ }
540
+
541
+ cmd_preview() {
542
+ local from_ref="${1:-}"
543
+ local to_ref="HEAD"
544
+
545
+ info "Parsing commits for preview..."
546
+ local changes_json
547
+ changes_json=$(parse_commits "$from_ref" "$to_ref")
548
+
549
+ local current_version
550
+ current_version=$(git -C "$REPO_DIR" describe --tags --abbrev=0 2>/dev/null || echo "0.1.0")
551
+ current_version="${current_version#v}"
552
+
553
+ local next_version
554
+ next_version=$(recommend_version "$changes_json" "$current_version")
555
+
556
+ echo ""
557
+ echo -e "${CYAN}${BOLD}Preview: Release v${next_version}${RESET}"
558
+ echo ""
559
+
560
+ generate_markdown "$changes_json" "v${next_version}"
561
+ }
562
+
563
+ cmd_version() {
564
+ local from_ref="${1:-}"
565
+ local to_ref="HEAD"
566
+
567
+ info "Analyzing commits for version recommendation..."
568
+ local changes_json
569
+ changes_json=$(parse_commits "$from_ref" "$to_ref")
570
+
571
+ local current_version
572
+ current_version=$(git -C "$REPO_DIR" describe --tags --abbrev=0 2>/dev/null || echo "0.1.0")
573
+ current_version="${current_version#v}"
574
+
575
+ local next_version
576
+ next_version=$(recommend_version "$changes_json" "$current_version")
577
+
578
+ echo ""
579
+ echo -e "${BOLD}Current version:${RESET} ${current_version}"
580
+ echo -e "${BOLD}Recommended version:${RESET} ${CYAN}${next_version}${RESET}"
581
+
582
+ local breaking_count
583
+ breaking_count=$(echo "$changes_json" | jq '.breaking | length')
584
+ local features_count
585
+ features_count=$(echo "$changes_json" | jq '.features | length')
586
+ local fixes_count
587
+ fixes_count=$(echo "$changes_json" | jq '.fixes | length')
588
+
589
+ echo ""
590
+ echo "Rationale:"
591
+ [[ "$breaking_count" -gt 0 ]] && echo " - ${RED}${breaking_count} breaking changes${RESET} → major version bump"
592
+ [[ "$features_count" -gt 0 ]] && echo " - ${GREEN}${features_count} new features${RESET} → minor version bump"
593
+ [[ "$fixes_count" -gt 0 ]] && echo " - ${GREEN}${fixes_count} bug fixes${RESET} → patch version bump"
594
+ echo ""
595
+ }
596
+
597
+ cmd_migrate() {
598
+ local from_ref="${1:-}"
599
+ local to_ref="HEAD"
600
+
601
+ info "Generating migration guide..."
602
+ local changes_json
603
+ changes_json=$(parse_commits "$from_ref" "$to_ref")
604
+
605
+ local current_version
606
+ current_version=$(git -C "$REPO_DIR" describe --tags --abbrev=0 2>/dev/null || echo "0.1.0")
607
+ current_version="${current_version#v}"
608
+
609
+ local next_version
610
+ next_version=$(recommend_version "$changes_json" "$current_version")
611
+
612
+ local migration
613
+ migration=$(generate_migration_guide "$changes_json" "v${next_version}")
614
+
615
+ local output_file="${REPO_DIR}/MIGRATION-${next_version}.md"
616
+ echo "$migration" > "$output_file"
617
+ success "Migration guide generated: ${output_file}"
618
+
619
+ emit_event "changelog.migrate" "version=$next_version"
620
+ }
621
+
622
+ cmd_announce() {
623
+ local from_ref="${1:-}"
624
+ local to_ref="HEAD"
625
+
626
+ info "Generating stakeholder announcement..."
627
+ local changes_json
628
+ changes_json=$(parse_commits "$from_ref" "$to_ref")
629
+
630
+ local current_version
631
+ current_version=$(git -C "$REPO_DIR" describe --tags --abbrev=0 2>/dev/null || echo "0.1.0")
632
+ current_version="${current_version#v}"
633
+
634
+ local next_version
635
+ next_version=$(recommend_version "$changes_json" "$current_version")
636
+
637
+ local announcement
638
+ announcement=$(generate_announcement "$changes_json" "v${next_version}")
639
+
640
+ local output_file="${REPO_DIR}/ANNOUNCE-${next_version}.md"
641
+ echo "$announcement" > "$output_file"
642
+ success "Announcement generated: ${output_file}"
643
+
644
+ emit_event "changelog.announce" "version=$next_version"
645
+ }
646
+
647
+ cmd_formats() {
648
+ echo ""
649
+ echo -e "${BOLD}Available Output Formats:${RESET}"
650
+ echo ""
651
+ echo -e " ${CYAN}markdown${RESET} Release notes in markdown format (default)"
652
+ echo -e " ${CYAN}json${RESET} Structured changes in JSON"
653
+ echo -e " ${CYAN}html${RESET} HTML-formatted release notes"
654
+ echo -e " ${CYAN}text${RESET} Plain text format"
655
+ echo ""
656
+ }
657
+
658
+ show_help() {
659
+ echo -e "${CYAN}${BOLD}shipwright changelog${RESET} — Automated Release Notes & Migration Guides"
660
+ echo ""
661
+ echo -e "${BOLD}USAGE${RESET}"
662
+ echo -e " ${CYAN}shipwright changelog${RESET} <command> [options]"
663
+ echo ""
664
+ echo -e "${BOLD}COMMANDS${RESET}"
665
+ echo -e " ${CYAN}generate${RESET} Generate changelog since last release"
666
+ echo -e " ${CYAN}generate --from TAG --to TAG Changelog between specific tags"
667
+ echo -e " ${CYAN}preview${RESET} Preview next release notes without committing"
668
+ echo -e " ${CYAN}version${RESET} Recommend next semantic version"
669
+ echo -e " ${CYAN}migrate${RESET} Generate migration guide for breaking changes"
670
+ echo -e " ${CYAN}announce${RESET} Generate stakeholder announcement"
671
+ echo -e " ${CYAN}formats${RESET} List available output formats"
672
+ echo -e " ${CYAN}help${RESET} Show this help message"
673
+ echo ""
674
+ echo -e "${BOLD}EXAMPLES${RESET}"
675
+ echo -e " ${DIM}shipwright changelog generate${RESET}"
676
+ echo -e " ${DIM}shipwright changelog preview${RESET}"
677
+ echo -e " ${DIM}shipwright changelog version${RESET}"
678
+ echo -e " ${DIM}shipwright changelog generate --from v1.0.0 --to v1.1.0${RESET}"
679
+ echo ""
680
+ }
681
+
682
+ main() {
683
+ local cmd="${1:-help}"
684
+ shift 2>/dev/null || true
685
+
686
+ case "$cmd" in
687
+ generate) cmd_generate "$@" ;;
688
+ preview) cmd_preview "$@" ;;
689
+ version) cmd_version "$@" ;;
690
+ migrate) cmd_migrate "$@" ;;
691
+ announce) cmd_announce "$@" ;;
692
+ formats) cmd_formats "$@" ;;
693
+ help|--help|-h) show_help ;;
694
+ *)
695
+ error "Unknown command: $cmd"
696
+ show_help
697
+ exit 1
698
+ ;;
699
+ esac
700
+ }
701
+
702
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
703
+ main "$@"
704
+ fi