shipwright-cli 2.4.0 → 3.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 (161) hide show
  1. package/README.md +16 -11
  2. package/completions/_shipwright +1 -1
  3. package/completions/shipwright.bash +3 -8
  4. package/completions/shipwright.fish +1 -1
  5. package/config/defaults.json +111 -0
  6. package/config/event-schema.json +81 -0
  7. package/config/policy.json +13 -18
  8. package/dashboard/coverage/coverage-summary.json +14 -0
  9. package/dashboard/public/index.html +1 -1
  10. package/dashboard/server.ts +306 -17
  11. package/dashboard/src/components/charts/bar.test.ts +79 -0
  12. package/dashboard/src/components/charts/donut.test.ts +68 -0
  13. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  14. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  15. package/dashboard/src/core/api.test.ts +309 -0
  16. package/dashboard/src/core/helpers.test.ts +301 -0
  17. package/dashboard/src/core/router.test.ts +307 -0
  18. package/dashboard/src/core/router.ts +7 -0
  19. package/dashboard/src/core/sse.test.ts +144 -0
  20. package/dashboard/src/views/metrics.test.ts +186 -0
  21. package/dashboard/src/views/overview.test.ts +173 -0
  22. package/dashboard/src/views/pipelines.test.ts +183 -0
  23. package/dashboard/src/views/team.test.ts +253 -0
  24. package/dashboard/vitest.config.ts +14 -5
  25. package/docs/TIPS.md +1 -1
  26. package/docs/patterns/README.md +1 -1
  27. package/package.json +5 -7
  28. package/scripts/adapters/docker-deploy.sh +1 -1
  29. package/scripts/adapters/tmux-adapter.sh +11 -1
  30. package/scripts/adapters/wezterm-adapter.sh +1 -1
  31. package/scripts/check-version-consistency.sh +1 -1
  32. package/scripts/lib/architecture.sh +126 -0
  33. package/scripts/lib/bootstrap.sh +75 -0
  34. package/scripts/lib/compat.sh +89 -6
  35. package/scripts/lib/config.sh +91 -0
  36. package/scripts/lib/daemon-adaptive.sh +3 -3
  37. package/scripts/lib/daemon-dispatch.sh +39 -16
  38. package/scripts/lib/daemon-health.sh +1 -1
  39. package/scripts/lib/daemon-patrol.sh +24 -12
  40. package/scripts/lib/daemon-poll.sh +37 -25
  41. package/scripts/lib/daemon-state.sh +115 -23
  42. package/scripts/lib/daemon-triage.sh +30 -8
  43. package/scripts/lib/fleet-failover.sh +63 -0
  44. package/scripts/lib/helpers.sh +30 -6
  45. package/scripts/lib/pipeline-detection.sh +2 -2
  46. package/scripts/lib/pipeline-github.sh +9 -9
  47. package/scripts/lib/pipeline-intelligence.sh +85 -35
  48. package/scripts/lib/pipeline-quality-checks.sh +16 -16
  49. package/scripts/lib/pipeline-quality.sh +1 -1
  50. package/scripts/lib/pipeline-stages.sh +242 -28
  51. package/scripts/lib/pipeline-state.sh +40 -4
  52. package/scripts/lib/test-helpers.sh +247 -0
  53. package/scripts/postinstall.mjs +3 -11
  54. package/scripts/sw +10 -4
  55. package/scripts/sw-activity.sh +1 -11
  56. package/scripts/sw-adaptive.sh +109 -85
  57. package/scripts/sw-adversarial.sh +4 -14
  58. package/scripts/sw-architecture-enforcer.sh +1 -11
  59. package/scripts/sw-auth.sh +8 -17
  60. package/scripts/sw-autonomous.sh +111 -49
  61. package/scripts/sw-changelog.sh +1 -11
  62. package/scripts/sw-checkpoint.sh +144 -20
  63. package/scripts/sw-ci.sh +2 -12
  64. package/scripts/sw-cleanup.sh +13 -17
  65. package/scripts/sw-code-review.sh +16 -36
  66. package/scripts/sw-connect.sh +5 -12
  67. package/scripts/sw-context.sh +9 -26
  68. package/scripts/sw-cost.sh +6 -16
  69. package/scripts/sw-daemon.sh +75 -70
  70. package/scripts/sw-dashboard.sh +57 -17
  71. package/scripts/sw-db.sh +506 -15
  72. package/scripts/sw-decompose.sh +1 -11
  73. package/scripts/sw-deps.sh +15 -25
  74. package/scripts/sw-developer-simulation.sh +1 -11
  75. package/scripts/sw-discovery.sh +112 -30
  76. package/scripts/sw-doc-fleet.sh +7 -17
  77. package/scripts/sw-docs-agent.sh +6 -16
  78. package/scripts/sw-docs.sh +4 -12
  79. package/scripts/sw-doctor.sh +134 -43
  80. package/scripts/sw-dora.sh +11 -19
  81. package/scripts/sw-durable.sh +35 -52
  82. package/scripts/sw-e2e-orchestrator.sh +11 -27
  83. package/scripts/sw-eventbus.sh +115 -115
  84. package/scripts/sw-evidence.sh +114 -30
  85. package/scripts/sw-feedback.sh +3 -13
  86. package/scripts/sw-fix.sh +2 -20
  87. package/scripts/sw-fleet-discover.sh +1 -11
  88. package/scripts/sw-fleet-viz.sh +10 -18
  89. package/scripts/sw-fleet.sh +13 -17
  90. package/scripts/sw-github-app.sh +6 -16
  91. package/scripts/sw-github-checks.sh +1 -11
  92. package/scripts/sw-github-deploy.sh +1 -11
  93. package/scripts/sw-github-graphql.sh +2 -12
  94. package/scripts/sw-guild.sh +1 -11
  95. package/scripts/sw-heartbeat.sh +49 -12
  96. package/scripts/sw-hygiene.sh +45 -43
  97. package/scripts/sw-incident.sh +48 -74
  98. package/scripts/sw-init.sh +35 -37
  99. package/scripts/sw-instrument.sh +1 -11
  100. package/scripts/sw-intelligence.sh +362 -51
  101. package/scripts/sw-jira.sh +5 -14
  102. package/scripts/sw-launchd.sh +2 -12
  103. package/scripts/sw-linear.sh +8 -17
  104. package/scripts/sw-logs.sh +4 -12
  105. package/scripts/sw-loop.sh +641 -90
  106. package/scripts/sw-memory.sh +243 -17
  107. package/scripts/sw-mission-control.sh +2 -12
  108. package/scripts/sw-model-router.sh +73 -34
  109. package/scripts/sw-otel.sh +11 -21
  110. package/scripts/sw-oversight.sh +1 -11
  111. package/scripts/sw-patrol-meta.sh +5 -11
  112. package/scripts/sw-pipeline-composer.sh +7 -17
  113. package/scripts/sw-pipeline-vitals.sh +1 -11
  114. package/scripts/sw-pipeline.sh +478 -122
  115. package/scripts/sw-pm.sh +2 -12
  116. package/scripts/sw-pr-lifecycle.sh +27 -25
  117. package/scripts/sw-predictive.sh +16 -22
  118. package/scripts/sw-prep.sh +6 -16
  119. package/scripts/sw-ps.sh +1 -11
  120. package/scripts/sw-public-dashboard.sh +2 -12
  121. package/scripts/sw-quality.sh +77 -10
  122. package/scripts/sw-reaper.sh +1 -11
  123. package/scripts/sw-recruit.sh +15 -25
  124. package/scripts/sw-regression.sh +11 -21
  125. package/scripts/sw-release-manager.sh +19 -28
  126. package/scripts/sw-release.sh +8 -16
  127. package/scripts/sw-remote.sh +1 -11
  128. package/scripts/sw-replay.sh +48 -44
  129. package/scripts/sw-retro.sh +70 -92
  130. package/scripts/sw-review-rerun.sh +1 -1
  131. package/scripts/sw-scale.sh +109 -32
  132. package/scripts/sw-security-audit.sh +12 -22
  133. package/scripts/sw-self-optimize.sh +239 -23
  134. package/scripts/sw-session.sh +3 -13
  135. package/scripts/sw-setup.sh +8 -18
  136. package/scripts/sw-standup.sh +5 -15
  137. package/scripts/sw-status.sh +32 -23
  138. package/scripts/sw-strategic.sh +129 -13
  139. package/scripts/sw-stream.sh +1 -11
  140. package/scripts/sw-swarm.sh +76 -36
  141. package/scripts/sw-team-stages.sh +10 -20
  142. package/scripts/sw-templates.sh +4 -14
  143. package/scripts/sw-testgen.sh +3 -13
  144. package/scripts/sw-tmux-pipeline.sh +1 -19
  145. package/scripts/sw-tmux-role-color.sh +0 -10
  146. package/scripts/sw-tmux-status.sh +3 -11
  147. package/scripts/sw-tmux.sh +2 -20
  148. package/scripts/sw-trace.sh +1 -19
  149. package/scripts/sw-tracker-github.sh +0 -10
  150. package/scripts/sw-tracker-jira.sh +1 -11
  151. package/scripts/sw-tracker-linear.sh +1 -11
  152. package/scripts/sw-tracker.sh +7 -24
  153. package/scripts/sw-triage.sh +24 -34
  154. package/scripts/sw-upgrade.sh +5 -23
  155. package/scripts/sw-ux.sh +1 -19
  156. package/scripts/sw-webhook.sh +18 -32
  157. package/scripts/sw-widgets.sh +3 -21
  158. package/scripts/sw-worktree.sh +11 -27
  159. package/scripts/update-homebrew-sha.sh +67 -0
  160. package/templates/pipelines/tdd.json +72 -0
  161. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -8,7 +8,7 @@
