shipwright-cli 1.7.1 → 1.10.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 (115) hide show
  1. package/.claude/agents/code-reviewer.md +90 -0
  2. package/.claude/agents/devops-engineer.md +142 -0
  3. package/.claude/agents/pipeline-agent.md +80 -0
  4. package/.claude/agents/shell-script-specialist.md +150 -0
  5. package/.claude/agents/test-specialist.md +196 -0
  6. package/.claude/hooks/post-tool-use.sh +45 -0
  7. package/.claude/hooks/pre-tool-use.sh +25 -0
  8. package/.claude/hooks/session-started.sh +37 -0
  9. package/README.md +212 -814
  10. package/claude-code/CLAUDE.md.shipwright +54 -0
  11. package/claude-code/hooks/notify-idle.sh +2 -2
  12. package/claude-code/hooks/session-start.sh +24 -0
  13. package/claude-code/hooks/task-completed.sh +6 -2
  14. package/claude-code/settings.json.template +12 -0
  15. package/dashboard/public/app.js +4422 -0
  16. package/dashboard/public/index.html +816 -0
  17. package/dashboard/public/styles.css +4755 -0
  18. package/dashboard/server.ts +4315 -0
  19. package/docs/KNOWN-ISSUES.md +18 -10
  20. package/docs/TIPS.md +38 -26
  21. package/docs/patterns/README.md +33 -23
  22. package/package.json +9 -5
  23. package/scripts/adapters/iterm2-adapter.sh +1 -1
  24. package/scripts/adapters/tmux-adapter.sh +52 -23
  25. package/scripts/adapters/wezterm-adapter.sh +26 -14
  26. package/scripts/lib/compat.sh +200 -0
  27. package/scripts/lib/helpers.sh +72 -0
  28. package/scripts/postinstall.mjs +72 -13
  29. package/scripts/{cct → sw} +118 -22
  30. package/scripts/sw-adversarial.sh +274 -0
  31. package/scripts/sw-architecture-enforcer.sh +330 -0
  32. package/scripts/sw-checkpoint.sh +468 -0
  33. package/scripts/sw-cleanup.sh +359 -0
  34. package/scripts/sw-connect.sh +619 -0
  35. package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
  36. package/scripts/sw-daemon.sh +5574 -0
  37. package/scripts/sw-dashboard.sh +477 -0
  38. package/scripts/sw-developer-simulation.sh +252 -0
  39. package/scripts/sw-docs.sh +635 -0
  40. package/scripts/sw-doctor.sh +907 -0
  41. package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
  42. package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
  43. package/scripts/sw-github-checks.sh +521 -0
  44. package/scripts/sw-github-deploy.sh +533 -0
  45. package/scripts/sw-github-graphql.sh +972 -0
  46. package/scripts/sw-heartbeat.sh +293 -0
  47. package/scripts/{cct-init.sh → sw-init.sh} +144 -11
  48. package/scripts/sw-intelligence.sh +1196 -0
  49. package/scripts/sw-jira.sh +643 -0
  50. package/scripts/sw-launchd.sh +364 -0
  51. package/scripts/sw-linear.sh +648 -0
  52. package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
  53. package/scripts/sw-loop.sh +2217 -0
  54. package/scripts/{cct-memory.sh → sw-memory.sh} +514 -36
  55. package/scripts/sw-patrol-meta.sh +417 -0
  56. package/scripts/sw-pipeline-composer.sh +455 -0
  57. package/scripts/sw-pipeline-vitals.sh +1096 -0
  58. package/scripts/sw-pipeline.sh +7593 -0
  59. package/scripts/sw-predictive.sh +820 -0
  60. package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
  61. package/scripts/{cct-ps.sh → sw-ps.sh} +9 -6
  62. package/scripts/{cct-reaper.sh → sw-reaper.sh} +10 -6
  63. package/scripts/sw-remote.sh +687 -0
  64. package/scripts/sw-self-optimize.sh +1048 -0
  65. package/scripts/sw-session.sh +541 -0
  66. package/scripts/sw-setup.sh +234 -0
  67. package/scripts/sw-status.sh +796 -0
  68. package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
  69. package/scripts/sw-tmux.sh +591 -0
  70. package/scripts/sw-tracker-jira.sh +277 -0
  71. package/scripts/sw-tracker-linear.sh +292 -0
  72. package/scripts/sw-tracker.sh +409 -0
  73. package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
  74. package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
  75. package/templates/pipelines/autonomous.json +35 -6
  76. package/templates/pipelines/cost-aware.json +21 -0
  77. package/templates/pipelines/deployed.json +40 -6
  78. package/templates/pipelines/enterprise.json +16 -2
  79. package/templates/pipelines/fast.json +19 -0
  80. package/templates/pipelines/full.json +28 -2
  81. package/templates/pipelines/hotfix.json +19 -0
  82. package/templates/pipelines/standard.json +31 -0
  83. package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
  84. package/tmux/templates/accessibility.json +34 -0
  85. package/tmux/templates/api-design.json +35 -0
  86. package/tmux/templates/architecture.json +1 -0
  87. package/tmux/templates/bug-fix.json +9 -0
  88. package/tmux/templates/code-review.json +1 -0
  89. package/tmux/templates/compliance.json +36 -0
  90. package/tmux/templates/data-pipeline.json +36 -0
  91. package/tmux/templates/debt-paydown.json +34 -0
  92. package/tmux/templates/devops.json +1 -0
  93. package/tmux/templates/documentation.json +1 -0
  94. package/tmux/templates/exploration.json +1 -0
  95. package/tmux/templates/feature-dev.json +1 -0
  96. package/tmux/templates/full-stack.json +8 -0
  97. package/tmux/templates/i18n.json +34 -0
  98. package/tmux/templates/incident-response.json +36 -0
  99. package/tmux/templates/migration.json +1 -0
  100. package/tmux/templates/observability.json +35 -0
  101. package/tmux/templates/onboarding.json +33 -0
  102. package/tmux/templates/performance.json +35 -0
  103. package/tmux/templates/refactor.json +1 -0
  104. package/tmux/templates/release.json +35 -0
  105. package/tmux/templates/security-audit.json +8 -0
  106. package/tmux/templates/spike.json +34 -0
  107. package/tmux/templates/testing.json +1 -0
  108. package/tmux/tmux.conf +98 -9
  109. package/scripts/cct-cleanup.sh +0 -172
  110. package/scripts/cct-daemon.sh +0 -3189
  111. package/scripts/cct-doctor.sh +0 -414
  112. package/scripts/cct-loop.sh +0 -1332
  113. package/scripts/cct-pipeline.sh +0 -3844
  114. package/scripts/cct-session.sh +0 -284
  115. package/scripts/cct-status.sh +0 -169
