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,444 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright scale — Dynamic agent team scaling during pipeline execution ║
4
+ # ║ Scale up/down, manage rules, track history, recommend actions ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
+
9
+ VERSION="2.0.0"
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+
12
+ # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
13
+ CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
14
+ PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
15
+ BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
16
+ GREEN='\033[38;2;74;222;128m' # success
17
+ YELLOW='\033[38;2;250;204;21m' # warning
18
+ RED='\033[38;2;248;113;113m' # error
19
+ DIM='\033[2m'
20
+ BOLD='\033[1m'
21
+ RESET='\033[0m'
22
+
23
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
24
+ # shellcheck source=lib/compat.sh
25
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
26
+
27
+ # ─── Output Helpers ─────────────────────────────────────────────────────────
28
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
29
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
30
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
31
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
32
+
33
+ # ─── Constants ──────────────────────────────────────────────────────────────
34
+ SCALE_RULES_FILE="${HOME}/.shipwright/scale-rules.json"
35
+ SCALE_EVENTS_FILE="${HOME}/.shipwright/scale-events.jsonl"
36
+ SCALE_STATE_FILE="${HOME}/.shipwright/scale-state.json"
37
+
38
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
39
+ now_unix() { date +%s; }
40
+
41
+ # ─── Ensure directories exist ──────────────────────────────────────────────
42
+ ensure_dirs() {
43
+ mkdir -p "$HOME/.shipwright"
44
+ }
45
+
46
+ # ─── Initialize rules file ────────────────────────────────────────────────
47
+ init_rules() {
48
+ if [[ ! -f "$SCALE_RULES_FILE" ]]; then
49
+ local tmp_file
50
+ tmp_file=$(mktemp)
51
+ cat > "$tmp_file" << 'JSON'
52
+ {
53
+ "iteration_threshold": 3,
54
+ "coverage_threshold": 60,
55
+ "module_threshold": 5,
56
+ "budget_check": true,
57
+ "cooldown_seconds": 120,
58
+ "max_team_size": 8,
59
+ "roles": ["builder", "reviewer", "tester", "security"]
60
+ }
61
+ JSON
62
+ mv "$tmp_file" "$SCALE_RULES_FILE"
63
+ success "Initialized scaling rules"
64
+ fi
65
+ }
66
+
67
+ # ─── Get current unix timestamp ───────────────────────────────────────────
68
+ get_last_scale_time() {
69
+ if [[ -f "$SCALE_STATE_FILE" ]]; then
70
+ jq -r '.last_scale_time // 0' "$SCALE_STATE_FILE" 2>/dev/null || echo "0"
71
+ else
72
+ echo "0"
73
+ fi
74
+ }
75
+
76
+ # ─── Check if we're within cooldown period ───────────────────────────────
77
+ in_cooldown() {
78
+ local cooldown
79
+ cooldown=$(jq -r '.cooldown_seconds // 120' "$SCALE_RULES_FILE" 2>/dev/null || echo "120")
80
+ local last_time
81
+ last_time=$(get_last_scale_time)
82
+ local now
83
+ now=$(now_unix)
84
+ local elapsed=$((now - last_time))
85
+ [[ $elapsed -lt $cooldown ]]
86
+ }
87
+
88
+ # ─── Update scale state ───────────────────────────────────────────────────
89
+ update_scale_state() {
90
+ local tmp_file
91
+ tmp_file=$(mktemp)
92
+
93
+ if [[ -f "$SCALE_STATE_FILE" ]]; then
94
+ # Update existing state
95
+ jq --arg now "$(now_unix)" '.last_scale_time = ($now | tonumber)' "$SCALE_STATE_FILE" > "$tmp_file"
96
+ else
97
+ # Create new state
98
+ cat > "$tmp_file" << JSON
99
+ {
100
+ "last_scale_time": $(now_unix),
101
+ "team_size": 0,
102
+ "events_count": 0
103
+ }
104
+ JSON
105
+ fi
106
+
107
+ mv "$tmp_file" "$SCALE_STATE_FILE"
108
+ }
109
+
110
+ # ─── Emit scaling event ───────────────────────────────────────────────────
111
+ emit_scale_event() {
112
+ local action="$1" # up, down, auto
113
+ local role="$2" # builder, reviewer, tester, security
114
+ local reason="$3" # iteration_threshold, coverage_threshold, etc
115
+ local context="${4:-}" # additional context
116
+
117
+ ensure_dirs
118
+
119
+ local event
120
+ event=$(jq -c -n \
121
+ --arg ts "$(now_iso)" \
122
+ --arg action "$action" \
123
+ --arg role "$role" \
124
+ --arg reason "$reason" \
125
+ --arg context "$context" \
126
+ '{ts: $ts, action: $action, role: $role, reason: $reason, context: $context}')
127
+
128
+ echo "$event" >> "$SCALE_EVENTS_FILE"
129
+ }
130
+
131
+ # ─── Scale Up: spawn new agent ───────────────────────────────────────────
132
+ cmd_up() {
133
+ local role="${1:-builder}"
134
+ shift 2>/dev/null || true
135
+
136
+ ensure_dirs
137
+ init_rules
138
+
139
+ # Validate role
140
+ local valid_roles="builder reviewer tester security"
141
+ if ! echo "$valid_roles" | grep -q "$role"; then
142
+ error "Invalid role: $role. Valid roles: $valid_roles"
143
+ return 1
144
+ fi
145
+
146
+ # Check cooldown
147
+ if in_cooldown; then
148
+ local cooldown
149
+ cooldown=$(jq -r '.cooldown_seconds // 120' "$SCALE_RULES_FILE")
150
+ warn "Scaling cooldown active. Wait ${cooldown}s before next scale event."
151
+ return 1
152
+ fi
153
+
154
+ # Check max team size
155
+ local max_size
156
+ max_size=$(jq -r '.max_team_size // 8' "$SCALE_RULES_FILE")
157
+
158
+ info "Scaling up team with ${role} agent"
159
+ echo -e " Max team size: ${CYAN}${max_size}${RESET}"
160
+ echo -e " Role: ${CYAN}${role}${RESET}"
161
+ echo ""
162
+
163
+ # TODO: Integrate with tmux/SendMessage to spawn agent
164
+ # For now, emit event and log
165
+ emit_scale_event "up" "$role" "manual" "$*"
166
+ update_scale_state
167
+
168
+ success "Scale-up event recorded (role: ${role})"
169
+ echo ""
170
+ echo -e " ${DIM}Note: Actual agent spawn requires tmux/claude integration${RESET}"
171
+ }
172
+
173
+ # ─── Scale Down: send shutdown to agent ──────────────────────────────────
174
+ cmd_down() {
175
+ local agent_id="${1:-}"
176
+ shift 2>/dev/null || true
177
+
178
+ if [[ -z "$agent_id" ]]; then
179
+ error "Usage: shipwright scale down <agent-id>"
180
+ return 1
181
+ fi
182
+
183
+ ensure_dirs
184
+ init_rules
185
+
186
+ info "Scaling down agent: ${agent_id}"
187
+ echo ""
188
+
189
+ # TODO: Integrate with SendMessage to shut down agent
190
+ emit_scale_event "down" "unknown" "manual" "agent_id=$agent_id"
191
+ update_scale_state
192
+
193
+ success "Scale-down event recorded (agent: ${agent_id})"
194
+ echo -e " ${DIM}Note: Agent shutdown requires SendMessage integration${RESET}"
195
+ }
196
+
197
+ # ─── Manage scaling rules ────────────────────────────────────────────────
198
+ cmd_rules() {
199
+ local subcmd="${1:-show}"
200
+ shift 2>/dev/null || true
201
+
202
+ ensure_dirs
203
+ init_rules
204
+
205
+ case "$subcmd" in
206
+ show)
207
+ info "Scaling Rules"
208
+ echo ""
209
+ cat "$SCALE_RULES_FILE" | jq '.' | sed 's/^/ /'
210
+ echo ""
211
+ ;;
212
+ set)
213
+ local key="${1:-}"
214
+ local value="${2:-}"
215
+
216
+ if [[ -z "$key" || -z "$value" ]]; then
217
+ error "Usage: shipwright scale rules set <key> <value>"
218
+ return 1
219
+ fi
220
+
221
+ local tmp_file
222
+ tmp_file=$(mktemp)
223
+
224
+ jq --arg key "$key" --arg value "$value" \
225
+ 'if ($value | test("^[0-9]+$")) then
226
+ .[$key] = ($value | tonumber)
227
+ else
228
+ .[$key] = $value
229
+ end' "$SCALE_RULES_FILE" > "$tmp_file"
230
+
231
+ mv "$tmp_file" "$SCALE_RULES_FILE"
232
+ success "Updated: ${key} = ${value}"
233
+ ;;
234
+ reset)
235
+ rm -f "$SCALE_RULES_FILE"
236
+ init_rules
237
+ success "Rules reset to defaults"
238
+ ;;
239
+ *)
240
+ error "Unknown subcommand: $subcmd"
241
+ echo -e " Valid: ${CYAN}show${RESET}, ${CYAN}set${RESET}, ${CYAN}reset${RESET}"
242
+ return 1
243
+ ;;
244
+ esac
245
+ }
246
+
247
+ # ─── Show current status ──────────────────────────────────────────────────
248
+ cmd_status() {
249
+ ensure_dirs
250
+ init_rules
251
+
252
+ local team_size=0
253
+ local event_count=0
254
+
255
+ if [[ -f "$SCALE_STATE_FILE" ]]; then
256
+ team_size=$(jq -r '.team_size // 0' "$SCALE_STATE_FILE" 2>/dev/null || echo "0")
257
+ fi
258
+
259
+ if [[ -f "$SCALE_EVENTS_FILE" ]]; then
260
+ event_count=$(wc -l < "$SCALE_EVENTS_FILE" || echo "0")
261
+ fi
262
+
263
+ local last_scale_time
264
+ last_scale_time=$(get_last_scale_time)
265
+
266
+ info "Scaling Status"
267
+ echo ""
268
+ echo -e " Team size: ${CYAN}${team_size}${RESET}"
269
+ echo -e " Scale events: ${CYAN}${event_count}${RESET}"
270
+ echo -e " Last scale: ${CYAN}$(date -u -d @"$last_scale_time" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "never")${RESET}"
271
+
272
+ local max_size
273
+ max_size=$(jq -r '.max_team_size // 8' "$SCALE_RULES_FILE")
274
+ echo -e " Max team size: ${CYAN}${max_size}${RESET}"
275
+ echo ""
276
+ }
277
+
278
+ # ─── Show scaling history ────────────────────────────────────────────────
279
+ cmd_history() {
280
+ local limit="${1:-20}"
281
+ shift 2>/dev/null || true
282
+
283
+ ensure_dirs
284
+
285
+ if [[ ! -f "$SCALE_EVENTS_FILE" ]]; then
286
+ warn "No scaling events recorded"
287
+ return 0
288
+ fi
289
+
290
+ info "Scaling History (last ${limit} events)"
291
+ echo ""
292
+
293
+ tail -n "$limit" "$SCALE_EVENTS_FILE" | while IFS= read -r line; do
294
+ local ts action role reason
295
+ ts=$(echo "$line" | jq -r '.ts // "unknown"' 2>/dev/null || echo "unknown")
296
+ action=$(echo "$line" | jq -r '.action // "unknown"' 2>/dev/null || echo "unknown")
297
+ role=$(echo "$line" | jq -r '.role // "unknown"' 2>/dev/null || echo "unknown")
298
+ reason=$(echo "$line" | jq -r '.reason // "unknown"' 2>/dev/null || echo "unknown")
299
+ printf " %s ${CYAN}%-6s${RESET} %-10s %s\n" "$ts" "$action" "$role" "$reason"
300
+ done
301
+
302
+ echo ""
303
+ }
304
+
305
+ # ─── Recommend scaling actions ───────────────────────────────────────────
306
+ cmd_recommend() {
307
+ ensure_dirs
308
+ init_rules
309
+
310
+ local iteration_threshold
311
+ iteration_threshold=$(jq -r '.iteration_threshold // 3' "$SCALE_RULES_FILE")
312
+
313
+ local coverage_threshold
314
+ coverage_threshold=$(jq -r '.coverage_threshold // 60' "$SCALE_RULES_FILE")
315
+
316
+ local module_threshold
317
+ module_threshold=$(jq -r '.module_threshold // 5' "$SCALE_RULES_FILE")
318
+
319
+ info "Scaling Recommendations"
320
+ echo ""
321
+ echo -e " Thresholds:"
322
+ echo -e " Failed iterations: ${CYAN}${iteration_threshold}${RESET} (add tester on failure)"
323
+ echo -e " Test coverage: ${CYAN}${coverage_threshold}%${RESET} (add tester below this)"
324
+ echo -e " Modules changed: ${CYAN}${module_threshold}${RESET} (add reviewer above this)"
325
+ echo ""
326
+
327
+ # TODO: Parse pipeline context to generate actual recommendations
328
+ echo -e " ${DIM}Recommendations require active pipeline context (passed via environment)${RESET}"
329
+ echo ""
330
+
331
+ # Example output when context is available:
332
+ # echo -e " ${YELLOW}⚠${RESET} Failed 4 iterations (threshold: 3)"
333
+ # echo -e " ${CYAN}→ Recommend adding: tester${RESET}"
334
+ # echo ""
335
+ # echo -e " ${YELLOW}⚠${RESET} Coverage at 45% (threshold: 60%)"
336
+ # echo -e " ${CYAN}→ Recommend adding: tester${RESET}"
337
+ }
338
+
339
+ # ─── Help message ────────────────────────────────────────────────────────
340
+ cmd_help() {
341
+ cat << 'EOF'
342
+ shipwright scale — Dynamic agent team scaling during pipeline execution
343
+
344
+ USAGE
345
+ shipwright scale <command> [options]
346
+
347
+ COMMANDS
348
+ up [role] Spawn new agent with specific role (builder/reviewer/tester/security)
349
+ down <agent-id> Gracefully shutdown an agent (waits for task completion)
350
+ rules Manage scaling rules (iteration_threshold, coverage_threshold, etc)
351
+ status Show current team size, scaling history, budget impact
352
+ history [N] Show last N scaling events (default: 20)
353
+ recommend Analyze pipeline state and suggest scaling actions
354
+ help Show this help message
355
+
356
+ RULES SUBCOMMANDS
357
+ rules show Display current scaling rules
358
+ rules set <k> <v> Update a rule (e.g., rules set max_team_size 10)
359
+ rules reset Reset to default rules
360
+
361
+ OPTIONS
362
+ --cooldown <secs> Minimum seconds between scale events (default: 120)
363
+ --max-size <n> Maximum team size (default: 8)
364
+
365
+ RULES (stored in ~/.shipwright/scale-rules.json)
366
+ iteration_threshold Scale up after N failed iterations (default: 3)
367
+ coverage_threshold Add tester when coverage < N% (default: 60)
368
+ module_threshold Split builders when touching > N modules (default: 5)
369
+ budget_check Factor in remaining budget before scaling (default: true)
370
+ cooldown_seconds Minimum time between scale events (default: 120)
371
+ max_team_size Maximum agents per team (default: 8)
372
+
373
+ EXAMPLES
374
+ # Add a tester to the current team
375
+ shipwright scale up tester
376
+
377
+ # Remove an agent gracefully
378
+ shipwright scale down agent-42
379
+
380
+ # View current scaling rules
381
+ shipwright scale rules show
382
+
383
+ # Update max team size
384
+ shipwright scale rules set max_team_size 10
385
+
386
+ # Show recent scaling events
387
+ shipwright scale history
388
+
389
+ # Get scaling recommendations for current pipeline
390
+ shipwright scale recommend --issue 46
391
+
392
+ INTEGRATION
393
+ Scaling events are recorded in ~/.shipwright/scale-events.jsonl
394
+ State is persisted in ~/.shipwright/scale-state.json
395
+ Rules are stored in ~/.shipwright/scale-rules.json
396
+
397
+ Actual agent spawning/shutdown integrates with:
398
+ - tmux (for new pane creation)
399
+ - SendMessage tool (for agent communication)
400
+ - Pipeline context (from sw-pipeline.sh environment)
401
+
402
+ EOF
403
+ }
404
+
405
+ # ─── Main router ──────────────────────────────────────────────────────────
406
+ main() {
407
+ local cmd="${1:-help}"
408
+ shift 2>/dev/null || true
409
+
410
+ case "$cmd" in
411
+ up)
412
+ cmd_up "$@"
413
+ ;;
414
+ down)
415
+ cmd_down "$@"
416
+ ;;
417
+ rules)
418
+ cmd_rules "$@"
419
+ ;;
420
+ status)
421
+ cmd_status "$@"
422
+ ;;
423
+ history)
424
+ cmd_history "$@"
425
+ ;;
426
+ recommend)
427
+ cmd_recommend "$@"
428
+ ;;
429
+ help|--help|-h)
430
+ cmd_help
431
+ ;;
432
+ *)
433
+ error "Unknown command: $cmd"
434
+ echo ""
435
+ cmd_help
436
+ exit 1
437
+ ;;
438
+ esac
439
+ }
440
+
441
+ # Only run main if this script is executed directly (not sourced)
442
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
443
+ main "$@"
444
+ fi