8
8
  set -euo pipefail
9
9
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
10
 
11
- VERSION="2.4.0"
11
+ VERSION="3.0.0"
12
12
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
13
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
14
14
 
@@ -16,6 +16,7 @@ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
16
16
  [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
17
17
  # shellcheck source=lib/helpers.sh
18
18
  [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
19
+ [[ -f "$SCRIPT_DIR/lib/config.sh" ]] && source "$SCRIPT_DIR/lib/config.sh"
19
20
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
20
21
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
21
22
  [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
@@ -24,21 +25,13 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
24
25
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
25
26
  now_epoch() { date +%s; }
26
27
  fi
27
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
28
- emit_event() {
29
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
30
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
31
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
32
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
33
- }
34
- fi
35
28
 
36
29
  # Cross-platform timeout: macOS lacks GNU timeout
37
30
  _run_with_timeout() {
38
31
  local secs="$1"; shift
39
- if command -v gtimeout &>/dev/null; then
32
+ if command -v gtimeout >/dev/null 2>&1; then
40
33
  gtimeout "$secs" "$@"
41
- elif command -v timeout &>/dev/null; then
34
+ elif command -v timeout >/dev/null 2>&1; then
42
35
  timeout "$secs" "$@"
43
36
  else
44
37
  # Fallback: run without timeout
@@ -83,6 +76,61 @@ get_require_fresh() {
83
76
  fi
84
77
  }
85
78
 
79
+ # ═════════════════════════════════════════════════════════════════════════════
80
+ # ASSERTION EVALUATION
81
+ # Checks response body against assertions defined in policy.json.
82
+ # Assertion names map to simple content checks (case-insensitive).
83
+ # Returns the count of failed assertions (0 = all passed).
84
+ # ═════════════════════════════════════════════════════════════════════════════
85
+
86
+ evaluate_assertions() {
87
+ local collector_json="$1"
88
+ local response_body="$2"
89
+ local failed=0
90
+
91
+ local assertions
92
+ assertions=$(echo "$collector_json" | jq -r '.assertions[]? // empty' 2>/dev/null)
93
+ [[ -z "$assertions" ]] && { echo "0"; return; }
94
+
95
+ while IFS= read -r assertion; do
96
+ [[ -z "$assertion" ]] && continue
97
+ local check_passed="false"
98
+
99
+ case "$assertion" in
100
+ # Common assertion patterns — map names to body content checks
101
+ page-title-visible)
102
+ echo "$response_body" | grep -qi '<title>' && check_passed="true" ;;
103
+ websocket-connected|websocket-active)
104
+ echo "$response_body" | grep -qi 'websocket\|ws://' && check_passed="true" ;;
105
+ status-ok)
106
+ echo "$response_body" | grep -qi '"status"' && check_passed="true" ;;
107
+ response-has-version)
108
+ echo "$response_body" | grep -qi '"version"' && check_passed="true" ;;
109
+ valid-json-output|valid-json)
110
+ echo "$response_body" | jq empty 2>/dev/null && check_passed="true" ;;
111
+ has-pipeline-state)
112
+ echo "$response_body" | grep -qi 'pipeline\|status\|stage' && check_passed="true" ;;
113
+ stage-list-rendered)
114
+ echo "$response_body" | grep -qi 'stage\|pipeline' && check_passed="true" ;;
115
+ progress-indicator-visible)
116
+ echo "$response_body" | grep -qi 'progress\|percent\|stage' && check_passed="true" ;;
117
+ schema-valid|db-accessible)
118
+ echo "$response_body" | grep -qi 'schema\|version\|ok\|healthy' && check_passed="true" ;;
119
+ *)
120
+ # Generic: check if the assertion name (with hyphens as spaces) appears in body
121
+ local search_term="${assertion//-/ }"
122
+ echo "$response_body" | grep -qi "$search_term" && check_passed="true" ;;
123
+ esac
124
+
125
+ if [[ "$check_passed" != "true" ]]; then
126
+ warn "[assertion] '${assertion}' not satisfied" >&2
127
+ failed=$((failed + 1))
128
+ fi
129
+ done <<< "$assertions"
130
+
131
+ echo "$failed"
132
+ }
133
+
86
134
  # ═════════════════════════════════════════════════════════════════════════════
