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.
- package/README.md +114 -36
- package/completions/_shipwright +212 -32
- package/completions/shipwright.bash +97 -25
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/package.json +4 -2
- package/scripts/sw +208 -1
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +637 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +432 -130
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +540 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +59 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +471 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +617 -0
- package/scripts/sw-init.sh +88 -1
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +363 -28
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +64 -3
- package/scripts/sw-memory.sh +1 -1
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +689 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +687 -24
- package/scripts/sw-pm.sh +693 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +573 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +658 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +583 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +515 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +603 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
3
|
+
# β shipwright public-dashboard β Public real-time pipeline progress β
|
|
4
|
+
# β Shareable URLs Β· Self-contained HTML Β· Privacy controls β
|
|
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
|
+
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
|
+
|
|
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
|
+
# βββ Paths ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
38
|
+
PUB_DASH_DIR="${HOME}/.shipwright/public-dashboard"
|
|
39
|
+
SHARE_LINKS_FILE="${PUB_DASH_DIR}/share-links.json"
|
|
40
|
+
SHARE_CONFIG_FILE="${PUB_DASH_DIR}/config.json"
|
|
41
|
+
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
42
|
+
|
|
43
|
+
# βββ Structured Event Log ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
44
|
+
emit_event() {
|
|
45
|
+
local event_type="$1"
|
|
46
|
+
shift
|
|
47
|
+
local json_fields=""
|
|
48
|
+
for kv in "$@"; do
|
|
49
|
+
local key="${kv%%=*}"
|
|
50
|
+
local val="${kv#*=}"
|
|
51
|
+
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
52
|
+
json_fields="${json_fields},\"${key}\":${val}"
|
|
53
|
+
else
|
|
54
|
+
val="${val//\"/\\\"}"
|
|
55
|
+
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
56
|
+
fi
|
|
57
|
+
done
|
|
58
|
+
mkdir -p "${HOME}/.shipwright"
|
|
59
|
+
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# βββ Initialization ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
63
|
+
ensure_dirs() {
|
|
64
|
+
mkdir -p "$PUB_DASH_DIR"
|
|
65
|
+
[[ -f "$SHARE_LINKS_FILE" ]] || echo '{"links":[]}' > "$SHARE_LINKS_FILE"
|
|
66
|
+
[[ -f "$SHARE_CONFIG_FILE" ]] || echo '{"privacy":"stages_only","expiry_hours":24,"custom_domain":"","branding":""}' > "$SHARE_CONFIG_FILE"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# βββ Sanitize Functions ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
70
|
+
sanitize_for_privacy() {
|
|
71
|
+
local input="$1"
|
|
72
|
+
local privacy_level="${2:-stages_only}"
|
|
73
|
+
|
|
74
|
+
case "$privacy_level" in
|
|
75
|
+
public)
|
|
76
|
+
# Full details
|
|
77
|
+
echo "$input"
|
|
78
|
+
;;
|
|
79
|
+
anonymized)
|
|
80
|
+
# Hide paths and tokens
|
|
81
|
+
echo "$input" | sed -E \
|
|
82
|
+
-e 's|/Users/[^/]+|/home/user|g' \
|
|
83
|
+
-e 's/(ghp_|sk_live_)[A-Za-z0-9_-]+/[REDACTED_TOKEN]/g' \
|
|
84
|
+
-e 's/(CLAUDECODE|GITHUB_TOKEN)=[^ ]*/[REDACTED_ENV]/g' \
|
|
85
|
+
-e 's/@[^ ]*\.com/@redacted.com/g'
|
|
86
|
+
;;
|
|
87
|
+
stages_only)
|
|
88
|
+
# Only stage names and status
|
|
89
|
+
echo "$input" | sed -E \
|
|
90
|
+
-e 's|"description":"[^"]*"|"description":""|g' \
|
|
91
|
+
-e 's|"logs":"[^"]*"|"logs":""|g' \
|
|
92
|
+
-e 's|"output":"[^"]*"|"output":""|g' \
|
|
93
|
+
-e 's|/Users/[^/]+|/home/user|g'
|
|
94
|
+
;;
|
|
95
|
+
esac
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# βββ Gather Current Pipeline State ββββββββββββββββββββββββββββββββββββββββββ
|
|
99
|
+
gather_pipeline_state() {
|
|
100
|
+
local privacy="${1:-stages_only}"
|
|
101
|
+
|
|
102
|
+
local state_file="${REPO_DIR}/.claude/pipeline-state.md"
|
|
103
|
+
local daemon_state="${HOME}/.shipwright/daemon-state.json"
|
|
104
|
+
local pipeline_artifacts="${REPO_DIR}/.claude/pipeline-artifacts"
|
|
105
|
+
|
|
106
|
+
local pipeline_data='{
|
|
107
|
+
"status":"unknown",
|
|
108
|
+
"stages":[],
|
|
109
|
+
"agents":[],
|
|
110
|
+
"events":[],
|
|
111
|
+
"updated_at":"'"$(now_iso)"'"
|
|
112
|
+
}'
|
|
113
|
+
|
|
114
|
+
# Read daemon state
|
|
115
|
+
if [[ -f "$daemon_state" ]]; then
|
|
116
|
+
local active_jobs queued_count
|
|
117
|
+
active_jobs=$(jq -c '.active_jobs // []' "$daemon_state" 2>/dev/null || echo "[]")
|
|
118
|
+
queued_count=$(jq 'length' <<<"$active_jobs" 2>/dev/null || echo "0")
|
|
119
|
+
|
|
120
|
+
if [[ "$queued_count" -gt 0 ]]; then
|
|
121
|
+
pipeline_data=$(jq --argjson jobs "$active_jobs" '.agents = $jobs' <<<"$pipeline_data")
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# Read recent events (last 50)
|
|
126
|
+
if [[ -f "$EVENTS_FILE" ]]; then
|
|
127
|
+
local events
|
|
128
|
+
events=$(tail -50 "$EVENTS_FILE" | jq -c -s '.' 2>/dev/null || echo "[]")
|
|
129
|
+
pipeline_data=$(jq --argjson events "$events" '.events = $events' <<<"$pipeline_data")
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# Read pipeline artifacts if available
|
|
133
|
+
if [[ -d "$pipeline_artifacts" ]]; then
|
|
134
|
+
local stage_count
|
|
135
|
+
stage_count=$(find "$pipeline_artifacts" -name "*.md" -o -name "*.json" | wc -l || echo "0")
|
|
136
|
+
pipeline_data=$(jq --arg count "$stage_count" '.artifact_count = $count' <<<"$pipeline_data")
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# Sanitize based on privacy level
|
|
140
|
+
sanitize_for_privacy "$pipeline_data" "$privacy"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# βββ Generate Token βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
144
|
+
generate_token() {
|
|
145
|
+
# Create a read-only token (32 hex chars)
|
|
146
|
+
if command -v openssl &>/dev/null; then
|
|
147
|
+
openssl rand -hex 16
|
|
148
|
+
else
|
|
149
|
+
# Fallback to simple pseudo-random
|
|
150
|
+
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' '
|
|
151
|
+
fi
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# βββ Generate Self-Contained HTML ββββββββββββββββββββββββββββββββββββββββββ
|
|
155
|
+
generate_html() {
|
|
156
|
+
local data_json="$1"
|
|
157
|
+
local title="${2:-Shipwright Pipeline Progress}"
|
|
158
|
+
local privacy="${3:-stages_only}"
|
|
159
|
+
|
|
160
|
+
# Escape JSON for embedding in HTML
|
|
161
|
+
local json_escaped
|
|
162
|
+
json_escaped=$(echo "$data_json" | sed 's/"/\\"/g' | tr '\n' ' ')
|
|
163
|
+
|
|
164
|
+
cat <<'EOF'
|
|
165
|
+
<!DOCTYPE html>
|
|
166
|
+
<html lang="en">
|
|
167
|
+
<head>
|
|
168
|
+
<meta charset="UTF-8">
|
|
169
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
170
|
+
<title>TITLE_PLACEHOLDER</title>
|
|
171
|
+
<style>
|
|
172
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
173
|
+
body {
|
|
174
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
175
|
+
background: linear-gradient(135deg, #0a1428 0%, #1a2332 100%);
|
|
176
|
+
color: #e0e0e0;
|
|
177
|
+
padding: 20px;
|
|
178
|
+
min-height: 100vh;
|
|
179
|
+
}
|
|
180
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
181
|
+
header {
|
|
182
|
+
display: flex;
|
|
183
|
+
justify-content: space-between;
|
|
184
|
+
align-items: center;
|
|
185
|
+
margin-bottom: 30px;
|
|
186
|
+
padding-bottom: 20px;
|
|
187
|
+
border-bottom: 2px solid #00d4ff;
|
|
188
|
+
}
|
|
189
|
+
h1 { color: #00d4ff; font-size: 28px; }
|
|
190
|
+
.meta {
|
|
191
|
+
font-size: 12px;
|
|
192
|
+
color: #7c3aed;
|
|
193
|
+
display: flex;
|
|
194
|
+
gap: 20px;
|
|
195
|
+
}
|
|
196
|
+
.meta-item { display: flex; flex-direction: column; }
|
|
197
|
+
.meta-label { color: #999; text-transform: uppercase; }
|
|
198
|
+
.meta-value { color: #00d4ff; font-weight: bold; }
|
|
199
|
+
.grid {
|
|
200
|
+
display: grid;
|
|
201
|
+
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
202
|
+
gap: 20px;
|
|
203
|
+
margin-bottom: 30px;
|
|
204
|
+
}
|
|
205
|
+
.card {
|
|
206
|
+
background: rgba(30, 30, 40, 0.8);
|
|
207
|
+
border: 1px solid #333;
|
|
208
|
+
border-radius: 8px;
|
|
209
|
+
padding: 20px;
|
|
210
|
+
backdrop-filter: blur(10px);
|
|
211
|
+
transition: all 0.3s ease;
|
|
212
|
+
}
|
|
213
|
+
.card:hover { border-color: #00d4ff; box-shadow: 0 0 20px rgba(0, 212, 255, 0.2); }
|
|
214
|
+
.card-title {
|
|
215
|
+
color: #00d4ff;
|
|
216
|
+
font-size: 14px;
|
|
217
|
+
font-weight: bold;
|
|
218
|
+
text-transform: uppercase;
|
|
219
|
+
margin-bottom: 15px;
|
|
220
|
+
display: flex;
|
|
221
|
+
align-items: center;
|
|
222
|
+
gap: 8px;
|
|
223
|
+
}
|
|
224
|
+
.badge {
|
|
225
|
+
display: inline-block;
|
|
226
|
+
padding: 4px 12px;
|
|
227
|
+
border-radius: 20px;
|
|
228
|
+
font-size: 12px;
|
|
229
|
+
font-weight: bold;
|
|
230
|
+
}
|
|
231
|
+
.badge-success { background: rgba(74, 222, 128, 0.2); color: #4ade80; }
|
|
232
|
+
.badge-warning { background: rgba(250, 204, 21, 0.2); color: #f8cc15; }
|
|
233
|
+
.badge-error { background: rgba(248, 113, 113, 0.2); color: #f87171; }
|
|
234
|
+
.badge-info { background: rgba(0, 212, 255, 0.2); color: #00d4ff; }
|
|
235
|
+
.progress-bar {
|
|
236
|
+
width: 100%;
|
|
237
|
+
height: 6px;
|
|
238
|
+
background: #222;
|
|
239
|
+
border-radius: 3px;
|
|
240
|
+
overflow: hidden;
|
|
241
|
+
margin-bottom: 10px;
|
|
242
|
+
}
|
|
243
|
+
.progress-fill {
|
|
244
|
+
height: 100%;
|
|
245
|
+
background: linear-gradient(90deg, #00d4ff, #7c3aed);
|
|
246
|
+
width: 0%;
|
|
247
|
+
transition: width 0.3s ease;
|
|
248
|
+
}
|
|
249
|
+
.list-item {
|
|
250
|
+
padding: 10px 0;
|
|
251
|
+
border-bottom: 1px solid #222;
|
|
252
|
+
display: flex;
|
|
253
|
+
justify-content: space-between;
|
|
254
|
+
align-items: center;
|
|
255
|
+
font-size: 13px;
|
|
256
|
+
}
|
|
257
|
+
.list-item:last-child { border-bottom: none; }
|
|
258
|
+
.timestamp {
|
|
259
|
+
color: #666;
|
|
260
|
+
font-size: 11px;
|
|
261
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
262
|
+
}
|
|
263
|
+
.footer {
|
|
264
|
+
text-align: center;
|
|
265
|
+
padding: 20px;
|
|
266
|
+
color: #666;
|
|
267
|
+
font-size: 12px;
|
|
268
|
+
border-top: 1px solid #333;
|
|
269
|
+
}
|
|
270
|
+
.footer a { color: #00d4ff; text-decoration: none; }
|
|
271
|
+
.footer a:hover { text-decoration: underline; }
|
|
272
|
+
@media (prefers-reduced-motion: reduce) {
|
|
273
|
+
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
|
|
274
|
+
}
|
|
275
|
+
</style>
|
|
276
|
+
</head>
|
|
277
|
+
<body>
|
|
278
|
+
<div class="container">
|
|
279
|
+
<header>
|
|
280
|
+
<div>
|
|
281
|
+
<h1>β TITLE_PLACEHOLDER</h1>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="meta">
|
|
284
|
+
<div class="meta-item">
|
|
285
|
+
<span class="meta-label">Updated</span>
|
|
286
|
+
<span class="meta-value" id="updated-time">β</span>
|
|
287
|
+
</div>
|
|
288
|
+
<div class="meta-item">
|
|
289
|
+
<span class="meta-label">Privacy</span>
|
|
290
|
+
<span class="meta-value">PRIVACY_PLACEHOLDER</span>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
</header>
|
|
294
|
+
|
|
295
|
+
<div class="grid" id="dashboard">
|
|
296
|
+
<div class="card">
|
|
297
|
+
<div class="card-title">π Pipeline Status</div>
|
|
298
|
+
<div id="pipeline-status">
|
|
299
|
+
<div class="list-item">
|
|
300
|
+
<span>Overall</span>
|
|
301
|
+
<span class="badge badge-info" id="overall-status">Loading...</span>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="list-item">
|
|
304
|
+
<span>Active Agents</span>
|
|
305
|
+
<span id="agent-count">β</span>
|
|
306
|
+
</div>
|
|
307
|
+
<div class="list-item">
|
|
308
|
+
<span>Completed Events</span>
|
|
309
|
+
<span id="event-count">β</span>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<div class="card">
|
|
315
|
+
<div class="card-title">βοΈ Artifacts</div>
|
|
316
|
+
<div id="artifacts-info">
|
|
317
|
+
<div class="list-item">
|
|
318
|
+
<span>Stage Files</span>
|
|
319
|
+
<span id="artifact-count">β</span>
|
|
320
|
+
</div>
|
|
321
|
+
<div style="padding: 10px 0; font-size: 12px; color: #999;">
|
|
322
|
+
Stage outputs and checkpoints available in pipeline artifacts
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div class="card">
|
|
328
|
+
<div class="card-title">π Recent Events</div>
|
|
329
|
+
<div id="events-list" style="max-height: 300px; overflow-y: auto;">
|
|
330
|
+
<div style="color: #666; font-size: 12px;">Loading events...</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<div class="footer">
|
|
336
|
+
<p>Generated by <a href="https://github.com/sethdford/shipwright">Shipwright</a> v1.13.0</p>
|
|
337
|
+
<p style="margin-top: 8px; color: #555;">Dashboard auto-refreshes every 30s when served from dashboard server</p>
|
|
338
|
+
<p style="margin-top: 8px;" id="footer-timestamp">Generated: β</p>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<script>
|
|
343
|
+
const pipelineData = {DATA_PLACEHOLDER};
|
|
344
|
+
|
|
345
|
+
function formatTime(isoString) {
|
|
346
|
+
if (!isoString) return 'β';
|
|
347
|
+
const date = new Date(isoString);
|
|
348
|
+
return date.toLocaleTimeString();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function renderDashboard() {
|
|
352
|
+
if (!pipelineData) return;
|
|
353
|
+
|
|
354
|
+
document.getElementById('updated-time').textContent = formatTime(pipelineData.updated_at);
|
|
355
|
+
document.getElementById('footer-timestamp').textContent = 'Generated: ' + new Date().toLocaleString();
|
|
356
|
+
|
|
357
|
+
// Pipeline status
|
|
358
|
+
const agents = pipelineData.agents || [];
|
|
359
|
+
document.getElementById('agent-count').textContent = agents.length + ' running';
|
|
360
|
+
|
|
361
|
+
const events = pipelineData.events || [];
|
|
362
|
+
document.getElementById('event-count').textContent = events.length + ' total';
|
|
363
|
+
|
|
364
|
+
const artifactCount = pipelineData.artifact_count || 0;
|
|
365
|
+
document.getElementById('artifact-count').textContent = artifactCount + ' files';
|
|
366
|
+
|
|
367
|
+
// Render events
|
|
368
|
+
const eventsList = document.getElementById('events-list');
|
|
369
|
+
if (events.length === 0) {
|
|
370
|
+
eventsList.innerHTML = '<div style="color: #666; font-size: 12px;">No events recorded</div>';
|
|
371
|
+
} else {
|
|
372
|
+
eventsList.innerHTML = events
|
|
373
|
+
.slice(-10)
|
|
374
|
+
.reverse()
|
|
375
|
+
.map(e => {
|
|
376
|
+
const eventType = (e.type || 'unknown').toUpperCase();
|
|
377
|
+
const time = formatTime(e.ts);
|
|
378
|
+
return '<div class="list-item"><span>' + eventType + '</span><span class="timestamp">' + time + '</span></div>';
|
|
379
|
+
})
|
|
380
|
+
.join('');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Render on page load
|
|
385
|
+
document.addEventListener('DOMContentLoaded', renderDashboard);
|
|
386
|
+
|
|
387
|
+
// Auto-refresh every 30 seconds if this is served from a server
|
|
388
|
+
if (window.location.protocol.startsWith('http')) {
|
|
389
|
+
setInterval(function() {
|
|
390
|
+
fetch(window.location.href)
|
|
391
|
+
.then(r => r.text())
|
|
392
|
+
.then(html => {
|
|
393
|
+
const parser = new DOMParser();
|
|
394
|
+
const newDoc = parser.parseFromString(html, 'text/html');
|
|
395
|
+
const newScript = newDoc.querySelector('script');
|
|
396
|
+
if (newScript) {
|
|
397
|
+
eval(newScript.textContent);
|
|
398
|
+
renderDashboard();
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
.catch(e => console.log('Auto-refresh failed:', e));
|
|
402
|
+
}, 30000);
|
|
403
|
+
}
|
|
404
|
+
</script>
|
|
405
|
+
</body>
|
|
406
|
+
</html>
|
|
407
|
+
EOF
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
# βββ Export Command βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
411
|
+
cmd_export() {
|
|
412
|
+
local output_file="${1:-${PUB_DASH_DIR}/dashboard.html}"
|
|
413
|
+
local title="${2:-Shipwright Pipeline Progress}"
|
|
414
|
+
local privacy="${3:-stages_only}"
|
|
415
|
+
|
|
416
|
+
ensure_dirs
|
|
417
|
+
|
|
418
|
+
info "Gathering pipeline state (privacy: $privacy)..."
|
|
419
|
+
local state_data
|
|
420
|
+
state_data=$(gather_pipeline_state "$privacy")
|
|
421
|
+
|
|
422
|
+
info "Generating HTML export..."
|
|
423
|
+
local html
|
|
424
|
+
html=$(generate_html "$state_data" "$title" "$privacy")
|
|
425
|
+
|
|
426
|
+
# Replace placeholders
|
|
427
|
+
html="${html//TITLE_PLACEHOLDER/$title}"
|
|
428
|
+
html="${html//PRIVACY_PLACEHOLDER/$privacy}"
|
|
429
|
+
html="${html//\{DATA_PLACEHOLDER\}/$state_data}"
|
|
430
|
+
|
|
431
|
+
# Atomic write
|
|
432
|
+
local tmp_file
|
|
433
|
+
tmp_file=$(mktemp)
|
|
434
|
+
trap "rm -f '$tmp_file'" EXIT
|
|
435
|
+
echo "$html" > "$tmp_file"
|
|
436
|
+
mv "$tmp_file" "$output_file"
|
|
437
|
+
|
|
438
|
+
emit_event "public_dashboard_export" "privacy=$privacy" "path=$output_file"
|
|
439
|
+
|
|
440
|
+
success "Dashboard exported to: $output_file"
|
|
441
|
+
echo " Size: $(du -h "$output_file" | cut -f1)"
|
|
442
|
+
echo " Privacy: $privacy"
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# βββ Share Command ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
446
|
+
cmd_share() {
|
|
447
|
+
local expiry_hours="${1:-24}"
|
|
448
|
+
local privacy="${2:-stages_only}"
|
|
449
|
+
|
|
450
|
+
ensure_dirs
|
|
451
|
+
|
|
452
|
+
info "Creating share link (expires in ${expiry_hours}h)..."
|
|
453
|
+
local token
|
|
454
|
+
token=$(generate_token)
|
|
455
|
+
local expires_at
|
|
456
|
+
expires_at=$(($(now_epoch) + expiry_hours * 3600))
|
|
457
|
+
|
|
458
|
+
local link_entry
|
|
459
|
+
link_entry=$(jq -n \
|
|
460
|
+
--arg token "$token" \
|
|
461
|
+
--arg privacy "$privacy" \
|
|
462
|
+
--arg created "$(now_iso)" \
|
|
463
|
+
--arg expires "$(date -u -d "@$expires_at" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v +${expiry_hours}H +%Y-%m-%dT%H:%M:%SZ)" \
|
|
464
|
+
'{token:$token, privacy:$privacy, created:$created, expires:$expires, view_count:0, last_viewed:null}')
|
|
465
|
+
|
|
466
|
+
# Append to share links file (atomic)
|
|
467
|
+
local tmp_file
|
|
468
|
+
tmp_file=$(mktemp)
|
|
469
|
+
trap "rm -f '$tmp_file'" EXIT
|
|
470
|
+
jq ".links += [$link_entry]" "$SHARE_LINKS_FILE" > "$tmp_file"
|
|
471
|
+
mv "$tmp_file" "$SHARE_LINKS_FILE"
|
|
472
|
+
|
|
473
|
+
emit_event "public_dashboard_share" "token=$token" "privacy=$privacy" "expires_hours=$expiry_hours"
|
|
474
|
+
|
|
475
|
+
success "Share link created!"
|
|
476
|
+
echo " Token: $token"
|
|
477
|
+
echo " Privacy: $privacy"
|
|
478
|
+
echo " Expires: $(date -u -d "@$expires_at" +%Y-%m-%d\ %H:%M:%S 2>/dev/null || date -u -v +${expiry_hours}H +%Y-%m-%d\ %H:%M:%S)"
|
|
479
|
+
echo ""
|
|
480
|
+
echo " Share URL: https://your-domain.com/public-dashboard/$token"
|
|
481
|
+
echo " or embed: <iframe src=\"https://your-domain.com/public-dashboard/$token\"></iframe>"
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
# βββ Revoke Command βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
485
|
+
cmd_revoke() {
|
|
486
|
+
local token="$1"
|
|
487
|
+
|
|
488
|
+
[[ -z "$token" ]] && error "Token required" && return 1
|
|
489
|
+
|
|
490
|
+
ensure_dirs
|
|
491
|
+
|
|
492
|
+
info "Revoking share link: $token"
|
|
493
|
+
|
|
494
|
+
local tmp_file
|
|
495
|
+
tmp_file=$(mktemp)
|
|
496
|
+
trap "rm -f '$tmp_file'" EXIT
|
|
497
|
+
|
|
498
|
+
if jq ".links |= map(select(.token != \"$token\"))" "$SHARE_LINKS_FILE" > "$tmp_file"; then
|
|
499
|
+
mv "$tmp_file" "$SHARE_LINKS_FILE"
|
|
500
|
+
emit_event "public_dashboard_revoke" "token=$token"
|
|
501
|
+
success "Share link revoked"
|
|
502
|
+
else
|
|
503
|
+
error "Failed to revoke link"
|
|
504
|
+
return 1
|
|
505
|
+
fi
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
# βββ List Command βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
509
|
+
cmd_list() {
|
|
510
|
+
ensure_dirs
|
|
511
|
+
|
|
512
|
+
local now_epoch_val
|
|
513
|
+
now_epoch_val=$(now_epoch)
|
|
514
|
+
|
|
515
|
+
info "Active share links:"
|
|
516
|
+
echo ""
|
|
517
|
+
|
|
518
|
+
if ! jq empty "$SHARE_LINKS_FILE" 2>/dev/null; then
|
|
519
|
+
warn "No share links found"
|
|
520
|
+
return 0
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
local active_count=0
|
|
524
|
+
jq -r '.links[] |
|
|
525
|
+
if (.expires | fromdateiso8601) > '$now_epoch_val' then
|
|
526
|
+
.token + "|" + .privacy + "|" + .expires + "|" + (.view_count | tostring)
|
|
527
|
+
else
|
|
528
|
+
empty
|
|
529
|
+
end' "$SHARE_LINKS_FILE" | while IFS='|' read -r token privacy expires views; do
|
|
530
|
+
active_count=$((active_count + 1))
|
|
531
|
+
printf " %s... | Privacy: %-12s | Expires: %s | Views: %s\n" "${token:0:8}" "$privacy" "$expires" "$views"
|
|
532
|
+
done
|
|
533
|
+
|
|
534
|
+
local expired_count
|
|
535
|
+
expired_count=$(jq "[.links[] | select((.expires | fromdateiso8601) <= $now_epoch_val)] | length" "$SHARE_LINKS_FILE" 2>/dev/null || echo "0")
|
|
536
|
+
|
|
537
|
+
if [[ "$expired_count" -gt 0 ]]; then
|
|
538
|
+
echo ""
|
|
539
|
+
warn "$expired_count expired link(s) β run 'shipwright public-dashboard cleanup' to remove"
|
|
540
|
+
fi
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
# βββ Config Command βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
544
|
+
cmd_config() {
|
|
545
|
+
local key="${1:-}"
|
|
546
|
+
local value="${2:-}"
|
|
547
|
+
|
|
548
|
+
ensure_dirs
|
|
549
|
+
|
|
550
|
+
if [[ -z "$key" ]]; then
|
|
551
|
+
info "Current config:"
|
|
552
|
+
jq '.' "$SHARE_CONFIG_FILE"
|
|
553
|
+
return 0
|
|
554
|
+
fi
|
|
555
|
+
|
|
556
|
+
case "$key" in
|
|
557
|
+
privacy)
|
|
558
|
+
[[ -z "$value" ]] && error "Value required for privacy" && return 1
|
|
559
|
+
local tmp_file
|
|
560
|
+
tmp_file=$(mktemp)
|
|
561
|
+
trap "rm -f '$tmp_file'" EXIT
|
|
562
|
+
jq ".privacy = \"$value\"" "$SHARE_CONFIG_FILE" > "$tmp_file"
|
|
563
|
+
mv "$tmp_file" "$SHARE_CONFIG_FILE"
|
|
564
|
+
success "Privacy set to: $value"
|
|
565
|
+
;;
|
|
566
|
+
expiry)
|
|
567
|
+
[[ -z "$value" ]] && error "Value required for expiry (hours)" && return 1
|
|
568
|
+
local tmp_file
|
|
569
|
+
tmp_file=$(mktemp)
|
|
570
|
+
trap "rm -f '$tmp_file'" EXIT
|
|
571
|
+
jq ".expiry_hours = $value" "$SHARE_CONFIG_FILE" > "$tmp_file"
|
|
572
|
+
mv "$tmp_file" "$SHARE_CONFIG_FILE"
|
|
573
|
+
success "Default expiry set to: ${value}h"
|
|
574
|
+
;;
|
|
575
|
+
domain)
|
|
576
|
+
[[ -z "$value" ]] && error "Value required for domain" && return 1
|
|
577
|
+
local tmp_file
|
|
578
|
+
tmp_file=$(mktemp)
|
|
579
|
+
trap "rm -f '$tmp_file'" EXIT
|
|
580
|
+
jq ".custom_domain = \"$value\"" "$SHARE_CONFIG_FILE" > "$tmp_file"
|
|
581
|
+
mv "$tmp_file" "$SHARE_CONFIG_FILE"
|
|
582
|
+
success "Custom domain set to: $value"
|
|
583
|
+
;;
|
|
584
|
+
*)
|
|
585
|
+
error "Unknown config key: $key"
|
|
586
|
+
return 1
|
|
587
|
+
;;
|
|
588
|
+
esac
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
# βββ Embed Command ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
592
|
+
cmd_embed() {
|
|
593
|
+
local token="$1"
|
|
594
|
+
local format="${2:-iframe}"
|
|
595
|
+
|
|
596
|
+
[[ -z "$token" ]] && error "Token required" && return 1
|
|
597
|
+
|
|
598
|
+
ensure_dirs
|
|
599
|
+
|
|
600
|
+
local domain
|
|
601
|
+
domain=$(jq -r '.custom_domain // "your-domain.com"' "$SHARE_CONFIG_FILE")
|
|
602
|
+
local url="https://${domain}/public-dashboard/${token}"
|
|
603
|
+
|
|
604
|
+
case "$format" in
|
|
605
|
+
iframe)
|
|
606
|
+
cat <<EOF
|
|
607
|
+
<!-- Shipwright Public Dashboard Embed -->
|
|
608
|
+
<iframe
|
|
609
|
+
src="$url"
|
|
610
|
+
style="width: 100%; height: 600px; border: 1px solid #ddd; border-radius: 8px;"
|
|
611
|
+
title="Pipeline Progress"
|
|
612
|
+
sandbox="allow-same-origin"
|
|
613
|
+
></iframe>
|
|
614
|
+
EOF
|
|
615
|
+
;;
|
|
616
|
+
badge)
|
|
617
|
+
cat <<EOF
|
|
618
|
+
<!-- Shipwright Public Dashboard Badge -->
|
|
619
|
+
<a href="$url" style="display: inline-block;">
|
|
620
|
+
<img
|
|
621
|
+
alt="Pipeline Status"
|
|
622
|
+
src="$url/badge"
|
|
623
|
+
style="max-width: 200px;"
|
|
624
|
+
/>
|
|
625
|
+
</a>
|
|
626
|
+
EOF
|
|
627
|
+
;;
|
|
628
|
+
markdown)
|
|
629
|
+
cat <<EOF
|
|
630
|
+
<!-- Shipwright Public Dashboard -->
|
|
631
|
+
[]($url)
|
|
632
|
+
|
|
633
|
+
[View Full Dashboard]($url)
|
|
634
|
+
EOF
|
|
635
|
+
;;
|
|
636
|
+
link)
|
|
637
|
+
echo "$url"
|
|
638
|
+
;;
|
|
639
|
+
*)
|
|
640
|
+
error "Unknown format: $format (iframe, badge, markdown, link)"
|
|
641
|
+
return 1
|
|
642
|
+
;;
|
|
643
|
+
esac
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
# βββ Cleanup Command ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
647
|
+
cmd_cleanup() {
|
|
648
|
+
ensure_dirs
|
|
649
|
+
|
|
650
|
+
local now_epoch_val
|
|
651
|
+
now_epoch_val=$(now_epoch)
|
|
652
|
+
|
|
653
|
+
local before_count after_count
|
|
654
|
+
before_count=$(jq '.links | length' "$SHARE_LINKS_FILE" 2>/dev/null || echo "0")
|
|
655
|
+
|
|
656
|
+
local tmp_file
|
|
657
|
+
tmp_file=$(mktemp)
|
|
658
|
+
trap "rm -f '$tmp_file'" EXIT
|
|
659
|
+
|
|
660
|
+
jq ".links |= map(select((.expires | fromdateiso8601) > $now_epoch_val))" "$SHARE_LINKS_FILE" > "$tmp_file"
|
|
661
|
+
mv "$tmp_file" "$SHARE_LINKS_FILE"
|
|
662
|
+
|
|
663
|
+
after_count=$(jq '.links | length' "$SHARE_LINKS_FILE" 2>/dev/null || echo "0")
|
|
664
|
+
local removed=$((before_count - after_count))
|
|
665
|
+
|
|
666
|
+
if [[ "$removed" -gt 0 ]]; then
|
|
667
|
+
success "Cleaned up $removed expired link(s)"
|
|
668
|
+
else
|
|
669
|
+
info "No expired links to clean up"
|
|
670
|
+
fi
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
# βββ Help ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
674
|
+
show_help() {
|
|
675
|
+
cat <<EOF
|
|
676
|
+
${CYAN}${BOLD}shipwright public-dashboard${RESET} β Public real-time pipeline progress
|
|
677
|
+
|
|
678
|
+
${BOLD}USAGE${RESET}
|
|
679
|
+
${CYAN}shipwright public-dashboard${RESET} <command> [options]
|
|
680
|
+
${CYAN}shipwright share${RESET} [--expires 24h] [--privacy anonymized]
|
|
681
|
+
|
|
682
|
+
${BOLD}COMMANDS${RESET}
|
|
683
|
+
${CYAN}export${RESET} [file] [title] [privacy]
|
|
684
|
+
Generate self-contained HTML dashboard file
|
|
685
|
+
File: output path (default: ~/.shipwright/public-dashboard/dashboard.html)
|
|
686
|
+
Title: page title (default: "Shipwright Pipeline Progress")
|
|
687
|
+
Privacy: stages_only, anonymized, or public (default: stages_only)
|
|
688
|
+
|
|
689
|
+
${CYAN}share${RESET} [expiry_hours] [privacy_level]
|
|
690
|
+
Create a shareable link for real-time dashboard
|
|
691
|
+
Expiry: hours until link expires (default: 24)
|
|
692
|
+
Privacy: stages_only, anonymized, or public
|
|
693
|
+
|
|
694
|
+
${CYAN}revoke${RESET} <token>
|
|
695
|
+
Revoke a share link (invalidate the token)
|
|
696
|
+
|
|
697
|
+
${CYAN}list${RESET}
|
|
698
|
+
List all active share links
|
|
699
|
+
|
|
700
|
+
${CYAN}config${RESET} [key] [value]
|
|
701
|
+
View or modify dashboard configuration
|
|
702
|
+
Keys: privacy, expiry, domain
|
|
703
|
+
|
|
704
|
+
${CYAN}embed${RESET} <token> [format]
|
|
705
|
+
Generate embed code (iframe, badge, markdown, link)
|
|
706
|
+
|
|
707
|
+
${CYAN}cleanup${RESET}
|
|
708
|
+
Remove expired share links
|
|
709
|
+
|
|
710
|
+
${CYAN}help${RESET}
|
|
711
|
+
Show this help message
|
|
712
|
+
|
|
713
|
+
${BOLD}EXAMPLES${RESET}
|
|
714
|
+
# Export static HTML
|
|
715
|
+
${DIM}shipwright public-dashboard export${RESET}
|
|
716
|
+
${DIM}shipwright public-dashboard export dashboard.html "My Pipeline"${RESET}
|
|
717
|
+
|
|
718
|
+
# Create shareable link (requires dashboard server)
|
|
719
|
+
${DIM}shipwright public-dashboard share 48 anonymized${RESET}
|
|
720
|
+
|
|
721
|
+
# Generate embed code for README
|
|
722
|
+
${DIM}shipwright public-dashboard embed abc123def456 markdown${RESET}
|
|
723
|
+
|
|
724
|
+
# Configure default privacy level
|
|
725
|
+
${DIM}shipwright public-dashboard config privacy anonymized${RESET}
|
|
726
|
+
${DIM}shipwright public-dashboard config domain app.example.com${RESET}
|
|
727
|
+
|
|
728
|
+
${BOLD}PRIVACY LEVELS${RESET}
|
|
729
|
+
${CYAN}stages_only${RESET}
|
|
730
|
+
Only stage names and generic status info (most private)
|
|
731
|
+
|
|
732
|
+
${CYAN}anonymized${RESET}
|
|
733
|
+
Full details with paths and tokens redacted
|
|
734
|
+
|
|
735
|
+
${CYAN}public${RESET}
|
|
736
|
+
All details including paths and environment (least private)
|
|
737
|
+
|
|
738
|
+
${BOLD}OUTPUT${RESET}
|
|
739
|
+
Generated HTML files are completely self-contained:
|
|
740
|
+
- No external resources (all CSS/JS embedded)
|
|
741
|
+
- ~50 KB gzipped
|
|
742
|
+
- Works offline
|
|
743
|
+
- Safe to share
|
|
744
|
+
|
|
745
|
+
${BOLD}SHARE LINKS${RESET}
|
|
746
|
+
Share links require a running dashboard server to serve the public endpoint.
|
|
747
|
+
By default, requires dashboard to serve at: https://your-domain.com/public-dashboard/<token>
|
|
748
|
+
|
|
749
|
+
${DIM}Docs: https://sethdford.github.io/shipwright${RESET}
|
|
750
|
+
EOF
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
# βββ Main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
754
|
+
main() {
|
|
755
|
+
local cmd="${1:-help}"
|
|
756
|
+
|
|
757
|
+
case "$cmd" in
|
|
758
|
+
export)
|
|
759
|
+
shift
|
|
760
|
+
cmd_export "$@"
|
|
761
|
+
;;
|
|
762
|
+
share)
|
|
763
|
+
shift
|
|
764
|
+
cmd_share "$@"
|
|
765
|
+
;;
|
|
766
|
+
revoke)
|
|
767
|
+
shift
|
|
768
|
+
cmd_revoke "$@"
|
|
769
|
+
;;
|
|
770
|
+
list)
|
|
771
|
+
cmd_list
|
|
772
|
+
;;
|
|
773
|
+
config)
|
|
774
|
+
shift
|
|
775
|
+
cmd_config "$@"
|
|
776
|
+
;;
|
|
777
|
+
embed)
|
|
778
|
+
shift
|
|
779
|
+
cmd_embed "$@"
|
|
780
|
+
;;
|
|
781
|
+
cleanup)
|
|
782
|
+
cmd_cleanup
|
|
783
|
+
;;
|
|
784
|
+
help|--help|-h)
|
|
785
|
+
show_help
|
|
786
|
+
;;
|
|
787
|
+
*)
|
|
788
|
+
error "Unknown command: $cmd"
|
|
789
|
+
echo ""
|
|
790
|
+
show_help
|
|
791
|
+
exit 1
|
|
792
|
+
;;
|
|
793
|
+
esac
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
797
|
+
main "$@"
|
|
798
|
+
fi
|