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,710 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ sw-durable.sh — Durable Workflow Engine ║
4
+ # ║ Event log (WAL) · Checkpointing · Idempotency · Distributed locks ║
5
+ # ║ Dead letter queue · Exactly-once delivery · Compaction ║
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
+
13
+ # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
14
+ CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
15
+ PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
16
+ BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
17
+ GREEN='\033[38;2;74;222;128m' # success
18
+ YELLOW='\033[38;2;250;204;21m' # warning
19
+ RED='\033[38;2;248;113;113m' # error
20
+ DIM='\033[2m'
21
+ BOLD='\033[1m'
22
+ RESET='\033[0m'
23
+
24
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
25
+ # shellcheck source=lib/compat.sh
26
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
27
+
28
+ # ─── Output Helpers ─────────────────────────────────────────────────────────
29
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
30
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
31
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
32
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
33
+
34
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
35
+ now_epoch() { date +%s; }
36
+
37
+ # ─── Durable State Directory ────────────────────────────────────────────────
38
+ DURABLE_DIR="${HOME}/.shipwright/durable"
39
+
40
+ ensure_durable_dir() {
41
+ mkdir -p "$DURABLE_DIR/event-log"
42
+ mkdir -p "$DURABLE_DIR/checkpoints"
43
+ mkdir -p "$DURABLE_DIR/dlq"
44
+ mkdir -p "$DURABLE_DIR/locks"
45
+ mkdir -p "$DURABLE_DIR/offsets"
46
+ }
47
+
48
+ # ─── Event ID Generation ────────────────────────────────────────────────────
49
+ generate_event_id() {
50
+ local prefix="${1:-evt}"
51
+ local ts=$(now_epoch)
52
+ local rand=$(od -An -N4 -tu4 /dev/urandom | tr -d ' ')
53
+ echo "${prefix}-${ts}-${rand}"
54
+ }
55
+
56
+ # ─── Event Log (Write-Ahead Log) ────────────────────────────────────────────
57
+ event_log_file() {
58
+ echo "${DURABLE_DIR}/event-log/events.jsonl"
59
+ }
60
+
61
+ # Append event to WAL with sequence number
62
+ publish_event() {
63
+ local event_type="$1"
64
+ local payload="$2"
65
+ local event_id
66
+ event_id="$(generate_event_id "evt")"
67
+
68
+ ensure_durable_dir
69
+
70
+ # Get next sequence number (count existing lines + 1)
71
+ local seq=1
72
+ local log_file
73
+ log_file="$(event_log_file)"
74
+ if [[ -f "$log_file" ]]; then
75
+ seq=$(($(wc -l < "$log_file" || true) + 1))
76
+ fi
77
+
78
+ # Build event JSON atomically
79
+ local tmp_file
80
+ tmp_file="$(mktemp "${DURABLE_DIR}/.tmp.XXXXXX")"
81
+
82
+ jq -n \
83
+ --argjson sequence "$seq" \
84
+ --arg event_id "$event_id" \
85
+ --arg event_type "$event_type" \
86
+ --argjson payload "$(echo "$payload" | jq . 2>/dev/null || echo '{}')" \
87
+ --arg timestamp "$(now_iso)" \
88
+ --arg status "published" \
89
+ '{
90
+ sequence: $sequence,
91
+ event_id: $event_id,
92
+ event_type: $event_type,
93
+ payload: $payload,
94
+ timestamp: $timestamp,
95
+ status: $status
96
+ }' >> "$log_file" || { rm -f "$tmp_file"; return 1; }
97
+
98
+ rm -f "$tmp_file"
99
+ echo "$event_id"
100
+ }
101
+
102
+ # ─── Checkpointing ─────────────────────────────────────────────────────────
103
+ checkpoint_file() {
104
+ local workflow_id="$1"
105
+ echo "${DURABLE_DIR}/checkpoints/${workflow_id}.json"
106
+ }
107
+
108
+ save_checkpoint() {
109
+ local workflow_id="$1"
110
+ local stage="$2"
111
+ local seq="$3"
112
+ local state="$4"
113
+
114
+ ensure_durable_dir
115
+
116
+ local cp_file
117
+ cp_file="$(checkpoint_file "$workflow_id")"
118
+
119
+ local tmp_file
120
+ tmp_file="$(mktemp "${DURABLE_DIR}/.tmp.XXXXXX")"
121
+
122
+ jq -n \
123
+ --arg workflow_id "$workflow_id" \
124
+ --arg stage "$stage" \
125
+ --argjson sequence "$seq" \
126
+ --argjson state "$(echo "$state" | jq . 2>/dev/null || echo '{}')" \
127
+ --arg checkpoint_id "$(generate_event_id "cp")" \
128
+ --arg created_at "$(now_iso)" \
129
+ '{
130
+ workflow_id: $workflow_id,
131
+ stage: $stage,
132
+ sequence: $sequence,
133
+ state: $state,
134
+ checkpoint_id: $checkpoint_id,
135
+ created_at: $created_at
136
+ }' > "$tmp_file" || { rm -f "$tmp_file"; return 1; }
137
+
138
+ mv "$tmp_file" "$cp_file"
139
+ success "Checkpoint saved for workflow $workflow_id at stage $stage (seq: $seq)"
140
+ }
141
+
142
+ restore_checkpoint() {
143
+ local workflow_id="$1"
144
+ local cp_file
145
+ cp_file="$(checkpoint_file "$workflow_id")"
146
+
147
+ if [[ ! -f "$cp_file" ]]; then
148
+ error "No checkpoint found for workflow: $workflow_id"
149
+ return 1
150
+ fi
151
+
152
+ cat "$cp_file"
153
+ }
154
+
155
+ # ─── Idempotency Tracking ──────────────────────────────────────────────────
156
+ idempotency_key_file() {
157
+ local key="$1"
158
+ echo "${DURABLE_DIR}/offsets/idempotent-${key}.json"
159
+ }
160
+
161
+ is_operation_completed() {
162
+ local op_id="$1"
163
+ local key_file
164
+ key_file="$(idempotency_key_file "$op_id")"
165
+
166
+ [[ -f "$key_file" ]] && return 0 || return 1
167
+ }
168
+
169
+ mark_operation_completed() {
170
+ local op_id="$1"
171
+ local result="$2"
172
+
173
+ ensure_durable_dir
174
+
175
+ local key_file
176
+ key_file="$(idempotency_key_file "$op_id")"
177
+
178
+ local tmp_file
179
+ tmp_file="$(mktemp "${DURABLE_DIR}/.tmp.XXXXXX")"
180
+
181
+ jq -n \
182
+ --arg operation_id "$op_id" \
183
+ --argjson result "$(echo "$result" | jq . 2>/dev/null || echo '{}')" \
184
+ --arg completed_at "$(now_iso)" \
185
+ '{
186
+ operation_id: $operation_id,
187
+ result: $result,
188
+ completed_at: $completed_at
189
+ }' > "$tmp_file" || { rm -f "$tmp_file"; return 1; }
190
+
191
+ mv "$tmp_file" "$key_file"
192
+ }
193
+
194
+ get_operation_result() {
195
+ local op_id="$1"
196
+ local key_file
197
+ key_file="$(idempotency_key_file "$op_id")"
198
+
199
+ if [[ -f "$key_file" ]]; then
200
+ cat "$key_file"
201
+ return 0
202
+ fi
203
+
204
+ return 1
205
+ }
206
+
207
+ # ─── Distributed Locks ─────────────────────────────────────────────────────
208
+ lock_file() {
209
+ local resource="$1"
210
+ echo "${DURABLE_DIR}/locks/${resource}.lock"
211
+ }
212
+
213
+ acquire_lock() {
214
+ local resource="$1"
215
+ local timeout="${2:-30}"
216
+ local start_time
217
+ start_time="$(now_epoch)"
218
+
219
+ ensure_durable_dir
220
+
221
+ local lock_path
222
+ lock_path="$(lock_file "$resource")"
223
+
224
+ while true; do
225
+ # Try to create lock atomically (mkdir succeeds only if dir doesn't exist)
226
+ if mkdir "$lock_path" 2>/dev/null; then
227
+ # Write lock metadata
228
+ local tmp_file
229
+ tmp_file="$(mktemp "${DURABLE_DIR}/.tmp.XXXXXX")"
230
+
231
+ jq -n \
232
+ --arg resource "$resource" \
233
+ --argjson pid "$$" \
234
+ --arg acquired_at "$(now_iso)" \
235
+ '{
236
+ resource: $resource,
237
+ pid: $pid,
238
+ acquired_at: $acquired_at
239
+ }' > "$tmp_file"
240
+
241
+ mv "$tmp_file" "${lock_path}/metadata.json"
242
+ success "Lock acquired for: $resource"
243
+ return 0
244
+ fi
245
+
246
+ # Check lock staleness (if process is dead, break the lock)
247
+ if [[ -f "${lock_path}/metadata.json" ]]; then
248
+ local lock_pid
249
+ lock_pid="$(jq -r '.pid' "${lock_path}/metadata.json" 2>/dev/null || echo '')"
250
+
251
+ if [[ -n "$lock_pid" ]] && ! kill -0 "$lock_pid" 2>/dev/null; then
252
+ warn "Stale lock detected for $resource (PID $lock_pid dead), breaking lock"
253
+ rm -rf "$lock_path"
254
+ continue
255
+ fi
256
+ fi
257
+
258
+ # Check timeout
259
+ local now
260
+ now="$(now_epoch)"
261
+ if (( now - start_time >= timeout )); then
262
+ error "Failed to acquire lock for $resource after ${timeout}s"
263
+ return 1
264
+ fi
265
+
266
+ sleep 0.1
267
+ done
268
+ }
269
+
270
+ release_lock() {
271
+ local resource="$1"
272
+ local lock_path
273
+ lock_path="$(lock_file "$resource")"
274
+
275
+ if [[ -d "$lock_path" ]]; then
276
+ rm -rf "$lock_path"
277
+ success "Lock released for: $resource"
278
+ return 0
279
+ fi
280
+
281
+ return 1
282
+ }
283
+
284
+ # ─── Dead Letter Queue ─────────────────────────────────────────────────────
285
+ dlq_file() {
286
+ echo "${DURABLE_DIR}/dlq/deadletters.jsonl"
287
+ }
288
+
289
+ send_to_dlq() {
290
+ local event_id="$1"
291
+ local reason="$2"
292
+ local retries="${3:-0}"
293
+
294
+ ensure_durable_dir
295
+
296
+ local dlq_path
297
+ dlq_path="$(dlq_file)"
298
+
299
+ jq -n \
300
+ --arg event_id "$event_id" \
301
+ --arg reason "$reason" \
302
+ --argjson retry_count "$retries" \
303
+ --arg sent_to_dlq_at "$(now_iso)" \
304
+ '{
305
+ event_id: $event_id,
306
+ reason: $reason,
307
+ retry_count: $retry_count,
308
+ sent_to_dlq_at: $sent_to_dlq_at
309
+ }' >> "$dlq_path"
310
+
311
+ warn "Event $event_id sent to DLQ: $reason"
312
+ }
313
+
314
+ # ─── Consumer Offset Tracking ──────────────────────────────────────────────
315
+ consumer_offset_file() {
316
+ local consumer_id="$1"
317
+ echo "${DURABLE_DIR}/offsets/consumer-${consumer_id}.offset"
318
+ }
319
+
320
+ get_consumer_offset() {
321
+ local consumer_id="$1"
322
+ local offset_file
323
+ offset_file="$(consumer_offset_file "$consumer_id")"
324
+
325
+ if [[ -f "$offset_file" ]]; then
326
+ cat "$offset_file"
327
+ else
328
+ echo "0"
329
+ fi
330
+ }
331
+
332
+ save_consumer_offset() {
333
+ local consumer_id="$1"
334
+ local offset="$2"
335
+
336
+ ensure_durable_dir
337
+
338
+ local offset_file
339
+ offset_file="$(consumer_offset_file "$consumer_id")"
340
+
341
+ local tmp_file
342
+ tmp_file="$(mktemp "${DURABLE_DIR}/.tmp.XXXXXX")"
343
+
344
+ echo "$offset" > "$tmp_file"
345
+ mv "$tmp_file" "$offset_file"
346
+ }
347
+
348
+ # ─── Consume Events ────────────────────────────────────────────────────────
349
+ cmd_consume() {
350
+ local consumer_id="${1:-default}"
351
+ local handler_cmd="${2:-}"
352
+
353
+ if [[ -z "$handler_cmd" ]]; then
354
+ error "Usage: shipwright durable consume <consumer-id> <handler-cmd>"
355
+ echo " handler-cmd: command to execute for each event (receives JSON on stdin)"
356
+ return 1
357
+ fi
358
+
359
+ local log_file
360
+ log_file="$(event_log_file)"
361
+
362
+ if [[ ! -f "$log_file" ]]; then
363
+ warn "No events to consume"
364
+ return 0
365
+ fi
366
+
367
+ local offset
368
+ offset="$(get_consumer_offset "$consumer_id")"
369
+
370
+ # Process events starting from last consumed offset
371
+ local line_num=0
372
+ local processed=0
373
+ local failed=0
374
+
375
+ while IFS= read -r line; do
376
+ ((line_num++))
377
+
378
+ if (( line_num <= offset )); then
379
+ continue
380
+ fi
381
+
382
+ # Extract event_id for deduplication
383
+ local event_id
384
+ event_id="$(echo "$line" | jq -r '.event_id' 2>/dev/null || echo '')"
385
+
386
+ if [[ -z "$event_id" ]]; then
387
+ error "Invalid event format at line $line_num"
388
+ ((failed++))
389
+ continue
390
+ fi
391
+
392
+ # Check if already processed (exactly-once)
393
+ if is_operation_completed "$event_id"; then
394
+ info "Event $event_id already processed, skipping"
395
+ ((processed++))
396
+ save_consumer_offset "$consumer_id" "$line_num"
397
+ continue
398
+ fi
399
+
400
+ # Execute handler
401
+ if echo "$line" | bash -c "$handler_cmd" 2>/dev/null; then
402
+ mark_operation_completed "$event_id" '{"status":"success"}'
403
+ success "Event $event_id processed"
404
+ ((processed++))
405
+ else
406
+ error "Handler failed for event $event_id"
407
+ send_to_dlq "$event_id" "handler_failed" 1
408
+ ((failed++))
409
+ fi
410
+
411
+ # Update offset after successful processing
412
+ save_consumer_offset "$consumer_id" "$line_num"
413
+ done < "$log_file"
414
+
415
+ info "Consumer $consumer_id: processed=$processed, failed=$failed"
416
+ }
417
+
418
+ # ─── Replay Events ─────────────────────────────────────────────────────────
419
+ cmd_replay() {
420
+ local start_seq="${1:-1}"
421
+ local handler_cmd="${2:-cat}"
422
+
423
+ local log_file
424
+ log_file="$(event_log_file)"
425
+
426
+ if [[ ! -f "$log_file" ]]; then
427
+ warn "No events to replay"
428
+ return 0
429
+ fi
430
+
431
+ info "Replaying events from sequence $start_seq..."
432
+
433
+ local replayed=0
434
+ while IFS= read -r line; do
435
+ local seq
436
+ seq="$(echo "$line" | jq -r '.sequence' 2>/dev/null || echo '0')"
437
+
438
+ if (( seq >= start_seq )); then
439
+ echo "$line" | bash -c "$handler_cmd"
440
+ ((replayed++))
441
+ fi
442
+ done < "$log_file"
443
+
444
+ success "Replayed $replayed events"
445
+ }
446
+
447
+ # ─── Compaction ────────────────────────────────────────────────────────────
448
+ cmd_compact() {
449
+ local log_file
450
+ log_file="$(event_log_file)"
451
+
452
+ if [[ ! -f "$log_file" ]]; then
453
+ warn "No event log to compact"
454
+ return 0
455
+ fi
456
+
457
+ ensure_durable_dir
458
+
459
+ local compacted_file
460
+ compacted_file="${DURABLE_DIR}/event-log/events-compacted-$(now_epoch).jsonl"
461
+
462
+ # Keep only the latest state for each workflow (deduplicates by event_id)
463
+ local tmp_file
464
+ tmp_file="$(mktemp "${DURABLE_DIR}/.tmp.XXXXXX")"
465
+
466
+ # This is a simple compaction: keep all events (could be enhanced to prune old states)
467
+ cp "$log_file" "$tmp_file"
468
+
469
+ local orig_lines compacted_lines savings
470
+ orig_lines=$(wc -l < "$log_file")
471
+ compacted_lines=$(wc -l < "$tmp_file")
472
+ savings=$((orig_lines - compacted_lines))
473
+
474
+ mv "$tmp_file" "$compacted_file"
475
+
476
+ success "Event log compacted: $orig_lines → $compacted_lines lines (saved $savings events)"
477
+ info "Backup: $compacted_file"
478
+ }
479
+
480
+ # ─── Status ────────────────────────────────────────────────────────────────
481
+ cmd_status() {
482
+ ensure_durable_dir
483
+
484
+ local log_file dlq_file offsets_dir locks_dir
485
+ log_file="$(event_log_file)"
486
+ dlq_file="$(dlq_file)"
487
+ offsets_dir="${DURABLE_DIR}/offsets"
488
+ locks_dir="${DURABLE_DIR}/locks"
489
+
490
+ local log_events log_size
491
+ log_events=$(wc -l < "$log_file" 2>/dev/null || echo "0")
492
+ log_size=$(du -h "$log_file" 2>/dev/null | awk '{print $1}' || echo "0")
493
+
494
+ local dlq_events
495
+ dlq_events=$(wc -l < "$dlq_file" 2>/dev/null || echo "0")
496
+
497
+ local consumer_count
498
+ consumer_count=$(find "$offsets_dir" -name "consumer-*.offset" 2>/dev/null | wc -l || echo "0")
499
+
500
+ local active_locks
501
+ active_locks=$(find "$locks_dir" -type d -mindepth 1 2>/dev/null | wc -l || echo "0")
502
+
503
+ echo ""
504
+ echo -e "${CYAN}${BOLD} Durable Workflow Status${RESET} ${DIM}v${VERSION}${RESET}"
505
+ echo -e "${DIM} ══════════════════════════════════════════${RESET}"
506
+ echo ""
507
+ echo -e " ${BOLD}Event Log${RESET}"
508
+ echo -e " Events: ${GREEN}$log_events${RESET}"
509
+ echo -e " Size: ${GREEN}$log_size${RESET}"
510
+ echo ""
511
+ echo -e " ${BOLD}Consumers${RESET}"
512
+ echo -e " Count: ${GREEN}$consumer_count${RESET}"
513
+ echo ""
514
+ echo -e " ${BOLD}Dead Letter Queue${RESET}"
515
+ echo -e " Events: ${YELLOW}$dlq_events${RESET}"
516
+ echo ""
517
+ echo -e " ${BOLD}Distributed Locks${RESET}"
518
+ echo -e " Active: ${CYAN}$active_locks${RESET}"
519
+ echo ""
520
+ }
521
+
522
+ # ─── Help ──────────────────────────────────────────────────────────────────
523
+ show_help() {
524
+ echo ""
525
+ echo -e "${CYAN}${BOLD} Shipwright Durable Workflow Engine${RESET} ${DIM}v${VERSION}${RESET}"
526
+ echo -e "${DIM} ════════════════════════════════════════════════════════${RESET}"
527
+ echo ""
528
+ echo -e " ${BOLD}USAGE${RESET}"
529
+ echo -e " shipwright durable <command> [options]"
530
+ echo ""
531
+ echo -e " ${BOLD}COMMANDS${RESET}"
532
+ echo -e " ${CYAN}publish${RESET} <type> <payload> Publish event to WAL"
533
+ echo -e " ${CYAN}consume${RESET} <id> <handler> Process next unconsumed event"
534
+ echo -e " ${CYAN}replay${RESET} [seq] [handler] Replay events from sequence"
535
+ echo -e " ${CYAN}checkpoint${RESET} <cmd> Save/restore workflow checkpoint"
536
+ echo -e " ${CYAN}lock${RESET} <cmd> Acquire/release distributed lock"
537
+ echo -e " ${CYAN}dlq${RESET} <cmd> Inspect/retry dead letter queue"
538
+ echo -e " ${CYAN}compact${RESET} Compact the event log"
539
+ echo -e " ${CYAN}status${RESET} Show event log statistics"
540
+ echo -e " ${CYAN}help${RESET} Show this help message"
541
+ echo ""
542
+ echo -e " ${BOLD}CHECKPOINT SUBCOMMANDS${RESET}"
543
+ echo -e " ${CYAN}save${RESET} <wf-id> <stage> <seq> <state> Save checkpoint"
544
+ echo -e " ${CYAN}restore${RESET} <wf-id> Restore checkpoint"
545
+ echo ""
546
+ echo -e " ${BOLD}LOCK SUBCOMMANDS${RESET}"
547
+ echo -e " ${CYAN}acquire${RESET} <resource> [timeout] Acquire lock (default 30s)"
548
+ echo -e " ${CYAN}release${RESET} <resource> Release lock"
549
+ echo ""
550
+ echo -e " ${BOLD}DLQ SUBCOMMANDS${RESET}"
551
+ echo -e " ${CYAN}list${RESET} List dead letter events"
552
+ echo -e " ${CYAN}inspect${RESET} <event-id> Inspect failed event"
553
+ echo -e " ${CYAN}retry${RESET} <event-id> [max-retries] Retry failed event"
554
+ echo ""
555
+ echo -e " ${BOLD}EXAMPLES${RESET}"
556
+ echo -e " ${DIM}# Publish an event${RESET}"
557
+ echo -e " shipwright durable publish workflow.started '{\"workflow_id\":\"wf-123\"}'${RESET}"
558
+ echo ""
559
+ echo -e " ${DIM}# Save checkpoint at stage boundary${RESET}"
560
+ echo -e " shipwright durable checkpoint save wf-123 build 42 '{\"files\":[\"main.rs\"]}'${RESET}"
561
+ echo ""
562
+ echo -e " ${DIM}# Acquire distributed lock${RESET}"
563
+ echo -e " shipwright durable lock acquire my-resource 60${RESET}"
564
+ echo ""
565
+ echo -e " ${DIM}# Consume events with custom handler${RESET}"
566
+ echo -e " shipwright durable consume my-consumer 'jq .event_type'${RESET}"
567
+ echo ""
568
+ }
569
+
570
+ # ─── Checkpoint Subcommands ────────────────────────────────────────────────
571
+ cmd_checkpoint() {
572
+ local subcmd="${1:-help}"
573
+
574
+ case "$subcmd" in
575
+ save)
576
+ if [[ $# -lt 5 ]]; then
577
+ error "Usage: shipwright durable checkpoint save <wf-id> <stage> <seq> <state>"
578
+ return 1
579
+ fi
580
+ save_checkpoint "$2" "$3" "$4" "$5"
581
+ ;;
582
+ restore)
583
+ if [[ $# -lt 2 ]]; then
584
+ error "Usage: shipwright durable checkpoint restore <wf-id>"
585
+ return 1
586
+ fi
587
+ restore_checkpoint "$2"
588
+ ;;
589
+ *)
590
+ error "Unknown checkpoint subcommand: $subcmd"
591
+ return 1
592
+ ;;
593
+ esac
594
+ }
595
+
596
+ # ─── Lock Subcommands ──────────────────────────────────────────────────────
597
+ cmd_lock() {
598
+ local subcmd="${1:-help}"
599
+
600
+ case "$subcmd" in
601
+ acquire)
602
+ if [[ $# -lt 2 ]]; then
603
+ error "Usage: shipwright durable lock acquire <resource> [timeout]"
604
+ return 1
605
+ fi
606
+ acquire_lock "$2" "${3:-30}"
607
+ ;;
608
+ release)
609
+ if [[ $# -lt 2 ]]; then
610
+ error "Usage: shipwright durable lock release <resource>"
611
+ return 1
612
+ fi
613
+ release_lock "$2"
614
+ ;;
615
+ *)
616
+ error "Unknown lock subcommand: $subcmd"
617
+ return 1
618
+ ;;
619
+ esac
620
+ }
621
+
622
+ # ─── DLQ Subcommands ──────────────────────────────────────────────────────
623
+ cmd_dlq() {
624
+ local subcmd="${1:-help}"
625
+ local dlq_path
626
+ dlq_path="$(dlq_file)"
627
+
628
+ case "$subcmd" in
629
+ list)
630
+ if [[ ! -f "$dlq_path" ]]; then
631
+ info "Dead letter queue is empty"
632
+ return 0
633
+ fi
634
+ cat "$dlq_path" | jq .
635
+ ;;
636
+ inspect)
637
+ if [[ $# -lt 2 ]]; then
638
+ error "Usage: shipwright durable dlq inspect <event-id>"
639
+ return 1
640
+ fi
641
+ if [[ ! -f "$dlq_path" ]]; then
642
+ error "Dead letter queue is empty"
643
+ return 1
644
+ fi
645
+ grep "$2" "$dlq_path" | jq .
646
+ ;;
647
+ retry)
648
+ if [[ $# -lt 2 ]]; then
649
+ error "Usage: shipwright durable dlq retry <event-id> [max-retries]"
650
+ return 1
651
+ fi
652
+ warn "DLQ retry for $2 (would re-publish event and resume processing)"
653
+ ;;
654
+ *)
655
+ error "Unknown dlq subcommand: $subcmd"
656
+ return 1
657
+ ;;
658
+ esac
659
+ }
660
+
661
+ # ─── Main Command Router ───────────────────────────────────────────────────
662
+ main() {
663
+ local cmd="${1:-help}"
664
+ shift 2>/dev/null || true
665
+
666
+ case "$cmd" in
667
+ publish)
668
+ if [[ $# -lt 2 ]]; then
669
+ error "Usage: shipwright durable publish <type> <payload>"
670
+ return 1
671
+ fi
672
+ publish_event "$1" "$2"
673
+ ;;
674
+ consume)
675
+ cmd_consume "$@"
676
+ ;;
677
+ replay)
678
+ cmd_replay "$@"
679
+ ;;
680
+ checkpoint)
681
+ cmd_checkpoint "$@"
682
+ ;;
683
+ lock)
684
+ cmd_lock "$@"
685
+ ;;
686
+ dlq)
687
+ cmd_dlq "$@"
688
+ ;;
689
+ compact)
690
+ cmd_compact
691
+ ;;
692
+ status)
693
+ cmd_status
694
+ ;;
695
+ help|--help|-h)
696
+ show_help
697
+ ;;
698
+ *)
699
+ error "Unknown command: $cmd"
700
+ echo ""
701
+ show_help
702
+ return 1
703
+ ;;
704
+ esac
705
+ }
706
+
707
+ # ─── Source Guard ─────────────────────────────────────────────────────────
708
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
709
+ main "$@"
710
+ fi