87
135
  # TYPE-SPECIFIC COLLECTORS
88
136
  # Each returns a JSON evidence record written to EVIDENCE_DIR/<name>.json
@@ -96,7 +144,8 @@ collect_browser() {
96
144
 
97
145
  local entrypoint base_url url
98
146
  entrypoint=$(echo "$collector_json" | jq -r '.entrypoint // "/"')
99
- base_url=$(echo "$collector_json" | jq -r '.baseUrl // "http://localhost:3000"')
147
+ base_url=$(echo "$collector_json" | jq -r '.baseUrl // ""')
148
+ [[ -z "$base_url" ]] && base_url="http://localhost:$(_config_get_int "dashboard.port" 8767)"
100
149
  url="${base_url}${entrypoint}"
101
150
 
102
151
  info "[browser] ${name}: ${url}"
@@ -105,8 +154,9 @@ collect_browser() {
105
154
  local response_size="0"
106
155
  local response_body=""
107
156
 
108
- if command -v curl &>/dev/null; then
109
- local tmpfile="/tmp/sw-evidence-${name}.txt"
157
+ if command -v curl >/dev/null 2>&1; then
158
+ local tmpfile
159
+ tmpfile=$(mktemp "${TMPDIR:-/tmp}/sw-evidence-browser.XXXXXX")
110
160
  http_status=$(curl -s -o "$tmpfile" -w "%{http_code}" --max-time 30 "$url" 2>/dev/null || echo "0")
111
161
  if [[ -f "$tmpfile" ]]; then
112
162
  response_size=$(wc -c < "$tmpfile" 2>/dev/null || echo "0")
@@ -118,9 +168,17 @@ collect_browser() {
118
168
  local passed="false"
119
169
  [[ "$http_status" -ge 200 && "$http_status" -lt 400 ]] && passed="true"
120
170
 
171
+ # Evaluate assertions against response body (if status check passed)
172
+ local assertion_failures=0
173
+ if [[ "$passed" == "true" && -n "$response_body" ]]; then
174
+ assertion_failures=$(evaluate_assertions "$collector_json" "$response_body")
175
+ [[ "$assertion_failures" -gt 0 ]] && passed="false"
176
+ fi
177
+
121
178
  write_evidence_record "$name" "browser" "$passed" \
122
179
  "$(jq -n --arg url "$url" --argjson status "$http_status" --argjson size "$response_size" \
123
- '{url: $url, http_status: $status, response_size: $size}')"
180
+ --argjson assertion_failures "$assertion_failures" \
181
+ '{url: $url, http_status: $status, response_size: $size, assertion_failures: $assertion_failures}')"
124
182
  }