@@ -0,0 +1,643 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright jira — Jira ↔ GitHub Bidirectional Sync ║
4
+ # ║ Sync issues · Update statuses · Link PRs · Pipeline integration ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
+
9
+ VERSION="1.10.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
+ # ─── 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
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
34
+ now_epoch() { date +%s; }
35
+
36
+ # ─── Structured Event Log ──────────────────────────────────────────────────
37
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
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
+ # ─── Configuration ─────────────────────────────────────────────────────────
58
+ CONFIG_DIR="${HOME}/.shipwright"
59
+ TRACKER_CONFIG="${CONFIG_DIR}/tracker-config.json"
60
+
61
+ JIRA_BASE_URL=""
62
+ JIRA_EMAIL=""
63
+ JIRA_API_TOKEN=""
64
+ JIRA_PROJECT_KEY=""
65
+
66
+ load_config() {
67
+ if [[ -f "$TRACKER_CONFIG" ]]; then
68
+ JIRA_BASE_URL="${JIRA_BASE_URL:-$(jq -r '.jira_base_url // empty' "$TRACKER_CONFIG" 2>/dev/null || true)}"
69
+ JIRA_EMAIL="${JIRA_EMAIL:-$(jq -r '.jira_email // empty' "$TRACKER_CONFIG" 2>/dev/null || true)}"
70
+ JIRA_API_TOKEN="${JIRA_API_TOKEN:-$(jq -r '.jira_api_token // empty' "$TRACKER_CONFIG" 2>/dev/null || true)}"
71
+ JIRA_PROJECT_KEY="${JIRA_PROJECT_KEY:-$(jq -r '.jira_project_key // empty' "$TRACKER_CONFIG" 2>/dev/null || true)}"
72
+ fi
73
+
74
+ JIRA_BASE_URL="${JIRA_BASE_URL:-}"
75
+ JIRA_EMAIL="${JIRA_EMAIL:-}"
76
+ JIRA_API_TOKEN="${JIRA_API_TOKEN:-}"
77
+ JIRA_PROJECT_KEY="${JIRA_PROJECT_KEY:-}"
78
+ }
79
+
80
+ check_config() {
81
+ if [[ -z "$JIRA_BASE_URL" ]] || [[ -z "$JIRA_EMAIL" ]] || [[ -z "$JIRA_API_TOKEN" ]]; then
82
+ error "Jira not configured"
83
+ echo ""
84
+ echo -e " Set via environment: ${DIM}export JIRA_BASE_URL=https://your-org.atlassian.net${RESET}"
85
+ echo -e " ${DIM}export JIRA_EMAIL=you@example.com${RESET}"
86
+ echo -e " ${DIM}export JIRA_API_TOKEN=your-token${RESET}"
87
+ echo -e " Or run: ${DIM}shipwright jira init${RESET}"
88
+ exit 1
89
+ fi
90
+ }
91
+
92
+ # ─── Jira REST API Helper ─────────────────────────────────────────────────
93
+ # Executes a REST request against the Jira API.
94
+ # Uses Basic auth (email:token base64-encoded).
95
+ jira_api() {
96
+ local method="$1" endpoint="$2" data="${3:-}"
97
+ local auth
98
+ auth=$(printf '%s:%s' "$JIRA_EMAIL" "$JIRA_API_TOKEN" | base64)
99
+ local args=(-sf -X "$method" \
100
+ -H "Authorization: Basic $auth" \
101
+ -H "Content-Type: application/json")
102
+ [[ -n "$data" ]] && args+=(-d "$data")
103
+ curl "${args[@]}" "${JIRA_BASE_URL}/rest/api/3/${endpoint}" 2>&1
104
+ }
105
+
106
+ # ─── Helper: Add ADF Comment to Jira Issue ─────────────────────────────────
107
+ # Jira comments use Atlassian Document Format (ADF).
108
+ jira_add_comment() {
109
+ local issue_key="$1" body_text="$2"
110
+ local payload
111
+ payload=$(jq -n --arg text "$body_text" '{
112
+ body: {
113
+ type: "doc",
114
+ version: 1,
115
+ content: [{
116
+ type: "paragraph",
117
+ content: [{type: "text", text: $text}]
118
+ }]
119
+ }
120
+ }')
121
+ jira_api "POST" "issue/${issue_key}/comment" "$payload"
122
+ }
123
+
124
+ # ─── Helper: Transition Jira Issue ─────────────────────────────────────────
125
+ # Finds the transition ID by name and applies it.
126
+ jira_transition() {
127
+ local issue_key="$1" target_name="$2"
128
+ local transitions
129
+ transitions=$(jira_api "GET" "issue/${issue_key}/transitions") || return 1
130
+ local tid
131
+ tid=$(echo "$transitions" | jq -r --arg name "$target_name" \
132
+ '.transitions[] | select(.name == $name) | .id' 2>/dev/null || true)
133
+ if [[ -z "$tid" ]]; then
134
+ return 0 # transition not available — silently skip
135
+ fi
136
+ local payload
137
+ payload=$(jq -n --arg id "$tid" '{transition: {id: $id}}')
138
+ jira_api "POST" "issue/${issue_key}/transitions" "$payload"
139
+ }
140
+
141
+ # ─── Sync: Jira Todo → GitHub Issues ──────────────────────────────────────
142
+
143
+ cmd_sync() {
144
+ check_config
145
+ info "Syncing Jira To Do issues → GitHub..."
146
+
147
+ local dry_run=false
148
+ while [[ $# -gt 0 ]]; do
149
+ case "$1" in
150
+ --dry-run) dry_run=true; shift ;;
151
+ *) shift ;;
152
+ esac
153
+ done
154
+
155
+ # Fetch Jira issues in "To Do" status within the project
156
+ local jql
157
+ jql=$(printf 'project = %s AND status = "To Do"' "$JIRA_PROJECT_KEY")
158
+ local encoded_jql
159
+ encoded_jql=$(printf '%s' "$jql" | jq -sRr @uri)
160
+
161
+ local response
162
+ response=$(jira_api "GET" "search?jql=${encoded_jql}&fields=summary,description,priority,status,labels") || {
163
+ error "Failed to fetch Jira issues"
164
+ return 1
165
+ }
166
+
167
+ local count
168
+ count=$(echo "$response" | jq '.issues | length')
169
+ if [[ "$count" -eq 0 ]]; then
170
+ info "No Jira issues in To Do status"
171
+ return 0
172
+ fi
173
+
174
+ info "Found ${count} Jira issue(s) in To Do"
175
+
176
+ local synced=0
177
+ local skipped=0
178
+
179
+ # Process each Jira issue
180
+ local i=0
181
+ while [[ $i -lt $count ]]; do
182
+ local issue
183
+ issue=$(echo "$response" | jq ".issues[$i]")
184
+ local jira_key title description priority_name
185
+ jira_key=$(echo "$issue" | jq -r '.key')
186
+ title=$(echo "$issue" | jq -r '.fields.summary')
187
+ description=$(echo "$issue" | jq -r '.fields.description // ""')
188
+ priority_name=$(echo "$issue" | jq -r '.fields.priority.name // ""')
189
+
190
+ # Extract plain text from ADF description if present
191
+ if echo "$description" | jq -e '.type' >/dev/null 2>&1; then
192
+ description=$(echo "$description" | jq -r '
193
+ [.. | .text? // empty] | join(" ")
194
+ ' 2>/dev/null || echo "")
195
+ fi
196
+
197
+ # Check if GitHub issue already exists for this Jira issue
198
+ local existing_gh
199
+ existing_gh=$(gh issue list --label "ready-to-build" --search "Jira: ${jira_key}" --json number --jq '.[0].number // empty' 2>/dev/null || true)
200
+
201
+ if [[ -n "$existing_gh" ]]; then
202
+ echo -e " ${DIM}Skip${RESET} ${jira_key}: ${title} ${DIM}(GitHub #${existing_gh})${RESET}"
203
+ skipped=$((skipped + 1))
204
+ i=$((i + 1))
205
+ continue
206
+ fi
207
+
208
+ # Map priority to label
209
+ local priority_label=""
210
+ case "$priority_name" in
211
+ Highest|Blocker) priority_label="priority-urgent" ;;
212
+ High) priority_label="priority-high" ;;
213
+ Medium) priority_label="priority-medium" ;;
214
+ Low|Lowest) priority_label="priority-low" ;;
215
+ esac
216
+
217
+ # Build GitHub issue body with Jira back-link
218
+ local jira_url="${JIRA_BASE_URL}/browse/${jira_key}"
219
+ local gh_body
220
+ gh_body=$(printf "## %s\n\n%s\n\n---\n**Jira:** [%s](%s)\n**Jira Key:** %s" \
221
+ "$title" "$description" "$jira_key" "$jira_url" "$jira_key")
222
+
223
+ if [[ "$dry_run" == "true" ]]; then
224
+ echo -e " ${CYAN}Would create${RESET} GitHub issue: ${title} ${DIM}(${jira_key})${RESET}"
225
+ synced=$((synced + 1))
226
+ else
227
+ # Create GitHub issue
228
+ local labels="ready-to-build"
229
+ if [[ -n "$priority_label" ]]; then
230
+ labels="${labels},${priority_label}"
231
+ fi
232
+
233
+ local gh_num
234
+ gh_num=$(gh issue create --title "$title" --body "$gh_body" --label "$labels" --json number --jq '.number' 2>&1) || {
235
+ error "Failed to create GitHub issue for ${jira_key}: ${gh_num}"
236
+ i=$((i + 1))
237
+ continue
238
+ }
239
+
240
+ # Add comment on Jira issue linking back to GitHub
241
+ local comment_body
242
+ comment_body=$(printf "Synced to GitHub issue #%s — the daemon will pick this up for autonomous delivery." "$gh_num")
243
+ jira_add_comment "$jira_key" "$comment_body" >/dev/null 2>&1 || true
244
+
245
+ # Move Jira issue to In Progress
246
+ jira_transition "$jira_key" "In Progress" >/dev/null 2>&1 || true
247
+
248
+ success "${jira_key} → GitHub #${gh_num}: ${title}"
249
+ emit_event "jira.sync" "jira_key=$jira_key" "github_issue=$gh_num" "title=$title"
250
+ synced=$((synced + 1))
251
+ fi
252
+
253
+ i=$((i + 1))
254
+ done
255
+
256
+ echo ""
257
+ if [[ "$dry_run" == "true" ]]; then
258
+ info "Dry run: ${synced} would be created, ${skipped} already synced"
259
+ else
260
+ success "Synced ${synced} issue(s), ${skipped} already linked"
261
+ fi
262
+ }
263
+
264
+ # ─── Update: GitHub → Jira Status ─────────────────────────────────────────
265
+
266
+ cmd_update() {
267
+ check_config
268
+
269
+ if [[ $# -lt 2 ]]; then
270
+ error "Usage: shipwright jira update <github-issue-num> <status>"
271
+ echo ""
272
+ echo -e " Statuses: ${CYAN}started${RESET} | ${CYAN}review${RESET} | ${CYAN}done${RESET} | ${CYAN}failed${RESET}"
273
+ echo ""
274
+ echo -e " ${DIM}shipwright jira update 42 started${RESET} # → In Progress"
275
+ echo -e " ${DIM}shipwright jira update 42 review${RESET} # → In Review"
276
+ echo -e " ${DIM}shipwright jira update 42 done${RESET} # → Done"
277
+ echo -e " ${DIM}shipwright jira update 42 failed${RESET} # → adds failure comment"
278
+ exit 1
279
+ fi
280
+
281
+ local gh_issue="$1"
282
+ local status="$2"
283
+ local detail="${3:-}"
284
+
285
+ # Find the Jira key from the GitHub issue body
286
+ local jira_key
287
+ jira_key=$(gh issue view "$gh_issue" --json body --jq '.body' 2>/dev/null | \
288
+ grep -o 'Jira Key:.*' | sed 's/.*\*\*Jira Key:\*\* //' | tr -d '[:space:]' || true)
289
+
290
+ if [[ -z "$jira_key" ]]; then
291
+ error "No Jira Key found in GitHub issue #${gh_issue}"
292
+ echo -e " ${DIM}The issue body must contain: **Jira Key:** PROJECT-123${RESET}"
293
+ return 1
294
+ fi
295
+
296
+ # Map status to Jira transition
297
+ local target_name="" status_label=""
298
+ case "$status" in
299
+ started|in-progress|in_progress)
300
+ target_name="In Progress"
301
+ status_label="In Progress"
302
+ ;;
303
+ review|in-review|in_review|pr)
304
+ target_name="In Review"
305
+ status_label="In Review"
306
+ ;;
307
+ done|completed|merged)
308
+ target_name="Done"
309
+ status_label="Done"
310
+ ;;
311
+ failed|error)
312
+ # Don't change status, just add a comment
313
+ local comment="Pipeline failed for GitHub issue #${gh_issue}"
314
+ if [[ -n "$detail" ]]; then
315
+ comment="${comment}\n\n${detail}"
316
+ fi
317
+ jira_add_comment "$jira_key" "$comment" >/dev/null 2>&1 || return 1
318
+ warn "Added failure comment to Jira issue ${jira_key}"
319
+ emit_event "jira.update" "github_issue=$gh_issue" "status=failed"
320
+ return 0
321
+ ;;
322
+ *)
323
+ error "Unknown status: ${status}"
324
+ echo -e " Valid: ${CYAN}started${RESET} | ${CYAN}review${RESET} | ${CYAN}done${RESET} | ${CYAN}failed${RESET}"
325
+ return 1
326
+ ;;
327
+ esac
328
+
329
+ jira_transition "$jira_key" "$target_name" >/dev/null 2>&1 || return 1
330
+
331
+ # Add status transition comment
332
+ local comment="Status updated to ${status_label} (GitHub #${gh_issue})"
333
+ if [[ -n "$detail" ]]; then
334
+ comment="${comment}\n\n${detail}"
335
+ fi
336
+ jira_add_comment "$jira_key" "$comment" >/dev/null 2>&1 || true
337
+
338
+ success "Jira ${jira_key} updated → ${status_label} (GitHub #${gh_issue})"
339
+ emit_event "jira.update" "github_issue=$gh_issue" "jira_key=$jira_key" "status=$status"
340
+ }
341
+
342
+ # ─── Status Dashboard ──────────────────────────────────────────────────────
343
+
344
+ cmd_status() {
345
+ check_config
346
+
347
+ echo -e "${PURPLE}${BOLD}━━━ Jira Board Status ━━━${RESET}"
348
+ echo ""
349
+
350
+ # Query issues by status
351
+ local statuses="To Do:To Do:YELLOW In Progress:In Progress:CYAN In Review:In Review:BLUE Done:Done:GREEN"
352
+
353
+ for entry in $statuses; do
354
+ local status_name="${entry%%:*}"
355
+ local rest="${entry#*:}"
356
+ local display_name="${rest%%:*}"
357
+ local color_name="${rest#*:}"
358
+
359
+ local color="$DIM"
360
+ case "$color_name" in
361
+ CYAN) color="$CYAN" ;;
362
+ BLUE) color="$BLUE" ;;
363
+ GREEN) color="$GREEN" ;;
364
+ YELLOW) color="$YELLOW" ;;
365
+ esac
366
+
367
+ # URL-encode the status name for JQL
368
+ local jql
369
+ jql=$(printf 'project = %s AND status = "%s"' "$JIRA_PROJECT_KEY" "$status_name")
370
+ local encoded_jql
371
+ encoded_jql=$(printf '%s' "$jql" | jq -sRr @uri)
372
+
373
+ local response
374
+ response=$(jira_api "GET" "search?jql=${encoded_jql}&fields=summary,status&maxResults=50" 2>/dev/null) || {
375
+ echo -e " ${RED}✗${RESET} ${display_name}: ${DIM}(API error)${RESET}"
376
+ continue
377
+ }
378
+
379
+ local count
380
+ count=$(echo "$response" | jq '.total // 0')
381
+
382
+ echo -e " ${color}${BOLD}${display_name}${RESET} ${count}"
383
+
384
+ # Show individual issues for active states
385
+ if [[ "$count" -gt 0 ]] && [[ "$status_name" != "Done" ]]; then
386
+ local issue_count
387
+ issue_count=$(echo "$response" | jq '.issues | length')
388
+ local j=0
389
+ while [[ $j -lt $issue_count ]]; do
390
+ local key title
391
+ key=$(echo "$response" | jq -r ".issues[$j].key")
392
+ title=$(echo "$response" | jq -r ".issues[$j].fields.summary")
393
+ echo -e " ${DIM}${key}${RESET} ${title}"
394
+ j=$((j + 1))
395
+ done
396
+ fi
397
+ done
398
+
399
+ echo ""
400
+
401
+ # Show recent sync events
402
+ if [[ -f "$EVENTS_FILE" ]]; then
403
+ local recent_syncs
404
+ recent_syncs=$(grep '"type":"jira\.' "$EVENTS_FILE" 2>/dev/null | tail -5 || true)
405
+ if [[ -n "$recent_syncs" ]]; then
406
+ echo -e "${BOLD}Recent Activity${RESET}"
407
+ echo "$recent_syncs" | while IFS= read -r line; do
408
+ local ts type
409
+ ts=$(echo "$line" | jq -r '.ts' 2>/dev/null || true)
410
+ type=$(echo "$line" | jq -r '.type' 2>/dev/null || true)
411
+ local short_ts="${ts:-unknown}"
412
+ echo -e " ${DIM}${short_ts}${RESET} ${type}"
413
+ done
414
+ echo ""
415
+ fi
416
+ fi
417
+ }
418
+
419
+ # ─── Init: Save Configuration ──────────────────────────────────────────────
420
+
421
+ cmd_init() {
422
+ echo -e "${PURPLE}${BOLD}━━━ Jira Integration Setup ━━━${RESET}"
423
+ echo ""
424
+
425
+ mkdir -p "$CONFIG_DIR"
426
+
427
+ # Base URL
428
+ local base_url="${JIRA_BASE_URL:-}"
429
+ if [[ -z "$base_url" ]]; then
430
+ echo -e " ${CYAN}1.${RESET} Enter your Jira base URL (e.g. ${DIM}https://your-org.atlassian.net${RESET})"
431
+ echo ""
432
+ read -rp " Jira Base URL: " base_url
433
+ if [[ -z "$base_url" ]]; then
434
+ error "Base URL is required"
435
+ exit 1
436
+ fi
437
+ # Strip trailing slash
438
+ base_url="${base_url%/}"
439
+ fi
440
+
441
+ # Email
442
+ local email="${JIRA_EMAIL:-}"
443
+ if [[ -z "$email" ]]; then
444
+ echo ""
445
+ echo -e " ${CYAN}2.${RESET} Enter your Jira account email"
446
+ echo ""
447
+ read -rp " Email: " email
448
+ if [[ -z "$email" ]]; then
449
+ error "Email is required"
450
+ exit 1
451
+ fi
452
+ fi
453
+
454
+ # API Token
455
+ local api_token="${JIRA_API_TOKEN:-}"
456
+ if [[ -z "$api_token" ]]; then
457
+ echo ""
458
+ echo -e " ${CYAN}3.${RESET} Create an API token at ${DIM}https://id.atlassian.com/manage-profile/security/api-tokens${RESET}"
459
+ echo -e " Paste it below"
460
+ echo ""
461
+ read -rp " API Token: " api_token
462
+ if [[ -z "$api_token" ]]; then
463
+ error "API token is required"
464
+ exit 1
465
+ fi
466
+ fi
467
+
468
+ # Project Key
469
+ local project_key="${JIRA_PROJECT_KEY:-}"
470
+ if [[ -z "$project_key" ]]; then
471
+ echo ""
472
+ echo -e " ${CYAN}4.${RESET} Enter your Jira project key (e.g. ${DIM}PROJ${RESET})"
473
+ echo ""
474
+ read -rp " Project Key: " project_key
475
+ if [[ -z "$project_key" ]]; then
476
+ error "Project key is required"
477
+ exit 1
478
+ fi
479
+ fi
480
+
481
+ # Merge into existing tracker-config.json if present
482
+ local tmp_config="${TRACKER_CONFIG}.tmp"
483
+ local existing="{}"
484
+ if [[ -f "$TRACKER_CONFIG" ]]; then
485
+ existing=$(cat "$TRACKER_CONFIG" 2>/dev/null || echo "{}")
486
+ fi
487
+
488
+ echo "$existing" | jq \
489
+ --arg base_url "$base_url" \
490
+ --arg email "$email" \
491
+ --arg api_token "$api_token" \
492
+ --arg project_key "$project_key" \
493
+ --arg provider "jira" \
494
+ --arg updated_at "$(now_iso)" \
495
+ '. + {
496
+ provider: $provider,
497
+ jira_base_url: $base_url,
498
+ jira_email: $email,
499
+ jira_api_token: $api_token,
500
+ jira_project_key: $project_key,
501
+ jira_updated_at: $updated_at
502
+ }' > "$tmp_config"
503
+ mv "$tmp_config" "$TRACKER_CONFIG"
504
+ chmod 600 "$TRACKER_CONFIG"
505
+
506
+ success "Configuration saved to ${TRACKER_CONFIG}"
507
+ echo ""
508
+
509
+ # Validate connection
510
+ info "Validating Jira connection..."
511
+ JIRA_BASE_URL="$base_url"
512
+ JIRA_EMAIL="$email"
513
+ JIRA_API_TOKEN="$api_token"
514
+ JIRA_PROJECT_KEY="$project_key"
515
+
516
+ local test_response
517
+ test_response=$(jira_api "GET" "myself") || {
518
+ error "Jira connection failed — check your credentials"
519
+ exit 1
520
+ }
521
+
522
+ local display_name
523
+ display_name=$(echo "$test_response" | jq -r '.displayName // "Unknown"')
524
+ success "Authenticated as: ${display_name}"
525
+
526
+ emit_event "jira.init" "user=$display_name" "project=$project_key"
527
+ }
528
+
529
+ # ─── Daemon Integration: Notify Jira on Pipeline Events ───────────────────
530
+ # Called by sw-daemon.sh at spawn, stage transition, completion, and failure.
531
+ # This function is designed to be sourced or called externally.
532
+
533
+ jira_notify() {
534
+ local event="$1"
535
+ local gh_issue="${2:-}"
536
+ local detail="${3:-}"
537
+
538
+ # Only proceed if Jira config exists and credentials are available
539
+ load_config
540
+ if [[ -z "$JIRA_BASE_URL" ]] || [[ -z "$JIRA_EMAIL" ]] || [[ -z "$JIRA_API_TOKEN" ]]; then
541
+ return 0 # silently skip if no Jira integration
542
+ fi
543
+
544
+ # Find the Jira key from GitHub issue
545
+ local jira_key=""
546
+ if [[ -n "$gh_issue" ]]; then
547
+ jira_key=$(gh issue view "$gh_issue" --json body --jq '.body' 2>/dev/null | \
548
+ grep -o 'Jira Key:.*' | sed 's/.*\*\*Jira Key:\*\* //' | tr -d '[:space:]' || true)
549
+ fi
550
+
551
+ if [[ -z "$jira_key" ]]; then
552
+ return 0 # no linked Jira issue
553
+ fi
554
+
555
+ case "$event" in
556
+ spawn|started)
557
+ jira_transition "$jira_key" "In Progress" >/dev/null 2>&1 || true
558
+ jira_add_comment "$jira_key" "Pipeline started for GitHub issue #${gh_issue}" >/dev/null 2>&1 || true
559
+ ;;
560
+ review|pr-created)
561
+ jira_transition "$jira_key" "In Review" >/dev/null 2>&1 || true
562
+ if [[ -n "$detail" ]]; then
563
+ jira_add_comment "$jira_key" "PR linked: ${detail} (GitHub #${gh_issue})" >/dev/null 2>&1 || true
564
+ fi
565
+ ;;
566
+ completed|done)
567
+ jira_transition "$jira_key" "Done" >/dev/null 2>&1 || true
568
+ jira_add_comment "$jira_key" "Pipeline completed successfully for GitHub issue #${gh_issue}" >/dev/null 2>&1 || true
569
+ ;;
570
+ failed)
571
+ local msg="Pipeline failed for GitHub issue #${gh_issue}"
572
+ if [[ -n "$detail" ]]; then
573
+ msg="${msg}\n\nDetails:\n${detail}"
574
+ fi
575
+ jira_add_comment "$jira_key" "$msg" >/dev/null 2>&1 || true
576
+ ;;
577
+ esac
578
+
579
+ emit_event "jira.notify" "event=$event" "github_issue=$gh_issue" "jira_key=$jira_key"
580
+ }
581
+
582
+ # ─── Help ──────────────────────────────────────────────────────────────────
583
+
584
+ show_help() {
585
+ echo -e "${CYAN}${BOLD}shipwright jira${RESET} — Jira ↔ GitHub Bidirectional Sync"
586
+ echo ""
587
+ echo -e "${BOLD}USAGE${RESET}"
588
+ echo -e " ${CYAN}shipwright jira${RESET} <command> [options]"
589
+ echo ""
590
+ echo -e "${BOLD}COMMANDS${RESET}"
591
+ echo -e " ${CYAN}sync${RESET} [--dry-run] Sync Jira To Do issues → GitHub"
592
+ echo -e " ${CYAN}update${RESET} <issue> <status> Update linked Jira ticket status"
593
+ echo -e " ${CYAN}status${RESET} Show Jira board dashboard"
594
+ echo -e " ${CYAN}init${RESET} Configure Jira connection"
595
+ echo -e " ${CYAN}help${RESET} Show this help"
596
+ echo ""
597
+ echo -e "${BOLD}STATUS VALUES${RESET}"
598
+ echo -e " ${CYAN}started${RESET} Pipeline spawned → Jira: In Progress"
599
+ echo -e " ${CYAN}review${RESET} PR created → Jira: In Review"
600
+ echo -e " ${CYAN}done${RESET} Pipeline complete → Jira: Done"
601
+ echo -e " ${CYAN}failed${RESET} Pipeline failed → Jira: adds failure comment"
602
+ echo ""
603
+ echo -e "${BOLD}EXAMPLES${RESET}"
604
+ echo -e " ${DIM}shipwright jira init${RESET} # Set up Jira connection"
605
+ echo -e " ${DIM}shipwright jira sync${RESET} # Sync To Do → GitHub"
606
+ echo -e " ${DIM}shipwright jira sync --dry-run${RESET} # Preview what would sync"
607
+ echo -e " ${DIM}shipwright jira update 42 started${RESET} # Mark as In Progress"
608
+ echo -e " ${DIM}shipwright jira update 42 review${RESET} # Mark as In Review"
609
+ echo -e " ${DIM}shipwright jira update 42 done${RESET} # Mark as Done"
610
+ echo -e " ${DIM}shipwright jira status${RESET} # Show board dashboard"
611
+ echo ""
612
+ echo -e "${BOLD}ENVIRONMENT${RESET}"
613
+ echo -e " ${DIM}JIRA_BASE_URL${RESET} Jira instance URL (or use 'jira init' to save)"
614
+ echo -e " ${DIM}JIRA_EMAIL${RESET} Account email for authentication"
615
+ echo -e " ${DIM}JIRA_API_TOKEN${RESET} API token from Atlassian account"
616
+ echo -e " ${DIM}JIRA_PROJECT_KEY${RESET} Jira project key (e.g. PROJ)"
617
+ }
618
+
619
+ # ─── Command Router ─────────────────────────────────────────────────────
620
+
621
+ main() {
622
+ load_config
623
+
624
+ local cmd="${1:-help}"
625
+ shift 2>/dev/null || true
626
+
627
+ case "$cmd" in
628
+ sync) cmd_sync "$@" ;;
629
+ update) cmd_update "$@" ;;
630
+ status) cmd_status "$@" ;;
631
+ init) cmd_init "$@" ;;
632
+ notify) jira_notify "$@" ;;
633
+ help|--help|-h) show_help ;;
634
+ *)
635
+ error "Unknown command: ${cmd}"
636
+ echo ""
637
+ show_help
638
+ exit 1
639
+ ;;
640
+ esac
641
+ }
642
+
643
+ main "$@"