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,450 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ sw-stream.sh — Live terminal output streaming from agent panes ║
|
|
4
|
+
# ║ ║
|
|
5
|
+
# ║ Streams tmux pane output in real-time to the dashboard or CLI. ║
|
|
6
|
+
# ║ Captures output periodically, tags by agent/team, supports replay. ║
|
|
7
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
8
|
+
VERSION="2.0.0"
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
11
|
+
|
|
12
|
+
# ─── Colors ──────────────────────────────────────────────────────────────────
|
|
13
|
+
CYAN='\033[38;2;0;212;255m'
|
|
14
|
+
PURPLE='\033[38;2;124;58;237m'
|
|
15
|
+
BLUE='\033[38;2;0;102;255m'
|
|
16
|
+
GREEN='\033[38;2;74;222;128m'
|
|
17
|
+
YELLOW='\033[38;2;250;204;21m'
|
|
18
|
+
RED='\033[38;2;248;113;113m'
|
|
19
|
+
DIM='\033[2m'
|
|
20
|
+
BOLD='\033[1m'
|
|
21
|
+
RESET='\033[0m'
|
|
22
|
+
|
|
23
|
+
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
24
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
25
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
26
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
27
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
28
|
+
|
|
29
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
30
|
+
_COMPAT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/compat.sh"
|
|
31
|
+
# shellcheck source=lib/compat.sh
|
|
32
|
+
[[ -f "$_COMPAT" ]] && source "$_COMPAT"
|
|
33
|
+
|
|
34
|
+
# ─── Event logging ─────────────────────────────────────────────────────────
|
|
35
|
+
emit_event() {
|
|
36
|
+
local event_type="$1"
|
|
37
|
+
shift
|
|
38
|
+
local json_fields=""
|
|
39
|
+
for kv in "$@"; do
|
|
40
|
+
local key="${kv%%=*}"
|
|
41
|
+
local value="${kv#*=}"
|
|
42
|
+
value="${value//\"/\\\"}" # Escape quotes
|
|
43
|
+
json_fields="${json_fields}\"${key}\": \"${value}\", "
|
|
44
|
+
done
|
|
45
|
+
json_fields="${json_fields%, }" # Remove trailing comma
|
|
46
|
+
local ts
|
|
47
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
48
|
+
echo "{\"timestamp\": \"${ts}\", \"type\": \"${event_type}\", ${json_fields}}" >> \
|
|
49
|
+
"${HOME}/.shipwright/events.jsonl" 2>/dev/null || true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ─── Stream configuration ─────────────────────────────────────────────────────
|
|
53
|
+
STREAM_CONFIG="${HOME}/.shipwright/stream-config.json"
|
|
54
|
+
STREAM_DIR="${HOME}/.shipwright/streams"
|
|
55
|
+
OUTPUT_FORMAT="jsonl" # jsonl, json, or text
|
|
56
|
+
CAPTURE_INTERVAL=1
|
|
57
|
+
BUFFER_LINES=500
|
|
58
|
+
RUNNING_PID_FILE="${HOME}/.shipwright/stream.pid"
|
|
59
|
+
|
|
60
|
+
# Load config if exists
|
|
61
|
+
load_config() {
|
|
62
|
+
if [[ -f "$STREAM_CONFIG" ]]; then
|
|
63
|
+
CAPTURE_INTERVAL=$(jq -r '.capture_interval_seconds // 1' "$STREAM_CONFIG" 2>/dev/null || echo 1)
|
|
64
|
+
BUFFER_LINES=$(jq -r '.buffer_lines // 500' "$STREAM_CONFIG" 2>/dev/null || echo 500)
|
|
65
|
+
OUTPUT_FORMAT=$(jq -r '.output_format // "jsonl"' "$STREAM_CONFIG" 2>/dev/null || echo "jsonl")
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# ─── Stream management ─────────────────────────────────────────────────────
|
|
70
|
+
init_stream_dir() {
|
|
71
|
+
mkdir -p "$STREAM_DIR"
|
|
72
|
+
mkdir -p "${HOME}/.shipwright"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get_pane_agent_name() {
|
|
76
|
+
local pane_id="$1"
|
|
77
|
+
tmux display-message -p -t "$pane_id" '#{pane_title}' 2>/dev/null || echo "unknown"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get_pane_window_name() {
|
|
81
|
+
local pane_id="$1"
|
|
82
|
+
tmux display-message -p -t "$pane_id" '#{window_name}' 2>/dev/null || echo "unknown"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Extract team name from window name (e.g., "claude-myteam" → "myteam")
|
|
86
|
+
extract_team_name() {
|
|
87
|
+
local window_name="$1"
|
|
88
|
+
echo "$window_name" | sed 's/^claude-//; s/-[0-9]*$//'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# ─── Capture a single pane's output ──────────────────────────────────────────
|
|
92
|
+
capture_pane_output() {
|
|
93
|
+
local pane_id="$1"
|
|
94
|
+
local agent_name="$2"
|
|
95
|
+
local team_name="$3"
|
|
96
|
+
|
|
97
|
+
local pane_file="${STREAM_DIR}/${team_name}/${agent_name}.jsonl"
|
|
98
|
+
mkdir -p "${STREAM_DIR}/${team_name}"
|
|
99
|
+
|
|
100
|
+
local ts
|
|
101
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
102
|
+
|
|
103
|
+
# Capture last N lines from pane
|
|
104
|
+
local pane_output
|
|
105
|
+
pane_output=$(tmux capture-pane -p -t "$pane_id" -S "-${BUFFER_LINES}" 2>/dev/null || echo "")
|
|
106
|
+
|
|
107
|
+
if [[ -z "$pane_output" ]]; then
|
|
108
|
+
return 0
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# Write JSONL entry with timestamp, pane_id, agent, team, content
|
|
112
|
+
local tmp_file
|
|
113
|
+
tmp_file=$(mktemp)
|
|
114
|
+
trap "rm -f '$tmp_file'" RETURN
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
while IFS= read -r line; do
|
|
118
|
+
# Escape newlines and quotes in output
|
|
119
|
+
line="${line//\"/\\\"}"
|
|
120
|
+
printf '{"timestamp":"%s","pane_id":"%s","agent_name":"%s","team":"%s","content":"%s"}\n' \
|
|
121
|
+
"$ts" "$pane_id" "$agent_name" "$team_name" "$line"
|
|
122
|
+
done <<< "$pane_output"
|
|
123
|
+
} >> "$tmp_file"
|
|
124
|
+
|
|
125
|
+
# Atomic write: append to pane file and trim to buffer size
|
|
126
|
+
cat "$tmp_file" >> "$pane_file" 2>/dev/null || true
|
|
127
|
+
|
|
128
|
+
# Trim to buffer size (keep latest N lines)
|
|
129
|
+
local line_count
|
|
130
|
+
line_count=$(wc -l < "$pane_file" 2>/dev/null || echo 0)
|
|
131
|
+
if [[ "$line_count" -gt "$BUFFER_LINES" ]]; then
|
|
132
|
+
local skip=$((line_count - BUFFER_LINES))
|
|
133
|
+
tail -n "$BUFFER_LINES" "$pane_file" > "${pane_file}.tmp"
|
|
134
|
+
mv "${pane_file}.tmp" "$pane_file"
|
|
135
|
+
fi
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# ─── Capture all agent panes ──────────────────────────────────────────────────
|
|
139
|
+
capture_all_panes() {
|
|
140
|
+
local filter_team="${1:-}"
|
|
141
|
+
local filter_agent="${2:-}"
|
|
142
|
+
|
|
143
|
+
# Find all claude-* windows and capture their panes
|
|
144
|
+
tmux list-panes -a -F '#{pane_id}|#{window_name}' 2>/dev/null | while IFS='|' read -r pane_id window_name; do
|
|
145
|
+
[[ -z "$pane_id" ]] && continue
|
|
146
|
+
|
|
147
|
+
# Only claude-* windows
|
|
148
|
+
echo "$window_name" | grep -q "^claude" || continue
|
|
149
|
+
|
|
150
|
+
local agent_name
|
|
151
|
+
agent_name=$(get_pane_agent_name "$pane_id")
|
|
152
|
+
|
|
153
|
+
local team_name
|
|
154
|
+
team_name=$(extract_team_name "$window_name")
|
|
155
|
+
|
|
156
|
+
# Apply filters
|
|
157
|
+
if [[ -n "$filter_team" ]]; then
|
|
158
|
+
[[ "$team_name" != "$filter_team" ]] && continue
|
|
159
|
+
fi
|
|
160
|
+
if [[ -n "$filter_agent" ]]; then
|
|
161
|
+
[[ "$agent_name" != "$filter_agent" ]] && continue
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
capture_pane_output "$pane_id" "$agent_name" "$team_name"
|
|
165
|
+
done
|
|
166
|
+
|
|
167
|
+
emit_event "stream.capture_cycle" \
|
|
168
|
+
"team=$filter_team" \
|
|
169
|
+
"agent=$filter_agent" \
|
|
170
|
+
"interval=$CAPTURE_INTERVAL"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# ─── Stream start (background polling) ───────────────────────────────────────
|
|
174
|
+
stream_start() {
|
|
175
|
+
local team="${1:-}"
|
|
176
|
+
|
|
177
|
+
init_stream_dir
|
|
178
|
+
load_config
|
|
179
|
+
|
|
180
|
+
if [[ -f "$RUNNING_PID_FILE" ]]; then
|
|
181
|
+
local pid
|
|
182
|
+
pid=$(cat "$RUNNING_PID_FILE")
|
|
183
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
184
|
+
warn "Stream is already running (PID $pid)"
|
|
185
|
+
return 0
|
|
186
|
+
else
|
|
187
|
+
rm -f "$RUNNING_PID_FILE"
|
|
188
|
+
fi
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
# Start background capture loop
|
|
192
|
+
(
|
|
193
|
+
while true; do
|
|
194
|
+
capture_all_panes "$team"
|
|
195
|
+
sleep "$CAPTURE_INTERVAL"
|
|
196
|
+
done
|
|
197
|
+
) &
|
|
198
|
+
local loop_pid=$!
|
|
199
|
+
echo "$loop_pid" > "$RUNNING_PID_FILE"
|
|
200
|
+
|
|
201
|
+
success "Stream started (PID $loop_pid)"
|
|
202
|
+
info "Capturing every ${CAPTURE_INTERVAL}s from team: ${team:-all}"
|
|
203
|
+
emit_event "stream.started" "team=$team" "pid=$loop_pid"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# ─── Stream stop ─────────────────────────────────────────────────────────────
|
|
207
|
+
stream_stop() {
|
|
208
|
+
if [[ ! -f "$RUNNING_PID_FILE" ]]; then
|
|
209
|
+
warn "Stream is not running"
|
|
210
|
+
return 1
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
local pid
|
|
214
|
+
pid=$(cat "$RUNNING_PID_FILE")
|
|
215
|
+
|
|
216
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
217
|
+
kill "$pid"
|
|
218
|
+
rm -f "$RUNNING_PID_FILE"
|
|
219
|
+
success "Stream stopped (PID $pid)"
|
|
220
|
+
emit_event "stream.stopped" "pid=$pid"
|
|
221
|
+
else
|
|
222
|
+
warn "Stream process (PID $pid) is not running"
|
|
223
|
+
rm -f "$RUNNING_PID_FILE"
|
|
224
|
+
fi
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# ─── Live watch (tail stream in terminal) ────────────────────────────────────
|
|
228
|
+
stream_watch() {
|
|
229
|
+
local team="${1:-}"
|
|
230
|
+
local agent="${2:-}"
|
|
231
|
+
|
|
232
|
+
init_stream_dir
|
|
233
|
+
load_config
|
|
234
|
+
|
|
235
|
+
if [[ -z "$team" ]]; then
|
|
236
|
+
warn "Usage: shipwright stream watch <team> [agent]"
|
|
237
|
+
return 1
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
local watch_path="${STREAM_DIR}/${team}"
|
|
241
|
+
if [[ -n "$agent" ]]; then
|
|
242
|
+
watch_path="${watch_path}/${agent}.jsonl"
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
if [[ ! -e "$watch_path" ]]; then
|
|
246
|
+
error "No stream data for team '$team'${agent:+ agent '$agent'}"
|
|
247
|
+
return 1
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
info "Watching $watch_path..."
|
|
251
|
+
echo ""
|
|
252
|
+
|
|
253
|
+
tail -f "$watch_path" | while IFS= read -r line; do
|
|
254
|
+
# Parse JSONL and pretty-print
|
|
255
|
+
local timestamp agent_name content
|
|
256
|
+
timestamp=$(echo "$line" | jq -r '.timestamp // ""' 2>/dev/null || echo "")
|
|
257
|
+
agent_name=$(echo "$line" | jq -r '.agent_name // ""' 2>/dev/null || echo "")
|
|
258
|
+
content=$(echo "$line" | jq -r '.content // ""' 2>/dev/null || echo "")
|
|
259
|
+
|
|
260
|
+
if [[ -n "$timestamp" && -n "$agent_name" && -n "$content" ]]; then
|
|
261
|
+
printf "${DIM}%s${RESET} ${CYAN}[%s]${RESET} %s\n" "$timestamp" "$agent_name" "$content"
|
|
262
|
+
fi
|
|
263
|
+
done
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# ─── List active streams ─────────────────────────────────────────────────────
|
|
267
|
+
stream_list() {
|
|
268
|
+
init_stream_dir
|
|
269
|
+
|
|
270
|
+
if [[ ! -d "$STREAM_DIR" ]] || [[ -z "$(find "$STREAM_DIR" -type f -name '*.jsonl' 2>/dev/null)" ]]; then
|
|
271
|
+
warn "No active streams"
|
|
272
|
+
return 0
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
echo ""
|
|
276
|
+
info "Active Streams:"
|
|
277
|
+
echo ""
|
|
278
|
+
|
|
279
|
+
find "$STREAM_DIR" -type f -name '*.jsonl' | sort | while read -r stream_file; do
|
|
280
|
+
# Extract team and agent from path
|
|
281
|
+
local relative_path
|
|
282
|
+
relative_path="${stream_file#$STREAM_DIR/}"
|
|
283
|
+
local team_name
|
|
284
|
+
team_name=$(echo "$relative_path" | cut -d'/' -f1)
|
|
285
|
+
local agent_name
|
|
286
|
+
agent_name=$(basename "$relative_path" .jsonl)
|
|
287
|
+
|
|
288
|
+
# Get file size and line count
|
|
289
|
+
local file_size lines_count
|
|
290
|
+
file_size=$(stat -f%z "$stream_file" 2>/dev/null || stat -c%s "$stream_file" 2>/dev/null || echo 0)
|
|
291
|
+
lines_count=$(wc -l < "$stream_file" 2>/dev/null || echo 0)
|
|
292
|
+
|
|
293
|
+
# Get latest timestamp
|
|
294
|
+
local latest_ts
|
|
295
|
+
latest_ts=$(tail -1 "$stream_file" 2>/dev/null | jq -r '.timestamp // ""' 2>/dev/null || echo "")
|
|
296
|
+
|
|
297
|
+
printf " ${CYAN}%-20s${RESET} ${PURPLE}%-20s${RESET} %s ${DIM}(%s lines, %s bytes)${RESET}\n" \
|
|
298
|
+
"$team_name" "$agent_name" "$latest_ts" "$lines_count" "$file_size"
|
|
299
|
+
done
|
|
300
|
+
|
|
301
|
+
echo ""
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# ─── Replay recent output for a pane ──────────────────────────────────────────
|
|
305
|
+
stream_replay() {
|
|
306
|
+
local team="${1:-}"
|
|
307
|
+
local agent="${2:-}"
|
|
308
|
+
local lines="${3:-50}"
|
|
309
|
+
|
|
310
|
+
init_stream_dir
|
|
311
|
+
|
|
312
|
+
if [[ -z "$team" ]] || [[ -z "$agent" ]]; then
|
|
313
|
+
warn "Usage: shipwright stream replay <team> <agent> [lines]"
|
|
314
|
+
return 1
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
local stream_file="${STREAM_DIR}/${team}/${agent}.jsonl"
|
|
318
|
+
|
|
319
|
+
if [[ ! -f "$stream_file" ]]; then
|
|
320
|
+
error "No stream data for team '$team' agent '$agent'"
|
|
321
|
+
return 1
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
info "Replay (last ${lines} lines from ${team}/${agent}):"
|
|
325
|
+
echo ""
|
|
326
|
+
|
|
327
|
+
tail -n "$lines" "$stream_file" | while IFS= read -r line; do
|
|
328
|
+
local timestamp agent_name content
|
|
329
|
+
timestamp=$(echo "$line" | jq -r '.timestamp // ""' 2>/dev/null || echo "")
|
|
330
|
+
agent_name=$(echo "$line" | jq -r '.agent_name // ""' 2>/dev/null || echo "")
|
|
331
|
+
content=$(echo "$line" | jq -r '.content // ""' 2>/dev/null || echo "")
|
|
332
|
+
|
|
333
|
+
if [[ -n "$timestamp" && -n "$agent_name" && -n "$content" ]]; then
|
|
334
|
+
printf "${DIM}%s${RESET} ${CYAN}[%s]${RESET} %s\n" "$timestamp" "$agent_name" "$content"
|
|
335
|
+
fi
|
|
336
|
+
done
|
|
337
|
+
|
|
338
|
+
echo ""
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# ─── Configure stream settings ───────────────────────────────────────────────
|
|
342
|
+
stream_config() {
|
|
343
|
+
local key="${1:-}"
|
|
344
|
+
local value="${2:-}"
|
|
345
|
+
|
|
346
|
+
if [[ -z "$key" ]]; then
|
|
347
|
+
warn "Usage: shipwright stream config <key> <value>"
|
|
348
|
+
echo " Available keys: capture_interval_seconds, buffer_lines, output_format"
|
|
349
|
+
return 1
|
|
350
|
+
fi
|
|
351
|
+
|
|
352
|
+
mkdir -p "${HOME}/.shipwright"
|
|
353
|
+
|
|
354
|
+
# Load existing config or create new
|
|
355
|
+
local config="{}"
|
|
356
|
+
if [[ -f "$STREAM_CONFIG" ]]; then
|
|
357
|
+
config=$(cat "$STREAM_CONFIG")
|
|
358
|
+
fi
|
|
359
|
+
|
|
360
|
+
# Create tmp file for atomic write
|
|
361
|
+
local tmp_file
|
|
362
|
+
tmp_file=$(mktemp)
|
|
363
|
+
trap "rm -f '$tmp_file'" RETURN
|
|
364
|
+
|
|
365
|
+
case "$key" in
|
|
366
|
+
capture_interval_seconds)
|
|
367
|
+
echo "$config" | jq ".capture_interval_seconds = ($value | tonumber)" > "$tmp_file"
|
|
368
|
+
;;
|
|
369
|
+
buffer_lines)
|
|
370
|
+
echo "$config" | jq ".buffer_lines = ($value | tonumber)" > "$tmp_file"
|
|
371
|
+
;;
|
|
372
|
+
output_format)
|
|
373
|
+
echo "$config" | jq ".output_format = \"$value\"" > "$tmp_file"
|
|
374
|
+
;;
|
|
375
|
+
*)
|
|
376
|
+
error "Unknown config key: $key"
|
|
377
|
+
return 1
|
|
378
|
+
;;
|
|
379
|
+
esac
|
|
380
|
+
|
|
381
|
+
mv "$tmp_file" "$STREAM_CONFIG"
|
|
382
|
+
success "Config updated: $key = $value"
|
|
383
|
+
emit_event "stream.config_updated" "key=$key" "value=$value"
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
# ─── Help ───────────────────────────────────────────────────────────────────
|
|
387
|
+
show_help() {
|
|
388
|
+
echo ""
|
|
389
|
+
echo -e "${CYAN}${BOLD}shipwright stream${RESET} — Live terminal output streaming"
|
|
390
|
+
echo ""
|
|
391
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
392
|
+
echo -e " ${CYAN}shipwright stream${RESET} <command> [options]"
|
|
393
|
+
echo ""
|
|
394
|
+
echo -e "${BOLD}COMMANDS${RESET}"
|
|
395
|
+
echo -e " ${CYAN}start${RESET} [team] Start streaming agent panes (all or by team)"
|
|
396
|
+
echo -e " ${CYAN}stop${RESET} Stop streaming"
|
|
397
|
+
echo -e " ${CYAN}watch${RESET} <team> [agent] Live tail of stream output in terminal"
|
|
398
|
+
echo -e " ${CYAN}list${RESET} Show active streams"
|
|
399
|
+
echo -e " ${CYAN}replay${RESET} <team> <agent> [N] Show recent N lines from stream (default 50)"
|
|
400
|
+
echo -e " ${CYAN}config${RESET} <key> <value> Set stream configuration"
|
|
401
|
+
echo -e " ${CYAN}help${RESET} Show this help message"
|
|
402
|
+
echo ""
|
|
403
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
404
|
+
echo -e " ${DIM}shipwright stream start${RESET} # Start streaming all teams"
|
|
405
|
+
echo -e " ${DIM}shipwright stream start myteam${RESET} # Stream only 'myteam'"
|
|
406
|
+
echo -e " ${DIM}shipwright stream watch myteam builder${RESET} # Watch builder agent in myteam"
|
|
407
|
+
echo -e " ${DIM}shipwright stream replay myteam builder 100${RESET} # Show last 100 lines"
|
|
408
|
+
echo -e " ${DIM}shipwright stream config capture_interval_seconds 2${RESET} # Capture every 2s"
|
|
409
|
+
echo -e " ${DIM}shipwright stream list${RESET} # Show all active streams"
|
|
410
|
+
echo ""
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
# ─── Main ───────────────────────────────────────────────────────────────────
|
|
414
|
+
main() {
|
|
415
|
+
local cmd="${1:-help}"
|
|
416
|
+
|
|
417
|
+
case "$cmd" in
|
|
418
|
+
start)
|
|
419
|
+
stream_start "${2:-}"
|
|
420
|
+
;;
|
|
421
|
+
stop)
|
|
422
|
+
stream_stop
|
|
423
|
+
;;
|
|
424
|
+
watch)
|
|
425
|
+
stream_watch "${2:-}" "${3:-}"
|
|
426
|
+
;;
|
|
427
|
+
list)
|
|
428
|
+
stream_list
|
|
429
|
+
;;
|
|
430
|
+
replay)
|
|
431
|
+
stream_replay "${2:-}" "${3:-}" "${4:-50}"
|
|
432
|
+
;;
|
|
433
|
+
config)
|
|
434
|
+
stream_config "${2:-}" "${3:-}"
|
|
435
|
+
;;
|
|
436
|
+
help|--help|-h)
|
|
437
|
+
show_help
|
|
438
|
+
;;
|
|
439
|
+
*)
|
|
440
|
+
error "Unknown command: $cmd"
|
|
441
|
+
echo ""
|
|
442
|
+
show_help
|
|
443
|
+
exit 1
|
|
444
|
+
;;
|
|
445
|
+
esac
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
449
|
+
main "$@"
|
|
450
|
+
fi
|