125
183
 
126
184
  # ─── API: REST/GraphQL endpoint verification ─────────────────────────────────
@@ -138,7 +196,8 @@ collect_api() {
138
196
 
139
197
  if [[ -z "$url" ]]; then
140
198
  local base_url entrypoint
141
- base_url=$(echo "$collector_json" | jq -r '.baseUrl // "http://localhost:3000"')
199
+ base_url=$(echo "$collector_json" | jq -r '.baseUrl // ""')
200
+ [[ -z "$base_url" ]] && base_url="http://localhost:$(_config_get_int "dashboard.port" 8767)"
142
201
  entrypoint=$(echo "$collector_json" | jq -r '.entrypoint // "/"')
143
202
  url="${base_url}${entrypoint}"
144
203
  fi
@@ -150,9 +209,10 @@ collect_api() {
150
209
  local response_body=""
151
210
  local content_type=""
152
211
 
153
- if command -v curl &>/dev/null; then
154
- local tmpfile="/tmp/sw-evidence-${name}.txt"
155
- local header_file="/tmp/sw-evidence-${name}-headers.txt"
212
+ if command -v curl >/dev/null 2>&1; then
213
+ local tmpfile header_file
214
+ tmpfile=$(mktemp "${TMPDIR:-/tmp}/sw-evidence-api.XXXXXX")
215
+ header_file=$(mktemp "${TMPDIR:-/tmp}/sw-evidence-api-headers.XXXXXX")
156
216
  local curl_args=(-s -o "$tmpfile" -D "$header_file" -w "%{http_code}" -X "$method" --max-time "$timeout")
157
217
 
158
218
  # Add custom headers
@@ -181,6 +241,7 @@ collect_api() {
181
241
  content_type=$(grep -i "^content-type:" "$header_file" 2>/dev/null | head -1 | sed 's/^[^:]*: *//' | tr -d '\r' || echo "")
182
242
  rm -f "$header_file"
183
243
  fi
244
+ rm -f "$tmpfile"
184
245
  fi
185
246
 
186
247
  local passed="false"
@@ -192,12 +253,19 @@ collect_api() {
192
253
  valid_json="true"
193
254
  fi
194
255
 
256
+ # Evaluate assertions against response body (if status check passed)
257
+ local assertion_failures=0
258
+ if [[ "$passed" == "true" && -n "$response_body" ]]; then
259
+ assertion_failures=$(evaluate_assertions "$collector_json" "$response_body")
260
+ [[ "$assertion_failures" -gt 0 ]] && passed="false"
261
+ fi
262
+
195
263
  write_evidence_record "$name" "api" "$passed" \
196
264
  "$(jq -n --arg url "$url" --arg method "$method" \
197
265
  --argjson status "$http_status" --argjson expected "$expected_status" \
198
266
  --argjson size "$response_size" --arg content_type "$content_type" \
199
- --arg valid_json "$valid_json" \
200
- '{url: $url, method: $method, http_status: $status, expected_status: $expected, response_size: $size, content_type: $content_type, valid_json: ($valid_json == "true")}')"
267
+ --arg valid_json "$valid_json" --argjson assertion_failures "$assertion_failures" \
268
+ '{url: $url, method: $method, http_status: $status, expected_status: $expected, response_size: $size, content_type: $content_type, valid_json: ($valid_json == "true"), assertion_failures: $assertion_failures}')"
201
269
  }
202
270
 
203
271
  # ─── CLI: Execute a command and check exit code ──────────────────────────────
@@ -236,6 +304,13 @@ collect_cli() {
236
304
  valid_json="true"
237
305
  fi
238
306
 
307
+ # Evaluate assertions against command output (if exit code check passed)
308
+ local assertion_failures=0
309
+ if [[ "$passed" == "true" && -n "$output" ]]; then
310
+ assertion_failures=$(evaluate_assertions "$collector_json" "$output")
311
+ [[ "$assertion_failures" -gt 0 ]] && passed="false"
312
+ fi
313
+
239
314
  local output_size=${#output}
240
315
  # Truncate output for the evidence record (keep first 2000 chars)
241
316
  local output_preview="${output:0:2000}"
@@ -274,12 +349,20 @@ collect_database() {
274
349
  local passed="false"
275
350
  [[ "$exit_code" -eq "$expected_exit" ]] && passed="true"
276
351
 
352
+ # Evaluate assertions against command output (if exit code check passed)
353
+ local assertion_failures=0
354
+ if [[ "$passed" == "true" && -n "$output" ]]; then
355
+ assertion_failures=$(evaluate_assertions "$collector_json" "$output")
356
+ [[ "$assertion_failures" -gt 0 ]] && passed="false"
357
+ fi
358
+
277
359
  local output_preview="${output:0:2000}"
278
360
 
279
361
  write_evidence_record "$name" "database" "$passed" \
280
362
  "$(jq -n --arg cmd "$command_str" --argjson exit_code "$exit_code" \
281
363
  --argjson expected "$expected_exit" --arg output_preview "$output_preview" \
282
- '{command: $cmd, exit_code: $exit_code, expected_exit_code: $expected, output_preview: $output_preview}')"
364
+ --argjson assertion_failures "$assertion_failures" \
365
+ '{command: $cmd, exit_code: $exit_code, expected_exit_code: $expected, output_preview: $output_preview, assertion_failures: $assertion_failures}')"
283
366
  }
284
367
 
285
368
  # ─── Webhook: Issue a callback and verify response ───────────────────────────
@@ -306,8 +389,9 @@ collect_webhook() {
306
389
  local http_status="0"
307
390
  local response_body=""
308
391
 
309
- if command -v curl &>/dev/null; then
310
- local tmpfile="/tmp/sw-evidence-${name}.txt"
392
+ if command -v curl >/dev/null 2>&1; then
393
+ local tmpfile
394
+ tmpfile=$(mktemp "${TMPDIR:-/tmp}/sw-evidence-webhook.XXXXXX")
311
395
  http_status=$(curl -s -o "$tmpfile" -w "%{http_code}" -X "$method" \
312
396
  -H "Content-Type: application/json" -d "$body" \
313
397
  --max-time "$timeout" "$url" 2>/dev/null || echo "0")
@@ -433,7 +517,7 @@ cmd_capture() {
433
517
  *) warn "Unknown collector type: ${ctype} (skipping ${cname})" ; continue ;;
434
518
  esac
435
519
 
436
- ((total++))
520
+ total=$((total + 1))
437
521
 
438
522
  local evidence_file="${EVIDENCE_DIR}/${cname}.json"
439
523
  local cpassed="false"
@@ -442,9 +526,9 @@ cmd_capture() {
442
526
  fi
443
527
 
444
528
  if [[ "$cpassed" == "true" ]]; then
445
- ((passed++))
529
+ passed=$((passed + 1))
446
530
  else
447
- ((failed++))
531
+ failed=$((failed + 1))
448
532
  fi
449
533
 
450
534
  manifest_entries=$(echo "$manifest_entries" | jq \
@@ -506,7 +590,7 @@ cmd_verify() {
506
590
  if [[ "$require_fresh" == "true" && "$age_seconds" -gt "$max_age_seconds" ]]; then
507
591
  error "Evidence is stale: captured ${age_seconds}s ago (max: ${max_age_seconds}s)"
508
592
  all_passed="false"
509
- ((failed++))
593
+ failed=$((failed + 1))
510
594
  else
511
595
  local age_minutes=$((age_seconds / 60))
512
596
  info "Evidence age: ${age_minutes}m (max: ${max_age_minutes}m)"
@@ -521,7 +605,7 @@ cmd_verify() {
521
605
 
522
606
  while IFS= read -r entry; do
523
607
  [[ -z "$entry" ]] && continue
524
- ((checked++))
608
+ checked=$((checked + 1))
525
609
 
526
610
  local cname ctype cpassed
527
611
  cname=$(echo "$entry" | jq -r '.name')
@@ -531,7 +615,7 @@ cmd_verify() {
531
615
  if [[ "$cpassed" != "true" ]]; then
532
616
  error "Collector '${cname}' (${ctype}) failed"
533
617
  all_passed="false"
534
- ((failed++))
618
+ failed=$((failed + 1))
535
619
  else
536
620
  success "Collector '${cname}' (${ctype}) passed"
537
621
  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="2.4.0"
9
+ VERSION="3.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
34
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
35
  }
36
36
  fi
37
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
- RED="${RED:-\033[38;2;248;113;113m}"
43
- DIM="${DIM:-\033[2m}"
44
- BOLD="${BOLD:-\033[1m}"
45
- RESET="${RESET:-\033[0m}"
46
-
47
37
  # ─── Storage Paths ──────────────────────────────────────────────────────────
48
38
  INCIDENTS_FILE="${HOME}/.shipwright/incidents.jsonl"
49
39
  ERROR_THRESHOLD=5 # Create issue if error count >= threshold
@@ -262,7 +252,7 @@ git show $regression_commit
262
252
  EOF
263
253
  )
264
254
 
265
- if ! command -v gh &>/dev/null; then
255
+ if ! command -v gh >/dev/null 2>&1; then
266
256
  error "gh CLI not found — cannot create issue"
267
257
  return 1
268
258
  fi
@@ -275,7 +265,7 @@ EOF
275
265
  --body "$issue_body" \
276
266
  --label "shipwright" \
277
267
  --label "hotfix" \
278
- 2>&1 | tail -1)
268
+ 2>&1 | tail -1) || true
279
269
 
280
270
  if [[ -n "$issue_url" ]]; then
281
271
  success "Created issue: $issue_url"
package/scripts/sw-fix.sh CHANGED
@@ -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="2.4.0"
9
+ VERSION="3.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
@@ -25,24 +25,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
25
25
  now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
26
26
  now_epoch() { date +%s; }
27
27
  fi
28
- if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
29
- emit_event() {
30
- local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
31
- local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
32
- while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
33
- echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
34
- }
35
- fi
36
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
37
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
38
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
39
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
40
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
41
- RED="${RED:-\033[38;2;248;113;113m}"
42
- DIM="${DIM:-\033[2m}"
43
- BOLD="${BOLD:-\033[1m}"
44
- RESET="${RESET:-\033[0m}"
45
-
46
28
  format_duration() {
47
29
  local secs="$1"
48
30
  if [[ "$secs" -ge 3600 ]]; then
@@ -341,7 +323,7 @@ fix_start() {
341
323
 
342
324
  # Spawn pipeline in subshell
343
325
  (
344
- cd "$expanded"
326
+ cd "$expanded" || exit 1
345
327
 
346
328
  # Determine base branch
347
329
  local base
@@ -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="2.4.0"
9
+ VERSION="3.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
34
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
35
  }
36
36
  fi
37
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
- RED="${RED:-\033[38;2;248;113;113m}"
43
- DIM="${DIM:-\033[2m}"
44
- BOLD="${BOLD:-\033[1m}"
45
- RESET="${RESET:-\033[0m}"
46
-
47
37
  # ─── Help ───────────────────────────────────────────────────────────────────
48
38
 
49
39
  show_help() {
@@ -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="2.4.0"
9
+ VERSION="3.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -19,6 +19,8 @@ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
19
19
  [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
20
20
  # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
21
21
  [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
22
+ # Color fallbacks when helpers not loaded
23
+ : "${CYAN:=}" "${BOLD:=}" "${RESET:=}" "${DIM:=}" "${GREEN:=}" "${RED:=}" "${YELLOW:=}" "${PURPLE:=}" "${WHITE:=}" "${BLUE:=}"
22
24
  [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
23
25
  [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
24
26
  [[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
@@ -34,16 +36,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
36
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
37
  }
36
38
  fi
37
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
- RED="${RED:-\033[38;2;248;113;113m}"
43
- DIM="${DIM:-\033[2m}"
44
- BOLD="${BOLD:-\033[1m}"
45
- RESET="${RESET:-\033[0m}"
46
-
47
39
  format_duration() {
48
40
  local secs="$1"
49
41
  if [[ "$secs" -ge 3600 ]]; then
@@ -70,7 +62,7 @@ get_health_status() {
70
62
  # degraded (yellow) = some failures but recovering
71
63
  # failing (red) = persistent failures
72
64
 
73
- if ! command -v jq &>/dev/null; then
65
+ if ! command -v jq >/dev/null 2>&1; then
74
66
  echo "unknown"
75
67
  return
76
68
  fi
@@ -98,7 +90,7 @@ color_health() {
98
90
 
99
91
  # ─── Overview Subcommand ───────────────────────────────────────────────────
100
92
  show_overview() {
101
- if ! command -v jq &>/dev/null; then
93
+ if ! command -v jq >/dev/null 2>&1; then
102
94
  error "jq is required for fleet visualization"
103
95
  exit 1
104
96
  fi
@@ -144,7 +136,7 @@ show_overview() {
144
136
 
145
137
  # ─── Workers Subcommand ────────────────────────────────────────────────────
146
138
  show_workers() {
147
- if ! command -v jq &>/dev/null; then
139
+ if ! command -v jq >/dev/null 2>&1; then
148
140
  error "jq is required for fleet visualization"
149
141
  exit 1
150
142
  fi
@@ -197,7 +189,7 @@ show_workers() {
197
189
 
198
190
  # ─── Insights Subcommand ───────────────────────────────────────────────────
199
191
  show_insights() {
200
- if ! command -v jq &>/dev/null; then
192
+ if ! command -v jq >/dev/null 2>&1; then
201
193
  error "jq is required for fleet visualization"
202
194
  exit 1
203
195
  fi
@@ -238,7 +230,7 @@ show_insights() {
238
230
 
239
231
  # ─── Queue Subcommand ──────────────────────────────────────────────────────
240
232
  show_queue() {
241
- if ! command -v jq &>/dev/null; then
233
+ if ! command -v jq >/dev/null 2>&1; then
242
234
  error "jq is required for fleet visualization"
243
235
  exit 1
244
236
  fi
@@ -281,7 +273,7 @@ show_queue() {
281
273
 
282
274
  # ─── Costs Subcommand ──────────────────────────────────────────────────────
283
275
  show_costs() {
284
- if ! command -v jq &>/dev/null; then
276
+ if ! command -v jq >/dev/null 2>&1; then
285
277
  error "jq is required for fleet visualization"
286
278
  exit 1
287
279
  fi
@@ -323,7 +315,7 @@ show_costs() {
323
315
 
324
316
  # ─── Export Subcommand ─────────────────────────────────────────────────────
325
317
  show_export() {
326
- if ! command -v jq &>/dev/null; then
318
+ if ! command -v jq >/dev/null 2>&1; then
327
319
  error "jq is required for fleet visualization"
328
320
  exit 1
329
321
  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="2.4.0"
9
+ VERSION="3.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
34
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
35
  }
36
36
  fi
37
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
- RED="${RED:-\033[38;2;248;113;113m}"
43
- DIM="${DIM:-\033[2m}"
44
- BOLD="${BOLD:-\033[1m}"
45
- RESET="${RESET:-\033[0m}"
46
-
47
37
  epoch_to_iso() {
48
38
  local epoch="$1"
49
39
  date -u -r "$epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
@@ -200,7 +190,7 @@ _fleet_repo_priority() {
200
190
  local repo_path="$1"
201
191
  local priority=50 # default neutral priority
202
192
 
203
- type _gh_detect_repo &>/dev/null 2>&1 || { echo "$priority"; return 0; }
193
+ type _gh_detect_repo >/dev/null 2>&1 || { echo "$priority"; return 0; }
204
194
 
205
195
  # Detect repo from the repo path (run in subshell to avoid cd side-effects)
206
196
  local gh_priority
@@ -213,7 +203,7 @@ _fleet_repo_priority() {
213
203
  local p=50
214
204
 
215
205
  # Factor: security alerts (urgent work)
216
- if type gh_security_alerts &>/dev/null 2>&1; then
206
+ if type gh_security_alerts >/dev/null 2>&1; then
217
207
  local alerts
218
208
  alerts=$(gh_security_alerts "$owner" "$repo" 2>/dev/null | jq 'length' 2>/dev/null || echo "0")
219
209
  if [[ "${alerts:-0}" -gt 5 ]]; then
@@ -224,7 +214,7 @@ _fleet_repo_priority() {
224
214
  fi
225
215
 
226
216
  # Factor: contributor count (more contributors = more active = higher priority)
227
- if type gh_contributors &>/dev/null 2>&1; then
217
+ if type gh_contributors >/dev/null 2>&1; then
228
218
  local contribs
229
219
  contribs=$(gh_contributors "$owner" "$repo" 2>/dev/null | jq 'length' 2>/dev/null || echo "0")
230
220
  if [[ "${contribs:-0}" -gt 10 ]]; then
@@ -643,6 +633,12 @@ check_machine_health() {
643
633
  emit_event "fleet.machine_offline" "machine=$name" "host=$host"
644
634
  fi
645
635
  done
636
+
637
+ # After health checks: trigger failover to re-queue work from offline machines
638
+ if [[ -f "$SCRIPT_DIR/lib/fleet-failover.sh" ]]; then
639
+ source "$SCRIPT_DIR/lib/fleet-failover.sh" 2>/dev/null || true
640
+ fleet_failover_check 2>/dev/null || true
641
+ fi
646
642
  }
647
643
 
648
644
  # ─── Cross-Machine Event Aggregation ───────────────────────────────────
@@ -749,12 +745,12 @@ fleet_start() {
749
745
  echo -e "${PURPLE}${BOLD}━━━ shipwright fleet v${VERSION} — start ━━━${RESET}"
750
746
  echo ""
751
747
 
752
- if ! command -v tmux &>/dev/null; then
748
+ if ! command -v tmux >/dev/null 2>&1; then
753
749
  error "tmux is required for fleet mode"
754
750
  exit 1
755
751
  fi
756
752
 
757
- if ! command -v jq &>/dev/null; then
753
+ if ! command -v jq >/dev/null 2>&1; then
758
754
  error "jq is required. Install: brew install jq"
759
755
  exit 1
760
756
  fi
@@ -1146,7 +1142,7 @@ fleet_metrics() {
1146
1142
  exit 1
1147
1143
  fi
1148
1144
 
1149
- if ! command -v jq &>/dev/null; then
1145
+ if ! command -v jq >/dev/null 2>&1; then
1150
1146
  error "jq is required. Install: brew install jq"
1151
1147
  exit 1
1152
1148
  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="2.4.0"
9
+ VERSION="3.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
34
34
  echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
35
  }
36
36
  fi
37
- CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
- PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
- BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
- GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
- YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
- RED="${RED:-\033[38;2;248;113;113m}"
43
- DIM="${DIM:-\033[2m}"
44
- BOLD="${BOLD:-\033[1m}"
45
- RESET="${RESET:-\033[0m}"
46
-
47
37
  # ─── Config File Locations ────────────────────────────────────────────────
48
38
  CONFIG_DIR="${HOME}/.shipwright"
49
39
  CONFIG_FILE="${CONFIG_DIR}/github-app.json"
@@ -76,16 +66,16 @@ cmd_setup() {
76
66
  info "GitHub App Configuration"
77
67
  echo ""
78
68
 
79
- read -p "App ID: " app_id
80
- read -p "Private key file path: " key_path
69
+ read -rp "App ID: " app_id
70
+ read -rp "Private key file path: " key_path
81
71
 
82
72
  if [[ ! -f "$key_path" ]]; then
83
73
  error "Private key file not found: $key_path"
84
74
  return 1
85
75
  fi
86
76
 
87
- read -p "Installation ID: " installation_id
88
- read -p "Webhook secret (optional, press Enter to skip): " webhook_secret
77
+ read -rp "Installation ID: " installation_id
78
+ read -rp "Webhook secret (optional, press Enter to skip): " webhook_secret
89
79
 
90
80
  # Create config atomically
91
81
  local tmp_config
@@ -171,7 +161,7 @@ _get_installation_token() {
171
161
  fi
172
162
 
173
163
  local response
174
- response=$(curl -s -H "Authorization: Bearer $jwt" \
164
+ response=$(curl -s --connect-timeout 10 --max-time 30 -H "Authorization: Bearer $jwt" \
175
165
  -H "Accept: application/vnd.github+json" \
176
166
  "https://api.github.com/app/installations/${installation_id}/access_tokens" \
177
167
  -d '{}' -X POST 2>/dev/null) || true