shipwright-cli 1.9.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/.claude/hooks/post-tool-use.sh +12 -5
- 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 +217 -2
- 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 +79 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +192 -7
- 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 +812 -138
- 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 +366 -31
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +507 -51
- package/scripts/sw-memory.sh +198 -3
- 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 +8 -8
- package/scripts/sw-pipeline-vitals.sh +1096 -0
- package/scripts/sw-pipeline.sh +2451 -180
- 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 +4 -3
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +5 -3
- 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 +109 -8
- package/scripts/sw-session.sh +31 -9
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +192 -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
- package/templates/pipelines/autonomous.json +8 -1
- package/templates/pipelines/cost-aware.json +21 -0
- package/templates/pipelines/deployed.json +40 -6
- package/templates/pipelines/enterprise.json +16 -2
- package/templates/pipelines/fast.json +19 -0
- package/templates/pipelines/full.json +16 -2
- package/templates/pipelines/hotfix.json +19 -0
- package/templates/pipelines/standard.json +19 -0
|
@@ -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
|